1use crate::processor::frame::Frame;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum TurnStartDecision {
31 Triggered,
33 Continue,
35 ResetAggregation,
37}
38
39pub trait TurnStartStrategy: Send {
42 fn on_frame(&mut self, frame: &Frame) -> TurnStartDecision;
44 fn reset(&mut self);
46}
47
48#[derive(Debug, Default, Clone)]
52pub struct VadTurnStart;
53
54impl TurnStartStrategy for VadTurnStart {
55 fn on_frame(&mut self, frame: &Frame) -> TurnStartDecision {
56 match frame {
57 Frame::VadUserStartedSpeaking { .. } => TurnStartDecision::Triggered,
58 _ => TurnStartDecision::Continue,
59 }
60 }
61 fn reset(&mut self) {}
62}
63
64#[derive(Debug, Clone)]
69pub struct MinWordsTurnStart {
70 min_words: usize,
71 use_interim: bool,
72 bot_speaking: bool,
73}
74
75impl MinWordsTurnStart {
76 pub fn new(min_words: usize, use_interim: bool) -> Self {
79 Self {
80 min_words,
81 use_interim,
82 bot_speaking: false,
83 }
84 }
85
86 fn handle_transcription(&self, text: &str) -> TurnStartDecision {
87 let floor = if self.bot_speaking {
89 self.min_words.max(1)
90 } else {
91 1
92 };
93 let words = text.split_whitespace().count();
94 if words >= floor {
95 TurnStartDecision::Triggered
96 } else {
97 TurnStartDecision::ResetAggregation
98 }
99 }
100}
101
102impl TurnStartStrategy for MinWordsTurnStart {
103 fn on_frame(&mut self, frame: &Frame) -> TurnStartDecision {
104 match frame {
105 Frame::BotStartedSpeaking => {
106 self.bot_speaking = true;
107 TurnStartDecision::Continue
108 }
109 Frame::BotStoppedSpeaking => {
110 self.bot_speaking = false;
111 TurnStartDecision::Continue
112 }
113 Frame::Transcription { text, .. } => self.handle_transcription(text),
114 Frame::InterimTranscription { text, .. } if self.use_interim => {
115 self.handle_transcription(text)
116 }
117 _ => TurnStartDecision::Continue,
118 }
119 }
120 fn reset(&mut self) {
121 self.bot_speaking = false;
122 }
123}
124
125#[derive(Debug, Default, Clone)]
130pub struct ExternalTurnStart {
131 triggered: bool,
132}
133
134impl ExternalTurnStart {
135 pub fn trigger(&mut self) {
137 self.triggered = true;
138 }
139}
140
141impl TurnStartStrategy for ExternalTurnStart {
142 fn on_frame(&mut self, _frame: &Frame) -> TurnStartDecision {
143 if self.triggered {
144 self.triggered = false;
145 TurnStartDecision::Triggered
146 } else {
147 TurnStartDecision::Continue
148 }
149 }
150 fn reset(&mut self) {
151 self.triggered = false;
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum TurnStopDecision {
159 Triggered,
161 Waiting,
163}
164
165pub trait TurnStopStrategy: Send {
171 fn on_frame(&mut self, frame: &Frame) -> TurnStopDecision;
173 fn on_silence_tick(&mut self, elapsed_ms: f32) -> TurnStopDecision;
175 fn reset(&mut self);
177}
178
179#[derive(Debug, Clone)]
185pub struct TurnAnalyzerStop {
186 stt_timeout_ms: f32,
189 vad_user_speaking: bool,
190 turn_complete: bool,
192 have_text: bool,
193 transcript_finalized: bool,
194 timeout_expired: bool,
195}
196
197impl TurnAnalyzerStop {
198 pub fn new(stt_timeout_ms: f32) -> Self {
200 Self {
201 stt_timeout_ms,
202 vad_user_speaking: false,
203 turn_complete: false,
204 have_text: false,
205 transcript_finalized: false,
206 timeout_expired: false,
207 }
208 }
209
210 pub fn set_turn_complete(&mut self, complete: bool) -> TurnStopDecision {
213 self.turn_complete = complete;
214 self.evaluate()
215 }
216
217 fn evaluate(&self) -> TurnStopDecision {
218 if !self.have_text || !self.turn_complete || self.vad_user_speaking {
219 return TurnStopDecision::Waiting;
220 }
221 if self.transcript_finalized || self.timeout_expired {
223 TurnStopDecision::Triggered
224 } else {
225 TurnStopDecision::Waiting
226 }
227 }
228}
229
230impl TurnStopStrategy for TurnAnalyzerStop {
231 fn on_frame(&mut self, frame: &Frame) -> TurnStopDecision {
232 match frame {
233 Frame::VadUserStartedSpeaking { .. } => {
234 self.vad_user_speaking = true;
235 self.turn_complete = false;
236 self.transcript_finalized = false;
237 self.timeout_expired = false;
238 TurnStopDecision::Waiting
239 }
240 Frame::VadUserStoppedSpeaking { .. } => {
241 self.vad_user_speaking = false;
242 self.evaluate()
243 }
244 Frame::Transcription { text, final_, .. } => {
245 if !text.is_empty() {
246 self.have_text = true;
247 }
248 if *final_ {
249 self.transcript_finalized = true;
250 }
251 self.evaluate()
252 }
253 _ => TurnStopDecision::Waiting,
254 }
255 }
256
257 fn on_silence_tick(&mut self, elapsed_ms: f32) -> TurnStopDecision {
258 if !self.vad_user_speaking && elapsed_ms >= self.stt_timeout_ms {
259 self.timeout_expired = true;
260 }
261 self.evaluate()
262 }
263
264 fn reset(&mut self) {
265 self.vad_user_speaking = false;
266 self.turn_complete = false;
267 self.have_text = false;
268 self.transcript_finalized = false;
269 self.timeout_expired = false;
270 }
271}
272
273#[derive(Debug, Clone)]
279pub struct SpeechTimeoutStop {
280 user_speech_timeout_ms: f32,
281 stt_timeout_ms: f32,
282 vad_user_speaking: bool,
283 have_text: bool,
284 transcript_finalized: bool,
285 user_speech_done: bool,
286 stt_done: bool,
287}
288
289impl SpeechTimeoutStop {
290 pub fn new(user_speech_timeout_ms: f32, stt_timeout_ms: f32) -> Self {
293 Self {
294 user_speech_timeout_ms,
295 stt_timeout_ms,
296 vad_user_speaking: false,
297 have_text: false,
298 transcript_finalized: false,
299 user_speech_done: false,
300 stt_done: false,
301 }
302 }
303
304 fn evaluate(&self) -> TurnStopDecision {
305 if self.vad_user_speaking || !self.have_text {
306 return TurnStopDecision::Waiting;
307 }
308 if self.user_speech_done && self.stt_done {
309 TurnStopDecision::Triggered
310 } else {
311 TurnStopDecision::Waiting
312 }
313 }
314}
315
316impl TurnStopStrategy for SpeechTimeoutStop {
317 fn on_frame(&mut self, frame: &Frame) -> TurnStopDecision {
318 match frame {
319 Frame::VadUserStartedSpeaking { .. } => {
320 self.vad_user_speaking = true;
321 self.transcript_finalized = false;
322 self.user_speech_done = false;
323 self.stt_done = false;
324 TurnStopDecision::Waiting
325 }
326 Frame::VadUserStoppedSpeaking { stop_secs } => {
327 self.vad_user_speaking = false;
328 if self.transcript_finalized || stop_secs * 1000.0 >= self.stt_timeout_ms {
330 self.stt_done = true;
331 }
332 self.evaluate()
333 }
334 Frame::Transcription { text, final_, .. } => {
335 if !text.is_empty() {
336 self.have_text = true;
337 }
338 if *final_ {
339 self.transcript_finalized = true;
340 self.stt_done = true;
341 }
342 self.evaluate()
343 }
344 _ => TurnStopDecision::Waiting,
345 }
346 }
347
348 fn on_silence_tick(&mut self, elapsed_ms: f32) -> TurnStopDecision {
349 if !self.vad_user_speaking {
350 if elapsed_ms >= self.user_speech_timeout_ms {
351 self.user_speech_done = true;
352 }
353 if elapsed_ms >= self.stt_timeout_ms {
354 self.stt_done = true;
355 }
356 }
357 self.evaluate()
358 }
359
360 fn reset(&mut self) {
361 self.vad_user_speaking = false;
362 self.have_text = false;
363 self.transcript_finalized = false;
364 self.user_speech_done = false;
365 self.stt_done = false;
366 }
367}
368
369pub trait MuteStrategy: Send {
374 fn on_frame(&mut self, frame: &Frame) -> bool;
376 fn muted(&self) -> bool;
378}
379
380#[derive(Debug, Default, Clone)]
382pub struct AlwaysMute;
383
384impl MuteStrategy for AlwaysMute {
385 fn on_frame(&mut self, _frame: &Frame) -> bool {
386 true
387 }
388 fn muted(&self) -> bool {
389 true
390 }
391}
392
393#[derive(Debug, Default, Clone)]
396pub struct MuteUntilFirstBotComplete {
397 first_speech_handled: bool,
398}
399
400impl MuteStrategy for MuteUntilFirstBotComplete {
401 fn on_frame(&mut self, frame: &Frame) -> bool {
402 if let Frame::BotStoppedSpeaking = frame {
403 self.first_speech_handled = true;
404 }
405 self.muted()
406 }
407 fn muted(&self) -> bool {
408 !self.first_speech_handled
409 }
410}
411
412#[derive(Debug, Default, Clone)]
415pub struct FirstSpeechMute {
416 bot_speaking: bool,
417 first_speech_handled: bool,
418}
419
420impl MuteStrategy for FirstSpeechMute {
421 fn on_frame(&mut self, frame: &Frame) -> bool {
422 match frame {
423 Frame::BotStartedSpeaking => self.bot_speaking = true,
424 Frame::BotStoppedSpeaking => {
425 self.bot_speaking = false;
426 self.first_speech_handled = true;
427 }
428 _ => {}
429 }
430 self.muted()
431 }
432 fn muted(&self) -> bool {
433 self.bot_speaking && !self.first_speech_handled
434 }
435}
436
437#[derive(Debug, Default, Clone)]
440pub struct FunctionCallMute {
441 in_progress: std::collections::HashSet<String>,
442}
443
444impl MuteStrategy for FunctionCallMute {
445 fn on_frame(&mut self, frame: &Frame) -> bool {
446 match frame {
447 Frame::FunctionCallsStarted(calls) => {
448 for c in calls {
449 self.in_progress.insert(c.tool_call_id.clone());
450 }
451 }
452 Frame::FunctionCallResult(r) => {
453 self.in_progress.remove(&r.tool_call_id);
454 }
455 Frame::FunctionCallCancel { tool_call_id, .. } => {
456 self.in_progress.remove(tool_call_id);
457 }
458 _ => {}
459 }
460 self.muted()
461 }
462 fn muted(&self) -> bool {
463 !self.in_progress.is_empty()
464 }
465}
466
467pub trait InterruptionStrategy: Send {
473 fn should_interrupt(&self) -> bool;
476 fn append_transcription(&mut self, frame: &Frame);
478 fn append_audio_volume(&mut self, _volume: f32) {}
480 fn reset(&mut self);
482}
483
484#[derive(Debug, Default, Clone)]
487pub struct AlwaysInterrupt;
488
489impl InterruptionStrategy for AlwaysInterrupt {
490 fn should_interrupt(&self) -> bool {
491 true
492 }
493 fn append_transcription(&mut self, _frame: &Frame) {}
494 fn reset(&mut self) {}
495}
496
497#[derive(Debug, Clone)]
501pub struct MinWordsInterrupt {
502 min_words: usize,
503 accumulated_words: usize,
504}
505
506impl MinWordsInterrupt {
507 pub fn new(min_words: usize) -> Self {
509 Self {
510 min_words,
511 accumulated_words: 0,
512 }
513 }
514}
515
516impl InterruptionStrategy for MinWordsInterrupt {
517 fn should_interrupt(&self) -> bool {
518 self.accumulated_words >= self.min_words
519 }
520 fn append_transcription(&mut self, frame: &Frame) {
521 match frame {
522 Frame::Transcription { text, .. } | Frame::InterimTranscription { text, .. } => {
523 self.accumulated_words += text.split_whitespace().count();
524 }
525 _ => {}
526 }
527 }
528 fn reset(&mut self) {
529 self.accumulated_words = 0;
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use crate::processor::frame::{FunctionCall, FunctionCallResult};
537 use std::sync::Arc;
538
539 fn transcription(text: &str, final_: bool) -> Frame {
540 Frame::Transcription {
541 text: text.into(),
542 user_id: Arc::from("u"),
543 language: None,
544 final_,
545 }
546 }
547 fn interim(text: &str) -> Frame {
548 Frame::InterimTranscription {
549 text: text.into(),
550 user_id: Arc::from("u"),
551 language: None,
552 }
553 }
554
555 #[test]
558 fn vad_start_triggers_on_vad_started() {
559 let mut s = VadTurnStart;
560 assert_eq!(
561 s.on_frame(&Frame::Text("noise".into())),
562 TurnStartDecision::Continue
563 );
564 assert_eq!(
565 s.on_frame(&Frame::VadUserStartedSpeaking { start_secs: 0.2 }),
566 TurnStartDecision::Triggered
567 );
568 }
569
570 #[test]
571 fn min_words_start_single_word_when_bot_silent() {
572 let mut s = MinWordsTurnStart::new(3, true);
573 assert_eq!(
575 s.on_frame(&transcription("hello", true)),
576 TurnStartDecision::Triggered
577 );
578 }
579
580 #[test]
581 fn min_words_start_needs_min_while_bot_speaking() {
582 let mut s = MinWordsTurnStart::new(3, true);
583 s.on_frame(&Frame::BotStartedSpeaking);
584 assert_eq!(
586 s.on_frame(&transcription("hi", false)),
587 TurnStartDecision::ResetAggregation
588 );
589 assert_eq!(
591 s.on_frame(&transcription("hold on please", false)),
592 TurnStartDecision::Triggered
593 );
594 s.on_frame(&Frame::BotStoppedSpeaking);
596 assert_eq!(
597 s.on_frame(&transcription("yes", false)),
598 TurnStartDecision::Triggered
599 );
600 }
601
602 #[test]
603 fn min_words_start_ignores_interim_when_disabled() {
604 let mut s = MinWordsTurnStart::new(2, false);
605 assert_eq!(
606 s.on_frame(&interim("one two three")),
607 TurnStartDecision::Continue
608 );
609 }
610
611 #[test]
612 fn external_start_only_triggers_once_armed() {
613 let mut s = ExternalTurnStart::default();
614 assert_eq!(
615 s.on_frame(&Frame::VadUserStartedSpeaking { start_secs: 0.2 }),
616 TurnStartDecision::Continue,
617 "VAD must not trigger an external strategy"
618 );
619 s.trigger();
620 assert_eq!(
621 s.on_frame(&Frame::Text("x".into())),
622 TurnStartDecision::Triggered
623 );
624 assert_eq!(
626 s.on_frame(&Frame::Text("x".into())),
627 TurnStartDecision::Continue
628 );
629 }
630
631 #[test]
634 fn turn_analyzer_stop_finalized_transcript_triggers_immediately() {
635 let mut s = TurnAnalyzerStop::new(1000.0);
636 assert_eq!(
637 s.on_frame(&Frame::VadUserStoppedSpeaking { stop_secs: 0.2 }),
638 TurnStopDecision::Waiting
639 );
640 assert_eq!(s.set_turn_complete(true), TurnStopDecision::Waiting);
642 assert_eq!(
644 s.on_frame(&transcription("done", true)),
645 TurnStopDecision::Triggered
646 );
647 }
648
649 #[test]
650 fn turn_analyzer_stop_waits_for_timeout_without_finalize() {
651 let mut s = TurnAnalyzerStop::new(800.0);
652 s.on_frame(&Frame::VadUserStoppedSpeaking { stop_secs: 0.2 });
653 s.set_turn_complete(true);
654 assert_eq!(
656 s.on_frame(&transcription("partial", false)),
657 TurnStopDecision::Waiting
658 );
659 assert_eq!(s.on_silence_tick(500.0), TurnStopDecision::Waiting);
661 assert_eq!(s.on_silence_tick(800.0), TurnStopDecision::Triggered);
663 }
664
665 #[test]
666 fn turn_analyzer_stop_blocks_while_user_speaking() {
667 let mut s = TurnAnalyzerStop::new(0.0);
668 s.on_frame(&Frame::VadUserStartedSpeaking { start_secs: 0.2 });
669 s.set_turn_complete(true);
670 assert_eq!(
672 s.on_frame(&transcription("more", true)),
673 TurnStopDecision::Waiting
674 );
675 }
676
677 #[test]
678 fn speech_timeout_stop_needs_both_waits_and_text() {
679 let mut s = SpeechTimeoutStop::new(600.0, 1200.0);
680 s.on_frame(&Frame::VadUserStoppedSpeaking { stop_secs: 0.2 });
681 s.on_frame(&transcription("ok", false));
682 assert_eq!(s.on_silence_tick(600.0), TurnStopDecision::Waiting);
684 assert_eq!(s.on_silence_tick(1200.0), TurnStopDecision::Triggered);
686 }
687
688 #[test]
689 fn speech_timeout_stop_finalize_shortcircuits_stt_wait() {
690 let mut s = SpeechTimeoutStop::new(600.0, 5000.0);
691 s.on_frame(&Frame::VadUserStoppedSpeaking { stop_secs: 0.2 });
692 s.on_frame(&transcription("done", true));
694 assert_eq!(s.on_silence_tick(600.0), TurnStopDecision::Triggered);
696 }
697
698 #[test]
699 fn speech_timeout_stop_no_text_never_triggers() {
700 let mut s = SpeechTimeoutStop::new(0.0, 0.0);
701 s.on_frame(&Frame::VadUserStoppedSpeaking { stop_secs: 0.5 });
702 assert_eq!(s.on_silence_tick(1000.0), TurnStopDecision::Waiting);
704 }
705
706 #[test]
709 fn always_mute_is_always_muted() {
710 let mut s = AlwaysMute;
711 assert!(s.on_frame(&Frame::BotStoppedSpeaking));
712 assert!(s.muted());
713 }
714
715 #[test]
716 fn mute_until_first_bot_complete() {
717 let mut s = MuteUntilFirstBotComplete::default();
718 assert!(s.muted(), "muted from the very start");
719 assert!(
720 s.on_frame(&Frame::BotStartedSpeaking),
721 "still muted while speaking"
722 );
723 assert!(
724 !s.on_frame(&Frame::BotStoppedSpeaking),
725 "unmuted after first complete"
726 );
727 assert!(!s.on_frame(&Frame::BotStartedSpeaking));
729 }
730
731 #[test]
732 fn first_speech_mute_only_during_first_bot_turn() {
733 let mut s = FirstSpeechMute::default();
734 assert!(!s.muted(), "not muted before the bot speaks");
735 assert!(
736 s.on_frame(&Frame::BotStartedSpeaking),
737 "muted during first speech"
738 );
739 assert!(!s.on_frame(&Frame::BotStoppedSpeaking), "unmuted after");
740 assert!(!s.on_frame(&Frame::BotStartedSpeaking));
742 }
743
744 #[test]
745 fn function_call_mute_tracks_in_flight_calls() {
746 let mut s = FunctionCallMute::default();
747 let call = FunctionCall {
748 function_name: "lookup".into(),
749 tool_call_id: "c1".into(),
750 arguments: serde_json::json!({}),
751 };
752 assert!(s.on_frame(&Frame::FunctionCallsStarted(vec![call])));
753 assert!(s.muted());
754 assert!(!s.on_frame(&Frame::FunctionCallResult(FunctionCallResult {
756 function_name: "lookup".into(),
757 tool_call_id: "c1".into(),
758 result: serde_json::json!({"ok": true}),
759 })));
760 }
761
762 #[test]
763 fn function_call_mute_cancel_also_unmutes() {
764 let mut s = FunctionCallMute::default();
765 let call = FunctionCall {
766 function_name: "lookup".into(),
767 tool_call_id: "c2".into(),
768 arguments: serde_json::json!({}),
769 };
770 s.on_frame(&Frame::FunctionCallsStarted(vec![call]));
771 assert!(!s.on_frame(&Frame::FunctionCallCancel {
772 function_name: "lookup".into(),
773 tool_call_id: "c2".into(),
774 }));
775 }
776
777 #[test]
780 fn always_interrupt_allows_barge_in() {
781 let s = AlwaysInterrupt;
782 assert!(s.should_interrupt());
783 }
784
785 #[test]
786 fn min_words_interrupt_gates_on_word_count() {
787 let mut s = MinWordsInterrupt::new(3);
788 assert!(!s.should_interrupt());
789 s.append_transcription(&interim("hold"));
790 assert!(!s.should_interrupt(), "1 word < 3");
791 s.append_transcription(&transcription("on please", false));
792 assert!(s.should_interrupt(), "3 words >= 3");
793 s.reset();
794 assert!(!s.should_interrupt());
795 }
796}