Spaces:
Sleeping
Sleeping
| """Confidence scoring and decision engine β explainable decisions.""" | |
| from __future__ import annotations | |
| from ml_utils.ocr import OcrResult, get_average_confidence | |
| # Legacy constants used by fft_detect.py and edge_detect.py | |
| FFT_MAX = 8.0 | |
| EDGE_MAX = 7.0 | |
| # ββ Required fields per doc type ββββββββββββββββββββββββββββββββββββββββ | |
| REQUIRED_FIELDS: dict[str, set[str]] = { | |
| "aadhaar": {"aadhaar_number", "name", "dob"}, | |
| "pan": {"pan_number", "name"}, | |
| "caste": {"applicant_name", "caste_category"}, | |
| "experience": {"company_name", "employee_name"}, | |
| "education": {"candidate_name", "institute", "degree"}, | |
| "resume": {"name", "email"}, | |
| "general": set(), | |
| } | |
| # ββ Score Components ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def compute_ocr_score(ocr_results: list[OcrResult]) -> float: | |
| """OCR quality: avg confidence Γ 35 (max 35).""" | |
| avg = get_average_confidence(ocr_results) | |
| return round(min(35.0, avg * 35.0), 1) | |
| def compute_field_score(fields: dict, doc_type: str) -> float: | |
| """Field completeness: (found / required) Γ 30 (max 30).""" | |
| required = REQUIRED_FIELDS.get(doc_type, set()) | |
| if not required: | |
| # For general docs, count any detected fields | |
| detected_count = len(fields) | |
| return round(min(30.0, (detected_count / 5.0) * 30.0), 1) | |
| found = sum(1 for f in required if fields.get(f)) | |
| ratio = found / len(required) | |
| return round(ratio * 30.0, 1) | |
| def compute_validation_score(validation_score: float) -> float: | |
| """Already computed by validators as 0-20. Just pass through.""" | |
| return round(min(20.0, max(0.0, validation_score)), 1) | |
| def compute_image_score(fft_score: float | None, edge_score: float | None) -> float: | |
| """Image quality from forgery detection (max 15). | |
| fft_score is 0-15 from fft_detect (higher = more authentic). | |
| edge_score is 0-12 from edge_detect (higher = more consistent). | |
| We normalize to max 15. | |
| """ | |
| fft = fft_score if fft_score is not None else 8.0 | |
| edge = edge_score if edge_score is not None else 7.0 | |
| # fft max 8, edge max 7 β total 15 | |
| return round(min(15.0, fft + edge), 1) | |
| def compute_final_score( | |
| ocr_quality: float, | |
| field_completeness: float, | |
| validation: float, | |
| image_quality: float, | |
| ) -> float: | |
| """Sum all components, cap at 100.""" | |
| return round(min(100.0, ocr_quality + field_completeness + validation + image_quality), 1) | |
| # ββ Decision ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STATUS_THRESHOLDS = [ | |
| (90, "Auto-Verified"), | |
| (75, "Verified"), | |
| (50, "Manual Review Required"), | |
| (25, "Low Confidence"), | |
| (0, "Rejected"), | |
| ] | |
| def decide_status(score: float, flags: list[str]) -> str: | |
| """Map score + flags to a decision status.""" | |
| # Critical flags force rejection regardless of score | |
| critical_flags = {"VERHOEFF_CHECKSUM_FAILED", "INVALID_PAN_FORMAT", "TEXT_EXTRACT_FAILED"} | |
| if any(f in critical_flags for f in flags): | |
| return "Rejected" | |
| for threshold, status in STATUS_THRESHOLDS: | |
| if score >= threshold: | |
| return status | |
| return "Rejected" | |
| def build_score_breakdown( | |
| ocr_results: list[OcrResult], | |
| fields: dict, | |
| doc_type: str, | |
| validation_score: float, | |
| fft_score: float | None, | |
| edge_score: float | None, | |
| ) -> dict: | |
| """Compute all scores and return full breakdown dict.""" | |
| ocr_q = compute_ocr_score(ocr_results) | |
| field_c = compute_field_score(fields, doc_type) | |
| valid_s = compute_validation_score(validation_score) | |
| img_q = compute_image_score(fft_score, edge_score) | |
| final = compute_final_score(ocr_q, field_c, valid_s, img_q) | |
| return { | |
| "ocr_quality": ocr_q, | |
| "field_completeness": field_c, | |
| "validation": valid_s, | |
| "image_quality": img_q, | |
| "overall": final, | |
| } | |