GOT-OCR 2.0, tested on a real scanned PDF
1,936
of 1,955 scan words, read in order
Use format mode, not plain mode
134 s for 10 pages, Mac GPU
1.1 GB model download
-
Install Poppler. Run
pip install torch transformers pillow. - Save got.py below next to book.pdf.
-
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.
| 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:
| 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.
| 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
| 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.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First 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.
Related guides
- olmOCR on a Mac: the toolkit with llama.cpp, tested
- Florence-2 OCR on a scanned PDF: Mac setup, tested
- docTR OCR on a scanned PDF: Mac setup and results, tested
- Qwen3-VL OCR on a scanned PDF: tested in Ollama on a Mac
- Ollama OCR on a scanned PDF: 2 models tested on a Mac
- GLM-OCR on a scanned PDF: Mac setup and results, tested
- dots.ocr on a scanned PDF: Mac setup and results, tested
- DeepSeek OCR on a scanned PDF: Mac setup and results, tested
- PDF to Markdown in Python: 5 libraries tested (2026)