kneedle-gemma / app.py
Prithvigg's picture
Update app.py
c06d20b verified
Raw
History Blame Contribute Delete
4.43 kB
"""
Kneedle β€” Gemma 4 E4B inference Space.
Accepts system_prompt + user_prompt + images (base64 JSON list) and returns
the model's raw JSON string. The backend (gemma_client.py) does all parsing.
For persistent GPU hardware (T4, L4, etc.) β€” no @spaces.GPU needed.
Set INFERENCE_BACKEND = "hf_space" and HF_SPACE_URL in gemma_client.py.
"""
import json
import base64
import io
import os
import sys
import time
import traceback
import torch
import gradio as gr
from PIL import Image
from transformers import AutoProcessor, AutoModelForImageTextToText
# Force unbuffered stdout/stderr so prints appear immediately in HF Space logs.
sys.stdout.reconfigure(line_buffering=True)
sys.stderr.reconfigure(line_buffering=True)
MODEL_ID = os.getenv("MODEL_ID", "google/gemma-4-E4B-it")
print(f"[boot] Loading processor for {MODEL_ID}", flush=True)
processor = AutoProcessor.from_pretrained(MODEL_ID, padding_side="left")
print(f"[boot] Loading model {MODEL_ID}", flush=True)
model = AutoModelForImageTextToText.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16,
device_map="auto",
attn_implementation="sdpa",
)
model.eval()
print(f"[boot] Model ready on device: {model.device}", flush=True)
def generate(system_prompt: str, user_prompt: str, images_json: str) -> str:
t0 = time.time()
print(f"[generate] called β€” sys_len={len(system_prompt)} usr_len={len(user_prompt)} images_json_len={len(images_json)}", flush=True)
try:
raw_images: list[str] = json.loads(images_json)
print(f"[generate] decoded {len(raw_images)} base64 images", flush=True)
pil_images: list[Image.Image] = []
for b64 in raw_images:
if "," in b64:
b64 = b64.split(",", 1)[1]
img_bytes = base64.b64decode(b64)
pil_images.append(Image.open(io.BytesIO(img_bytes)).convert("RGB"))
print(f"[generate] loaded {len(pil_images)} PIL images, sizes={[img.size for img in pil_images]}", flush=True)
# Gemma 4 supports a native system role β€” keep system and user separate.
# System prompt may begin with <|think|> to activate thinking mode.
# Per Gemma 4 docs, image content should come BEFORE text in the user turn.
image_content = [{"type": "image", "image": img} for img in pil_images]
messages = [
{
"role": "system",
"content": [{"type": "text", "text": system_prompt}],
},
{
"role": "user",
"content": image_content + [{"type": "text", "text": user_prompt}],
},
]
print(f"[generate] running apply_chat_template...", flush=True)
inputs = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
).to(model.device)
input_len = inputs["input_ids"].shape[-1]
print(f"[generate] inputs ready β€” input_len={input_len} keys={list(inputs.keys())}", flush=True)
print(f"[generate] starting model.generate (greedy, max_new_tokens=1500)...", flush=True)
with torch.inference_mode():
outputs = model.generate(
**inputs,
max_new_tokens=1500,
do_sample=False,
use_cache=True,
)
print(f"[generate] generation done β€” total tokens={outputs.shape[-1]} elapsed={time.time()-t0:.1f}s", flush=True)
generated_ids = outputs[0][input_len:]
result = processor.decode(generated_ids, skip_special_tokens=True)
print(f"[generate] returning {len(result)} chars", flush=True)
return result
except Exception as e:
tb = traceback.format_exc()
print(f"[generate] EXCEPTION: {e}\n{tb}", flush=True)
raise
demo = gr.Interface(
fn=generate,
inputs=[
gr.Textbox(label="System Prompt", lines=10),
gr.Textbox(label="User Prompt", lines=10),
gr.Textbox(label="Images JSON (base64 list)"),
],
outputs=gr.Textbox(label="Model Response"),
title="Kneedle β€” Gemma 4 E4B",
description="Gait analysis inference endpoint. Called programmatically by the Kneedle backend.",
api_name="generate",
).queue(max_size=4, default_concurrency_limit=1)
if __name__ == "__main__":
demo.launch(show_error=True, max_threads=2)