Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions crates/buzz-voice/src/pocket_april.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
8 changes: 4 additions & 4 deletions desktop/src-tauri/src/huddle/tts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
53 changes: 40 additions & 13 deletions desktop/src-tauri/src/huddle/tts_streaming.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<usize> {
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<usize> {
(!matches!(enabled, Some("0" | "false")))
.then(|| emit_frames.and_then(|v| v.parse().ok()).unwrap_or(12))
}

/// Playback context threaded through one streamed chunk.
Expand Down Expand Up @@ -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);
Expand Down
Loading