multimodalart HF Staff commited on
Commit
30a9ce4
Β·
verified Β·
1 Parent(s): 10e676d

Add single-region mode, code fence languages, measured GPU duration

Browse files
Files changed (2) hide show
  1. README.md +5 -0
  2. app.py +100 -17
README.md CHANGED
@@ -35,6 +35,11 @@ The app follows the authors' two-stage pipeline:
35
  converted to HTML, equations to LaTeX, using the authors' post-processors
36
  (vendored under `NaviOCR/`).
37
 
 
 
 
 
 
38
  Outputs: rendered document, Markdown source, layout overlay, and the raw block
39
  list as JSON.
40
 
 
35
  converted to HTML, equations to LaTeX, using the authors' post-processors
36
  (vendored under `NaviOCR/`).
37
 
38
+ A third **Single region** mode skips layout and runs the authors' one-block path
39
+ (`block_parse`) over the whole image with the prompt for a chosen block type β€”
40
+ table, formula, code, seal, or a chart / scientific figure, which the model
41
+ converts into the table it implies.
42
+
43
  Outputs: rendered document, Markdown source, layout overlay, and the raw block
44
  list as JSON.
45
 
app.py CHANGED
@@ -11,12 +11,16 @@ Two-stage pipeline, faithful to the authors' reference implementation
11
  de-rotated, and recognized with the block-type-specific prompt and sampling
12
  parameters from `NaviOCR/vlm_utils/NaviOCR_client.py`, then post-processed
13
  (OTSL tables -> HTML, LaTeX equation fixes) with the authors' post-processors.
 
 
 
 
14
  """
15
 
16
  import base64
17
  import io
18
- import json
19
  import os
 
20
  import tempfile
21
  import time
22
  from dataclasses import asdict
@@ -38,6 +42,7 @@ from NaviOCR.vlm_utils.NaviOCR_client import (
38
  NaviOCRClient,
39
  )
40
  from NaviOCR.vlm_utils.post_process.otsl2html import convert_otsl_to_html
 
41
  from NaviOCR.vlm_utils.vlm_client import SamplingParams
42
 
43
  MODEL_ID = "StarDoc-AI/NaviDC-OCR"
@@ -90,6 +95,18 @@ BLOCK_COLORS = {
90
  }
91
  DEFAULT_COLOR = (117, 117, 117)
92
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  def _sampling_params(task: str, max_new_tokens: int) -> SamplingParams:
95
  """Authors' per-task sampling params, with a bounded generation length."""
@@ -165,6 +182,15 @@ def draw_layout(image: Image.Image, blocks: list) -> Image.Image:
165
  return Image.alpha_composite(canvas.convert("RGBA"), overlay).convert("RGB")
166
 
167
 
 
 
 
 
 
 
 
 
 
168
  def blocks_to_markdown(image: Image.Image, blocks: list, drop_paratext: bool):
169
  """Assemble reading-ordered blocks into Markdown (raw + display variants)."""
170
  parts: list[str] = []
@@ -193,7 +219,7 @@ def blocks_to_markdown(image: Image.Image, blocks: list, drop_paratext: bool):
193
  elif block_type == "char":
194
  parts.append(convert_otsl_to_html(content) or content)
195
  elif block_type in {"code", "algorithm"}:
196
- parts.append(f"```\n{content}\n```")
197
  elif block_type in CAPTION_TYPES:
198
  parts.append(f"*{content}*")
199
  elif block_type == "seal":
@@ -219,10 +245,20 @@ def _write_markdown(markdown: str) -> str:
219
  return path
220
 
221
 
222
- @spaces.GPU(duration=150)
 
 
 
 
 
 
 
 
 
