gemini_genai_rs/turn/
turn_detection.rs

1//! Client-side turn detection — complements Gemini's server-side VAD.
2//!
3//! Provides configurable end-of-speech detection to signal `activityEnd`,
4//! allowing the server to start model generation faster.
5
6use std::time::{Duration, Instant};
7
8/// Configuration for client-side turn detection.
9#[derive(Debug, Clone)]
10pub struct TurnDetectionConfig {
11    /// Delay after speech ends before signaling end-of-turn (ms).
12    pub end_of_speech_delay_ms: u64,
13    /// Whether client-side turn detection is enabled.
14    /// When disabled, we rely entirely on server-side VAD.
15    pub enabled: bool,
16}
17
18impl Default for TurnDetectionConfig {
19    fn default() -> Self {
20        Self {
21            end_of_speech_delay_ms: 300,
22            enabled: true,
23        }
24    }
25}
26
27/// Events from the turn detector.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TurnDetectionEvent {
30    /// User started speaking.
31    SpeechStarted,
32    /// User finished speaking (end-of-speech delay elapsed).
33    TurnEnded,
34}
35
36/// Client-side turn detector.
37pub struct TurnDetector {
38    config: TurnDetectionConfig,
39    /// Whether the user is currently speaking.
40    is_speaking: bool,
41    /// When speech last ended (VAD transitioned to silence).
42    speech_ended_at: Option<Instant>,
43    /// Whether we've already emitted TurnEnded for this speech segment.
44    turn_ended_emitted: bool,
45}
46
47impl TurnDetector {
48    /// Create a new turn detector.
49    pub fn new(config: TurnDetectionConfig) -> Self {
50        Self {
51            config,
52            is_speaking: false,
53            speech_ended_at: None,
54            turn_ended_emitted: false,
55        }
56    }
57
58    /// Update with the current VAD state.
59    ///
60    /// Returns a `TurnDetectionEvent` if a transition occurred.
61    pub fn update(&mut self, vad_is_speaking: bool) -> Option<TurnDetectionEvent> {
62        if !self.config.enabled {
63            return None;
64        }
65
66        if vad_is_speaking && !self.is_speaking {
67            // Speech started
68            self.is_speaking = true;
69            self.speech_ended_at = None;
70            self.turn_ended_emitted = false;
71            return Some(TurnDetectionEvent::SpeechStarted);
72        }
73
74        if !vad_is_speaking && self.is_speaking {
75            // Speech just ended — start the delay timer
76            self.is_speaking = false;
77            self.speech_ended_at = Some(Instant::now());
78        }
79
80        // Check if end-of-speech delay has elapsed
81        if let Some(ended_at) = self.speech_ended_at
82            && !self.turn_ended_emitted
83            && ended_at.elapsed() >= Duration::from_millis(self.config.end_of_speech_delay_ms)
84        {
85            self.turn_ended_emitted = true;
86            self.speech_ended_at = None;
87            return Some(TurnDetectionEvent::TurnEnded);
88        }
89
90        None
91    }
92
93    /// Whether speech is currently in progress.
94    pub fn is_speaking(&self) -> bool {
95        self.is_speaking
96    }
97
98    /// Whether we're waiting for the end-of-speech delay.
99    pub fn is_pending_turn_end(&self) -> bool {
100        self.speech_ended_at.is_some() && !self.turn_ended_emitted
101    }
102
103    /// Reset the detector state.
104    pub fn reset(&mut self) {
105        self.is_speaking = false;
106        self.speech_ended_at = None;
107        self.turn_ended_emitted = false;
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use std::thread;
115
116    #[test]
117    fn speech_start_detected() {
118        let mut detector = TurnDetector::new(TurnDetectionConfig::default());
119
120        let event = detector.update(true);
121        assert_eq!(event, Some(TurnDetectionEvent::SpeechStarted));
122        assert!(detector.is_speaking());
123    }
124
125    #[test]
126    fn turn_end_after_delay() {
127        let mut detector = TurnDetector::new(TurnDetectionConfig {
128            end_of_speech_delay_ms: 50,
129            enabled: true,
130        });
131
132        // Start speaking
133        detector.update(true);
134
135        // Stop speaking
136        detector.update(false);
137        assert!(detector.is_pending_turn_end());
138
139        // Not enough time yet
140        let event = detector.update(false);
141        assert!(event.is_none() || matches!(event, Some(TurnDetectionEvent::TurnEnded)));
142
143        // Wait for delay
144        thread::sleep(Duration::from_millis(60));
145        let event = detector.update(false);
146        assert_eq!(event, Some(TurnDetectionEvent::TurnEnded));
147    }
148
149    #[test]
150    fn speech_resume_cancels_turn_end() {
151        let mut detector = TurnDetector::new(TurnDetectionConfig {
152            end_of_speech_delay_ms: 200,
153            enabled: true,
154        });
155
156        // Start and stop speaking
157        detector.update(true);
158        detector.update(false);
159        assert!(detector.is_pending_turn_end());
160
161        // Resume speaking before delay elapses
162        let event = detector.update(true);
163        assert_eq!(event, Some(TurnDetectionEvent::SpeechStarted));
164        assert!(!detector.is_pending_turn_end());
165    }
166
167    #[test]
168    fn disabled_detector_emits_nothing() {
169        let mut detector = TurnDetector::new(TurnDetectionConfig {
170            enabled: false,
171            ..Default::default()
172        });
173
174        assert!(detector.update(true).is_none());
175        assert!(detector.update(false).is_none());
176    }
177}