Rthur2003 commited on
Commit
9afcc8e
·
1 Parent(s): 29d1ce6

feat: implement score fusion engine for AI music detection

Browse files
Files changed (1) hide show
  1. app/services/score_fusion.py +327 -0
app/services/score_fusion.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Score fusion engine for AURIS AI music detection.
3
+
4
+ Combines signals from multiple analysis sources (feature extraction,
5
+ vocal analysis, external services) into a single confidence score
6
+ with human-readable indicators.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import List, Literal, Optional
13
+
14
+ from .feature_extractor import AudioFeatures
15
+ from .vocal_analyzer import VocalFeatures
16
+ from .logging_config import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class FusionResult:
23
+ """Final fused detection result."""
24
+
25
+ is_ai_generated: bool
26
+ confidence: float # 0.51 – 0.97
27
+ decision_source: str # "auris_fusion"
28
+ model_version: str # "auris-v1-fusion"
29
+
30
+ # Per-domain scores
31
+ spectral_regularity: float
32
+ temporal_patterns: float
33
+ harmonic_structure: float
34
+ vocal_ai_score: float # 0 if no vocals
35
+
36
+ # Indicators
37
+ indicators: List[str] = field(default_factory=list)
38
+
39
+ # Raw sub-results for debugging / advanced UI
40
+ has_vocals: bool = False
41
+ analysis_mode: Literal[
42
+ "production", "preview"
43
+ ] = "production"
44
+
45
+
46
+ def fuse_scores(
47
+ features: AudioFeatures,
48
+ vocals: Optional[VocalFeatures] = None,
49
+ *,
50
+ external_music_ai: Optional[dict] = None,
51
+ external_ses_analizi: Optional[dict] = None,
52
+ ) -> FusionResult:
53
+ """
54
+ Fuse all analysis signals into a single detection result.
55
+
56
+ Priority hierarchy:
57
+ 1. External production models (music_ai, ses_analizi) if available
58
+ 2. Local feature + vocal analysis (always available)
59
+
60
+ When external models agree with local analysis, confidence is boosted.
61
+ When they disagree, confidence is moderated.
62
+
63
+ Args:
64
+ features: Audio feature extraction results.
65
+ vocals: Vocal analysis results (None if skipped).
66
+ external_music_ai: Response from MusicAI service.
67
+ external_ses_analizi: Response from SesAnalizi service.
68
+
69
+ Returns:
70
+ FusionResult with final detection decision.
71
+ """
72
+ indicators: List[str] = []
73
+
74
+ # ── Local feature scores ─────────────────────────────────────────
75
+ local_score = _compute_local_score(features, vocals)
76
+
77
+ vocal_ai_score = 0.0
78
+ if vocals and vocals.has_vocals:
79
+ vocal_ai_score = vocals.vocal_ai_score
80
+ has_vocals = vocals is not None and vocals.has_vocals
81
+
82
+ # ── External service scores ──────────────────────────────────────
83
+ ext_music_ai = _parse_external_music_ai(external_music_ai)
84
+ ext_ses = _parse_external_ses_analizi(external_ses_analizi)
85
+
86
+ # ── Fusion logic ─────────────────────────────────────────────────
87
+ confidence, decision_source, mode = _fuse(
88
+ local_score, ext_music_ai, ext_ses
89
+ )
90
+
91
+ is_ai = confidence > 0.5
92
+
93
+ # ── Build indicators ─────────────────────────────────────────────
94
+ indicators.extend(_build_indicators(
95
+ is_ai, confidence, features, vocals, ext_music_ai, ext_ses
96
+ ))
97
+
98
+ return FusionResult(
99
+ is_ai_generated=is_ai,
100
+ confidence=confidence,
101
+ decision_source=decision_source,
102
+ model_version="auris-v1-fusion",
103
+ spectral_regularity=features.spectral_regularity,
104
+ temporal_patterns=features.temporal_patterns,
105
+ harmonic_structure=features.harmonic_structure,
106
+ vocal_ai_score=vocal_ai_score,
107
+ indicators=indicators,
108
+ has_vocals=has_vocals,
109
+ analysis_mode=mode,
110
+ )
111
+
112
+
113
+ # ═══════════════════════════════════════════════════════════════════════
114
+ # PRIVATE — Local score computation
115
+ # ═══════════════════════════════════════════════════════════════════════
116
+
117
+ def _compute_local_score(
118
+ features: AudioFeatures,
119
+ vocals: Optional[VocalFeatures],
120
+ ) -> float:
121
+ """
122
+ Compute local AI detection score from feature extraction + vocals.
123
+
124
+ Without vocals: 3 feature domains equally weighted.
125
+ With vocals: vocal score gets 30% weight, features split remaining 70%.
126
+ """
127
+ feat_score = (
128
+ features.spectral_regularity * 0.35
129
+ + features.temporal_patterns * 0.35
130
+ + features.harmonic_structure * 0.30
131
+ )
132
+
133
+ if vocals and vocals.has_vocals:
134
+ # Vocals are a strong signal — give them significant weight
135
+ combined = feat_score * 0.65 + vocals.vocal_ai_score * 0.35
136
+ logger.info(
137
+ f"Local score: features={feat_score:.3f}, "
138
+ f"vocals={vocals.vocal_ai_score:.3f}, combined={combined:.3f}"
139
+ )
140
+ return combined
141
+
142
+ logger.info(f"Local score: features={feat_score:.3f} (no vocals)")
143
+ return feat_score
144
+
145
+
146
+ # ═══════════════════════════════════════════════════════════════════════
147
+ # PRIVATE — External service parsing
148
+ # ═══════════════════════════════════════════════════════════════════════
149
+
150
+ @dataclass
151
+ class _ExternalScore:
152
+ available: bool
153
+ is_ai: bool
154
+ confidence: float
155
+
156
+
157
+ def _parse_external_music_ai(data: Optional[dict]) -> Optional[_ExternalScore]:
158
+ if not data or not isinstance(data, dict):
159
+ return None
160
+ prediction = data.get("prediction")
161
+ confidence = data.get("confidence")
162
+ is_valid = (
163
+ prediction in ("AI", "Human")
164
+ and isinstance(confidence, (int, float))
165
+ )
166
+ if is_valid:
167
+ return _ExternalScore(
168
+ available=True,
169
+ is_ai=prediction == "AI",
170
+ confidence=float(confidence),
171
+ )
172
+ return None
173
+
174
+
175
+ def _parse_external_ses_analizi(data: Optional[dict]) -> Optional[_ExternalScore]:
176
+ if not data or not isinstance(data, dict):
177
+ return None
178
+ authenticity = data.get("authenticity_score")
179
+ if isinstance(authenticity, (int, float)):
180
+ return _ExternalScore(
181
+ available=True,
182
+ is_ai=float(authenticity) >= 0.5,
183
+ confidence=float(authenticity),
184
+ )
185
+ return None
186
+
187
+
188
+ # ═══════════════════════════════════════════════════════════════════════
189
+ # PRIVATE — Score fusion
190
+ # ═══════════════════════════════════════════════════════════════════════
191
+
192
+ def _fuse(
193
+ local: float,
194
+ ext_mai: Optional[_ExternalScore],
195
+ ext_ses: Optional[_ExternalScore],
196
+ ) -> tuple[float, str, str]:
197
+ """
198
+ Fuse local + external scores.
199
+
200
+ Returns: (confidence, decision_source, analysis_mode)
201
+ """
202
+ scores: list[tuple[float, float, str]] = [] # (score, weight, source)
203
+
204
+ # Local always present
205
+ scores.append((local, 0.5, "auris_local"))
206
+
207
+ if ext_mai and ext_mai.available:
208
+ scores.append((ext_mai.confidence, 0.3, "music_ai"))
209
+
210
+ if ext_ses and ext_ses.available:
211
+ scores.append((ext_ses.confidence, 0.2, "ses_analizi"))
212
+
213
+ # Normalize weights
214
+ total_weight = sum(w for _, w, _ in scores)
215
+ confidence = sum(s * (w / total_weight) for s, w, _ in scores)
216
+
217
+ # Determine primary decision source
218
+ if ext_mai and ext_mai.available:
219
+ decision_source = "auris_fusion"
220
+ mode = "production"
221
+ elif ext_ses and ext_ses.available:
222
+ decision_source = "auris_fusion"
223
+ mode = "production"
224
+ else:
225
+ decision_source = "auris_local"
226
+ mode = "production" # Still production — we have real feature analysis
227
+
228
+ # Clamp to valid range (never show 0.50 or 1.00)
229
+ confidence = max(0.51, min(0.97, confidence))
230
+ confidence = round(confidence, 4)
231
+
232
+ # Agreement bonus: if all sources agree, boost confidence toward edges
233
+ if len(scores) > 1:
234
+ all_ai = all(s > 0.5 for s, _, _ in scores)
235
+ all_human = all(s <= 0.5 for s, _, _ in scores)
236
+ if all_ai:
237
+ confidence = min(0.97, confidence * 1.05)
238
+ elif all_human:
239
+ confidence = max(0.51, confidence * 0.95)
240
+
241
+ confidence = round(max(0.51, min(0.97, confidence)), 4)
242
+
243
+ logger.info(
244
+ f"Fusion: {len(scores)} sources → confidence={confidence}, "
245
+ f"source={decision_source}, mode={mode}"
246
+ )
247
+
248
+ return confidence, decision_source, mode
249
+
250
+
251
+ # ═══════════════════════════════════════════════════════════════════════
252
+ # PRIVATE — Indicator generation
253
+ # ═══════════════════════════════════════════════════════════════════════
254
+
255
+ def _build_indicators(
256
+ is_ai: bool,
257
+ confidence: float,
258
+ features: AudioFeatures,
259
+ vocals: Optional[VocalFeatures],
260
+ ext_mai: Optional[_ExternalScore],
261
+ ext_ses: Optional[_ExternalScore],
262
+ ) -> List[str]:
263
+ """Generate comprehensive human-readable indicators."""
264
+ indicators = []
265
+
266
+ # Overall confidence indicator
267
+ if confidence > 0.85:
268
+ indicators.append(
269
+ "High confidence classification based on "
270
+ "multi-signal analysis."
271
+ )
272
+ elif confidence > 0.70:
273
+ indicators.append(
274
+ "Moderate confidence with consistent "
275
+ "feature signals."
276
+ )
277
+ else:
278
+ indicators.append(
279
+ "Lower confidence suggests borderline "
280
+ "characteristics."
281
+ )
282
+
283
+ # Feature-specific indicators
284
+ if features.spectral_regularity > 0.7:
285
+ indicators.append(
286
+ "Spectral patterns show high regularity typical of AI generation."
287
+ )
288
+ elif features.spectral_regularity < 0.3:
289
+ indicators.append(
290
+ "Spectral variation is consistent with natural human composition."
291
+ )
292
+
293
+ if features.temporal_patterns > 0.7:
294
+ indicators.append(
295
+ f"Temporal patterns are metronomically precise "
296
+ f"(tempo stability: {features.tempo_stability:.3f}s std)."
297
+ )
298
+ elif features.temporal_patterns < 0.3:
299
+ indicators.append(
300
+ "Natural timing variation detected in rhythmic patterns."
301
+ )
302
+
303
+ if features.harmonic_structure > 0.7:
304
+ indicators.append(
305
+ "Harmonic progressions follow predictable AI-typical patterns."
306
+ )
307
+
308
+ # Vocal indicators
309
+ if vocals and vocals.has_vocals:
310
+ indicators.extend(vocals.indicators)
311
+
312
+ # External source indicators
313
+ if ext_mai and ext_mai.available:
314
+ label = "AI-generated" if ext_mai.is_ai else "human-composed"
315
+ indicators.append(
316
+ f"External Music-AI detector classified as {label} "
317
+ f"({ext_mai.confidence:.1%} confidence)."
318
+ )
319
+
320
+ if ext_ses and ext_ses.available:
321
+ label = "synthetic" if ext_ses.is_ai else "authentic"
322
+ indicators.append(
323
+ f"Ses-Analizi service classified as {label} "
324
+ f"({ext_ses.confidence:.1%} confidence)."
325
+ )
326
+
327
+ return indicators