Spaces:
Sleeping
Sleeping
Download app/services/score_fusion.py from Rthur2003/crowncode-backend: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/services/score_fusion.py
- Command line
-
hf download hf://spaces/Rthur2003/crowncode-backend/app/services/score_fusion.py
-
curl -L -o score_fusion.py https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/services/score_fusion.py
15.6 kB
| """ | |
| 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__) | |
| 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 | |
| # ═══════════════════════════════════════════════════════════════════════ | |
| 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 | |