Skip to content

Commit ad17d0f

Browse files
feat: expand fail-closed KYC document OCR
1 parent e0c5f2c commit ad17d0f

37 files changed

Lines changed: 6327 additions & 82 deletions

.env.deploy.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ DOCKERHUB_TOKEN=
1616
# GCP_REGION=asia-south1
1717
# ALLOW_UNAUTHENTICATED=false # opt in only behind your own auth layer
1818
# DOCUMENT_OCR_API_TOKEN= # optional Bearer token for /scan
19+
# DOCUMENT_OCR_KYC_LANGS=en,devanagari # optional KYC-only OCR model passes

.github/workflows/test.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
name: Test
22

33
on:
4+
push:
5+
branches: [main]
46
pull_request:
57
branches: [main]
68

@@ -22,9 +24,6 @@ jobs:
2224
- name: Run tests
2325
run: uv run pytest tests/python -v
2426

25-
- name: Run benchmark
26-
run: uv run python benchmarks/accuracy.py
27-
2827
typescript:
2928
runs-on: ubuntu-latest
3029
steps:

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
# Changelog
22

3+
## 3.0.0 — 2026-07-27
4+
5+
### Non-passport document coverage
6+
7+
- Added conservative MGNREGA/NREGA job-card extraction for household,
8+
registration/validity, administrative-location, BPL/category, family, and
9+
repeating adult-member fields across English and selected Indian-script
10+
label variants.
11+
- Added conservative National Population Register name-and-address-letter
12+
extraction for labelled resident, address, pincode, reference, and issue-date
13+
fields. Bare `NPR` acronyms are rejected without recognized issuer context.
14+
- Added automatic classification, pipeline result blocks
15+
(`nregaJobCardFields`, `nprLetterFields`), and matching TypeScript contracts.
16+
17+
### Accuracy and failure semantics
18+
19+
- Non-passport scans now require a document-specific minimum field set and a
20+
conservative per-geometry OCR confidence gate before returning `success`;
21+
low-confidence regions are excluded before extraction, and partial or
22+
low-confidence results return `failure` with structured errors. The bundled
23+
HTTP server therefore returns `422` instead of `200` for partial KYC
24+
extractions.
25+
- Added `identifierValid` for offline format/checksum results, semantic
26+
date/age checks, and conservative confidence caps. These checks do not claim
27+
document authenticity.
28+
- Non-passport responses no longer expose raw classifier OCR through
29+
`probeText`, preventing secondary identifiers from leaking outside the
30+
extractor's documented field contract.
31+
- Added optional KYC-only multi-model recognition through
32+
`DOCUMENT_OCR_KYC_LANGS`, leaving passport OCR model selection unchanged.
33+
Distinct script readings for the same text box are preserved, and readiness
34+
initializes all configured models. Invalid model names fail closed, and the
35+
npm local server now waits for `/ready` rather than liveness before accepting
36+
scans.
37+
- Added a private KYC accuracy evaluator with a versioned manifest schema,
38+
per-document/per-field and complete-record metrics, false-success gates, and
39+
global plus document-specific design/year/issuer/language/capture-quality
40+
slice gates. Required per-document variants must have accepted-positive
41+
samples before evaluation can run.
42+
43+
Dedicated passport OCR modules (page classification, MRZ parsing, back-page
44+
extraction, and passport validation) and passport-positive routing are unchanged
45+
in this release. KYC OCR runs only when the passport probe is unknown or finds
46+
no targeted text.
47+
48+
### Migration
49+
50+
- Consumers that construct `DocumentScanResult` values must add
51+
`identifierValid`, `missingRequiredFields`, `nregaJobCardFields`, and
52+
`nprLetterFields`.
53+
- Treat HTTP `422` as a completed, fail-closed scan result and inspect its
54+
structured body; the JavaScript client already does this.
55+
356
## 1.2.0 — 2026-06-07
457

558
### Multi-document support (additive, backward-compatible)

CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ fixtures must be generated, contain fictitious data, and carry a prominent
2727
snapshots, logs, or commit messages.
2828

2929
Private accuracy datasets can be placed in `benchmark-data/`, which is ignored
30-
by Git.
30+
by Git. Non-passport datasets must follow
31+
[`benchmarks/KYC_DATASET.md`](benchmarks/KYC_DATASET.md): use the versioned
32+
manifest, record design/year/language/capture-quality slices, and keep the
33+
locked release split separate from tuning data.
3134

3235
## Pull requests
3336

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: dev test test-py test-ts build benchmark docker-build install sync
1+
.PHONY: dev test test-py test-ts build benchmark benchmark-kyc docker-build install sync
22

33
install:
44
uv sync
@@ -21,6 +21,10 @@ build:
2121
benchmark:
2222
uv run python benchmarks/accuracy.py
2323

24+
benchmark-kyc:
25+
@test -n "$(KYC_MANIFEST)" || (echo "Set KYC_MANIFEST=/secure/path/manifest.json"; exit 2)
26+
uv run python benchmarks/kyc_accuracy.py --manifest "$(KYC_MANIFEST)" $(if $(KYC_DATASET_ROOT),--dataset-root "$(KYC_DATASET_ROOT)",) $(if $(KYC_REPORT),--output "$(KYC_REPORT)",)
27+
2428
sync:
2529
cd packages/passport-ocr && bash scripts/sync-python.sh
2630

README.md

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Local-first OCR pipeline for passports and Indian KYC documents. It
44
preprocesses scans, classifies the document, runs targeted OCR with RapidOCR
55
(PP-OCRv5), and extracts structured fields—including passport MRZ data, Indian
66
passport back-page fields, and identifier/holder fields for PAN, Aadhaar,
7-
driving licences, and voter IDs.
7+
driving licences, voter IDs, MGNREGA/NREGA job cards, and National Population
8+
Register (NPR) name-and-address letters.
89

910
Ships as a Python package with a FastAPI server, plus an npm wrapper at [`packages/passport-ocr`](packages/passport-ocr) that auto-spawns the Python server for Node.js consumers.
1011

@@ -24,8 +25,28 @@ Ships as a Python package with a FastAPI server, plus an npm wrapper at [`packag
2425
| Aadhaar (front/back) | Aadhaar no. (+ VID, masked-card support), name, DOB/YOB, gender, address, pincode | Verhoeff checksum |
2526
| Driving licence | DL no., name, DOB, issue/validity dates (NT + TR), address, blood group, vehicle class | DL format |
2627
| Voter ID (EPIC) | EPIC no., name, relation name + type, gender, DOB/age | EPIC format |
28+
| MGNREGA/NREGA job card | job-card no., household head, registration/validity, location, category/BPL, adult members | Conservative hierarchical format |
29+
| NPR name/address letter | reference no., resident name, address, pincode, issue date | Not offline-verifiable |
2730

28-
The document type is detected automatically; `/scan` returns the matching field block (`fields`/`backPageFields` for passports, `panFields`/`aadhaarFields`/`drivingLicenceFields`/`voterIdFields` for the others) keyed by `documentType`.
31+
Together with the existing passport path, this covers the officially valid
32+
document categories listed in the
33+
[RBI KYC Master Direction](https://www.rbi.org.in/Scripts/BS_ViewMasDirections.aspx?id=11566);
34+
PAN is supported as a separate tax identifier.
35+
36+
The document type is detected automatically; `/scan` returns the matching
37+
field block (`fields`/`backPageFields` for passports, or `panFields`,
38+
`aadhaarFields`, `drivingLicenceFields`, `voterIdFields`,
39+
`nregaJobCardFields`, or `nprLetterFields`) keyed by `documentType`.
40+
41+
NREGA and NPR support is experimental until measured against a representative
42+
private image dataset. The extraction layer handles multiple label/layout
43+
variants, but deterministic text-region tests are not evidence of real-image
44+
accuracy.
45+
46+
To keep passport OCR behavior unchanged, a positive passport-page probe is
47+
never overridden by the KYC router. A driving licence or voter card whose crop
48+
contains multiple passport-like labels can therefore remain ambiguous; an
49+
explicit KYC-only entry point is tracked as future work.
2950

3051
## Quickstart
3152

@@ -86,14 +107,16 @@ The package auto-creates a `.venv`, installs the Python deps, and manages the lo
86107
internally. Set `DOCUMENT_OCR_API_TOKEN` to require
87108
`Authorization: Bearer <token>` on `/scan`. For internet-facing deployments,
88109
use platform IAM or an API gateway in addition to application-level controls.
110+
Incomplete or semantically invalid non-passport extractions return HTTP `422`
111+
with the full structured failure result.
89112

90113
## Output
91114

92115
```jsonc
93116
{
94117
"status": "success", // success | failure | unsupported_page
95-
"documentType": "passport", // passport | pan | aadhaar | driving_licence | voter_id | unknown
96-
"pageType": "passport_biodata", // passport_biodata | passport_non_biodata | pan | aadhaar | driving_licence | voter_id | unknown
118+
"documentType": "passport", // passport | pan | aadhaar | driving_licence | voter_id | nrega_job_card | npr_letter | unknown
119+
"pageType": "passport_biodata", // same document values, plus passport_biodata | passport_non_biodata | unknown
97120
"confidence": 0.91,
98121
"fields": {
99122
"surname": "...", "givenNames": "...", "fullName": "...",
@@ -119,8 +142,18 @@ use platform IAM or an API gateway in addition to application-level controls.
119142
"classOfVehicle": "...", "validityDateTransport": null },
120143
"voterIdFields": { "epicNumber": "...", "name": "...", "relationName": "...", "relationType": "father",
121144
"gender": "...", "dateOfBirth": "...", "age": null },
145+
"nregaJobCardFields": { "jobCardNumber": "...", "headOfHousehold": "...", "category": "SC",
146+
"registrationDate": "...", "validityFrom": "...", "validityTo": "...",
147+
"address": "...", "village": "...", "gramPanchayat": "...", "block": "...",
148+
"district": "...", "state": "...", "bplStatus": true, "familyId": "...",
149+
"members": [{ "serialNumber": "1", "name": "...",
150+
"fatherOrHusbandName": "...", "gender": "FEMALE", "age": 39 }] },
151+
"nprLetterFields": { "referenceNumber": "...", "name": "...", "address": "...",
152+
"pincode": "...", "issueDate": "..." },
122153
"mrzRaw": ["P<IND...", "..."], "mrzValid": true,
123154
"lowConfidence": false,
155+
"identifierValid": null, // format/checksum only; never an authenticity verdict
156+
"missingRequiredFields": [],
124157
"errors": [], "warnings": [],
125158
"processingMs": 412
126159
}
@@ -131,7 +164,37 @@ use platform IAM or an API gateway in addition to application-level controls.
131164
1. `preprocess` — orientation, document boundary detection, perspective correction, quality checks
132165
2. `classify_passport_page` — biodata vs non-biodata vs not-a-passport (cheap bottom-crop probe)
133166
3. passport path: `run_ocr` (RapidOCR PP-OCRv5, full-page fallback when MRZ is missing) → `parse_mrz` (TD3 MRZ with per-field + overall checksum validation) → `extract_back_page` (bilingual label-aware extraction) → `validate` (cross-checks MRZ vs visual fields, computes confidence)
134-
4. non-passport path: `classify_document` routes full-page OCR to the matching extractor (`pan` / `aadhaar` / `driving_licence` / `voter_id`), validating each document's identifier (PAN format, Verhoeff for Aadhaar, EPIC/DL format)
167+
4. non-passport path: `classify_document` routes full-page OCR to the matching
168+
extractor (`pan` / `aadhaar` / `driving_licence` / `voter_id` /
169+
`nrega_job_card` / `npr_letter`), checks minimum required fields, validates
170+
identifiers where possible, and fails closed on partial or semantically
171+
implausible records
172+
173+
For non-passport documents, `status: "success"` means the extractor returned
174+
the document-specific minimum field set and passed a conservative OCR-region
175+
confidence gate. Alternate model readings for the same detected geometry count
176+
once. `identifierValid` means only an offline format/checksum check passed. NPR
177+
references have no universal public checksum, so this value is `null`; it is
178+
never evidence of authenticity.
179+
180+
### Non-passport OCR languages
181+
182+
The default recognition behavior is unchanged (`en`, with the existing
183+
automatic fallback). If the expected KYC population uses a known script, run
184+
up to four recognition passes only on the non-passport path:
185+
186+
```bash
187+
DOCUMENT_OCR_KYC_LANGS=en,devanagari make dev
188+
```
189+
190+
Available values are `en`, `latin`, `devanagari`, `ka` (Kannada), `ta`
191+
(Tamil), and `te` (Telugu). Extra models increase latency and memory. The
192+
bundled RapidOCR version has no Bengali recognition model: Bengali-labelled
193+
extractor fixtures prove parsing behavior only, not Bengali image OCR. Select
194+
languages and release thresholds from measured benchmark slices. Server
195+
readiness initializes every configured model so model-download or startup
196+
failures are reported before the first scan. Unsupported names or more than
197+
four configured models fail readiness instead of silently falling back.
135198

136199
Single entry point: `core.pipeline.scan(image_input)`.
137200

@@ -183,15 +246,32 @@ The per-document extractors are covered by deterministic `TextRegion` fixtures
183246
under `tests/python/test_*_extractor.py`. These tests verify parsing and
184247
validation behavior; they are not a claim of real-world OCR accuracy.
185248

186-
For image-level evaluation, place a private dataset and `manifest.json` under
187-
`benchmark-data/` and run:
249+
For the legacy passport image benchmark, place its private dataset and
250+
`manifest.json` under `benchmark-data/` and run:
188251

189252
```bash
190253
make benchmark
191254
```
192255

193-
The directory is ignored by Git. Never commit identity documents or personal
194-
data. See [CONTRIBUTING.md](CONTRIBUTING.md) for fixture rules.
256+
For a non-passport KYC dataset, use the versioned manifest and release gates:
257+
258+
```bash
259+
make benchmark-kyc \
260+
KYC_MANIFEST=/secure/kyc-eval/manifest.json \
261+
KYC_DATASET_ROOT=/secure/kyc-eval \
262+
KYC_REPORT=/secure/kyc-eval/reports/main.json
263+
```
264+
265+
The KYC evaluator reports classification, acceptance/false-success,
266+
exact/normalized field, complete-record, runtime, per-document, and
267+
design/year/issuer/language/capture-quality slice metrics without copying
268+
ground-truth values into its report. Required per-document slices fail manifest
269+
validation when a declared variant is absent. See
270+
[`benchmarks/KYC_DATASET.md`](benchmarks/KYC_DATASET.md) for the variant matrix
271+
and annotation workflow.
272+
273+
`benchmark-data/` is ignored by Git. Never commit identity documents or
274+
personal data. See [CONTRIBUTING.md](CONTRIBUTING.md) for fixture rules.
195275

196276
## Privacy and security
197277

TODOS.md

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,53 @@ TypeScript SDK tests, back-page accuracy, Cloud Run deploy) were completed in
1010
anonymized documents or unmistakably synthetic specimens for the private
1111
`benchmark-data/` suite. Do not commit identity documents to this repository.
1212

