""" Explainability service for VoiceAuth API. Generates human-readable explanations for voice detection results. """ import random from typing import Literal from app.models.enums import Classification from app.utils.constants import AI_INDICATORS from app.utils.constants import CONFIDENCE_DESCRIPTORS from app.utils.constants import CONFIDENCE_THRESHOLD_HIGH from app.utils.constants import CONFIDENCE_THRESHOLD_LOW from app.utils.constants import CONFIDENCE_THRESHOLD_MEDIUM from app.utils.constants import HUMAN_INDICATORS from app.utils.logger import get_logger logger = get_logger(__name__) class ExplainabilityService: """ Service for generating explanations for voice detection results. Provides human-readable explanations based on classification and confidence levels. """ def __init__(self) -> None: """Initialize ExplainabilityService.""" self.ai_indicators = AI_INDICATORS.copy() self.human_indicators = HUMAN_INDICATORS.copy() self.confidence_descriptors = CONFIDENCE_DESCRIPTORS.copy() def get_confidence_level( self, confidence: float ) -> Literal["very_high", "high", "medium", "low"]: """ Map confidence score to a descriptive level. Args: confidence: Confidence score between 0.0 and 1.0 Returns: Confidence level string """ if confidence >= CONFIDENCE_THRESHOLD_HIGH: return "very_high" elif confidence >= CONFIDENCE_THRESHOLD_MEDIUM: return "high" elif confidence >= CONFIDENCE_THRESHOLD_LOW: return "medium" else: return "low" def select_indicators( self, classification: Classification, count: int = 2, ) -> list[str]: """ Select random indicators based on classification. Args: classification: AI_GENERATED or HUMAN count: Number of indicators to select Returns: List of selected indicators """ if classification == Classification.AI_GENERATED: indicators = self.ai_indicators else: indicators = self.human_indicators # Select random indicators (with shuffle for variety) selected = random.sample(indicators, min(count, len(indicators))) return selected def format_explanation( self, classification: Classification, confidence: float, indicators: list[str] | None = None, ) -> str: """ Format a complete explanation string. Args: classification: Classification result confidence: Confidence score indicators: Optional list of indicators (will be generated if not provided) Returns: Formatted explanation string """ # Get confidence level and descriptor confidence_level = self.get_confidence_level(confidence) descriptor = self.confidence_descriptors.get(confidence_level, "Indicators of") # Select indicators if not provided if indicators is None: indicators = self.select_indicators(classification, count=2) # Join indicators naturally if len(indicators) == 1: indicator_text = indicators[0] elif len(indicators) == 2: indicator_text = f"{indicators[0]} and {indicators[1]}" else: indicator_text = ", ".join(indicators[:-1]) + f", and {indicators[-1]}" # Determine classification-specific suffix if classification == Classification.AI_GENERATED: suffix = "detected" else: suffix = "observed" # Build final explanation explanation = f"{descriptor} {indicator_text} {suffix}" # Ensure explanation fits within limits if len(explanation) > 195: explanation = explanation[:192] + "..." return explanation def generate_explanation( self, classification: Classification, confidence: float, audio_metadata: dict | None = None, ) -> str: """ Generate a complete explanation for the detection result. Args: classification: Classification result (AI_GENERATED or HUMAN) confidence: Confidence score (0.0 to 1.0) audio_metadata: Optional audio metadata for enhanced explanations Returns: Human-readable explanation string """ logger.debug( "Generating explanation", classification=classification.value, confidence=confidence, ) # Select number of indicators based on confidence confidence_level = self.get_confidence_level(confidence) if confidence_level in ("very_high", "high"): num_indicators = 3 elif confidence_level == "medium": num_indicators = 2 else: num_indicators = 1 indicators = self.select_indicators(classification, count=num_indicators) explanation = self.format_explanation(classification, confidence, indicators) logger.debug( "Generated explanation", explanation=explanation, num_indicators=len(indicators), ) return explanation def generate_detailed_explanation( self, classification: Classification, confidence: float, audio_metadata: dict, ) -> dict: """ Generate a detailed explanation with metrics. Args: classification: Classification result confidence: Confidence score audio_metadata: Audio metadata from processing Returns: Dictionary with explanation details """ explanation = self.generate_explanation( classification=classification, confidence=confidence, audio_metadata=audio_metadata, ) confidence_level = self.get_confidence_level(confidence) indicators = self.select_indicators(classification, count=3) return { "summary": explanation, "confidence_level": confidence_level, "indicators": indicators, "audio_metrics": { "duration": audio_metadata.get("duration_seconds"), "energy": audio_metadata.get("rms_energy"), }, }