multimodalart HF Staff commited on
Commit
0f5ba4a
·
verified ·
1 Parent(s): fecf805

NaviDC-OCR two-stage document parsing demo

Browse files
.gitattributes CHANGED
@@ -36,3 +36,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
36
  examples/code.png filter=lfs diff=lfs merge=lfs -text
37
  examples/layout.jpg filter=lfs diff=lfs merge=lfs -text
38
  examples/layout_distorted.jpg filter=lfs diff=lfs merge=lfs -text
 
 
 
36
  examples/code.png filter=lfs diff=lfs merge=lfs -text
37
  examples/layout.jpg filter=lfs diff=lfs merge=lfs -text
38
  examples/layout_distorted.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/crumpled_page.jpg filter=lfs diff=lfs merge=lfs -text
40
+ examples/journal_page.jpg filter=lfs diff=lfs merge=lfs -text
NaviOCR/config.py CHANGED
@@ -4,8 +4,9 @@
4
 
5
  model_path = "StarDoc-AI/NaviDC-OCR"
6
 
7
- BACKEND = "vllm-async-engine"
8
- # [vllm-engine, vllm-async-engine]
 
9
 
10
 
11
  # =========================
 
4
 
5
  model_path = "StarDoc-AI/NaviDC-OCR"
6
 
7
+ BACKEND = "transformers"
8
+ # [transformers, vllm-engine, vllm-async-engine]
9
+ # This Space uses the transformers backend (ZeroGPU).
10
 
11
 
12
  # =========================
NaviOCR/vlm_utils/__init__.py CHANGED
@@ -5,7 +5,6 @@ import importlib
5
  __lazy_attrs__ = {
6
  "NaviOCRClient": (".NaviOCR_client", "NaviOCRClient"),
7
  "NaviOCRSamplingParams": (".NaviOCR_client", "NaviOCRSamplingParams"),
8
- "NaviOCRLogitsProcessor": (".vlm_client.vllm_v1_no_repeat_ngram", "VllmV1NoRepeatNGramLogitsProcessor"),
9
  }
10
 
11
 
 
5
  __lazy_attrs__ = {
6
  "NaviOCRClient": (".NaviOCR_client", "NaviOCRClient"),
7
  "NaviOCRSamplingParams": (".NaviOCR_client", "NaviOCRSamplingParams"),
 
8
  }
9
 
10
 
README.md CHANGED
@@ -6,28 +6,42 @@ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.24.0
8
  app_file: app.py
9
- short_description: Document parsing for digital and camera-captured docs
10
  python_version: "3.12"
11
  startup_duration_timeout: 30m
 
 
 
 
12
  ---
13
 
14
  # NaviDC-OCR
15
 
