ATH-MaaS/OvisOCR2

Hugging Face Models Trending Models

Summary

OvisOCR2 is a compact 0.8B end-to-end model for page-level document parsing, achieving state-of-the-art performance on OmniDocBench and PureDocBench benchmarks.

Task: image-text-to-text Tags: transformers, safetensors, qwen3_5, image-text-to-text, ocr, document-parsing, multimodal, markdown, tables, formulas, vllm, conversational, base_model:Qwen/Qwen3.5-0.8B, base_model:finetune:Qwen/Qwen3.5-0.8B, license:apache-2.0, endpoints_compatible, region:us
Original Article
View Cached Full Text

Cached at: 07/14/26, 10:15 PM

ATH-MaaS/OvisOCR2 · Hugging Face

Source: https://huggingface.co/ATH-MaaS/OvisOCR2 Ovis

https://huggingface.co/ATH-MaaS/OvisOCR2#introductionIntroduction

We are pleased to announce the release of OvisOCR2, a compact 0.8B end-to-end model for page-level document parsing. Given a document page image, OvisOCR2 generates a Markdown representation in natural reading order, covering text, formulas, tables, and visual regions.

OvisOCR2 is developed by post-training Qwen3.5-0.8B using a carefully designed data engine that combines real-world and synthetic data, together with a multi-stage training recipe integrating SFT, RL, and OPD. The model delivers strong document parsing performance while maintaining a small deployment footprint.

OvisOCR2 achieves an overall score of 96.58 on OmniDocBench v1.6, establishing a new state of the art andbecoming the first end-to-end model to top this leaderboard previously dominated by pipeline methods. On PureDocBench, OvisOCR2 also achieves the highest Avg3 score of 75.06.

Performance of OvisOCR2 on OmniDocBench v1.6

https://huggingface.co/ATH-MaaS/OvisOCR2#performancePerformance

OmniDocBench v1.6 comparison

PureDocBench comparison

https://huggingface.co/ATH-MaaS/OvisOCR2#inferenceInference

pip install "vllm==0.22.1" pillow
from PIL import Image
from vllm import LLM, SamplingParams

class OvisOCR2Parser:
    def __init__(self, model_name_or_path: str):
        self.model = LLM(
            model=model_name_or_path,
            tensor_parallel_size=1,
            gpu_memory_utilization=0.8,
            gdn_prefill_backend="triton"
        )

        prompt = '\nExtract all readable content from the image in natural human reading order and output the result as a single Markdown document. For charts or images, represent them using an HTML image tag: <' + 'img src="images/bbox_{left}_{top}_{right}_{bottom}.jpg" />, where left, top, right, bottom are bounding box coordinates scaled to [0, 1000). Format formulas as LaTeX. Format tables as HTML: <table>...</table>. Transcribe all other text as standard Markdown. Preserve the original text without translation or paraphrasing.'
        self.prompt = self.model.get_tokenizer().apply_chat_template(
            [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": prompt}]}],
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=False
        )

        self.sampling_params = SamplingParams(
            max_tokens=16384,
            temperature=0.0
        )

    def _clean_truncated_repeats(
        self,
        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

    def parse(self, images: list[Image.Image], filter_imgtags: bool = True) -> list[str]:
        vllm_inputs = [
            {
                "prompt": self.prompt,
                "multi_modal_data": {"image": image},
                "mm_processor_kwargs": {
                    "images_kwargs": {
                        "min_pixels": 448 * 448,
                        "max_pixels": 2880 * 2880
                    }
                }
            }
            for image in images
        ]

        outputs = self.model.generate(vllm_inputs, self.sampling_params)

        markdowns = []
        for output in outputs:
            text = output.outputs[0].text.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_')
                )
            markdowns.append(self._clean_truncated_repeats(text))

        return markdowns

if __name__ == "__main__":
    parser = OvisOCR2Parser("ATH-MaaS/OvisOCR2")
    images = [Image.open("test1.jpg"), Image.open("test2.jpg")]
    markdowns = parser.parse(images)
    print(markdowns[0])

By default,parseremoves HTML image tags for visual regions. To render Markdown with visual regions, setfilter\_imgtags=Falseand save the Markdown file together with the referenced image crops as follows:

import re
from pathlib import Path

from PIL import Image

BBOX_IMAGE_PATTERN = re.compile(
    r'<img src=' + r'"images/bbox_(\d+)_(\d+)_(\d+)_(\d+)\.jpg" />'
)

def save_renderable_markdown_with_visual_regions(
    markdown: str,
    page_image: Image.Image,
    output_dir: str,
) -> None:
    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 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:
            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")

parser = OvisOCR2Parser("ATH-MaaS/OvisOCR2")
page_image = Image.open("test1.jpg")
markdown = parser.parse([page_image], filter_imgtags=False)[0]
save_renderable_markdown_with_visual_regions(markdown, page_image, "output")

https://huggingface.co/ATH-MaaS/OvisOCR2#citationCitation

If you find OvisOCR2 useful, please consider citing our technical report:

@misc{lu2026ovisocr2,
  title        = {{OvisOCR2 Technical Report}},
  author       = {Lu, Shiyin and Li, Yinglun and Xia, Yu and Chen, Yuhui and Ji, An-Yang and Jiang, Jun-Peng and Chen, Qing-Guo and Zhao, Jianshan and Lin, En and Li, Haijun and Qin, Cheng and Xu, Zhao and Luo, Weihua},
  year         = {2026}
}

https://huggingface.co/ATH-MaaS/OvisOCR2#licenseLicense

This project is licensed under theApache License, Version 2.0(SPDX-License-Identifier: Apache-2.0).

https://huggingface.co/ATH-MaaS/OvisOCR2#disclaimerDisclaimer

We used filtering and quality-assurance procedures during data construction to reduce parsing errors such as repeated outputs, incomplete content, invalid table/formula structures, and reading-order inconsistencies. Due to the diversity and complexity of real-world documents, OvisOCR2 may still produce incorrect or incomplete outputs. Please manually verify results in critical applications.

Similar Articles

OvisOCR2: a promising 0.8B local document parser

Reddit r/LocalLLaMA

OvisOCR2 is a new 0.8B end-to-end OCR model based on Qwen3.5-0.8B that converts full document pages directly into structured Markdown, including text, tables, formulas, and reading order. It achieves strong benchmark scores and is released under Apache 2.0 with vLLM support.

OvisOCR2 Technical Report

Hugging Face Daily Papers

OvisOCR2 is a 0.8B parameter end-to-end document parsing model that converts document page images to Markdown, achieving state-of-the-art scores on public benchmarks through a combination of supervised fine-tuning, reinforcement learning, and model fusion.

@berryxia: https://x.com/berryxia/status/2067078380017828205

X AI KOLs Timeline

The author tested the three tiers of PP-OCRv6 models and provided open-source tools for local deployment. They demonstrated performance comparisons of each model on OmniDocBench and real-world scenarios, emphasizing the advantages of lightweight specialized models for OCR tasks.