gemini_adk_fluent_rs/telephony/
bridge.rs1use std::sync::Arc;
15use std::time::Duration;
16
17use tokio::sync::{broadcast, mpsc};
18use tokio::task::JoinHandle;
19
20use gemini_adk_rs::State;
21use gemini_adk_rs::live::{LiveEvent, LiveHandle};
22
23use crate::voice::Playback;
24
25pub const KEY_DTMF: &str = "telephony:dtmf";
33pub const KEY_DTMF_HISTORY: &str = "telephony:dtmf_history";
35pub const KEY_CALL_SID: &str = "telephony:call_sid";
37pub const KEY_STREAM_SID: &str = "telephony:stream_sid";
39pub const KEY_CALLER: &str = "telephony:caller";
42
43pub fn record_dtmf(state: &State, digit: char) {
48 let _ = state.set(KEY_DTMF, digit.to_string());
49 let _ = state.modify(KEY_DTMF_HISTORY, String::new(), |mut history| {
50 history.push(digit);
51 history
52 });
53}
54
55#[derive(Debug, Default)]
62pub struct DtmfDeduper {
63 last_end_timestamp: Option<u32>,
64}
65
66impl DtmfDeduper {
67 pub fn accept(&mut self, end: bool, rtp_timestamp: u32) -> bool {
70 if !end {
71 return false;
72 }
73 if self.last_end_timestamp == Some(rtp_timestamp) {
74 return false;
75 }
76 self.last_end_timestamp = Some(rtp_timestamp);
77 true
78 }
79}
80
81#[derive(Clone)]
85pub struct FillerConfig {
86 pub clip: Arc<Vec<i16>>,
90 pub delay: Duration,
94 pub min_interval: Duration,
97}
98
99impl FillerConfig {
100 pub fn new(clip: Vec<i16>) -> Self {
103 Self {
104 clip: Arc::new(clip),
105 delay: Duration::from_secs(2),
106 min_interval: Duration::from_secs(10),
107 }
108 }
109
110 pub fn delay(mut self, delay: Duration) -> Self {
112 self.delay = delay;
113 self
114 }
115
116 pub fn min_interval(mut self, interval: Duration) -> Self {
118 self.min_interval = interval;
119 self
120 }
121}
122
123pub fn spawn_latency_filler(
138 handle: &LiveHandle,
139 speaker: mpsc::Sender<Playback>,
140 config: FillerConfig,
141) -> JoinHandle<()> {
142 let events = handle.events();
143 tokio::spawn(filler_task(events, speaker, config))
144}
145
146pub(crate) async fn filler_task(
149 mut events: broadcast::Receiver<LiveEvent>,
150 speaker: mpsc::Sender<Playback>,
151 config: FillerConfig,
152) {
153 let mut armed_at: Option<tokio::time::Instant> = None;
154 let mut last_filler: Option<tokio::time::Instant> = None;
155 loop {
156 let deadline = armed_at.map(|at| at + config.delay);
157 tokio::select! {
158 event = events.recv() => match event {
159 Ok(LiveEvent::VadEnd) => armed_at = Some(tokio::time::Instant::now()),
160 Ok(LiveEvent::Audio(_)) | Ok(LiveEvent::VadStart) | Ok(LiveEvent::Interrupted) => {
162 armed_at = None;
163 }
164 Ok(_) => {}
165 Err(broadcast::error::RecvError::Lagged(_)) => continue,
166 Err(broadcast::error::RecvError::Closed) => break,
167 },
168 () = async {
169 match deadline {
170 Some(deadline) => tokio::time::sleep_until(deadline).await,
171 None => std::future::pending().await,
172 }
173 } => {
174 armed_at = None;
175 let recently = last_filler
176 .is_some_and(|at| at.elapsed() < config.min_interval);
177 if !recently {
178 last_filler = Some(tokio::time::Instant::now());
179 let _ = speaker
180 .send(Playback::Chunk(config.clip.as_ref().clone()))
181 .await;
182 }
183 }
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn dtmf_dedup_accepts_one_end_per_timestamp() {
194 let mut dedup = DtmfDeduper::default();
195 assert!(!dedup.accept(false, 100), "non-end packets never emit");
196 assert!(dedup.accept(true, 100), "first end emits");
197 assert!(!dedup.accept(true, 100), "retransmitted end is dropped");
198 assert!(!dedup.accept(true, 100));
199 assert!(dedup.accept(true, 900), "next keypress emits again");
200 }
201
202 #[test]
203 fn record_dtmf_writes_the_shared_keys() {
204 let state = State::new();
205 record_dtmf(&state, '4');
206 record_dtmf(&state, '#');
207 assert_eq!(state.get::<String>(KEY_DTMF), Some("#".into()));
208 assert_eq!(state.get::<String>(KEY_DTMF_HISTORY), Some("4#".into()));
209 }
210
211 #[tokio::test(start_paused = true)]
212 async fn filler_fires_after_silence_and_respects_min_interval() {
213 let (event_tx, event_rx) = broadcast::channel(16);
214 let (speaker_tx, mut speaker_rx) = mpsc::channel(4);
215 let config = FillerConfig::new(vec![7i16; 80])
216 .delay(Duration::from_secs(2))
217 .min_interval(Duration::from_secs(10));
218 let task = tokio::spawn(filler_task(event_rx, speaker_tx, config));
219
220 event_tx.send(LiveEvent::VadEnd).unwrap();
222 tokio::time::sleep(Duration::from_millis(2100)).await;
223 match speaker_rx.recv().await {
224 Some(Playback::Chunk(samples)) => assert_eq!(samples, vec![7i16; 80]),
225 other => panic!("expected filler chunk, got {other:?}"),
226 }
227
228 event_tx.send(LiveEvent::VadEnd).unwrap();
230 tokio::time::sleep(Duration::from_millis(2100)).await;
231 assert!(
232 speaker_rx.try_recv().is_err(),
233 "min_interval suppresses a second filler"
234 );
235
236 drop(event_tx);
237 let _ = task.await;
238 }
239
240 #[tokio::test(start_paused = true)]
241 async fn model_audio_disarms_the_filler() {
242 let (event_tx, event_rx) = broadcast::channel(16);
243 let (speaker_tx, mut speaker_rx) = mpsc::channel(4);
244 let task = tokio::spawn(filler_task(
245 event_rx,
246 speaker_tx,
247 FillerConfig::new(vec![1i16]).delay(Duration::from_secs(2)),
248 ));
249
250 event_tx.send(LiveEvent::VadEnd).unwrap();
251 tokio::time::sleep(Duration::from_millis(500)).await;
252 event_tx
254 .send(LiveEvent::Audio(bytes::Bytes::from_static(&[0, 0])))
255 .unwrap();
256 tokio::time::sleep(Duration::from_secs(5)).await;
257 assert!(speaker_rx.try_recv().is_err(), "audio disarmed the filler");
258
259 drop(event_tx);
260 let _ = task.await;
261 }
262}