Download app.py from WofoLab/kvc-ocr: direct link, hf CLI and curl.
- Browser
- Download file 6.63 kB
-
https://huggingface.co/spaces/WofoLab/kvc-ocr/resolve/main/app.py
- Command line
-
hf download hf://spaces/WofoLab/kvc-ocr/app.py
-
curl -L -o app.py https://huggingface.co/spaces/WofoLab/kvc-ocr/resolve/main/app.py
6.63 kB
| """ | |
| 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() |