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()