File size: 8,271 Bytes
127b976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

from typing import Any, Dict, List

from .evidence import build_evidence_packet
from .labels import ID_TO_EXPLANATION


def decision_from_probability(

    fake_probability: float,

    real_threshold: float = 0.35,

    fake_threshold: float = 0.85,

    suspicious_segments: List[Dict[str, float]] | None = None,

    suspicious_window_threshold: float = 0.75,

) -> str:
    """Safer decision gate for real-world speech.



    Old/simple logic: fake_prob >= 0.65 => likely fake.

    New logic: fake_prob must be very high, otherwise suspicious/human review.



    This reduces false accusations on real accented/interview/noisy speech.

    """
    suspicious_segments = suspicious_segments or []

    if fake_probability < real_threshold:
        return "likely real"

    # If probability is not very high, avoid a hard fake decision.
    if fake_probability < fake_threshold:
        return "suspicious / human review"

    # If very high probability, still check if there is some consistent evidence.
    strong_windows = [
        s for s in suspicious_segments
        if float(s.get("fake_score", 0.0)) >= suspicious_window_threshold
    ]

    # If no localization evidence is available, allow likely fake based on high utterance score.
    if not suspicious_segments:
        return "likely fake"

    # If localization evidence exists and at least one strong window agrees, likely fake.
    if strong_windows:
        return "likely fake"

    # High utterance score but weak/unclear window evidence: safer to review.
    return "suspicious / human review"


def beginner_friendly_explanation(

    fake_prob: float,

    attack_type: str,

    suspicious_segments: List[Dict[str, float]] | None = None,

) -> str:
    suspicious_segments = suspicious_segments or []

    if fake_prob < 0.35:
        return (
            "The model thinks this audio is likely real because the fake score is low. "
            "The speech patterns look closer to real human speech examples than to synthetic or manipulated speech."
        )

    if fake_prob < 0.85:
        return (
            "The model is not fully sure. The fake score is not low, but it is also not safe enough to call the audio fake. "
            "This should be treated as suspicious and reviewed by a human, especially if the clip contains an accent, interview speech, noise, or recording-quality differences."
        )

    if attack_type == "tts_vc":
        reason = (
            "The model thinks this audio may be fake because it contains patterns similar to text-to-speech "
            "or voice-conversion examples from training."
        )
    elif attack_type == "codec":
        reason = (
            "The model thinks this audio may be fake because it contains patterns often linked to codec-based generated speech."
        )
    elif attack_type == "partial":
        reason = (
            "The model thinks only part of the audio may be manipulated because suspicious evidence appears in specific time regions."
        )
    elif attack_type == "bonafide":
        reason = (
            "The fake score is high, but the attack-type head predicts bonafide speech. "
            "This conflict means the safest decision is human review rather than automatic accusation."
        )
    else:
        reason = "The model found strong fake-like patterns in the audio."

    if suspicious_segments:
        top = max(suspicious_segments, key=lambda x: float(x.get("fake_score", 0.0)))
        time_reason = (
            f" The strongest suspicious part is around {float(top['start']):.1f}s to {float(top['end']):.1f}s, "
            f"with a fake score of {float(top['fake_score']):.2f}."
        )
    else:
        time_reason = " The evidence appears to affect the whole clip rather than one clear small region."

    return reason + time_reason


def explanation_text(

    fake_probability: float,

    attack_type: str,

    explanation_id: int,

    suspicious_segments: List[Dict[str, float]] | None = None,

) -> List[str]:
    suspicious_segments = suspicious_segments or []
    explanation_name = ID_TO_EXPLANATION.get(int(explanation_id), "unknown")

    reasons: List[str] = []

    if fake_probability >= 0.85:
        reasons.append("The shared AASIST-based representation produced very strong utterance-level fake evidence.")
    elif fake_probability >= 0.65:
        reasons.append("The shared AASIST-based representation produced moderate-to-strong fake evidence, but this is kept in the human-review range for real-world speech.")
    elif fake_probability >= 0.35:
        reasons.append("The fake probability is in the uncertain range, so the audio should be treated as suspicious rather than automatically fake.")
    else:
        reasons.append("The full-audio fake probability is low, so no strong utterance-level fake evidence was detected.")

    if attack_type == "codec":
        reasons.append("The attack-type head predicts a codec-based fake category; this is treated as one clue, not standalone codec source tracing.")
    elif attack_type == "partial":
        reasons.append("The attack-type head predicts localized or partially manipulated speech.")
    elif attack_type == "tts_vc":
        reasons.append("The attack-type head predicts a text-to-speech or voice-conversion style spoof.")
    elif attack_type == "bonafide":
        reasons.append("The attack-type head predicts bonafide speech.")

    if suspicious_segments:
        best = max(suspicious_segments, key=lambda x: float(x.get("fake_score", 0.0)))
        reasons.append(
            f"Sliding-window inference found suspicious evidence, with the strongest region at "
            f"{best['start']}s–{best['end']}s and window fake score {float(best['fake_score']):.2f}."
        )
    else:
        reasons.append("Sliding-window inference did not find a window above the suspicious threshold.")

    if explanation_name == "codec_based_artifact":
        reasons.append("The explanation head groups the evidence as codec-related artifacts.")
    elif explanation_name == "localized_partial_manipulation":
        reasons.append("The explanation head groups the evidence as localized partial manipulation.")
    elif explanation_name == "global_synthetic_artifact":
        reasons.append("The explanation head groups the evidence as global synthetic speech artifacts.")
    elif explanation_name == "no_strong_fake_evidence":
        reasons.append("The explanation head predicts the no-strong-fake-evidence category.")

    return reasons


def build_report(

    fake_probability: float,

    attack_type: str,

    explanation_id: int,

    suspicious_segments: List[Dict[str, float]],

    real_threshold: float = 0.35,

    fake_threshold: float = 0.85,

    suspicious_window_threshold: float = 0.75,

) -> Dict[str, Any]:
    decision = decision_from_probability(
        fake_probability,
        real_threshold=real_threshold,
        fake_threshold=fake_threshold,
        suspicious_segments=suspicious_segments,
        suspicious_window_threshold=suspicious_window_threshold,
    )
    explanation_category = ID_TO_EXPLANATION.get(int(explanation_id), "unknown")

    evidence = build_evidence_packet(
        fake_probability=fake_probability,
        attack_type=attack_type,
        explanation_category=explanation_category,
        suspicious_segments=suspicious_segments,
        thresholds={
            "real": real_threshold,
            "fake": fake_threshold,
            "suspicious_window": suspicious_window_threshold,
        },
    )

    return {
        "decision": decision,
        "fake_probability": round(float(fake_probability), 4),
        "attack_type": attack_type,
        "suspicious_segments": suspicious_segments,
        "explanation_category": explanation_category,
        "explanation": explanation_text(fake_probability, attack_type, explanation_id, suspicious_segments),
        "beginner_explanation": beginner_friendly_explanation(fake_probability, attack_type, suspicious_segments),
        "evidence": evidence,
    }