File size: 7,889 Bytes
ac9876a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8c1205
 
ac9876a
f8c1205
 
 
 
 
 
 
 
 
ac9876a
 
 
 
 
 
 
 
 
 
 
f8c1205
ac9876a
 
 
 
f8c1205
ac9876a
91c5b8e
ac9876a
 
91c5b8e
 
 
 
ac9876a
91c5b8e
f8c1205
91c5b8e
 
f8c1205
91c5b8e
 
ac9876a
 
 
 
 
f8c1205
ac9876a
 
 
 
 
 
 
 
 
f8c1205
ac9876a
f8c1205
ac9876a
 
 
 
 
f8c1205
ac9876a
 
 
 
 
 
f8c1205
ac9876a
 
 
 
 
f8c1205
ac9876a
 
 
 
f8c1205
ac9876a
 
 
 
 
 
 
f8c1205
ac9876a
 
 
 
 
 
 
 
f8c1205
 
ac9876a
 
 
 
 
 
 
 
 
 
 
 
 
f8c1205
 
 
 
 
ac9876a
 
 
 
 
 
 
 
 
f8c1205
 
ac9876a
 
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""
Scoring Engine
Advanced heuristic and logic layer for combining ML predictions with expert rules.
Implements a "Signal Detection" approach using Evidence Accumulation.
"""

import numpy as np
from typing import Dict, Tuple

class ScoringEngine:
    """
    Bio-inspired Signal Detection System.
    Accumulates evidence for "Busy" vs "Not Busy".
    """
    
    def __init__(self):
        # Evidence Weights (Log-Odds contributions)
        # Positive = Evidence for BUSY
        # Negative = Evidence for NOT BUSY
        self.WEIGHTS = {
            'explicit_busy': 6.0,      # Strongest signal
            'explicit_free': -4.0,     # Strong negative signal (if we had it)
            'traffic_noise': 3.0,      # Strong context
            'office_noise': 1.0,       # Weak context
            'rushed_speech': 1.5,      # Medium context
            'short_answers': 1.2,      # Medium context
            'deflection': 2.0,         # Medium-Strong context
            'latency': 0.5,            # Weak context
            'ml_model_factor': 0.5,     # Multiplier for ML log-odds (reduced)
            'emotion_stress': 2.5,     # Paper 1: Strong indicator of busy state
            'emotion_energy': 0.8,     # Medium indicator

        }
        
    def _sigmoid(self, x: float) -> float:
        """Convert log-odds to probability (0-1)"""
        return 1.0 / (1.0 + np.exp(-x))
    
    def _logit(self, p: float) -> float:
        """Convert probability to log-odds (-inf to +inf)"""
        p = np.clip(p, 0.01, 0.99) # Avoid inf
        return np.log(p / (1.0 - p))

    def calculate_score(
        self, 
        audio_features: Dict[str, float], 
        text_features: Dict[str, float], 
        ml_probability: float
    ) -> Tuple[float, Dict]:
        """
        Calculate Busy Score using Evidence Accumulation.
        """
        
        evidence = 0.0
        positive_evidence = 0.0
        negative_evidence = 0.0
        details = []

        def add_evidence(points: float, label: str) -> None:
            nonlocal evidence, positive_evidence, negative_evidence
            evidence += points
            if points >= 0:
                positive_evidence += points
            else:
                negative_evidence += points
            details.append(label)
        
        # Check if user explicitly invited conversation (intent overrides context)
        explicit_free = text_features.get('t0_explicit_free', 0.0)
        intent_overrides_context = explicit_free > 0.5
        
        # --- 1. Text Evidence (Intent) ---
        
        # Explicit Busy
        explicit = text_features.get('t1_explicit_busy', 0.0)
        if explicit > 0.5:
            points = self.WEIGHTS['explicit_busy'] * explicit
            add_evidence(points, f"Explicit Intent (+{points:.1f})")

        # Explicit Free (negative evidence)
        if explicit_free > 0.5:
            points = self.WEIGHTS['explicit_free'] * explicit_free
            add_evidence(points, f"Explicit Free ({points:.1f})")
            
        # Short Answers (Brevity) - only counts when there's other busy evidence
        short_ratio = text_features.get('t3_short_ratio', 0.0)
        if short_ratio > 0.3:
            deflection = text_features.get('t6_deflection', 0.0)
            time_pressure = text_features.get('t5_time_pressure', 0.0)
            busy_context = (explicit > 0.5) or (deflection > 0.1) or (time_pressure > 0.1)

            if intent_overrides_context:
                points = self.WEIGHTS['short_answers'] * short_ratio * 0.4
                add_evidence(points, f"Brief Responses (+{points:.1f}, reduced - user invited talk)")
            elif busy_context:
                points = self.WEIGHTS['short_answers'] * short_ratio
                add_evidence(points, f"Brief Responses (+{points:.1f})")
            else:
                details.append("Brief Responses (ignored - no busy evidence)")
            
        # Deflection / Time Pressure
        deflection = text_features.get('t6_deflection', 0.0)
        if deflection > 0.1:
            points = self.WEIGHTS['deflection'] * deflection
            add_evidence(points, f"Deflection (+{points:.1f})")

        # --- 2. Audio Evidence (Context) ---
        
        # Traffic Noise (reduced when user explicitly invites talk)
        traffic = audio_features.get('v2_noise_traffic', 0.0)
        if traffic > 0.5:
            points = self.WEIGHTS['traffic_noise'] * traffic
            if intent_overrides_context:
                points *= 0.3  # Strong availability signal overrides traffic context
                add_evidence(points, f"Traffic Context (+{points:.1f}, reduced - user invited talk)")
            else:
                add_evidence(points, f"Traffic Context (+{points:.1f})")
            
        # Speech Rate
        rate = audio_features.get('v3_speech_rate', 0.0)
        if rate > 3.5: # Fast speech
            points = self.WEIGHTS['rushed_speech']
            add_evidence(points, f"Rushed Speech (+{points:.1f})")
        elif rate < 1.0: # Very slow speech (might be distracted)
            pass # Neutral for now
            
        # Energy/Pitch (Stress)
        pitch_std = audio_features.get('v5_pitch_std', 0.0)
        if pitch_std > 80.0: # High variation
            add_evidence(0.5, "Voice Stress (+0.5)")

        # --- 2b. Emotion Evidence (if present) ---
        emotion_stress = audio_features.get('v11_emotion_stress', 0.0)
        if emotion_stress > 0.6:
            points = self.WEIGHTS['emotion_stress'] * emotion_stress
            add_evidence(points, f"Emotional Stress (+{points:.1f})")

        emotion_energy = audio_features.get('v12_emotion_energy', 0.0)
        if emotion_energy > 0.7:
            points = self.WEIGHTS['emotion_energy'] * emotion_energy
            add_evidence(points, f"High Energy (+{points:.1f})")

        # --- 3. Machine Learning Evidence (Baseline) ---
        
        # Convert Model Probability to Log-Odds Evidence
        ml_evidence = self._logit(ml_probability)
        weighted_ml_evidence = ml_evidence * self.WEIGHTS['ml_model_factor']
        
        add_evidence(weighted_ml_evidence, f"ML Baseline ({weighted_ml_evidence:+.1f})")
        
        # --- 4. Final Calculation ---... more value ot the voice features, especially the emotional ones
        
        # Sigmoid converts total evidence back to 0-1 probability
        final_score = self._sigmoid(evidence)
        
        breakdown = {
            'total_evidence': evidence,
            'positive_evidence': positive_evidence,
            'negative_evidence': negative_evidence,
            'details': details,
            'ml_contribution': weighted_ml_evidence
        }
        
        return final_score, breakdown


    def get_confidence(self, score: float, breakdown: Dict) -> float:
        """
        Calculate confidence based on EVIDENCE MAGNITUDE.
        Strong evidence (positive or negative) = High Confidence.
        Zero evidence = Low Confidence.
        """
        positive_evidence = breakdown.get('positive_evidence', 0.0)
        negative_evidence = abs(breakdown.get('negative_evidence', 0.0))
        total_strength = positive_evidence + negative_evidence
        conflict = min(positive_evidence, negative_evidence)
        conflict_ratio = conflict / (total_strength + 1e-6)
        
        # We model confidence as a sigmoid of absolute evidence
        # |evidence| = 0  -> Confidence = 0.5 (Unsure) ?? No, 0.0 (Total Guess)
        # But standard sigmoid(0) is 0.5.
        # We want 0->0, values->1.
        
        # Use a scaling factor. Evidence of > 3.0 is very strong.
        # tanh is good: tanh(0)=0, tanh(3)≈0.995
        
        base_confidence = np.tanh(total_strength / 2.0)
        confidence = base_confidence * (1.0 - conflict_ratio)
        
        return float(confidence)