gemini_adk_rs/live/
soft_turn.rs

1//! Soft turn detection for proactive silence awareness.
2//!
3//! When `proactiveAudio` is enabled, the model may choose not to respond.
4//! No `TurnComplete` fires, so the 17-step pipeline never runs. This module
5//! detects when the user finished speaking (VAD end) but the model stayed
6//! silent, and triggers a lightweight "soft turn" to keep state updated.
7
8use std::time::{Duration, Instant};
9
10/// Default timeout before a soft turn fires after VAD end.
11pub const DEFAULT_SOFT_TURN_TIMEOUT: Duration = Duration::from_secs(2);
12
13/// Detects proactive silence — user stopped speaking but model didn't respond.
14pub struct SoftTurnDetector {
15    /// When VAD end was last observed (reset when model responds).
16    vad_ended_at: Option<Instant>,
17    /// How long to wait after VAD end before declaring a soft turn.
18    timeout: Duration,
19}
20
21impl SoftTurnDetector {
22    /// Create with a custom timeout.
23    pub fn new(timeout: Duration) -> Self {
24        Self {
25            vad_ended_at: None,
26            timeout,
27        }
28    }
29
30    /// Called when VAD end is observed.
31    pub fn on_vad_end(&mut self) {
32        self.vad_ended_at = Some(Instant::now());
33    }
34
35    /// Called when the model produces any response (text, audio, tool call).
36    /// Resets the detector — no soft turn needed.
37    pub fn on_model_response(&mut self) {
38        self.vad_ended_at = None;
39    }
40
41    /// Check if a soft turn should fire.
42    pub fn check(&self, now: Instant) -> bool {
43        self.vad_ended_at
44            .map(|t| now.duration_since(t) >= self.timeout)
45            .unwrap_or(false)
46    }
47
48    /// Reset after a soft turn fires.
49    pub fn reset(&mut self) {
50        self.vad_ended_at = None;
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn no_soft_turn_without_vad_end() {
60        let d = SoftTurnDetector::new(Duration::from_millis(100));
61        assert!(!d.check(Instant::now()));
62    }
63
64    #[test]
65    fn soft_turn_after_timeout() {
66        let mut d = SoftTurnDetector::new(Duration::from_millis(50));
67        d.on_vad_end();
68        // Immediately: no
69        assert!(!d.check(Instant::now()));
70        // After timeout
71        std::thread::sleep(Duration::from_millis(60));
72        assert!(d.check(Instant::now()));
73    }
74
75    #[test]
76    fn model_response_cancels_soft_turn() {
77        let mut d = SoftTurnDetector::new(Duration::from_millis(50));
78        d.on_vad_end();
79        d.on_model_response();
80        std::thread::sleep(Duration::from_millis(60));
81        assert!(!d.check(Instant::now()));
82    }
83
84    #[test]
85    fn reset_clears_state() {
86        let mut d = SoftTurnDetector::new(Duration::from_millis(50));
87        d.on_vad_end();
88        std::thread::sleep(Duration::from_millis(60));
89        assert!(d.check(Instant::now()));
90        d.reset();
91        assert!(!d.check(Instant::now()));
92    }
93}