Image-Text-to-Text
Transformers
Safetensors
qwen3_5
ocr
document-parsing
multimodal
markdown
tables
formulas
key-information-extraction
figure-captioning
vllm
conversational
Instructions to use sekkit/OvisOCR3 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use sekkit/OvisOCR3 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="sekkit/OvisOCR3") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("sekkit/OvisOCR3") model = AutoModelForMultimodalLM.from_pretrained("sekkit/OvisOCR3", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use sekkit/OvisOCR3 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "sekkit/OvisOCR3" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "sekkit/OvisOCR3", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/sekkit/OvisOCR3
- SGLang
How to use sekkit/OvisOCR3 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "sekkit/OvisOCR3" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "sekkit/OvisOCR3", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "sekkit/OvisOCR3" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "sekkit/OvisOCR3", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use sekkit/OvisOCR3 with Docker Model Runner:
docker model run hf.co/sekkit/OvisOCR3
File size: 27,267 Bytes
00f7cea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 | ---
license: apache-2.0
library_name: transformers
pipeline_tag: image-text-to-text
base_model: Qwen/Qwen3.5-0.8B
tags:
- ocr
- document-parsing
- multimodal
- markdown
- tables
- formulas
- key-information-extraction
- figure-captioning
- vllm
- qwen3_5
---
# OvisOCR3
<p align="center">
<img src="https://cdn-uploads.huggingface.co/production/uploads/658a8a837959448ef5500ce5/vRCIu5QD8VuIJolkC_ZHQ.png" alt="Ovis" width="30%" />
</p>
<p align="center">
<a href="https://arxiv.org/abs/2610.33066">Technical Report</a>
|
<a href="https://huggingface.co/spaces/ATH-MaaS/OvisOCR3">Online Demo</a>
</p>
## Introduction
OvisOCR3 is a **unified 0.8B multimodal model for OCR and document understanding**. It integrates **document parsing, general OCR, key information extraction, document figure captioning, and cross-page table reconstruction** into a single compact model. These capabilities are selected through task-specific prompts, bringing recognition, extraction, and understanding together without requiring separate model deployments for each task.
Built on Qwen3.5-0.8B, OvisOCR3 is trained in four stages: continued pretraining, mid-training, on-policy distillation, and reinforcement learning. This training process builds a strong foundation in text recognition and document structure understanding, then refines these capabilities through domain-specific supervision and rewards.
OvisOCR3 achieves **state-of-the-art performance among models of comparable size across all five domains**, demonstrating that a single compact model can combine strong document parsing with a broad range of OCR and document-understanding capabilities.

## Capabilities
| Capability | Input | Output |
| --- | --- | --- |
| Document parsing | A page image and a parsing-mode prompt | Structured JSON, layout JSON, or Markdown with LaTeX formulas and HTML tables |
| General OCR | An image, optionally with a region, references, a question, a target language, or candidate labels | Text, bounding boxes, LaTeX, chart data, a label, or an answer |
| Key information extraction | An image, optionally with specified fields or a JSON output template | Key-value records or JSON following the supplied template |
| Document figure captioning | A page image and a document-figure-caption-mode prompt | Structured page content with localized visual captions and Mermaid representations for flowcharts |
| Cross-page table reconstruction | Consecutive page images and their parsed Markdown | Complete HTML tables with source-page associations, or a no-continuation prediction |
The [task prompts](./prompts/) specify the output contracts. The examples below use these files directly.
## Performance
Performance comparisons across five task domains are reported below. `--` denotes an unreported value, not zero. Bold scores indicate the best result in each column.
### Document Parsing
| Model | OmniDocBench1.6 | MDPBench | PureDocBench | Real5-OmniDocBench |
|---|---:|---:|---:|---:|
| Qwen3.5-OCR | 91.27 | 60.60 | 68.38 | 82.48 |
| Qwen3.8-Max | 93.70 | 86.40 | 80.56 | 86.86 |
| GLM-OCR | 95.22 | 67.30 | 63.34 | 90.32 |
| PaddleOCR-VL-1.6 | 96.34 | 78.90 | 65.44 | 93.19 |
| MonkeyOCRv2-B-Parsing | 91.57 | 83.30 | 64.91 | 89.22 |
| OvisOCR2 | 96.58 | 69.00 | 75.10 | 92.29 |
| HunyuanOCR-1.5 | 94.74 | 76.80 | 67.58 | 88.53 |
| Xiaomi-OCR-0 | **96.83** | 73.00 | 74.60 | 92.73 |
| **OvisOCR3** | 96.74 | **88.80** | **82.31** | **93.63** |
### General OCR
| Model | OCRBench | OCRBench v2 EN | OCRBench v2 ZH |
| --- | ---: | ---: | ---: |
| Qwen3.5-0.8B | 79.1 | 41.5 | 43.9 |
| Qwen3.5-2B | 85.4 | 48.1 | 55.4 |
| Qwen3.5-4B | 85.0 | 54.1 | 61.0 |
| Qwen3.5-OCR | 76.8 | 44.5 | 43.8 |
| PaddleOCR-VL-1.6 | 55.9 | 23.8 | 30.4 |
| HunyuanOCR-1.5 | 86.1 | 50.7 | 57.7 |
| GLM-OCR | 83.8 | 32.9 | 48.1 |
| Xiaomi-OCR-0 | 84.6 | 40.1 | 48.4 |
| **OvisOCR3** | **88.8** | **65.9** | **61.3** |
### Key Information Extraction
| Model | OCRBench KIE | OCRBench v2 KIE | CC-OCR KIE | In-house KIE Bench |
| --- | ---: | ---: | ---: | ---: |
| Qwen3.5-OCR | 78.50 | 72.90 | 88.43 | 59.90 |
| GLM-OCR | 79.00 | 74.22 | 68.36 | 75.25 |
| HunyuanOCR-1.5 | 81.00 | 68.63 | 66.08 | 75.96 |
| Xiaomi-OCR-0 | 76.50 | 74.66 | 79.53 | 75.68 |
| **OvisOCR3** | **90.50** | **83.01** | **88.69** | **77.73** |
### Document Figure Captioning
| Model | DocFigCapBench Localization F1 | DocFigCapBench Caption Score |
| --- | ---: | ---: |
| HunyuanOCR-1.5 | 48.24 | -- |
| Qwen3.5-OCR | 78.47 | 4.74 |
| OvisOCR2 | 82.18 | -- |
| MinerU2.5-Pro | 85.00 | 4.61 |
| PaddleOCR-VL-1.6 | 85.32 | 4.29 |
| Qwen3.8-Max | 86.67 | **7.30** |
| **OvisOCR3** | **90.97** | 6.93 |
### Cross-Page Table Reconstruction
| Model | CrossTabBench Relation F1 (P/R) | CrossTabBench Reconstruction TEDS | MPDocBench-Parse Truncated Table TEDS |
| :--- | ---: | ---: | ---: |
| PaddleOCR-VL-1.6 | 42.61 (90.13/27.90) | 24.24 | 83.39 |
| Logics-Parsing-V3 | 44.55 (90.63/29.53) | 26.10 | 88.05 |
| MinerU2.5-Pro | 59.14 (93.81/43.18) | 41.02 | 91.44 |
| MinerU2.5-Pro + MinerU-PoPo | 54.76 (97.40/38.09) | 36.42 | 91.87 |
| Qwen3.8-Max | 94.47 (90.19/99.19) | **91.71** | 89.57 |
| **OvisOCR3** | **97.07** (96.20/97.96) | 89.66 | **92.04** |
## Inference
The example uses a local model directory containing the weights, configuration, tokenizer, processor files, and `prompts/`. Run it from that directory, or set `MODEL_PATH` to its location. A GPU environment supported by vLLM is required.
```bash
pip install "vllm==0.22.1" pillow
```
```python
import warnings
from pathlib import Path
from PIL import Image
from vllm import LLM, SamplingParams
MODEL_PATH = Path(".")
model = LLM(
model=str(MODEL_PATH.resolve()),
tensor_parallel_size=1,
gpu_memory_utilization=0.8,
gdn_prefill_backend="triton",
limit_mm_per_prompt={"image": 2},
)
tokenizer = model.get_tokenizer()
sampling_params = SamplingParams(max_tokens=16384, temperature=0.0)
def load_prompt(name: str) -> str:
return (MODEL_PATH / "prompts" / f"{name}.txt").read_text(encoding="utf-8")
def infer(
images: list[Image.Image], task_prompt: str, *,
return_metadata=False,
):
if not 1 <= len(images) <= 2:
raise ValueError("This example accepts one or two images per request.")
content = [{"type": "image"} for _ in images]
content.append({"type": "text", "text": task_prompt})
prompt = tokenizer.apply_chat_template(
[{"role": "user", "content": content}],
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
request = {
"prompt": prompt,
"multi_modal_data": {"image": [image.convert("RGB") for image in images]},
"mm_processor_kwargs": {
"images_kwargs": {
"min_pixels": 448 * 448,
"max_pixels": 2880 * 2880,
}
},
}
output = model.generate([request], sampling_params)[0].outputs[0]
if output.finish_reason == "length":
warnings.warn("Generation reached a token limit; the output may be incomplete.")
return (output.text, output.finish_reason) if return_metadata else output.text
page = Image.open("test1.jpg").convert("RGB")
text = infer([page], load_prompt("plain_ocr"))
Path("output.txt").write_text(text, encoding="utf-8")
print(text)
```
`infer` returns the raw generated text by default; `return_metadata=True` also returns the finish reason. It does not remove image tags, trim repetitions, repair JSON, or otherwise rewrite the response. All task examples share the inference settings above. These are usage settings, not benchmark reproduction settings.
An image list passed to `infer` belongs to **one request**. For independent pages, call `infer([page], prompt)` for each page. The two-image form is used for cross-page reconstruction below.
## Task-Specific Usage
The following snippets reuse `page`, `infer`, and `load_prompt` from the inference example. Replace placeholder values with the fields, regions, or questions needed for your document. Use literal placeholder replacement rather than Python `str.format`, since prompts also contain JSON and LaTeX braces.
### Document Parsing
Compared with OvisOCR2's Markdown parsing, OvisOCR3 adds structured document parsing and document layout detection. Structured parsing returns region-level records with bounding boxes, categories, and content; Markdown parsing returns a continuous document without explicit bounding boxes or category labels for individual text regions.
| Mode | Prompt | Output |
| --- | --- | --- |
| Markdown | [document_markdown.txt](./prompts/document_markdown.txt) | A document in natural reading order, with LaTeX formulas, HTML tables, and image-region references |
| Structured | [document_structured.txt](./prompts/document_structured.txt) | A JSON array with `bbox`, `category`, and `content` for each element |
| Layout only | [document_layout.txt](./prompts/document_layout.txt) | A JSON array with `bbox` and `category`, without transcription |
For `document_markdown`, follow the [OvisOCR2](https://huggingface.co/ATH-MaaS/OvisOCR2) postprocessing order: strip leading and trailing whitespace, optionally filter image-tag blocks (enabled by default), and then apply repeated-tail cleanup to every response regardless of the finish reason. Set `filter_imgtags=False` to retain visual-region references for rendering. Keep `raw_markdown` for inspection; structured and layout JSON outputs are left unchanged. The helper checks its own length and repetition thresholds. It does not recover missing content or make a truncated response complete.
```python
def _clean_truncated_repeats(
text: str,
min_text_len: int = 8000,
max_period: int = 200,
min_period: int = 1,
min_repeat_chars: int = 100,
min_repeat_times: int = 5,
) -> str:
n = len(text)
if n < min_text_len:
return text
max_period = min(max_period, n - 1)
for unit_len in range(min_period, max_period + 1):
if text[n - 1] != text[n - 1 - unit_len]:
continue
match_len = 1
idx = n - 2
while idx >= unit_len and text[idx] == text[idx - unit_len]:
match_len += 1
idx -= 1
total_len = match_len + unit_len
repeat_times = total_len // unit_len
tail_len = total_len % unit_len
if repeat_times >= min_repeat_times and total_len >= min_repeat_chars:
return text[: n - total_len + unit_len] + text[n - tail_len :]
return text
# Format 1: Markdown, with optional image-tag filtering and repeated-tail cleanup.
raw_markdown = infer([page], load_prompt("document_markdown"))
filter_imgtags = True
text = raw_markdown.strip()
if filter_imgtags:
text = "\n\n".join(
block
for block in text.split("\n\n")
if not block.strip().startswith('<img src="images/bbox_')
)
markdown = _clean_truncated_repeats(text)
# Format 2: Structured JSON with bbox, category, and content.
structured_json = infer([page], load_prompt("document_structured"))
# Format 3: Layout JSON with bbox and category only, without transcription.
layout_json = infer([page], load_prompt("document_layout"))
```
Structured and layout outputs follow natural reading order. The allowed category lists and category-specific content formats are defined in their respective prompts.
#### Coordinates and Visual Regions
Bounding boxes use `[left, top, right, bottom]` order with integer coordinates in `[0, 1000)`, relative to the image supplied to the model. The origin is the top-left corner. Convert to pixels with `x * image_width / 1000` and `y * image_height / 1000`. For cropped inputs, coordinates are relative to that crop, not the original page.
Markdown and structured parsing can reference visual regions using `<img src="images/bbox_left_top_right_bottom.jpg" />`. These paths describe crops to create from the input image; the model does not write image files. Preserve the image orientation used for inference when extracting crops.
<details>
<summary>Save Markdown together with its referenced image crops</summary>
```python
import re
BBOX_IMAGE_PATTERN = re.compile(r'images/bbox_(\d+)_(\d+)_(\d+)_(\d+)\.jpg')
def save_markdown_with_crops(markdown: str, page_image: Image.Image, output_dir: str):
output_dir = Path(output_dir)
images_dir = output_dir / "images"
images_dir.mkdir(parents=True, exist_ok=True)
width, height = page_image.size
for left, top, right, bottom in set(BBOX_IMAGE_PATTERN.findall(markdown)):
x1 = max(0, min(width, round(int(left) * width / 1000)))
y1 = max(0, min(height, round(int(top) * height / 1000)))
x2 = max(0, min(width, round(int(right) * width / 1000)))
y2 = max(0, min(height, round(int(bottom) * height / 1000)))
if x2 <= x1 or y2 <= y1:
warnings.warn(f"Skipping invalid crop: {(left, top, right, bottom)}")
continue
crop_path = images_dir / f"bbox_{left}_{top}_{right}_{bottom}.jpg"
page_image.crop((x1, y1, x2, y2)).convert("RGB").save(crop_path)
(output_dir / "output.md").write_text(markdown, encoding="utf-8")
save_markdown_with_crops(markdown, page, "output")
```
This helper handles single-page Markdown references. For reconstructed tables, resolve each `page_N/` prefix against its corresponding source page and save crops under that page's directory.
</details>
### General OCR
<a id="general-ocr-prompts"></a>
| Task | Prompt | Output |
| --- | --- | --- |
| Plain OCR | [plain_ocr.txt](./prompts/plain_ocr.txt) | Text without markup or bounding boxes |
| Word-level text spotting | [word_spotting.txt](./prompts/word_spotting.txt) | JSON records with `bbox` and `text`, one per word or text token |
| Reference grounding | [reference_grounding.txt](./prompts/reference_grounding.txt) | JSON records with `bbox` and the requested `reference` |
| Region OCR | [region_ocr.txt](./prompts/region_ocr.txt) | Text within a specified region |
| Formula recognition | [formula_recognition.txt](./prompts/formula_recognition.txt) | LaTeX expressions enclosed in `$$ ... $$` |
| Chart parsing | [chart_parsing.txt](./prompts/chart_parsing.txt) | JSON with `title`, `source`, `x_title`, `y_title`, and `values` |
| Text translation | [text_translation.txt](./prompts/text_translation.txt) | Text from a specified region translated into the target language |
| Image classification | [image_classification.txt](./prompts/image_classification.txt) | Exactly one label from the supplied candidates |
| Visual question answering | [visual_question_answering.txt](./prompts/visual_question_answering.txt) | Reasoning followed by a final answer inside `\boxed{}` |
| Position-aware VQA | [position_aware_vqa.txt](./prompts/position_aware_vqa.txt) | JSON with `answer` and a string-valued `bbox` |
All ten tasks read the linked `.txt` prompt files. Image classification takes a target description and candidate labels, and position-aware VQA uses its own answer-and-box instruction. Fill only the requested task inputs in each template.
```python
import re
def general_ocr_prompt(name: str) -> str:
prompt = load_prompt(name)
return prompt if name == "position_aware_vqa" else prompt.strip()
def general_vqa_prompt(question: str) -> str:
prompt = general_ocr_prompt("visual_question_answering").replace("{question}", question)
return re.sub(r"\n{3,}", "\n\n", prompt).strip()
```
```python
import json
text = infer([page], general_ocr_prompt("plain_ocr"))
words_json = infer([page], general_ocr_prompt("word_spotting"))
references = json.dumps(["Invoice number", "Total"], ensure_ascii=False, separators=(",", ":"))
grounding_prompt = general_ocr_prompt("reference_grounding").replace("{references}", references)
grounding_json = infer([page], grounding_prompt)
region = json.dumps([100, 100, 900, 300], separators=(",", ":"))
region_prompt = general_ocr_prompt("region_ocr").replace("{target_region}", region)
region_text = infer([page], region_prompt)
formula_latex = infer([page], general_ocr_prompt("formula_recognition"))
chart_json = infer([page], general_ocr_prompt("chart_parsing"))
translation_prompt = general_ocr_prompt("text_translation").replace("{target_region}", region)
translation_prompt = translation_prompt.replace("{target_language}", "English")
translated_text = infer([page], translation_prompt)
target_description = "the main subject"
candidate_labels = json.dumps(["cat", "dog", "bird", "fish"], ensure_ascii=False)
classification_prompt = general_ocr_prompt("image_classification").replace(
"{target description}", target_description
).replace("{candidate_labels}", candidate_labels)
selected_label = infer([page], classification_prompt)
question = "What is the invoice number?"
vqa_prompt = general_vqa_prompt(question)
answer_with_reasoning = infer([page], vqa_prompt)
position_prompt = general_ocr_prompt("position_aware_vqa").replace("{question}", question)
answer_json = infer([page], position_prompt)
```
References and regions use compact JSON. Supply references expected to be visible in the image. For classification, `{target description}` specifies what to classify, and `{candidate_labels}` is a JSON array of candidate labels. VQA accepts the complete question, including answer choices when applicable; its visible reasoning ends with a boxed answer. Position-aware VQA accepts the question text and uses its own answer-and-box prompt. Formula recognition and chart parsing need no template substitutions.
Spotting and grounding boxes, as well as input regions, use `[x_min,y_min,x_max,y_max]` with integer coordinates in `[0,1000)`, normalized independently by image width and height. The origin is the top-left corner, and coordinates refer to the supplied image, including when it is a crop. Use `[0,0,999,999]` for whole-image region OCR or translation.
Chart parsing represents scalar data values as strings and uses the literal string `"None"` for missing title, source, or axis metadata. Position-aware VQA uses `"bbox": "(x1,y1,x2,y2)"`, unlike the numeric arrays used by spotting and grounding.
### Key Information Extraction
| Mode | Prompt | Additional input |
| --- | --- | --- |
| Open extraction | [kie_open.txt](./prompts/kie_open.txt) | None; extract all visible key-value pairs |
| Specified-field extraction | [kie_fields.txt](./prompts/kie_fields.txt) | `{keys_json}`: a JSON array of field names |
| Schema-based extraction | [kie_schema.txt](./prompts/kie_schema.txt) | `{schema_json}`: a JSON template specifying the desired output structure |
Schema-based extraction uses an example of the desired JSON output, not a formal JSON Schema declaration. Empty strings serve as value placeholders, while nested objects and arrays define the output structure.
```python
import json
open_json = infer([page], load_prompt("kie_open"))
keys = json.dumps(["invoice_number", "date", "total"], ensure_ascii=False)
fields_prompt = load_prompt("kie_fields").replace("{keys_json}", keys)
fields_json = infer([page], fields_prompt)
schema = {
"invoice_number": "",
"date": "",
"seller": {
"name": "",
"address": "",
},
"items": [
{
"name": "",
"quantity": "",
"unit_price": "",
"amount": "",
}
],
"total": "",
}
schema_prompt = load_prompt("kie_schema").replace(
"{schema_json}", json.dumps(schema, ensure_ascii=False)
)
schema_json = infer([page], schema_prompt)
```
Values should retain the visible source text. Open and specified-field extraction use arrays for repeated values and empty strings for empty values; specified-field extraction also uses empty strings for missing fields. Schema-based extraction follows the supplied template and uses empty strings for missing values. Adherence to the template is prompted, not enforced by constrained decoding in this example; validate generated JSON before downstream use.
### Document Figure Captioning
Use the **full page image** with [figure_captioning.txt](./prompts/figure_captioning.txt):
```python
caption_json = infer([page], load_prompt("figure_captioning"))
```
The model returns a structured page JSON array with `bbox`, `category`, and `content`. For visual categories such as `image` and `chart`, `content` contains a concise description inside `<caption>...</caption>`, with a maximum of 100 words requested by the prompt. These captions replace the image references used for visual regions in structured document parsing. Flowcharts additionally include a Mermaid representation inside `<mermaid>...</mermaid>`. Other page elements retain their task-specific text, formula, or table content.
This is localized figure description within a document page, not just transcription of a printed figure caption.
### Cross-Page Table Reconstruction
OvisOCR3 reconstructs cross-page tables in two stages using the same checkpoint:
1. **Stage 1: page parsing.** Parse each page independently with [document_markdown.txt](./prompts/document_markdown.txt) or [document_structured.txt](./prompts/document_structured.txt). For structured JSON output, concatenate each element’s `content` in reading order to obtain Markdown, preserving HTML tables, formulas, and image references.
2. **Stage 2: table reconstruction.** Supply two adjacent page images and their parsed Markdown with [crosspage_table.txt](./prompts/crosspage_table.txt). The model identifies whether a table continues across the page boundary and reconstructs continued fragments as a single HTML table; otherwise, it returns a negative prediction.
```python
import re
pages = [
Image.open("page1.jpg").convert("RGB"),
Image.open("page2.jpg").convert("RGB"),
]
page_results = [
infer([image], load_prompt("document_markdown"), return_metadata=True)
for image in pages
]
page_markdowns = []
for index, (raw, reason) in enumerate(page_results, start=1):
if reason not in ("stop", "length"):
raise ValueError(f"Page {index}: finish_reason={reason}")
markdown = _clean_truncated_repeats(raw.strip())
markdown = re.sub(r'^```(?:markdown|html)?\s*\n|\n```\s*$', '', markdown.strip(), flags=re.I)
if not markdown:
raise ValueError(f"Page {index}: empty Markdown")
page_markdowns.append(re.sub(
r'(?<![\w/])(?:page_\d+/)?(images/bbox_\d+_\d+_\d+_\d+\.jpg)',
rf'page_{index}/\1', markdown,
))
if all(re.search(r'<table\b[^>]*>.*?</table\s*>', md, re.I | re.S) for md in page_markdowns):
page_context = "\n\n".join(
f'<INPUT_PAGE index="{index}">\n{markdown}\n</INPUT_PAGE>'
for index, markdown in enumerate(page_markdowns, start=1)
)
prompt = load_prompt("crosspage_table").removesuffix("\n") + "\n\nInput Markdown:\n\n" + page_context
tables_json, reason = infer(pages, prompt, return_metadata=True)
if reason != "stop":
raise ValueError(f"Cross-page output is incomplete: finish_reason={reason}")
else:
tables_json = '[{"source_pages": [], "html": "No cross-page table exists between these pages."}]'
print(tables_json)
```
The result is a JSON array. Each record has `source_pages`, using **input-local, 1-based page numbers**, and `html`, containing one complete HTML table. Images within tables retain page-qualified references such as `page_1/images/bbox_100_200_300_400.jpg`.
When no cross-page table exists, the specified response is:
```json
[{"source_pages": [], "html": "No cross-page table exists between these pages."}]
```
For tables spanning more than two pages, **Stage 2 is applied recursively**, processing adjacent pairs from the end of each consecutive sequence of table-bearing pages toward its beginning. After each merge, the reconstructed table is written into the earlier page and its updated Markdown is re-rendered before the next step, keeping the image and text consistent. For example, the model first merges pages 2 and 3, then pairs the updated page 2 with page 1. Each step uses only two page images while preserving the original source-page associations. A ready-to-run document-level implementation is available in [document_crosspage.py](./document_crosspage.py), supporting either `document_markdown.txt` or `document_structured.txt` for Stage 1 and converting structured JSON to Markdown before reconstruction.

*Stage 2 first reconstructs the table across pages 2 and 3, then pairs the re-rendered page 2 with page 1. Each step uses two page images and their current Markdown.*
The script uses the same vLLM version and inference settings as the example above. Install the additional dependencies and run the command below from the model repository root, supplying page images in reading order.
```bash
python -m pip install beautifulsoup4 markdown-it-py playwright
python -m playwright install --with-deps chromium
python document_crosspage.py \
--model . \
--stage1-prompt document_structured.txt \
--output crosspage_output \
page1.jpg page2.jpg page3.jpg
```
Use a new output directory for each run; it contains `document.md`, per-page Markdown, raw responses, and `result.json` with table provenance and any processing errors.
## Output Handling
### Validation and Rendering
- Preserve raw outputs before parsing, rendering, or applying cleanup. JSON, HTML, LaTeX, and Mermaid may need validation, and generation can stop before an output is complete.
- Render Markdown with support for HTML tables and LaTeX. Figure captions and Mermaid content require their own handling inside structured JSON.
- Treat generated HTML and Mermaid as untrusted content. Sanitize or isolate them before rendering in a browser.
- For evaluation, freeze prompts, preprocessing, generation parameters, and postprocessing. These usage examples do not define a benchmark protocol.
## Limitations
OvisOCR3 can produce incorrect, incomplete, repeated, or malformed outputs. Small or blurred text, severe perspective distortion, dense layouts, complex formulas, and long tables may affect recognition and structure recovery. Captions and answers can include unsupported details; verify them against the source image.
Cross-page reconstruction also depends on the quality of the first-stage page parsing and correct page order. Missing table fragments, inconsistent page context, or truncated generations can lead to incorrect merges. Review results before using them in critical applications.
## Technical Report and Citation
If you find OvisOCR3 useful, please consider citing our technical report:
```bibtex
@article{xia2026ovisocr3,
title={OvisOCR3 Technical Report},
author={Yu Xia and Yinglun Li and Yuhui Chen and Jinlong Huang and Yuyang Wanyan and Shengze Shi and Yifei Song and Yue Duan and Shubo Lin and Jianshan Zhao and En Lin and Shiyin Lu and Qing-Guo Chen and Zhao Xu and Weihua Luo},
journal={arXiv preprint arXiv:2610.33066},
year={2026}
}
```
## License
This project is licensed under the [Apache License, Version 2.0](./LICENSE) (SPDX-License-Identifier: Apache-2.0).
|