test(huddle): reproduce dropped streaming TTS tail

Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
John Tennant
2026-08-19 13:46:46 -04:00
parent fe7c6808e7
commit e09a91b7d8
2 changed files with 171 additions and 1 deletions

View File

@@ -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);
}
}

View File

@@ -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<f32>) -> bool) -> Result<bool, String>,
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);
}
}