| | """
|
| | 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
|
| |
|
| |
|
| | 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
|
| | """
|
| |
|
| | confidence_level = self.get_confidence_level(confidence)
|
| | descriptor = self.confidence_descriptors.get(confidence_level, "Indicators of")
|
| |
|
| |
|
| | if indicators is None:
|
| | indicators = self.select_indicators(classification, count=2)
|
| |
|
| |
|
| | 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]}"
|
| |
|
| |
|
| | if classification == Classification.AI_GENERATED:
|
| | suffix = "detected"
|
| | else:
|
| | suffix = "observed"
|
| |
|
| |
|
| | explanation = f"{descriptor} {indicator_text} {suffix}"
|
| |
|
| |
|
| | 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,
|
| | )
|
| |
|
| |
|
| | 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"),
|
| | },
|
| | }
|
| |
|