Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |