""" Score fusion engine for AURIS AI music detection. Combines signals from multiple analysis sources (feature extraction, vocal analysis, external services) into a single confidence score with human-readable indicators. """ from __future__ import annotations from dataclasses import dataclass, field from typing import List, Literal, Optional from .feature_extractor import AudioFeatures from .vocal_analyzer import VocalFeatures from .clap_detector import CLAPResult from .fst_client import FSTResult from .wav2vec2_detector import Wav2Vec2Result from .logging_config import get_logger logger = get_logger(__name__) @dataclass class FusionResult: """Final fused detection result.""" is_ai_generated: bool confidence: float # 0.03 – 0.97 decision_source: str # "auris_fusion" model_version: str # "auris-v1-fusion" # Per-domain scores spectral_regularity: float temporal_patterns: float harmonic_structure: float vocal_ai_score: float # 0 if no vocals # Indicators indicators: List[str] = field(default_factory=list) # Raw sub-results for debugging / advanced UI has_vocals: bool = False analysis_mode: Literal[ "production", "preview" ] = "production" def fuse_scores( features: AudioFeatures, vocals: Optional[VocalFeatures] = None, *, clap_result: Optional[CLAPResult] = None, fst_result: Optional[FSTResult] = None, wav2vec2_result: Optional[Wav2Vec2Result] = None, external_music_ai: Optional[dict] = None, external_ses_analizi: Optional[dict] = None, ) -> FusionResult: """ Fuse all analysis signals into a single detection result. 3-Layer architecture: Layer 1: Local feature + vocal analysis (always available) Layer 2: CLAP embedding classifier (optional) Layer 3: FST external API (optional) Legacy: music_ai / ses_analizi (optional) When multiple layers agree, confidence is boosted. When they disagree, confidence is moderated. Args: features: Audio feature extraction results. vocals: Vocal analysis results (None if skipped). clap_result: CLAP detector result (Layer 2). fst_result: FST external result (Layer 3). external_music_ai: Response from MusicAI service. external_ses_analizi: Response from SesAnalizi service. Returns: FusionResult with final detection decision. """ indicators: List[str] = [] # ── Local feature scores (Layer 1) ─────────────────────────────── local_score = _compute_local_score(features, vocals) vocal_ai_score = 0.0 if vocals and vocals.has_vocals: vocal_ai_score = vocals.vocal_ai_score has_vocals = vocals is not None and vocals.has_vocals # ── wav2vec2 score (Tower 1) ───────────────────────────────────── w2v_score: Optional[float] = None if wav2vec2_result and wav2vec2_result.available: w2v_score = wav2vec2_result.p_ai # ── CLAP score (Layer 2) ───────────────────────────────────────── clap_ext = _parse_clap_result(clap_result) # ── FST score (Layer 3) ────────────────────────────────────────── fst_ext = _parse_fst_result(fst_result) # ── Legacy external service scores ─────────────────────────────── ext_music_ai = _parse_external_music_ai(external_music_ai) ext_ses = _parse_external_ses_analizi(external_ses_analizi) # ── Fusion logic ───────────────────────────────────────────────── confidence, decision_source, mode = _fuse( local_score, clap_ext, fst_ext, ext_music_ai, ext_ses, wav2vec2_score=w2v_score, ) is_ai = confidence > 0.5 # ── Build indicators ───────────────────────────────────────────── indicators.extend(_build_indicators( is_ai, confidence, features, vocals, clap_ext, fst_ext, ext_music_ai, ext_ses, )) return FusionResult( is_ai_generated=is_ai, confidence=confidence, decision_source=decision_source, model_version="auris-v1-fusion", spectral_regularity=features.spectral_regularity, temporal_patterns=features.temporal_patterns, harmonic_structure=features.harmonic_structure, vocal_ai_score=vocal_ai_score, indicators=indicators, has_vocals=has_vocals, analysis_mode=mode, ) # ═══════════════════════════════════════════════════════════════════════ # PRIVATE — Local score computation # ═══════════════════════════════════════════════════════════════════════ def _compute_local_score( features: AudioFeatures, vocals: Optional[VocalFeatures], ) -> float: """ Compute local AI detection score from feature extraction + vocals. Without vocals: 3 feature domains equally weighted. With vocals: vocal score gets 30% weight, features split remaining 70%. """ feat_score = ( features.spectral_regularity * 0.35 + features.temporal_patterns * 0.35 + features.harmonic_structure * 0.30 ) if vocals and vocals.has_vocals: # Vocals are a strong signal — give them significant weight combined = feat_score * 0.65 + vocals.vocal_ai_score * 0.35 logger.info( f"Local score: features={feat_score:.3f}, " f"vocals={vocals.vocal_ai_score:.3f}, combined={combined:.3f}" ) return combined logger.info(f"Local score: features={feat_score:.3f} (no vocals)") return feat_score # ═══════════════════════════════════════════════════════════════════════ # PRIVATE — External service parsing # ═══════════════════════════════════════════════════════════════════════ @dataclass class _ExternalScore: available: bool is_ai: bool confidence: float classifier_used: str = "unknown" def _parse_external_music_ai(data: Optional[dict]) -> Optional[_ExternalScore]: if not data or not isinstance(data, dict): return None prediction = data.get("prediction") confidence = data.get("confidence") is_valid = ( prediction in ("AI", "Human") and isinstance(confidence, (int, float)) ) if is_valid: return _ExternalScore( available=True, is_ai=prediction == "AI", confidence=float(confidence), ) return None def _parse_external_ses_analizi(data: Optional[dict]) -> Optional[_ExternalScore]: if not data or not isinstance(data, dict): return None authenticity = data.get("authenticity_score") if isinstance(authenticity, (int, float)): return _ExternalScore( available=True, is_ai=float(authenticity) >= 0.5, confidence=float(authenticity), ) return None def _parse_clap_result( result: Optional[CLAPResult], ) -> Optional[_ExternalScore]: """Convert CLAPResult to internal score format.""" if not result or not result.available: return None return _ExternalScore( available=True, is_ai=result.is_ai, confidence=result.confidence, classifier_used=getattr(result, "classifier_used", "unknown"), ) def _parse_fst_result( result: Optional[FSTResult], ) -> Optional[_ExternalScore]: """Convert FSTResult to internal score format.""" if not result or not result.available: return None return _ExternalScore( available=True, is_ai=result.is_ai, confidence=result.confidence, ) # ═══════════════════════════════════════════════════════════════════════ # PRIVATE — Score fusion # ═══════════════════════════════════════════════════════════════════════ def _fuse( local: float, clap: Optional[_ExternalScore], fst: Optional[_ExternalScore], ext_mai: Optional[_ExternalScore], ext_ses: Optional[_ExternalScore], wav2vec2_score: Optional[float] = None, ) -> tuple[float, str, str]: """ Fuse local + wav2vec2 + CLAP + FST + legacy external scores. Weight allocation (normalized when layers are missing): Tower 0 (wav2vec2 transformer): 0.30 Layer 1 (local features+vocals): 0.30 Layer 2 (CLAP embedding): 0.15 Layer 3 (FST external): 0.20 Legacy (music_ai): 0.04 Legacy (ses_analizi): 0.01 Returns: (confidence, decision_source, analysis_mode) """ scores: list[tuple[float, float, str]] = [] # Tower 0 — wav2vec2 transformer (highest accuracy when available) if wav2vec2_score is not None: scores.append((wav2vec2_score, 0.30, "wav2vec2")) # Layer 1 — always present scores.append((local, 0.30, "auris_local")) # Layer 2 — CLAP embeddings (reduced weight if heuristic fallback) if clap and clap.available: clap_weight = 0.15 if clap.classifier_used == "clap_embedding" else 0.06 scores.append((clap.confidence, clap_weight, "clap")) # Layer 3 — FST external if fst and fst.available: scores.append((fst.confidence, 0.20, "fst")) # Legacy external services if ext_mai and ext_mai.available: scores.append((ext_mai.confidence, 0.04, "music_ai")) if ext_ses and ext_ses.available: scores.append((ext_ses.confidence, 0.01, "ses_analizi")) # Normalize weights total_weight = sum(w for _, w, _ in scores) confidence = sum(s * (w / total_weight) for s, w, _ in scores) # Determine decision source based on available layers n_layers = sum(1 for _, _, src in scores if src != "auris_local") if n_layers >= 2: decision_source = "auris_fusion" elif n_layers == 1: # Name includes the single extra layer extra = [src for _, _, src in scores if src != "auris_local"][0] decision_source = f"auris_fusion_{extra}" else: decision_source = "auris_local" mode: str = "production" # Agreement bonus/dampen if len(scores) > 1: all_ai = all(s > 0.5 for s, _, _ in scores) all_human = all(s <= 0.5 for s, _, _ in scores) if all_ai: boost = 1.0 + 0.02 * len(scores) confidence = min(0.97, confidence * boost) elif all_human: dampen = 1.0 - 0.02 * len(scores) confidence = max(0.03, confidence * dampen) confidence = round(max(0.03, min(0.97, confidence)), 4) sources_str = ", ".join(f"{src}={s:.3f}" for s, _, src in scores) logger.info( f"Fusion: {len(scores)} sources [{sources_str}] " f"-> confidence={confidence}, source={decision_source}" ) return confidence, decision_source, mode # ═══════════════════════════════════════════════════════════════════════ # PRIVATE — Indicator generation # ═══════════════════════════════════════════════════════════════════════ def _build_indicators( is_ai: bool, confidence: float, features: AudioFeatures, vocals: Optional[VocalFeatures], clap: Optional[_ExternalScore], fst: Optional[_ExternalScore], ext_mai: Optional[_ExternalScore], ext_ses: Optional[_ExternalScore], ) -> List[str]: """Generate comprehensive human-readable indicators.""" indicators = [] # Overall confidence indicator if confidence > 0.85: indicators.append( "High confidence classification based on " "multi-signal analysis." ) elif confidence > 0.70: indicators.append( "Moderate confidence with consistent " "feature signals." ) else: indicators.append( "Lower confidence suggests borderline " "characteristics." ) # Feature-specific indicators if features.spectral_regularity > 0.7: indicators.append( "Spectral patterns show high regularity " "typical of AI generation." ) elif features.spectral_regularity < 0.3: indicators.append( "Spectral variation is consistent with " "natural human composition." ) if features.temporal_patterns > 0.7: indicators.append( f"Temporal patterns are metronomically precise " f"(tempo stability: " f"{features.tempo_stability:.3f}s std)." ) elif features.temporal_patterns < 0.3: indicators.append( "Natural timing variation detected in " "rhythmic patterns." ) if features.harmonic_structure > 0.7: indicators.append( "Harmonic progressions follow predictable " "AI-typical patterns." ) # Vocal indicators if vocals and vocals.has_vocals: indicators.extend(vocals.indicators) # CLAP embedding indicator (Layer 2) if clap and clap.available: label = "AI-generated" if clap.is_ai else "human-composed" indicators.append( f"CLAP embedding analysis classified as {label} " f"({clap.confidence:.1%} confidence)." ) # FST indicator (Layer 3) if fst and fst.available: label = "AI-generated" if fst.is_ai else "human-composed" indicators.append( f"FST (Fusion Segment Transformer) classified as " f"{label} ({fst.confidence:.1%} confidence)." ) # Legacy external source indicators if ext_mai and ext_mai.available: label = "AI-generated" if ext_mai.is_ai else "human-composed" indicators.append( f"External Music-AI detector classified as " f"{label} ({ext_mai.confidence:.1%} confidence)." ) if ext_ses and ext_ses.available: label = "synthetic" if ext_ses.is_ai else "authentic" indicators.append( f"Ses-Analizi service classified as {label} " f"({ext_ses.confidence:.1%} confidence)." ) return indicators