deegerwalker commited on
Commit
b66a4bf
·
verified ·
1 Parent(s): 6ec1e3e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +169 -734
app.py CHANGED
@@ -1,823 +1,258 @@
1
- import base64
2
  import json
3
  import os
4
- import re
5
- from typing import Any, Dict, List, Optional, Tuple
6
- from urllib.parse import urlparse
7
 
8
  import gradio as gr
9
- import requests
 
 
10
 
11
 
12
  # =========================
13
  # Config
14
  # =========================
15
 
16
- API_URL = os.environ.get("API_URL")
17
- TOKEN = os.environ.get("TOKEN")
18
-
19
- APP_TITLE = os.environ.get("APP_TITLE", "OCR Model Test Lab")
20
  APP_SUBTITLE = os.environ.get(
21
  "APP_SUBTITLE",
22
- "Upload an image and test document parsing, element recognition, and spotting."
23
  )
24
 
25
- LOGO_IMAGE_PATH = os.environ.get("LOGO_IMAGE_PATH", "./assets/logo.jpg")
26
- EXAMPLES_BASE_URL = os.environ.get("EXAMPLES_BASE_URL") or os.environ.get("BOS_URL")
27
-
28
- GOOGLE_FONTS_URL = """
29
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
30
- """
31
 
32
- LATEX_DELIMS = [
33
- {"left": "$$", "right": "$$", "display": True},
34
- {"left": "$", "right": "$", "display": False},
35
- {"left": "\\(", "right": "\\)", "display": False},
36
- {"left": "\\[", "right": "\\]", "display": True},
37
- ]
38
-
39
- AUTH_HEADER = {"Authorization": f"Bearer {TOKEN}"} if TOKEN else {}
40
- JSON_HEADERS = {
41
- **AUTH_HEADER,
42
- "Content-Type": "application/json",
43
- "Client-Platform": "ocr-test-lab",
44
- }
45
 
46
 
47
  # =========================
48
- # Image / Example Helpers
49
  # =========================
50
 
51
- def image_to_base64_data_url(filepath: str) -> str:
52
- try:
53
- ext = os.path.splitext(filepath)[1].lower()
54
- mime_types = {
55
- ".jpg": "image/jpeg",
56
- ".jpeg": "image/jpeg",
57
- ".png": "image/png",
58
- ".gif": "image/gif",
59
- ".webp": "image/webp",
60
- ".bmp": "image/bmp",
61
- }
62
- mime_type = mime_types.get(ext, "image/jpeg")
63
-
64
- with open(filepath, "rb") as image_file:
65
- encoded = base64.b64encode(image_file.read()).decode("utf-8")
66
-
67
- return f"data:{mime_type};base64,{encoded}"
68
-
69
- except Exception as exc:
70
- print(f"Error encoding image to base64: {exc}")
71
- return ""
72
-
73
-
74
- def _get_examples_from_dir(dir_path: str) -> List[List[str]]:
75
- supported_exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
76
- examples = []
77
-
78
- if not os.path.exists(dir_path):
79
- print(f"Example directory not found: {dir_path}")
80
- return []
81
-
82
- for filename in sorted(os.listdir(dir_path)):
83
- ext = os.path.splitext(filename)[1].lower()
84
- if ext not in supported_exts:
85
- continue
86
-
87
- local_path = os.path.join(dir_path, filename)
88
-
89
- if EXAMPLES_BASE_URL:
90
- subdir = os.path.basename(dir_path.rstrip("/"))
91
- examples.append([f"{EXAMPLES_BASE_URL.rstrip('/')}/{subdir}/{filename}"])
92
- else:
93
- examples.append([local_path])
94
-
95
- return examples
96
-
97
-
98
- def render_uploaded_image_div(path_or_url: str) -> str:
99
- if not path_or_url:
100
- return ""
101
-
102
- is_url = isinstance(path_or_url, str) and path_or_url.startswith(("http://", "https://"))
103
- src = path_or_url if is_url else image_to_base64_data_url(path_or_url)
104
-
105
- return f"""
106
- <div class="uploaded-image">
107
- <img
108
- src="{src}"
109
- alt="Preview image"
110
- style="width:100%;height:100%;object-fit:contain;"
111
- loading="lazy"
112
- />
113
- </div>
114
- """
115
-
116
 
117
- def update_preview_visibility(path_or_url: Optional[str]) -> Dict:
118
- if path_or_url:
119
- return gr.update(value=render_uploaded_image_div(path_or_url), visible=True)
120
 
121
- return gr.update(value="", visible=False)
 
122
 
