""" Explainer — turns a ConfidenceScore or UnifiedFaceReport into a multi-line human-readable string for display in the UI / logs. """ from __future__ import annotations from models.reports import ( ConfidenceScore, ConflictReport, UnifiedFaceReport, ) class Explainer: """Renders confidence + report as human-readable text.""" @staticmethod def explain_score(score: ConfidenceScore) -> str: lines = [f"Overall confidence: {score.overall:.2%} ({score.method})"] if score.explanation: lines.append(f" {score.explanation}") if score.components: lines.append(" Components:") for k, v in score.components.items(): lines.append(f" - {k}: {v:.3f}" if isinstance(v, float) else f" - {k}: {v}") return "\n".join(lines) @staticmethod def explain_conflicts(conflicts: list[ConflictReport]) -> str: if not conflicts: return "No conflicts detected." lines = [f"{len(conflicts)} conflict(s) detected:"] for c in conflicts: lines.append( f" [{c.severity.upper()}] {c.kind}: {c.description} " f"(providers: {', '.join(c.providers)})" ) return "\n".join(lines) @staticmethod def explain_report(report: UnifiedFaceReport) -> str: m = report.metadata lines = [ f"Job {m.job_id} — {m.total_elapsed_ms:.0f}ms total", f" Providers invoked: {', '.join(m.providers_invoked) or 'none'}", f" Succeeded: {', '.join(m.providers_succeeded) or 'none'}", f" Failed: {', '.join(m.providers_failed) or 'none'}", f" Detections: {len(report.detections)}", f" Matches: {len(report.matches)}", f" Scraped images: {len(report.scraped_images)}", f" Reverse matches: {len(report.reverse_matches)}", f" Image analyses: {len(report.image_analyses)}", f" Metadata extractions: {len(report.metadata_extractions)}", f" Forensics: {len(report.forensics)}", f" Conflicts: {len(report.conflicts)}", ] if m.limitations: lines.append(" Limitations:") for lim in m.limitations: lines.append(f" - {lim}") if report.overall_confidence: lines.append("") lines.append(Explainer.explain_score(report.overall_confidence)) if report.conflicts: lines.append("") lines.append(Explainer.explain_conflicts(report.conflicts)) return "\n".join(lines)