Tools,

GOT-OCR 2.0, tested on a real scanned PDF

1,936

of 1,955 scan words, read in order

Get the setup

Use format mode, not plain mode

134 s for 10 pages, Mac GPU

1.1 GB model download

  1. Install Poppler. Run pip install torch transformers pillow.
  2. Save got.py below next to book.pdf.
  3. Run python3 got.py --format. It writes book.md. The first run downloads the model.
got.py
# GOT-OCR 2.0 on every page of book.pdf, one page at a time, to book.md.
# pip install torch transformers pillow   (and poppler for pdftoppm)
# python3 got.py            plain text (the model's "ocr" mode)
# python3 got.py --format   formatted text (the model's "format" mode)
# python3 got.py --patches  cut each page into tiles first (for tall or two-column pages)
import glob
import subprocess
import sys

import torch
from transformers import AutoModelForImageTextToText, AutoProcessor

M = "stepfun-ai/GOT-OCR-2.0-hf"
dev = "mps" if torch.backends.mps.is_available() else "cpu"
model = AutoModelForImageTextToText.from_pretrained(M).to(dev)
proc = AutoProcessor.from_pretrained(M)
fmt = "--format" in sys.argv
patches = "--patches" in sys.argv

subprocess.run(["pdftoppm", "-r", "200", "-png", "book.pdf", "page"], check=True)
for png in sorted(glob.glob("page-*.png")):
    # .to(dev) alone fails: the model loads in bfloat16, the image is float32
    x = proc(png, return_tensors="pt", format=fmt, crop_to_patches=patches)
    x = x.to(dev, model.dtype)
    ids = model.generate(
        **x,
        do_sample=False,
        tokenizer=proc.tokenizer,
        stop_strings="<|im_end|>",
        max_new_tokens=4096,
    )
    text = proc.decode(ids[0, x["input_ids"].shape[1] :], skip_special_tokens=True)
    with open(png[:-4] + ".md", "w") as f:
        f.write(text + "\n")

with open("book.md", "w") as out:
    for md in sorted(glob.glob("page-*.md")):
        out.write(open(md).read())

Plain mode looped on page 3

In plain mode, page 3 became one line, "You're a friend of mine,", until the 4,096-token limit. Both plain runs looped the same way.

Scan words in order, of 1,955
Setup Words Time
Plain 1,591 178 s
Plain, no repeat 20 1,593 131 s
--patches 1,838 194 s
--patches, no repeat 20 1,839 150 s
--format 1,936 134 s

"No repeat 20" adds no_repeat_ngram_size=20 to generate(). It stopped the loop, but page 3 still ended early.

Format mode had no loop.

The model card code fails on transformers 5

The GOT-OCR-2.0-hf card sends the inputs with .to(device). With transformers 5.22.2 on a Mac, that failed on page 1:

RuntimeError: Input type (float) and bias type (c10::BFloat16) should be the same

The model loads in bfloat16, and the image stays float32. got.py uses .to(dev, model.dtype).

The card of the original GOT-OCR2_0 model is for NVIDIA GPUs. got.py uses the transformers port.

glm-ocr and Qwen3-VL read a few more

The same 10 scanned pages, scorer and Mac GPU:

Scan words in order, of 1,955
Model Words Time
glm-ocr, Ollama 1,953 104 s
Qwen3-VL 8B 1,951 139 s
GOT-OCR 2.0, format 1,936 134 s

The Qwen3-VL download is 6.2 GB, more than 5 times the GOT download.

The misses: 13 places, and every header

In format mode, GOT wrote "pow-wowing" as "pow-wooding" and "old-fashioned" as "oldfashioned".

It lost 3 spaces and two double dashes. The initials "W. M. M. H." came out as LaTeX math.

It kept the running page header on all 10 pages. Delete those lines if you need clean book text.

Typed pages: 13 of 13 cover words

GOT reads every page as a picture. Format mode put the cover title in LaTeX: \title{COVER SHEET}.

In the 11-page file, the scan words stayed at 1,936, in 130 s.

Each scan page came out the same as in the scan run, byte for byte.

German: the two-column page failed

The script has no language option. I gave it 3 German pages. One is a magazine page with two columns.

Of 206 umlauts and 972 two-column words
Mode Umlauts Two columns
Plain 122 266
--format 101 30
--patches 66 23

Plain mode read across both columns and repeated made-up words. Format mode wrote only the magazine title.

On the two one-column pages, plain mode read 882 of 915 words.

No GPU: minutes for each page

I ran the same script in format mode on the CPU only, with 2 threads.

206 s German page 1, title only

291 s German page 2, a full page

I stopped it at 560 s, before page 3.

Two words on page 2 differ from the GPU text.

GOT and our Actor: 1 win, 3 losses

Our Actor, from a file URL, with no install:

curl -s -X POST "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": ["https://api.apify.com/v2/key-value-stores/sTnmNrGoSsi7tLQvs/records/anythingllm-mixed-11-pages.pdf"]}' \
  | jq -r '.[].markdown' > mixed.md
Same files, GOT in its best mode for each
Test GOT Actor
Scan words in order 1,936 1,874
Typed cover words 13 13
Umlauts 122 192
Two columns, in order 266 954
Time, 11 pages 130 s 133 s
Time, 3 German pages 76 s 59 s

GOT used format mode for English and plain mode for German.

The Actor needs no GPU and no model download. It costs $0.006 a page.

The weights allow business use

Both GOT-OCR 2.0 model cards on Hugging Face say apache-2.0. I read them in full before the download.

The GitHub README shows a code badge, Apache 2.0, and a data badge, CC BY-NC 4.0. The repo has no license file, and I used no GOT data.

I skipped ByteDance Dolphin. Its GitHub license file is the Qwen Research License, for non-commercial use.

How I tested it
  • Scan: the 10 pages of OCR in n8n, proofread Wikisource book scans, 1,955 true words. The 11-page file adds the 13-word typed cover page of AnythingLLM OCR.
  • German: the 3 pages of Paperless-ngx OCR, 1,887 words, 206 umlauts.
  • Score: true words found in order in book.md, with the scorer of the tessdata test. German: the scorer of Tesseract German.
  • transformers 5.22.2 and torch 2.15.0, Python 3.12, on an Apple Silicon Mac GPU, at the lowest priority. Model stepfun-ai/GOT-OCR-2.0-hf, revision d3017ef.
  • Times count the whole script, with the model load. One run of each setup.
  • The "no repeat 20" rows and the CPU run change one line of got.py at run time. Nothing else changes.
  • The glm-ocr and Qwen3-VL rows: the same files and scorer, on the same Mac, on November 12. They have their own pages.
  • The Actor: two real runs earlier the same day, build 0.1.65, on the same files.
What I did not test
  • The original GOT-OCR2_0 code, and its box, color and multi-page modes.
  • Format mode with patches, and other patch counts.
  • float32 on the CPU, and more CPU threads.
  • Community GGUF, ONNX and llama.cpp ports.
  • Windows, Linux and NVIDIA GPUs.
  • Handwriting, tables, math, charts and sheet music.
Sources

Two ways to skip the work

Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one page free

Questions people ask

Can GOT-OCR 2.0 run on a Mac?

Yes. With transformers on a Mac GPU, format mode read 10 scanned pages in 134 s.

Is GOT-OCR 2.0 in Ollama?

Not in the official Ollama library. A search on November 12 found no GOT-OCR model, so I ran it with transformers.

Why does GOT-OCR repeat the same line?

Plain mode can loop until the token limit. Format mode had no loop on our scan.