Spaces:
Sleeping
Sleeping
File size: 4,419 Bytes
f741165 45898ed f741165 45898ed f741165 45898ed f741165 45898ed f741165 45aa0de f741165 347d0b0 45aa0de 347d0b0 45aa0de 347d0b0 f741165 45898ed f741165 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | import base64
import io
import json
import os
import spaces
import gradio as gr
import torch
from PIL import Image
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
API_KEY = os.environ.get("KHANA_API_KEY", "")
MODEL_ID = "Qwen/Qwen3-VL-4B-Instruct"
# Load to CPU at startup; moved to CUDA inside @spaces.GPU per-call
model = Qwen3VLForConditionalGeneration.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16,
)
processor = AutoProcessor.from_pretrained(MODEL_ID)
FOOD_PROMPT = """You are a nutrition expert. Analyze this food image and return ONLY raw JSON, no markdown fences.
For each food item visible return:
{{
"is_real_food": true,
"is_harmful": false,
"harmful_reason": "",
"items": [
{{
"name": "item name",
"quantity": "e.g. 1 cup / 200g / 1 piece",
"calories": 250,
"protein_g": 10.0,
"carbs_g": 30.0,
"fat_g": 8.0,
"saturated_fat_g": 3.0,
"sugars_g": 5.0,
"fiber_g": 2.0,
"sodium_mg": 400
}}
],
"total_calories": 250,
"notes": "one short line: label read / estimated from similar product / assumptions"
}}
RULES:
- For packaged food, read the Nutrition Facts label if visible; otherwise use known product values.
- If the product is not recognisable, speculate from the closest known product and say so in notes.
- Estimate for the full visible serving unless the user specifies otherwise.
- Judge alcohol from packaging, label, or context -- NOT colour. Black coffee, cranberry juice, pomegranate juice, kokum, rooh afza are NOT alcohol.
- If the image shows alcohol, tobacco, a vape, or drugs: return {{"is_real_food": true, "is_harmful": true, "harmful_reason": "what it is", "items": [], "total_calories": 0, "notes": "harmful"}}
- If the image is NOT food at all (screenshot, ad, cartoon): return {{"is_real_food": false, "items": [], "total_calories": 0, "notes": "reason"}}
"""
def _extract_json(text: str) -> str:
text = text.strip()
if "```" in text:
for part in text.split("```"):
part = part.strip()
if part.startswith("json"):
part = part[4:].strip()
if part.startswith("{"):
return part
return text
@spaces.GPU(duration=120)
def analyze(image_b64: str, extra_notes: str, api_key: str) -> str:
if API_KEY and api_key.strip() != API_KEY:
return json.dumps({"error": "unauthorized"})
try:
img_bytes = base64.b64decode(image_b64)
image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
except Exception as e:
return json.dumps({"error": f"image decode failed: {e}"})
RAW_PREFIX = "RAW_PROMPT:"
raw_mode = extra_notes.startswith(RAW_PREFIX)
if raw_mode:
# benchmark mode: use extra_notes verbatim as the whole prompt, no JSON wrapping
prompt = extra_notes[len(RAW_PREFIX):]
else:
prompt = FOOD_PROMPT
if extra_notes:
prompt += f"\nUser notes: {extra_notes}"
messages = [{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": prompt},
],
}]
try:
inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
).to("cuda")
model.to("cuda")
with torch.no_grad():
output_ids = model.generate(**inputs, max_new_tokens=(30 if raw_mode else 900), do_sample=False)
generated = output_ids[0][inputs.input_ids.shape[1]:]
raw = processor.decode(generated, skip_special_tokens=True)
return raw if raw_mode else _extract_json(raw)
except Exception as e:
import traceback
return json.dumps({"error": f"{type(e).__name__}: {e}", "trace": traceback.format_exc()[-1500:]})
demo = gr.Interface(
fn=analyze,
inputs=[
gr.Textbox(label="image_b64", lines=1, placeholder="base64-encoded image"),
gr.Textbox(label="extra_notes", lines=1, placeholder="e.g. 2 rotis, no oil"),
gr.Textbox(label="api_key", lines=1, placeholder="KHANA_API_KEY value"),
],
outputs=gr.Textbox(label="result_json"),
title="KhanaVision",
description="Qwen3-VL-4B-Instruct food analysis endpoint for Khana Tracker",
allow_flagging="never",
)
demo.launch()
|