TitanOCR
A standalone, general-purpose OCR and document-structure engine. Give it any PDF or image — born-digital, scanned, or phone-photographed; printed, hybrid, or handwritten — and it returns faithful text and structure with pixel-level provenance: every value carries its page, bounding box, origin and confidence. Deterministic, auditable, and CPU-only on the primary path.
Scope: a product in its own right, not a component of any particular domain pipeline. Consumers take the Document IR (or its Markdown projection) and do their own downstream processing.
The split this project is built on
Sending document images to a vision LLM collapses two different problems into one opaque call:
| Problem | Right tool | Cost profile | Verifiable? |
|---|---|---|---|
| Transcription — what characters are on this page, where | OCR (detection + recognition + layout + tables) | ~₹0.00X/page, CPU | yes — bboxes + per-line confidence |
| Understanding — what the text means for a given consumer | the caller's own logic/LLM, on text | caller's choice | caller's problem — now grounded in auditable text |
A vision-LLM-only approach pays frontier rates on every page, with no bounding box, no calibrated confidence, and no way to tell a correct answer from a fluent hallucination. This engine makes transcription deterministic, nearly free, and verifiable — and keeps a VLM only as a budgeted escalation for what CPU OCR cannot yet read confidently (today: handwriting). The roadmap (phase 17) is to read handwriting on CPU too, with a deterministic model, shrinking escalation to a safety net.
Design invariants
These are load-bearing. Changing one is an ADR, not a refactor.
- OCR always runs. Every raster page, no pre-classification gate. It is cheap enough that running it unconditionally is free insurance — and its confidence output is the triage signal. This is why the handwritten/printed split never needs to be known up front.
- The VLM is an escalation, never the default path. It fires on a measured signal, on the narrowest possible unit (field > region > page > document), and always with OCR output supplied as grounding context.
- Every emitted value is traceable to pixels. Page, bbox, source stage, confidence. If you can't point at where a value came from, it doesn't ship.
- No accuracy claim without the golden set. Route mix, cost/page and per-stratum accuracy are measured artifacts, not vibes.
- A fallback that fires often is not a fallback. Escalation rate has an explicit budget and alerts when breached.
- Every stage is a swappable implementation behind an interface, wired by config, pure over the IR, with no module-level state. "Modular" is enforced by the dependency graph and a shared contract test suite, not by intent. → 08
- Nothing blocks the event loop. CPU work runs in a process pool; the async front door stays responsive under saturation so health probes never fail mid-document. → 07
- Abstention over guessing. Below-threshold content escalates or is flagged — never emitted as if certain.
Layout
new/
├── README.md ← you are here
├── implementation_plan.md build-level plan (titanocr + API + demo UI)
├── plans/ numbered phase plans + master tracker (execution source of truth)
├── docs/
│ ├── 01-architecture.md pipeline, components, service topology, stack + licences
│ ├── 02-document-ir.md ⭐ the Document IR schema — the central contract
│ ├── 03-triage-and-escalation.md confidence signals, routing policy, budget
│ ├── 04-handwriting-roadmap.md the end-state: deterministic CPU handwriting (the roadmap)
│ ├── 05-eval-harness.md golden set, metrics, release gates
│ ├── 06-deployment-and-operations.md container, probes, sizing, operational behaviour
│ ├── 07-concurrency-and-scaling.md execution models, thread/memory arithmetic, backpressure
│ ├── 08-modularity-and-interfaces.md stage protocols, registry, module boundaries
│ ├── 09-success-criteria.md ⭐ what "good" means, how it's proven, kill criteria
│ └── adr/ architecture decision records (0001-…, append-only)
├── src/titanocr/ ONE Python project (3.13) — ir/ (the contract), stages/, engines/,
│ render_md/, service code, demo UI static/
├── pipelines/ · fixtures/ · models/ · scripts/ · tests/
├── packaging/pypi-placeholder/ the `titanocr` PyPI name reservation (phase 18)
└── eval/ ⭐ the accuracy-proof layer (phase 16)
├── gates.yaml release gates — `target: null` means "not measured yet"
├── golden/ golden sets: manifest + per-document truth (smoke · hard)
├── harness/ run · score · diff · report · thresholds
└── scripts/ confidence-histogram batch tooling (threshold bootstrapping)
v1 is a single project (ADR 0003): the IR, stage
interfaces and engines live as modules inside titanocr, with dependency direction
(ir → stages → engines → service) enforced by import-linter. Extracting ir/ into a standalone
package is the recorded revert path if the contract ever gains a second consumer.
Status
| Area | State |
|---|---|
| Design docs | 01–09 drafted and maintained as law (adr/ records decisions) |
| Document IR | signed off — ir_version 1.8 (phase 02 signed off 1.0, 2026-08-13; minor bumps ADR 0004: table blocks may carry their recognised lines, ADR 0005: table_failed degrade flag, ADR 0006: quality.ink_ratio; ADR 0007 fixes what escalation may claim; later minors add the block types figure/signature without lines ADR 0010, rule ADR 0011, connector ADR 0012 + its label anchors ADR 0016, and formula ADR 0018) |
titanocr service |
released v0.1.0 (2026-08-13) — pipeline stages 0–5 feature-complete and containerised: digital fast path (exact, ₹0), spawn pool with thread pinning + warm-up-gated readiness, PP-OCRv5 det/rec chosen accuracy-first by fixture bake-off (scan CER 0.15%, photo 0.30%), layout + reading order, geometric table reconstruction with ML fallback (cells exact on every fixture), triage confidence-v1 + reject path + signals JSONL, VLM escalation (cropped, OCR-grounded, additive merge, caps + breaker; live-provider smoke blocked on an endpoint/key), demo UI at /demo, 976 MB linux/amd64 image starting to ready offline. Full per-phase detail: plans/ |
| Accuracy | still no accuracy claim. The phase-16 harness is built and in CI (make eval — stratified CER, cell accuracy at (row,col), detection recall, route mix, ₹/page, abstention honesty, determinism, 14 release gates, clickable HTML report), and it enforces the honesty rules rather than stating them: a model-produced label is refused, a synthetic set reports unclaimable, an unset target can never report pass. The golden set — real, human-labelled documents — does not exist yet, so the verdict is incomplete by construction and phases 17/18/21 stay gated. First run's smoke baseline and the real defect it found: note |
| Packaging | the PyPI name titanocr is reserved (0.0.1 placeholder, no OCR code — make pypi-placeholder). Release automation is in place (Releasing to PyPI). Shipping the engine itself as pip install titanocr is phase 18, gated on the accuracy proof (ADR 0009) |
| Structure fidelity | phases 19–20 done (2026-08-26) — the engine no longer asserts structure a page does not have (a stacked formula is not a grid; an equation number stays in its own y-band), the born-digital route types its regions with the layout stage in a pool worker (ADR 0017, ≈0.26 s/page), and mathematics is a block type excluded from structure-guessing by type (ADR 0018). Renderer 1.12 |
| Roadmap | phase 16 (accuracy proof) → phase 17 (handwritten OCR on CPU, deterministic), phase 18 (pip install titanocr) and phase 21 (formula → LaTeX), all gated on it — see plans/ |
Conventions
- Non-obvious or reversible-at-cost decisions get an ADR in
docs/adr/. Numbered, append-only, superseded rather than edited. - The Document IR is versioned. Breaking changes bump the major and require a migration note.
- Anything measured (thresholds, costs, route mix) cites the run that produced it. No folk numbers.
Releasing to PyPI
Releases are published by .github/workflows/release.yml when a
v* tag is pushed. It uses PyPI trusted publishing (OIDC), so no API token is stored in the
repository or its secrets.
Gated: publishing the engine is phase 18, and phase 18 waits on phase 16's accuracy proof (hard rule 14). The pipeline is ready; pushing a real version tag is the release decision.
One-time setup
- On pypi.org: titanocr →
Settings → Publishing → Add a new publisher → GitHub with owner
Phantom-IN, repositorytitanocr, workflowrelease.yml, environmentpypi. - Optional, for dry runs: the same on test.pypi.org with environment
testpypi(the name must be registered there too). - On GitHub: Settings → Environments → create
pypiandtestpypi. Adding yourself as a required reviewer onpypimakes every release wait for a manual approval before upload.
Cutting a release
uv version 0.2.0 # bump the version in pyproject.toml
git commit -am "release 0.2.0"
git tag v0.2.0
git push origin main v0.2.0 # the tag push starts the release
The workflow then runs, in order — and nothing is uploaded unless every step passes:
- the full CI gate (
ci.yml: lint, typecheck, tests, real-engine contract suites, eval, container); - a check that the tag matches the
pyproject.tomlversion; uv build(sdist + wheel) andtwine check --strict;- a clean-venv install and import of the wheel on Linux and macOS;
- upload to PyPI (after approval, if the
pypienvironment requires it).
Dry run: Actions → release → Run workflow runs the same steps and publishes to TestPyPI only, never PyPI.
Rules
- PyPI never accepts the same version twice — even after a deletion. A failed or wrong release is fixed by publishing the next version, not by re-tagging.
- Keep versions above the published ones (the placeholder is
0.0.1; the engine is at0.1.0). - Models are never in the distribution; they are fetched and checksummed (
make models).
Read order
New to the project: this file → 01-architecture → 02-document-ir. Those three are the whole idea.
Before writing service code, also read 07 and 08 — they constrain how the code is structured, and retrofitting either is expensive.
Reviewing the project: this file → 09-success-criteria. That doc carries the targets, how each claim gets proven, and the conditions under which we stop.
Metadata
Release files for titanocr 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| titanocr-0.1.2.tar.gz | 538.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| titanocr-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / titanocr-0.1.2.tar.gz
| Download URL | titanocr-0.1.2.tar.gz |
|---|---|
| Size | 538.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2e53dde01d5a9c58ea01cac55655ae2f26d18aa786eeddfc6787bd5a153fcc14
|
|
BLAKE2b-256 checksum How to use checksums |
feb066a7cc7273186bfce1fdd9e6033604210e5ae30e1b394f48abd18d964d79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency logRelease files / titanocr-0.1.2-py3-none-any.whl
| Download URL | titanocr-0.1.2-py3-none-any.whl |
|---|---|
| Size | 561.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b12cddc81f81782748b8f1ffa397a65da1dffe43940b80da45b2dd903087b97f
|
|
BLAKE2b-256 checksum How to use checksums |
915bd5b21281fae49c2c89728fa2bd63c507b26fed92dde993313f2d0eb71a9f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.
Transparency log