DYDYLAN commited on
Commit
1a92a4c
ยท
verified ยท
1 Parent(s): b547608

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -145
app.py CHANGED
@@ -1,34 +1,21 @@
1
  import os
2
- import re
 
 
3
  import gradio as gr
4
- from transformers import pipeline
5
- import torch
6
  from openai import OpenAI
7
- from PIL import Image
8
  from pypdf import PdfReader
9
 
10
-
11
- # =========================
12
- # 1. Device & local vision model
13
- # =========================
14
-
15
- device = "cuda" if torch.cuda.is_available() else "cpu"
16
-
17
- # local image->text (fast rough caption)
18
- vision_pipe = pipeline(
19
- "image-to-text",
20
- model="nlpconnect/vit-gpt2-image-captioning",
21
- device=0 if device == "cuda" else -1
22
- )
23
-
24
  # =========================
25
- # 2. Yunwu / OpenAI-compatible client
26
  # =========================
27
 
28
  YUNWU_API_KEY = os.environ.get("YUNWU_API_KEY")
29
  if not YUNWU_API_KEY:
30
  raise RuntimeError(
31
- "YUNWU_API_KEY not set. Add it in HF Space Settings โ†’ Variables and secrets."
 
32
  )
33
 
34
  client = OpenAI(
@@ -36,39 +23,64 @@ client = OpenAI(
36
  base_url="https://yunwu.ai/v1"
37
  )
38
 
39
- # =========================
40
- # 3. Per-step model routing (edit here)
41
- # =========================
42
- MODEL_STEP1 = "gpt-5.1" # strongest reasoning for paper+figure meaning
43
- MODEL_STEP2 = "deepseek-chat" # cheap/fast for annotation suggestions
44
- MODEL_STEP3B = "gpt-4o" # good fluency for plain-language explanation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
 
 
 
 
 
 
 
 
46
 
47
- def call_llm(prompt: str,
48
- model: str,
49
- temperature: float = 0.2,
50
- max_tokens: int = 512) -> str:
51
  resp = client.chat.completions.create(
52
  model=model,
53
  messages=[
 
 
54
  {
55
- "role": "system",
56
- "content": (
57
- "You are a helpful academic assistant. "
58
- "Be accurate, concrete, and avoid hallucinating numbers. "
59
- "Do not use markdown unless asked."
60
- ),
61
- },
62
- {"role": "user", "content": prompt},
63
  ],
64
  temperature=temperature,
65
  max_tokens=max_tokens,
 
66
  )
67
  return resp.choices[0].message.content.strip()
68
 
69
-
70
  # =========================
71
- # 4. Utilities: PDF context + output cleaning
72
  # =========================
73
 
74
  def extract_pdf_snippet(pdf_path: str | None, max_chars: int = 2500) -> str:
@@ -87,177 +99,254 @@ def extract_pdf_snippet(pdf_path: str | None, max_chars: int = 2500) -> str:
87
  except Exception:
88
  return ""
89
 
 
 
 
90
 
91
- def clean_text(text: str) -> str:
92
  """
93
- Remove markdown-ish / AI-ish formatting and obvious repetition.
 
94
  """
95
- if not text:
96
- return text
97
-
98
- # remove bold/italic markers
99
- text = text.replace("**", "").replace("__", "")
100
-
101
- # remove leading markdown bullets like "- ", "* ", "โ€ข "
102
- text = re.sub(r"(?m)^\s*[-โ€ข*]\s+", "", text)
103
-
104
- # collapse repeated whitespace
105
- text = re.sub(r"\n{3,}", "\n\n", text)
106
- text = re.sub(r"[ \t]{2,}", " ", text)
107
-
108
- return text.strip()
109
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  # =========================
112
- # 5. Main workflow
113
  # =========================
114
 
115
  def run_workflow(image, keywords, style, pdf_path):
116
  if image is None:
117
- return "", "", ""
118
 
119
- # Step 0: rough vision caption
120
- vision_raw = vision_pipe(image)[0]["generated_text"]
121
-
122
- # contexts
123
- kw_text = keywords.strip() if keywords else ""
124
  pdf_context = extract_pdf_snippet(pdf_path)
125
 
126
- # style control for Step1 only
127
- if style == "formal":
128
- style_instruction = (
129
- "Write in formal academic English suitable for a Results/Figure explanation."
130
- )
131
- elif style == "fluency":
132
- style_instruction = (
133
- "Write in smooth, natural academic English, clear and readable."
134
- )
135
- else: # simple
136
- style_instruction = (
137
- "Write clearly with simpler wording, but still accurate."
138
- )
139
-
140
- # -------- Step 1: Explanation of what the figure shows (paper+figure) --------
141
  step1_prompt = f"""
142
- You are given a scientific figure and some related paper text.
143
 
144
- Rough vision caption of the image:
145
- \"\"\"{vision_raw}\"\"\"
 
 
 
146
 
147
  Figure keywords:
148
  \"\"\"{kw_text}\"\"\"
149
 
150
- Relevant paper context (may be partial/noisy):
151
  \"\"\"{pdf_context}\"\"\"
152
 
153
- Task:
154
- - Explain what this figure means in 6โ€“8 sentences.
155
- - Say what is compared on the x-axis (or panels) and what the y-axis measures.
156
- - Describe the main trends and the scientific takeaway.
157
- - If the paper context implies a specific mechanism, mention it briefly.
158
- - Do NOT invent exact numbers, statistics, or p-values.
159
- - Do NOT describe unrelated parts of the paper.
160
-
161
- {style_instruction}
162
- Return plain text only.
163
  """
164
- step1_text = call_llm(step1_prompt, model=MODEL_STEP1, max_tokens=520)
165
- step1_text = clean_text(step1_text)
 
 
166
 
167
- # -------- Step 2: Suggestions for annotating the figure (less AI-ish) --------
168
  step2_prompt = f"""
169
- You are helping someone improve a scientific figure for a presentation.
170
-
171
- Figure meaning (for context):
172
- \"\"\"{step1_text}\"\"\"
173
-
174
- Task:
175
- Give 5โ€“7 practical, specific suggestions for how to annotate this figure directly on the image
176
- (e.g., labels, arrows, callouts, highlights), so the key message is immediately obvious.
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  Rules:
179
- - Write like a human TA giving advice.
180
- - Avoid fancy formatting, no markdown, no bold, no bullet symbols like "-" or "โ€ข".
181
- - Each suggestion should be one short sentence.
182
- - Do not invent values; only suggest how to visually emphasize real trends.
 
 
183
 
184
- Return plain text, one suggestion per line.
 
185
  """
186
- step2_text = call_llm(step2_prompt, model=MODEL_STEP2, temperature=0.3, max_tokens=260)
187
- step2_text = clean_text(step2_text)
 
 
 
188
 
189
- # -------- Step 3B: Plain-language explanation (replaces old paraphrase) --------
190
- step3b_prompt = f"""
191
- Explain this same figure to a smart high-school or first-year university student.
192
 
193
- Figure meaning:
194
- \"\"\"{step1_text}\"\"\"
 
 
 
 
 
 
 
 
 
 
 
195
 
196
  Rules:
197
- - 4โ€“6 sentences.
198
- - Use simple, conversational English.
199
- - Keep the science correct but avoid jargon unless necessary.
200
- - No numbers or stats unless they were explicitly in the meaning above.
201
 
202
- Return plain text only.
 
203
  """
204
- step3b_text = call_llm(step3b_prompt, model=MODEL_STEP3B, temperature=0.4, max_tokens=240)
205
- step3b_text = clean_text(step3b_text)
206
-
207
- return step1_text, step2_text, step3b_text
208
 
 
209
 
210
  # =========================
211
- # 6. Gradio UI
212
  # =========================
213
 
214
  with gr.Blocks() as demo:
215
- gr.Markdown("## ChartSmith โ€“ AI Figure Explainer (multi-model workflow)")
216
 
217
  with gr.Row():
218
  with gr.Column():
219
  img_in = gr.Image(type="pil", label="Upload your scientific figure (screenshot is fine)")
220
-
221
  keywords = gr.Textbox(
222
  label="Figure keywords / variables (English, comma-separated)",
223
  placeholder="bleaching, coral, temperature, CO2"
224
  )
225
-
226
  style = gr.Radio(
227
  ["formal", "fluency", "simple"],
228
- value="formal",
229
- label="Explanation style (for Step 1)"
230
  )
231
-
232
  pdf_in = gr.File(
233
- label="Upload the paper PDF (for context, optional)",
234
  type="filepath"
235
  )
236
-
237
  run_btn = gr.Button("Run workflow", variant="primary")
238
 
239
  with gr.Column():
240
  step1_box = gr.Textbox(
241
- label="Step 1: Explanation of what the figure shows (paper-aware)",
242
- lines=10
243
- )
244
-
245
- step2_box = gr.Textbox(
246
- label="Step 2: Suggestions for annotating the figure",
247
  lines=8
248
  )
249
-
250
- step3b_box = gr.Textbox(
251
- label="Step 3: Plain-language explanation (for class/presentation)",
 
 
 
 
 
252
  lines=6
253
  )
254
 
255
  run_btn.click(
256
  run_workflow,
257
  inputs=[img_in, keywords, style, pdf_in],
258
- outputs=[step1_box, step2_box, step3b_box],
259
  )
260
 
261
-
262
  if __name__ == "__main__":
263
  demo.launch()
 
1
  import os
2
+ import io
3
+ import json
4
+ import base64
5
  import gradio as gr
 
 
6
  from openai import OpenAI
7
+ from PIL import Image, ImageDraw, ImageFont
8
  from pypdf import PdfReader
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  # =========================
11
+ # 1. ไบ‘้›พ / OpenAI-compatible API ่ฎพ็ฝฎ
12
  # =========================
13
 
14
  YUNWU_API_KEY = os.environ.get("YUNWU_API_KEY")
15
  if not YUNWU_API_KEY:
16
  raise RuntimeError(
17
+ "็Žฏๅขƒๅ˜้‡ YUNWU_API_KEY ๆœช่ฎพ็ฝฎใ€‚"
18
+ "่ฏทๅœจ Hugging Face Space ็š„ Settings โ†’ Variables and secrets ไธญๆทปๅŠ ใ€‚"
19
  )
20
 
21
  client = OpenAI(
 
23
  base_url="https://yunwu.ai/v1"
24
  )
25
 
26
+ VISION_MODEL = "gpt-4o" # ่ง†่ง‰ๅผบๆจกๅž‹๏ผˆๅฏๆ›ฟๆขๆˆไบ‘้›พ้‡Œๅฏ็”จ็š„่ง†่ง‰ๆจกๅž‹ๅ๏ผ‰
27
+ TEXT_MODEL = "deepseek-chat" # ็บฏๆ–‡ๆœฌๆจกๅž‹๏ผˆไพฟๅฎœๅฟซ๏ผ‰
28
+
29
+ def call_text_llm(prompt: str, model: str = TEXT_MODEL, temperature=0.2, max_tokens=700):
30
+ resp = client.chat.completions.create(
31
+ model=model,
32
+ messages=[
33
+ {"role": "system",
34
+ "content": "You are a careful academic assistant. Write clean, natural English."},
35
+ {"role": "user", "content": prompt}
36
+ ],
37
+ temperature=temperature,
38
+ max_tokens=max_tokens
39
+ )
40
+ return resp.choices[0].message.content.strip()
41
+
42
+ def pil_to_data_url(img: Image.Image, max_side=1400) -> str:
43
+ img = img.convert("RGB")
44
+ w, h = img.size
45
+ scale = min(1.0, max_side / max(w, h))
46
+ if scale < 1.0:
47
+ img = img.resize((int(w * scale), int(h * scale)))
48
+
49
+ buf = io.BytesIO()
50
+ img.save(buf, format="PNG")
51
+ b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
52
+ return f"data:image/png;base64,{b64}"
53
 
54
+ def call_vision_llm(prompt: str, image: Image.Image, model: str = VISION_MODEL,
55
+ temperature=0.2, max_tokens=700, force_json=False):
56
+ data_url = pil_to_data_url(image)
57
+
58
+ # ๅฐ่ฏ•่ฎฉๆจกๅž‹็›ดๆŽฅ่ฟ”ๅ›ž JSON๏ผˆๅฆ‚ๆžœๅ…ผๅฎน response_format๏ผ‰
59
+ kwargs = {}
60
+ if force_json:
61
+ kwargs["response_format"] = {"type": "json_object"}
62
 
 
 
 
 
63
  resp = client.chat.completions.create(
64
  model=model,
65
  messages=[
66
+ {"role": "system",
67
+ "content": "You are a careful academic assistant. Read the figure precisely and write natural English."},
68
  {
69
+ "role": "user",
70
+ "content": [
71
+ {"type": "text", "text": prompt},
72
+ {"type": "image_url", "image_url": {"url": data_url}}
73
+ ]
74
+ }
 
 
75
  ],
76
  temperature=temperature,
77
  max_tokens=max_tokens,
78
+ **kwargs
79
  )
80
  return resp.choices[0].message.content.strip()
81
 
 
82
  # =========================
83
+ # 2. PDF ไธŠไธ‹ๆ–‡ๆๅ–
84
  # =========================
85
 
86
  def extract_pdf_snippet(pdf_path: str | None, max_chars: int = 2500) -> str:
 
99
  except Exception:
100
  return ""
101
 
102
+ # =========================
103
+ # 3. Step2 JSON ่งฃๆžไธŽๆธฒๆŸ“
104
+ # =========================
105
 
106
+ def safe_json_parse(text: str):
107
  """
108
+ ๅฐ่ฏ•่งฃๆžๆจกๅž‹่พ“ๅ‡บไธบ JSONใ€‚
109
+ ่‹ฅๆจกๅž‹ๆฒกไธฅๆ ผ่ฟ”ๅ›ž็บฏ JSON๏ผŒๅฐฑไปŽๆ–‡ๆœฌไธญๆˆชๅ–ๆœ€ๅค–ๅฑ‚ {...} ๅ† parseใ€‚
110
  """
111
+ try:
112
+ return json.loads(text)
113
+ except Exception:
114
+ # ็ฒ—ๆšดๆˆชๅ–็ฌฌไธ€ไธช { ๅˆฐๆœ€ๅŽไธ€ไธช }
115
+ start = text.find("{")
116
+ end = text.rfind("}")
117
+ if start != -1 and end != -1 and end > start:
118
+ try:
119
+ return json.loads(text[start:end+1])
120
+ except Exception:
121
+ return None
122
+ return None
123
+
124
+ def load_font(size=18):
125
+ # HF ้€šๅธธๆœ‰ DejaVuSans
126
+ for path in [
127
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
128
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
129
+ ]:
130
+ try:
131
+ return ImageFont.truetype(path, size=size)
132
+ except Exception:
133
+ continue
134
+ return ImageFont.load_default()
135
+
136
+ def draw_arrow(draw, x1, y1, x2, y2, color="red", width=3):
137
+ # ็”ป็บฟ
138
+ draw.line([(x1, y1), (x2, y2)], fill=color, width=width)
139
+ # ็ฎ€ๅ•็ฎญๅคดไธ‰่ง’
140
+ import math
141
+ angle = math.atan2(y2 - y1, x2 - x1)
142
+ head_len = 12
143
+ head_angle = math.pi / 7
144
+ p1 = (x2 - head_len * math.cos(angle - head_angle),
145
+ y2 - head_len * math.sin(angle - head_angle))
146
+ p2 = (x2 - head_len * math.cos(angle + head_angle),
147
+ y2 - head_len * math.sin(angle + head_angle))
148
+ draw.polygon([ (x2, y2), p1, p2 ], fill=color)
149
+
150
+ def render_annotations(image: Image.Image, ann_json: dict):
151
+ """
152
+ ann_json schema:
153
+ {
154
+ "annotations": [
155
+ {"type":"text","text":"...","x":0.5,"y":0.1,"size":18,"color":"red"},
156
+ {"type":"box","xy":[x1,y1,x2,y2],"outline":"red","width":3},
157
+ {"type":"arrow","from":[x1,y1],"to":[x2,y2],"color":"red","width":3}
158
+ ]
159
+ }
160
+ Coords normalized 0-1.
161
+ """
162
+ if not ann_json or "annotations" not in ann_json:
163
+ return image
164
+
165
+ img = image.convert("RGB").copy()
166
+ draw = ImageDraw.Draw(img)
167
+ W, H = img.size
168
+
169
+ for ann in ann_json["annotations"]:
170
+ a_type = ann.get("type", "").lower()
171
+
172
+ if a_type == "text":
173
+ x = ann.get("x", 0.5) * W
174
+ y = ann.get("y", 0.5) * H
175
+ txt = ann.get("text", "")
176
+ color = ann.get("color", "red")
177
+ size = int(ann.get("size", 18))
178
+ font = load_font(size=size)
179
+ # text stroke for visibility
180
+ draw.text((x, y), txt, fill=color, font=font, stroke_width=2, stroke_fill="white")
181
+
182
+ elif a_type == "box":
183
+ xy = ann.get("xy", [0.1,0.1,0.3,0.3])
184
+ x1, y1, x2, y2 = xy
185
+ x1, y1, x2, y2 = x1*W, y1*H, x2*W, y2*H
186
+ outline = ann.get("outline", "red")
187
+ width = int(ann.get("width", 3))
188
+ draw.rectangle([x1,y1,x2,y2], outline=outline, width=width)
189
+
190
+ elif a_type == "arrow":
191
+ f = ann.get("from", [0.2,0.2])
192
+ t = ann.get("to", [0.4,0.4])
193
+ x1, y1 = f[0]*W, f[1]*H
194
+ x2, y2 = t[0]*W, t[1]*H
195
+ color = ann.get("color", "red")
196
+ width = int(ann.get("width", 3))
197
+ draw_arrow(draw, x1,y1,x2,y2, color=color, width=width)
198
+
199
+ return img
200
 
201
  # =========================
202
+ # 4. ไธป workflow
203
  # =========================
204
 
205
  def run_workflow(image, keywords, style, pdf_path):
206
  if image is None:
207
+ return "", {}, None, ""
208
 
209
+ kw_text = (keywords or "").strip()
 
 
 
 
210
  pdf_context = extract_pdf_snippet(pdf_path)
211
 
212
+ # -------- Step1: ่ฎบๆ–‡ๅผ่งฃ้‡Š ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  step1_prompt = f"""
214
+ You are given a scientific figure from a paper.
215
 
216
+ Tasks:
217
+ 1) Identify what kind of figure it is (bar chart, line plot, conceptual diagram, etc.).
218
+ 2) Describe what the x-axis / panels represent and what the y-axis represents in general terms.
219
+ 3) State the main pattern or comparison you observe.
220
+ 4) Use the paper context to stay on-topic, but do NOT hallucinate exact numbers or p-values.
221
 
