Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

paperstack

A local, open, routed alternative to hosted Document AI.

Status: early but working. Contract, registry, router, evaluation harness and two OCR engines run end to end on CPU. The VLM tier is not wired yet. Every performance number below was measured on a synthetic development set and is a proof of mechanism, not a claim about your documents. See Honest limitations before trusting anything here.

Why this exists

Most of what Google Document AI sells, open components already do well — and often better, locally, for free. On OmniDocBench v1.6 a 0.9B open parser scores 96.34 while a 235B general VLM scores 89.78. A 7.7M-parameter PP-OCRv6 reads printed text better than pipelines six times its size.

So the gap is not models. It is three specific things:

  1. No open orchestrator reads handwriting. Docling — MIT, Linux Foundation, the best of them — has no cursive-capable OCR backend as of 2026. The community workaround is to hand pages to a 7B VLM.
  2. Everyone picks one engine per pipeline, not per page. Engines are not equally good at the same things, and the spread within a document type is larger than the spread between engines overall.
  3. There is no honest way to choose. Published comparisons routinely give the home team post-processing the competitors did not get, drop hard pages without penalty, and report word-level intervals on page-clustered data.

paperstack is those three things and nothing else. Layout, tables, reading order, chunking and export are Docling's job and it does them better than a reimplementation would.

What routing buys

Measured on a 300-page development set, classifier and routing policy fit under 5-fold cross-validation so the router pays for its own mistakes:

spotH
best single engine (PP-OCRv6) 0.869
routed 0.898
oracle: route on true class 0.900
oracle: best engine per page 0.938 (ceiling)

+0.029, paired page-bootstrap 95% CI [+0.016, +0.045].

The classifier is essentially free — routing on its predictions costs 0.002 against routing on ground truth — because the binary handwriting/print gate hits 0.963 trained, 0.917 zero-shot with no training at all. And ~+0.040 remains available to a router that predicts per page rather than per class.

Engines are complementary, which is the whole point:

cursive-tuned PP-OCRv6 docTR
handwriting (97 pp) 0.847 0.728 0.674
printed (203 pp) 0.814 0.922 0.886

Released weights

The detector ships, MIT-licensed. It was trained on 100% synthetic pages rendered from google/fonts (OFL 1.1 / Apache-2.0 / UFL) — no IAM, no IMGUR5K, no archival or scraped material — so it is redistributable without restriction. paperstack fetches it on first use and verifies its sha256.

The fastest useful thing here needs no local checkpoint at all:

from paperstack import analyze, engines
rec = analyze("scan.png", ocr=engines.build("hybrid"))   # downloads on first call

hybrid = our MIT detector + PP-OCRv6's Apache-2.0 recognizer. Every weight is redistributable, and it is exactly the configuration that produced the strongest result in this project: +0.241 cursive spotH [+0.133, +0.343] over PP-OCRv6's own detector, with nothing of ours doing the reading. That claim survives even though our recognizer, measured honestly, does not beat PP-OCRv6's.

Route to it, don't default to it — on printed prose it is −0.064 against us.

The recognizer is now released too, retrained on permissive data only — so engines.build("cursive") needs no local checkpoint either:

rec = analyze("scan.png", ocr=engines.build("cursive"))   # both halves fetched

That cost +0.030 CER on held-out handwriting versus a checkpoint trained with IAM/IMGUR5K/archival data (0.061 vs 0.031). The number is published, the restricted checkpoint is not, and the deficit is concentrated entirely in 1–2 character fragments (0.150 vs 0.012). If your material is dense with initials, ticks and single letters, route those crops to a larger model.

Restoring short-fragment coverage synthetically did not work — a second model with generated fragments at 9% of batches scored 0.155 on that band, paired CI [−0.0014, +0.0030]. The constraint is domain, not coverage. Recorded so nobody spends the GPU-hours again.

Install

pip install -e ".[rapidocr,gate]"     # printed-text engine + CLIP router
pip install -e ".[all]"               # everything, including the heavy tiers

The core depends on numpy, pillow and scipy only. import paperstack works on a machine with none of the engines installed — you can always inspect the registry even when you cannot run it.

Use

paperstack engines                          # what exists, and what runs here
paperstack analyze scans/ --ocr rapidocr --gate clip --out pages.jsonl
paperstack score pages.jsonl truth.jsonl    # the acceptance gate

# the flagship: a cheap gate picks the engine per page
paperstack analyze scans/ --ocr router \
    --cursive-det det.pt --cursive-rec rec.pt --out pages.jsonl

Verified end to end on 20 mixed pages (2,669 words), CPU only:

pipeline detH spotH mIoU matched CER
--ocr rapidocr 0.820 0.755 0.847 0.039
--ocr cursive 0.981 0.757 0.829 0.060
--ocr router 0.855 0.798 0.859 0.036