16
- NaviDC-OCR is a lightweight (~1.2B parameters) Vision-Language Model for unified document parsing across digital and camera-captured documents. It extracts structured text, tables, formulas, and layout from document images.
17
-
18
- ## How it works
19
-
20
- 1. **Layout Detection**: The model first analyzes the document layout, identifying regions like text, tables, formulas, images, and code blocks.
21
- 2. **Content Extraction**: Each detected region is then individually parsed to extract the content (text, LaTeX formulas, OTSL tables, code, etc.).
22
-
23
- The model is based on the Qwen2.5-VL architecture and uses a two-step extraction pipeline.
24
-
25
- ## Links
26
-
27
- - [Paper (arXiv:2608.12898)](https://arxiv.org/abs/2608.12898)
28
- - [Model on Hugging Face](https://huggingface.co/StarDoc-AI/NaviDC-OCR)
29
- - [GitHub Repository](https://github.com/caipeng328/NaviDC-OCR)
30
-
31
- ## License
32
-
33
- Apache-2.0
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: gradio
7
  sdk_version: 6.24.0
8
  app_file: app.py
9
+ short_description: Parse digital & photographed documents into Markdown
10
  python_version: "3.12"
11
  startup_duration_timeout: 30m
12
+ license: apache-2.0
13
+ models:
14
+ - StarDoc-AI/NaviDC-OCR
15
+ pinned: false
16
  ---
17
 
18
  # NaviDC-OCR
19
 
20
+ Demo of [StarDoc-AI/NaviDC-OCR](https://huggingface.co/StarDoc-AI/NaviDC-OCR), a 1.2B
21
+ document-parsing vision-language model that unifies **digital** and
22
+ **camera-captured** documents
23
+ ([paper](https://huggingface.co/papers/2608.12898) ·
24
+ [code](https://github.com/caipeng328/NaviDC-OCR)).
25
+
26
+ The app follows the authors' two-stage pipeline:
27
+
28
+ 1. **Layout** — the page is resized to 1036×1036 and the model emits
29
+ reading-ordered regions. `Detection` mode returns axis-aligned boxes (digital
30
+ pages, flat scans); `Segmentation` mode returns multi-point polygons, which is
31
+ the paper's geometry-aware path for photographed, curved or crumpled pages.
32
+ 2. **Recognition** each region is cropped (polygon-masked and de-rotated where
33
+ needed) and recognized with the block-type-specific prompt and sampling
34
+ parameters from the reference implementation. Tables come back as OTSL and are
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
+
41
+ ## Credits
42
+
43
+ Example pages are the official assets from the
44
+ [NaviDC-OCR model card](https://huggingface.co/StarDoc-AI/NaviDC-OCR/tree/main/assets)
45
+ (Apache-2.0). The `NaviOCR/` package is a trimmed copy of the authors'
46
+ [reference implementation](https://github.com/caipeng328/NaviDC-OCR) (Apache-2.0),
47
+ limited to the modules needed for the transformers backend.
app.py CHANGED
@@ -1,150 +1,404 @@
1
- import os
2
 
3
- os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
 
4
 
5
- import spaces # MUST come before any CUDA-touching import
6
- import torch
7
- import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
8
  import time
9
- from PIL import Image
 
10
 
11
- import NaviOCR.config as CONFIG
12
 
13
- # Force transformers backend (vLLM is not available on ZeroGPU)
14
- CONFIG.BACKEND = "transformers"
 
 
 
 
15
 
16
- from transformers import AutoProcessor, AutoModelForImageTextToText
17
- from NaviOCR.vlm_utils.NaviOCR_client import NaviOCRClient
 
 
 
 
 
 
18
 
19
  MODEL_ID = "StarDoc-AI/NaviDC-OCR"
20
 
21
- # Load model eagerly at module scope (ZeroGPU rule #2)
22
- print(f"Loading {MODEL_ID} ...")
23
- processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
24
  model = AutoModelForImageTextToText.from_pretrained(
25
  MODEL_ID,
26
  trust_remote_code=True,
27
  torch_dtype=torch.bfloat16,
28
  attn_implementation="sdpa",
29
- ).to("cuda").eval()
 
30
 
31
- # Initialize the NaviOCR client with transformers backend directly
32
- predictor = NaviOCRClient(
33
  backend="transformers",
34
  model=model,
35
  processor=processor,
36
- use_tqdm=False,
 
 
 
37
  )
38
 
39
- print("Model loaded successfully.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
 
42
- def blocks_to_markdown(blocks):
43
- """Convert NaviOCR ContentBlock list to markdown string."""
44
- md_parts = []
 
 
45
  for block in blocks:
46
- btype = block.get("type", "text")
47
- content = block.get("content")
48
- if content is None:
 
 
 
 
 
 
 
49
  continue
50
- content = content.strip()
51
  if not content:
52
  continue
53
- if btype == "title":
54
- md_parts.append(f"## {content}\n")
55
- elif btype == "table":
56
- md_parts.append(f"{content}\n")
57
- elif btype == "equation":
58
- md_parts.append(f"{content}\n")
59
- elif btype == "code":
60
- md_parts.append(f"```\n{content}\n```\n")
61
- elif btype == "image":
62
- md_parts.append(f"![image]({content})\n")
63
- elif btype == "list":
64
- md_parts.append(f"{content}\n")
65
- else:
66
- md_parts.append(f"{content}\n")
67
- return "\n".join(md_parts)
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- @spaces.GPU(duration=120)
71
- def parse_document(image: Image.Image, progress=gr.Progress(track_tqdm=True)):
72
- """Parse a document image and extract structured text, tables, formulas, and layout.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  Args:
75
- image: A document image (digital or camera-captured).
 
 
 
 
 
76
 
77
  Returns:
78
- Markdown-formatted structured text with layout, tables, formulas, and code blocks.
 
79
  """
80
  if image is None:
81
- return "Please upload an image first."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- if image.mode != "RGB":
84
- image = image.convert("RGB")
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- start = time.time()
87
- # The NaviOCR two-step extract: layout detection → content extraction per block
88
- results = predictor.batch_two_step_extract(images=[image])
89
- elapsed = time.time() - start
 
 
 
 
 
90
 
91
- blocks = results[0]
92
- markdown = blocks_to_markdown(blocks)
93
 
94
- # Append timing info
95
- markdown += f"\n\n---\n*Parsed in {elapsed:.1f}s*"
 
96
 
97
- return markdown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
 
100
  CSS = """
101
- #col-container { max-width: 1100px; margin: 0 auto; }
102
  .dark .gradio-container { color: var(--body-text-color); }
 
 
 
103
  """
104
 
105
- with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
106
- gr.Markdown(
107
- """
108
- # NaviDC-OCR: Document Parsing Across Digital and Camera-Captured Documents
 
 
109
 
110
- Upload a document image (digital or camera-captured) to extract structured text,
111
- tables, formulas, and layout as Markdown.
 
 
 
112
 
113
- [Paper](https://arxiv.org/abs/2608.12898) · [Model](https://huggingface.co/StarDoc-AI/NaviDC-OCR) · [GitHub](https://github.com/caipeng328/NaviDC-OCR)
114
- """
115
- )
116
- with gr.Row():
117
- image_in = gr.Image(
118
- label="Document Image",
119
- type="pil",
120
- sources=["upload", "clipboard"],
121
- height=500,
122
- )
123
 
124
- run_btn = gr.Button("Parse Document", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- output_md = gr.Markdown(
127
- label="Parsed Result",
128
- value="Upload an image and click **Parse Document** to see results here.",
129
- )
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- run_btn.click(fn=parse_document, inputs=[image_in], outputs=[output_md], api_name="parse")
132
-
133
- gr.Examples(
134
- examples=[
135
- "examples/text.png",
136
- "examples/table.png",
137
- "examples/formula.png",
138
- "examples/code.png",
139
- "examples/layout.jpg",
140
- "examples/layout_distorted.jpg",
141
- "examples/scientific_figure.png",
142
- ],
143
- inputs=[image_in],
144
- outputs=[output_md],
145
  fn=parse_document,
146
- cache_examples=True,
147
- cache_mode="lazy",
148
  )
149
 
150
- demo.launch(mcp_server=True)
 
 
1
+ """NaviDC-OCR — document parsing across digital and camera-captured documents.
2
 
3
+ Two-stage pipeline, faithful to the authors' reference implementation
4
+ (https://github.com/caipeng328/NaviDC-OCR):
5
 
6
+ 1. Layout stage the page is resized to 1036x1036 and the model predicts
7
+ reading-ordered blocks as `<box:...><label:...><angle>` (boxes in
8
+ "Detection" mode, multi-point polygons in "Segmentation" mode, which is what
9
+ the paper uses for curved / camera-captured pages).
10
+ 2. Recognition stage — every block is cropped (polygon-masked when needed),
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
23
+ from typing import Any
24
 
25
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
26
 
27
+ import spaces # noqa: F401 (must precede torch / transformers)
28
+ import gradio as gr
29
+ import numpy as np
30
+ import torch
31
+ from PIL import Image, ImageDraw, ImageFont
32
+ from transformers import AutoModelForImageTextToText, AutoProcessor
33
 
34
+ from NaviOCR.vlm_utils.NaviOCR_client import (
35
+ DEFAULT_PROMPTS,
36
+ DEFAULT_SAMPLING_PARAMS,
37
+ LAYOUT_PROMPTS,
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"
44
 
45
+ processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True, use_fast=True)
 
 
46
  model = AutoModelForImageTextToText.from_pretrained(
47
  MODEL_ID,
48
  trust_remote_code=True,
49
  torch_dtype=torch.bfloat16,
50
  attn_implementation="sdpa",
51
+ )
52
+ model = model.eval().to("cuda")
53
 
54
+ client = NaviOCRClient(
 
55
  backend="transformers",
56
  model=model,
57
  processor=processor,
58
+ prompts=DEFAULT_PROMPTS,
59
+ sampling_params=DEFAULT_SAMPLING_PARAMS,
60
+ batch_size=0, # the authors' transformers backend default: one region at a time
61
+ use_tqdm=True,
62
  )
63
 
64
+ PARATEXT_TYPES = {"header", "footer", "page_number", "aside_text", "page_footnote", "unknown"}
65
+ CAPTION_TYPES = {
66
+ "table_caption",
67
+ "image_caption",
68
+ "code_caption",
69
+ "table_footnote",
70
+ "image_footnote",
71
+ }
72
+ BLOCK_COLORS = {
73
+ "title": (216, 27, 96),
74
+ "text": (30, 136, 229),
75
+ "table": (0, 137, 123),
76
+ "table_caption": (0, 172, 193),
77
+ "table_footnote": (0, 172, 193),
78
+ "image": (245, 124, 0),
79
+ "image_caption": (251, 192, 45),
80
+ "image_footnote": (251, 192, 45),
81
+ "equation": (142, 36, 170),
82
+ "equation_block": (142, 36, 170),
83
+ "code": (94, 53, 177),
84
+ "code_caption": (121, 85, 72),
85
+ "algorithm": (94, 53, 177),
86
+ "list": (57, 73, 171),
87
+ "ref_text": (109, 76, 65),
88
+ "seal": (211, 47, 47),
89
+ "char": (0, 121, 107),
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."""
96
+ base = DEFAULT_SAMPLING_PARAMS.get(task) or DEFAULT_SAMPLING_PARAMS["default"]
97
+ fields = asdict(base)
98
+ fields["max_new_tokens"] = int(max_new_tokens)
99
+ return SamplingParams(**fields)
100
+
101
+
102
+ def _points(bbox, width: int, height: int) -> np.ndarray:
103
+ pts = np.array(bbox, dtype=np.float32).reshape(-1, 2)
104
+ pts[:, 0] *= width
105
+ pts[:, 1] *= height
106
+ return pts.astype(np.int32)
107
+
108
+
109
+ def _crop(image: Image.Image, bbox) -> Image.Image:
110
+ pts = _points(bbox, image.width, image.height)
111
+ x1, y1 = int(pts[:, 0].min()), int(pts[:, 1].min())
112
+ x2, y2 = int(pts[:, 0].max()), int(pts[:, 1].max())
113
+ x1, y1 = max(0, x1), max(0, y1)
114
+ x2, y2 = min(image.width, max(x2, x1 + 1)), min(image.height, max(y2, y1 + 1))
115
+ return image.crop((x1, y1, x2, y2))
116
+
117
+
118
+ def _data_uri(image: Image.Image, max_width: int = 900) -> str:
119
+ if image.width > max_width:
120
+ ratio = max_width / image.width
121
+ image = image.resize((max_width, max(1, int(image.height * ratio))), Image.Resampling.LANCZOS)
122
+ buffer = io.BytesIO()
123
+ image.convert("RGB").save(buffer, format="JPEG", quality=88)
124
+ return "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()
125
+
126
+
127
+ def _font(size: int):
128
+ try:
129
+ return ImageFont.load_default(size=size)
130
+ except TypeError: # very old Pillow
131
+ return ImageFont.load_default()
132
+
133
+
134
+ def draw_layout(image: Image.Image, blocks: list) -> Image.Image:
135
+ """Overlay the predicted blocks, numbered in predicted reading order."""
136
+ canvas = image.convert("RGB").copy()
137
+ overlay = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
138
+ draw = ImageDraw.Draw(overlay)
139
+ line_width = max(2, round(min(canvas.size) / 400))
140
+ font = _font(max(13, round(min(canvas.size) / 55)))
141
+
142
+ for order, block in enumerate(blocks, start=1):
143
+ color = BLOCK_COLORS.get(block.type, DEFAULT_COLOR)
144
+ pts = _points(block.bbox, canvas.width, canvas.height)
145
+ if len(pts) == 2:
146
+ xy = [(int(pts[0][0]), int(pts[0][1])), (int(pts[1][0]), int(pts[1][1]))]
147
+ draw.rectangle(xy, outline=color + (255,), width=line_width)
148
+ anchor = xy[0]
149
+ else:
150
+ polygon = [(int(x), int(y)) for x, y in pts]
151
+ draw.polygon(polygon, outline=color + (255,), fill=color + (28,), width=line_width)
152
+ anchor = min(polygon, key=lambda p: (p[1], p[0]))
153
+
154
+ label = f"{order} {block.type}"
155
+ if block.angle:
156
+ label += f" {block.angle}\u00b0"
157
+ tx, ty = anchor[0], max(0, anchor[1] - font.size - 4)
158
+ text_box = draw.textbbox((tx, ty), label, font=font)
159
+ draw.rectangle(
160
+ (text_box[0] - 2, text_box[1] - 2, text_box[2] + 2, text_box[3] + 2),
161
+ fill=color + (235,),
162
+ )
163
+ draw.text((tx, ty), label, fill=(255, 255, 255, 255), font=font)
164
+
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] = []
171
+ figures: dict[str, Image.Image] = {}
172
+
173
  for block in blocks:
174
+ block_type = block.type
175
+ content = (block.content or "").strip()
176
+
177
+ if drop_paratext and block_type in PARATEXT_TYPES:
178
+ continue
179
+
180
+ if block_type == "image":
181
+ key = f"figure_{len(figures) + 1}.jpg"
182
+ figures[key] = _crop(image, block.bbox)
183
+ parts.append(f"![{key}]({key})")
184
  continue
185
+
186
  if not content:
187
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ if block_type == "title":
190
+ parts.append(f"## {content}")
191
+ elif block_type == "table":
192
+ parts.append(content) # already OTSL -> HTML in post-processing
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":
200
+ parts.append(f"**[seal]** {content}")
201
+ else: # text, list, ref_text, equation, phonetic, header/footer, ...
202
+ parts.append(content)
203
 
204
+ raw_markdown = "\n\n".join(parts).strip()
205
+ display_markdown = raw_markdown
206
+ for key, crop in figures.items():
207
+ display_markdown = display_markdown.replace(
208
+ f"![{key}]({key})",
209
+ f'<img src="{_data_uri(crop)}" style="max-width:100%;border-radius:6px" />',
210
+ )
211
+ return raw_markdown, display_markdown
212
+
213
+
214
+ def _write_markdown(markdown: str) -> str:
215
+ directory = tempfile.mkdtemp(prefix="navidc_ocr_")
216
+ path = os.path.join(directory, "navidc_ocr.md")
217
+ with open(path, "w", encoding="utf-8") as handle:
218
+ handle.write(markdown)
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),
229
+ ) -> tuple[Image.Image, str, str, list[dict[str, Any]], str, str]:
230
+ """Parse a document page into Markdown with NaviDC-OCR.
231
 
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
 
240
  Returns:
241
+ The layout overlay, rendered Markdown, raw Markdown, the block list as
242
+ JSON, a downloadable .md file, and a short run report.
243
  """
244
  if image is None:
245
+ raise gr.Error("Please provide a document image first.")
246
+
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
254
+ raw_layout = client.client.predict(
255
+ layout_image,
256
+ LAYOUT_PROMPTS[mode],
257
+ _sampling_params("layout", max(1024, int(max_new_tokens))),
258
+ )
259
+ blocks = helper.parse_layout_output(raw_layout)
260
+ layout_seconds = time.time() - started
261
 
262
+ if not blocks:
263
+ report = (
264
+ f"No layout blocks were parsed in **{mode}** mode "
265
+ f"({layout_seconds:.1f}s). Raw layout output is in the *Blocks* tab."
266
+ )
267
+ return (
268
+ page,
269
+ "",
270
+ "",
271
+ [{"raw_layout_output": raw_layout}],
272
+ _write_markdown(""),
273
+ report,
274
+ )
275
 
276
+ # ---- stage 2: per-region recognition --------------------------------
277
+ block_images, prompts, params, indices = helper.prepare_for_extract(page, blocks)
278
+ params = [
279
+ _sampling_params(blocks[idx].type, max_new_tokens) for idx in indices
280
+ ]
281
+ if block_images:
282
+ outputs = client.client.batch_predict(block_images, prompts, params)
283
+ for idx, output in zip(indices, outputs):
284
+ blocks[idx].content = output
285
 
286
+ blocks = helper.post_process(blocks)
 
287
 
288
+ raw_markdown, display_markdown = blocks_to_markdown(page, blocks, drop_paratext)
289
+ overlay = draw_layout(page, blocks)
290
+ total_seconds = time.time() - started
291
 
292
+ counts: dict[str, int] = {}
293
+ for block in blocks:
294
+ counts[block.type] = counts.get(block.type, 0) + 1
295
+ summary = ", ".join(f"{count}\u00d7{name}" for name, count in sorted(counts.items()))
296
+ report = (
297
+ f"**{len(blocks)} regions** in `{mode}` mode \u2014 {summary}. \n"
298
+ f"Layout {layout_seconds:.1f}s \u00b7 total {total_seconds:.1f}s."
299
+ )
300
+
301
+ return (
302
+ overlay,
303
+ display_markdown,
304
+ raw_markdown,
305
+ [dict(block) for block in blocks],
306
+ _write_markdown(raw_markdown),
307
+ report,
308
+ )
309
 
310
 
311
  CSS = """
312
+ #col-container { max-width: 1400px; margin: 0 auto; }
313
  .dark .gradio-container { color: var(--body-text-color); }
314
+ #doc-md { overflow-x: auto; }
315
+ #doc-md table { border-collapse: collapse; }
316
+ #doc-md td, #doc-md th { border: 1px solid var(--border-color-primary); padding: 4px 8px; }
317
  """
318
 
319
+ LATEX = [
320
+ {"left": "$$", "right": "$$", "display": True},
321
+ {"left": "$", "right": "$", "display": False},
322
+ {"left": "\\(", "right": "\\)", "display": False},
323
+ {"left": "\\[", "right": "\\]", "display": True},
324
+ ]
325
 
326
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="NaviDC-OCR") as demo:
327
+ with gr.Column(elem_id="col-container"):
328
+ gr.Markdown(
329
+ """
330
+ # NaviDC-OCR — document parsing, digital *and* camera-captured
331
 
332
+ A 1.2B document-parsing VLM that reads layout, text, tables, formulas and code
333
+ off flat scans **and** photographed / crumpled pages, and returns Markdown.
 
 
 
 
 
 
 
 
334
 
335
+ [model](https://huggingface.co/StarDoc-AI/NaviDC-OCR) ·
336
+ [paper](https://huggingface.co/papers/2608.12898) ·
337
+ [code](https://github.com/caipeng328/NaviDC-OCR)
338
+ """
339
+ )
340
+ with gr.Row():
341
+ with gr.Column(scale=4):
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()
353
+ with gr.Accordion("Advanced settings", open=False):
354
+ drop_paratext = gr.Checkbox(
355
+ value=True,
356
+ label="Drop headers, footers, page numbers and margin notes",
357
+ )
358
+ max_new_tokens = gr.Slider(
359
+ 256, 4096, value=2048, step=128, label="Max new tokens per region"
360
+ )
361
+ with gr.Column(scale=6):
362
+ with gr.Tabs():
363
+ with gr.Tab("Document"):
364
+ document = gr.Markdown(
365
+ latex_delimiters=LATEX,
366
+ elem_id="doc-md",
367
+ show_copy_button=True,
368
+ )
369
+ with gr.Tab("Markdown source"):
370
+ markdown_source = gr.Code(
371
+ language="markdown", lines=28, interactive=False, label=None
372
+ )
373
+ with gr.Tab("Layout"):
374
+ overlay = gr.Image(label="Predicted regions (reading order)", height=620)
375
+ with gr.Tab("Blocks"):
376
+ blocks_json = gr.JSON(label="Blocks")
377
+ markdown_file = gr.DownloadButton("Download 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,
392
+ cache_mode="lazy",
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
 
403
+ if __name__ == "__main__":
404
+ demo.queue(max_size=16).launch(mcp_server=True)
examples/crumpled_page.jpg ADDED

Git LFS Details

  • SHA256: 86000b5847dca0464838c9672650b7688fa942d92c241b2ec778ca613cd25dcd
  • Pointer size: 131 Bytes
  • Size of remote file: 613 kB
examples/journal_page.jpg ADDED

Git LFS Details

  • SHA256: 2ae27f3a9284810bf7ce479ac8921359ccf00b7ebee1017b5e8da3e28ebe4af4
  • Pointer size: 131 Bytes
  • Size of remote file: 749 kB
requirements.txt CHANGED
@@ -1,20 +1,9 @@
1
- torch
2
  torchvision
3
- transformers
4
  accelerate
5
- sentencepiece
6
- safetensors
7
- qwen-vl-utils
8
- pillow>=11,<12
9
  numpy
10
- opencv-python-headless>=4.12,<5
 
 
11
  loguru
12
  tqdm
13
- pyclipper
14
- shapely
15
- fast-langdetect
16
- beautifulsoup4
17
- pydantic
18
- json-repair
19
- aiofiles
20
- httpx
 
1
+ transformers==4.57.1
2
  torchvision
 
3
  accelerate
 
 
 
 
4
  numpy
5
+ pillow
6
+ opencv-python-headless
7
+ pydantic
8
  loguru
9
  tqdm