222
  Figure keywords:
223
  \"\"\"{kw_text}\"\"\"
224
 
225
+ Paper context (may be incomplete/noisy):
226
  \"\"\"{pdf_context}\"\"\"
227
 
228
+ Write one compact Results-style paragraph (4โ€“6 sentences). Natural academic English.
 
 
 
 
 
 
 
 
 
229
  """
230
+ try:
231
+ step1_text = call_vision_llm(step1_prompt, image, max_tokens=520)
232
+ except Exception:
233
+ step1_text = call_text_llm(step1_prompt, max_tokens=520)
234
 
235
+ # -------- Step2: ่พ“ๅ‡บ JSON ๆ‰นๆณจ่ฎกๅˆ’ ----------
236
  step2_prompt = f"""
237
+ You will propose how to annotate this figure for a presentation.
238
+
239
+ Return ONLY valid JSON with this schema:
240
+ {{
241
+ "annotations": [
242
+ {{
243
+ "type": "text" | "box" | "arrow",
244
+ "text": "string (only for type=text)",
245
+ "x": 0-1, "y": 0-1 (only for type=text),
246
+ "size": integer (optional, only for type=text),
247
+ "color": "red/blue/green/black/orange" (optional),
248
+
249
+ "xy": [x1,y1,x2,y2] (only for type=box),
250
+ "outline": "color" (optional for box),
251
+ "width": integer (optional for box/arrow),
252
+
253
+ "from": [x1,y1], "to": [x2,y2] (only for type=arrow)
254
+ }}
255
+ ]
256
+ }}
257
 