123
 
124
- # =========================
125
- # Markdown / Math Helpers
126
- # =========================
 
 
127
 
128
- def _escape_inequalities_in_math(md: str) -> str:
129
- math_patterns = [
130
- re.compile(r"\$\$([\s\S]+?)\$\$"),
131
- re.compile(r"\$([^\$]+?)\$"),
132
- re.compile(r"\\\[([\s\S]+?)\\\]"),
133
- re.compile(r"\\\(([\s\S]+?)\\\)"),
134
- ]
135
 
136
- def fix_math_symbols(text: str) -> str:
137
- text = text.replace("<=", r" \le ").replace(">=", r" \ge ")
138
- text = text.replace("≤", r" \le ").replace("≥", r" \ge ")
139
- text = text.replace("<", r" \lt ").replace(">", r" \gt ")
140
- return text
141
 
142
- for pattern in math_patterns:
143
- md = pattern.sub(
144
- lambda match: match.group(0).replace(match.group(1), fix_math_symbols(match.group(1))),
145
- md,
146
  )
147
 
148
- return md
149
-
150
-
151
- # =========================
152
- # API Logic
153
- # =========================
154
 
155
- def _file_to_b64_image_only(path_or_url: str) -> Tuple[str, int]:
156
- if not path_or_url:
157
- raise ValueError("Please upload an image first.")
158
 
159
- is_url = isinstance(path_or_url, str) and path_or_url.startswith(("http://", "https://"))
160
 
