SPP commited on
Commit
099ea5d
Β·
1 Parent(s): 58e494c

feat: tabbed multi-app UI

Browse files
Files changed (1) hide show
  1. app.py +107 -130
app.py CHANGED
@@ -1,14 +1,3 @@
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
@@ -21,14 +10,6 @@ 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":
@@ -59,53 +40,48 @@ def _collect_images(raw) -> list:
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":
@@ -126,98 +102,99 @@ def _build_output_components(component_types: list[str]) -> list:
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)
 
 
 
 
 
 
 
 
 
 
 
 
1
  import spaces # MUST be the first import for ZeroGPU
2
 
3
  import os
 
10
  from core.model import vision_infer
11
  from core.parse import parse_json
12
 
 
 
 
 
 
 
 
 
13
  # ── Image helpers ─────────────────────────────────────────────────────────────
14
 
15
  def _pil(item) -> "Image.Image | None":
 
40
 
41
  # ── Status constants ──────────────────────────────────────────────────────────
42
 
43
+ _STATUS_WARM = "⏳ Warming up the model β€” first run takes a few seconds…"
44
  _STATUS_CLEAR = ""
45
 
46
+ # ── Per-spec inference handler factory ────────────────────────────────────────
 
 
 
 
 
 
 
 
47
 
48
+ def _make_run(spec):
49
+ def _run(image_input, *extra_inputs):
50
+ n_out = len(spec.output_components)
51
+ yield [gr.update(value=_STATUS_WARM)] + [gr.update()] * n_out
52
 
53
+ images = _collect_images(image_input)
54
+ if not images:
55
+ yield [gr.update(value=_STATUS_CLEAR)] + _error_updates(
56
+ n_out, "Please upload at least one image."
57
+ )
58
+ return
59
 
60
+ instruction = spec.instruction_fn(*extra_inputs)
61
 
62
+ try:
63
+ raw = vision_infer(
64
+ images=images,
65
+ instruction=instruction,
66
+ json_mode=(spec.output_mode == "json"),
67
+ max_tokens=spec.max_tokens,
68
+ )
69
+ except Exception as exc:
70
+ yield [gr.update(value=_STATUS_CLEAR)] + _error_updates(
71
+ n_out, f"Inference error: {exc}"
72
+ )
73
+ return
74
 
75
+ data = parse_json(raw) if spec.output_mode == "json" else raw
76
+ updates = spec.render_fn(data)
77
+ yield [gr.update(value=_STATUS_CLEAR)] + updates
78
 
79
+ return _run
 
80
 
81
 
82
+ # ── Output component builder ─────��────────────────────────────────────────────
83
 
84
  def _build_output_components(component_types: list[str]) -> list:
 
85
  comps = []
86
  for kind in component_types:
87
  if kind == "html":
 
102
  return comps
103
 
104
 
105
+ # ── Tab builder ───────────────────────────────────────────────────────────────
 
 
 
 
106
 
107
+ def _build_tab(spec):
108
+ status_md = gr.Markdown(_STATUS_CLEAR, visible=True)
 
 
 
 
 
109
 
110
+ with gr.Row(equal_height=False):
111
+ with gr.Column(scale=1):
112
+ image_comp = gr.Image(
113
+ label="Upload image",
114
+ type="pil",
115
+ sources=["upload", "webcam", "clipboard"],
116
+ height=300,
117
+ )
 
 
 
 
118
 
119
+ extra_comps = []
120
 
121
+ if spec.input_spec.text_label:
122
+ extra_comps.append(gr.Textbox(
123
+ label=spec.input_spec.text_label,
124
+ placeholder=spec.input_spec.text_placeholder,
125
+ lines=2,
126
+ ))
 
127
 
128
+ if spec.input_spec.dropdown_choices:
129
+ extra_comps.append(gr.Dropdown(
130
+ choices=spec.input_spec.dropdown_choices,
131
+ value=spec.input_spec.dropdown_default,
132
+ label=spec.input_spec.dropdown_label,
133
+ ))
 
134
 
135
+ submit = gr.Button("Analyze ✦", variant="primary", elem_classes=["submit-btn"])
136
 
137
+ with gr.Column(scale=1):
138
+ output_comps = _build_output_components(spec.output_components)
 
139
 
140
+ all_inputs = [image_comp] + extra_comps
141
+ all_outputs = [status_md] + output_comps
142
 
143
+ submit.click(fn=_make_run(spec), inputs=all_inputs, outputs=all_outputs)
144
 
145
+ placeholder = (
146
+ '<div style="min-height:180px;display:flex;align-items:center;'
147
+ 'justify-content:center;color:#aaa;font-family:system-ui">'
148
+ 'Click Analyze ✦ to process</div>'
149
+ )
 
 
 
 
 
 
 
150
 
151
+ def _on_image_change(*_):
152
+ return [gr.update(value=_STATUS_CLEAR)] + [
153
+ gr.update(value=placeholder) for _ in spec.output_components
 
154
  ]
 
 
 
 
 
 
 
155
 
156
+ image_comp.change(fn=_on_image_change, inputs=[image_comp], outputs=all_outputs)
157
+
158
+ valid_examples = [
159
+ row for row in spec.examples
160
+ if row and Path(str(row[0])).exists()
161
+ ]
162
+ if valid_examples:
163
+ gr.Examples(
164
+ examples=valid_examples,
165
+ inputs=all_inputs,
166
+ label="Try an example",
167
+ cache_examples=False,
168
  )
169
 
 
 
170
 
171
+ # ── Main demo ─────────────────────────────────────────────────────────────────
172
+
173
+ css = """
174
+ .submit-btn { font-size: 16px !important; padding: 12px 0 !important; }
175
+ footer { display: none !important; }
176
+ """
177
+
178
+ with gr.Blocks(title="Vision Base", css=css, theme=gr.themes.Soft()) as demo:
179
+ gr.HTML(
180
+ '<div style="padding:16px 0 8px">'
181
+ '<h1 style="margin:0;font-size:28px">πŸ‘οΈ Vision Base</h1>'
182
+ '<p style="margin:4px 0 0;color:#555;font-size:15px">'
183
+ 'Four vision tools powered by MiniCPM-V 4.6 Β· ZeroGPU'
184
+ '</p></div>'
185
+ )
186
+
187
+ with gr.Tabs():
188
+ for spec in APP_REGISTRY.values():
189
+ with gr.Tab(spec.title):
190
+ gr.Markdown(f"*{spec.tagline}*")
191
+ _build_tab(spec)
192
+
193
+ gr.HTML(
194
+ '<div style="text-align:center;color:#aaa;font-size:12px;margin-top:16px">'
195
+ 'Powered by <b>MiniCPM-V 4.6 (1.3B)</b> Β· ZeroGPU'
196
+ '</div>'
197
+ )
198
 
199
  if __name__ == "__main__":
200
  demo.launch(show_error=True)