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"),
            },
        }