SPP Claude Sonnet 4.6 commited on
Commit
98ceb88
·
1 Parent(s): 7836b23

Initial upload: Vision Base MiniCPM-V 4.6 demo suite

Browse files

Four apps (Allergen Lens, Fridge Dinner, Object Oracle, What's That Error)
switchable via APP_ID env var, running on ZeroGPU.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ .env
app.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Deploy each app by setting APP_ID before launching:
3
+ #
4
+ # APP_ID=allergen_lens python app.py # food label scanner
5
+ # APP_ID=fridge_dinner python app.py # fridge-to-dinner planner
6
+ # APP_ID=object_oracle python app.py # tarot-style reading
7
+ # APP_ID=whats_that_error python app.py # error code fixer
8
+ #
9
+ # On Hugging Face Spaces: set APP_ID in Space → Settings → Variables & Secrets.
10
+ # ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ import spaces # MUST be the first import for ZeroGPU
13
+
14
+ import os
15
+ from pathlib import Path
16
+
17
+ import gradio as gr
18
+ from PIL import Image
19
+
20
+ from core.apps import APP_REGISTRY, _error_html, _error_updates
21
+ from core.model import vision_infer
22
+ from core.parse import parse_json
23
+
24
+ # ── Config ────────────────────────────────────────────────────────────────────
25
+
26
+ APP_ID = os.environ.get("APP_ID", "allergen_lens")
27
+ if APP_ID not in APP_REGISTRY:
28
+ raise ValueError(f"Unknown APP_ID '{APP_ID}'. Choose from: {list(APP_REGISTRY)}")
29
+
30
+ spec = APP_REGISTRY[APP_ID]
31
+
32
+ # ── Image helpers ─────────────────────────────────────────────────────────────
33
+
34
+ def _pil(item) -> "Image.Image | None":
35
+ if item is None:
36
+ return None
37
+ if isinstance(item, Image.Image):
38
+ return item.convert("RGB")
39
+ if isinstance(item, str) and Path(item).exists():
40
+ return Image.open(item).convert("RGB")
41
+ if isinstance(item, (list, tuple)) and len(item) >= 1:
42
+ return _pil(item[0])
43
+ if hasattr(item, "__array__"):
44
+ import numpy as np
45
+ arr = item if isinstance(item, np.ndarray) else item.__array__()
46
+ return Image.fromarray(arr).convert("RGB")
47
+ return None
48
+
49
+
50
+ def _collect_images(raw) -> list:
51
+ if raw is None:
52
+ return []
53
+ if not isinstance(raw, (list, tuple)):
54
+ img = _pil(raw)
55
+ return [img] if img else []
56
+ imgs = [_pil(item) for item in raw]
57
+ return [i for i in imgs if i is not None]
58
+
59
+
60
+ # ── Status constants ──────────────────────────────────────────────────────────
61
+
62
+ _STATUS_WARM = "⏳ Warming up the model — first run takes a few seconds…"
63
+ _STATUS_CLEAR = ""
64
+
65
+ # ── Inference handler (generator — enables warm-up status) ────────────────────
66
+
67
+ def _run(image_input, *extra_inputs):
68
+ """
69
+ Generator so we can yield the warm-up message before the GPU call blocks,
70
+ then yield the result (+ cleared status) when done.
71
+ Outputs: [status_md] + one gr.update per spec.output_components entry.
72
+ """
73
+ n_out = len(spec.output_components)
74
+
75
+ # Phase 1: show warm-up status immediately; leave result components unchanged
76
+ yield [gr.update(value=_STATUS_WARM)] + [gr.update()] * n_out
77
+
78
+ images = _collect_images(image_input)
79
+ if not images:
80
+ yield [gr.update(value=_STATUS_CLEAR)] + _error_updates(
81
+ n_out, "Please upload at least one image."
82
+ )
83
+ return
84
+
85
+ instruction = spec.instruction_fn(*extra_inputs)
86
+
87
+ try:
88
+ raw = vision_infer(
89
+ images=images,
90
+ instruction=instruction,
91
+ json_mode=(spec.output_mode == "json"),
92
+ max_tokens=spec.max_tokens,
93
+ )
94
+ except Exception as exc:
95
+ yield [gr.update(value=_STATUS_CLEAR)] + _error_updates(n_out, f"Inference error: {exc}")
96
+ return
97
+
98
+ data = parse_json(raw) if spec.output_mode == "json" else raw
99
+ updates = spec.render_fn(data)
100
+
101
+ # Phase 2: clear status, deliver result
102
+ yield [gr.update(value=_STATUS_CLEAR)] + updates
103
+
104
+
105
+ # ── UI builder (generic — driven entirely by AppSpec) ─────────────────────────
106
+
107
+ def _build_output_components(component_types: list[str]) -> list:
108
+ """Create one Gradio component per declared output slot."""
109
+ comps = []
110
+ for kind in component_types:
111
+ if kind == "html":
112
+ comps.append(
113
+ gr.HTML(
114
+ value=(
115
+ '<div style="min-height:180px;display:flex;align-items:center;'
116
+ 'justify-content:center;color:#aaa;font-family:system-ui;font-size:15px">'
117
+ 'Upload an image and click Analyze ✦'
118
+ '</div>'
119
+ )
120
+ )
121
+ )
122
+ elif kind == "markdown":
123
+ comps.append(gr.Markdown(""))
124
+ else:
125
+ comps.append(gr.HTML(""))
126
+ return comps
127
+
128
+
129
+ def build_demo(spec) -> gr.Blocks:
130
+ css = """
131
+ .submit-btn { font-size: 16px !important; padding: 12px 0 !important; }
132
+ footer { display: none !important; }
133
+ """
134
+
135
+ with gr.Blocks(title=spec.title, css=css, theme=gr.themes.Soft()) as demo:
136
+ gr.HTML(
137
+ f'<div style="padding:16px 0 8px">'
138
+ f'<h1 style="margin:0;font-size:26px">{spec.title}</h1>'
139
+ f'<p style="margin:4px 0 0;color:#555;font-size:15px">{spec.tagline}</p>'
140
+ f'</div>'
141
+ )
142
+
143
+ # Shared status indicator — one per page, always present
144
+ status_md = gr.Markdown(_STATUS_CLEAR, visible=True)
145
+
146
+ with gr.Row(equal_height=False):
147
+ # ── Left column: inputs ───────────────────────────────────────────
148
+ with gr.Column(scale=1):
149
+ image_comp = gr.Image(
150
+ label="Upload image",
151
+ type="pil",
152
+ sources=["upload", "webcam", "clipboard"],
153
+ height=300,
154
+ )
155
+
156
+ extra_comps = []
157
+
158
+ if spec.input_spec.text_label:
159
+ text_comp = gr.Textbox(
160
+ label=spec.input_spec.text_label,
161
+ placeholder=spec.input_spec.text_placeholder,
162
+ lines=2,
163
+ )
164
+ extra_comps.append(text_comp)
165
+
166
+ if spec.input_spec.dropdown_choices:
167
+ drop_comp = gr.Dropdown(
168
+ choices=spec.input_spec.dropdown_choices,
169
+ value=spec.input_spec.dropdown_default,
170
+ label=spec.input_spec.dropdown_label,
171
+ )
172
+ extra_comps.append(drop_comp)
173
+
174
+ submit = gr.Button("Analyze ✦", variant="primary", elem_classes=["submit-btn"])
175
+
176
+ # ── Right column: outputs (built from spec) ───────────────────────
177
+ with gr.Column(scale=1):
178
+ output_comps = _build_output_components(spec.output_components)
179
+
180
+ all_inputs = [image_comp] + extra_comps
181
+ all_outputs = [status_md] + output_comps
182
+
183
+ submit.click(fn=_run, inputs=all_inputs, outputs=all_outputs)
184
+
185
+ # Reset outputs when a new image is uploaded
186
+ def _on_image_change(*_):
187
+ placeholder = (
188
+ '<div style="min-height:180px;display:flex;align-items:center;'
189
+ 'justify-content:center;color:#aaa;font-family:system-ui">'
190
+ 'Click Analyze ✦ to process</div>'
191
+ )
192
+ return [gr.update(value=_STATUS_CLEAR)] + [
193
+ gr.update(value=placeholder) for _ in spec.output_components
194
+ ]
195
+
196
+ image_comp.change(fn=_on_image_change, inputs=[image_comp], outputs=all_outputs)
197
+
198
+ # Examples (only when local files exist)
199
+ valid_examples = [
200
+ row for row in spec.examples
201
+ if row and Path(str(row[0])).exists()
202
+ ]
203
+ if valid_examples:
204
+ gr.Examples(
205
+ examples=valid_examples,
206
+ inputs=all_inputs,
207
+ label="Try an example",
208
+ cache_examples=False,
209
+ )
210
+
211
+ gr.HTML(
212
+ '<div style="text-align:center;color:#aaa;font-size:12px;margin-top:16px">'
213
+ f'Powered by <b>MiniCPM-V 4.6 (1.3B)</b> · ZeroGPU · App: <code>{APP_ID}</code>'
214
+ '</div>'
215
+ )
216
+
217
+ return demo
218
+
219
+
220
+ demo = build_demo(spec)
221
+
222
+ if __name__ == "__main__":
223
+ demo.launch(show_error=True)
core/__init__.py ADDED
File without changes
core/apps.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from dataclasses import dataclass, field
3
+ from typing import Callable, List, Optional
4
+
5
+
6
+ @dataclass
7
+ class InputSpec:
8
+ multi_image: bool = False
9
+ text_label: Optional[str] = None
10
+ text_placeholder: str = ""
11
+ dropdown_choices: Optional[List[str]] = None
12
+ dropdown_label: str = ""
13
+ dropdown_default: str = ""
14
+
15
+
16
+ @dataclass
17
+ class AppSpec:
18
+ app_id: str
19
+ title: str
20
+ tagline: str
21
+ # instruction_fn(*extra_inputs) -> str (extra_inputs = text + dropdown, if present)
22
+ instruction_fn: Callable
23
+ output_mode: str # "text" | "json"
24
+ input_spec: InputSpec
25
+ max_tokens: int # floor 512 (oracle) or 1024 (JSON apps)
26
+ examples: List[List] # Gradio examples rows; each row matches inputs order
27
+ # render_fn(result: str|dict|list) -> list[gr.update], one per output_components entry
28
+ render_fn: Callable
29
+ output_components: List[str] = field(default_factory=lambda: ["html"])
30
+ theme_color: str = "#5c6bc0"
31
+
32
+
33
+ # ─────────────────────────────────────────────────────────────────────────────
34
+ # Shared render helpers
35
+ # ─────────────────────────────────────────────────────────────────────────────
36
+
37
+ def _card(body: str, border_color: str = "#5c6bc0", bg: str = "#fafafa") -> str:
38
+ return (
39
+ f'<div style="border-left:5px solid {border_color};padding:20px 24px;'
40
+ f'background:{bg};border-radius:8px;font-family:system-ui,sans-serif">'
41
+ f"{body}</div>"
42
+ )
43
+
44
+
45
+ def _error_html(msg: str) -> str:
46
+ return _card(
47
+ f"<p style='color:#c62828;margin:0'><b>Error:</b> {msg}</p>",
48
+ "#c62828",
49
+ "#fff8f8",
50
+ )
51
+
52
+
53
+ def _error_updates(n_components: int, msg: str) -> list:
54
+ """n_components gr.update objects; error goes in slot 0 (always an html component)."""
55
+ updates = [gr.update(value=_error_html(msg))]
56
+ updates += [gr.update()] * (n_components - 1)
57
+ return updates
58
+
59
+
60
+ # ─────────────────────────────────────────────────────────────────────────────
61
+ # 1. allergen_lens
62
+ # ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ _ALLERGEN_SCHEMA = (
65
+ '{"allergens": ["peanuts", "..."], '
66
+ '"dietary": {"vegetarian": true, "vegan": false, "gluten_free": false}, '
67
+ '"prep_instructions": "...", '
68
+ '"verdict": "Safe to eat" | "Contains allergens" | "Check carefully"}'
69
+ )
70
+
71
+ _ALLERGEN_LANGUAGES = [
72
+ "English", "Spanish", "French", "German", "Portuguese",
73
+ "Italian", "Japanese", "Korean", "Chinese (Simplified)", "Arabic",
74
+ ]
75
+
76
+
77
+ def _allergen_instruction(allergies: str = "", language: str = "English") -> str:
78
+ prompt = (
79
+ "You are a food-safety assistant. Analyze this food product label image.\n"
80
+ "Extract ALL allergen information (common allergens: peanuts, tree nuts, milk, eggs, "
81
+ "wheat/gluten, soy, fish, shellfish, sesame).\n"
82
+ "Determine vegetarian / vegan / gluten-free status.\n"
83
+ "Extract preparation instructions if visible.\n"
84
+ )
85
+ if allergies.strip():
86
+ prompt += (
87
+ f"\nThe user has these dietary restrictions / allergies: {allergies.strip()}\n"
88
+ "Set 'verdict' to 'Contains allergens' if any of the user's allergens are present, "
89
+ "'Safe to eat' if none are present, or 'Check carefully' if unsure.\n"
90
+ )
91
+ else:
92
+ prompt += "\nSet 'verdict' to a general safety summary.\n"
93
+ prompt += f"\nRespond in {language}.\n"
94
+ prompt += f"\nOutput schema: {_ALLERGEN_SCHEMA}"
95
+ return prompt
96
+
97
+
98
+ def _allergen_render(data) -> list:
99
+ if isinstance(data, dict) and "error" in data:
100
+ return [gr.update(value=_error_html(data.get("raw", data["error"])))]
101
+
102
+ verdict = data.get("verdict", "Unknown")
103
+ allergens = data.get("allergens", [])
104
+ dietary = data.get("dietary", {})
105
+ prep = data.get("prep_instructions", "")
106
+
107
+ has_allergens = bool(allergens)
108
+ border = "#c62828" if has_allergens else "#2e7d32"
109
+ verdict_bg = "#ffebee" if has_allergens else "#e8f5e9"
110
+ verdict_color = "#c62828" if has_allergens else "#2e7d32"
111
+
112
+ chips = "".join(
113
+ f'<span style="display:inline-block;background:#ef5350;color:white;'
114
+ f'padding:3px 10px;border-radius:12px;margin:3px 3px 3px 0;font-size:13px">{a}</span>'
115
+ for a in allergens
116
+ )
117
+ if not chips:
118
+ chips = '<span style="color:#757575;font-style:italic">None detected</span>'
119
+
120
+ dietary_icons = []
121
+ if dietary.get("vegetarian"):
122
+ dietary_icons.append("🥬 Vegetarian")
123
+ if dietary.get("vegan"):
124
+ dietary_icons.append("🌱 Vegan")
125
+ if dietary.get("gluten_free"):
126
+ dietary_icons.append("✓ Gluten-Free")
127
+ dietary_text = " &nbsp;·&nbsp; ".join(dietary_icons) if dietary_icons else "No special designations"
128
+
129
+ prep_section = (
130
+ f'<div style="margin-top:14px">'
131
+ f'<b style="color:#555">Preparation:</b> '
132
+ f'<span style="color:#333">{prep}</span></div>'
133
+ if prep else ""
134
+ )
135
+
136
+ body = (
137
+ f'<div style="background:{verdict_bg};border-radius:6px;padding:12px 16px;margin-bottom:16px">'
138
+ f'<span style="font-size:22px;font-weight:700;color:{verdict_color}">{verdict}</span>'
139
+ f'</div>'
140
+ f'<div style="margin-bottom:12px"><b style="color:#555">Allergens found:</b><br/>{chips}</div>'
141
+ f'<div><b style="color:#555">Dietary:</b> {dietary_text}</div>'
142
+ f'{prep_section}'
143
+ )
144
+ return [gr.update(value=_card(body, border))]
145
+
146
+
147
+ # ─────────────────────────────────────────────────────────────────────────────
148
+ # 2. fridge_dinner
149
+ # ─────────────────────────────────────────────────────────────────────────────
150
+
151
+ _FRIDGE_SCHEMA = (
152
+ '{"available": ["eggs", "spinach", "cheddar", "..."], '
153
+ '"dinners": ['
154
+ ' {"name": "Spinach Omelette", "uses": ["eggs", "spinach", "cheddar"]}, '
155
+ ' {"name": "...", "uses": [...]}, '
156
+ ' {"name": "...", "uses": [...]}'
157
+ '], '
158
+ '"use_soon": ["open yogurt", "wilted lettuce", "..."]}'
159
+ )
160
+
161
+ _DINNER_PALETTE = [
162
+ ("#1565c0", "#e3f2fd", "🍳"),
163
+ ("#2e7d32", "#e8f5e9", "🥘"),
164
+ ("#6a1b9a", "#f3e5f5", "🍲"),
165
+ ]
166
+
167
+
168
+ def _fridge_instruction() -> str:
169
+ return (
170
+ "You are a creative home chef. Carefully look at this photo of an open fridge or pantry.\n"
171
+ "1. List ALL visible ingredients and food items you can identify "
172
+ "(be specific: 'Greek yogurt' not 'dairy', '2% milk' not 'milk').\n"
173
+ "2. Suggest EXACTLY THREE dinner ideas using ONLY what is visible — "
174
+ "no grocery run required. For each dinner, list which visible ingredients it uses.\n"
175
+ "3. Flag any items that look like they should be used soon: "
176
+ "opened packages, produce past peak freshness, leftovers, etc.\n"
177
+ f"Output schema (JSON only): {_FRIDGE_SCHEMA}"
178
+ )
179
+
180
+
181
+ def _fridge_render(data) -> list:
182
+ if isinstance(data, dict) and "error" in data:
183
+ return [gr.update(value=_error_html(data.get("raw", data["error"])))]
184
+
185
+ available = data.get("available", [])
186
+ dinners = data.get("dinners", [])
187
+ use_soon = data.get("use_soon", [])
188
+
189
+ # Three dinner cards
190
+ cards_html = ""
191
+ for i, dinner in enumerate(dinners[:3]):
192
+ name = dinner.get("name", f"Dinner {i + 1}") if isinstance(dinner, dict) else str(dinner)
193
+ uses = dinner.get("uses", []) if isinstance(dinner, dict) else []
194
+ color, bg, emoji = _DINNER_PALETTE[i % len(_DINNER_PALETTE)]
195
+
196
+ uses_chips = "".join(
197
+ f'<span style="background:{color}18;color:{color};border:1px solid {color}44;'
198
+ f'padding:2px 8px;border-radius:10px;font-size:12px;'
199
+ f'margin:2px 2px 2px 0;display:inline-block">{ing}</span>'
200
+ for ing in uses
201
+ )
202
+
203
+ cards_html += (
204
+ f'<div style="flex:1;min-width:160px;background:{bg};'
205
+ f'border:2px solid {color}44;border-radius:10px;padding:16px">'
206
+ f'<div style="font-size:24px;margin-bottom:6px">{emoji}</div>'
207
+ f'<div style="font-weight:700;font-size:15px;color:{color};margin-bottom:10px">{name}</div>'
208
+ f'<div style="font-size:11px;color:#757575;margin-bottom:5px;text-transform:uppercase;'
209
+ f'letter-spacing:.5px">Uses</div>'
210
+ f'<div>{uses_chips or "<span style=\'color:#aaa;font-size:12px\'>—</span>"}</div>'
211
+ f'</div>'
212
+ )
213
+
214
+ available_text = ", ".join(available) if available else "Nothing detected"
215
+
216
+ use_soon_chips = "".join(
217
+ f'<span style="background:#fff3e0;color:#e65100;border:1px solid #ff980044;'
218
+ f'padding:3px 10px;border-radius:10px;font-size:13px;'
219
+ f'margin:3px 3px 3px 0;display:inline-block">⚠ {item}</span>'
220
+ for item in use_soon
221
+ )
222
+ use_soon_section = (
223
+ f'<div style="margin-top:20px;padding:14px 16px;background:#fff8e1;'
224
+ f'border-radius:8px;border-left:4px solid #ffa000">'
225
+ f'<div style="font-weight:600;color:#e65100;margin-bottom:8px">⏰ Use these soon</div>'
226
+ f'<div>{use_soon_chips}</div>'
227
+ f'</div>'
228
+ if use_soon else ""
229
+ )
230
+
231
+ html = (
232
+ f'<div style="font-family:system-ui,sans-serif">'
233
+ f'<div style="font-size:12px;color:#757575;margin-bottom:14px">'
234
+ f'Spotted: {available_text}</div>'
235
+ f'<div style="display:flex;gap:12px;flex-wrap:wrap">{cards_html}</div>'
236
+ f'{use_soon_section}'
237
+ f'</div>'
238
+ )
239
+ return [gr.update(value=html)]
240
+
241
+
242
+ # ─────────────────────────────────────────────────────────────────────────────
243
+ # 3. object_oracle
244
+ # ─────────────────────────────────────────────────────────────────────────────
245
+
246
+ def _oracle_instruction() -> str:
247
+ return (
248
+ "You are the Object Oracle — a mystical seer who reveals the hidden essence of things.\n"
249
+ "Look at this image and deliver a tarot-style poetic reading.\n"
250
+ "Rules:\n"
251
+ "- Clearly reference what is VISIBLY in the photo (name the object, scene, or subject).\n"
252
+ "- Speak in vivid, poetic language with a mystical/spiritual tone.\n"
253
+ "- Give the reading a title (e.g. 'The Wandering Teapot', 'Spirit of the Forgotten Key').\n"
254
+ "- 3-5 sentences maximum. Lead with the title on its own line.\n"
255
+ "- End with a one-line fortune or warning (prefix with 'Fortune:').\n"
256
+ "Do NOT mention that you are an AI or a language model."
257
+ )
258
+
259
+
260
+ def _oracle_render(text: str) -> list:
261
+ lines = text.strip().split("\n", 1)
262
+ title = lines[0].strip().strip("*#_") if lines else "The Oracle Speaks"
263
+ body = lines[1].strip() if len(lines) > 1 else text.strip()
264
+
265
+ fortune = ""
266
+ if "Fortune:" in body:
267
+ parts = body.rsplit("Fortune:", 1)
268
+ body = parts[0].strip()
269
+ fortune = parts[1].strip()
270
+
271
+ fortune_html = (
272
+ f'<div style="margin-top:20px;padding:12px 16px;background:rgba(255,255,255,0.1);'
273
+ f'border-radius:6px;font-style:italic;font-size:14px;color:#f0d080">'
274
+ f'✦ {fortune} ✦</div>'
275
+ if fortune else ""
276
+ )
277
+
278
+ html = (
279
+ '<div style="background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);'
280
+ 'color:#e8d5a3;border-radius:14px;padding:32px 36px;font-family:Georgia,serif;'
281
+ 'border:1px solid #e0c97f44;min-height:180px">'
282
+ f'<div style="text-align:center;font-size:13px;letter-spacing:3px;color:#c9a84c;'
283
+ f'margin-bottom:16px;text-transform:uppercase">✦ The Oracle Speaks ✦</div>'
284
+ f'<div style="font-size:20px;font-weight:bold;color:#f0d080;'
285
+ f'text-align:center;margin-bottom:16px">{title}</div>'
286
+ f'<div style="font-size:16px;line-height:1.9;color:#e0cfa0;white-space:pre-line">{body}</div>'
287
+ f'{fortune_html}'
288
+ f'<div style="text-align:center;margin-top:20px;font-size:22px;color:#c9a84c">⟡</div>'
289
+ '</div>'
290
+ )
291
+ return [gr.update(value=html)]
292
+
293
+
294
+ # ─────────────────────────────────────────────────────────────────────────────
295
+ # 4. whats_that_error
296
+ # ─────────────────────────────────────────────────────────────────────────────
297
+
298
+ _ERROR_SCHEMA = (
299
+ '{"detected_code": "E01", "likely_cause": "...", '
300
+ '"fix_steps": ["Step 1: ...", "Step 2: ..."], "severity": "low|medium|high|critical"}'
301
+ )
302
+
303
+ _SEVERITY_STYLE = {
304
+ "low": ("#2e7d32", "#e8f5e9"),
305
+ "medium": ("#e65100", "#fff3e0"),
306
+ "high": ("#c62828", "#ffebee"),
307
+ "critical": ("#6a1b9a", "#f3e5f5"),
308
+ }
309
+
310
+
311
+ def _error_instruction(appliance: str = "") -> str:
312
+ device_hint = f" on a '{appliance.strip()}'" if appliance.strip() else ""
313
+ return (
314
+ f"You are an appliance and electronics repair expert.\n"
315
+ f"Analyze this photo of an error screen, display panel, or error code{device_hint}.\n"
316
+ f"Identify the error code or fault indicator shown.\n"
317
+ f"Provide: the exact detected code/indicator, the most likely root cause, "
318
+ f"clear numbered fix steps a non-expert can follow, and severity level.\n"
319
+ f"If no code is visible, describe what you see and give general troubleshooting steps.\n"
320
+ f"Output schema: {_ERROR_SCHEMA}"
321
+ )
322
+
323
+
324
+ def _error_render(data) -> list:
325
+ if isinstance(data, dict) and "error" in data:
326
+ return [gr.update(value=_error_html(data.get("raw", data["error"])))]
327
+
328
+ code = data.get("detected_code", "Unknown")
329
+ cause = data.get("likely_cause", "")
330
+ steps = data.get("fix_steps", [])
331
+ severity = str(data.get("severity", "medium")).lower()
332
+
333
+ color, bg = _SEVERITY_STYLE.get(severity, ("#555", "#f5f5f5"))
334
+
335
+ steps_html = "".join(
336
+ f'<li style="margin:8px 0;color:#333;font-size:14px">{step}</li>'
337
+ for step in (steps if isinstance(steps, list) else [steps])
338
+ )
339
+
340
+ html = (
341
+ f'<div style="font-family:system-ui,sans-serif;max-width:620px">'
342
+ f'<div style="background:{color};color:white;padding:14px 20px;'
343
+ f'border-radius:8px 8px 0 0;display:flex;align-items:center;gap:12px">'
344
+ f'<span style="font-size:22px">⚠️</span>'
345
+ f'<div><div style="font-size:18px;font-weight:700">{code}</div>'
346
+ f'<div style="font-size:12px;opacity:.85;text-transform:uppercase;letter-spacing:1px">'
347
+ f'Severity: {severity}</div></div>'
348
+ f'</div>'
349
+ f'<div style="border:2px solid {color};border-top:none;border-radius:0 0 8px 8px;'
350
+ f'padding:18px 20px;background:{bg}">'
351
+ f'<div style="font-size:13px;font-weight:600;color:#555;margin-bottom:4px">LIKELY CAUSE</div>'
352
+ f'<p style="margin:0 0 16px;font-size:15px;color:#222">{cause}</p>'
353
+ f'<div style="font-size:13px;font-weight:600;color:#555;margin-bottom:6px">FIX STEPS</div>'
354
+ f'<ol style="margin:0;padding-left:22px">{steps_html or "<li>No steps available.</li>"}</ol>'
355
+ f'</div></div>'
356
+ )
357
+ return [gr.update(value=html)]
358
+
359
+
360
+ # ─────────────────────────────────────────────────────────────────────────────
361
+ # APP_REGISTRY
362
+ # ─────────────────────────────────────────────────────────────────────────────
363
+
364
+ APP_REGISTRY: dict[str, AppSpec] = {
365
+ "allergen_lens": AppSpec(
366
+ app_id="allergen_lens",
367
+ title="🔍 Allergen Lens",
368
+ tagline="Snap a food label — get instant allergen & dietary analysis.",
369
+ instruction_fn=_allergen_instruction,
370
+ output_mode="json",
371
+ input_spec=InputSpec(
372
+ text_label="My allergies / dietary restrictions (optional)",
373
+ text_placeholder="e.g. peanuts, dairy, gluten",
374
+ dropdown_choices=_ALLERGEN_LANGUAGES,
375
+ dropdown_label="Response language",
376
+ dropdown_default="English",
377
+ ),
378
+ max_tokens=1024,
379
+ examples=[
380
+ ["examples/allergen_label.jpg", "peanuts, dairy", "English"],
381
+ ["examples/allergen_label2.jpg", "", "Spanish"],
382
+ ],
383
+ render_fn=_allergen_render,
384
+ output_components=["html"],
385
+ theme_color="#2e7d32",
386
+ ),
387
+
388
+ "fridge_dinner": AppSpec(
389
+ app_id="fridge_dinner",
390
+ title="🍽️ Fridge Dinner",
391
+ tagline="Photo your fridge — get three dinners you can make right now.",
392
+ instruction_fn=_fridge_instruction,
393
+ output_mode="json",
394
+ input_spec=InputSpec(),
395
+ max_tokens=1024,
396
+ examples=[
397
+ ["examples/fridge_dinner.jpg"],
398
+ ],
399
+ render_fn=_fridge_render,
400
+ output_components=["html"],
401
+ theme_color="#2e7d32",
402
+ ),
403
+
404
+ "object_oracle": AppSpec(
405
+ app_id="object_oracle",
406
+ title="🔮 Object Oracle",
407
+ tagline="Hold up any object — receive its mystical reading.",
408
+ instruction_fn=_oracle_instruction,
409
+ output_mode="text",
410
+ input_spec=InputSpec(),
411
+ max_tokens=512,
412
+ examples=[
413
+ ["examples/oracle_object.jpg"],
414
+ ],
415
+ render_fn=_oracle_render,
416
+ output_components=["html"],
417
+ theme_color="#4a148c",
418
+ ),
419
+
420
+ "whats_that_error": AppSpec(
421
+ app_id="whats_that_error",
422
+ title="🛠️ What's That Error?",
423
+ tagline="Photo your error screen — get a plain-English fix.",
424
+ instruction_fn=_error_instruction,
425
+ output_mode="json",
426
+ input_spec=InputSpec(
427
+ text_label="Appliance or device name (optional)",
428
+ text_placeholder="e.g. Samsung washing machine, iPhone, HP printer",
429
+ ),
430
+ max_tokens=1024,
431
+ examples=[
432
+ ["examples/error_screen.jpg", ""],
433
+ ["examples/error_screen2.jpg", "LG dishwasher"],
434
+ ],
435
+ render_fn=_error_render,
436
+ output_components=["html"],
437
+ theme_color="#c62828",
438
+ ),
439
+ }
core/model.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces # ZeroGPU: must precede torch/transformers imports
2
+
3
+ import torch
4
+ from PIL import Image
5
+ from transformers import AutoModelForImageTextToText, AutoProcessor
6
+
7
+ MODEL_ID = "openbmb/MiniCPM-V-4.6"
8
+
9
+ processor = AutoProcessor.from_pretrained(MODEL_ID)
10
+ model = AutoModelForImageTextToText.from_pretrained(
11
+ MODEL_ID,
12
+ torch_dtype=torch.bfloat16,
13
+ )
14
+ model = model.to("cuda")
15
+ model.eval()
16
+
17
+
18
+ def _to_pil(img) -> Image.Image:
19
+ if isinstance(img, Image.Image):
20
+ return img.convert("RGB")
21
+ if hasattr(img, "__array__"):
22
+ import numpy as np
23
+ arr = img if isinstance(img, np.ndarray) else img.__array__()
24
+ return Image.fromarray(arr).convert("RGB")
25
+ if isinstance(img, str):
26
+ return Image.open(img).convert("RGB")
27
+ raise TypeError(f"Cannot convert {type(img)} to PIL Image")
28
+
29
+
30
+ @spaces.GPU(duration=120)
31
+ def vision_infer(
32
+ images,
33
+ instruction: str,
34
+ json_mode: bool = False,
35
+ max_tokens: int = 768,
36
+ ) -> str:
37
+ """Single GPU entrypoint. images: PIL Image or list of PIL Images."""
38
+ if not isinstance(images, list):
39
+ images = [images]
40
+
41
+ pil_images = [_to_pil(img) for img in images]
42
+
43
+ if json_mode:
44
+ instruction = (
45
+ instruction
46
+ + "\n\nRespond with ONLY valid JSON. No markdown fences, no prose, no explanation."
47
+ )
48
+
49
+ content = [{"type": "image", "image": img} for img in pil_images]
50
+ content.append({"type": "text", "text": instruction})
51
+
52
+ messages = [{"role": "user", "content": content}]
53
+
54
+ inputs = processor.apply_chat_template(
55
+ messages,
56
+ tokenize=True,
57
+ add_generation_prompt=True,
58
+ return_dict=True,
59
+ return_tensors="pt",
60
+ downsample_mode="16x",
61
+ max_slice_nums=36,
62
+ ).to(model.device)
63
+
64
+ with torch.no_grad():
65
+ generated_ids = model.generate(
66
+ **inputs,
67
+ downsample_mode="16x",
68
+ max_new_tokens=max_tokens,
69
+ do_sample=False,
70
+ )
71
+
72
+ trimmed = [
73
+ out_ids[len(in_ids):]
74
+ for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
75
+ ]
76
+ return processor.batch_decode(
77
+ trimmed,
78
+ skip_special_tokens=True,
79
+ clean_up_tokenization_spaces=False,
80
+ )[0]
core/parse.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+
4
+
5
+ def _strip_fences(text: str) -> str:
6
+ text = text.strip()
7
+ text = re.sub(r"^```(?:json)?\s*\n?", "", text, flags=re.MULTILINE)
8
+ text = re.sub(r"\n?```\s*$", "", text, flags=re.MULTILINE)
9
+ return text.strip()
10
+
11
+
12
+ def _extract_first_json(text: str):
13
+ """Try to pull the first {...} or [...] block from noisy text."""
14
+ for pattern in (r"\{[\s\S]*\}", r"\[[\s\S]*\]"):
15
+ match = re.search(pattern, text)
16
+ if match:
17
+ try:
18
+ return json.loads(match.group())
19
+ except json.JSONDecodeError:
20
+ pass
21
+ return None
22
+
23
+
24
+ def parse_json(text: str):
25
+ """
26
+ Defensively parse model JSON output.
27
+ Returns parsed dict/list, or {"error": "...", "raw": "..."} on failure.
28
+ Never raises.
29
+ """
30
+ cleaned = _strip_fences(text)
31
+
32
+ try:
33
+ return json.loads(cleaned)
34
+ except json.JSONDecodeError:
35
+ pass
36
+
37
+ recovered = _extract_first_json(cleaned)
38
+ if recovered is not None:
39
+ return recovered
40
+
41
+ return {"error": "Could not parse model output as JSON.", "raw": text[:600]}
examples/allergen_label.jpg ADDED
examples/allergen_label2.jpg ADDED
examples/concierge_event.jpg ADDED
examples/concierge_receipt.jpg ADDED
examples/concierge_screenshot.jpg ADDED
examples/error_screen.jpg ADDED
examples/error_screen2.jpg ADDED
examples/fridge_dinner.jpg ADDED
examples/oracle_object.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.40.0
2
+ spaces>=0.30.0
3
+ transformers>=4.51.0
4
+ torch>=2.1.0
5
+ Pillow>=10.0.0
6
+ accelerate>=0.26.0
7
+ sentencepiece
8
+ torchvision
scripts/fetch_examples.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Download public-domain example images for all four demo apps.
3
+ Run once before first launch: python scripts/fetch_examples.py
4
+ """
5
+
6
+ import urllib.request
7
+ from pathlib import Path
8
+
9
+ EXAMPLES_DIR = Path(__file__).parent.parent / "examples"
10
+ EXAMPLES_DIR.mkdir(exist_ok=True)
11
+
12
+ # Each entry: (local_filename, url, description)
13
+ IMAGES = [
14
+ # allergen_lens — food labels
15
+ (
16
+ "allergen_label.jpg",
17
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Nutrition_facts_label_example.jpg/640px-Nutrition_facts_label_example.jpg",
18
+ "Nutrition facts label (allergen_lens example 1)",
19
+ ),
20
+ (
21
+ "allergen_label2.jpg",
22
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a3/Nutritional_labels.jpg/640px-Nutritional_labels.jpg",
23
+ "Ingredient list label (allergen_lens example 2)",
24
+ ),
25
+ # camera_roll_concierge — varied everyday photos
26
+ (
27
+ "concierge_receipt.jpg",
28
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Store_receipt.jpg/640px-Store_receipt.jpg",
29
+ "Receipt photo (camera_roll example)",
30
+ ),
31
+ (
32
+ "concierge_recipe.jpg",
33
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat_03.jpg/640px-Cat_03.jpg",
34
+ "Placeholder photo (camera_roll example)",
35
+ ),
36
+ # object_oracle — interesting objects
37
+ (
38
+ "oracle_object.jpg",
39
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png",
40
+ "Interesting object (oracle example)",
41
+ ),
42
+ # whats_that_error — error screens
43
+ (
44
+ "error_screen.jpg",
45
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Windows_9X_BSOD.png/640px-Windows_9X_BSOD.png",
46
+ "BSOD error screen (whats_that_error example 1)",
47
+ ),
48
+ (
49
+ "error_screen2.jpg",
50
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Windows_XP_BSOD.png/640px-Windows_XP_BSOD.png",
51
+ "XP BSOD error screen (whats_that_error example 2)",
52
+ ),
53
+ ]
54
+
55
+
56
+ def fetch_all():
57
+ for filename, url, desc in IMAGES:
58
+ dest = EXAMPLES_DIR / filename
59
+ if dest.exists():
60
+ print(f" skip {filename}")
61
+ continue
62
+ print(f" get {filename} — {desc}")
63
+ try:
64
+ urllib.request.urlretrieve(url, dest)
65
+ print(f" ok {filename}")
66
+ except Exception as exc:
67
+ print(f" FAIL {filename}: {exc}")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ print(f"Downloading examples to {EXAMPLES_DIR} ...")
72
+ fetch_all()
73
+ print("Done.")
scripts/generate_examples.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate demo example images using PIL only (no network required).
3
+ Run: python scripts/generate_examples.py
4
+ """
5
+
6
+ from pathlib import Path
7
+ from PIL import Image, ImageDraw, ImageFont
8
+
9
+ EXAMPLES = Path(__file__).parent.parent / "examples"
10
+ EXAMPLES.mkdir(exist_ok=True)
11
+
12
+
13
+ def _font(size=16):
14
+ try:
15
+ return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", size)
16
+ except Exception:
17
+ try:
18
+ return ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", size)
19
+ except Exception:
20
+ return ImageFont.load_default()
21
+
22
+
23
+ def _font_bold(size=16):
24
+ try:
25
+ return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", size)
26
+ except Exception:
27
+ try:
28
+ return ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", size)
29
+ except Exception:
30
+ return ImageFont.load_default()
31
+
32
+
33
+ # ── allergen_label.jpg ────────────────────────────────────────────────────────
34
+
35
+ def make_allergen_label(path: Path):
36
+ img = Image.new("RGB", (480, 640), "#ffffff")
37
+ d = ImageDraw.Draw(img)
38
+
39
+ # outer border
40
+ d.rectangle([10, 10, 469, 629], outline="#000000", width=3)
41
+
42
+ fb = _font_bold(28)
43
+ fm = _font(14)
44
+ fs = _font(11)
45
+
46
+ y = 24
47
+ d.text((240, y), "Nutrition Facts", font=fb, fill="#000", anchor="mt")
48
+ y += 36
49
+ d.line([14, y, 466, y], fill="#000", width=2)
50
+ y += 8
51
+ d.text((20, y), "Serving Size 1 cup (240ml)", font=fm, fill="#000")
52
+ y += 22
53
+ d.text((20, y), "Servings Per Container: 2", font=fm, fill="#000")
54
+ y += 18
55
+ d.line([14, y, 466, y], fill="#000", width=6)
56
+ y += 8
57
+ d.text((20, y), "Amount Per Serving", font=fs, fill="#000")
58
+ y += 16
59
+ d.text((20, y), "Calories", font=_font_bold(20), fill="#000")
60
+ d.text((300, y), "250", font=_font_bold(30), fill="#000")
61
+ y += 34
62
+ d.line([14, y, 466, y], fill="#000", width=3)
63
+
64
+ nutrients = [
65
+ ("Total Fat", "8g", "10%"),
66
+ (" Saturated Fat", "3g", "15%"),
67
+ (" Trans Fat", "0g", ""),
68
+ ("Cholesterol", "25mg", "8%"),
69
+ ("Sodium", "580mg", "25%"),
70
+ ("Total Carbohydrate", "37g", "13%"),
71
+ (" Dietary Fiber", "4g", "14%"),
72
+ (" Total Sugars", "12g", ""),
73
+ ("Protein", "5g", ""),
74
+ ]
75
+ for name, amt, pct in nutrients:
76
+ y += 4
77
+ d.text((20, y), name, font=fm, fill="#000")
78
+ d.text((340, y), amt, font=fm, fill="#000")
79
+ if pct:
80
+ d.text((420, y), pct, font=fm, fill="#000")
81
+ y += 18
82
+ d.line([14, y, 466, y], fill="#cccccc", width=1)
83
+
84
+ y += 12
85
+ d.line([14, y, 466, y], fill="#000", width=6)
86
+ y += 10
87
+ d.text((20, y), "INGREDIENTS:", font=_font_bold(13), fill="#000")
88
+ y += 18
89
+ ingredients = (
90
+ "Whole milk, enriched flour (wheat flour, niacin, reduced iron,\n"
91
+ "thiamine mononitrate, riboflavin, folic acid), sugar, palm oil,\n"
92
+ "cocoa butter, SOY lecithin, PEANUT oil, modified starch,\n"
93
+ "salt, vanilla extract, MILK protein concentrate."
94
+ )
95
+ for line in ingredients.split("\n"):
96
+ d.text((20, y), line, font=fs, fill="#000")
97
+ y += 15
98
+
99
+ y += 10
100
+ d.line([14, y, 466, y], fill="#000", width=2)
101
+ y += 8
102
+ allergen_text = "CONTAINS: WHEAT, MILK, SOY, PEANUTS"
103
+ d.text((240, y), allergen_text, font=_font_bold(13), fill="#cc0000", anchor="mt")
104
+
105
+ img.save(path, "JPEG", quality=90)
106
+ print(f" created {path.name}")
107
+
108
+
109
+ def make_allergen_label2(path: Path):
110
+ img = Image.new("RGB", (480, 360), "#f9f9f0")
111
+ d = ImageDraw.Draw(img)
112
+ d.rectangle([8, 8, 471, 351], outline="#888", width=2)
113
+ fb = _font_bold(18)
114
+ fm = _font(13)
115
+
116
+ d.text((240, 20), "Ingredients List", font=fb, fill="#222", anchor="mt")
117
+ d.line([10, 44, 470, 44], fill="#888", width=1)
118
+
119
+ lines = [
120
+ "Water, sugar, modified corn starch, citric acid,",
121
+ "natural flavors, sodium benzoate, MILK powder,",
122
+ "caramel color, TREE NUTS (almonds, cashews),",
123
+ "ascorbic acid (Vitamin C), beta-carotene.",
124
+ "",
125
+ "ALLERGEN INFORMATION:",
126
+ "Contains: MILK, TREE NUTS",
127
+ "Manufactured in a facility that also processes EGGS.",
128
+ "",
129
+ "Vegetarian ✓ Gluten-Free ✓ NOT Vegan (contains milk)",
130
+ ]
131
+ y = 56
132
+ for line in lines:
133
+ color = "#cc0000" if line.startswith("ALLERGEN") or "Contains:" in line else "#333"
134
+ bold = line.startswith("ALLERGEN") or line.startswith("Vegetarian")
135
+ d.text((20, y), line, font=_font_bold(13) if bold else fm, fill=color)
136
+ y += 20
137
+
138
+ img.save(path, "JPEG", quality=90)
139
+ print(f" created {path.name}")
140
+
141
+
142
+ # ── camera_roll examples ────────────────────────────────────────���─────────────
143
+
144
+ def make_receipt(path: Path):
145
+ img = Image.new("RGB", (380, 520), "#fffef8")
146
+ d = ImageDraw.Draw(img)
147
+ fb = _font_bold(16)
148
+ fm = _font(13)
149
+ fs = _font(11)
150
+
151
+ y = 20
152
+ d.text((190, y), "WHOLE FOODS MARKET", font=fb, fill="#000", anchor="mt")
153
+ y += 22
154
+ d.text((190, y), "Store #0472 · San Francisco, CA", font=fs, fill="#555", anchor="mt")
155
+ y += 18
156
+ d.text((190, y), "Tel: (415) 555-0192", font=fs, fill="#555", anchor="mt")
157
+ y += 24
158
+ d.line([10, y, 370, y], fill="#000", width=1)
159
+ y += 10
160
+ d.text((20, y), "Date: 06/13/2026 10:42 AM", font=fs, fill="#333")
161
+ d.text((370, y), "REG #3", font=fs, fill="#333", anchor="rt")
162
+ y += 20
163
+ d.line([10, y, 370, y], fill="#999", width=1)
164
+ y += 8
165
+
166
+ items = [
167
+ ("Organic Whole Milk 1gal", "5.99"),
168
+ ("Sourdough Bread", "4.49"),
169
+ ("Free Range Eggs 12ct", "6.29"),
170
+ ("Mixed Salad Greens 5oz", "3.99"),
171
+ ("Sparkling Water 12pk", "11.99"),
172
+ ("Dark Chocolate 70%", "3.49"),
173
+ ("Almond Butter 16oz", "8.99"),
174
+ ]
175
+ for name, price in items:
176
+ d.text((20, y), name, font=fm, fill="#222")
177
+ d.text((360, y), f"${price}", font=fm, fill="#222", anchor="rt")
178
+ y += 20
179
+
180
+ y += 6
181
+ d.line([10, y, 370, y], fill="#000", width=1)
182
+ y += 8
183
+ subtotal = "45.23"
184
+ tax = "3.62"
185
+ total = "48.85"
186
+ for label, val in [("Subtotal", subtotal), ("Tax (8%)", tax)]:
187
+ d.text((20, y), label, font=fm, fill="#444")
188
+ d.text((360, y), f"${val}", font=fm, fill="#444", anchor="rt")
189
+ y += 20
190
+ d.text((20, y), "TOTAL", font=fb, fill="#000")
191
+ d.text((360, y), f"${total}", font=fb, fill="#000", anchor="rt")
192
+ y += 24
193
+ d.line([10, y, 370, y], fill="#000", width=2)
194
+ y += 10
195
+ d.text((190, y), "VISA **** 4821", font=fm, fill="#555", anchor="mt")
196
+ y += 18
197
+ d.text((190, y), "Thank you for shopping!", font=fs, fill="#888", anchor="mt")
198
+
199
+ img.save(path, "JPEG", quality=90)
200
+ print(f" created {path.name}")
201
+
202
+
203
+ def make_event_flyer(path: Path):
204
+ img = Image.new("RGB", (480, 360), "#1a237e")
205
+ d = ImageDraw.Draw(img)
206
+
207
+ d.rectangle([16, 16, 463, 343], outline="#ffffff", width=2)
208
+ y = 40
209
+ d.text((240, y), "TECH MEETUP", font=_font_bold(32), fill="#ffeb3b", anchor="mt")
210
+ y += 50
211
+ d.text((240, y), "Building Small, Shipping Big", font=_font(18), fill="#e8eaf6", anchor="mt")
212
+ y += 40
213
+ d.line([40, y, 440, y], fill="#3949ab", width=1)
214
+ y += 20
215
+ details = [
216
+ ("📅 Date:", "Thursday, June 18, 2026"),
217
+ ("🕖 Time:", "6:30 PM – 9:00 PM"),
218
+ ("📍 Venue:", "Hugging Face HQ, NYC"),
219
+ ("🎟️ RSVP:", "events.hf.co/buildsmall"),
220
+ ]
221
+ for label, val in details:
222
+ d.text((60, y), label, font=_font_bold(14), fill="#90caf9")
223
+ d.text((175, y), val, font=_font(14), fill="#ffffff")
224
+ y += 26
225
+ y += 12
226
+ d.text((240, y), "Free admission · Light refreshments provided", font=_font(11), fill="#9fa8da", anchor="mt")
227
+
228
+ img.save(path, "JPEG", quality=90)
229
+ print(f" created {path.name}")
230
+
231
+
232
+ def make_screenshot(path: Path):
233
+ img = Image.new("RGB", (640, 400), "#ffffff")
234
+ d = ImageDraw.Draw(img)
235
+
236
+ # Fake browser chrome
237
+ d.rectangle([0, 0, 640, 40], fill="#dee1e6")
238
+ d.ellipse([10, 12, 26, 28], fill="#ff5f57")
239
+ d.ellipse([34, 12, 50, 28], fill="#febc2e")
240
+ d.ellipse([58, 12, 74, 28], fill="#28c840")
241
+ d.rectangle([90, 8, 540, 32], fill="#ffffff", outline="#b8b8b8")
242
+ d.text((315, 20), "https://news.ycombinator.com", font=_font(12), fill="#555", anchor="mm")
243
+
244
+ # Page content
245
+ y = 60
246
+ d.text((20, y), "Hacker News", font=_font_bold(22), fill="#ff6600")
247
+ y += 36
248
+ stories = [
249
+ "Show HN: I built a 1.3B VLM that fits in a ZeroGPU Space (748 pts)",
250
+ "Ask HN: What makes a great hackathon demo? (312 pts)",
251
+ "MiniCPM-V achieves SOTA on OCRBench at 1.3B params (521 pts)",
252
+ "The unreasonable effectiveness of small vision models (189 pts)",
253
+ "From zero to ZeroGPU in 24 hours (245 pts)",
254
+ ]
255
+ for i, story in enumerate(stories):
256
+ d.text((20, y), f"{i+1}.", font=_font(13), fill="#828282")
257
+ d.text((44, y), story, font=_font(13), fill="#000000")
258
+ y += 22
259
+ d.text((44, y), f"{(i+1)*47} points by user{i+1} | {i+2} hours ago | {(i+1)*3} comments", font=_font(11), fill="#828282")
260
+ y += 24
261
+
262
+ img.save(path, "JPEG", quality=90)
263
+ print(f" created {path.name}")
264
+
265
+
266
+ # ── oracle_object.jpg ─────────────────────────────────────────────────────────
267
+
268
+ def make_oracle_object(path: Path):
269
+ img = Image.new("RGB", (480, 480), "#1c1c2e")
270
+ d = ImageDraw.Draw(img)
271
+
272
+ # Draw a stylized antique key
273
+ cx, cy = 240, 240
274
+
275
+ # Key handle — circle
276
+ d.ellipse([cx-90, cy-180, cx+90, cy-0], outline="#c9a84c", width=8)
277
+ d.ellipse([cx-55, cy-145, cx+55, cy-35], outline="#c9a84c", width=4)
278
+ # Key shaft
279
+ d.rectangle([cx-10, cy-10, cx+10, cy+160], fill="#c9a84c")
280
+ # Key teeth
281
+ for offset in [100, 130, 155]:
282
+ d.rectangle([cx+10, cy+offset, cx+35, cy+offset+14], fill="#c9a84c")
283
+
284
+ # Mystical glow effect (concentric ellipses)
285
+ for r in range(5, 0, -1):
286
+ alpha = 60 - r * 10
287
+ color = (201, 168, 76, alpha)
288
+ size = r * 18
289
+ d.ellipse([cx-size, cy-size, cx+size, cy+size], outline="#c9a84c33")
290
+
291
+ # Stars
292
+ import random
293
+ rng = random.Random(42)
294
+ for _ in range(30):
295
+ x, y_ = rng.randint(10, 470), rng.randint(10, 60)
296
+ d.ellipse([x, y_, x+2, y_+2], fill="#ffffff")
297
+ for _ in range(20):
298
+ x, y_ = rng.randint(10, 470), rng.randint(390, 470)
299
+ d.ellipse([x, y_, x+2, y_+2], fill="#ffffff")
300
+
301
+ d.text((240, 440), "What does this object reveal?", font=_font(13), fill="#9e8fcc", anchor="mt")
302
+
303
+ img.save(path, "JPEG", quality=90)
304
+ print(f" created {path.name}")
305
+
306
+
307
+ # ── error screens ─────────────────────────────────────────────────────────────
308
+
309
+ def make_error_screen(path: Path):
310
+ img = Image.new("RGB", (640, 400), "#0a0aff")
311
+ d = ImageDraw.Draw(img)
312
+
313
+ y = 30
314
+ d.text((40, y), ":(", font=_font_bold(80), fill="#ffffff")
315
+ y += 110
316
+ d.text((40, y), "Your PC ran into a problem and needs to restart.", font=_font_bold(20), fill="#ffffff")
317
+ y += 36
318
+ d.text((40, y), "We're just collecting some error info, and then", font=_font(16), fill="#ffffff")
319
+ y += 24
320
+ d.text((40, y), "we'll restart for you.", font=_font(16), fill="#ffffff")
321
+ y += 50
322
+ d.text((40, y), "100% complete", font=_font(16), fill="#ffffff")
323
+ y += 40
324
+ d.text((40, y), "For more information about this issue and possible fixes,", font=_font(13), fill="#ccccff")
325
+ y += 20
326
+ d.text((40, y), "visit https://www.windows.com/stopcode", font=_font(13), fill="#ccccff")
327
+ y += 30
328
+ d.text((40, y), "Stop code: CRITICAL_PROCESS_DIED", font=_font_bold(14), fill="#ffffff")
329
+ y += 24
330
+ d.text((40, y), "What failed: ntoskrnl.exe", font=_font(13), fill="#ccccff")
331
+
332
+ img.save(path, "JPEG", quality=90)
333
+ print(f" created {path.name}")
334
+
335
+
336
+ def make_error_screen2(path: Path):
337
+ img = Image.new("RGB", (560, 420), "#1a1a1a")
338
+ d = ImageDraw.Draw(img)
339
+
340
+ # Fake washing machine display
341
+ d.rectangle([20, 20, 540, 400], fill="#222222", outline="#444444", width=3)
342
+ d.rectangle([40, 40, 520, 200], fill="#001a33", outline="#0066cc", width=2)
343
+
344
+ # Display panel
345
+ d.text((280, 80), "ERROR", font=_font_bold(40), fill="#ff3300", anchor="mt")
346
+ d.text((280, 130), "E4", font=_font_bold(60), fill="#ff6600", anchor="mt")
347
+ d.text((280, 195), "DOOR OPEN", font=_font_bold(16), fill="#ffaa00", anchor="mt")
348
+
349
+ # Status lights
350
+ for x, col in [(100, "#ff0000"), (200, "#ff0000"), (300, "#888888"), (400, "#888888")]:
351
+ d.ellipse([x-8, 220, x+8, 236], fill=col)
352
+
353
+ y = 260
354
+ d.text((280, y), "Samsung WF45T6000AW", font=_font(13), fill="#888888", anchor="mt")
355
+ y += 22
356
+ d.text((280, y), "Washing Machine", font=_font(12), fill="#666666", anchor="mt")
357
+ y += 30
358
+ d.text((280, y), "Press POWER to reset after fixing", font=_font(11), fill="#555555", anchor="mt")
359
+
360
+ # Physical buttons
361
+ for x, label in [(120, "POWER"), (200, "START"), (290, "PAUSE"), (380, "MODE"), (460, "TEMP")]:
362
+ d.ellipse([x-20, 330, x+20, 370], fill="#333333", outline="#555555")
363
+ d.text((x, 395), label, font=_font(9), fill="#666666", anchor="mt")
364
+
365
+ img.save(path, "JPEG", quality=90)
366
+ print(f" created {path.name}")
367
+
368
+
369
+ # ── Main ──────────────────────────────────────────────────────────────────────
370
+
371
+ TASKS = [
372
+ ("allergen_label.jpg", make_allergen_label),
373
+ ("allergen_label2.jpg", make_allergen_label2),
374
+ ("concierge_receipt.jpg", make_receipt),
375
+ ("concierge_event.jpg", make_event_flyer),
376
+ ("concierge_screenshot.jpg", make_screenshot),
377
+ ("oracle_object.jpg", make_oracle_object),
378
+ ("error_screen.jpg", make_error_screen),
379
+ ("error_screen2.jpg", make_error_screen2),
380
+ ]
381
+
382
+ if __name__ == "__main__":
383
+ print(f"Generating examples in {EXAMPLES} ...")
384
+ for filename, fn in TASKS:
385
+ p = EXAMPLES / filename
386
+ fn(p)
387
+ print("Done.")