Spaces:
Running
Running
| import logging | |
| logger = logging.getLogger(__name__) | |
| _mrz = None | |
| def _get_mrz(): | |
| """Lazy-initialize FastMRZ. First call loads ONNX model.""" | |
| global _mrz | |
| if _mrz is None: | |
| logger.info("Initializing FastMRZ...") | |
| from fastmrz import FastMRZ | |
| _mrz = FastMRZ() | |
| logger.info("FastMRZ initialized") | |
| return _mrz | |
| def read_mrz(image_path: str) -> dict | None: | |
| """Read MRZ (Machine Readable Zone) from a passport image. | |
| Returns dict with extracted fields, or None if MRZ not found/readable. | |
| Fields returned: | |
| passport_number, surname, given_names, nationality, | |
| date_of_birth, sex, expiry_date, issuing_country, | |
| mrz_valid (bool — check digits passed) | |
| """ | |
| try: | |
| mrz = _get_mrz() | |
| mrz_data = mrz.get_mrz(image_path) | |
| if not mrz_data: | |
| logger.info("No MRZ found in image") | |
| return None | |
| # FastMRZ returns a dict with standardized field names | |
| result = { | |
| "source": "mrz", | |
| "passport_number": mrz_data.get("document_number", "").replace("<", "").strip() or None, | |
| "surname": mrz_data.get("surname", "").replace("<", " ").strip() or None, | |
| "given_names": mrz_data.get("given_name", "").replace("<", " ").strip() or None, | |
| "nationality": mrz_data.get("nationality", "").strip() or None, | |
| "date_of_birth": _format_mrz_date(mrz_data.get("birth_date", "")), | |
| "sex": _normalize_sex(mrz_data.get("sex", "")), | |
| "expiry_date": _format_mrz_date(mrz_data.get("expiry_date", "")), | |
| "issuing_country": mrz_data.get("country", "").strip() or None, | |
| "mrz_valid": mrz_data.get("valid_score", False), | |
| } | |
| # Clean up names | |
| if result["surname"]: | |
| result["surname"] = result["surname"].title() | |
| if result["given_names"]: | |
| result["given_names"] = result["given_names"].title() | |
| logger.info( | |
| "MRZ extracted: nationality=%s passport=%s valid=%s", | |
| result["nationality"], | |
| result["passport_number"], | |
| result["mrz_valid"], | |
| ) | |
| return result | |
| except Exception as e: | |
| logger.warning("MRZ reading failed: %s", e) | |
| return None | |
| def _format_mrz_date(date_str: str) -> str | None: | |
| """Convert MRZ date (YYMMDD) to ISO format (YYYY-MM-DD).""" | |
| if not date_str or len(date_str) < 6: | |
| return None | |
| date_str = date_str.strip().replace("-", "").replace("/", "") | |
| if len(date_str) < 6: | |
| return None | |
| try: | |
| yy = int(date_str[:2]) | |
| mm = int(date_str[2:4]) | |
| dd = int(date_str[4:6]) | |
| # Century heuristic: same as SA ID parser | |
| from datetime import datetime | |
| current_yy = datetime.now().year % 100 | |
| year = 1900 + yy if yy > current_yy else 2000 + yy | |
| from datetime import date | |
| d = date(year, mm, dd) | |
| return d.isoformat() | |
| except (ValueError, IndexError): | |
| return None | |
| def _normalize_sex(sex_str: str) -> str | None: | |
| """Normalize MRZ sex field to 'Male' or 'Female'.""" | |
| s = sex_str.strip().upper() | |
| if s in ("M", "MALE"): | |
| return "Male" | |
| if s in ("F", "FEMALE"): | |
| return "Female" | |
| return None | |
| def warm_mrz() -> bool: | |
| """Pre-load FastMRZ model. Returns True if successful.""" | |
| try: | |
| _get_mrz() | |
| return True | |
| except Exception as e: | |
| logger.warning("FastMRZ warm-up failed: %s", e) | |
| return False | |