from .features import AcousticFeatures from .schema import AudioQuality, EmotionalIntensity, EmotionalTone, NoiseSeverity, Result def _intensity(rms_std: float, ser_margin: float | None) -> EmotionalIntensity: margin = ser_margin or 0.0 if rms_std >= 0.085 or margin >= 0.5: return EmotionalIntensity.high if rms_std >= 0.03 or margin >= 0.15: return EmotionalIntensity.medium return EmotionalIntensity.low def _tone( ser_top: str | None, ser_margin: float | None, intensity: EmotionalIntensity ) -> EmotionalTone: margin = ser_margin or 0.0 if ser_top is None or margin < 0.15: return EmotionalTone.neutral if ser_top == "hap": return EmotionalTone.satisfied if ser_top == "neu": return EmotionalTone.neutral if ser_top == "sad": return EmotionalTone.distressed if intensity == EmotionalIntensity.high else EmotionalTone.upset if ser_top == "ang": if intensity == EmotionalIntensity.high: return EmotionalTone.upset if intensity == EmotionalIntensity.medium: return EmotionalTone.frustrated return EmotionalTone.neutral return EmotionalTone.neutral def _noise_severity(snr_gap_db: float | None) -> NoiseSeverity: if snr_gap_db is None: return NoiseSeverity.none if snr_gap_db >= 18: return NoiseSeverity.none if snr_gap_db >= 11: return NoiseSeverity.low if snr_gap_db >= 4: return NoiseSeverity.medium return NoiseSeverity.high def _audio_quality(clipping_ratio: float, rms_mean: float) -> AudioQuality: if clipping_ratio > 0.02 or rms_mean < 0.002: return AudioQuality.severely_impaired if clipping_ratio > 0.002 or rms_mean < 0.01: return AudioQuality.slightly_impaired return AudioQuality.clear def fuse( feat: AcousticFeatures, snr_gap_db: float | None, ser_categorical: list[dict] | None, dimensional_emotion: dict | None, noise_type: str, overlap_s: float | None, longest_gap_s: float, ) -> Result: ser_top = ser_categorical[0]["label"] if ser_categorical else None ser_margin = ( ser_categorical[0]["score"] - ser_categorical[1]["score"] if ser_categorical and len(ser_categorical) > 1 else None ) intensity = _intensity(feat.rms_std, ser_margin) tone = _tone(ser_top, ser_margin, intensity) severity = _noise_severity(snr_gap_db) present = severity != NoiseSeverity.none quality = _audio_quality(feat.clipping_ratio, feat.rms_mean) overlap_present = (overlap_s or 0.0) >= 2.0 long_silence = longest_gap_s > 15.0 confidence = 0.5 + (ser_margin or 0.0) if ser_categorical is None: confidence -= 0.15 if dimensional_emotion is None: confidence -= 0.05 confidence = max(0.35, min(0.95, confidence)) return Result( emotional_tone=tone, emotional_intensity=intensity, background_noise_present=present, background_noise_type=noise_type if present else "", background_noise_severity=severity, audio_quality=quality, speaker_overlap_present=overlap_present, long_silence_present=long_silence, confidence=round(confidence, 2), )