161
- if is_url:
162
- response = requests.get(path_or_url, timeout=600)
163
- response.raise_for_status()
164
- content = response.content
165
- else:
166
- with open(path_or_url, "rb") as file:
167
- content = file.read()
168
-
169
- return base64.b64encode(content).decode("utf-8"), 1
170
-
171
-
172
- def _call_api(
173
- api_url: str,
174
- path_or_url: str,
175
- use_layout_detection: bool,
176
- prompt_label: Optional[str],
177
- use_chart_recognition: bool = False,
178
- use_doc_unwarping: bool = True,
179
- use_doc_orientation_classify: bool = True,
180
- ) -> Dict[str, Any]:
181
- if not api_url:
182
- raise gr.Error("API_URL is not set. Add it as an environment variable.")
183
-
184
- is_url = isinstance(path_or_url, str) and path_or_url.startswith(("http://", "https://"))
185
-
186
- if is_url:
187
- payload = {
188
- "file": path_or_url,
189
- "matchHistoryJob": False,
190
- "useLayoutDetection": bool(use_layout_detection),
191
- "useDocUnwarping": bool(use_doc_unwarping),
192
- "useDocOrientationClassify": bool(use_doc_orientation_classify),
193
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  else:
195
- b64, file_type = _file_to_b64_image_only(path_or_url)
196
- payload = {
197
- "file": b64,
198
- "fileType": file_type,
199
- "matchHistoryJob": False,
200
- "useLayoutDetection": bool(use_layout_detection),
201
- "useDocUnwarping": bool(use_doc_unwarping),
202
- "useDocOrientationClassify": bool(use_doc_orientation_classify),
203
- }
204
-
205
- if not use_layout_detection:
206
- if not prompt_label:
207
- raise gr.Error("Please select a recognition type.")
208
-
209
- payload["promptLabel"] = prompt_label.strip().lower()
210
-
211
- if use_layout_detection and use_chart_recognition:
212
- payload["useChartRecognition"] = True
213
-
214
- try:
215
- print(f"Sending OCR API request to {api_url}")
216
- print(json.dumps(payload, ensure_ascii=False, default=str)[:2000])
217
-
218
- response = requests.post(
219
- api_url,
220
- json=payload,
221
- headers=JSON_HEADERS,
222
- timeout=600,
223
- )
224
- response.raise_for_status()
225
- data = response.json()
226
-
227
- except Exception as exc:
228
- print(exc)
229
- raise gr.Error(f"API request failed: {exc}")
230
-
231
- if data.get("errorCode", -1) != 0:
232
- message = data.get("errorMsg") or data.get("message") or "API returned an error."
233
- raise gr.Error(message)
234
-
235
- return data
236
 
 
 
237
 
238
- def _process_api_response_page(result: Dict[str, Any]) -> Tuple[str, str, str]:
239
- layout_results = (result or {}).get("layoutParsingResults", [])
240
 
241
- if not layout_results:
242
- return "No content was recognized.", "<p>No visualization available.</p>", ""
 
 
 
243
 
244
- page0 = layout_results[0] or {}
 
245
 
246
- md_data = page0.get("markdown") or {}
247
- md_text = md_data.get("text", "") or ""
248
- md_images_map = md_data.get("images", {}) or {}
249
-
250
- for placeholder_path, image_url in md_images_map.items():
251
- md_text = (
252
- md_text
253
- .replace(f'src="{placeholder_path}"', f'src="{image_url}"')
254
- .replace(f"]({placeholder_path})", f"]({image_url})")
255
- )
256
-
257
- output_html = "<p class='empty-state'>No visualization image available.</p>"
258
- output_images = page0.get("outputImages") or {}
259
-
260
- sorted_urls = [image_url for _, image_url in sorted(output_images.items()) if image_url]
261
-
262
- output_image_url: Optional[str] = None
263
- if len(sorted_urls) >= 2:
264
- output_image_url = sorted_urls[1]
265
- elif sorted_urls:
266
- output_image_url = sorted_urls[0]
267
-
268
- if output_image_url:
269
- output_html = f"""
270
- <img
271
- src="{output_image_url}"
272
- alt="Detection visualization"
273
- loading="lazy"
274
- />
275
- """
276
-
277
- md_text = _escape_inequalities_in_math(md_text)
278
-
279
- return md_text or "(Empty result)", output_html, md_text
280
-
281
-
282
- def handle_complex_doc(
283
- path_or_url: str,
284
- use_chart_recognition: bool,
285
- use_doc_unwarping: bool,
286
- use_doc_orientation_classify: bool,
287
- ) -> Tuple[str, str, str]:
288
- if not path_or_url:
289
- raise gr.Error("Please upload an image first.")
290
-
291
- data = _call_api(
292
- API_URL,
293
- path_or_url,
294
- use_layout_detection=True,
295
- prompt_label=None,
296
- use_chart_recognition=use_chart_recognition,
297
- use_doc_unwarping=use_doc_unwarping,
298
- use_doc_orientation_classify=use_doc_orientation_classify,
299
- )
300
-
301
- return _process_api_response_page(data.get("result", {}))
302
-
303
-
304
- def handle_targeted_recognition(path_or_url: str, prompt_choice: str) -> Tuple[str, str, str]:
305
- if not path_or_url:
306
- raise gr.Error("Please upload an image first.")
307
-
308
- mapping = {
309
- "Text Recognition": "ocr",
310
- "Formula Recognition": "formula",
311
- "Table Recognition": "table",
312
- "Chart Recognition": "chart",
313
- "Spotting": "spotting",
314
- "Seal Recognition": "seal",
315
- }
316
-
317
- label = mapping.get(prompt_choice, "ocr")
318
-
319
- data = _call_api(
320
- API_URL,
321
- path_or_url,
322
- use_layout_detection=False,
323
- prompt_label=label,
324
- use_doc_unwarping=False,
325
- use_doc_orientation_classify=False,
326
- )
327
-
328
- result = data.get("result", {})
329
-
330
- md_preview, _, md_raw = _process_api_response_page(result)
331
- vis_html = "<p class='empty-state'>No visualization available.</p>"
332
-
333
- if label == "spotting":
334
- page0 = (result.get("layoutParsingResults") or [{}])[0] or {}
335
- pruned = page0.get("prunedResult") or {}
336
- spotting_res = pruned.get("spotting_res") or {}
337
-
338
- json_raw = json.dumps(spotting_res, ensure_ascii=False, indent=2)
339
-
340
- output_images = page0.get("outputImages") or {}
341
- spotting_image_url = output_images.get("spotting_res_img")
342
-
343
- if spotting_image_url:
344
- vis_html = f"""
345
- <img
346
- src="{spotting_image_url}"
347
- alt="Spotting visualization"
348
- loading="lazy"
349
- />
350
- """
351
-
352
- return md_preview, json_raw, vis_html
353
-
354
- return md_preview, md_raw, vis_html
355
 
356
 
357
  # =========================
358
- # Examples
359
- # =========================
360
-
361
- TARGETED_EXAMPLES_DIR = "examples/targeted"
362
- COMPLEX_EXAMPLES_DIR = "examples/complex"
363
- SPOTTING_EXAMPLES_DIR = "examples/spotting"
364
-
365
- targeted_recognition_examples = _get_examples_from_dir(TARGETED_EXAMPLES_DIR)
366
- complex_document_examples = _get_examples_from_dir(COMPLEX_EXAMPLES_DIR)
367
- spotting_recognition_examples = _get_examples_from_dir(SPOTTING_EXAMPLES_DIR)
368
-
369
-
370
- # =========================
371
- # CSS
372
  # =========================
373
 
374
  custom_css = """
375
  body,
376
  .gradio-container {
377
- font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
378
- }
379
-
380
- .gradio-container {
381
- padding: 8px 0 !important;
382
  }
383
 
384
  .app-header {
385
  text-align: center;
386
- max-width: 960px;
387
- margin: 0 auto 12px !important;
388
- }
389
-
390
- .app-logo {
391
- max-height: 72px;
392
- width: auto;
393
- margin: 8px auto 12px;
394
- display: block;
395
- border-radius: 14px;
396
  }
397
 
398
  .app-title {
399
- font-size: 30px;
400
  font-weight: 800;
401
- letter-spacing: -0.03em;
402
- margin: 4px 0;
403
  }
404
 
405
  .app-subtitle {
406
  color: #64748b;
407
  font-size: 15px;
408
- margin: 0 auto;
409
- max-width: 760px;
410
  line-height: 1.5;
 
 
 
 
 
 
411
  }
412
 
413
  .notice {
414
- margin: 10px auto 12px;
415
- max-width: 960px;
416
  padding: 12px 14px;
417
  border: 1px solid #e5e7eb;
418
  border-radius: 12px;
419
  background: #f8fafc;
420
- font-size: 14px;
421
- line-height: 1.6;
422
  color: #334155;
423
- }
424
-
425
- .prompt-grid {
426
- display: flex;
427
- flex-wrap: wrap;
428
- gap: 8px;
429
- margin-top: 6px;
430
- }
431
-
432
- .prompt-grid button {
433
- height: 40px !important;
434
- padding: 0 12px !important;
435
- border-radius: 9px !important;
436
- font-weight: 700 !important;
437
- font-size: 13px !important;
438
- }
439
-
440
- .checkbox-row .gradio-checkbox {
441
- flex-grow: 1;
442
- text-align: center;
443
- }
444
-
445
- #image_preview_vl,
446
- #image_preview_doc,
447
- #image_preview_spot {
448
- height: 400px !important;
449
- overflow: auto;
450
- }
451
-
452
- #image_preview_vl img,
453
- #image_preview_doc img,
454
- #image_preview_spot img,
455
- #vis_image_doc img,
456
- #vis_image_spot img {
457
- width: 100% !important;
458
- height: auto !important;
459
- object-fit: contain !important;
460
- display: block;
461
- }
462
-
463
- #md_preview_vl,
464
- #md_preview_doc {
465
- max-height: 540px;
466
- min-height: 180px;
467
- overflow: auto;
468
- scrollbar-gutter: stable both-edges;
469
- }
470
-
471
- #md_preview_vl .prose,
472
- #md_preview_doc .prose {
473
- line-height: 1.7 !important;
474
- }
475
-
476
- #md_preview_vl .prose img,
477
- #md_preview_doc .prose img {
478
- display: block;
479
- margin: 0 auto;
480
- max-width: 100%;
481
- height: auto;
482
- }
483
-
484
- .empty-state {
485
- text-align: center;
486
- color: #94a3b8;
487
- padding: 24px;
488
  }
489
  """
490
 
491
-
492
- # =========================
493
- # UI
494
- # =========================
495
-
496
- with gr.Blocks(
497
- head=GOOGLE_FONTS_URL,
498
- css=custom_css,
499
- theme=gr.themes.Soft(),
500
- title=APP_TITLE,
501
- ) as demo:
502
- logo_data_url = (
503
- image_to_base64_data_url(LOGO_IMAGE_PATH)
504
- if os.path.exists(LOGO_IMAGE_PATH)
505
- else ""
506
- )
507
-
508
- logo_html = (
509
- f'<img src="{logo_data_url}" alt="App logo" class="app-logo" />'
510
- if logo_data_url
511
- else ""
512
- )
513
-
514
  gr.HTML(
515
  f"""
516
  <div class="app-header">
517
- {logo_html}
518
  <div class="app-title">{APP_TITLE}</div>
519
  <div class="app-subtitle">{APP_SUBTITLE}</div>
520
  </div>
521
  """
522
  )
523
 
524
- if not API_URL:
525
- gr.HTML(
526
- """
527
- <div class="notice">
528
- <strong>Setup needed:</strong> API_URL is not currently set.
529
- Add it as an environment variable before running OCR requests.
530
- </div>
531
- """
532
- )
533
- else:
534
- gr.HTML(
535
- """
536
- <div class="notice">
537
- <strong>Testing note:</strong> Large images and complex documents can take a little while.
538
- For best results, start with a clean screenshot or scan.
539
- </div>
540
- """
541
- )
542
-
543
- with gr.Tabs():
544
- # =====================
545
- # Tab 1: Document Parsing
546
- # =====================
547
-
548
- with gr.Tab("Document Parsing"):
549
- with gr.Row():
550
- with gr.Column(scale=5):
551
- file_doc = gr.File(
552
- label="Upload Image",
553
- file_count="single",
554
- type="filepath",
555
- file_types=["image"],
556
- )
557
-
558
- preview_doc_html = gr.HTML(
559
- value="",
560
- elem_id="image_preview_doc",
561
- visible=False,
562
- )
563
-
564
- gr.Markdown("Use this mode for full-page documents.")
565
-
566
- example_url_doc = gr.State(value=None)
567
-
568
- with gr.Row(variant="panel"):
569
- with gr.Column(scale=2):
570
- btn_parse = gr.Button("Parse Document", variant="primary")
571
-
572
- with gr.Column(scale=3):
573
- with gr.Row(elem_classes=["checkbox-row"]):
574
- chart_switch = gr.Checkbox(label="Chart parsing", value=False)
575
- unwarp_switch = gr.Checkbox(label="Doc unwarping", value=False)
576
- orient_switch = gr.Checkbox(label="Orientation", value=False)
577
-
578
- if complex_document_examples:
579
- complex_paths = [example[0] for example in complex_document_examples]
580
- complex_state = gr.State(complex_paths)
581
-
582
- gallery_complex = gr.Gallery(
583
- value=complex_paths,
584
- columns=4,
585
- height=400,
586
- preview=False,
587
- label="Examples",
588
- allow_preview=False,
589
- )
590
-
591
- def on_gallery_doc(paths, evt: gr.SelectData):
592
- index = int(evt.index) if isinstance(evt.index, int) else evt.index[0]
593
- url = paths[index]
594
- return url, update_preview_visibility(url)
595
-
596
- gallery_complex.select(
597
- on_gallery_doc,
598
- complex_state,
599
- [example_url_doc, preview_doc_html],
600
- )
601
-
602
- with gr.Column(scale=7):
603
- with gr.Tabs():
604
- with gr.Tab("Markdown Preview"):
605
- md_preview_doc = gr.Markdown(
606
- latex_delimiters=LATEX_DELIMS,
607
- elem_id="md_preview_doc",
608
- )
609
-
610
- with gr.Tab("Visualization"):
611
- vis_image_doc = gr.HTML(elem_id="vis_image_doc")
612
-
613
- with gr.Tab("Markdown Source"):
614
- md_raw_doc = gr.Code(language="markdown")
615
-
616
- file_doc.change(
617
- lambda filepath: (None, update_preview_visibility(filepath)),
618
- file_doc,
619
- [example_url_doc, preview_doc_html],
620
- )
621
-
622
- def parse_doc(filepath, example_url, chart, unwarp, orientation):
623
- src = filepath if filepath else example_url
624
-
625
- if not src:
626
- raise gr.Error("Please upload an image or choose an example.")
627
-
628
- return handle_complex_doc(src, chart, unwarp, orientation)
629
-
630
- btn_parse.click(
631
- parse_doc,
632
- [file_doc, example_url_doc, chart_switch, unwarp_switch, orient_switch],
633
- [md_preview_doc, vis_image_doc, md_raw_doc],
634
- )
635
-
636
- # =====================
637
- # Tab 2: Element Recognition
638
- # =====================
639
-
640
- with gr.Tab("Element Recognition"):
641
- with gr.Row():
642
- with gr.Column(scale=5):
643
- file_vl = gr.File(
644
- label="Upload Image",
645
- file_count="single",
646
- type="filepath",
647
- file_types=["image"],
648
- )
649
-
650
- preview_vl_html = gr.HTML(
651
- value="",
652
- elem_id="image_preview_vl",
653
- visible=False,
654
- )
655
-
656
- gr.Markdown("Use this mode for single elements like text blocks, tables, charts, seals, or formulas.")
657
-
658
- with gr.Row(elem_classes=["prompt-grid"]):
659
- btn_ocr = gr.Button("Text Recognition", variant="secondary")
660
- btn_formula = gr.Button("Formula Recognition", variant="secondary")
661
-
662
- with gr.Row(elem_classes=["prompt-grid"]):
663
- btn_table = gr.Button("Table Recognition", variant="secondary")
664
- btn_chart = gr.Button("Chart Recognition", variant="secondary")
665
-
666
- with gr.Row(elem_classes=["prompt-grid"]):
667
- btn_seal = gr.Button("Seal Recognition", variant="secondary")
668
-
669
- example_url_vl = gr.State(value=None)
670
-
671
- if targeted_recognition_examples:
672
- targeted_paths = [example[0] for example in targeted_recognition_examples]
673
- targeted_state = gr.State(targeted_paths)
674
-
675
- gallery_targeted = gr.Gallery(
676
- value=targeted_paths,
677
- columns=4,
678
- height=400,
679
- preview=False,
680
- label="Examples",
681
- allow_preview=False,
682
- )
683
-
684
- def on_gallery_vl(paths, evt: gr.SelectData):
685
- index = int(evt.index) if isinstance(evt.index, int) else evt.index[0]
686
- url = paths[index]
687
- return url, update_preview_visibility(url)
688
-
689
- gallery_targeted.select(
690
- on_gallery_vl,
691
- targeted_state,
692
- [example_url_vl, preview_vl_html],
693
- )
694
-
695
- with gr.Column(scale=7):
696
- with gr.Tabs():
697
- with gr.Tab("Recognition Result"):
698
- md_preview_vl = gr.Markdown(
699
- latex_delimiters=LATEX_DELIMS,
700
- elem_id="md_preview_vl",
701
- )
702
-
703
- with gr.Tab("Raw Output"):
704
- md_raw_vl = gr.Code(language="markdown")
705
-
706
- with gr.Tab("Visualization"):
707
- hidden_vis_vl = gr.HTML(visible=False)
708
-
709
- file_vl.change(
710
- lambda filepath: (None, update_preview_visibility(filepath)),
711
- file_vl,
712
- [example_url_vl, preview_vl_html],
713
- )
714
 
715
- def run_vl(filepath, example_url, prompt):
716
- src = filepath if filepath else example_url
717
-
718
- if not src:
719
- raise gr.Error("Please upload an image or choose an example.")
720
-
721
- return handle_targeted_recognition(src, prompt)
722
-
723
- for button, prompt in [
724
- (btn_ocr, "Text Recognition"),
725
- (btn_formula, "Formula Recognition"),
726
- (btn_table, "Table Recognition"),
727
- (btn_chart, "Chart Recognition"),
728
- (btn_seal, "Seal Recognition"),
729
- ]:
730
- button.click(
731
- run_vl,
732
- [file_vl, example_url_vl, gr.State(prompt)],
733
- [md_preview_vl, md_raw_vl, hidden_vis_vl],
734
- )
735
-
736
- # =====================
737
- # Tab 3: Spotting
738
- # =====================
739
-
740
- with gr.Tab("Spotting"):
741
- with gr.Row():
742
- with gr.Column(scale=5):
743
- file_spot = gr.File(
744
- label="Upload Image",
745
- file_count="single",
746
- type="filepath",
747
- file_types=["image"],
748
- )
749
-
750
- preview_spot_html = gr.HTML(
751
- value="",
752
- elem_id="image_preview_spot",
753
- visible=False,
754
- )
755
-
756
- gr.Markdown("Use this mode to detect and locate elements in the image.")
757
-
758
- btn_run_spot = gr.Button("Run Spotting", variant="primary")
759
-
760
- example_url_spot = gr.State(value=None)
761
-
762
- if spotting_recognition_examples:
763
- spotting_paths = [example[0] for example in spotting_recognition_examples]
764
- spot_state = gr.State(spotting_paths)
765
-
766
- gallery_spot = gr.Gallery(
767
- value=spotting_paths,
768
- columns=4,
769
- height=400,
770
- preview=False,
771
- label="Examples",
772
- allow_preview=False,
773
- )
774
-
775
- def on_gallery_spot(paths, evt: gr.SelectData):
776
- index = int(evt.index) if isinstance(evt.index, int) else evt.index[0]
777
- url = paths[index]
778
- return url, update_preview_visibility(url)
779
-
780
- gallery_spot.select(
781
- on_gallery_spot,
782
- spot_state,
783
- [example_url_spot, preview_spot_html],
784
- )
785
-
786
- with gr.Column(scale=7):
787
- with gr.Tabs():
788
- with gr.Tab("Visualization"):
789
- vis_image_spot = gr.HTML(
790
- "<p class='empty-state'>No visualization yet.</p>",
791
- elem_id="vis_image_spot",
792
- )
793
-
794
- with gr.Tab("JSON Result"):
795
- json_spot = gr.Code(
796
- label="Detection Results",
797
- language="json",
798
- )
799
-
800
- file_spot.change(
801
- lambda filepath: (None, update_preview_visibility(filepath)),
802
- file_spot,
803
- [example_url_spot, preview_spot_html],
804
  )
805
 
806
- def run_spotting_wrapper(filepath, example_url):
807
- src = filepath if filepath else example_url
808
 
809
- if not src:
810
- raise gr.Error("Please upload an image or choose an example.")
 
 
811
 
812
- _, json_result, visualization = handle_targeted_recognition(src, "Spotting")
813
- return visualization, json_result
814
 
815
- btn_run_spot.click(
816
- fn=run_spotting_wrapper,
817
- inputs=[file_spot, example_url_spot],
818
- outputs=[vis_image_spot, json_spot],
819
- )
820
 
821
 
822
  if __name__ == "__main__":
823
- demo.queue(max_size=64).launch()
 
 
1
  import json
2
  import os
3
+ import tempfile
4
+ from typing import Any, Dict, List, Tuple
 
5
 
6
  import gradio as gr
7
+ from PIL import Image
8
+ import fitz # PyMuPDF
9
+ from paddleocr import PaddleOCR
10
 
11
 
12
  # =========================
13
  # Config
14
  # =========================
15
 
16
+ APP_TITLE = os.environ.get("APP_TITLE", "DJ OCR Lab")
 
 
 
17
  APP_SUBTITLE = os.environ.get(
18
  "APP_SUBTITLE",
19
+ "Upload an image or PDF and run OCR locally. No API. No branding. No cloud goblin."
20
  )
21
 
22
+ LANG = os.environ.get("OCR_LANG", "en")
23
+ PDF_DPI = int(os.environ.get("PDF_DPI", "200"))
 
 
 
 
24
 
25
+ # Initialize OCR once when the app starts.
26
+ # use_angle_cls helps rotated/skewed text.
27
+ ocr = PaddleOCR(
28
+ use_angle_cls=True,
29
+ lang=LANG,
30
+ )
 
 
 
 
 
 
 
31
 
32
 
33
  # =========================
34
+ # File Helpers
35
  # =========================
36
 
37
+ def is_pdf(path: str) -> bool:
38
+ return path.lower().endswith(".pdf")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
 
 
 
40
 
41
+ def is_image(path: str) -> bool:
42
+ return path.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"))
43
 
44
 
45
+ def render_pdf_to_images(pdf_path: str, dpi: int = 200) -> List[str]:
46
+ """
47
+ Converts each PDF page to a temporary PNG image.
48
+ """
49
+ image_paths = []
50
 
51
+ doc = fitz.open(pdf_path)
52
+ zoom = dpi / 72
53
+ matrix = fitz.Matrix(zoom, zoom)
 
 
 
 
54
 
55
+ for page_index in range(len(doc)):
56
+ page = doc.load_page(page_index)
57
+ pix = page.get_pixmap(matrix=matrix, alpha=False)
 
 
58
 
59
+ out_path = os.path.join(
60
+ tempfile.gettempdir(),
61
+ f"ocr_page_{os.path.basename(pdf_path)}_{page_index + 1}.png"
 
62
  )
63
 
64
+ pix.save(out_path)
65
+ image_paths.append(out_path)
 
 
 
 
66
 
67
+ doc.close()
68
+ return image_paths
 
69
 
 
70
 
71
+ def normalize_ocr_result(raw_result: Any) -> List[Dict[str, Any]]:
72
+ """
73
+ PaddleOCR return shapes can vary a bit by version.
74
+ This tries to normalize common outputs into:
75
+ [
76
+ {
77
+ "text": "...",
78
+ "confidence": 0.99,
79
+ "box": [...]
80
+ }
81
+ ]
82
+ """
83
+ items = []
84
+
85
+ if not raw_result:
86
+ return items
87
+
88
+ # Common shape:
89
+ # [
90
+ # [
91
+ # [[[x,y],...], ("text", confidence)],
92
+ # ...
93
+ # ]
94
+ # ]
95
+ if isinstance(raw_result, list):
96
+ first_layer = raw_result
97
+
98
+ # If wrapped as one page, unwrap it.
99
+ if len(first_layer) == 1 and isinstance(first_layer[0], list):
100
+ first_layer = first_layer[0]
101
+
102
+ for entry in first_layer:
103
+ try:
104
+ box = entry[0]
105
+ text = entry[1][0]
106
+ confidence = float(entry[1][1])
107
+
108
+ items.append({
109
+ "text": text,
110
+ "confidence": confidence,
111
+ "box": box,
112
+ })
113
+ except Exception:
114
+ # Keep weird entries instead of losing evidence.
115
+ items.append({
116
+ "text": str(entry),
117
+ "confidence": None,
118
+ "box": None,
119
+ })
120
+
121
+ return items
122
+
123
+
124
+ def ocr_image(image_path: str) -> Tuple[str, List[Dict[str, Any]]]:
125
+ raw = ocr.ocr(image_path, cls=True)
126
+ items = normalize_ocr_result(raw)
127
+
128
+ text = "\n".join(item["text"] for item in items if item.get("text"))
129
+ return text.strip(), items
130
+
131
+
132
+ def run_local_ocr(file_path: str) -> Tuple[str, str]:
133
+ if not file_path:
134
+ raise gr.Error("Please upload an image or PDF first.")
135
+
136
+ if not os.path.exists(file_path):
137
+ raise gr.Error("Uploaded file was not found.")
138
+
139
+ page_outputs = []
140
+ structured_outputs = []
141
+
142
+ if is_pdf(file_path):
143
+ image_paths = render_pdf_to_images(file_path, dpi=PDF_DPI)
144
+ elif is_image(file_path):
145
+ image_paths = [file_path]
146
  else:
147
+ raise gr.Error("Unsupported file type. Please upload an image or PDF.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ for index, image_path in enumerate(image_paths, start=1):
150
+ text, items = ocr_image(image_path)
151
 
152
+ page_outputs.append(f"## Page {index}\n\n{text or '[No text recognized]'}")
 
153
 
154
+ structured_outputs.append({
155
+ "page": index,
156
+ "image_path": image_path,
157
+ "items": items,
158
+ })
159
 
160
+ markdown_text = "\n\n---\n\n".join(page_outputs)
161
+ json_output = json.dumps(structured_outputs, indent=2, ensure_ascii=False)
162
 
163
+ return markdown_text, json_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
 
166
  # =========================
167
+ # UI
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  # =========================
169
 
170
  custom_css = """
171
  body,
172
  .gradio-container {
173
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
 
 
 
 
174
  }
175
 
176
  .app-header {
177
  text-align: center;
178
+ max-width: 900px;
179
+ margin: 0 auto 16px;
 
 
 
 
 
 
 
 
180
  }
181
 
182
  .app-title {
183
+ font-size: 32px;
184
  font-weight: 800;
185
+ letter-spacing: -0.04em;
 
186
  }
187
 
188
  .app-subtitle {
189
  color: #64748b;
190
  font-size: 15px;
 
 
191
  line-height: 1.5;
192
+ margin-top: 6px;
193
+ }
194
+
195
+ #ocr_output {
196
+ max-height: 640px;
197
+ overflow: auto;
198
  }
199
 
200
  .notice {
201
+ margin: 10px auto 16px;
202
+ max-width: 900px;
203
  padding: 12px 14px;
204
  border: 1px solid #e5e7eb;
205
  border-radius: 12px;
206
  background: #f8fafc;
 
 
207
  color: #334155;
208
+ font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }
210
  """
211
 
212
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft(), title=APP_TITLE) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  gr.HTML(
214
  f"""
215
  <div class="app-header">
 
216
  <div class="app-title">{APP_TITLE}</div>
217
  <div class="app-subtitle">{APP_SUBTITLE}</div>
218
  </div>
219
  """
220
  )
221
 
222
+ gr.HTML(
223
+ f"""
224
+ <div class="notice">
225
+ <strong>Local mode:</strong> OCR runs inside this app using PaddleOCR.
226
+ Current language: <code>{LANG}</code>. PDF render DPI: <code>{PDF_DPI}</code>.
227
+ </div>
228
+ """
229
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
+ with gr.Row():
232
+ with gr.Column(scale=4):
233
+ upload = gr.File(
234
+ label="Upload image or PDF",
235
+ file_count="single",
236
+ type="filepath",
237
+ file_types=[".pdf", ".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  )
239
 
240
+ run_btn = gr.Button("Run OCR", variant="primary")
 
241
 
242
+ with gr.Column(scale=8):
243
+ with gr.Tabs():
244
+ with gr.Tab("Text Output"):
245
+ text_output = gr.Markdown(elem_id="ocr_output")
246
 
247
+ with gr.Tab("Structured Output"):
248
+ json_output = gr.Code(language="json")
249
 
250
+ run_btn.click(
251
+ fn=run_local_ocr,
252
+ inputs=[upload],
253
+ outputs=[text_output, json_output],
254
+ )
255
 
256
 
257
  if __name__ == "__main__":
258
+ demo.queue(max_size=8).launch()