From e09a91b7d8433a0b4af8e90a57fba9c24b3723d4 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Wed, 19 Aug 2026 13:46:46 -0400 Subject: [PATCH] test(huddle): reproduce dropped streaming TTS tail Signed-off-by: John Tennant --- desktop/src-tauri/src/huddle/tts_audio.rs | 23 +++ desktop/src-tauri/src/huddle/tts_streaming.rs | 149 +++++++++++++++++- 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/desktop/src-tauri/src/huddle/tts_audio.rs b/desktop/src-tauri/src/huddle/tts_audio.rs index 80bf0c4661..4c5608a8cb 100644 --- a/desktop/src-tauri/src/huddle/tts_audio.rs +++ b/desktop/src-tauri/src/huddle/tts_audio.rs @@ -165,4 +165,27 @@ mod tests { .iter() .all(|sample| *sample == 0.0)); } + + #[test] + fn only_the_final_stream_block_receives_a_fade() { + let mut chunk = PlaybackChunkAudio::new(); + let mut first_append = true; + let first_samples = vec![0.5; FADE_OUT_SAMPLES * 2]; + let final_samples = vec![0.75; FADE_OUT_SAMPLES * 2]; + + assert!(chunk + .push(first_samples.clone(), 0, &mut first_append, false) + .is_none()); + let first = chunk + .push(final_samples.clone(), 1, &mut first_append, false) + .expect("first stream block"); + let final_block = chunk + .finish(&mut first_append, false) + .expect("final stream block"); + + assert_eq!(first.buffer, first_samples); + assert_eq!(final_block.buffer[0], 0.75); + assert_eq!(final_block.buffer.last(), Some(&0.0)); + assert_ne!(final_block.buffer, final_samples); + } } diff --git a/desktop/src-tauri/src/huddle/tts_streaming.rs b/desktop/src-tauri/src/huddle/tts_streaming.rs index 2bb401c43f..ba068f309e 100644 --- a/desktop/src-tauri/src/huddle/tts_streaming.rs +++ b/desktop/src-tauri/src/huddle/tts_streaming.rs @@ -50,6 +50,20 @@ pub(super) fn synthesize_streaming( signals: (&AtomicBool, &AtomicBool, &AtomicBool), playback: StreamingPlayback<'_>, append_audio: &mut dyn FnMut(PreparedModelAudio) -> bool, +) -> Option<&'static str> { + drive_streaming( + |on_audio| engine.synth_chunk_streaming(text, style, emit_frames, on_audio), + signals, + playback, + append_audio, + ) +} + +fn drive_streaming( + synthesize: impl FnOnce(&mut dyn FnMut(Vec) -> bool) -> Result, + signals: (&AtomicBool, &AtomicBool, &AtomicBool), + playback: StreamingPlayback<'_>, + append_audio: &mut dyn FnMut(PreparedModelAudio) -> bool, ) -> Option<&'static str> { let (cancel, voice_cancel, shutdown) = signals; let StreamingPlayback { @@ -59,7 +73,7 @@ pub(super) fn synthesize_streaming( } = playback; let mut playback_audio = PlaybackChunkAudio::new(); let mut delta_index = 0usize; - let stream_result = engine.synth_chunk_streaming(text, style, emit_frames, &mut |samples| { + let stream_result = synthesize(&mut |samples| { if cancel.load(Ordering::Acquire) || voice_cancel.load(Ordering::Acquire) || shutdown.load(Ordering::Acquire) @@ -102,3 +116,136 @@ pub(super) fn synthesize_streaming( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inference_failure_fades_the_retained_final_block() { + let cancel = AtomicBool::new(false); + let voice_cancel = AtomicBool::new(false); + let shutdown = AtomicBool::new(false); + let mut first_append = true; + let (player, _output) = rodio::Player::new(); + let mut appended = Vec::new(); + + let outcome = drive_streaming( + |on_audio| { + assert!(on_audio(vec![0.4; 16])); + assert!(on_audio(vec![0.5; 16])); + Err("inference failed after output".into()) + }, + (&cancel, &voice_cancel, &shutdown), + StreamingPlayback { + player: &player, + first_append: &mut first_append, + route_id: 7, + }, + &mut |prepared| { + appended.push(prepared); + true + }, + ); + + assert_eq!(outcome, Some("failed")); + assert_eq!(appended.len(), 2); + assert!(appended[0].buffer.ends_with(&[0.4; 16])); + let final_samples = &appended[1].buffer[appended[1].buffer.len() - 16..]; + assert_eq!(final_samples.first(), Some(&0.5)); + assert_eq!(final_samples.last(), Some(&0.0)); + assert!(!first_append, "the partial phrase remains queued"); + } + + #[test] + fn inference_failure_before_output_preserves_existing_playback() { + let cancel = AtomicBool::new(false); + let voice_cancel = AtomicBool::new(false); + let shutdown = AtomicBool::new(false); + let mut first_append = false; + let (player, _output) = rodio::Player::new(); + + let outcome = drive_streaming( + |_| Err("inference failed before output".into()), + (&cancel, &voice_cancel, &shutdown), + StreamingPlayback { + player: &player, + first_append: &mut first_append, + route_id: 7, + }, + &mut |_| panic!("no audio should be appended"), + ); + + assert_eq!(outcome, Some("failed")); + assert!(!first_append, "the existing utterance still owns playback"); + } + + #[test] + fn each_stop_signal_prevents_later_stream_output() { + for signal_index in 0..3 { + let signals = [ + AtomicBool::new(false), + AtomicBool::new(false), + AtomicBool::new(false), + ]; + let mut first_append = true; + let (player, _output) = rodio::Player::new(); + let mut appended = 0; + + let outcome = drive_streaming( + |on_audio| { + assert!(on_audio(vec![0.4; 16])); + signals[signal_index].store(true, Ordering::Release); + assert!(!on_audio(vec![0.5; 16])); + Ok(false) + }, + (&signals[0], &signals[1], &signals[2]), + StreamingPlayback { + player: &player, + first_append: &mut first_append, + route_id: 7, + }, + &mut |_| { + appended += 1; + true + }, + ); + + assert_eq!(outcome, Some("cancelled")); + assert_eq!(appended, 0, "no retained block may escape after Stop"); + assert!(first_append); + } + } + + #[test] + fn rejected_append_never_flushes_the_retained_final_block() { + let cancel = AtomicBool::new(false); + let voice_cancel = AtomicBool::new(false); + let shutdown = AtomicBool::new(false); + let mut first_append = true; + let (player, _output) = rodio::Player::new(); + let mut append_attempts = 0; + + let outcome = drive_streaming( + |on_audio| { + assert!(on_audio(vec![0.4; 16])); + assert!(!on_audio(vec![0.5; 16])); + Ok(false) + }, + (&cancel, &voice_cancel, &shutdown), + StreamingPlayback { + player: &player, + first_append: &mut first_append, + route_id: 7, + }, + &mut |_| { + append_attempts += 1; + false + }, + ); + + assert_eq!(outcome, Some("cancelled")); + assert_eq!(append_attempts, 1); + assert!(first_append); + } +}