Instructions to use typhoon-ai/typhoon-ocr2-0.8b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use typhoon-ai/typhoon-ocr2-0.8b with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="typhoon-ai/typhoon-ocr2-0.8b") 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("typhoon-ai/typhoon-ocr2-0.8b") model = AutoModelForMultimodalLM.from_pretrained("typhoon-ai/typhoon-ocr2-0.8b", 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 typhoon-ai/typhoon-ocr2-0.8b with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "typhoon-ai/typhoon-ocr2-0.8b" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "typhoon-ai/typhoon-ocr2-0.8b", "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/typhoon-ai/typhoon-ocr2-0.8b
- SGLang
How to use typhoon-ai/typhoon-ocr2-0.8b 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 "typhoon-ai/typhoon-ocr2-0.8b" \ --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": "typhoon-ai/typhoon-ocr2-0.8b", "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 "typhoon-ai/typhoon-ocr2-0.8b" \ --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": "typhoon-ai/typhoon-ocr2-0.8b", "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 typhoon-ai/typhoon-ocr2-0.8b with Docker Model Runner:
docker model run hf.co/typhoon-ai/typhoon-ocr2-0.8b
Typhoon OCR V2 0.8B
Typhoon OCR V2 0.8B is a 0.8B-parameter document parser for Thai and English. Given an image of a page, it returns the page's layout elements in reading order, each with a category, a bounding box and its text. Tables come out as HTML, formulas as LaTeX and other text as Markdown. It was trained on the same tasks as the 2B model, but it is much weaker outside full-page parsing, so we recommend it as a page parser only (see Limitations).
The model is a fine-tune of OvisOCR2. We trained it with supervised fine-tuning on the same curated Thai data and public English and Chinese data as the 2B model, then with GRPO, using rewards that give zero to outputs that run into the token limit. On MDPBench, GRPO reduced looping pages from 12 to 6 and raised the score from 75.8 to 77.4. The larger Typhoon OCR V2 2B is more accurate and covers the other tasks.
Our goal was the same as for the 2B model: start from a parser that is already strong on English, raise its Thai accuracy, and keep what it already knew about English. We did not run OvisOCR2 through our evaluation scripts, so this card does not report the change from the base model. On olmOCR-bench, which is English only, it scores 81.7, against 84.7 for the 2B model.
- Code: https://github.com/scb-10x/typhoon-ocr
- Website: https://opentyphoon.ai
- Previous version: Typhoon OCR 1.5 2B
Use of this model is subject to the OpenTyphoon Terms and Conditions.
Use vLLM 0.33.0 or newer. On older vLLM versions, run in eager mode (
--enforce-eagerforvllm serve,enforce_eager=TrueforLLM(...)). With vLLM 0.27.1 in its default compiled mode (torch.compile and CUDA graphs), we measured much lower accuracy: an intermediate GRPO checkpoint of this model scored 72.2 on MDPBench in compiled mode and 76.6 in eager mode, and the 2B model scored 70.8 and 80.0 with the same weights in the two modes. With vLLM 0.33.0, the 2B model, which has the same architecture, scored 79.7 in the default mode and 79.3 in eager mode, so the problem no longer appears there. Transformers is not affected. The benchmark scores on this card were measured with vLLM 0.27.1 in eager mode. See Usage.
Key enhancements over Typhoon OCR 1.5
- Layout with bounding boxes. Each element comes with a category (13 types, such as title, text, table, formula, header and footer) and a box, in reading order. Typhoon OCR 1.5 returned the page as Markdown without positions.
- Smaller: 0.8B parameters, against 2B for Typhoon OCR 1.5.
- Higher page-parsing accuracy on public Thai and English benchmarks: MDPBench from 62.8 to 77.4 (Thai pages from 64.3 to 77.2), olmOCR-bench from 62.0 to 81.7, OmniDocBench v1.6 from 70.97 to 89.37, and SEA-DocBench NED from 0.1726 to 0.1081 (lower is better; the lowest of all models we evaluated).
- Better on photographed pages: MDPBench photographed pages from 61.6 to 75.9.
- Markdown is still available. A few lines of code convert the JSON to Markdown, with or without headers and footers (see Usage).
The 0.8B model was trained on the same general question-answering tasks as the 2B model, but at this size it scores lower on them than Typhoon OCR 1.5 (ThaiOCRBench 36.4 against 40.5). For questions about documents, use Typhoon OCR V2 2B.
Model details
| Parameters | 0.8B, BF16 |
| Architecture | Qwen3.5 (Qwen3_5ForConditionalGeneration), hidden size 1,024 |
| Base model | ATH-MaaS/OvisOCR2 |
| Training | Supervised fine-tuning, then GRPO with a LoRA adapter, merged into the weights in fp32 and stored in bf16 |
| Languages | Thai and English. The training data also contains Chinese, which we did not evaluate separately. |
| Input | One page image. Trained with images of up to 4 MP; the benchmark numbers below use up to 16.7 MP. |
| Output | A JSON list of layout elements in a json code block |
Results
We ran every model below ourselves with the same evaluation scripts. The numbers are not copied from other papers or model cards, so some differ from what the other authors report.
- Typhoon OCR V2 and Infinity-Parser2-Flash use the doc2json prompt shown in Usage, images of up to 16.7 MP, greedy decoding, at most 16,384 new tokens and vLLM 0.27.1 in eager mode. Their JSON is converted to Markdown with Infinity-Parser2's conversion code, which drops headers, footers and page footnotes. The model still outputs these elements; only the conversion leaves them out. Usage shows how to keep them.
- The other models use the prompts and post-processing published with them.
- MDPBench and SEA-DocBench are limited to their English and Thai subsets.
- PaddleOCR-VL-1.6 does not support VQA, so it has no ThaiOCRBench score.
In each column, bold is the best result and underlined the second best. ↑ means higher is better and ↓ lower is better.
| Model | Params | MDPBench ↑ | MDPBench TH ↑ | olmOCR-bench ↑ | ThaiOCRBench ↑ | SEA-DocBench NED ↓ | OmniDocBench v1.6 ↑ |
|---|---|---|---|---|---|---|---|
| Chandra-OCR-2 | 5B | 74.2 | 71.1 | 84.4 | 26.4 | 0.1141 | 89.65 |
| dots.mocr | 3B | 76.2 | 73.6 | 85.2 | 37.9 | 0.1135 | 91.09 |
| PaddleOCR-VL-1.6 | 0.9B | 72.5 | 68.4 | 80.8 | n/a | 0.1323 | 95.83 |
| Infinity-Parser2-Flash | 2B | 68.3 | 61.3 | 84.4 | 35.0 | 0.1266 | 91.03 |
| Typhoon OCR 1.5 | 2B | 62.8 | 64.3 | 62.0 | 40.5 | 0.1726 | 70.97 |
| Typhoon OCR V2 0.8B | 0.8B | 77.4 | 77.2 | 81.7 | 36.4 | 0.1081 | 89.37 |
| Typhoon OCR V2 2B | 2B | 79.8 | 81.3 | 84.7 | 59.9 | 0.1153 | 90.97 |
Per-benchmark component scores will be in the technical report (coming soon).
Examples
All three examples are outputs of this model on benchmark items, run with vLLM 0.27.1 in eager mode. The two parsing examples use max_pixels at 4 MP; the handwriting example comes from our ThaiOCRBench run, where the item's question is the prompt. The same items are shown for the 2B model on its card.
1. Full-page parsing: a dense Thai exam page
Left: the input (MDPBench). Right: the predicted boxes, numbered in reading order. 21 of the 27 predicted boxes match the benchmark's layout boxes (IoU ≥ 0.5). The layout is right, but the text has a few small Thai errors that the 2B model does not make, for example กระดาน for กระดาษ and หลักในการตอบ for หลักในการทำตอบ in the first elements below.
Model output (first 5 of 27 elements)
[{"bbox": [939, 14, 979, 27], "category": "header", "text": "8 / 29"},
{"bbox": [409, 74, 591, 92], "category": "title", "text": "การสรุปความจากภาษา"},
{"bbox": [128, 96, 870, 169], "category": "text", "text": "คำสั่ง ในแต่ละข้อจะประกอบด้วยเงื่อนไขหรือข้อมูลที่กำหนดให้ และข้อสรุปเป็นคู่ ๆ จากเงื่อนไขนั้นให้ศึกษาเงื่อนไขที่กำหนดให้ก่อน แล้วจึงอาศัยความรู้ที่ได้จากเงื่อนไขดังกล่าวมาใช้พิจารณาข้อสรุปทั้งสองของแต่ละข้อ แล้วทำตอบลงในกระดานคำตอบ โดยยึดหลักในการตอบดังนี้"},
{"bbox": [129, 200, 273, 219], "category": "title", "text": "หลักในการตอบ"},
{"bbox": [129, 225, 474, 245], "category": "text", "text": "ตอบ 1. ถ้าข้อสรุปทั้งสองเป็นจริงตามเงื่อนไข"},
...]
2. Full-page parsing: a Thai nurse-training handout with references
18 of the 20 predicted boxes match the benchmark's layout boxes (IoU ≥ 0.5).
Model output (first 5 of 20 elements)
[{"bbox": [496, 43, 883, 77], "category": "header", "text": "เอกสารประกอบการอบรม การดูแลผู้บาดเจ็บขั้นสูง รุ่นที่ 13 Advanced trauma emergency care for nurse :ATEN"},
{"bbox": [897, 45, 929, 61], "category": "header", "text": "127"},
{"bbox": [176, 94, 724, 157], "category": "text", "text": "สูตรการคำนวณพลังงานแคลอรี่ (25 Kcal x kg.)+(40 Kcal x % BSA)\nตัวอย่างชายไทย นน. 50 กก. Burn 80%TBSA Total Calories/day\n= (25x50)+(40x80)=4450 Kcal/day"},
{"bbox": [178, 181, 362, 198], "category": "title", "text": "10.การพยาบาลด้านจิตใจ"},
{"bbox": [208, 202, 460, 220], "category": "text", "text": "■ มีการประคับประกอบด้านจิตใจ"},
...]
3. Handwriting: lecture notes
Prompt: ข้อที่ 4 ในภาพนี้ เป็นข้อความที่เขียนด้วยมือว่าอะไรบ้าง ตอบเฉพาะข้อความตามภาพตรง ๆ อย่างกระชับ โดยไม่ใส่คำอธิบายเพิ่มเติม (What is handwritten in item 4?)
Output, score 0.84 (Typhoon OCR V2 2B: 0.96; best of the four other open models we ran on ThaiOCRBench: 0.38). One phrase is misread: มากเลย instead of มาทดแทน.
④ ผูกขาด // ผู้ขายรายเดียว ไม่มีสินค้าอื่นมากเลย ex. ไฟฟ้า ประปา
Usage
The typhoon-ocr Python package (pip install typhoon-ocr), which runs Typhoon OCR 1.5, does not support V2 yet. Use vLLM or Transformers as shown below.
The model was trained on a fixed set of prompts. For full-page parsing, use this prompt exactly as written; it is the prompt from Infinity-Parser2.
PROMPT = """- Extract layout information from the provided PDF image.
- For each layout element, output its bbox, category, and the text content within the bbox.
- Bbox format: [x1, y1, x2, y2].
- Allowed layout categories: ['header', 'title', 'text', 'figure', 'table', 'formula', 'figure_caption', 'table_caption', 'formula_caption', 'figure_footnote', 'table_footnote', 'page_footnote', 'footer'].
- Text extraction and formatting:
1) For 'figure', the text field must be an empty string.
2) For 'formula', format text as LaTeX.
3) For 'table', format text as HTML.
4) For all other categories (e.g., text, title), format text as Markdown.
- The output text must be exactly the original text from the image, with no translation or rewriting.
- Sort all layout elements in human reading order.
- Final output must be a single JSON object."""
Although the last line asks for a JSON object, the model returns a JSON list inside a json code block:
[{"bbox": [62, 48, 938, 96], "category": "title", "text": "Annual Report 2025"},
{"bbox": [62, 120, 938, 410], "category": "text", "text": "The company ..."},
{"bbox": [62, 432, 938, 700], "category": "table", "text": "<table>...</table>"},
{"bbox": [480, 950, 520, 968], "category": "footer", "text": "12"}]
Bounding boxes use a 0–1000 grid relative to the page width and height, so they do not depend on the image resolution. These helpers parse the output, convert it to Markdown and map a box back to pixels:
import json, re
def parse_elements(raw: str) -> list[dict]:
"""Parse the model output: a JSON list of elements inside a ```json fence."""
m = re.search(r"```json\n(.*?)(?:\n```|$)", raw, re.DOTALL)
text = (m.group(1) if m else raw).strip()
try:
return json.loads(text)
except json.JSONDecodeError:
# Output stopped at the token limit: drop the last, incomplete element.
cut = text.rfind('{"bbox":')
return json.loads(text[:cut].rstrip().rstrip(",") + "]")
def to_markdown(elements: list[dict], keep_header_footer: bool = False) -> str:
skip = set() if keep_header_footer else {"header", "footer", "page_footnote"}
return "\n\n".join(e["text"] for e in elements if e.get("text") and e["category"] not in skip)
def to_pixels(bbox, width, height):
x1, y1, x2, y2 = bbox
return [x1 * width / 1000, y1 * height / 1000, x2 * width / 1000, y2 * height / 1000]
The JSON from the model always contains every element. to_markdown leaves out header, footer and page_footnote elements by default, which is how we evaluated the model. To keep them, pass keep_header_footer=True:
elements = parse_elements(raw) # raw: the text the model returned
md = to_markdown(elements) # drops header, footer, page_footnote (as in our evaluation)
md_all = to_markdown(elements, keep_header_footer=True) # keeps every element
The examples below need PROMPT and the helpers above. They were tested with vLLM 0.27.1 (eager mode) and 0.33.0 (with the 2B model) and Transformers 5.15.1. Use the other settings in the code as they are; the benchmark numbers were produced with them.
pip install "vllm>=0.33.0"
On vLLM versions older than 0.33.0, add --enforce-eager to vllm serve, or enforce_eager=True to LLM(...). Without it, vLLM 0.27.1 compiles the model with torch.compile and CUDA graphs, and in that mode we measured lower MDPBench scores for this model (72.2 against 76.6 for an intermediate GRPO checkpoint) and for the 2B model (70.8 against 80.0). We have not tested versions between 0.27.1 and 0.33.0.
vLLM server
vllm serve typhoon-ai/typhoon-ocr2-0.8b \
--served-model-name typhoon-ocr2 \
--max-model-len 40960 \
--limit-mm-per-prompt '{"image": 1}' \
--mm-processor-kwargs '{"min_pixels": 2048, "max_pixels": 16777216}' \
--default-chat-template-kwargs '{"enable_thinking": false}'
# vLLM < 0.33.0: add --enforce-eager
import base64
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
with open("page.jpg", "rb") as f:
image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
resp = client.chat.completions.create(
model="typhoon-ocr2",
messages=[{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": image_url}},
{"type": "text", "text": PROMPT},
]}],
temperature=0.0,
max_tokens=16384,
)
elements = parse_elements(resp.choices[0].message.content)
print(to_markdown(elements))
vLLM offline
import base64
from vllm import LLM, SamplingParams
def main():
llm = LLM(
model="typhoon-ai/typhoon-ocr2-0.8b",
max_model_len=40960,
limit_mm_per_prompt={"image": 1},
mm_processor_kwargs={"min_pixels": 2048, "max_pixels": 16777216},
# enforce_eager=True, # needed on vLLM < 0.33.0 (see above)
)
sampling = SamplingParams(temperature=0.0, max_tokens=16384)
with open("page.jpg", "rb") as f:
image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
messages = [{"role": "user", "content": [
{"type": "image_url", "image_url": {"url": image_url}},
{"type": "text", "text": PROMPT},
]}]
out = llm.chat(messages, sampling, chat_template_kwargs={"enable_thinking": False})
print(to_markdown(parse_elements(out[0].outputs[0].text)))
if __name__ == "__main__":
main()
Transformers
import torch
from PIL import Image
from transformers import AutoModelForImageTextToText, AutoProcessor
model_id = "typhoon-ai/typhoon-ocr2-0.8b"
model = AutoModelForImageTextToText.from_pretrained(model_id, dtype=torch.bfloat16, device_map="auto")
processor = AutoProcessor.from_pretrained(model_id)
image = Image.open("page.jpg").convert("RGB")
messages = [{"role": "user", "content": [
{"type": "image", "image": image},
{"type": "text", "text": PROMPT},
]}]
inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
enable_thinking=False,
processor_kwargs={"images_kwargs": {"min_pixels": 2048, "max_pixels": 16777216}},
).to(model.device)
out = model.generate(**inputs, max_new_tokens=16384, do_sample=False)
raw = processor.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
print(to_markdown(parse_elements(raw)))
Questions about a document
To ask a question about a document, send the question as the text prompt in place of PROMPT. The answer comes back as plain text.
Extracting key information as JSON
To pull the fields out of a receipt, form, label or poster without listing them, use this prompt (with this model, check the output carefully; see Limitations):
Perform open-schema key information extraction (WYSIWYG) on this image: one JSON object, printed labels as keys, values copied exactly, "" for blank fields, arrays for repeated rows. Return only JSON.
To get specific fields, list them in the prompt instead.
Limitations
- Outside full-page parsing the model is weak. Its ThaiOCRBench score is 36.4, with chart parsing at 7.4, full-page OCR at 47.9 and key-information extraction at 10.9.
- The model can fall into repetition loops, repeating text until it reaches the token limit. When that happens,
parse_elementsabove keeps only the complete elements. - Extracting all fields as JSON without a field list (see Usage) works much less well than with the 2B model: on 40 training documents, 28 outputs were valid JSON (2B: 39) and 5 ran into the token limit. Use the 2B model for this task.
- PaddleOCR-VL-1.6, at a similar size, is well ahead on tables and formulas (OmniDocBench table TEDS 93.38 against 82.49, formula CDM 97.48 against 89.80).
- We did not hold out a validation set for parsing. The SFT checkpoint this model continues from was chosen by MDPBench score, so its MDPBench numbers are likely somewhat optimistic. The GRPO checkpoint is the last saved checkpoint of its run and was not selected by score. The other four benchmarks were not used for any choice.
- Bounding boxes and image size. The model was trained with images of up to 4 MP, and the Usage examples allow up to 16.7 MP. On 13 digital MDPBench pages larger than 4 MP, the share of predicted boxes matching the benchmark's layout boxes (F1 at IoU ≥ 0.5) was 0.75 at 4 MP and 0.72 at 16.7 MP; the difference comes from one dense newspaper page whose output at 16.7 MP was not valid JSON. Text is more accurate at 16.7 MP: with
max_pixelsat 4,194,304 (4 MP), MDPBench drops from 77.4 to 75.6, mostly on Thai pages (77.2 to 74.1) and tables (TEDS 0.663 to 0.580). Keep 16.7 MP unless you need less GPU memory or faster inference. Better box accuracy is a goal for the next version. - Accuracy can depend on the inference engine. With vLLM 0.27.1, the default compiled mode lowered MDPBench in our tests; use vLLM 0.33.0 or newer, or eager mode on older versions (see Usage). Other serving engines (SGLang, llama.cpp, Ollama) have not been tested.
- We studied Thai and English only. Chinese is in the training data and in OmniDocBench but was not evaluated on its own, and other languages were not tested.
- The model has no safety filtering and can output text that is not in the image. It is meant for reading documents, not for open-ended chat.
Citation
Typhoon OCR V2 technical report: coming soon.
Typhoon OCR 1.5:
@misc{nonesung2026typhoonocropenvisionlanguage,
title = {Typhoon OCR: Open Vision-Language Model For Thai Document Extraction},
author = {Surapon Nonesung and Natapong Nitarach and Teetouch Jaknamon and Pittawat Taveekitworachai and Kunat Pipatanakul},
year = {2026},
eprint = {2601.14722},
archivePrefix = {arXiv},
primaryClass = {cs.CL},
url = {https://arxiv.org/abs/2601.14722}
}
Support
- Discord: https://discord.gg/us5gAYmrxw
- X (Twitter): https://twitter.com/opentyphoon
- Downloads last month
- 49
