File size: 6,683 Bytes
6b408d7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """
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"),
},
}
|