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()