Fast Surya OCR on Apple Silicon for Indian electoral rolls (MLX runtime + a distilled terse roll model)
Project description
savitr — fast Surya OCR on Apple Silicon, for Indian electoral rolls
savitr makes Surya OCR (datalab-to/surya-ocr-2, a 650M
Qwen3.5-VL-style model) fast on Apple Silicon via MLX (~3.6× over llama.cpp), and ships a
distilled, electoral-roll-specific model — terse-Surya — that emits one compact line per
voter instead of verbose HTML (~5× fewer decode tokens at Surya's accuracy), plus a pipeline that
turns scanned roll PDFs into the canonical voter CSV. Runs locally; no cloud GPU.
Install
Requirements: an Apple-Silicon Mac (M-series — the OCR runs on MLX) and poppler (used to read PDFs):
brew install poppler # macOS (Debian: sudo apt-get install poppler-utils)
Then:
pip install savitr # MLX runtime + terse roll model (auto-downloaded from HF)
pip install "savitr[backend]" # + the generic MLX Backend for Surya's own pipeline
pip install "savitr[train]" # + the distillation toolchain (transformers/peft/torch)
Latest from git: pip install "git+https://github.com/in-rolls/savitr"
The terse model is fetched from gojiberries/savitr on
first use (~800 MB, one time). The pure-Python parsing API (parse_terse) imports on any platform;
only the OCR itself needs Apple Silicon.
Quickstart
The distilled terse model is the default and auto-downloads on first run — a bundled sample roll lets you try it immediately:
# OCR the bundled sample roll -> per-page voter summary (works from any install)
savitr ocr "$(savitr sample)"
# ... or write the voter records straight to CSV
savitr ocr "$(savitr sample)" -o voters.csv
# whole rolls -> canonical voter CSV (a directory of *_ENG.pdf, or a single -f file)
savitr parse-rolls -d english/ -o voters.csv
from savitr import MLXSuryaOCR, parse_terse, resolve_terse_model
from savitr.rolls.parse import TERSE_PROMPT
eng = MLXSuryaOCR(resolve_terse_model(), prompt=TERSE_PROMPT) # downloads the model if not local
text, _ = eng.ocr_image("page.png")
voters = parse_terse(text) # [{'id': 'KMY...', 'elector_name': ..., 'age': ..., ...}]
terse-Surya (gojiberries/savitr)
Surya self-distilled to emit pipe-delimited voter rows
(epic|name|relation(F/H/M)|relative|house|age|sex). Trained for $0 on a free Kaggle T4 by
labeling roll pages with Surya itself (teacher) and LoRA-fine-tuning to the terse format.
Speed (M4 / 16 GB)
| Pipeline | s/page |
|---|---|
| Original Surya (llama.cpp f16) | ~178 |
| MLX 4-bit, HTML | ~38 |
| terse-Surya MLX 8-bit | ~17.5 |
Out-of-sample fidelity to the Surya teacher (held-out constituencies — 37 pages, 1,076 voters)
| Field | Field | ||
|---|---|---|---|
| voter recall | 99.3% | relative name | 96.2% |
| EPIC | 97.2% | relation (F/H/M) | 97.9% |
| name | 96.2% | house | 98.8% |
| age | 97.5% | sex | 98.2% |
Per-voter record similarity 98.7%, whole-page 92.9% (1 − normalized edit distance).
v0.2 — 450 training pages, constituency-holdout. Absolute accuracy ≈ these × the teacher's own ~93–95%.
What's in the box
Three layers — two you install and use, one for reproducing the model:
savitr/ # the pip package (use it)
mlx_ocr.py # GENERIC MLX Surya engine (MLXSuryaOCR) — run any Surya OCR fast
mlx_backend.py # GENERIC Surya Backend (mlx) — also offered upstream (PR.md)
rolls/ # ELECTORAL-ROLL app: parse · fields · schema · pipeline · ocr
cli.py, __init__.py
training/ # repo-only: build_corpus · train · eval · merge · kaggle_* (reproduce the model)
- Use it — electoral rolls (the product):
savitr parse-rolls/savitr ocrrun the distilled terse model (the default) on roll PDFs → voter records / canonical CSV (savitr.rolls). - Use it — generic fast Surya:
savitr.MLXSuryaOCR+ themlxBackendrun any Surya OCR ~3.6× faster on Apple Silicon (also offered upstream to Surya — seePR.md). - Reproduce it — training/distillation: lives in top-level
training/, not shipped in the wheel (install the[train]extra to run it). We ship code to use the model, not to train it.
How it was built / what was tried
See FINDINGS.md for the measured baseline (decode, not cold-start, dominates; ~110 s/page on llama.cpp), the MLX win, the tested negatives (compact prompt, guided JSON, parallelism), and the distillation method + numbers.
Develop
pip install -e ".[backend,train,dev]"
ruff format . && ruff check .
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file savitr-0.2.0.tar.gz.
File metadata
- Download URL: savitr-0.2.0.tar.gz
- Upload date:
- Size: 785.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5324506ba1bb71887f7096fab96835060a63674fbe0e4e453f12321bc679c3b8
|
|
| MD5 |
49b380e2f7351e4f38876d7dd79f2fc7
|
|
| BLAKE2b-256 |
869b15d107e0344d895d3b839f0c4911398dde15a84edf1c607f22b9970cdb7b
|
File details
Details for the file savitr-0.2.0-py3-none-any.whl.
File metadata
- Download URL: savitr-0.2.0-py3-none-any.whl
- Upload date:
- Size: 752.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1427ee799b4fab2584c792201c7ab30412891d2953b36657d977ca6d0d16ecc
|
|
| MD5 |
badb6284baac39cba7642a183e8286b9
|
|
| BLAKE2b-256 |
cdbc18c0ca6f6106aba634ce219311e90e1d5e122106b63abd75989d30936c84
|