9 5 hours ago

vision tools thinking
ollama run SuraponOCR/typhoon-ocr2-0.8b

Applications

Claude Code
Claude Code ollama launch claude --model SuraponOCR/typhoon-ocr2-0.8b
OpenCode
OpenCode ollama launch opencode --model SuraponOCR/typhoon-ocr2-0.8b
Hermes Agent
Hermes Agent ollama launch hermes --model SuraponOCR/typhoon-ocr2-0.8b
OpenClaw
OpenClaw ollama launch openclaw --model SuraponOCR/typhoon-ocr2-0.8b

Models

View all →

Readme

Typhoon OCR V2 0.8B

Typhoon OCR V2 0.8B 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. Use it for full-page parsing only: it is much weaker than the 2B model at questions and extraction.

Use of this model is subject to the OpenTyphoon Terms and Conditions.

Tags

Tag Precision Size MDPBench ↑ MDPBench Thai ↑
latest, int8 8-bit 1.3 GB 75.7 76.3
bf16 bfloat16, not quantized 1.7 GB 75.7 75.6
int4 4-bit 1.1 GB 68.4 66.5

latest (8-bit) scores as well as bf16. Avoid int4 for Thai documents: it loses about 10 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 77.4 / 77.2 in vLLM.

Requirements

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.

Full-page parsing

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-0.8b"):
    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).

Questions about a document

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"))

Limitations

  • The model can fall into repetition loops on dense multi-column pages, such as Thai newspapers. parse_elements above keeps the complete elements when that happens.
  • For questions about documents and key-information extraction, use typhoon-ocr2-2b instead.
  • 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.
  • Full limitations: model card.