""" Conflict detector — finds cross-provider disagreements. Currently detects: - face_count_mismatch (detectors disagree on number of faces) - match_disagreement (recognizers disagree on best match for same face) - quality_disagreement (image-quality providers disagree substantially) - integrity_disagreement (forensics providers disagree on integrity) - format_mismatch (metadata providers disagree on format) """ from __future__ import annotations from typing import Dict, List, Protocol from models.reports import ConflictReport from providers.base import ProviderResult # Duck-typed protocols (confidence must not import from normalization) class _BoxLike(Protocol): detector: str confidence: float class _MatchLike(Protocol): query_face_index: int best_match: str | None recognizer: str class ConflictDetector: """Detects cross-provider disagreements.""" def detect( self, results: Dict[str, ProviderResult], boxes: List[_BoxLike], matches: List[_MatchLike], ) -> List[ConflictReport]: conflicts: List[ConflictReport] = [] conflicts.extend(self._detect_face_count_mismatch(results)) conflicts.extend(self._detect_match_disagreement(matches)) conflicts.extend(self._detect_quality_disagreement(results)) conflicts.extend(self._detect_integrity_disagreement(results)) conflicts.extend(self._detect_format_mismatch(results)) return conflicts # ------------------------------------------------------------------ # # Individual conflict detectors # ------------------------------------------------------------------ # def _detect_face_count_mismatch(self, results: Dict[str, ProviderResult]) -> List[ConflictReport]: detector_counts: Dict[str, int] = {} for r in results.values(): if r.success and r.capability.value == "detection": detector_counts[r.provider] = r.normalized.get("num_faces", 0) if len(detector_counts) < 2: return [] counts = list(detector_counts.values()) if max(counts) == min(counts): return [] return [ConflictReport( kind="face_count_mismatch", providers=list(detector_counts.keys()), description=( f"Detectors disagree on face count: {detector_counts}" ), severity="warning", )] def _detect_match_disagreement(self, matches: List[_MatchLike]) -> List[ConflictReport]: by_face: Dict[int, List[_MatchLike]] = {} for m in matches: by_face.setdefault(m.query_face_index, []).append(m) out: List[ConflictReport] = [] for face_idx, face_matches in by_face.items(): if len(face_matches) < 2: continue best_matches = {m.best_match for m in face_matches if m.best_match} if len(best_matches) > 1: out.append(ConflictReport( kind="match_disagreement", providers=[m.recognizer for m in face_matches], description=( f"Recognizers disagree on best match for face #{face_idx}: " f"{[(m.recognizer, m.best_match) for m in face_matches]}" ), severity="warning", )) return out def _detect_quality_disagreement(self, results: Dict[str, ProviderResult]) -> List[ConflictReport]: """Flag if image-quality providers disagree on quality by > 0.3.""" quality_scores: Dict[str, float] = {} for r in results.values(): if r.success and r.capability.value == "image_analysis": qs = r.normalized.get("quality_score") if qs is not None: quality_scores[r.provider] = float(qs) if len(quality_scores) < 2: return [] scores = list(quality_scores.values()) if max(scores) - min(scores) > 0.3: return [ConflictReport( kind="quality_disagreement", providers=list(quality_scores.keys()), description=f"Image-quality providers disagree by >0.3: {quality_scores}", severity="info", )] return [] def _detect_integrity_disagreement(self, results: Dict[str, ProviderResult]) -> List[ConflictReport]: """Flag if forensics providers disagree on integrity by > 0.3.""" integrity_scores: Dict[str, float] = {} for r in results.values(): if r.success and r.capability.value == "forensics": ii = r.normalized.get("integrity_score") if ii is not None: integrity_scores[r.provider] = float(ii) if len(integrity_scores) < 2: return [] scores = list(integrity_scores.values()) if max(scores) - min(scores) > 0.3: return [ConflictReport( kind="integrity_disagreement", providers=list(integrity_scores.keys()), description=f"Forensics providers disagree on integrity: {integrity_scores}", severity="warning", )] return [] def _detect_format_mismatch(self, results: Dict[str, ProviderResult]) -> List[ConflictReport]: """Flag if metadata providers disagree on image format.""" formats: Dict[str, str] = {} for r in results.values(): if r.success and r.capability.value == "metadata": fmt = r.normalized.get("format") if fmt: formats[r.provider] = fmt if len(formats) < 2: return [] unique_formats = set(formats.values()) if len(unique_formats) > 1: return [ConflictReport( kind="format_mismatch", providers=list(formats.keys()), description=f"Metadata providers disagree on format: {formats}", severity="info", )] return []