ProjectApplied AI · 2026
Document OCR
Local-first OCR for passports and Indian KYC documents, on RapidOCR and PP-OCRv5. Packaged for Python, HTTP, Docker and Node.js. No image leaves your network.
- Python
- TypeScript
- FastAPI
- RapidOCR
- PP-OCRv5
- OpenCV
- Docker
Applied AI · Project
Document OCR pulls structured fields out of passports and Indian KYC documents without the scan leaving the network it arrived on. It reads passport biodata and back pages, PAN cards, Aadhaar, driving licenses, voter IDs, NREGA job cards and NPR letters. I run it as a Python package, a FastAPI server or a Docker image. An npm wrapper spawns and manages the Python server for Node.js callers.
A single generic recognition pass gives you text, not fields. Each of these documents has its own layout, its own bilingual labels, and its own place where the identifier sits. One shared prompt over all of them hides which document actually failed and why, so every document type gets its own extractor instead.
# local server, no image leaves the machine make devuvicorn running on http://0.0.0.0:8000 curl -s localhost:8000/ready{"status":"ready"} curl -F [email protected] localhost:8000/scan{"status":"success","documentType":"aadhaar", "aadhaarFields":{"checksumValid":true}, …Two paths through
core.pipeline.scan() is the single entry point. Preprocessing corrects orientation, finds the document boundary, applies perspective correction and checks scan quality. Then classify_passport_page runs a cheap bottom-crop probe: biodata, non-biodata, or not a passport at all. The passport path runs RapidOCR on PP-OCRv5, parses the TD3 MRZ with per-field and overall check digits, extracts bilingual back-page labels, and cross-checks the MRZ against the visual fields. Full-page enrichment runs even when the cropped MRZ already validates. The other path calls classify_document and routes full-page OCR to the matching extractor. Exactly one field block comes back, keyed by documentType, and every other block is null.
Failing closed
Success on the non-passport path means two specific things. The extractor returned that document’s minimum field set, and the reading passed a conservative OCR-region confidence gate. Alternate model readings of the same detected geometry count once. A partial or implausible record returns HTTP 422 carrying the full structured result, rather than half a record presented as a success. identifierValid is deliberately narrow. ICAO check digits cover the passport MRZ, the Verhoeff checksum covers Aadhaar, and format rules cover PAN, driving license and EPIC numbers. NPR references have no public checksum, so that field stays null. Unknown or conflicting document hints fail explicitly instead of guessing.
Running a scan
make install prepares the environment and make dev puts uvicorn on port 8000.
make dev
curl -s localhost:8000/ready
curl -F [email protected] localhost:8000/scan
DOCUMENT_OCR_KYC_LANGS=en,devanagari make dev
/ready holds at 503 until every configured model has loaded, so a failed model download surfaces before the first scan rather than during it. /scan accepts images and PDFs up to 10 MB, and concurrency is serialized internally. /scan/batch preserves input order. /scan/document groups front and back images, or all pages of a PDF, and reports conflicts between them. Setting API_TOKEN requires a bearer token on the document and job endpoints.
Decisions and costs
- One extractor per document type rather than one prompt across all of them. A shared prompt hides which document failed and why. The cost is a separate code path per type, each needing its own fixtures and its own benchmark slice.
- Extra scripts only on request.
DOCUMENT_OCR_KYC_LANGStakes up to four ofen,latin,devanagari,ka,taandte, and applies only on the non-passport path, because more models mean more latency and memory. - Fail readiness rather than degrade. An unsupported language name, or more than four configured models, fails
/readyinstead of falling back silently. That is stricter than some deployments want, and I would revisit putting it behind a flag. - Wrap the Python server for Node.js rather than port the pipeline. The npm package creates a
.venv, installs dependencies and owns the server lifecycle, so a Node.js caller inherits a Python runtime requirement.
How accuracy is measured
make test runs the Python suite, the TypeScript SDK tests and the review UI tests. Those are regression tests, and the repository is explicit that they are not accuracy evidence. Accuracy comes from make benchmark-public and make benchmark-http over downloaded public samples, and from make benchmark-kyc against a private manifest that never enters the repository. Missing required coverage fails the release gates. NREGA and NPR support stays marked experimental, because deterministic text-region tests prove parsing behavior rather than real-image accuracy. The bundled RapidOCR build has no Bengali recognition model, so Bengali fixtures prove parsing alone.
Limits and privacy
The same pipeline deploys as a container, on Cloud Run, on Lambda, on Cloudflare Containers or on a plain server, with Terraform for two of those. Experimental US identity and immigration profiles sit alongside the Indian set, together with PDF417 and TD1 parsing. It remains beta software for data extraction. Checksums say an identifier is well formed. They never say a document is genuine, that a person is who they claim, or that a KYC obligation has been met. The repository carries no identity-document fixtures and no personal data. An internet-facing deployment still wants platform IAM or a gateway above API_TOKEN, plus a deliberate retention policy.
Next projectDeveloper Tools