File size: 6,633 Bytes
2aec074 2f1623d 2aec074 5494c53 2aec074 b308e8d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | """
KYC OCR โ Gradio app for Hugging Face Spaces.
Upload a PAN card, cheque, Aadhaar, or passport (PDF or image) and get
structured field extraction. Runs on CPU using RapidOCR (ONNX).
"""
# โโ Patch a known gradio_client bug โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# gradio_client.utils.get_type assumes schema is always a dict, but JSON
# Schema's `additionalProperties` field can legitimately be a bool. When
# Gradio's /info endpoint runs (HF Spaces health checks ping it), the
# function crashes with: "TypeError: argument of type 'bool' is not iterable".
# Fixed upstream in newer releases but still present in many 5.x. Patch it.
import gradio_client.utils as _gcu
_orig_get_type = _gcu.get_type
def _patched_get_type(schema):
if not isinstance(schema, dict):
return "Any"
return _orig_get_type(schema)
_gcu.get_type = _patched_get_type
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import gradio as gr
import time
import traceback
from pathlib import Path
from kyc_extract import process, RapidOCR
# โโ Initialise OCR engine once at startup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
print("Loading RapidOCR engine (ONNX models)โฆ")
_t0 = time.time()
ENGINE = RapidOCR()
print(f"Ready. Engine init took {time.time() - _t0:.1f}s")
# โโ Pretty-format the extracted fields as Markdown โโโโโโโโโโโโโโโโโโโโ
def _fmt_fields(fields: dict) -> str:
if not fields:
return "_No fields extracted._"
lines = []
for k, v in fields.items():
label = k.replace("_", " ").title()
if isinstance(v, bool):
v = "โ
Yes" if v else "โ No"
elif v is None:
v = "_(not found)_"
lines.append(f"**{label}:** {v}")
return "\n\n".join(lines)
def _fmt_info(result: dict) -> str:
bits = [
f"**Detected type:** `{result.get('detected_type', 'unknown').upper()}`",
f"**OCR time:** {result.get('ocr_time_s')}s",
f"**Rotation applied:** {result.get('rotation_applied', 0)}ยฐ",
f"**OCR quality score:** {result.get('ocr_score')}",
]
return " โข ".join(bits)
# โโ Main handler โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def extract(file):
if file is None:
return "_Please upload a file._", "", ""
file_path = file if isinstance(file, str) else file.name
try:
pages = process(file_path, ENGINE)
except Exception as exc:
return (
f"โ **Error:** {exc}\n\n```\n{traceback.format_exc()}\n```",
"",
"",
)
if not pages:
return "_No pages processed._", "", ""
# Render all pages (cheques and PANs are normally single-page; multi-page
# PDFs of compound KYC packets get each page rendered separately).
info_blocks, field_blocks, raw_blocks = [], [], []
for p in pages:
if "error" in p:
info_blocks.append(f"### Page {p['page']}\n_{p['error']}_")
continue
info_blocks.append(f"### Page {p['page']}\n{_fmt_info(p)}")
field_blocks.append(f"### Page {p['page']}\n{_fmt_fields(p['fields'])}")
raw_blocks.append(
f"โโโ Page {p['page']} โโโ\n"
+ "\n".join(f"[{c:.2f}] {t}" for c, t in p["_raw_lines"])
)
return (
"\n\n".join(info_blocks),
"\n\n---\n\n".join(field_blocks) if field_blocks else "_No fields extracted._",
"\n\n".join(raw_blocks),
)
# โโ Gradio UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DESCRIPTION = """
# ๐ฎ๐ณ KYC Document OCR
Extract structured fields from Indian KYC documents โ **PAN card**, **cheque**,
**Aadhaar**, and **passport** โ from images or PDFs. Runs entirely on CPU using
[RapidOCR](https://github.com/RapidAI/RapidOCR) (PaddleOCR PP-OCRv4 models in ONNX).
**Supported documents and fields:**
- **PAN:** PAN number, name, father's name, DOB / date of incorporation, entity type
- **Cheque:** bank name, address, IFSC, account number, MICR code, cheque number, transaction code
- **Aadhaar / Passport:** _coming soon โ Aadhaar reads best from QR code (`pyzbar`), passport from MRZ (`passporteye`)_
**Privacy note:** this is a public Space. Don't upload real personal documents.
For production use, fork it and run on private infrastructure.
"""
with gr.Blocks(title="KYC OCR") as demo:
gr.Markdown(DESCRIPTION)
with gr.Row():
with gr.Column(scale=1):
file_input = gr.File(
label="Upload PAN / Cheque (PDF or image)",
file_types=[".pdf", ".png", ".jpg", ".jpeg", ".webp"],
type="filepath",
)
submit_btn = gr.Button("Extract fields", variant="primary", size="lg")
# Use an absolute path; skip the Examples block if the example file
# wasn't uploaded (won't crash the whole app).
_example_path = Path(__file__).parent / "examples" / "synthetic_pan.png"
if _example_path.exists():
gr.Examples(
examples=[[str(_example_path)]],
inputs=file_input,
label="Example (synthetic PAN with fake data)",
)
with gr.Column(scale=2):
info_out = gr.Markdown()
fields_out = gr.Markdown()
with gr.Accordion("Raw OCR output (for debugging)", open=False):
raw_out = gr.Textbox(label="Raw OCR lines with confidence scores",
lines=20)
gr.Markdown(
"---\n"
"Pipeline source: `kyc_extract.py`. "
"All extraction is rule-based โ no hardcoded names/banks. "
"Doc-type detection โ optional rotation correction (cheques) โ "
"format-spec regex + heuristics + checksum validation."
)
submit_btn.click(
extract,
inputs=file_input,
outputs=[info_out, fields_out, raw_out],
)
file_input.change(
extract,
inputs=file_input,
outputs=[info_out, fields_out, raw_out],
)
if __name__ == "__main__":
demo.launch() |