diff --git a/crates/buzz-voice/src/pocket_april.rs b/crates/buzz-voice/src/pocket_april.rs index 9ace5001daa..0a4ac3fd409 100644 --- a/crates/buzz-voice/src/pocket_april.rs +++ b/crates/buzz-voice/src/pocket_april.rs @@ -1799,10 +1799,8 @@ mod tests { .zip(&streamed) .map(|(a, b)| (a - b).abs()) .fold(0.0f32, f32::max); - assert!( - max_diff <= 1.0e-4, - "incremental decode diverged from batch decode: max |diff| = {max_diff}" - ); + eprintln!("delta_frames={DECODER_CHUNK_FRAMES}: max|diff|={max_diff:.6}"); + assert_eq!(max_diff, 0.0, "12-frame streaming must remain bit-exact"); } #[test] diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index aca2339a3c4..f4071034bc9 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -426,8 +426,8 @@ fn tts_worker( // `tts_active` lifecycle: set on the first append while idle, cleared // whenever the player has fully drained — either in the idle timeout // arm or on item receipt before synthesis begins. - // EXPERIMENTAL (latency bench): `Some(emit_frames)` = stream PCM deltas - // out of Pocket as they are generated (see tts_streaming.rs). + // `Some(emit_frames)` streams PCM deltas out of Pocket as they are + // generated (see tts_streaming.rs). `None` is the operational fallback. let tts_streaming = streaming_emit_frames(); // `first_append` = "no audio queued since the player last went idle". // Flipped by `build_sentence_append_buffer` on the first real append; the @@ -728,8 +728,8 @@ fn tts_worker( continue; } - // EXPERIMENTAL (latency bench): streaming synthesis path — see - // tts_streaming.rs for the mechanics and exactness constraints. + // Streaming synthesis path. See tts_streaming.rs for the mechanics + // and exactness constraints. if let Some(emit_frames) = tts_streaming { let outcome = synthesize_streaming( &engine, diff --git a/desktop/src-tauri/src/huddle/tts_streaming.rs b/desktop/src-tauri/src/huddle/tts_streaming.rs index 2df81d557de..9b6d0a45ba7 100644 --- a/desktop/src-tauri/src/huddle/tts_streaming.rs +++ b/desktop/src-tauri/src/huddle/tts_streaming.rs @@ -1,7 +1,8 @@ -//! EXPERIMENTAL (latency bench): streaming synthesis path for the TTS worker. +//! Streaming synthesis path for the TTS worker. //! -//! `BUZZ_TTS_STREAMING=1` streams PCM deltas out of Pocket as they are -//! generated instead of waiting for the full first-chunk synthesis. +//! PCM deltas stream out of Pocket as they are generated instead of waiting +//! for the full first-chunk synthesis. `BUZZ_TTS_STREAMING=0` or `false` +//! restores the batch path as an operational fallback. //! `BUZZ_TTS_EMIT_FRAMES` tunes the delta size in Flow LM frames (80 ms of //! audio each). Default 12 = the Mimi decoder's native chunk, which keeps //! streamed audio bit-identical to the batch path; smaller deltas are faster @@ -12,17 +13,22 @@ use super::*; use crate::huddle::pocket::{PocketTts, VoiceStyle}; -/// Read the streaming env overrides once per worker: `Some(emit_frames)` -/// when `BUZZ_TTS_STREAMING=1`, `None` for the production batch path. +/// Read the streaming env overrides once per worker. Streaming defaults to the +/// Mimi decoder's native 12-frame chunk; `BUZZ_TTS_STREAMING=0` or `false` +/// opts out. pub(super) fn streaming_emit_frames() -> Option { - std::env::var("BUZZ_TTS_STREAMING") - .is_ok_and(|v| v == "1") - .then(|| { - std::env::var("BUZZ_TTS_EMIT_FRAMES") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(12) - }) + resolve_streaming_emit_frames( + std::env::var("BUZZ_TTS_STREAMING").ok().as_deref(), + std::env::var("BUZZ_TTS_EMIT_FRAMES").ok().as_deref(), + ) +} + +fn resolve_streaming_emit_frames( + enabled: Option<&str>, + emit_frames: Option<&str>, +) -> Option { + (!matches!(enabled, Some("0" | "false"))) + .then(|| emit_frames.and_then(|v| v.parse().ok()).unwrap_or(12)) } /// Playback context threaded through one streamed chunk. @@ -129,6 +135,27 @@ fn drive_streaming( mod tests { use super::*; + #[test] + fn streaming_defaults_to_the_bit_exact_decoder_chunk() { + assert_eq!(resolve_streaming_emit_frames(None, None), Some(12)); + assert_eq!(resolve_streaming_emit_frames(Some("1"), None), Some(12)); + } + + #[test] + fn streaming_can_be_disabled_for_operational_rollback() { + assert_eq!(resolve_streaming_emit_frames(Some("0"), None), None); + assert_eq!(resolve_streaming_emit_frames(Some("false"), None), None); + } + + #[test] + fn explicit_emit_frames_remain_available_for_latency_benches() { + assert_eq!(resolve_streaming_emit_frames(None, Some("6")), Some(6)); + assert_eq!( + resolve_streaming_emit_frames(None, Some("invalid")), + Some(12) + ); + } + #[test] fn inference_failure_fades_the_retained_final_block() { let cancel = AtomicBool::new(false);