eoeooe commited on
Commit
83c3a8b
·
verified ·
1 Parent(s): 3763ce7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +447 -0
app.py CHANGED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import json
4
+ import os
5
+ from typing import Dict, List, Tuple, Any, Optional
6
+ import time
7
+ import requests
8
+ from PIL import Image
9
+ import gradio as gr
10
+ import re
11
+ import tempfile
12
+ from urllib.parse import urlparse
13
+
14
+ # =========================
15
+ # Config
16
+ # =========================
17
+ DEFAULT_API_URL = os.environ.get("API_URL")
18
+ TOKEN = os.environ.get("TOKEN")
19
+ LOGO_IMAGE_PATH = "./assets/logo.jpg"
20
+ GOOGLE_FONTS_URL = "<link href='https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap' rel='stylesheet'>"
21
+ LATEX_DELIMS = [
22
+ {"left": "$$", "right": "$$", "display": True},
23
+ {"left": "$", "right": "$", "display": False},
24
+ {"left": "\\(", "right": "\\)", "display": False},
25
+ {"left": "\\[", "right": "\\]", "display": True},
26
+ ]
27
+ AUTH_HEADER = {"Authorization": f"bearer {TOKEN}"} if TOKEN else {}
28
+ JSON_HEADERS = {**AUTH_HEADER, "Content-Type": "application/json"} if AUTH_HEADER else {"Content-Type": "application/json"}
29
+ JSON_HEADERS["Client-Platform"] = "huggingface-demo"
30
+
31
+ # =========================
32
+ # Base64 & Examples (URL直链渲染)
33
+ # =========================
34
+ def image_to_base64_data_url(filepath: str) -> str:
35
+ """仅用于本地上传预览的兼容方案;URL 预览不会用到它。"""
36
+ try:
37
+ ext = os.path.splitext(filepath)[1].lower()
38
+ mime_types = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp"}
39
+ mime_type = mime_types.get(ext, "image/jpeg")
40
+ with open(filepath, "rb") as image_file:
41
+ encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
42
+ return f"data:{mime_type};base64,{encoded_string}"
43
+ except Exception as e:
44
+ print(f"Error encoding image to Base64: {e}")
45
+ return ""
46
+
47
+ def _escape_inequalities_in_math(md: str) -> str:
48
+ """把数学块中的 < > 替换为 \\lt \\gt,避免被 Markdown 误解析。"""
49
+ _MATH_PATTERNS = [
50
+ re.compile(r"\$\$([\s\S]+?)\$\$"),
51
+ re.compile(r"\$([^\$]+?)\$"),
52
+ re.compile(r"\\\[([\s\S]+?)\\\]"),
53
+ re.compile(r"\\\(([\s\S]+?)\\\)"),
54
+ ]
55
+
56
+ def fix(s: str) -> str:
57
+ s = s.replace("<=", r" \le ").replace(">=", r" \ge ")
58
+ s = s.replace("≤", r" \le ").replace("≥", r" \ge ")
59
+ s = s.replace("<", r" \lt ").replace(">", r" \gt ")
60
+ return s
61
+
62
+ for pat in _MATH_PATTERNS:
63
+ md = pat.sub(lambda m: m.group(0).replace(m.group(1), fix(m.group(1))), md)
64
+ return md
65
+
66
+ def _get_examples_from_dir(dir_path: str) -> List[List[str]]:
67
+ """
68
+ 从本地目录读取文件名,拼出远程直链 URL(不下载、不转码),用于 <img src="URL"> 直接渲染。
69
+ 你原来使用的 BOS 基础路径保留。
70
+ """
71
+ BASE_URL = "https://paddle-model-ecology.bj.bcebos.com/PPOCRVL/dataset/examples"
72
+ supported_exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
73
+ examples = []
74
+ if not os.path.exists(dir_path):
75
+ print(f"Warning: example dir {dir_path} not found.")
76
+ return []
77
+ for filename in sorted(os.listdir(dir_path)):
78
+ ext = os.path.splitext(filename)[1].lower()
79
+ if ext in supported_exts:
80
+ subdir = os.path.basename(dir_path.rstrip("/"))
81
+ img_url = f"{BASE_URL}/{subdir}/{filename}"
82
+ examples.append([img_url])
83
+ return examples
84
+
85
+ def _on_gallery_select(example_paths: List[str], evt: gr.SelectData):
86
+ """
87
+ 与原版不同:直接返回 URL,不再下载到本地临时文件。
88
+ """
89
+ idx = evt.index
90
+ selected = example_paths[idx]
91
+ if isinstance(selected, list):
92
+ selected = selected[0]
93
+ return selected # 直接是 https://... URL
94
+
95
+ TARGETED_EXAMPLES_DIR = "examples/targeted"
96
+ COMPLEX_EXAMPLES_DIR = "examples/complex"
97
+ targeted_recognition_examples = _get_examples_from_dir(TARGETED_EXAMPLES_DIR)
98
+ complex_document_examples = _get_examples_from_dir(COMPLEX_EXAMPLES_DIR)
99
+
100
+ # =========================
101
+ # UI Helpers(URL直链渲染)
102
+ # =========================
103
+ def render_uploaded_image_div(path_or_url: str) -> str:
104
+ """
105
+ 支持两种输入:
106
+ - 远程 URL:直接用 <img src="URL"> 渲染
107
+ - 本地文件:为兼容旧逻辑,依然转 data: URL 预览(也可以改为 File 组件,这里先保持一致)
108
+ """
109
+ if not path_or_url:
110
+ return ""
111
+ is_url = isinstance(path_or_url, str) and path_or_url.startswith(("http://", "https://"))
112
+ if is_url:
113
+ src = path_or_url # 直接远程URL
114
+ else:
115
+ src = image_to_base64_data_url(path_or_url) # 本地上传时的兼容
116
+ return f"""
117
+ <div class="uploaded-image">
118
+ <img src="{src}" alt="Preview image" style="width:100%;height:100%;object-fit:contain;" loading="lazy"/>
119
+ </div>
120
+ """
121
+
122
+ def update_preview_visibility(path_or_url: Optional[str]) -> Dict:
123
+ if path_or_url:
124
+ html_content = render_uploaded_image_div(path_or_url)
125
+ return gr.update(value=html_content, visible=True)
126
+ else:
127
+ return gr.update(value="", visible=False)
128
+
129
+ # =========================
130
+ # API 调用逻辑(支持URL或本地文件)
131
+ # =========================
132
+ def _file_to_b64_image_only(path_or_url: str) -> Tuple[str, int]:
133
+ """
134
+ 输入可以是本地文件路径或远程URL。
135
+ - URL:仅在发请求给后端时下载字节转Base64(不影响前端渲染)。
136
+ - 本地:读取文件字节。
137
+ """
138
+ if not path_or_url:
139
+ raise ValueError("Please upload an image first.")
140
+
141
+ is_url = isinstance(path_or_url, str) and path_or_url.startswith(("http://", "https://"))
142
+ content: bytes
143
+ if is_url:
144
+ r = requests.get(path_or_url, timeout=600)
145
+ r.raise_for_status()
146
+ content = r.content
147
+ ext = os.path.splitext(urlparse(path_or_url).path)[1].lower()
148
+ else:
149
+ ext = os.path.splitext(path_or_url)[1].lower()
150
+ with open(path_or_url, "rb") as f:
151
+ content = f.read()
152
+
153
+ # 放宽后缀限制:有些URL可能没有后缀,这里仅在极端情况下提示
154
+ supported = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
155
+ if ext and (ext not in supported):
156
+ print(f"Warning: file extension {ext} not in supported set {supported}, continue anyway.")
157
+
158
+ return base64.b64encode(content).decode("utf-8"), 1 # 1 = image 类型
159
+
160
+ def _call_api(api_url: str, path_or_url: str, use_layout_detection: bool,
161
+ prompt_label: Optional[str], use_chart_recognition: bool = False,
162
+ use_doc_unwarping: bool = True, use_doc_orientation_classify: bool = True) -> Dict[str, Any]:
163
+ b64, file_type = _file_to_b64_image_only(path_or_url)
164
+ payload = {
165
+ "file": b64,
166
+ "useLayoutDetection": bool(use_layout_detection),
167
+ "fileType": file_type,
168
+ "useDocUnwarping": use_doc_unwarping,
169
+ "useDocOrientationClassify": use_doc_orientation_classify
170
+ }
171
+ if not use_layout_detection:
172
+ if not prompt_label:
173
+ raise ValueError("Please select a recognition type.")
174
+ payload["promptLabel"] = prompt_label.strip().lower()
175
+ if use_layout_detection and use_chart_recognition:
176
+ payload["useChartRecognition"] = True
177
+
178
+ try:
179
+ print(f"Sending API request to {api_url}...")
180
+ start_time = time.time()
181
+ resp = requests.post(api_url, json=payload, headers=JSON_HEADERS, timeout=600)
182
+ end_time = time.time()
183
+ print(f"Received API response in {end_time - start_time:.2f} seconds.")
184
+ resp.raise_for_status()
185
+ data = resp.json()
186
+ except requests.exceptions.RequestException as e:
187
+ raise gr.Error(f"API request failed: {e}")
188
+ except json.JSONDecodeError:
189
+ raise gr.Error(f"Invalid JSON response from server:\n{getattr(resp, 'text', '')}")
190
+
191
+ if data.get("errorCode", -1) != 0:
192
+ raise gr.Error("API returned an error:")
193
+ return data
194
+
195
+ def _process_api_response_page(result: Dict[str, Any]) -> Tuple[str, str, str]:
196
+ """
197
+ 处理后端返回结果:
198
+ 1) 把 markdown 里的占位图路径替换为真实URL
199
+ 2) 构造一个可视化<img>(如果有)
200
+ """
201
+ layout_results = (result or {}).get("layoutParsingResults", [])
202
+ if not layout_results:
203
+ return "No content was recognized.", "<p>No visualization available.</p>", ""
204
+
205
+ page0 = layout_results[0] or {}
206
+ md_data = page0.get("markdown") or {}
207
+ md_text = md_data.get("text", "") or ""
208
+ md_images_map = md_data.get("images", {})
209
+
210
+ if md_images_map:
211
+ for placeholder_path, image_url in md_images_map.items():
212
+ md_text = md_text.replace(f'src="{placeholder_path}"', f'src="{image_url}"') \
213
+ .replace(f']({placeholder_path})', f']({image_url})')
214
+
215
+ output_html = "<p style='text-align:center; color:#888;'>No visualization image available.</p>"
216
+ out_imgs = page0.get("outputImages") or {}
217
+ sorted_urls = [img_url for _, img_url in sorted(out_imgs.items()) if img_url]
218
+
219
+ output_image_url: Optional[str] = None
220
+ if len(sorted_urls) >= 2:
221
+ output_image_url = sorted_urls[1]
222
+ elif sorted_urls:
223
+ output_image_url = sorted_urls[0]
224
+
225
+ if output_image_url:
226
+ print(f"Found visualization image URL: {output_image_url}")
227
+ output_html = f'<img src="{output_image_url}" alt="Detection Visualization" loading="lazy">'
228
+
229
+ md_text = _escape_inequalities_in_math(md_text)
230
+ return md_text or "(Empty result)", output_html, md_text
231
+
232
+ def handle_complex_doc(path_or_url: str, use_chart_recognition: bool, use_doc_unwarping: bool, use_doc_orientation_classify: bool) -> Tuple[str, str, str]:
233
+ if not path_or_url:
234
+ raise gr.Error("Please upload an image first.")
235
+ data = _call_api(DEFAULT_API_URL, path_or_url, use_layout_detection=True,
236
+ prompt_label=None, use_chart_recognition=use_chart_recognition,
237
+ use_doc_unwarping=use_doc_unwarping, use_doc_orientation_classify=use_doc_orientation_classify)
238
+ result = data.get("result", {})
239
+ return _process_api_response_page(result)
240
+
241
+ def handle_targeted_recognition(path_or_url: str, prompt_choice: str) -> Tuple[str, str]:
242
+ if not path_or_url:
243
+ raise gr.Error("Please upload an image first.")
244
+ mapping = {
245
+ "Text Recognition": "ocr",
246
+ "Formula Recognition": "formula",
247
+ "Table Recognition": "table",
248
+ "Chart Recognition": "chart",
249
+ }
250
+ label = mapping.get(prompt_choice, "ocr")
251
+ data = _call_api(DEFAULT_API_URL, path_or_url, use_layout_detection=False, prompt_label=label, use_doc_unwarping=False, use_doc_orientation_classify=False)
252
+ result = data.get("result", {})
253
+ md_preview, _, md_raw = _process_api_response_page(result)
254
+ return md_preview, md_raw
255
+
256
+ # =========================
257
+ # CSS & UI
258
+ # =========================
259
+ custom_css = """
260
+ body, .gradio-container { font-family: "Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif; }
261
+ .app-header { text-align: center; max-width: 900px; margin: 0 auto 8px !important; }
262
+ .gradio-container { padding: 4px 0 !important; }
263
+ .gradio-container [data-testid="tabs"], .gradio-container .tabs { margin-top: 0 !important; }
264
+ .gradio-container [data-testid="tabitem"], .gradio-container .tabitem { padding-top: 4px !important; }
265
+ .quick-links { text-align: center; padding: 8px 0; border: 1px solid #e5e7eb; border-radius: 8px; margin: 8px auto; max-width: 900px; }
266
+ .quick-links a { margin: 0 12px; font-size: 14px; font-weight: 600; color: #3b82f6; text-decoration: none; }
267
+ .quick-links a:hover { text-decoration: underline; }
268
+ .prompt-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
269
+ .prompt-grid button { height: 40px !important; padding: 0 12px !important; border-radius: 8px !important; font-weight: 600 !important; font-size: 13px !important; letter-spacing: 0.2px; }
270
+ #image_preview_vl, #image_preview_doc { height: 400px !important; overflow: auto; }
271
+ #image_preview_vl img, #image_preview_doc img, #vis_image_doc img { width: 100% !important; height: auto !important; object-fit: contain !important; display: block; }
272
+ #md_preview_vl, #md_preview_doc { max-height: 540px; min-height: 180px; overflow: auto; scrollbar-gutter: stable both-edges; }
273
+ #md_preview_vl .prose, #md_preview_doc .prose { line-height: 1.7 !important; }
274
+ #md_preview_vl .prose img, #md_preview_doc .prose img { display: block; margin: 0 auto; max-width: 100%; height: auto; }
275
+ .notice { margin: 8px auto 0; max-width: 900px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #f8fafc; font-size: 14px; line-height: 1.6; }
276
+ .notice strong { font-weight: 700; }
277
+ .notice a { color: #3b82f6; text-decoration: none; }
278
+ .notice a:hover { text-decoration: underline; }
279
+ .checkbox-row .gradio-checkbox { flex-grow: 1; text-align: center; }
280
+ """
281
+
282
+ with gr.Blocks(head=GOOGLE_FONTS_URL, css=custom_css, theme=gr.themes.Soft()) as demo:
283
+ logo_data_url = image_to_base64_data_url(LOGO_IMAGE_PATH) if os.path.exists(LOGO_IMAGE_PATH) else ""
284
+ gr.HTML(f"""<div class="app-header"><img src="{logo_data_url}" alt="App Logo" style="max-height:10%; width: auto; margin: 10px auto; display: block;"></div>""")
285
+ gr.HTML("""<div class="notice"><strong>Heads up:</strong> The Hugging Face demo can be slow at times. For a faster experience, please try <a href="https://aistudio.baidu.com/application/detail/98365" target="_blank" rel="noopener noreferrer">Baidu AI Studio</a> or <a href="https://modelscope.cn/studios/PaddlePaddle/PaddleOCR-VL_Online_Demo/summary" target="_blank" rel="noopener noreferrer">ModelScope</a>.</div>""")
286
+
287
+ gr.HTML("""<div class="quick-links"><a href="https://github.com/PaddlePaddle/PaddleOCR" target="_blank">GitHub</a> | <a href="https://ernie.baidu.com/blog/publication/PaddleOCR-VL_Technical_Report.pdf" target="_blank">Technical Report</a> | <a href="https://huggingface.co/PaddlePaddle/PaddleOCR-VL" target="_blank">Model</a> | <a href="https://aistudio.baidu.com/paddleocr" target="_blank">Official Website</a></div>""")
288
+
289
+ with gr.Tabs():
290
+ # ===================== Document Parsing =====================
291
+ with gr.Tab("Document Parsing"):
292
+ with gr.Row():
293
+ with gr.Column(scale=5):
294
+ file_doc = gr.File(label="Upload Image", file_count="single", type="filepath", file_types=["image"])
295
+ preview_doc_html = gr.HTML(value="", elem_id="image_preview_doc", visible=False)
296
+ gr.Markdown("_(Use this mode for recognizing full-page documents such as reports, papers, or magazines. **Note: Headers, footers, and page numbers are ignored by default. For full results including these elements, you can obtain the raw JSON via locally deployed PaddleOCR inference.**)_")
297
+ gr.Markdown("💡 *To recognize a single, pre-cropped element (e.g., a table or formula), switch to the 'Element-level Recognition' tab for better results.*")
298
+
299
+ example_url_doc = gr.State(value=None)
300
+
301
+ with gr.Row(variant="panel"):
302
+ with gr.Column(scale=2):
303
+ btn_parse = gr.Button("Parse Document", variant="primary")
304
+ with gr.Column(scale=3):
305
+ with gr.Row(elem_classes=["checkbox-row"]):
306
+ chart_parsing_switch = gr.Checkbox(label="Enable chart parsing", value=False, min_width=10)
307
+ # ############### 修改点在这里 ###############
308
+ doc_unwarping_switch = gr.Checkbox(label="Enable document unwarping", value=False, min_width=10)
309
+ doc_orientation_switch = gr.Checkbox(label="Enable orientation classification", value=False, min_width=10)
310
+
311
+ if complex_document_examples:
312
+ complex_paths = [e[0] for e in complex_document_examples]
313
+ complex_state = gr.State(complex_paths)
314
+
315
+ gallery_complex = gr.Gallery(
316
+ value=complex_paths, columns=4, height=400,
317
+ preview=False, label=None, allow_preview=False
318
+ )
319
+
320
+ def on_gallery_select_for_doc(paths, evt: gr.SelectData):
321
+ idx = evt.index
322
+ if isinstance(idx, (list, tuple)):
323
+ idx = idx[0]
324
+ try:
325
+ url = paths[int(idx)]
326
+ except Exception:
327
+ raise gr.Error(f"Invalid index from gallery: {evt.index}")
328
+
329
+ return url, update_preview_visibility(url)
330
+
331
+ gallery_complex.select(
332
+ fn=on_gallery_select_for_doc,
333
+ inputs=[complex_state],
334
+ outputs=[example_url_doc, preview_doc_html],
335
+ )
336
+
337
+ # ===================== 更新日志模块 =====================
338
+ gr.Markdown("""
339
+ <div class="notice">
340
+ <h3>History Updates</h3>
341
+ <ul>
342
+ <li>
343
+ <strong>Oct 30, 2025:</strong>
344
+ Added two advanced control options under the "Document Parsing" tab. These features were previously enabled by default (set to true) but are now user-configurable and default to false.
345
+ <ul>
346
+ <li><strong>Enable document unwarping:</strong> Corrects distortions in bent or poorly photographed documents.</li>
347
+ <li><strong>Enable orientation classification:</strong> Automatically corrects the orientation of rotated or upside-down images.</li>
348
+ </ul>
349
+ </li>
350
+ <li>
351
+ <strong>Oct 16, 2025:</strong> Initial release of the demo.
352
+ </li>
353
+ </ul>
354
+ </div>
355
+ """)
356
+
357
+ with gr.Column(scale=7):
358
+ with gr.Tabs():
359
+ with gr.Tab("Markdown Preview"):
360
+ md_preview_doc = gr.Markdown("Please upload an image and click 'Parse Document'.", latex_delimiters=LATEX_DELIMS, elem_id="md_preview_doc")
361
+ with gr.Tab("Visualization"):
362
+ vis_image_doc = gr.HTML(label="Detection Visualization", elem_id="vis_image_doc")
363
+ with gr.Tab("Markdown Source"):
364
+ md_raw_doc = gr.Code(label="Markdown Source Code", language="markdown")
365
+
366
+ def on_file_doc_change(fp):
367
+ return None, update_preview_visibility(fp)
368
+
369
+ file_doc.change(fn=on_file_doc_change, inputs=[file_doc], outputs=[example_url_doc, preview_doc_html])
370
+
371
+ def parse_doc_router(fp, example_url, use_chart, use_unwarping, use_orientation):
372
+ src = fp if fp else example_url
373
+ if not src:
374
+ raise gr.Error("Please upload an image or pick an example first.")
375
+ return handle_complex_doc(src, use_chart, use_unwarping, use_orientation)
376
+
377
+ btn_parse.click(fn=parse_doc_router, inputs=[file_doc, example_url_doc, chart_parsing_switch, doc_unwarping_switch, doc_orientation_switch],
378
+ outputs=[md_preview_doc, vis_image_doc, md_raw_doc])
379
+
380
+ # ===================== Element-level Recognition =====================
381
+ with gr.Tab("Element-level Recognition"):
382
+ with gr.Row():
383
+ with gr.Column(scale=5):
384
+ file_vl = gr.File(label="Upload Image", file_count="single", type="filepath", file_types=["image"])
385
+ preview_vl_html = gr.HTML(value="", elem_id="image_preview_vl", visible=False)
386
+ gr.Markdown("_(Best for images with a **simple, single-column layout** (e.g., pure text), or for a **pre-cropped single element** like a table, formula, or chart.)_")
387
+ gr.Markdown("Choose a recognition type:")
388
+
389
+ with gr.Row(elem_classes=["prompt-grid"]):
390
+ btn_ocr = gr.Button("Text Recognition", variant="secondary")
391
+ btn_formula = gr.Button("Formula Recognition", variant="secondary")
392
+ with gr.Row(elem_classes=["prompt-grid"]):
393
+ btn_table = gr.Button("Table Recognition", variant="secondary")
394
+ btn_chart = gr.Button("Chart Recognition", variant="secondary")
395
+
396
+ example_url_vl = gr.State(value=None)
397
+
398
+ if targeted_recognition_examples:
399
+ targeted_paths = [e[0] for e in targeted_recognition_examples]
400
+ targeted_state = gr.State(targeted_paths)
401
+
402
+ gallery_targeted = gr.Gallery(
403
+ value=targeted_paths, columns=4, height=400,
404
+ preview=False, label=None, allow_preview=False
405
+ )
406
+
407
+ def on_gallery_select_for_vl(paths, evt: gr.SelectData):
408
+ idx = evt.index
409
+ if isinstance(idx, (list, tuple)):
410
+ idx = idx[0]
411
+ try:
412
+ url = paths[int(idx)]
413
+ except Exception:
414
+ raise gr.Error(f"Invalid index from gallery: {evt.index}")
415
+ return url, update_preview_visibility(url)
416
+
417
+ gallery_targeted.select(
418
+ fn=on_gallery_select_for_vl,
419
+ inputs=[targeted_state],
420
+ outputs=[example_url_vl, preview_vl_html],
421
+ )
422
+
423
+ with gr.Column(scale=7):
424
+ with gr.Tabs():
425
+ with gr.Tab("Recognition Result"):
426
+ md_preview_vl = gr.Markdown("Please upload an image and click a recognition type.", latex_delimiters=LATEX_DELIMS, elem_id="md_preview_vl")
427
+ with gr.Tab("Raw Output"):
428
+ md_raw_vl = gr.Code(label="Raw Output", language="markdown")
429
+
430
+ def on_file_vl_change(fp):
431
+ return None, update_preview_visibility(fp)
432
+
433
+ file_vl.change(fn=on_file_vl_change, inputs=[file_vl], outputs=[example_url_vl, preview_vl_html])
434
+
435
+ def parse_vl_router(fp, example_url, prompt_choice):
436
+ src = fp if fp else example_url
437
+ if not src:
438
+ raise gr.Error("Please upload an image or pick an example first.")
439
+ return handle_targeted_recognition(src, prompt_choice)
440
+
441
+ btn_ocr.click(fn=parse_vl_router, inputs=[file_vl, example_url_vl, gr.State("Text Recognition")], outputs=[md_preview_vl, md_raw_vl])
442
+ btn_formula.click(fn=parse_vl_router, inputs=[file_vl, example_url_vl, gr.State("Formula Recognition")], outputs=[md_preview_vl, md_raw_vl])
443
+ btn_table.click(fn=parse_vl_router, inputs=[file_vl, example_url_vl, gr.State("Table Recognition")], outputs=[md_preview_vl, md_raw_vl])
444
+ btn_chart.click(fn=parse_vl_router, inputs=[file_vl, example_url_vl, gr.State("Chart Recognition")], outputs=[md_preview_vl, md_raw_vl])
445
+
446
+ if __name__ == "__main__":
447
+ demo.queue(max_size=64).launch()