Skip to main content

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

PyPI Model Docs

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 ocr run the distilled terse model (the default) on roll PDFs → voter records / canonical CSV (savitr.rolls).
  • Use it — generic fast Surya: savitr.MLXSuryaOCR + the mlx Backend run any Surya OCR ~3.6× faster on Apple Silicon (also offered upstream to Surya — see PR.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

savitr-0.2.0.tar.gz (785.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

savitr-0.2.0-py3-none-any.whl (752.2 kB view details)

Uploaded Python 3

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

Hashes for savitr-0.2.0.tar.gz
Algorithm Hash digest
SHA256 5324506ba1bb71887f7096fab96835060a63674fbe0e4e453f12321bc679c3b8
MD5 49b380e2f7351e4f38876d7dd79f2fc7
BLAKE2b-256 869b15d107e0344d895d3b839f0c4911398dde15a84edf1c607f22b9970cdb7b

See more details on using hashes here.

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

Hashes for savitr-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d1427ee799b4fab2584c792201c7ab30412891d2953b36657d977ca6d0d16ecc
MD5 badb6284baac39cba7642a183e8286b9
BLAKE2b-256 cdbc18c0ca6f6106aba634ce219311e90e1d5e122106b63abd75989d30936c84

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page