| 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), |
| ) |
|
|