258
  Rules:
259
+ - Coordinates are normalized (0-1) relative to image width/height.
260
+ - Do not invent variables not implied by the figure/paper.
261
+ - Prefer 4โ€“8 annotations max.
262
+
263
+ Figure keywords:
264
+ \"\"\"{kw_text}\"\"\"
265
 
266
+ Paper context:
267
+ \"\"\"{pdf_context}\"\"\"
268
  """
269
+ raw_step2 = ""
270
+ try:
271
+ raw_step2 = call_vision_llm(step2_prompt, image, max_tokens=380, temperature=0.3, force_json=True)
272
+ except Exception:
273
+ raw_step2 = call_text_llm(step2_prompt, max_tokens=380, temperature=0.3)
274
 
275
+ ann_json = safe_json_parse(raw_step2) or {"annotations": []}
 
 
276
 
277
+ # ๆธฒๆŸ“ๆ‰นๆณจๅ›พ
278
+ annotated_img = render_annotations(image, ann_json)
279
+
280
+ # -------- Step3: ้€šไฟ—่งฃ้‡Š๏ผˆๆŒ‰ style๏ผ‰ ----------
281
+ style_map = {
282
+ "formal": "formal but still clear, like a polished class presentation",
283
+ "fluency": "smooth, natural spoken English for a talk",
284
+ "simple": "very simple, beginner-friendly English"
285
+ }
286
+ style_inst = style_map.get(style, style_map["fluency"])
287
+
288
+ step3_prompt = f"""
289
+ Rewrite the explanation below into {style_inst}.
290
 
