Spaces:
Sleeping
Sleeping
File size: 4,150 Bytes
0e39d80 | 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 | """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,
}
|