dots.ocr, tested on a real scanned PDF
1,946
of 1,955 scan words, read in order
On a Mac, change three CUDA parts
The parser is built for NVIDIA GPUs. I changed three parts:
- Attention: sdpa, not flash_attention_2.
- Processor: its base class, Qwen2_5_VLProcessor.
- Device: mps, not cuda.
1,946 of 1,955 words, in order
168 s for 10 pages, Mac GPU
In the repo folder, run pip install -e ., then
python3 dots_mac.py book.pdf. The 6.1 GB model goes
in weights/DotsOCR. Each page gets a Markdown file in out/book/.
The full dots_mac.py script
# dots.ocr on a Mac: the repo's own parser, with its CUDA-only parts swapped for the Mac GPU.
# Run in the dots.ocr repo folder (pip install -e .), weights in ./weights/DotsOCR:
# python3 dots_mac.py book.pdf (Markdown per page in out/book/)
import sys
import torch
from qwen_vl_utils import process_vision_info
from transformers import AutoConfig, AutoModelForCausalLM, Qwen2_5_VLProcessor
from dots_ocr.parser import DotsOCRParser
PATH = "./weights/DotsOCR"
class MacParser(DotsOCRParser):
def _load_hf_model(self):
cfg = AutoConfig.from_pretrained(PATH, trust_remote_code=True)
cfg.vision_config.attn_implementation = "sdpa" # not flash_attention_2 (CUDA only)
self.model = AutoModelForCausalLM.from_pretrained(
PATH, config=cfg, attn_implementation="sdpa", torch_dtype=torch.bfloat16,
device_map="mps", trust_remote_code=True,
)
# The model's own processor class fails on transformers 4.56.1 (video_processor is None),
# so load its base class and set the two values it adds.
self.processor = Qwen2_5_VLProcessor.from_pretrained(PATH, use_fast=True)
self.processor.image_token, self.processor.image_token_id = "<|imgpad|>", 151665
self.process_vision_info = process_vision_info
def _inference_with_hf(self, image, prompt):
msgs = [{"role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": prompt}]}]
text = self.processor.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
images, videos = self.process_vision_info(msgs)
inputs = self.processor(text=[text], images=images, videos=videos, padding=True, return_tensors="pt")
inputs = inputs.to("mps") # the repo has "cuda" here
out = self.model.generate(**inputs, max_new_tokens=24000)
return self.processor.batch_decode(
out[:, inputs.input_ids.shape[1]:], skip_special_tokens=True, clean_up_tokenization_spaces=False
)[0]
MacParser(use_hf=True).parse_file(sys.argv[1], output_dir="out")
The README's commands stop on a Mac
-
dots_mocr/parser.py, the README's line: that file is not in the repo. Exit code 2. -
dots_ocr/parser.pycalls a vLLM server on localhost:8000. With none, a connection error after 6 s. Exit code 1. -
With
--use_hf true: flash_attn is not installed, after 6 s. Exit code 1. - With sdpa set, the model's own processor class fails on transformers 4.56.1, the version the repo pins: video_processor is None. Exit code 1.
All four stop loudly, so a script can catch them. Compare Chandra OCR, which wrote an empty file with exit code 0.
Typed pages: 13 of 13 cover words
dots.ocr reads every page as a picture, typed pages too. In the 11-page file, it read all 13 words of the typed cover page. The scan words stayed at 1,946.
German worked with no language setting
dots.ocr has no language option. I gave it 3 German pages. One is a magazine page with two columns.
204 of 206 umlauts
969 of 972 words in order, two columns
dots.ocr read more, our Actor was faster
Our Actor makes Markdown 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 | dots.ocr | Actor |
|---|---|---|
| Scan words in order | 1,946 | 1,874 |
| Typed cover words | 13 | 13 |
| Umlauts | 204 | 192 |
| Two columns, in order | 969 | 954 |
| Time, 11 pages | 180 s | 133 s |
| Time, 3 German pages | 157 s | 59 s |
The Actor needs no GPU and no 7.4 GB setup. It costs $0.006 a page.
The license allows business use
The code is MIT. The model comes with a license agreement on top. It allows commercial use, paid services and closed products.
It bars fake documents, personal data without consent, and scans of copyrighted books without permission.
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 the parser's Markdown pages, with the scorer of the tessdata test. German: the scorer of Tesseract German.
- dots.ocr repo at commit 36d7248878f1, transformers 4.56.1, PyTorch 2.15.0, Python 3.12 arm64, on an Apple Silicon Mac GPU, at the lowest priority. Model revision c0111ce6bc07. One run each. Times include the model start.
- The script above is the file that ran. The Actor: two real runs earlier the same day, build 0.1.65, on the same files.
What I did not test
- dots.mocr, the newer model that the README calls dots.ocr 1.5. The repo's download script now fetches it into weights/DotsMOCR, but parser.py loads weights/DotsOCR. I read that in the code.
- vLLM, NVIDIA GPUs, and a run with no GPU.
- Handwriting, tables, math and forms.
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
Does dots.ocr run on a Mac?
Yes, with three changes to its parser: sdpa attention, the base processor class and the mps device. On a Mac GPU it read 10 scanned pages in 168 s.
Why does dots.ocr say flash_attn is not installed?
With --use_hf true, the parser loads the model with flash_attention_2, which needs an NVIDIA GPU. On a Mac it stopped after 6 s with that error. Use sdpa.
Can a business use dots.ocr?
Yes. The license agreement allows commercial use, paid services and closed products. It bars fake documents, personal data without consent and copyrighted scans without permission.
Related guides
- Qwen3-VL OCR on a scanned PDF: tested in Ollama on a Mac
- DeepSeek OCR on a scanned PDF: Mac setup and results, tested
- PaddleOCR-VL on a scanned PDF: Mac setup and results, tested
- Chandra OCR on a scanned PDF: Mac setup and results, tested
- Surya OCR on a scanned PDF: setup and results, tested
- Marker OCR for scanned PDFs: setup and results, tested
- PDF to Markdown in Python: 5 libraries tested
- EasyOCR vs Tesseract vs PaddleOCR vs RapidOCR, tested