File size: 3,270 Bytes
c3eb7ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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),
    )