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