The gate sent 6 pages to cursive and 14 to rapidocr. Twenty pages is far too few for the interval to exclude zero — it is a working demonstration of the mechanism, not a result.

from paperstack import analyze, engines, Policy, Router
from paperstack.gate import ClipGate

router = Router(
    policy=Policy(by_class={"handwriting": "cursive", "printed": "rapidocr"},
                  default="rapidocr",
                  escalate_to="qianfan", escalate_below=0.70),
    engines={"rapidocr": engines.build("rapidocr")},
    gate=ClipGate())

rec = analyze("scan.png", ocr=router)
print(rec.doc_class, rec.stages, rec.mean_conf)
print(rec.text)

The contract

Everything speaks PageRecord. Adding a field is a breaking change; adding an engine is not. Coordinates are always [x0, y0, x1, y1] in original page pixels — conversion happens once, at the engine boundary, and never again.

slot what it does built in candidates
preprocess polarity, orientation ScanTailor, unpaper
gate document class → routing ✅ CLIP 0.963 / ink baseline DINOv2
ocr words + boxes + confidence ✅ PP-OCRv6, ✅ cursive docTR, Qianfan-OCR 4B
layout regions + reading order RT-DocLayout (33M), Docling
tables structure + content TableSeq, DELTA, TableFormer
kie key–value pairs LayTextLLM, Qwen3-VL-4B
summarize page/doc summary Gemma 4 E4B, Qwen3

Is the cursive engine actually better than what's public?

Partly. The honest split:

claim evidence verdict
cursive detection detH 0.984 vs docTR 0.732, PP-OCRv6 0.548 strong, CI excludes 0
our boxes improve another recognizer on cursive +0.241 [+0.133, +0.343] strongest result
cursive recognition at equal boxes 0.811 vs 0.746, CI [−0.017, +0.139] not significant

The detection rows describe the released MIT weights. The recognition row was produced by a restricted checkpoint that cannot ship; the permissive replacement is a different, measurably weaker model. MODEL_CARD.md keeps them apart.

The detector is the contribution. The recognizer is not distinguishable from PP-OCRv6's once both get the same boxes.

And the baselines were weak: docTR, PP-OCRv6 and Tesseract are all print-oriented, so "beats them at cursive" is a low bar. No comparison has been run against a handwriting-specialised system — TrOCR-handwritten, HTR-ConvText (3.45% CER on IAM), Qwen3-VL, or Nemotron Parse 2.0. Until that exists, treat the detection margin as a promising hypothesis.

Honest limitations

Read this part.

  • All numbers above come from a synthetic generator, on a development set that shares its fonts, word list, transforms and box conventions with the training data. No font, writer, vocabulary or domain is held out. Treat them as evidence that the mechanism works, not that it will work on your corpus. Re-fit the policy on your own labelled pages.
  • matched CER excludes misses and hallucinations. It is conditional on a box matching at IoU ≥ 0.5. It is a recognizer diagnostic and is not an end-to-end error rate.
  • Handwriting is not one problem. On modern handwriting (IAM), general VLMs now reach ~1.7% CER and beat specialized HTR (~3.45%). The open gap is degraded archival handwriting under scan damage — that is the claim this project makes, and it is narrower than "handwriting".
  • Orientation detection here separates 0/180 from 90/270 and nothing more. Upside-down pages need a trained head. Stated rather than hidden.
  • The router's escalation threshold is a default, not a tuned value. Tune it on your own data with paperstack score.

Licensing

Code: MIT — chosen to compose with Docling (MIT) and to stay usable by anyone.

Weights are a separate question and are not covered by this licence. Any cursive checkpoint trained on IAM or IMGUR5K is non-commercial and cannot be redistributed under MIT. A releasable checkpoint has to be retrained on permissively-licensed corpora only (CC0 / CC-BY / MIT) — a genuine retrain, not a filter applied afterwards. Until that exists, no weights ship here, and .gitignore blocks *.pt / *.onnx / *.gguf so one cannot be added by accident.

  • THIRD_PARTY.md — every dependency licence, verified against installed distributions. All permissive, no copyleft.
  • MODEL_CARD.md — what the reference checkpoints were trained on, why they cannot be released, and how to build one that can.

Engines wrapping third-party models carry that model's licence, and paperstack engines prints it. No weights are downloaded at import.

Roadmap

  1. Wire the cursive OCR engine + publish a permissively-trained checkpoint.
  2. to_docling() — emit a DoclingDocument so layout/tables/chunking come free.
  3. VLM tier (Qianfan-OCR 4B, Apache-2.0) behind the cascade.
  4. Per-page router to claim the remaining +0.040.
  5. Taxonomized benchmark: modality × degradation × structure × condition, on free public sets (OmniDocBench, PureDocBench, DocLayNet, FUNSD/CORD/SROIE).

About

A local, open, routed alternative to hosted Document AI: per-page engine routing, handwriting-capable OCR, and an evaluation harness honest enough to defend the choice.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages