document-ocr 0.0.0-stage → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/index.cjs +659 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +293 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +616 -0
- package/dist/index.js.map +1 -0
- package/hosting/.dockerignore +17 -0
- package/hosting/.gcloudignore +14 -0
- package/hosting/FEATURES.md +172 -0
- package/hosting/HOSTING.md +268 -0
- package/hosting/LICENSE +21 -0
- package/hosting/README.md +369 -0
- package/hosting/core/__init__.py +0 -0
- package/hosting/core/aadhaar_extractor.py +298 -0
- package/hosting/core/back_page_extractor.py +492 -0
- package/hosting/core/barcodes.py +125 -0
- package/hosting/core/card_recovery.py +244 -0
- package/hosting/core/document_bundle.py +82 -0
- package/hosting/core/document_classifier.py +233 -0
- package/hosting/core/document_input.py +135 -0
- package/hosting/core/document_registry.py +143 -0
- package/hosting/core/driving_licence_extractor.py +510 -0
- package/hosting/core/ead_recovery.py +54 -0
- package/hosting/core/evidence.py +54 -0
- package/hosting/core/form_recovery.py +61 -0
- package/hosting/core/jobs.py +232 -0
- package/hosting/core/kyc_ocr.py +132 -0
- package/hosting/core/kyc_validation.py +411 -0
- package/hosting/core/model_setup.py +13 -0
- package/hosting/core/mrz_parser.py +336 -0
- package/hosting/core/mrz_recovery.py +193 -0
- package/hosting/core/npr_extractor.py +410 -0
- package/hosting/core/nrega_extractor.py +827 -0
- package/hosting/core/ocr_engine.py +207 -0
- package/hosting/core/page_classifier.py +137 -0
- package/hosting/core/pan_extractor.py +183 -0
- package/hosting/core/pipeline.py +712 -0
- package/hosting/core/preprocessor.py +297 -0
- package/hosting/core/recovery_budget.py +67 -0
- package/hosting/core/redaction.py +48 -0
- package/hosting/core/review.html +244 -0
- package/hosting/core/structured_documents.py +422 -0
- package/hosting/core/travel_mrz.py +164 -0
- package/hosting/core/travel_recovery.py +187 -0
- package/hosting/core/validator.py +396 -0
- package/hosting/core/validators.py +179 -0
- package/hosting/core/voter_id_extractor.py +363 -0
- package/hosting/deploy/cloudflare/deploy.sh +44 -0
- package/hosting/deploy/cloudflare/package-lock.json +3088 -0
- package/hosting/deploy/cloudflare/package.json +29 -0
- package/hosting/deploy/cloudflare/src/gateway.ts +42 -0
- package/hosting/deploy/cloudflare/src/index.ts +21 -0
- package/hosting/deploy/cloudflare/test/gateway.test.ts +59 -0
- package/hosting/deploy/cloudflare/tsconfig.json +12 -0
- package/hosting/deploy/cloudflare/worker-configuration.d.ts +16253 -0
- package/hosting/deploy/cloudflare/wrangler.jsonc +26 -0
- package/hosting/deploy/cloudrun/cloudbuild.yaml +18 -0
- package/hosting/deploy/cloudrun/deploy.sh +71 -0
- package/hosting/deploy/cloudrun/render_service.py +47 -0
- package/hosting/deploy/common.sh +14 -0
- package/hosting/deploy/docker/Dockerfile +16 -0
- package/hosting/deploy/docker/server.py +313 -0
- package/hosting/deploy/lambda/Dockerfile.lambda +11 -0
- package/hosting/deploy/lambda/deploy.sh +22 -0
- package/hosting/deploy/lambda/handler.py +132 -0
- package/hosting/deploy/lambda/template.yaml +59 -0
- package/hosting/deploy/local/dev.sh +18 -0
- package/hosting/deploy/server/Caddyfile +5 -0
- package/hosting/deploy/server/compose.jobs.yaml +36 -0
- package/hosting/deploy/server/compose.yaml +38 -0
- package/hosting/deploy/server/deploy.sh +23 -0
- package/hosting/infra/cloudrun/main.tf +104 -0
- package/hosting/infra/cloudrun/terraform.tfvars.example +7 -0
- package/hosting/infra/cloudrun/variables.tf +55 -0
- package/hosting/infra/lambda/main.tf +63 -0
- package/hosting/infra/lambda/terraform.tfvars.example +6 -0
- package/hosting/infra/lambda/variables.tf +37 -0
- package/hosting/pyproject.toml +65 -0
- package/hosting/requirements.lock +134 -0
- package/package.json +76 -4
- package/python/core/__init__.py +0 -0
- package/python/core/aadhaar_extractor.py +298 -0
- package/python/core/back_page_extractor.py +492 -0
- package/python/core/barcodes.py +125 -0
- package/python/core/card_recovery.py +244 -0
- package/python/core/document_bundle.py +82 -0
- package/python/core/document_classifier.py +233 -0
- package/python/core/document_input.py +135 -0
- package/python/core/document_registry.py +143 -0
- package/python/core/driving_licence_extractor.py +510 -0
- package/python/core/ead_recovery.py +54 -0
- package/python/core/evidence.py +54 -0
- package/python/core/form_recovery.py +61 -0
- package/python/core/jobs.py +232 -0
- package/python/core/kyc_ocr.py +132 -0
- package/python/core/kyc_validation.py +411 -0
- package/python/core/model_setup.py +13 -0
- package/python/core/mrz_parser.py +336 -0
- package/python/core/mrz_recovery.py +193 -0
- package/python/core/npr_extractor.py +410 -0
- package/python/core/nrega_extractor.py +827 -0
- package/python/core/ocr_engine.py +207 -0
- package/python/core/page_classifier.py +137 -0
- package/python/core/pan_extractor.py +183 -0
- package/python/core/pipeline.py +712 -0
- package/python/core/preprocessor.py +297 -0
- package/python/core/recovery_budget.py +67 -0
- package/python/core/redaction.py +48 -0
- package/python/core/review.html +244 -0
- package/python/core/structured_documents.py +422 -0
- package/python/core/travel_mrz.py +164 -0
- package/python/core/travel_recovery.py +187 -0
- package/python/core/validator.py +396 -0
- package/python/core/validators.py +179 -0
- package/python/core/voter_id_extractor.py +363 -0
- package/python/pyproject.toml +34 -0
- package/python/requirements.lock +134 -0
- package/python/server.py +313 -0
- package/scripts/dococr.mjs +163 -0
- package/scripts/postinstall.mjs +173 -0
- package/scripts/sync-hosting.mjs +35 -0
- package/scripts/sync-python.sh +20 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Validation and confidence assessment for non-passport document extraction.
|
|
2
|
+
|
|
3
|
+
This module deliberately separates three different questions:
|
|
4
|
+
|
|
5
|
+
* did OCR classify the document family?
|
|
6
|
+
* did the extractor return the minimum fields needed for that family?
|
|
7
|
+
* does the printed identifier pass the validation that is possible offline?
|
|
8
|
+
|
|
9
|
+
It does not establish document authenticity. Format checks and Aadhaar's
|
|
10
|
+
Verhoeff checksum can reject obvious OCR errors, but issuer verification
|
|
11
|
+
requires the appropriate signed QR, digital signature, or issuer service.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import date, datetime
|
|
18
|
+
from statistics import fmean
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
from .ocr_engine import TextRegion
|
|
22
|
+
from .validators import (
|
|
23
|
+
is_valid_aadhaar,
|
|
24
|
+
is_valid_dl,
|
|
25
|
+
is_valid_epic,
|
|
26
|
+
is_valid_nrega_job_card,
|
|
27
|
+
is_valid_pan,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
MIN_OCR_GEOMETRY_CONFIDENCE_FOR_SUCCESS = 0.5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class KycExtractionAssessment:
|
|
35
|
+
"""Outcome of document-specific extraction validation."""
|
|
36
|
+
|
|
37
|
+
complete: bool
|
|
38
|
+
confidence: float
|
|
39
|
+
identifier_valid: Optional[bool]
|
|
40
|
+
missing_required_fields: list[str] = field(default_factory=list)
|
|
41
|
+
errors: list[str] = field(default_factory=list)
|
|
42
|
+
warnings: list[str] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def assess_kyc_extraction(
|
|
46
|
+
document_type: str,
|
|
47
|
+
extracted: Any,
|
|
48
|
+
regions: list[TextRegion],
|
|
49
|
+
) -> KycExtractionAssessment:
|
|
50
|
+
"""Validate one non-passport extraction and estimate extraction confidence.
|
|
51
|
+
|
|
52
|
+
Confidence is an explicit heuristic built from required-field completeness,
|
|
53
|
+
identifier validation, and OCR-region confidence. It is intentionally
|
|
54
|
+
capped for partial or invalid records and must not be interpreted as a
|
|
55
|
+
calibrated probability until measured against a private evaluation set.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if document_type == "pan":
|
|
59
|
+
required = {
|
|
60
|
+
"panNumber": _value(extracted, "pan_number"),
|
|
61
|
+
"name": _value(extracted, "name"),
|
|
62
|
+
"dateOfBirth": _value(extracted, "date_of_birth"),
|
|
63
|
+
}
|
|
64
|
+
identifier = _value(extracted, "pan_number")
|
|
65
|
+
identifier_valid: Optional[bool] = bool(
|
|
66
|
+
identifier and is_valid_pan(identifier)
|
|
67
|
+
)
|
|
68
|
+
warnings: list[str] = []
|
|
69
|
+
elif document_type == "aadhaar":
|
|
70
|
+
return _assess_aadhaar(extracted, regions)
|
|
71
|
+
elif document_type == "driving_licence":
|
|
72
|
+
required = {
|
|
73
|
+
"dlNumber": _value(extracted, "dl_number"),
|
|
74
|
+
"name": _value(extracted, "name"),
|
|
75
|
+
"dateOfBirth": _value(extracted, "date_of_birth"),
|
|
76
|
+
"issueDate": _value(extracted, "issue_date"),
|
|
77
|
+
"validityDate": _value(extracted, "validity_date"),
|
|
78
|
+
}
|
|
79
|
+
identifier = _value(extracted, "dl_number")
|
|
80
|
+
identifier_valid = bool(identifier and is_valid_dl(identifier))
|
|
81
|
+
warnings = []
|
|
82
|
+
elif document_type == "voter_id":
|
|
83
|
+
required = {
|
|
84
|
+
"epicNumber": _value(extracted, "epic_number"),
|
|
85
|
+
"name": _value(extracted, "name"),
|
|
86
|
+
"dateOfBirthOrAge": (
|
|
87
|
+
_value(extracted, "date_of_birth") or _value(extracted, "age")
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
identifier = _value(extracted, "epic_number")
|
|
91
|
+
identifier_valid = bool(identifier and is_valid_epic(identifier))
|
|
92
|
+
warnings = []
|
|
93
|
+
elif document_type == "nrega_job_card":
|
|
94
|
+
members = _value(extracted, "members") or []
|
|
95
|
+
member_names = [
|
|
96
|
+
_value(member, "name")
|
|
97
|
+
for member in members
|
|
98
|
+
if _value(member, "name")
|
|
99
|
+
]
|
|
100
|
+
job_card_number = _value(extracted, "job_card_number")
|
|
101
|
+
required = {
|
|
102
|
+
"jobCardNumber": job_card_number,
|
|
103
|
+
"householdOrMemberName": (
|
|
104
|
+
_value(extracted, "head_of_household") or member_names
|
|
105
|
+
),
|
|
106
|
+
"location": (
|
|
107
|
+
_value(extracted, "village")
|
|
108
|
+
or _value(extracted, "gram_panchayat")
|
|
109
|
+
or _value(extracted, "district")
|
|
110
|
+
or _value(extracted, "address")
|
|
111
|
+
),
|
|
112
|
+
}
|
|
113
|
+
identifier_valid = bool(
|
|
114
|
+
job_card_number and is_valid_nrega_job_card(job_card_number)
|
|
115
|
+
)
|
|
116
|
+
warnings = (
|
|
117
|
+
["IDENTIFIER_FORMAT_ONLY_NOT_AUTHENTICITY"]
|
|
118
|
+
if identifier_valid
|
|
119
|
+
else []
|
|
120
|
+
)
|
|
121
|
+
elif document_type == "npr_letter":
|
|
122
|
+
required = {
|
|
123
|
+
"name": _value(extracted, "name"),
|
|
124
|
+
"address": _value(extracted, "address"),
|
|
125
|
+
}
|
|
126
|
+
# RBI defines the NPR name/address letter as an OVD but there is no
|
|
127
|
+
# public universal checksum for a printed reference number.
|
|
128
|
+
identifier_valid = None
|
|
129
|
+
warnings = ["IDENTIFIER_NOT_OFFLINE_VERIFIABLE"]
|
|
130
|
+
else:
|
|
131
|
+
return KycExtractionAssessment(
|
|
132
|
+
complete=False,
|
|
133
|
+
confidence=0.0,
|
|
134
|
+
identifier_valid=False,
|
|
135
|
+
errors=["UNSUPPORTED_KYC_DOCUMENT_TYPE"],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
assessment = _build_assessment(required, identifier_valid, regions, warnings)
|
|
139
|
+
return _apply_semantic_checks(document_type, extracted, assessment)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _assess_aadhaar(
|
|
143
|
+
extracted: Any,
|
|
144
|
+
regions: list[TextRegion],
|
|
145
|
+
) -> KycExtractionAssessment:
|
|
146
|
+
number = _value(extracted, "aadhaar_number")
|
|
147
|
+
masked = bool(_value(extracted, "aadhaar_masked"))
|
|
148
|
+
last4 = _value(extracted, "aadhaar_last4")
|
|
149
|
+
identifier_present = number or (masked and last4)
|
|
150
|
+
|
|
151
|
+
# A back/address side usually omits the holder name and birth details.
|
|
152
|
+
address = _value(extracted, "address")
|
|
153
|
+
pincode = _value(extracted, "pincode")
|
|
154
|
+
has_front_demographics = bool(
|
|
155
|
+
_value(extracted, "name")
|
|
156
|
+
or _value(extracted, "date_of_birth")
|
|
157
|
+
or _value(extracted, "year_of_birth")
|
|
158
|
+
or _value(extracted, "gender")
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if (address or pincode) and not has_front_demographics:
|
|
162
|
+
required = {
|
|
163
|
+
"aadhaarNumberOrMaskedLast4": identifier_present,
|
|
164
|
+
"address": address,
|
|
165
|
+
"pincode": pincode,
|
|
166
|
+
}
|
|
167
|
+
else:
|
|
168
|
+
required = {
|
|
169
|
+
"aadhaarNumberOrMaskedLast4": identifier_present,
|
|
170
|
+
"name": _value(extracted, "name"),
|
|
171
|
+
"dateOrYearOfBirth": (
|
|
172
|
+
_value(extracted, "date_of_birth")
|
|
173
|
+
or _value(extracted, "year_of_birth")
|
|
174
|
+
),
|
|
175
|
+
"gender": _value(extracted, "gender"),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
warnings: list[str] = []
|
|
179
|
+
if number:
|
|
180
|
+
identifier_valid: Optional[bool] = is_valid_aadhaar(number)
|
|
181
|
+
elif masked and last4:
|
|
182
|
+
identifier_valid = None
|
|
183
|
+
warnings.append("MASKED_AADHAAR_CHECKSUM_UNAVAILABLE")
|
|
184
|
+
else:
|
|
185
|
+
identifier_valid = False
|
|
186
|
+
|
|
187
|
+
assessment = _build_assessment(required, identifier_valid, regions, warnings)
|
|
188
|
+
return _apply_semantic_checks("aadhaar", extracted, assessment)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _build_assessment(
|
|
192
|
+
required: dict[str, Any],
|
|
193
|
+
identifier_valid: Optional[bool],
|
|
194
|
+
regions: list[TextRegion],
|
|
195
|
+
warnings: list[str],
|
|
196
|
+
) -> KycExtractionAssessment:
|
|
197
|
+
missing = [name for name, value in required.items() if not _present(value)]
|
|
198
|
+
completeness = (len(required) - len(missing)) / len(required) if required else 0.0
|
|
199
|
+
geometry_confidences = _ocr_geometry_confidences(regions)
|
|
200
|
+
ocr_confidence = (
|
|
201
|
+
fmean(geometry_confidences)
|
|
202
|
+
if geometry_confidences
|
|
203
|
+
else 0.0
|
|
204
|
+
)
|
|
205
|
+
minimum_ocr_confidence = (
|
|
206
|
+
min(geometry_confidences)
|
|
207
|
+
if geometry_confidences
|
|
208
|
+
else 0.0
|
|
209
|
+
)
|
|
210
|
+
if identifier_valid is True:
|
|
211
|
+
identifier_score = 1.0
|
|
212
|
+
elif identifier_valid is None:
|
|
213
|
+
identifier_score = 0.6
|
|
214
|
+
else:
|
|
215
|
+
identifier_score = 0.0
|
|
216
|
+
|
|
217
|
+
confidence = (
|
|
218
|
+
(0.5 * completeness)
|
|
219
|
+
+ (0.3 * identifier_score)
|
|
220
|
+
+ (0.2 * ocr_confidence)
|
|
221
|
+
)
|
|
222
|
+
errors: list[str] = []
|
|
223
|
+
|
|
224
|
+
if identifier_valid is False:
|
|
225
|
+
errors.append("INVALID_OR_MISSING_DOCUMENT_IDENTIFIER")
|
|
226
|
+
confidence = min(confidence, 0.39)
|
|
227
|
+
if missing:
|
|
228
|
+
errors.append("MISSING_REQUIRED_FIELDS")
|
|
229
|
+
confidence = min(confidence, 0.69)
|
|
230
|
+
if minimum_ocr_confidence < MIN_OCR_GEOMETRY_CONFIDENCE_FOR_SUCCESS:
|
|
231
|
+
errors.append("LOW_OCR_CONFIDENCE")
|
|
232
|
+
confidence = min(confidence, 0.49)
|
|
233
|
+
|
|
234
|
+
complete = (
|
|
235
|
+
not missing
|
|
236
|
+
and identifier_valid is not False
|
|
237
|
+
and minimum_ocr_confidence
|
|
238
|
+
>= MIN_OCR_GEOMETRY_CONFIDENCE_FOR_SUCCESS
|
|
239
|
+
)
|
|
240
|
+
return KycExtractionAssessment(
|
|
241
|
+
complete=complete,
|
|
242
|
+
confidence=round(max(0.0, min(confidence, 1.0)), 3),
|
|
243
|
+
identifier_valid=identifier_valid,
|
|
244
|
+
missing_required_fields=missing,
|
|
245
|
+
errors=errors,
|
|
246
|
+
warnings=warnings,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _ocr_geometry_confidences(regions: list[TextRegion]) -> list[float]:
|
|
251
|
+
# Multiple configured recognizers can emit different script readings for
|
|
252
|
+
# the same physical box. Count that geometry once so adding a model does
|
|
253
|
+
# not multiply-weight a region or turn an otherwise identical extraction
|
|
254
|
+
# into a low-confidence failure.
|
|
255
|
+
confidence_by_geometry: dict[tuple[tuple[int, int], ...], float] = {}
|
|
256
|
+
for region in regions:
|
|
257
|
+
geometry = tuple(tuple(point) for point in region.bbox)
|
|
258
|
+
score = max(0.0, min(float(region.confidence), 1.0))
|
|
259
|
+
confidence_by_geometry[geometry] = max(
|
|
260
|
+
score,
|
|
261
|
+
confidence_by_geometry.get(geometry, 0.0),
|
|
262
|
+
)
|
|
263
|
+
return list(confidence_by_geometry.values())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _apply_semantic_checks(
|
|
267
|
+
document_type: str,
|
|
268
|
+
extracted: Any,
|
|
269
|
+
assessment: KycExtractionAssessment,
|
|
270
|
+
) -> KycExtractionAssessment:
|
|
271
|
+
semantic_errors: list[str] = []
|
|
272
|
+
|
|
273
|
+
if document_type == "pan":
|
|
274
|
+
_require_past_date(
|
|
275
|
+
_value(extracted, "date_of_birth"),
|
|
276
|
+
"INVALID_DATE_OF_BIRTH",
|
|
277
|
+
semantic_errors,
|
|
278
|
+
)
|
|
279
|
+
elif document_type == "aadhaar":
|
|
280
|
+
dob = _value(extracted, "date_of_birth")
|
|
281
|
+
yob = _value(extracted, "year_of_birth")
|
|
282
|
+
if dob:
|
|
283
|
+
_require_past_date(dob, "INVALID_DATE_OF_BIRTH", semantic_errors)
|
|
284
|
+
elif yob:
|
|
285
|
+
try:
|
|
286
|
+
year = int(yob)
|
|
287
|
+
except (TypeError, ValueError):
|
|
288
|
+
semantic_errors.append("INVALID_YEAR_OF_BIRTH")
|
|
289
|
+
else:
|
|
290
|
+
if not 1900 <= year <= date.today().year:
|
|
291
|
+
semantic_errors.append("INVALID_YEAR_OF_BIRTH")
|
|
292
|
+
elif document_type == "driving_licence":
|
|
293
|
+
dob = _parse_printed_date(_value(extracted, "date_of_birth"))
|
|
294
|
+
issue = _parse_printed_date(_value(extracted, "issue_date"))
|
|
295
|
+
validity = _parse_printed_date(_value(extracted, "validity_date"))
|
|
296
|
+
if dob is None:
|
|
297
|
+
semantic_errors.append("INVALID_DATE_OF_BIRTH")
|
|
298
|
+
if issue is None:
|
|
299
|
+
semantic_errors.append("INVALID_ISSUE_DATE")
|
|
300
|
+
if validity is None:
|
|
301
|
+
semantic_errors.append("INVALID_VALIDITY_DATE")
|
|
302
|
+
if issue and issue > date.today():
|
|
303
|
+
semantic_errors.append("INVALID_ISSUE_DATE")
|
|
304
|
+
if dob and issue and dob >= issue:
|
|
305
|
+
semantic_errors.append("INVALID_DL_DATE_CHRONOLOGY")
|
|
306
|
+
if issue and validity and issue >= validity:
|
|
307
|
+
semantic_errors.append("INVALID_DL_DATE_CHRONOLOGY")
|
|
308
|
+
elif document_type == "voter_id":
|
|
309
|
+
dob = _value(extracted, "date_of_birth")
|
|
310
|
+
age = _value(extracted, "age")
|
|
311
|
+
if dob:
|
|
312
|
+
_require_past_date(dob, "INVALID_DATE_OF_BIRTH", semantic_errors)
|
|
313
|
+
elif age:
|
|
314
|
+
try:
|
|
315
|
+
age_number = int(age)
|
|
316
|
+
except (TypeError, ValueError):
|
|
317
|
+
semantic_errors.append("INVALID_AGE")
|
|
318
|
+
else:
|
|
319
|
+
if not 18 <= age_number <= 130:
|
|
320
|
+
semantic_errors.append("INVALID_AGE")
|
|
321
|
+
elif document_type == "nrega_job_card":
|
|
322
|
+
for value, error in (
|
|
323
|
+
(_value(extracted, "registration_date"), "INVALID_REGISTRATION_DATE"),
|
|
324
|
+
(_value(extracted, "validity_from"), "INVALID_VALIDITY_FROM"),
|
|
325
|
+
(_value(extracted, "validity_to"), "INVALID_VALIDITY_TO"),
|
|
326
|
+
):
|
|
327
|
+
if value and _parse_printed_date(value) is None:
|
|
328
|
+
semantic_errors.append(error)
|
|
329
|
+
registration = _parse_printed_date(
|
|
330
|
+
_value(extracted, "registration_date")
|
|
331
|
+
)
|
|
332
|
+
if registration and registration > date.today():
|
|
333
|
+
semantic_errors.append("INVALID_REGISTRATION_DATE")
|
|
334
|
+
valid_from = _parse_printed_date(_value(extracted, "validity_from"))
|
|
335
|
+
valid_to = _parse_printed_date(_value(extracted, "validity_to"))
|
|
336
|
+
if valid_from and valid_to and valid_from >= valid_to:
|
|
337
|
+
semantic_errors.append("INVALID_NREGA_VALIDITY_CHRONOLOGY")
|
|
338
|
+
elif document_type == "npr_letter":
|
|
339
|
+
issue_date = _value(extracted, "issue_date")
|
|
340
|
+
if issue_date:
|
|
341
|
+
_require_not_future_date(
|
|
342
|
+
issue_date,
|
|
343
|
+
"INVALID_ISSUE_DATE",
|
|
344
|
+
semantic_errors,
|
|
345
|
+
)
|
|
346
|
+
pincode = _value(extracted, "pincode")
|
|
347
|
+
if pincode and not (
|
|
348
|
+
isinstance(pincode, str)
|
|
349
|
+
and len(pincode) == 6
|
|
350
|
+
and pincode.isdigit()
|
|
351
|
+
and not pincode.startswith("0")
|
|
352
|
+
):
|
|
353
|
+
semantic_errors.append("INVALID_PINCODE")
|
|
354
|
+
|
|
355
|
+
if semantic_errors:
|
|
356
|
+
assessment.complete = False
|
|
357
|
+
assessment.confidence = min(assessment.confidence, 0.49)
|
|
358
|
+
assessment.errors.extend(
|
|
359
|
+
error for error in semantic_errors if error not in assessment.errors
|
|
360
|
+
)
|
|
361
|
+
return assessment
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _require_past_date(
|
|
365
|
+
value: Any,
|
|
366
|
+
error: str,
|
|
367
|
+
errors: list[str],
|
|
368
|
+
) -> None:
|
|
369
|
+
parsed = _parse_printed_date(value)
|
|
370
|
+
if parsed is None or parsed >= date.today():
|
|
371
|
+
errors.append(error)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _require_not_future_date(
|
|
375
|
+
value: Any,
|
|
376
|
+
error: str,
|
|
377
|
+
errors: list[str],
|
|
378
|
+
) -> None:
|
|
379
|
+
parsed = _parse_printed_date(value)
|
|
380
|
+
if parsed is None or parsed > date.today():
|
|
381
|
+
errors.append(error)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _parse_printed_date(value: Any) -> Optional[date]:
|
|
385
|
+
if not isinstance(value, str) or not value.strip():
|
|
386
|
+
return None
|
|
387
|
+
normalised = value.strip().replace("/", "-").replace(".", "-")
|
|
388
|
+
for fmt in (
|
|
389
|
+
"%d-%m-%Y",
|
|
390
|
+
"%d-%m-%y",
|
|
391
|
+
"%Y-%m-%d",
|
|
392
|
+
"%d %B %Y",
|
|
393
|
+
"%d %b %Y",
|
|
394
|
+
):
|
|
395
|
+
try:
|
|
396
|
+
return datetime.strptime(normalised, fmt).date()
|
|
397
|
+
except ValueError:
|
|
398
|
+
continue
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _value(obj: Any, name: str) -> Any:
|
|
403
|
+
return getattr(obj, name, None)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _present(value: Any) -> bool:
|
|
407
|
+
if isinstance(value, str):
|
|
408
|
+
return bool(value.strip())
|
|
409
|
+
if isinstance(value, (list, tuple, set, dict)):
|
|
410
|
+
return bool(value)
|
|
411
|
+
return bool(value)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Fetch configured models at image build time or warm them before serving."""
|
|
2
|
+
from .kyc_ocr import configured_kyc_languages
|
|
3
|
+
from .ocr_engine import _get_ocr
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def warm_up() -> None:
|
|
7
|
+
for language in dict.fromkeys(("en", *configured_kyc_languages())):
|
|
8
|
+
_get_ocr(language)
|
|
9
|
+
_get_ocr("en", band=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
warm_up()
|