223
  def parse_document(
224
  image: Image.Image,
225
  layout_mode: str = "Detection",
 
226
  drop_paratext: bool = True,
227
  max_new_tokens: int = 2048,
228
  progress=gr.Progress(track_tqdm=True),
@@ -232,8 +268,11 @@ def parse_document(
232
  Args:
233
  image: A document page β€” a digital page, a scan, or a camera photo.
234
  layout_mode: "Detection" for axis-aligned boxes (digital pages, flat
235
- scans) or "Segmentation" for multi-point polygons (camera-captured,
236
- curved or crumpled pages).
 
 
 
237
  drop_paratext: Drop headers, footers, page numbers and margin notes.
238
  max_new_tokens: Generation cap per region.
239
 
@@ -247,7 +286,33 @@ def parse_document(
247
  started = time.time()
248
  page = image.convert("RGB") if isinstance(image, Image.Image) else Image.open(image).convert("RGB")
249
  helper = client.helper
250
- mode = layout_mode if layout_mode in LAYOUT_PROMPTS else "Detection"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
  # ---- stage 1: layout ------------------------------------------------
253
  layout_image = helper.prepare_for_layout(page) # resized to 1036x1036
@@ -342,11 +407,21 @@ off flat scans **and** photographed / crumpled pages, and returns Markdown.
342
  image = gr.Image(label="Document page", type="pil", height=460)
343
  layout_mode = gr.Radio(
344
  choices=[
345
- ("Boxes β€” digital pages & flat scans", "Detection"),
346
- ("Multi-point β€” photos, curved or crumpled pages", "Segmentation"),
 
 
 
 
347
  ],
348
  value="Detection",
349
- label="Layout mode",
 
 
 
 
 
 
350
  )
351
  run_button = gr.Button("Parse document", variant="primary")
352
  report = gr.Markdown()
@@ -378,14 +453,14 @@ off flat scans **and** photographed / crumpled pages, and returns Markdown.
378
 
379
  gr.Examples(
380
  examples=[
381
- ["examples/journal_page.jpg", "Detection"],
382
- ["examples/crumpled_page.jpg", "Segmentation"],
383
- ["examples/table.png", "Detection"],
384
- ["examples/formula.png", "Detection"],
385
- ["examples/code.png", "Detection"],
386
- ["examples/scientific_figure.png", "Detection"],
387
  ],
388
- inputs=[image, layout_mode],
389
  outputs=[overlay, document, markdown_source, blocks_json, markdown_file, report],
390
  fn=parse_document,
391
  cache_examples=True,
@@ -393,10 +468,18 @@ off flat scans **and** photographed / crumpled pages, and returns Markdown.
393
  label="Examples from the NaviDC-OCR model card",
394
  )
395
 
 
 
 
 
 
 
 
 
396
  gr.on(
397
  triggers=[run_button.click],
398
  fn=parse_document,
399
- inputs=[image, layout_mode, drop_paratext, max_new_tokens],
400
  outputs=[overlay, document, markdown_source, blocks_json, markdown_file, report],
401
  )
402
 
 
11
  de-rotated, and recognized with the block-type-specific prompt and sampling
12
  parameters from `NaviOCR/vlm_utils/NaviOCR_client.py`, then post-processed
13
  (OTSL tables -> HTML, LaTeX equation fixes) with the authors' post-processors.
14
+
15
+ A third mode skips layout and runs the authors' single-region path
16
+ (`NaviOCRClient.block_parse`) on the whole image, which is how the model card
17
+ demonstrates chart-to-table extraction, seal reading and table/formula crops.
18
  """
19
 
20
  import base64
21
  import io
 
22
  import os
23
+ import re
24
  import tempfile
25
  import time
26
  from dataclasses import asdict
 
42
  NaviOCRClient,
43
  )
44
  from NaviOCR.vlm_utils.post_process.otsl2html import convert_otsl_to_html
45
+ from NaviOCR.vlm_utils.structs import ContentBlock
46
  from NaviOCR.vlm_utils.vlm_client import SamplingParams
47
 
48
  MODEL_ID = "StarDoc-AI/NaviDC-OCR"
 
95
  }
96
  DEFAULT_COLOR = (117, 117, 117)
97
 
98
+ # Block types the single-region mode exposes, with the authors' prompt keys.
99
+ REGION_TASKS = [
100
+ ("Text", "text"),
101
+ ("Table \u2192 HTML", "table"),
102
+ ("Formula \u2192 LaTeX", "formula"),
103
+ ("Code", "code"),
104
+ ("Chart / scientific figure \u2192 table", "char"),
105
+ ("Seal", "seal"),
106
+ ]
107
+ # The model prefixes recognized code with its own language marker, e.g. `<_Python_>`.
108
+ CODE_LANG_RE = re.compile(r"^\s*<_([A-Za-z0-9+#._\- ]+)_>\s*")
109
+
110
 
111
  def _sampling_params(task: str, max_new_tokens: int) -> SamplingParams:
112
  """Authors' per-task sampling params, with a bounded generation length."""
 
182
  return Image.alpha_composite(canvas.convert("RGBA"), overlay).convert("RGB")
183
 
184
 
185
+ def _fenced_code(content: str) -> str:
186
+ match = CODE_LANG_RE.match(content)
187
+ language = ""
188
+ if match:
189
+ language = match.group(1).strip().lower().replace(" ", "")
190
+ content = content[match.end() :]
191
+ return f"```{language}\n{content}\n```"
192
+
193
+
194
  def blocks_to_markdown(image: Image.Image, blocks: list, drop_paratext: bool):
195
  """Assemble reading-ordered blocks into Markdown (raw + display variants)."""
196
  parts: list[str] = []
 
219
  elif block_type == "char":
220
  parts.append(convert_otsl_to_html(content) or content)
221
  elif block_type in {"code", "algorithm"}:
222
+ parts.append(_fenced_code(content))
223
  elif block_type in CAPTION_TYPES:
224
  parts.append(f"*{content}*")
225
  elif block_type == "seal":
 
245
  return path
246
 
247
 
248
+ def _estimate_duration(*args, **kwargs) -> int:
249
+ """A dense page measured 67 s end-to-end; scale mildly with the token cap."""
250
+ max_new_tokens = 2048
251
+ if len(args) > 4:
252
+ max_new_tokens = args[4]
253
+ max_new_tokens = int(kwargs.get("max_new_tokens", max_new_tokens) or 2048)
254
+ return int(min(180, 55 + 0.02 * max_new_tokens))
255
+
256
+
257
+ @spaces.GPU(duration=_estimate_duration)
258
  def parse_document(
259
  image: Image.Image,
260
  layout_mode: str = "Detection",
261
+ region_task: str = "text",
262
  drop_paratext: bool = True,
263
  max_new_tokens: int = 2048,
264
  progress=gr.Progress(track_tqdm=True),
 
268
  Args:
269
  image: A document page β€” a digital page, a scan, or a camera photo.
270
  layout_mode: "Detection" for axis-aligned boxes (digital pages, flat
271
+ scans), "Segmentation" for multi-point polygons (camera-captured,
272
+ curved or crumpled pages), or "Region" to skip layout and recognize
273
+ the whole image as one block.
274
+ region_task: The block type used in "Region" mode β€” one of text, table,
275
+ formula, code, char (chart/scientific figure), seal.
276
  drop_paratext: Drop headers, footers, page numbers and margin notes.
277
  max_new_tokens: Generation cap per region.
278
 
 
286
  started = time.time()
287
  page = image.convert("RGB") if isinstance(image, Image.Image) else Image.open(image).convert("RGB")
288
  helper = client.helper
289
+ mode = layout_mode if layout_mode in LAYOUT_PROMPTS else "Region"
290
+
291
+ # ---- single-region mode: the authors' block_parse path ----------------
292
+ if mode == "Region":
293
+ task = region_task if region_task in DEFAULT_PROMPTS else "text"
294
+ crop = helper.resize_by_need(page)
295
+ output = client.client.predict(
296
+ crop,
297
+ DEFAULT_PROMPTS[task],
298
+ _sampling_params(task, max_new_tokens),
299
+ )
300
+ block = ContentBlock(type=task, bbox=[[0.0, 0.0], [1.0, 1.0]], content=output)
301
+ blocks = helper.post_process([block]) or [block]
302
+ raw_markdown, display_markdown = blocks_to_markdown(page, blocks, False)
303
+ seconds = time.time() - started
304
+ report = (
305
+ f"Single region recognized as `{task}` \u2014 {seconds:.1f}s. \n"
306
+ f"Switch to a full-page mode to run layout analysis first."
307
+ )
308
+ return (
309
+ page,
310
+ display_markdown,
311
+ raw_markdown,
312
+ [dict(item) for item in blocks],
313
+ _write_markdown(raw_markdown),
314
+ report,
315
+ )
316
 
317
  # ---- stage 1: layout ------------------------------------------------
318
  layout_image = helper.prepare_for_layout(page) # resized to 1036x1036
 
407
  image = gr.Image(label="Document page", type="pil", height=460)
408
  layout_mode = gr.Radio(
409
  choices=[
410
+ ("Full page, boxes β€” digital pages & flat scans", "Detection"),
411
+ (
412
+ "Full page, multi-point β€” photos, curved or crumpled pages",
413
+ "Segmentation",
414
+ ),
415
+ ("Single region β€” the image is one table / formula / …", "Region"),
416
  ],
417
  value="Detection",
418
+ label="Parsing mode",
419
+ )
420
+ region_task = gr.Dropdown(
421
+ choices=REGION_TASKS,
422
+ value="table",
423
+ label="Region type",
424
+ visible=False,
425
  )
426
  run_button = gr.Button("Parse document", variant="primary")
427
  report = gr.Markdown()
 
453
 
454
  gr.Examples(
455
  examples=[
456
+ ["examples/journal_page.jpg", "Detection", "table"],
457
+ ["examples/crumpled_page.jpg", "Segmentation", "table"],
458
+ ["examples/table.png", "Region", "table"],
459
+ ["examples/formula.png", "Region", "formula"],
460
+ ["examples/code.png", "Region", "code"],
461
+ ["examples/scientific_figure.png", "Region", "char"],
462
  ],
463
+ inputs=[image, layout_mode, region_task],
464
  outputs=[overlay, document, markdown_source, blocks_json, markdown_file, report],
465
  fn=parse_document,
466
  cache_examples=True,
 
468
  label="Examples from the NaviDC-OCR model card",
469
  )
470
 
471
+ layout_mode.change(
472
+ fn=lambda mode: gr.update(visible=(mode == "Region")),
473
+ inputs=[layout_mode],
474
+ outputs=[region_task],
475
+ show_api=False,
476
+ queue=False,
477
+ )
478
+
479
  gr.on(
480
  triggers=[run_button.click],
481
  fn=parse_document,
482
+ inputs=[image, layout_mode, region_task, drop_paratext, max_new_tokens],
483
  outputs=[overlay, document, markdown_source, blocks_json, markdown_file, report],
484
  )
485