import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # noqa: E402 (must precede torch / CUDA-touching imports)
import torch # noqa: E402
import gradio as gr # noqa: E402
from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration # noqa: E402
MODEL_ID = "ATH-MaaS/OvisOCR2"
# The exact page-parsing system prompt from the OvisOCR2 model card.
OCR_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:
, '
"where left, top, right, bottom are bounding box coordinates scaled to [0, 1000). "
"Format formulas as LaTeX. Format tables as HTML:
. "
"Transcribe all other text as standard Markdown. Preserve the original text "
"without translation or paraphrasing."
)
# Match the reference implementation's pixel budget.
MIN_PIXELS = 448 * 448
MAX_PIXELS = 2880 * 2880
# ----------------------------------------------------------------------------
# Load model + processor at module scope, eagerly on CUDA (ZeroGPU pattern).
# ----------------------------------------------------------------------------
processor = AutoProcessor.from_pretrained(
MODEL_ID,
min_pixels=MIN_PIXELS,
max_pixels=MAX_PIXELS,
)
model = Qwen3_5ForConditionalGeneration.from_pretrained(
MODEL_ID,
dtype=torch.bfloat16,
attn_implementation="sdpa",
).to("cuda")
model.eval()
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:
"""Remove truncated repetition artifacts from generated text (from model card)."""
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 _filter_img_tags(text: str) -> str:
"""Drop the placeholder
blocks (from model card)."""
return "\n\n".join(
block
for block in text.split("\n\n")
if not block.strip().startswith('
bbox blocks the model emits for figures.
max_new_tokens: Maximum number of tokens to generate for the transcription.
Returns:
The extracted document content as a Markdown string.
"""
if image is None:
return ""
image = image.convert("RGB")
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": OCR_PROMPT},
],
}
]
inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
enable_thinking=False,
).to(model.device)
with torch.inference_mode():
generated = model.generate(
**inputs,
max_new_tokens=int(max_new_tokens),
do_sample=False,
temperature=None,
top_p=None,
top_k=None,
)
trimmed = generated[:, inputs["input_ids"].shape[1]:]
text = processor.batch_decode(
trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)[0].strip()
if filter_image_tags:
text = _filter_img_tags(text)
text = _clean_truncated_repeats(text)
return text
CSS = """
#col-container { max-width: 1200px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks() as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(
"""
# 📄 OvisOCR2 — Document → Markdown
End-to-end, page-level document parsing with
[**ATH-MaaS/OvisOCR2**](https://huggingface.co/ATH-MaaS/OvisOCR2) — a compact
0.9B vision-language model (Qwen3.5 backbone). Upload a document page and it
transcribes the content into a single **Markdown** document, formatting
formulas as LaTeX and tables as HTML.
"""
)
with gr.Row():
with gr.Column():
image_in = gr.Image(type="pil", label="Document image", height=460)
run = gr.Button("Extract Markdown", variant="primary")
with gr.Accordion("Advanced settings", open=False):
filter_tags = gr.Checkbox(
value=True,
label="Filter figure placeholder
tags",
)
max_tokens = gr.Slider(
512, 8192, value=4096, step=512,
label="Max new tokens",
)
with gr.Column():
md_out = gr.Code(
label="Extracted Markdown",
language="markdown",
lines=22,
)
with gr.Accordion("Rendered preview", open=False):
md_render = gr.Markdown()
def _run(image, filter_image_tags, max_new_tokens):
text = parse_document(image, filter_image_tags, max_new_tokens)
return text, text
def _run_example(image):
text = parse_document(image, True, 4096)
return text, text
run.click(
_run,
inputs=[image_in, filter_tags, max_tokens],
outputs=[md_out, md_render],
api_name="parse",
)
gr.Examples(
examples=[
["examples/academic_paper.jpg"],
["examples/math_textbook.png"],
["examples/magazine_page.png"],
],
inputs=[image_in],
outputs=[md_out, md_render],
fn=_run_example,
cache_examples=False,
run_on_click=True,
)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)