Extraction_validate / ml_utils /confidence.py
Monike123's picture
Deploy DocVerify FastAPI backend (EasyOCR + Gemini 3 Flash)
0e39d80
Raw
History Blame Contribute Delete
4.15 kB
"""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,
}