gemini_adk_rs/live/
playback.rs1use std::sync::Arc;
30use std::time::{Duration, Instant};
31
32use parking_lot::Mutex;
33
34use crate::clock::SharedClock;
35
36const DEFAULT_CHARS_PER_SEC: f64 = 16.0;
39const CALIBRATION_MIN: Duration = Duration::from_secs(2);
41const OUTPUT_BYTES_PER_SEC: f64 = 48_000.0;
43
44#[derive(Clone)]
50pub struct PlaybackClock {
51 inner: Arc<Mutex<Queue>>,
52 clock: SharedClock,
53}
54
55impl std::fmt::Debug for PlaybackClock {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 let q = self.inner.lock();
58 f.debug_struct("PlaybackClock")
59 .field("reporting", &q.reporting)
60 .field("scheduled", &q.scheduled)
61 .finish()
62 }
63}
64
65#[derive(Debug, Default)]
66struct Queue {
67 reporting: bool,
68 scheduled: Duration,
71 drains_at: Option<Instant>,
73}
74
75impl Queue {
76 fn remaining(&self, now: Instant) -> Duration {
77 self.drains_at
78 .map_or(Duration::ZERO, |end| end.saturating_duration_since(now))
79 }
80}
81
82impl PlaybackClock {
83 pub fn new(clock: SharedClock) -> Self {
86 Self {
87 inner: Arc::default(),
88 clock,
89 }
90 }
91
92 pub fn queued(&self, audio: Duration) {
95 self.queued_at(self.clock.now(), audio);
96 }
97
98 pub fn flushed(&self) {
101 self.flushed_at(self.clock.now());
102 }
103
104 pub fn heard(&self) -> Option<Duration> {
107 self.heard_at(self.clock.now())
108 }
109
110 pub(crate) fn queued_at(&self, now: Instant, audio: Duration) {
111 let mut q = self.inner.lock();
112 q.reporting = true;
113 let start = q.drains_at.filter(|&end| end > now).unwrap_or(now);
114 q.drains_at = Some(start + audio);
115 q.scheduled += audio;
116 }
117
118 pub(crate) fn flushed_at(&self, now: Instant) {
119 let mut q = self.inner.lock();
120 let dropped = q.remaining(now);
121 q.scheduled = q.scheduled.saturating_sub(dropped);
122 q.drains_at = Some(now);
123 }
124
125 pub(crate) fn heard_at(&self, now: Instant) -> Option<Duration> {
126 let q = self.inner.lock();
127 q.reporting
128 .then(|| q.scheduled.saturating_sub(q.remaining(now)))
129 }
130
131 fn scheduled(&self) -> Duration {
133 self.inner.lock().scheduled
134 }
135}
136
137#[derive(Debug, Default)]
140pub(crate) struct TurnSpeech {
141 audio: Duration,
143 chars: usize,
145 starts_at: Option<Duration>,
147 cut: bool,
149 calibrated_chars: usize,
151 calibrated_audio: Duration,
152}
153
154impl TurnSpeech {
155 pub(crate) fn on_audio(&mut self, bytes: usize, playback: &PlaybackClock) {
156 if self.cut {
157 return;
158 }
159 if self.starts_at.is_none() {
160 self.starts_at = Some(playback.scheduled());
161 }
162 self.audio += Duration::from_secs_f64(bytes as f64 / OUTPUT_BYTES_PER_SEC);
163 }
164
165 pub(crate) fn on_text(&mut self, text: &str) {
166 if !self.cut {
167 self.chars += text.chars().count();
168 }
169 }
170
171 pub(crate) fn on_interrupted(&mut self, playback: &PlaybackClock) -> Option<usize> {
174 self.on_interrupted_at(playback, playback.clock.now())
175 }
176
177 pub(crate) fn on_interrupted_at(
178 &mut self,
179 playback: &PlaybackClock,
180 now: Instant,
181 ) -> Option<usize> {
182 if self.cut {
183 return None;
184 }
185 self.cut = true;
186 if self.audio.is_zero() || self.chars == 0 {
187 return None;
188 }
189 let heard = match (playback.heard_at(now), self.starts_at) {
190 (Some(total), Some(start)) => total.saturating_sub(start).min(self.audio),
191 _ => self.audio,
192 };
193 let chars = (heard.as_secs_f64() * self.chars_per_sec()).floor() as usize;
194 Some(chars.min(self.chars))
195 }
196
197 pub(crate) fn on_turn_complete(&mut self) {
198 if !self.cut && self.chars > 0 && self.audio >= Duration::from_millis(500) {
199 self.calibrated_chars += self.chars;
200 self.calibrated_audio += self.audio;
201 }
202 self.audio = Duration::ZERO;
203 self.chars = 0;
204 self.starts_at = None;
205 self.cut = false;
206 }
207
208 fn chars_per_sec(&self) -> f64 {
209 if self.calibrated_audio >= CALIBRATION_MIN {
210 self.calibrated_chars as f64 / self.calibrated_audio.as_secs_f64()
211 } else {
212 DEFAULT_CHARS_PER_SEC
213 }
214 }
215}
216
217pub(crate) fn heard_prefix(text: &str, chars: usize) -> usize {
220 let Some((cut, next)) = text.char_indices().nth(chars) else {
221 return text.len();
222 };
223 let head = &text[..cut];
224 let head = if next.is_whitespace() || head.ends_with(char::is_whitespace) {
226 head
227 } else {
228 head.rfind(char::is_whitespace).map_or("", |i| &head[..i])
229 };
230 head.trim_end().len()
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::clock::system_clock;
237
238 fn ms(n: u64) -> Duration {
239 Duration::from_millis(n)
240 }
241
242 fn audio_bytes(n: u64) -> usize {
244 (n * 48) as usize
245 }
246
247 #[test]
248 fn heard_follows_real_time_and_flush_drops_the_rest() {
249 let clock = PlaybackClock::new(system_clock());
250 let t0 = Instant::now();
251 assert_eq!(clock.heard_at(t0), None);
252 clock.queued_at(t0, ms(3_000));
254 assert_eq!(clock.heard_at(t0 + ms(1_200)), Some(ms(1_200)));
255 clock.queued_at(t0 + ms(1_500), ms(1_000));
257 assert_eq!(clock.heard_at(t0 + ms(3_500)), Some(ms(3_500)));
258 clock.flushed_at(t0 + ms(3_600));
260 assert_eq!(clock.heard_at(t0 + ms(9_000)), Some(ms(3_600)));
261 clock.queued_at(t0 + ms(10_000), ms(500));
263 assert_eq!(clock.heard_at(t0 + ms(10_200)), Some(ms(3_800)));
264 }
265
266 #[test]
267 fn an_interrupted_turn_is_cut_to_the_audio_heard() {
268 let playback = PlaybackClock::new(system_clock());
269 let mut speech = TurnSpeech::default();
270 let t0 = Instant::now();
271 speech.on_audio(audio_bytes(2_000), &playback);
273 playback.queued_at(t0, ms(2_000));
274 speech.on_text(&"x".repeat(40));
275 speech.on_turn_complete();
276 assert_eq!(speech.chars_per_sec(), 20.0);
277
278 let start = t0 + ms(10_000);
282 speech.on_audio(audio_bytes(5_000), &playback);
283 playback.queued_at(start, ms(5_000));
284 speech.on_text(&"y".repeat(150));
285 let heard = speech.on_interrupted_at(&playback, start + ms(1_500));
286 assert_eq!(heard, Some(30)); speech.on_text("zzz");
290 speech.on_turn_complete();
291 assert_eq!(speech.chars_per_sec(), 20.0);
292 }
293
294 #[test]
295 fn without_a_reporter_the_cut_is_the_audio_received() {
296 let playback = PlaybackClock::new(system_clock());
297 let mut speech = TurnSpeech::default();
298 speech.on_audio(audio_bytes(2_000), &playback);
299 speech.on_text(&"w ".repeat(100));
300 assert_eq!(
302 speech.on_interrupted_at(&playback, Instant::now()),
303 Some(32)
304 );
305 }
306
307 #[test]
308 fn a_turn_without_audio_or_text_is_left_whole() {
309 let playback = PlaybackClock::new(system_clock());
310 let mut text_only = TurnSpeech::default();
311 text_only.on_text("a text session's reply");
312 assert_eq!(text_only.on_interrupted(&playback), None);
313
314 let mut untranscribed = TurnSpeech::default();
315 untranscribed.on_audio(audio_bytes(1_000), &playback);
316 assert_eq!(untranscribed.on_interrupted(&playback), None);
317 }
318
319 #[test]
320 fn the_heard_prefix_ends_at_a_whole_word() {
321 let text = "Your table is booked for Friday at eight.";
322 assert_eq!(&text[..heard_prefix(text, 17)], "Your table is"); assert_eq!(&text[..heard_prefix(text, 13)], "Your table is"); assert_eq!(&text[..heard_prefix(text, 14)], "Your table is"); assert_eq!(&text[..heard_prefix(text, 3)], ""); assert_eq!(heard_prefix(text, 400), text.len());
327 let accented = "Réservation confirmée à huit heures";
329 assert_eq!(
330 &accented[..heard_prefix(accented, 24)],
331 "Réservation confirmée à"
332 );
333 }
334}