13-
2. **End-to-end image benchmark for the new document types.** The PAN / Aadhaar /
14-
driving-licence / voter-ID extractors are covered only by deterministic
15-
`TextRegion` fixtures (`tests/python/test_*_extractor.py`) — they never run
16-
real OCR. Add an image-level benchmark (anonymized real cards with ground-truth
17-
labels) to measure true end-to-end accuracy.
18-
19-
3. **Deskew in the preprocessor.** `core/preprocessor.py` does document-boundary
13+
2. **Populate and lock the KYC image benchmark.** The versioned evaluator,
14+
manifest schema, variant slices, and release gates now exist, but the
15+
repository intentionally contains no identity-document images. Build the
16+
consented private dataset, establish baselines for all six non-passport
17+
document types, and require the locked split before release.
18+
19+
3. **Regional-script OCR coverage.** KYC-only model selection can now be
20+
configured with `DOCUMENT_OCR_KYC_LANGS`, but automatic selection and
21+
Bengali recognition are unavailable. Measure every configured language
22+
slice and add models only where the locked dataset demonstrates a gain.
23+
24+
4. **Deskew in the preprocessor.** `core/preprocessor.py` does document-boundary
2025
perspective correction but no text-line deskew, so rotated/tilted inputs shift
2126
the spatial label→value relationships the extractors rely on. A Hough /
2227
projection-profile deskew step would harden real-world phone-photo accuracy.
2328

