17 Downloads Updated 6 hours ago
ollama run SuraponOCR/typhoon-ocr2-2b
ollama launch claude --model SuraponOCR/typhoon-ocr2-2b
ollama launch opencode --model SuraponOCR/typhoon-ocr2-2b
ollama launch hermes --model SuraponOCR/typhoon-ocr2-2b
ollama launch openclaw --model SuraponOCR/typhoon-ocr2-2b
Name
4 models
typhoon-ocr2-2b:latest
3.2GB · 256K context window · Text, Image · 6 hours ago
typhoon-ocr2-2b:int4
2.6GB · 256K context window · Text, Image · 6 hours ago
typhoon-ocr2-2b:int8
latest3.2GB · 256K context window · Text, Image · 6 hours ago
typhoon-ocr2-2b:bf16
4.4GB · 256K context window · Text, Image · 6 hours ago
Typhoon OCR V2 2B is a document parser for Thai and English from Typhoon. 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 also answers questions about documents and extracts key information as JSON.
Use of this model is subject to the OpenTyphoon Terms and Conditions.
| Tag | Precision | Size | MDPBench ↑ | MDPBench Thai ↑ |
|---|---|---|---|---|
latest, int8 |
8-bit | 3.2 GB | 78.1 | 79.5 |
bf16 |
bfloat16, not quantized | 4.4 GB | 78.7 | 80.7 |
int4 |
4-bit | 2.6 GB | 77.2 | 77.6 |
latest (8-bit) is the best trade-off. int4 saves 0.6 GB but loses about 2 points on Thai pages. MDPBench has 320 Thai and English pages, both digital and photographed. We ran every tag with Ollama 0.40.15 on Linux with an NVIDIA H100, using the settings in the example below. The original weights score 79.8 / 81.3 in vLLM.
Ollama 0.40.15 or newer. Ollama runs this model on its MLX engine; on Linux with an NVIDIA GPU, this needs Ollama’s MLX runner (ollama-linux-amd64-mlx from the Ollama releases). We tested Linux with an NVIDIA GPU only; Apple silicon and Windows were not tested.
The model was trained on a fixed set of prompts. For full-page parsing, use this prompt exactly as written:
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."""
Send the page through Ollama’s chat API with greedy decoding and thinking off. Set num_predict: without a limit, a page that falls into a repetition loop never stops.
import base64, io, json, re
import requests
from PIL import Image
def upscale_min_long(image, min_long=1600):
"""Enlarge small images so the long side is at least 1,600 px (same aspect ratio)."""
s = min_long / max(image.size)
return image if s <= 1 else image.resize((round(image.width * s), round(image.height * s)), Image.LANCZOS)
def image_b64(path):
buf = io.BytesIO()
upscale_min_long(Image.open(path).convert("RGB")).save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode()
def ask(prompt, path, model="SuraponOCR/typhoon-ocr2-2b"):
r = requests.post("http://localhost:11434/api/chat", json={
"model": model, "stream": False, "think": False,
"messages": [{"role": "user", "content": prompt, "images": [image_b64(path)]}],
"options": {"temperature": 0, "num_predict": 16384, "num_ctx": 40960},
}, timeout=600)
r.raise_for_status()
return r.json()["message"]["content"]
def parse_elements(raw):
"""The model returns 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: # 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, keep_header_footer=False):
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)
elements = parse_elements(ask(PROMPT, "page.jpg"))
print(to_markdown(elements))
Each element looks like {"bbox": [62, 48, 938, 96], "category": "title", "text": "Annual Report 2025"}. Boxes use a 0–1000 grid relative to the page width and height, so they map back onto the original image whatever its size.
upscale_min_long matters for small images: the model reads small images poorly, Thai text most of all. On ThaiOCRBench, enlarging images whose long side is under 1,600 px to 1,600 px raised the 2B model’s score from 59.9 to 62.9 and the 0.8B model’s from 37.3 to 41.3 (measured with vLLM).
Send the question as the prompt instead of PROMPT; the answer comes back as plain text:
print(ask("What is the total amount on this receipt?", "receipt.jpg"))
parse_elements above keeps the complete elements when that happens.