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 const KEY_KEYPAD_MASK: &str = "telephony:keypad_mask";
49pub const KEY_KEYPAD_ENTRY: &str = "telephony:keypad_entry";
53pub const KEY_KEYPAD_LEN: &str = "telephony:keypad_len";
56pub const KEY_KEYPAD_DONE: &str = "telephony:keypad_done";
58
59pub fn keypad_masked(state: &State) -> bool {
61 state.get::<bool>(KEY_KEYPAD_MASK).unwrap_or(false)
62}
63
64pub fn record_dtmf(state: &State, digit: char) {
71 if keypad_masked(state) {
72 state.redact_keys([KEY_KEYPAD_ENTRY]);
73 if digit == '#' {
74 let _ = state.set(KEY_KEYPAD_DONE, true);
75 return;
76 }
77 let mut len = 0usize;
78 let _ = state.modify(KEY_KEYPAD_ENTRY, String::new(), |mut entry| {
79 entry.push(digit);
80 len = entry.len();
81 entry
82 });
83 let _ = state.set(KEY_KEYPAD_LEN, len);
84 return;
85 }
86 let _ = state.set(KEY_DTMF, digit.to_string());
87 let _ = state.modify(KEY_DTMF_HISTORY, String::new(), |mut history| {
88 history.push(digit);
89 history
90 });
91}
92
93pub struct KeypadGuard {
108 state: State,
109 detector: super::dtmf::DtmfDetector,
110 record_in_band: bool,
111 hold_samples: usize,
112 held: usize,
113}
114
115impl KeypadGuard {
116 pub fn new(state: State, sample_rate: u32) -> Self {
118 Self {
119 state,
120 detector: super::dtmf::DtmfDetector::new(sample_rate),
121 record_in_band: false,
122 hold_samples: (sample_rate as usize * 60) / 1000,
123 held: 0,
124 }
125 }
126
127 pub fn record_in_band(mut self, enabled: bool) -> Self {
129 self.record_in_band = enabled;
130 self
131 }
132}
133
134impl crate::voice::InputAudioProcessor for KeypadGuard {
135 fn process_frame(&mut self, frame: &mut Vec<i16>) {
136 for digit in self.detector.feed(frame) {
137 if self.record_in_band {
138 record_dtmf(&self.state, digit);
139 }
140 }
141 let tone = self.detector.tone_present();
142 if tone {
143 self.held = self.hold_samples;
144 }
145 let silence = tone || self.held > 0 || keypad_masked(&self.state);
146 self.held = self.held.saturating_sub(frame.len());
147 if silence {
148 frame.fill(0);
149 }
150 }
151}
152
153#[derive(Debug, Default)]
160pub struct DtmfDeduper {
161 last_end_timestamp: Option<u32>,
162}
163
164impl DtmfDeduper {
165 pub fn accept(&mut self, end: bool, rtp_timestamp: u32) -> bool {
168 if !end {
169 return false;
170 }
171 if self.last_end_timestamp == Some(rtp_timestamp) {
172 return false;
173 }
174 self.last_end_timestamp = Some(rtp_timestamp);
175 true
176 }
177}
178
179#[derive(Clone)]
183pub struct FillerConfig {
184 pub clip: Arc<Vec<i16>>,
188 pub delay: Duration,
192 pub min_interval: Duration,
195}
196
197impl FillerConfig {
198 pub fn new(clip: Vec<i16>) -> Self {
201 Self {
202 clip: Arc::new(clip),
203 delay: Duration::from_secs(2),
204 min_interval: Duration::from_secs(10),
205 }
206 }
207
208 pub fn delay(mut self, delay: Duration) -> Self {
210 self.delay = delay;
211 self
212 }
213
214 pub fn min_interval(mut self, interval: Duration) -> Self {
216 self.min_interval = interval;
217 self
218 }
219}
220
221pub fn spawn_latency_filler(
236 handle: &LiveHandle,
237 speaker: mpsc::Sender<Playback>,
238 config: FillerConfig,
239) -> JoinHandle<()> {
240 let events = handle.events();
241 tokio::spawn(filler_task(events, speaker, config))
242}
243
244pub(crate) async fn filler_task(
247 mut events: broadcast::Receiver<LiveEvent>,
248 speaker: mpsc::Sender<Playback>,
249 config: FillerConfig,
250) {
251 let mut armed_at: Option<tokio::time::Instant> = None;
252 let mut last_filler: Option<tokio::time::Instant> = None;
253 loop {
254 let deadline = armed_at.map(|at| at + config.delay);
255 tokio::select! {
256 event = events.recv() => match event {
257 Ok(LiveEvent::VadEnd) => armed_at = Some(tokio::time::Instant::now()),
258 Ok(LiveEvent::Audio(_)) | Ok(LiveEvent::VadStart) | Ok(LiveEvent::Interrupted) => {
260 armed_at = None;
261 }
262 Ok(_) => {}
263 Err(broadcast::error::RecvError::Lagged(_)) => continue,
264 Err(broadcast::error::RecvError::Closed) => break,
265 },
266 () = async {
267 match deadline {
268 Some(deadline) => tokio::time::sleep_until(deadline).await,
269 None => std::future::pending().await,
270 }
271 } => {
272 armed_at = None;
273 let recently = last_filler
274 .is_some_and(|at| at.elapsed() < config.min_interval);
275 if !recently {
276 last_filler = Some(tokio::time::Instant::now());
277 let _ = speaker
278 .send(Playback::Chunk(config.clip.as_ref().clone()))
279 .await;
280 }
281 }
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn dtmf_dedup_accepts_one_end_per_timestamp() {
292 let mut dedup = DtmfDeduper::default();
293 assert!(!dedup.accept(false, 100), "non-end packets never emit");
294 assert!(dedup.accept(true, 100), "first end emits");
295 assert!(!dedup.accept(true, 100), "retransmitted end is dropped");
296 assert!(!dedup.accept(true, 100));
297 assert!(dedup.accept(true, 900), "next keypress emits again");
298 }
299
300 #[test]
301 fn record_dtmf_writes_the_shared_keys() {
302 let state = State::new();
303 record_dtmf(&state, '4');
304 record_dtmf(&state, '#');
305 assert_eq!(state.get::<String>(KEY_DTMF), Some("#".into()));
306 assert_eq!(state.get::<String>(KEY_DTMF_HISTORY), Some("4#".into()));
307 }
308
309 #[test]
310 fn a_masked_keypad_keeps_digits_out_of_the_shared_keys() {
311 let state = State::new();
312 record_dtmf(&state, '1');
313 let _ = state.set(KEY_KEYPAD_MASK, true);
314 for digit in "4242#".chars() {
315 record_dtmf(&state, digit);
316 }
317 let _ = state.set(KEY_KEYPAD_MASK, false);
318 assert_eq!(state.get::<String>(KEY_DTMF_HISTORY), Some("1".into()));
320 assert_eq!(state.get::<String>(KEY_KEYPAD_ENTRY), Some("4242".into()));
321 assert_eq!(state.get::<usize>(KEY_KEYPAD_LEN), Some(4));
322 assert_eq!(state.get::<bool>(KEY_KEYPAD_DONE), Some(true));
323 assert!(state.redacted_keys().contains(KEY_KEYPAD_ENTRY));
325 }
326
327 #[test]
328 fn the_keypad_guard_silences_tones_and_masked_speech() {
329 use crate::voice::InputAudioProcessor as _;
330 let state = State::new();
331 let mut guard = KeypadGuard::new(state.clone(), 8_000).record_in_band(true);
332 let tones = crate::telephony::dtmf::tests::keypresses("73", 80, 60, 6_000.0);
333 let mut heard = Vec::new();
334 for chunk in tones.chunks(160) {
335 let mut frame = chunk.to_vec();
336 guard.process_frame(&mut frame);
337 heard.extend(frame);
338 }
339 assert_eq!(state.get::<String>(KEY_DTMF_HISTORY), Some("73".into()));
340 let leaked = heard.iter().filter(|&&s| s != 0).count();
342 assert!(leaked <= 2 * 205, "{leaked} tone samples reached the model");
343
344 let speech: Vec<i16> = (0..160).map(|n| ((n % 40) as i16 - 20) * 300).collect();
346 let mut frame = speech.clone();
347 guard.process_frame(&mut frame);
348 assert_eq!(frame, speech);
349 let _ = state.set(KEY_KEYPAD_MASK, true);
350 let mut frame = speech.clone();
351 guard.process_frame(&mut frame);
352 assert!(frame.iter().all(|&s| s == 0));
353 }
354
355 #[tokio::test(start_paused = true)]
356 async fn filler_fires_after_silence_and_respects_min_interval() {
357 let (event_tx, event_rx) = broadcast::channel(16);
358 let (speaker_tx, mut speaker_rx) = mpsc::channel(4);
359 let config = FillerConfig::new(vec![7i16; 80])
360 .delay(Duration::from_secs(2))
361 .min_interval(Duration::from_secs(10));
362 let task = tokio::spawn(filler_task(event_rx, speaker_tx, config));
363
364 event_tx.send(LiveEvent::VadEnd).unwrap();
366 tokio::time::sleep(Duration::from_millis(2100)).await;
367 match speaker_rx.recv().await {
368 Some(Playback::Chunk(samples)) => assert_eq!(samples, vec![7i16; 80]),
369 other => panic!("expected filler chunk, got {other:?}"),
370 }
371
372 event_tx.send(LiveEvent::VadEnd).unwrap();
374 tokio::time::sleep(Duration::from_millis(2100)).await;
375 assert!(
376 speaker_rx.try_recv().is_err(),
377 "min_interval suppresses a second filler"
378 );
379
380 drop(event_tx);
381 let _ = task.await;
382 }
383
384 #[tokio::test(start_paused = true)]
385 async fn model_audio_disarms_the_filler() {
386 let (event_tx, event_rx) = broadcast::channel(16);
387 let (speaker_tx, mut speaker_rx) = mpsc::channel(4);
388 let task = tokio::spawn(filler_task(
389 event_rx,
390 speaker_tx,
391 FillerConfig::new(vec![1i16]).delay(Duration::from_secs(2)),
392 ));
393
394 event_tx.send(LiveEvent::VadEnd).unwrap();
395 tokio::time::sleep(Duration::from_millis(500)).await;
396 event_tx
398 .send(LiveEvent::Audio(bytes::Bytes::from_static(&[0, 0])))
399 .unwrap();
400 tokio::time::sleep(Duration::from_secs(5)).await;
401 assert!(speaker_rx.try_recv().is_err(), "audio disarmed the filler");
402
403 drop(event_tx);
404 let _ = task.await;
405 }
406}