24-
4. **Passport probe over-eagerly claims KYC cards.** The cheap bottom-crop
25-
passport probe (`core/pipeline._extract_targeted_regions` + `classify_passport_page`)
26-
treats "SEX" and "DATE OF BIRTH" as biodata hints, so a voter/DL card showing
27-
those exact labels in its lower ~55% can mis-route to the passport path before
28-
`classify_document` runs. Fix: require an MRZ (or a passport keyword) before
29-
committing to the passport path, or run `classify_document` first and only
30-
treat as passport when it agrees.
29+
5. **Multi-page non-passport documents.** PDF preprocessing currently evaluates
30+
the first page. Add a KYC-only page aggregation contract before claiming
31+
complete support for multi-page NPR letters or job-card continuations.
32+
33+
6. **Explicit KYC routing for ambiguous documents.** To preserve the existing
34+
passport-positive path, `scan()` does not override a positive passport-page
35+
classification. Some driving licences and voter cards containing multiple
36+
passport-like labels can therefore remain on the passport path. Add a
37+
separate KYC-only entry point or independent router without changing
38+
passport OCR behavior.
3139

32-
5. **Driving licence layout variance.** The DL extractor is best-effort; layouts
40+
7. **Driving licence layout variance.** The DL extractor is best-effort; layouts
3341
differ substantially by issuing state. Gather fixtures from more states
3442
(especially Smart Card DLs and the newer Parivahan format) and tune
3543
`core/driving_licence_extractor.py`. The DL identifier format check in
3644
`core/validators.py` is also loose — tighten per-state if needed.
3745

38-
6. **Aadhaar name detection.** Aadhaar has no Latin label for the holder name, so
46+
8. **Aadhaar name detection.** Aadhaar has no Latin label for the holder name, so
3947
`_find_name` infers it spatially relative to the DOB line. Validate against
4048
more real layouts (vertical/horizontal cards, masked Aadhaar, mAadhaar PDF).
4149

42-
7. **SDK retry semantics for 4xx.** Thread structured HTTP status information
50+
9. **SDK retry semantics for 4xx.** Thread structured HTTP status information
4351
through retry handling rather than inferring retryability from error-message
4452
text.
53+
54+
## Completed in 3.0.0
55+
56+
- Added NREGA job-card and NPR-letter extraction, classification, public result
57+
contracts, and deterministic variation tests.
58+
- Added fail-closed non-passport completeness/identifier/semantic assessment.
59+
- Added the private KYC benchmark framework with versioned manifests,
60+
per-document/per-field metrics, variation slices, and threshold gates.
61+
- Kept dedicated passport modules and passport-positive routing unchanged; new
62+
KYC families use only the existing unknown/no-text routing boundary.

0 commit comments

Comments
 (0)