291
  Rules:
292
+ - Keep the scientific meaning the same.
293
+ - Avoid sounding like an AI template.
294
+ - 3โ€“5 sentences.
295
+ - No exact numeric values or p-values.
296
 
297
+ Original explanation:
298
+ \"\"\"{step1_text}\"\"\"
299
  """
300
+ step3_text = call_text_llm(step3_prompt, max_tokens=300, temperature=0.4)
 
 
 
301
 
302
+ return step1_text, ann_json, annotated_img, step3_text
303
 
304
  # =========================
305
+ # 5. Gradio UI
306
  # =========================
307
 
308
  with gr.Blocks() as demo:
309
+ gr.Markdown("## ChartSmith โ€“ AI Figure Explainer (JSON annotation + auto-render)")
310
 
311
  with gr.Row():
312
  with gr.Column():
313
  img_in = gr.Image(type="pil", label="Upload your scientific figure (screenshot is fine)")
 
314
  keywords = gr.Textbox(
315
  label="Figure keywords / variables (English, comma-separated)",
316
  placeholder="bleaching, coral, temperature, CO2"
317
  )
 
318
  style = gr.Radio(
319
  ["formal", "fluency", "simple"],
320
+ value="fluency",
321
+ label="Explanation style for Step 3"
322
  )
 
323
  pdf_in = gr.File(
324
+ label="Upload the paper PDF (optional)",
325
  type="filepath"
326
  )
 
327
  run_btn = gr.Button("Run workflow", variant="primary")
328
 
329
  with gr.Column():
330
  step1_box = gr.Textbox(
331
+ label="Step 1: Explanation of what the figure shows (paper-style)",
 
 
 
 
 
332
  lines=8
333
  )
334
+ step2_json = gr.JSON(
335
+ label="Step 2: Annotation plan (JSON)"
336
+ )
337
+ annotated_preview = gr.Image(
338
+ label="Step 2 Rendered: Annotated figure preview"
339
+ )
340
+ step3_box = gr.Textbox(
341
+ label="Step 3: Plain / presentation-friendly explanation",
342
  lines=6
343
  )
344
 
345
  run_btn.click(
346
  run_workflow,
347
  inputs=[img_in, keywords, style, pdf_in],
348
+ outputs=[step1_box, step2_json, annotated_preview, step3_box]
349
  )
350
 
 
351
  if __name__ == "__main__":
352
  demo.launch()