From c9dfe50a45ed1dc7a2e4c0c7141933fa4cf25d3f Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 18:55:01 -0400 Subject: [PATCH 01/27] refactor(desktop): extract reusable Pocket voice primitives Signed-off-by: John Tennant --- Cargo.lock | 66 ++ Cargo.toml | 1 + Justfile | 1 + crates/buzz-voice/Cargo.toml | 12 + crates/buzz-voice/src/lib.rs | 3 + crates/buzz-voice/src/pocket.rs | 654 +++++++++++++++++ desktop/src-tauri/Cargo.lock | 10 + desktop/src-tauri/Cargo.toml | 1 + .../src-tauri/examples/pocket_quality_ab.rs | 2 +- desktop/src-tauri/src/huddle/pocket.rs | 655 +----------------- scripts/run-tests.sh | 3 + 11 files changed, 754 insertions(+), 654 deletions(-) create mode 100644 crates/buzz-voice/Cargo.toml create mode 100644 crates/buzz-voice/src/lib.rs create mode 100644 crates/buzz-voice/src/pocket.rs diff --git a/Cargo.lock b/Cargo.lock index 3b60dc4579..5a57e203d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,6 +1262,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "buzz-voice" +version = "0.1.0" +dependencies = [ + "serde_json", + "sherpa-onnx", +] + [[package]] name = "buzz-workflow" version = "0.1.0" @@ -1329,6 +1337,26 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "castaway" version = "0.2.4" @@ -8049,6 +8077,28 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "sherpa-onnx" +version = "1.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b142d3f255cb4e4b7808ea25869db6f5714e0a3550da355234483b4db552055" +dependencies = [ + "serde", + "serde_json", + "sherpa-onnx-sys", +] + +[[package]] +name = "sherpa-onnx-sys" +version = "1.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc951af03dc0653c0622158ca8a585a6f2bc43b7b06048cf0e5b5020005c227" +dependencies = [ + "bzip2", + "tar", + "ureq", +] + [[package]] name = "shlex" version = "1.3.0" @@ -9539,6 +9589,22 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.8" diff --git a/Cargo.toml b/Cargo.toml index 3ac7ee4cce..3268cfaf8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "crates/buzz-pair-relay", "crates/buzz-relay-mesh", "crates/buzz-dev-mcp", + "crates/buzz-voice", "examples/countdown-bot", ] exclude = ["desktop/src-tauri"] diff --git a/Justfile b/Justfile index bcef8983bc..e5601607ba 100644 --- a/Justfile +++ b/Justfile @@ -276,6 +276,7 @@ test-unit: #!/usr/bin/env bash if command -v cargo-nextest &>/dev/null; then cargo nextest run -p buzz-core -p buzz-auth --lib + cargo nextest run -p buzz-voice --lib # buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra). # They guard the embedded-migrator invariant (exactly the consolidated # 0001; cutover/backfill stays an operator script, not startup state) diff --git a/crates/buzz-voice/Cargo.toml b/crates/buzz-voice/Cargo.toml new file mode 100644 index 0000000000..f577e3d1a6 --- /dev/null +++ b/crates/buzz-voice/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "buzz-voice" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Reusable local voice primitives for Buzz" + +[dependencies] +serde_json = { workspace = true } +sherpa-onnx = "1.12" diff --git a/crates/buzz-voice/src/lib.rs b/crates/buzz-voice/src/lib.rs new file mode 100644 index 0000000000..e2ce3f255b --- /dev/null +++ b/crates/buzz-voice/src/lib.rs @@ -0,0 +1,3 @@ +//! Reusable local voice primitives for Buzz. + +pub mod pocket; diff --git a/crates/buzz-voice/src/pocket.rs b/crates/buzz-voice/src/pocket.rs new file mode 100644 index 0000000000..ee1faf928a --- /dev/null +++ b/crates/buzz-voice/src/pocket.rs @@ -0,0 +1,654 @@ +//! Pocket TTS engine wrapper around sherpa-onnx's `OfflineTts`. +//! +//! Pocket TTS is a small (~473 MB fp32 ONNX) zero-shot voice-cloning TTS +//! model from Kyutai. It runs quickly on CPU via sherpa-onnx, replacing the +//! previous Kokoro-82M engine that also required an espeak-free but +//! lexicon-heavy G2P pipeline (Misaki + CMUdict). +//! +//! Full-precision fp32 sessions, not the ~189 MB int8 quantization we +//! originally shipped: a direct same-runtime A/B (k2-fsa/sherpa-onnx#3172) +//! found the int8 ONNX export audibly degraded output quality, and fp32 +//! "significantly improved quality even at 1 step". +//! +//! ## Attribution +//! +//! - **Model**: Kyutai *Pocket TTS* — Charles, Roebel, et al., 2026. +//! arXiv:2509.06926. Original repository: . +//! Licensed CC-BY-4.0. +//! - **Mimi neural codec**: Kyutai, bundled in the same release. CC-BY-4.0. +//! - **ONNX export**: KevinAHM — +//! . CC-BY-4.0. +//! - **sherpa-onnx repackage**: csukuangfj / k2-fsa — +//! . +//! Repackages KevinAHM's export with the file layout sherpa-onnx's +//! `OfflineTtsPocketModelConfig` expects. CC-BY-4.0. +//! - **Reference voice WAV** (`reference_sample.wav`): the "Mary +//! (f, conversation)" preset from the Kyutai TTS demo +//! (), which maps to `vctk/p333_023_enhanced.wav` +//! in . CC-BY-4.0, base recording +//! from the VCTK corpus, enhanced by ai-coustics. +//! +//! Buzz ships these files unmodified; see the on-disk `MODEL_LICENSE.txt` +//! sidecar written by `huddle::models` during install for the canonical +//! CC-BY-4.0 §3(a)(1) attribution block. +//! +//! ## Engine-module contract (see `huddle::tts`) +//! +//! `pocket.rs` exposes a fixed surface used by `tts.rs`. Mirroring this +//! contract is what lets the TTS pipeline stay engine-agnostic: +//! +//! - `SAMPLE_RATE: u32` — engine output sample rate in Hz. +//! - `DEFAULT_VOICE: &str` — default voice name (without extension). +//! - `VOICE_FILE_EXT: &str` — extension for per-voice files on disk. +//! - `load_text_to_speech(model_dir)` → `Result` +//! - `load_voice_style(path)` → `Result` +//! - `Engine::synth_chunk(&self, text, lang, &VoiceStyle, steps)` +//! → `Result, String>` +//! +//! `lang` and `steps` are accepted for API compatibility with the previous +//! Kokoro engine but are unused — Pocket TTS does its own language ID from +//! the input text and is not a diffusion model (consistency LM, one step). +//! There is no speed knob: sherpa-onnx's `GenerationConfig.speed` is only +//! read by some model families (vits), never by the Pocket impl +//! (`offline-tts-pocket-impl.h` — zero references), and upstream pocket-tts +//! has no speed parameter either. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use sherpa_onnx::{GenerationConfig, OfflineTts, OfflineTtsConfig, Wave}; + +// ── Engine-module contract: public consts ───────────────────────────────────── + +/// Pocket TTS emits 24 kHz mono PCM. Matches the previous Kokoro output rate, +/// so the rodio sink and inter-sentence silence buffer in `tts.rs` remain valid. +pub const SAMPLE_RATE: u32 = 24_000; + +/// Name (without extension) of the bundled reference voice. The model directory +/// is expected to contain `.` after install. +pub const DEFAULT_VOICE: &str = "reference_sample"; + +/// Voice files for Pocket TTS are reference audio (WAV). Distinct from the +/// Kokoro `.bin` style vectors — the model conditions on raw waveform samples, +/// not a precomputed embedding, so the extension change is honest. +pub const VOICE_FILE_EXT: &str = "wav"; + +// ── Tuning ──────────────────────────────────────────────────────────────────── + +/// Single-threaded ONNX execution for predictable CPU contention with the STT +/// pipeline. Matches `STT_NUM_THREADS` in `stt.rs`; raise only if a benchmark +/// argues for it. +const TTS_NUM_THREADS: i32 = 1; + +/// LRU cache size for cloned voice embeddings inside the sherpa-onnx engine. +/// We bind to one voice per pipeline today, but the upstream example uses 16 +/// and the cost is negligible — keep room for future multi-voice support. +const VOICE_EMBEDDING_CACHE_CAPACITY: i32 = 16; + +/// Pocket TTS is a consistency-based LM. Generation quality saturates at one +/// denoising step — the upstream `GenerationConfig` default of 5 multiplies +/// synthesis time by ~5× with no audible benefit on this model. +const SYNTH_NUM_STEPS: i32 = 1; + +/// Leave the generated audio's silences untouched (1.0 is the identity). +/// +/// sherpa-onnx's `ScaleSilence` (`offline-tts.cc`) is *not* pre/post padding +/// control: it finds every interior silence run ≥ 0.2 s (|s| ≤ 0.01) and +/// multiplies its length by this factor. The previous value of 0.0 — set +/// under the mistaken belief it disabled lead-in/lead-out padding — deleted +/// every natural pause inside an utterance: clause breaks, breaths, the gap +/// after a comma. Words slammed together and endings cut abruptly. The +/// reference Pocket TTS pipeline does not post-process silence at all; +/// 1.0 restores parity. +const SYNTH_SILENCE_SCALE: f32 = 1.0; + +/// sherpa-onnx upstream default for `max_frames` (LM steps), in +/// `offline-tts-pocket-impl.h:Generate`. 500 steps ≈ 40 s of audio at the +/// Mimi 12.5 Hz frame rate. Referenced only by the regression test below; +/// production code path never raises (or even reads) this value — we just +/// leave sherpa-onnx's own default in place by not setting the override. +#[cfg(test)] +const SHERPA_ONNX_MAX_FRAMES_DEFAULT: i32 = 500; + +/// Tight `max_frames` we ask for on short, padded prompts to bound the +/// original "monster breathing" runaway. 100 LM steps ≈ 8 s of audio — +/// roomy for any one-to-four-word utterance the user is likely to elicit +/// while still well short of the 40 s upstream default. Chosen with slack so +/// we never *truncate* a legitimate short reply. +const SHORT_PROMPT_MAX_FRAMES: i32 = 100; + +/// Word-count threshold (inclusive) below which we pad the prompt with +/// leading spaces and cap `max_frames` tighter than the upstream default. +/// Matches upstream `pocket_tts.models.tts_model.prepare_text_prompt`. Above +/// this threshold we leave sherpa-onnx's own defaults in place — overriding +/// them caused the "first 'yep' is just static" regression seen on +/// 2026-05-18, where dropping `frames_after_eos` below the upstream default +/// of 3 clipped the leading audio of multi-clause sentences. +const SHORT_PROMPT_WORD_THRESHOLD: usize = 4; + +/// Number of leading spaces prepended to short prompts. The upstream Python +/// uses exactly 8 — keep parity rather than tuning blindly. +/// +/// This is upstream's *only* mitigation for the FlowLM cold-start smear on +/// short utterances (kyutai-labs/pocket-tts #91, #70): the autoregressive +/// generation has a 2–3 step "settle" period where the first phoneme can be +/// smeared. A previous revision added a sacrificial `". . "` prefix plus an +/// amplitude-threshold trim to strip the rendered prefix from the output — +/// but the trim's absolute threshold (0.02 against raw peaks of ~0.076) sat +/// in soft-onset territory and could eat real word starts, and its tuning +/// was calibrated against `silence_scale = 0.0` audio. Deleted in favour of +/// upstream parity: accept the occasional smeared first syllable rather +/// than risk trimming real speech. +const SHORT_PROMPT_PAD_SPACES: usize = 8; + +/// sherpa-onnx's documented `frames_after_eos` default. We deliberately do +/// *not* override this knob — the previous attempt to bump it for short +/// inputs and lower it for long inputs lowered it below the upstream default +/// of 3, which clipped the leading audio of multi-clause sentences (the +/// "first 'yep' is static" regression). The constant exists only for the +/// regression test below. Source: `offline-tts-pocket-impl.h:Generate`. +#[cfg(test)] +const SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT: i32 = 3; + +// ── ONNX file names (five Pocket TTS sessions plus two JSON tables) ─────────── + +const FILE_LM_MAIN: &str = "lm_main.onnx"; +const FILE_LM_FLOW: &str = "lm_flow.onnx"; +const FILE_ENCODER: &str = "encoder.onnx"; +const FILE_DECODER: &str = "decoder.onnx"; +const FILE_TEXT_COND: &str = "text_conditioner.onnx"; +const FILE_VOCAB: &str = "vocab.json"; +const FILE_TOKEN_SCORES: &str = "token_scores.json"; + +// ── Voice style ─────────────────────────────────────────────────────────────── + +/// Loaded reference voice — normalised f32 PCM samples plus their sample rate. +/// +/// Pocket TTS takes a reference waveform per generation call (not a +/// precomputed style embedding), so we keep the samples in memory and clone +/// the small `Vec` into each `GenerationConfig` rather than re-reading the +/// WAV from disk on every sentence. +#[derive(Debug, Clone)] +pub struct VoiceStyle { + samples: Vec, + sample_rate: i32, +} + +/// Load a reference voice WAV from disk. +/// +/// Accepts any sample rate sherpa-onnx's `Wave::read` can decode — Pocket TTS +/// resamples internally using `reference_sample_rate`. The bundled +/// `reference_sample.wav` ("Mary" — VCTK p333, enhanced) is 32 kHz mono. +pub fn load_voice_style(path: &Path) -> Result { + let path_str = path + .to_str() + .ok_or_else(|| format!("voice path is not valid UTF-8: {}", path.display()))?; + let wave = Wave::read(path_str) + .ok_or_else(|| format!("could not read voice WAV at {}", path.display()))?; + let samples = wave.samples().to_vec(); + if samples.is_empty() { + return Err(format!("voice WAV is empty: {}", path.display())); + } + Ok(VoiceStyle { + samples, + sample_rate: wave.sample_rate(), + }) +} + +// ── Engine ──────────────────────────────────────────────────────────────────── + +/// Pocket TTS engine handle. Cheap to construct (one `OfflineTts::create` +/// call). Owned by the TTS worker thread for the lifetime of a huddle session. +/// +/// `OfflineTts` does not implement `Debug`, so we don't derive it here — the +/// pipeline only needs to move the engine into the worker thread and call +/// `synth_chunk` on it, never to print it. +pub struct PocketTts { + inner: OfflineTts, +} + +/// Build the Pocket TTS engine from the model directory installed by +/// `huddle::models`. Returns `Err` if any expected ONNX or JSON file is +/// missing — readiness is normally enforced by `is_tts_ready` upstream, but +/// the check is repeated here so a manually-modified model dir produces a +/// clear error string instead of an opaque sherpa-onnx `None`. +pub fn load_text_to_speech(model_dir: &str) -> Result { + let dir = PathBuf::from(model_dir); + for name in [ + FILE_LM_MAIN, + FILE_LM_FLOW, + FILE_ENCODER, + FILE_DECODER, + FILE_TEXT_COND, + FILE_VOCAB, + FILE_TOKEN_SCORES, + ] { + let p = dir.join(name); + if !p.is_file() { + return Err(format!("missing Pocket TTS file: {}", p.display())); + } + } + + let to_str = |name: &str| -> String { dir.join(name).to_string_lossy().into_owned() }; + + // Build the config by mutating defaults — mirrors `stt.rs` and stays + // resilient if sherpa-onnx adds unrelated model-family fields. + let mut cfg = OfflineTtsConfig::default(); + cfg.model.pocket.lm_main = Some(to_str(FILE_LM_MAIN)); + cfg.model.pocket.lm_flow = Some(to_str(FILE_LM_FLOW)); + cfg.model.pocket.encoder = Some(to_str(FILE_ENCODER)); + cfg.model.pocket.decoder = Some(to_str(FILE_DECODER)); + cfg.model.pocket.text_conditioner = Some(to_str(FILE_TEXT_COND)); + cfg.model.pocket.vocab_json = Some(to_str(FILE_VOCAB)); + cfg.model.pocket.token_scores_json = Some(to_str(FILE_TOKEN_SCORES)); + cfg.model.pocket.voice_embedding_cache_capacity = VOICE_EMBEDDING_CACHE_CAPACITY; + cfg.model.num_threads = TTS_NUM_THREADS; + // Explicit — defaults are not part of the API contract, and noisy debug + // logging in release builds would be expensive on every synthesized chunk. + cfg.model.debug = false; + + let inner = OfflineTts::create(&cfg) + .ok_or_else(|| "OfflineTts::create returned None for Pocket TTS".to_string())?; + Ok(PocketTts { inner }) +} + +// ── Prompt preparation ──────────────────────────────────────────────────────── + +/// Result of [`prepare_pocket_prompt`]: a synthesizer-ready prompt plus the +/// per-call generation overrides derived from the original text. +/// +/// `None` for either override means "leave sherpa-onnx's documented default +/// in place". The pipeline only sets `max_frames` (and only for short +/// padded inputs) so it can bound the original "monster breathing" runaway +/// without disturbing the rest of the LM sampling envelope. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct PreparedPrompt { + /// Text to hand to `OfflineTts::generate_with_config`. Capitalized, + /// punctuation-terminated, and (for short inputs) left-padded with + /// spaces — upstream's mitigation for the FlowLM cold-start smear. + pub text: String, + /// Value to pass via `GenerationConfig.extra["max_frames"]`, or `None` to + /// keep the upstream default of 500 LM steps. We only override on short + /// padded prompts where we have a tight expectation on output length. + pub max_frames: Option, +} + +/// Mirror of the *text-preparation* half of upstream +/// `pocket_tts.models.tts_model.prepare_text_prompt`. Sherpa-onnx's C++ +/// Pocket TTS impl does not run these preparation steps, so short / +/// unpunctuated / lowercase inputs can trigger up to 40 s of runaway +/// generation when the EOS logit never crosses its threshold. We replicate +/// the upstream Python recipe here: +/// +/// 1. Collapse interior whitespace (already done by `preprocess_for_tts`, but +/// cheap to re-check after sentence splitting). +/// 2. Capitalize the first letter. +/// 3. Append `.` if the text doesn't end in punctuation. +/// 4. If fewer than five words, prepend `SHORT_PROMPT_PAD_SPACES` spaces +/// (upstream's cold-start mitigation — see the constant's docstring) and +/// return a tight [`SHORT_PROMPT_MAX_FRAMES`] cap so the LM can't run +/// away if EOS still doesn't fire. +/// +/// We do **not** override `frames_after_eos` — sherpa-onnx's default of 3 +/// is what we want. An earlier version set it to 1 on long inputs, which +/// clipped the leading audio of multi-clause sentences ("first 'yep' is +/// just static" regression). Tests `prepare_prompt_never_lowers_frames_…` +/// lock this in. +/// +/// Returns `None` only if the input is empty after trimming — caller should +/// skip synthesis in that case. +pub(crate) fn prepare_pocket_prompt(input: &str) -> Option { + let trimmed = input.trim(); + if trimmed.is_empty() { + return None; + } + + // Collapse stray double-spaces / embedded newlines that may slip past + // `preprocess_for_tts` when sentences are spliced back together. + let mut cleaned = String::with_capacity(trimmed.len()); + let mut last_was_space = false; + for ch in trimmed.chars() { + let is_ws = ch.is_whitespace(); + if is_ws { + if !last_was_space { + cleaned.push(' '); + } + last_was_space = true; + } else { + cleaned.push(ch); + last_was_space = false; + } + } + + // Capitalize first character. Uses `to_uppercase` (multi-codepoint safe). + let first = cleaned.chars().next().expect("cleaned non-empty above"); + if first.is_lowercase() { + let upper: String = first.to_uppercase().collect(); + let mut iter = cleaned.chars(); + iter.next(); + cleaned = upper + iter.as_str(); + } + + // Ensure terminal punctuation. Anything not in `.!?;:,` gets a period. + // The upstream Python only checks `isalnum` → period, but for our agent + // text we already may end in `!` `?` `.` etc. — treat any of those as OK. + let last = cleaned + .chars() + .next_back() + .expect("cleaned non-empty above"); + if !matches!(last, '.' | '!' | '?' | ';' | ':' | ',') { + cleaned.push('.'); + } + + // Word count of the *cleaned but not padded* text — padding is whitespace + // only and would just lie to the threshold check below. + let word_count = cleaned.split_whitespace().count(); + + let (final_text, max_frames) = if word_count <= SHORT_PROMPT_WORD_THRESHOLD { + let mut padded = String::with_capacity(cleaned.len() + SHORT_PROMPT_PAD_SPACES); + for _ in 0..SHORT_PROMPT_PAD_SPACES { + padded.push(' '); + } + padded.push_str(&cleaned); + (padded, Some(SHORT_PROMPT_MAX_FRAMES)) + } else { + // For everything ≥5 words, fall back to upstream defaults. Overriding + // these is what caused the "first 'yep' is static" regression — the + // upstream LM has been tuned for `frames_after_eos = 3` and + // `max_frames = 500`, and there's no clear win in second-guessing. + (cleaned, None) + }; + + Some(PreparedPrompt { + text: final_text, + max_frames, + }) +} + +/// Build the `GenerationConfig.extra` HashMap from a [`PreparedPrompt`]. +/// +/// Centralised so the regression test below can assert that we **never** +/// emit a `frames_after_eos` override — the previous attempt to override +/// that knob (setting it to 1 for ≥5-word inputs) clipped the leading +/// audio of multi-clause sentences (the "first 'yep' is static" bug on +/// 2026-05-18). The upstream sherpa-onnx default of 3 is what we want, and +/// the right way to keep it is to not set it at all. +fn build_generation_extra(prepared: &PreparedPrompt) -> Option> { + prepared.max_frames.map(|mf| { + let mut h: HashMap = HashMap::with_capacity(1); + h.insert("max_frames".to_string(), serde_json::Value::from(mf)); + h + }) +} + +impl PocketTts { + /// Synthesise `text` with the given reference voice. + /// + /// `_lang` and `_steps` are accepted for API compatibility with the + /// previous Kokoro engine. Pocket TTS infers language from the input text + /// directly and is a one-step consistency model. Returns an empty buffer + /// for whitespace-only input. + pub fn synth_chunk( + &self, + text: &str, + _lang: &str, + style: &VoiceStyle, + _steps: usize, + ) -> Result, String> { + // Mirror upstream pocket-tts prompt prep — without this short or + // unpunctuated inputs can cause the LM's EOS logit to never trip, + // producing up to 40 s of "monster breathing" garbage on the first + // utterance. See `prepare_pocket_prompt` for the full recipe. + let prepared = match prepare_pocket_prompt(text) { + Some(p) => p, + None => return Ok(Vec::new()), + }; + + // Per-call generation hints sherpa-onnx forwards to + // `offline-tts-pocket-impl.h`. We only override `max_frames`, and + // only for short padded prompts where we have a tight expectation + // on output length — that bounds the original runaway without + // disturbing the rest of the LM sampling envelope. See + // `prepare_pocket_prompt` docs for the regression history. + let extra = build_generation_extra(&prepared); + + let cfg = GenerationConfig { + num_steps: SYNTH_NUM_STEPS, + silence_scale: SYNTH_SILENCE_SCALE, + reference_audio: Some(style.samples.clone()), + reference_sample_rate: style.sample_rate, + extra, + // `speed` stays at its default: the Pocket impl never reads it + // (see the engine-contract note in the module docs). + ..Default::default() + }; + + // No progress callback — synthesis is fast enough that returning the + // whole buffer at once keeps the lookahead pipelining in `tts.rs` + // simple. `None:: bool>` pins the callback type for the + // `generate_with_config` generic parameter. + let audio = self + .inner + .generate_with_config(&prepared.text, &cfg, None:: bool>) + .ok_or_else(|| { + format!( + "Pocket TTS synthesis failed for text ({} chars)", + prepared.text.len() + ) + })?; + + let sample_rate = audio.sample_rate(); + if sample_rate != SAMPLE_RATE as i32 { + eprintln!( + "buzz-desktop: Pocket TTS returned unexpected sample rate {sample_rate}Hz \ + (expected {SAMPLE_RATE}Hz); playback speed may be wrong" + ); + } + + Ok(audio.samples().to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── prepare_pocket_prompt ──────────────────────────────────────────────── + + #[test] + fn prepare_prompt_returns_none_for_empty_input() { + assert!(prepare_pocket_prompt("").is_none()); + assert!(prepare_pocket_prompt(" ").is_none()); + assert!(prepare_pocket_prompt("\n\t ").is_none()); + } + + /// Helper: the exact leading sequence prepended to every short prompt — + /// 8 spaces of padding (upstream's cold-start mitigation). + /// Centralising this keeps the assertions readable. + fn short_prefix() -> String { + " ".repeat(SHORT_PROMPT_PAD_SPACES) + } + + #[test] + fn prepare_prompt_pads_and_capitalizes_one_word() { + // The "yep" case Tyler hit in production — bare lowercase one-word + // utterance with no punctuation. Must be padded with the short-prompt + // space pad, capitalized, terminated, with a tight `max_frames` cap + // to bound runaway gen. + let out = prepare_pocket_prompt("yep").expect("non-empty"); + assert_eq!(out.text, format!("{}Yep.", short_prefix())); + assert_eq!(out.max_frames, Some(SHORT_PROMPT_MAX_FRAMES)); + const { + assert!( + SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT, + "short cap must be tighter than the upstream default" + ); + } + } + + #[test] + fn prepare_prompt_preserves_existing_punctuation() { + let out = prepare_pocket_prompt("yes!").expect("non-empty"); + assert_eq!(out.text, format!("{}Yes!", short_prefix())); // exclamation kept + let out = prepare_pocket_prompt("really?").expect("non-empty"); + assert_eq!(out.text, format!("{}Really?", short_prefix())); + } + + #[test] + fn prepare_prompt_threshold_is_inclusive_at_four_words() { + // 4 words = short (padded + tight max_frames); 5 words = long + // (no padding, no overrides — upstream defaults stand). + let four = prepare_pocket_prompt("one two three four").expect("non-empty"); + assert_eq!( + four.text, + format!("{}One two three four.", short_prefix()), + "four-word input should get exactly the space pad" + ); + assert_eq!(four.max_frames, Some(SHORT_PROMPT_MAX_FRAMES)); + + let five = prepare_pocket_prompt("one two three four five").expect("non-empty"); + assert!( + !five.text.starts_with(' '), + "five-word input should NOT be padded" + ); + assert_eq!( + five.max_frames, None, + "long inputs must leave sherpa-onnx's max_frames default in place" + ); + } + + #[test] + fn prepare_prompt_does_not_pad_long_text() { + let long = "This is a longer sentence that the model should handle just fine."; + let out = prepare_pocket_prompt(long).expect("non-empty"); + assert!(!out.text.starts_with(' ')); + assert_eq!(out.max_frames, None); + assert!(out.text.ends_with('.')); + } + + #[test] + fn prepare_prompt_collapses_whitespace() { + let out = prepare_pocket_prompt("Hello world\n\nfriend").expect("non-empty"); + // 3 words → short → padded. Interior whitespace collapsed. + assert_eq!(out.text, format!("{}Hello world friend.", short_prefix())); + } + + #[test] + fn prepare_prompt_does_not_double_capitalize_already_uppercase() { + let out = prepare_pocket_prompt("HELLO there").expect("non-empty"); + assert_eq!(out.text, format!("{}HELLO there.", short_prefix())); + } + + #[test] + fn prepare_prompt_handles_non_ascii_first_letter() { + // Cyrillic lowercase 'д' → uppercase 'Д'. Must not panic / produce + // mojibake. + let out = prepare_pocket_prompt("дa").expect("non-empty"); + assert!(out.text.contains("Дa.")); + } + + /// REGRESSION GUARD: short prompts must receive *only* whitespace + /// padding — no sacrificial text. A previous revision prepended a + /// `". . "` cold-start absorber and trimmed the rendered audio back out + /// with an amplitude threshold that could eat soft word onsets. If + /// non-whitespace ever reappears in the pad, the synth output will + /// contain audio for text the user never wrote. + #[test] + fn prepare_prompt_pad_is_whitespace_only() { + let out = prepare_pocket_prompt("I'm happy.").expect("non-empty"); + let pad_len = out.text.len() - "I'm happy.".len(); + assert!( + out.text[..pad_len].chars().all(|c| c == ' '), + "short-prompt pad must be spaces only, got {:?}", + &out.text[..pad_len] + ); + assert_eq!(out.text, format!("{}I'm happy.", short_prefix())); + } + + // ── build_generation_extra ─────────────────────────────────────────────── + // + // These tests pin down a behaviour we've now regressed twice on: + // 1) Not padding/punctuating short inputs → 40 s of "monster breathing" + // (pre-773a2a1). + // 2) Setting `frames_after_eos = 1` on long inputs → clipped leading + // audio of multi-clause sentences, e.g. "Yep, I can hear you. …" + // came out as a static burst (the 773a2a1 regression Tyler hit on + // 2026-05-18 ~14:30 UTC). + // + // The contract we enforce going forward: we **only** override + // `max_frames`, and only for ≤4-word inputs. Every other knob is left + // at sherpa-onnx's documented default (notably `frames_after_eos = 3`). + + #[test] + fn build_extra_short_prompt_sets_only_max_frames() { + let prepared = prepare_pocket_prompt("yep").expect("non-empty"); + let extra = build_generation_extra(&prepared).expect("short prompts get extra"); + // Exactly one key — `max_frames` — and nothing else. + assert_eq!(extra.len(), 1, "extra has unexpected keys: {extra:?}"); + assert_eq!( + extra.get("max_frames"), + Some(&serde_json::Value::from(SHORT_PROMPT_MAX_FRAMES)) + ); + assert!( + !extra.contains_key("frames_after_eos"), + "frames_after_eos must never be set — upstream default of {SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT} is what we want" + ); + } + + #[test] + fn build_extra_long_prompt_is_none() { + // ≥5 words: no extras at all. This is the key fix for the "first + // 'yep' in 'Yep, I can hear you. …' is static" regression — we + // were previously forcing `frames_after_eos = 1` on this path. + let prepared = prepare_pocket_prompt("Yep, I can hear you.").expect("non-empty"); + assert_eq!( + build_generation_extra(&prepared), + None, + "long prompts must not override any LM knob" + ); + } + + #[test] + fn build_extra_never_lowers_frames_after_eos_for_any_word_count() { + // Sweep a range of prompt lengths and assert the `extra` map (when + // present) never carries a `frames_after_eos` override that's lower + // than the upstream sherpa-onnx default. Implemented as a structural + // check — we just never set the key — but worth a property test in + // case someone reintroduces the override in the future. + let prompts: &[&str] = &[ + "hi", + "hi there", + "yes please", + "one two three four", + "one two three four five", + "a slightly longer reply, hopefully fine", + "This is a multi-clause sentence. It has two parts.", + "really really really really really long prompt with lots of words just to be sure", + ]; + for &p in prompts { + let prepared = prepare_pocket_prompt(p).expect("non-empty"); + if let Some(extra) = build_generation_extra(&prepared) { + if let Some(v) = extra.get("frames_after_eos") { + let n = v.as_i64().expect("frames_after_eos should be int"); + assert!( + n >= SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT as i64, + "prompt {p:?} set frames_after_eos={n}, below upstream default of {SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT}" + ); + } + } + } + } + + #[test] + fn short_prompt_max_frames_is_below_upstream_default() { + // Sanity: the override only ever *lowers* the cap, never raises it. + const { + assert!(SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT); + } + // …and is still large enough for a one-to-four-word reply. At Mimi's + // 12.5 Hz frame rate, 100 frames = 8 s, which is roomy. + const { + assert!(SHORT_PROMPT_MAX_FRAMES >= 50, "would risk truncation"); + } + } +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 7dcf615c00..17421c6447 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -991,6 +991,7 @@ dependencies = [ name = "buzz-core" version = "0.1.0" dependencies = [ + "base64 0.22.1", "chrono", "hex", "hmac 0.13.0", @@ -1022,6 +1023,7 @@ dependencies = [ "buzz-media", "buzz-persona", "buzz-sdk", + "buzz-voice", "bytes", "bzip2 0.6.1", "chrono", @@ -1142,6 +1144,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "buzz-voice" +version = "0.1.0" +dependencies = [ + "serde_json", + "sherpa-onnx", +] + [[package]] name = "by_address" version = "1.2.1" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d689544688..d3c52235fd 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -89,6 +89,7 @@ buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" } buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" } buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" } buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" } +buzz_voice_pkg = { package = "buzz-voice", path = "../../crates/buzz-voice" } iroh = { version = "1.0.2", optional = true } mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true } mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f455d493a2ae82baf2a326e2d0fda351433b4b30", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true } diff --git a/desktop/src-tauri/examples/pocket_quality_ab.rs b/desktop/src-tauri/examples/pocket_quality_ab.rs index 0c31f1c910..95f2e4815f 100644 --- a/desktop/src-tauri/examples/pocket_quality_ab.rs +++ b/desktop/src-tauri/examples/pocket_quality_ab.rs @@ -19,7 +19,7 @@ // standalone corpus generator deliberately does not call. #![allow(dead_code)] -#[path = "../src/huddle/pocket.rs"] +#[path = "../../../crates/buzz-voice/src/pocket.rs"] mod production_pocket; #[path = "../src/huddle/preprocessing.rs"] mod production_preprocessing; diff --git a/desktop/src-tauri/src/huddle/pocket.rs b/desktop/src-tauri/src/huddle/pocket.rs index ee1faf928a..8e19781eeb 100644 --- a/desktop/src-tauri/src/huddle/pocket.rs +++ b/desktop/src-tauri/src/huddle/pocket.rs @@ -1,654 +1,3 @@ -//! Pocket TTS engine wrapper around sherpa-onnx's `OfflineTts`. -//! -//! Pocket TTS is a small (~473 MB fp32 ONNX) zero-shot voice-cloning TTS -//! model from Kyutai. It runs quickly on CPU via sherpa-onnx, replacing the -//! previous Kokoro-82M engine that also required an espeak-free but -//! lexicon-heavy G2P pipeline (Misaki + CMUdict). -//! -//! Full-precision fp32 sessions, not the ~189 MB int8 quantization we -//! originally shipped: a direct same-runtime A/B (k2-fsa/sherpa-onnx#3172) -//! found the int8 ONNX export audibly degraded output quality, and fp32 -//! "significantly improved quality even at 1 step". -//! -//! ## Attribution -//! -//! - **Model**: Kyutai *Pocket TTS* — Charles, Roebel, et al., 2026. -//! arXiv:2509.06926. Original repository: . -//! Licensed CC-BY-4.0. -//! - **Mimi neural codec**: Kyutai, bundled in the same release. CC-BY-4.0. -//! - **ONNX export**: KevinAHM — -//! . CC-BY-4.0. -//! - **sherpa-onnx repackage**: csukuangfj / k2-fsa — -//! . -//! Repackages KevinAHM's export with the file layout sherpa-onnx's -//! `OfflineTtsPocketModelConfig` expects. CC-BY-4.0. -//! - **Reference voice WAV** (`reference_sample.wav`): the "Mary -//! (f, conversation)" preset from the Kyutai TTS demo -//! (), which maps to `vctk/p333_023_enhanced.wav` -//! in . CC-BY-4.0, base recording -//! from the VCTK corpus, enhanced by ai-coustics. -//! -//! Buzz ships these files unmodified; see the on-disk `MODEL_LICENSE.txt` -//! sidecar written by `huddle::models` during install for the canonical -//! CC-BY-4.0 §3(a)(1) attribution block. -//! -//! ## Engine-module contract (see `huddle::tts`) -//! -//! `pocket.rs` exposes a fixed surface used by `tts.rs`. Mirroring this -//! contract is what lets the TTS pipeline stay engine-agnostic: -//! -//! - `SAMPLE_RATE: u32` — engine output sample rate in Hz. -//! - `DEFAULT_VOICE: &str` — default voice name (without extension). -//! - `VOICE_FILE_EXT: &str` — extension for per-voice files on disk. -//! - `load_text_to_speech(model_dir)` → `Result` -//! - `load_voice_style(path)` → `Result` -//! - `Engine::synth_chunk(&self, text, lang, &VoiceStyle, steps)` -//! → `Result, String>` -//! -//! `lang` and `steps` are accepted for API compatibility with the previous -//! Kokoro engine but are unused — Pocket TTS does its own language ID from -//! the input text and is not a diffusion model (consistency LM, one step). -//! There is no speed knob: sherpa-onnx's `GenerationConfig.speed` is only -//! read by some model families (vits), never by the Pocket impl -//! (`offline-tts-pocket-impl.h` — zero references), and upstream pocket-tts -//! has no speed parameter either. +//! Compatibility re-export for the shared Pocket TTS engine. -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -use sherpa_onnx::{GenerationConfig, OfflineTts, OfflineTtsConfig, Wave}; - -// ── Engine-module contract: public consts ───────────────────────────────────── - -/// Pocket TTS emits 24 kHz mono PCM. Matches the previous Kokoro output rate, -/// so the rodio sink and inter-sentence silence buffer in `tts.rs` remain valid. -pub const SAMPLE_RATE: u32 = 24_000; - -/// Name (without extension) of the bundled reference voice. The model directory -/// is expected to contain `.` after install. -pub const DEFAULT_VOICE: &str = "reference_sample"; - -/// Voice files for Pocket TTS are reference audio (WAV). Distinct from the -/// Kokoro `.bin` style vectors — the model conditions on raw waveform samples, -/// not a precomputed embedding, so the extension change is honest. -pub const VOICE_FILE_EXT: &str = "wav"; - -// ── Tuning ──────────────────────────────────────────────────────────────────── - -/// Single-threaded ONNX execution for predictable CPU contention with the STT -/// pipeline. Matches `STT_NUM_THREADS` in `stt.rs`; raise only if a benchmark -/// argues for it. -const TTS_NUM_THREADS: i32 = 1; - -/// LRU cache size for cloned voice embeddings inside the sherpa-onnx engine. -/// We bind to one voice per pipeline today, but the upstream example uses 16 -/// and the cost is negligible — keep room for future multi-voice support. -const VOICE_EMBEDDING_CACHE_CAPACITY: i32 = 16; - -/// Pocket TTS is a consistency-based LM. Generation quality saturates at one -/// denoising step — the upstream `GenerationConfig` default of 5 multiplies -/// synthesis time by ~5× with no audible benefit on this model. -const SYNTH_NUM_STEPS: i32 = 1; - -/// Leave the generated audio's silences untouched (1.0 is the identity). -/// -/// sherpa-onnx's `ScaleSilence` (`offline-tts.cc`) is *not* pre/post padding -/// control: it finds every interior silence run ≥ 0.2 s (|s| ≤ 0.01) and -/// multiplies its length by this factor. The previous value of 0.0 — set -/// under the mistaken belief it disabled lead-in/lead-out padding — deleted -/// every natural pause inside an utterance: clause breaks, breaths, the gap -/// after a comma. Words slammed together and endings cut abruptly. The -/// reference Pocket TTS pipeline does not post-process silence at all; -/// 1.0 restores parity. -const SYNTH_SILENCE_SCALE: f32 = 1.0; - -/// sherpa-onnx upstream default for `max_frames` (LM steps), in -/// `offline-tts-pocket-impl.h:Generate`. 500 steps ≈ 40 s of audio at the -/// Mimi 12.5 Hz frame rate. Referenced only by the regression test below; -/// production code path never raises (or even reads) this value — we just -/// leave sherpa-onnx's own default in place by not setting the override. -#[cfg(test)] -const SHERPA_ONNX_MAX_FRAMES_DEFAULT: i32 = 500; - -/// Tight `max_frames` we ask for on short, padded prompts to bound the -/// original "monster breathing" runaway. 100 LM steps ≈ 8 s of audio — -/// roomy for any one-to-four-word utterance the user is likely to elicit -/// while still well short of the 40 s upstream default. Chosen with slack so -/// we never *truncate* a legitimate short reply. -const SHORT_PROMPT_MAX_FRAMES: i32 = 100; - -/// Word-count threshold (inclusive) below which we pad the prompt with -/// leading spaces and cap `max_frames` tighter than the upstream default. -/// Matches upstream `pocket_tts.models.tts_model.prepare_text_prompt`. Above -/// this threshold we leave sherpa-onnx's own defaults in place — overriding -/// them caused the "first 'yep' is just static" regression seen on -/// 2026-05-18, where dropping `frames_after_eos` below the upstream default -/// of 3 clipped the leading audio of multi-clause sentences. -const SHORT_PROMPT_WORD_THRESHOLD: usize = 4; - -/// Number of leading spaces prepended to short prompts. The upstream Python -/// uses exactly 8 — keep parity rather than tuning blindly. -/// -/// This is upstream's *only* mitigation for the FlowLM cold-start smear on -/// short utterances (kyutai-labs/pocket-tts #91, #70): the autoregressive -/// generation has a 2–3 step "settle" period where the first phoneme can be -/// smeared. A previous revision added a sacrificial `". . "` prefix plus an -/// amplitude-threshold trim to strip the rendered prefix from the output — -/// but the trim's absolute threshold (0.02 against raw peaks of ~0.076) sat -/// in soft-onset territory and could eat real word starts, and its tuning -/// was calibrated against `silence_scale = 0.0` audio. Deleted in favour of -/// upstream parity: accept the occasional smeared first syllable rather -/// than risk trimming real speech. -const SHORT_PROMPT_PAD_SPACES: usize = 8; - -/// sherpa-onnx's documented `frames_after_eos` default. We deliberately do -/// *not* override this knob — the previous attempt to bump it for short -/// inputs and lower it for long inputs lowered it below the upstream default -/// of 3, which clipped the leading audio of multi-clause sentences (the -/// "first 'yep' is static" regression). The constant exists only for the -/// regression test below. Source: `offline-tts-pocket-impl.h:Generate`. -#[cfg(test)] -const SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT: i32 = 3; - -// ── ONNX file names (five Pocket TTS sessions plus two JSON tables) ─────────── - -const FILE_LM_MAIN: &str = "lm_main.onnx"; -const FILE_LM_FLOW: &str = "lm_flow.onnx"; -const FILE_ENCODER: &str = "encoder.onnx"; -const FILE_DECODER: &str = "decoder.onnx"; -const FILE_TEXT_COND: &str = "text_conditioner.onnx"; -const FILE_VOCAB: &str = "vocab.json"; -const FILE_TOKEN_SCORES: &str = "token_scores.json"; - -// ── Voice style ─────────────────────────────────────────────────────────────── - -/// Loaded reference voice — normalised f32 PCM samples plus their sample rate. -/// -/// Pocket TTS takes a reference waveform per generation call (not a -/// precomputed style embedding), so we keep the samples in memory and clone -/// the small `Vec` into each `GenerationConfig` rather than re-reading the -/// WAV from disk on every sentence. -#[derive(Debug, Clone)] -pub struct VoiceStyle { - samples: Vec, - sample_rate: i32, -} - -/// Load a reference voice WAV from disk. -/// -/// Accepts any sample rate sherpa-onnx's `Wave::read` can decode — Pocket TTS -/// resamples internally using `reference_sample_rate`. The bundled -/// `reference_sample.wav` ("Mary" — VCTK p333, enhanced) is 32 kHz mono. -pub fn load_voice_style(path: &Path) -> Result { - let path_str = path - .to_str() - .ok_or_else(|| format!("voice path is not valid UTF-8: {}", path.display()))?; - let wave = Wave::read(path_str) - .ok_or_else(|| format!("could not read voice WAV at {}", path.display()))?; - let samples = wave.samples().to_vec(); - if samples.is_empty() { - return Err(format!("voice WAV is empty: {}", path.display())); - } - Ok(VoiceStyle { - samples, - sample_rate: wave.sample_rate(), - }) -} - -// ── Engine ──────────────────────────────────────────────────────────────────── - -/// Pocket TTS engine handle. Cheap to construct (one `OfflineTts::create` -/// call). Owned by the TTS worker thread for the lifetime of a huddle session. -/// -/// `OfflineTts` does not implement `Debug`, so we don't derive it here — the -/// pipeline only needs to move the engine into the worker thread and call -/// `synth_chunk` on it, never to print it. -pub struct PocketTts { - inner: OfflineTts, -} - -/// Build the Pocket TTS engine from the model directory installed by -/// `huddle::models`. Returns `Err` if any expected ONNX or JSON file is -/// missing — readiness is normally enforced by `is_tts_ready` upstream, but -/// the check is repeated here so a manually-modified model dir produces a -/// clear error string instead of an opaque sherpa-onnx `None`. -pub fn load_text_to_speech(model_dir: &str) -> Result { - let dir = PathBuf::from(model_dir); - for name in [ - FILE_LM_MAIN, - FILE_LM_FLOW, - FILE_ENCODER, - FILE_DECODER, - FILE_TEXT_COND, - FILE_VOCAB, - FILE_TOKEN_SCORES, - ] { - let p = dir.join(name); - if !p.is_file() { - return Err(format!("missing Pocket TTS file: {}", p.display())); - } - } - - let to_str = |name: &str| -> String { dir.join(name).to_string_lossy().into_owned() }; - - // Build the config by mutating defaults — mirrors `stt.rs` and stays - // resilient if sherpa-onnx adds unrelated model-family fields. - let mut cfg = OfflineTtsConfig::default(); - cfg.model.pocket.lm_main = Some(to_str(FILE_LM_MAIN)); - cfg.model.pocket.lm_flow = Some(to_str(FILE_LM_FLOW)); - cfg.model.pocket.encoder = Some(to_str(FILE_ENCODER)); - cfg.model.pocket.decoder = Some(to_str(FILE_DECODER)); - cfg.model.pocket.text_conditioner = Some(to_str(FILE_TEXT_COND)); - cfg.model.pocket.vocab_json = Some(to_str(FILE_VOCAB)); - cfg.model.pocket.token_scores_json = Some(to_str(FILE_TOKEN_SCORES)); - cfg.model.pocket.voice_embedding_cache_capacity = VOICE_EMBEDDING_CACHE_CAPACITY; - cfg.model.num_threads = TTS_NUM_THREADS; - // Explicit — defaults are not part of the API contract, and noisy debug - // logging in release builds would be expensive on every synthesized chunk. - cfg.model.debug = false; - - let inner = OfflineTts::create(&cfg) - .ok_or_else(|| "OfflineTts::create returned None for Pocket TTS".to_string())?; - Ok(PocketTts { inner }) -} - -// ── Prompt preparation ──────────────────────────────────────────────────────── - -/// Result of [`prepare_pocket_prompt`]: a synthesizer-ready prompt plus the -/// per-call generation overrides derived from the original text. -/// -/// `None` for either override means "leave sherpa-onnx's documented default -/// in place". The pipeline only sets `max_frames` (and only for short -/// padded inputs) so it can bound the original "monster breathing" runaway -/// without disturbing the rest of the LM sampling envelope. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct PreparedPrompt { - /// Text to hand to `OfflineTts::generate_with_config`. Capitalized, - /// punctuation-terminated, and (for short inputs) left-padded with - /// spaces — upstream's mitigation for the FlowLM cold-start smear. - pub text: String, - /// Value to pass via `GenerationConfig.extra["max_frames"]`, or `None` to - /// keep the upstream default of 500 LM steps. We only override on short - /// padded prompts where we have a tight expectation on output length. - pub max_frames: Option, -} - -/// Mirror of the *text-preparation* half of upstream -/// `pocket_tts.models.tts_model.prepare_text_prompt`. Sherpa-onnx's C++ -/// Pocket TTS impl does not run these preparation steps, so short / -/// unpunctuated / lowercase inputs can trigger up to 40 s of runaway -/// generation when the EOS logit never crosses its threshold. We replicate -/// the upstream Python recipe here: -/// -/// 1. Collapse interior whitespace (already done by `preprocess_for_tts`, but -/// cheap to re-check after sentence splitting). -/// 2. Capitalize the first letter. -/// 3. Append `.` if the text doesn't end in punctuation. -/// 4. If fewer than five words, prepend `SHORT_PROMPT_PAD_SPACES` spaces -/// (upstream's cold-start mitigation — see the constant's docstring) and -/// return a tight [`SHORT_PROMPT_MAX_FRAMES`] cap so the LM can't run -/// away if EOS still doesn't fire. -/// -/// We do **not** override `frames_after_eos` — sherpa-onnx's default of 3 -/// is what we want. An earlier version set it to 1 on long inputs, which -/// clipped the leading audio of multi-clause sentences ("first 'yep' is -/// just static" regression). Tests `prepare_prompt_never_lowers_frames_…` -/// lock this in. -/// -/// Returns `None` only if the input is empty after trimming — caller should -/// skip synthesis in that case. -pub(crate) fn prepare_pocket_prompt(input: &str) -> Option { - let trimmed = input.trim(); - if trimmed.is_empty() { - return None; - } - - // Collapse stray double-spaces / embedded newlines that may slip past - // `preprocess_for_tts` when sentences are spliced back together. - let mut cleaned = String::with_capacity(trimmed.len()); - let mut last_was_space = false; - for ch in trimmed.chars() { - let is_ws = ch.is_whitespace(); - if is_ws { - if !last_was_space { - cleaned.push(' '); - } - last_was_space = true; - } else { - cleaned.push(ch); - last_was_space = false; - } - } - - // Capitalize first character. Uses `to_uppercase` (multi-codepoint safe). - let first = cleaned.chars().next().expect("cleaned non-empty above"); - if first.is_lowercase() { - let upper: String = first.to_uppercase().collect(); - let mut iter = cleaned.chars(); - iter.next(); - cleaned = upper + iter.as_str(); - } - - // Ensure terminal punctuation. Anything not in `.!?;:,` gets a period. - // The upstream Python only checks `isalnum` → period, but for our agent - // text we already may end in `!` `?` `.` etc. — treat any of those as OK. - let last = cleaned - .chars() - .next_back() - .expect("cleaned non-empty above"); - if !matches!(last, '.' | '!' | '?' | ';' | ':' | ',') { - cleaned.push('.'); - } - - // Word count of the *cleaned but not padded* text — padding is whitespace - // only and would just lie to the threshold check below. - let word_count = cleaned.split_whitespace().count(); - - let (final_text, max_frames) = if word_count <= SHORT_PROMPT_WORD_THRESHOLD { - let mut padded = String::with_capacity(cleaned.len() + SHORT_PROMPT_PAD_SPACES); - for _ in 0..SHORT_PROMPT_PAD_SPACES { - padded.push(' '); - } - padded.push_str(&cleaned); - (padded, Some(SHORT_PROMPT_MAX_FRAMES)) - } else { - // For everything ≥5 words, fall back to upstream defaults. Overriding - // these is what caused the "first 'yep' is static" regression — the - // upstream LM has been tuned for `frames_after_eos = 3` and - // `max_frames = 500`, and there's no clear win in second-guessing. - (cleaned, None) - }; - - Some(PreparedPrompt { - text: final_text, - max_frames, - }) -} - -/// Build the `GenerationConfig.extra` HashMap from a [`PreparedPrompt`]. -/// -/// Centralised so the regression test below can assert that we **never** -/// emit a `frames_after_eos` override — the previous attempt to override -/// that knob (setting it to 1 for ≥5-word inputs) clipped the leading -/// audio of multi-clause sentences (the "first 'yep' is static" bug on -/// 2026-05-18). The upstream sherpa-onnx default of 3 is what we want, and -/// the right way to keep it is to not set it at all. -fn build_generation_extra(prepared: &PreparedPrompt) -> Option> { - prepared.max_frames.map(|mf| { - let mut h: HashMap = HashMap::with_capacity(1); - h.insert("max_frames".to_string(), serde_json::Value::from(mf)); - h - }) -} - -impl PocketTts { - /// Synthesise `text` with the given reference voice. - /// - /// `_lang` and `_steps` are accepted for API compatibility with the - /// previous Kokoro engine. Pocket TTS infers language from the input text - /// directly and is a one-step consistency model. Returns an empty buffer - /// for whitespace-only input. - pub fn synth_chunk( - &self, - text: &str, - _lang: &str, - style: &VoiceStyle, - _steps: usize, - ) -> Result, String> { - // Mirror upstream pocket-tts prompt prep — without this short or - // unpunctuated inputs can cause the LM's EOS logit to never trip, - // producing up to 40 s of "monster breathing" garbage on the first - // utterance. See `prepare_pocket_prompt` for the full recipe. - let prepared = match prepare_pocket_prompt(text) { - Some(p) => p, - None => return Ok(Vec::new()), - }; - - // Per-call generation hints sherpa-onnx forwards to - // `offline-tts-pocket-impl.h`. We only override `max_frames`, and - // only for short padded prompts where we have a tight expectation - // on output length — that bounds the original runaway without - // disturbing the rest of the LM sampling envelope. See - // `prepare_pocket_prompt` docs for the regression history. - let extra = build_generation_extra(&prepared); - - let cfg = GenerationConfig { - num_steps: SYNTH_NUM_STEPS, - silence_scale: SYNTH_SILENCE_SCALE, - reference_audio: Some(style.samples.clone()), - reference_sample_rate: style.sample_rate, - extra, - // `speed` stays at its default: the Pocket impl never reads it - // (see the engine-contract note in the module docs). - ..Default::default() - }; - - // No progress callback — synthesis is fast enough that returning the - // whole buffer at once keeps the lookahead pipelining in `tts.rs` - // simple. `None:: bool>` pins the callback type for the - // `generate_with_config` generic parameter. - let audio = self - .inner - .generate_with_config(&prepared.text, &cfg, None:: bool>) - .ok_or_else(|| { - format!( - "Pocket TTS synthesis failed for text ({} chars)", - prepared.text.len() - ) - })?; - - let sample_rate = audio.sample_rate(); - if sample_rate != SAMPLE_RATE as i32 { - eprintln!( - "buzz-desktop: Pocket TTS returned unexpected sample rate {sample_rate}Hz \ - (expected {SAMPLE_RATE}Hz); playback speed may be wrong" - ); - } - - Ok(audio.samples().to_vec()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // ── prepare_pocket_prompt ──────────────────────────────────────────────── - - #[test] - fn prepare_prompt_returns_none_for_empty_input() { - assert!(prepare_pocket_prompt("").is_none()); - assert!(prepare_pocket_prompt(" ").is_none()); - assert!(prepare_pocket_prompt("\n\t ").is_none()); - } - - /// Helper: the exact leading sequence prepended to every short prompt — - /// 8 spaces of padding (upstream's cold-start mitigation). - /// Centralising this keeps the assertions readable. - fn short_prefix() -> String { - " ".repeat(SHORT_PROMPT_PAD_SPACES) - } - - #[test] - fn prepare_prompt_pads_and_capitalizes_one_word() { - // The "yep" case Tyler hit in production — bare lowercase one-word - // utterance with no punctuation. Must be padded with the short-prompt - // space pad, capitalized, terminated, with a tight `max_frames` cap - // to bound runaway gen. - let out = prepare_pocket_prompt("yep").expect("non-empty"); - assert_eq!(out.text, format!("{}Yep.", short_prefix())); - assert_eq!(out.max_frames, Some(SHORT_PROMPT_MAX_FRAMES)); - const { - assert!( - SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT, - "short cap must be tighter than the upstream default" - ); - } - } - - #[test] - fn prepare_prompt_preserves_existing_punctuation() { - let out = prepare_pocket_prompt("yes!").expect("non-empty"); - assert_eq!(out.text, format!("{}Yes!", short_prefix())); // exclamation kept - let out = prepare_pocket_prompt("really?").expect("non-empty"); - assert_eq!(out.text, format!("{}Really?", short_prefix())); - } - - #[test] - fn prepare_prompt_threshold_is_inclusive_at_four_words() { - // 4 words = short (padded + tight max_frames); 5 words = long - // (no padding, no overrides — upstream defaults stand). - let four = prepare_pocket_prompt("one two three four").expect("non-empty"); - assert_eq!( - four.text, - format!("{}One two three four.", short_prefix()), - "four-word input should get exactly the space pad" - ); - assert_eq!(four.max_frames, Some(SHORT_PROMPT_MAX_FRAMES)); - - let five = prepare_pocket_prompt("one two three four five").expect("non-empty"); - assert!( - !five.text.starts_with(' '), - "five-word input should NOT be padded" - ); - assert_eq!( - five.max_frames, None, - "long inputs must leave sherpa-onnx's max_frames default in place" - ); - } - - #[test] - fn prepare_prompt_does_not_pad_long_text() { - let long = "This is a longer sentence that the model should handle just fine."; - let out = prepare_pocket_prompt(long).expect("non-empty"); - assert!(!out.text.starts_with(' ')); - assert_eq!(out.max_frames, None); - assert!(out.text.ends_with('.')); - } - - #[test] - fn prepare_prompt_collapses_whitespace() { - let out = prepare_pocket_prompt("Hello world\n\nfriend").expect("non-empty"); - // 3 words → short → padded. Interior whitespace collapsed. - assert_eq!(out.text, format!("{}Hello world friend.", short_prefix())); - } - - #[test] - fn prepare_prompt_does_not_double_capitalize_already_uppercase() { - let out = prepare_pocket_prompt("HELLO there").expect("non-empty"); - assert_eq!(out.text, format!("{}HELLO there.", short_prefix())); - } - - #[test] - fn prepare_prompt_handles_non_ascii_first_letter() { - // Cyrillic lowercase 'д' → uppercase 'Д'. Must not panic / produce - // mojibake. - let out = prepare_pocket_prompt("дa").expect("non-empty"); - assert!(out.text.contains("Дa.")); - } - - /// REGRESSION GUARD: short prompts must receive *only* whitespace - /// padding — no sacrificial text. A previous revision prepended a - /// `". . "` cold-start absorber and trimmed the rendered audio back out - /// with an amplitude threshold that could eat soft word onsets. If - /// non-whitespace ever reappears in the pad, the synth output will - /// contain audio for text the user never wrote. - #[test] - fn prepare_prompt_pad_is_whitespace_only() { - let out = prepare_pocket_prompt("I'm happy.").expect("non-empty"); - let pad_len = out.text.len() - "I'm happy.".len(); - assert!( - out.text[..pad_len].chars().all(|c| c == ' '), - "short-prompt pad must be spaces only, got {:?}", - &out.text[..pad_len] - ); - assert_eq!(out.text, format!("{}I'm happy.", short_prefix())); - } - - // ── build_generation_extra ─────────────────────────────────────────────── - // - // These tests pin down a behaviour we've now regressed twice on: - // 1) Not padding/punctuating short inputs → 40 s of "monster breathing" - // (pre-773a2a1). - // 2) Setting `frames_after_eos = 1` on long inputs → clipped leading - // audio of multi-clause sentences, e.g. "Yep, I can hear you. …" - // came out as a static burst (the 773a2a1 regression Tyler hit on - // 2026-05-18 ~14:30 UTC). - // - // The contract we enforce going forward: we **only** override - // `max_frames`, and only for ≤4-word inputs. Every other knob is left - // at sherpa-onnx's documented default (notably `frames_after_eos = 3`). - - #[test] - fn build_extra_short_prompt_sets_only_max_frames() { - let prepared = prepare_pocket_prompt("yep").expect("non-empty"); - let extra = build_generation_extra(&prepared).expect("short prompts get extra"); - // Exactly one key — `max_frames` — and nothing else. - assert_eq!(extra.len(), 1, "extra has unexpected keys: {extra:?}"); - assert_eq!( - extra.get("max_frames"), - Some(&serde_json::Value::from(SHORT_PROMPT_MAX_FRAMES)) - ); - assert!( - !extra.contains_key("frames_after_eos"), - "frames_after_eos must never be set — upstream default of {SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT} is what we want" - ); - } - - #[test] - fn build_extra_long_prompt_is_none() { - // ≥5 words: no extras at all. This is the key fix for the "first - // 'yep' in 'Yep, I can hear you. …' is static" regression — we - // were previously forcing `frames_after_eos = 1` on this path. - let prepared = prepare_pocket_prompt("Yep, I can hear you.").expect("non-empty"); - assert_eq!( - build_generation_extra(&prepared), - None, - "long prompts must not override any LM knob" - ); - } - - #[test] - fn build_extra_never_lowers_frames_after_eos_for_any_word_count() { - // Sweep a range of prompt lengths and assert the `extra` map (when - // present) never carries a `frames_after_eos` override that's lower - // than the upstream sherpa-onnx default. Implemented as a structural - // check — we just never set the key — but worth a property test in - // case someone reintroduces the override in the future. - let prompts: &[&str] = &[ - "hi", - "hi there", - "yes please", - "one two three four", - "one two three four five", - "a slightly longer reply, hopefully fine", - "This is a multi-clause sentence. It has two parts.", - "really really really really really long prompt with lots of words just to be sure", - ]; - for &p in prompts { - let prepared = prepare_pocket_prompt(p).expect("non-empty"); - if let Some(extra) = build_generation_extra(&prepared) { - if let Some(v) = extra.get("frames_after_eos") { - let n = v.as_i64().expect("frames_after_eos should be int"); - assert!( - n >= SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT as i64, - "prompt {p:?} set frames_after_eos={n}, below upstream default of {SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT}" - ); - } - } - } - } - - #[test] - fn short_prompt_max_frames_is_below_upstream_default() { - // Sanity: the override only ever *lowers* the cap, never raises it. - const { - assert!(SHORT_PROMPT_MAX_FRAMES < SHERPA_ONNX_MAX_FRAMES_DEFAULT); - } - // …and is still large enough for a one-to-four-word reply. At Mimi's - // 12.5 Hz frame rate, 100 frames = 8 s, which is roomy. - const { - assert!(SHORT_PROMPT_MAX_FRAMES >= 50, "would risk truncation"); - } - } -} +pub use buzz_voice_pkg::pocket::*; diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 4dd1142801..ae26fc6ba8 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -84,6 +84,9 @@ run_unit_tests() { run_test_step "buzz-auth unit tests" \ cargo test -p buzz-auth --lib -- --nocapture + run_test_step "buzz-voice tests" \ + cargo test -p buzz-voice --lib -- --nocapture + # buzz-db migrator/lint unit tests (no infra): guard the embedded-migrator # invariant (exactly the consolidated 0001; cutover/backfill stays an operator # script, not startup state) and the tenant-scoping lints. The Postgres-backed From 3e9dd6d3dacc6011838cec1302e4301039355378 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 23:51:07 -0400 Subject: [PATCH 02/27] Make Pocket TTS comments timeless Signed-off-by: John Tennant --- crates/buzz-voice/src/pocket.rs | 71 ++++++++++----------------------- 1 file changed, 22 insertions(+), 49 deletions(-) diff --git a/crates/buzz-voice/src/pocket.rs b/crates/buzz-voice/src/pocket.rs index ee1faf928a..e7daa3ed96 100644 --- a/crates/buzz-voice/src/pocket.rs +++ b/crates/buzz-voice/src/pocket.rs @@ -1,14 +1,11 @@ //! Pocket TTS engine wrapper around sherpa-onnx's `OfflineTts`. //! //! Pocket TTS is a small (~473 MB fp32 ONNX) zero-shot voice-cloning TTS -//! model from Kyutai. It runs quickly on CPU via sherpa-onnx, replacing the -//! previous Kokoro-82M engine that also required an espeak-free but -//! lexicon-heavy G2P pipeline (Misaki + CMUdict). +//! model from Kyutai that runs quickly on CPU via sherpa-onnx. //! -//! Full-precision fp32 sessions, not the ~189 MB int8 quantization we -//! originally shipped: a direct same-runtime A/B (k2-fsa/sherpa-onnx#3172) -//! found the int8 ONNX export audibly degraded output quality, and fp32 -//! "significantly improved quality even at 1 step". +//! Buzz uses full-precision fp32 sessions because a direct same-runtime A/B +//! (k2-fsa/sherpa-onnx#3172) found the ~189 MB int8 ONNX export audibly +//! degraded output quality. //! //! ## Attribution //! @@ -94,12 +91,9 @@ const SYNTH_NUM_STEPS: i32 = 1; /// /// sherpa-onnx's `ScaleSilence` (`offline-tts.cc`) is *not* pre/post padding /// control: it finds every interior silence run ≥ 0.2 s (|s| ≤ 0.01) and -/// multiplies its length by this factor. The previous value of 0.0 — set -/// under the mistaken belief it disabled lead-in/lead-out padding — deleted -/// every natural pause inside an utterance: clause breaks, breaths, the gap -/// after a comma. Words slammed together and endings cut abruptly. The -/// reference Pocket TTS pipeline does not post-process silence at all; -/// 1.0 restores parity. +/// multiplies its length by this factor. The reference Pocket TTS pipeline +/// preserves natural clause breaks, breaths, and punctuation pauses, so the +/// identity scale keeps those interior silences intact. const SYNTH_SILENCE_SCALE: f32 = 1.0; /// sherpa-onnx upstream default for `max_frames` (LM steps), in @@ -120,10 +114,9 @@ const SHORT_PROMPT_MAX_FRAMES: i32 = 100; /// Word-count threshold (inclusive) below which we pad the prompt with /// leading spaces and cap `max_frames` tighter than the upstream default. /// Matches upstream `pocket_tts.models.tts_model.prepare_text_prompt`. Above -/// this threshold we leave sherpa-onnx's own defaults in place — overriding -/// them caused the "first 'yep' is just static" regression seen on -/// 2026-05-18, where dropping `frames_after_eos` below the upstream default -/// of 3 clipped the leading audio of multi-clause sentences. +/// this threshold we leave sherpa-onnx's own defaults in place because +/// dropping `frames_after_eos` below the upstream default of 3 can clip the +/// leading audio of multi-clause sentences. const SHORT_PROMPT_WORD_THRESHOLD: usize = 4; /// Number of leading spaces prepended to short prompts. The upstream Python @@ -132,21 +125,14 @@ const SHORT_PROMPT_WORD_THRESHOLD: usize = 4; /// This is upstream's *only* mitigation for the FlowLM cold-start smear on /// short utterances (kyutai-labs/pocket-tts #91, #70): the autoregressive /// generation has a 2–3 step "settle" period where the first phoneme can be -/// smeared. A previous revision added a sacrificial `". . "` prefix plus an -/// amplitude-threshold trim to strip the rendered prefix from the output — -/// but the trim's absolute threshold (0.02 against raw peaks of ~0.076) sat -/// in soft-onset territory and could eat real word starts, and its tuning -/// was calibrated against `silence_scale = 0.0` audio. Deleted in favour of -/// upstream parity: accept the occasional smeared first syllable rather -/// than risk trimming real speech. +/// smeared. The pad must remain whitespace-only: synthesized sacrificial text +/// requires amplitude-threshold trimming, which can eat soft word starts. const SHORT_PROMPT_PAD_SPACES: usize = 8; /// sherpa-onnx's documented `frames_after_eos` default. We deliberately do -/// *not* override this knob — the previous attempt to bump it for short -/// inputs and lower it for long inputs lowered it below the upstream default -/// of 3, which clipped the leading audio of multi-clause sentences (the -/// "first 'yep' is static" regression). The constant exists only for the -/// regression test below. Source: `offline-tts-pocket-impl.h:Generate`. +/// *not* override this knob because values below the upstream default of 3 can +/// clip the leading audio of multi-clause sentences. The constant exists only +/// for the invariant test below. Source: `offline-tts-pocket-impl.h:Generate`. #[cfg(test)] const SHERPA_ONNX_FRAMES_AFTER_EOS_DEFAULT: i32 = 3; @@ -367,12 +353,9 @@ pub(crate) fn prepare_pocket_prompt(input: &str) -> Option { /// Build the `GenerationConfig.extra` HashMap from a [`PreparedPrompt`]. /// -/// Centralised so the regression test below can assert that we **never** -/// emit a `frames_after_eos` override — the previous attempt to override -/// that knob (setting it to 1 for ≥5-word inputs) clipped the leading -/// audio of multi-clause sentences (the "first 'yep' is static" bug on -/// 2026-05-18). The upstream sherpa-onnx default of 3 is what we want, and -/// the right way to keep it is to not set it at all. +/// Centralised so the invariant test below can assert that we **never** emit a +/// `frames_after_eos` override. Leaving the key unset preserves sherpa-onnx's +/// upstream default of 3. fn build_generation_extra(prepared: &PreparedPrompt) -> Option> { prepared.max_frames.map(|mf| { let mut h: HashMap = HashMap::with_capacity(1); @@ -567,17 +550,9 @@ mod tests { // ── build_generation_extra ─────────────────────────────────────────────── // - // These tests pin down a behaviour we've now regressed twice on: - // 1) Not padding/punctuating short inputs → 40 s of "monster breathing" - // (pre-773a2a1). - // 2) Setting `frames_after_eos = 1` on long inputs → clipped leading - // audio of multi-clause sentences, e.g. "Yep, I can hear you. …" - // came out as a static burst (the 773a2a1 regression Tyler hit on - // 2026-05-18 ~14:30 UTC). - // - // The contract we enforce going forward: we **only** override - // `max_frames`, and only for ≤4-word inputs. Every other knob is left - // at sherpa-onnx's documented default (notably `frames_after_eos = 3`). + // Short prompts override only `max_frames`; long prompts emit no extras. + // Every other knob remains at sherpa-onnx's documented default, notably + // `frames_after_eos = 3`. #[test] fn build_extra_short_prompt_sets_only_max_frames() { @@ -597,9 +572,7 @@ mod tests { #[test] fn build_extra_long_prompt_is_none() { - // ≥5 words: no extras at all. This is the key fix for the "first - // 'yep' in 'Yep, I can hear you. …' is static" regression — we - // were previously forcing `frames_after_eos = 1` on this path. + // ≥5 words: no extras, so the upstream LM defaults remain authoritative. let prepared = prepare_pocket_prompt("Yep, I can hear you.").expect("non-empty"); assert_eq!( build_generation_extra(&prepared), From c6df66a36355e5f2321c2f732888bfc45b784bea Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 15:16:20 -0400 Subject: [PATCH 03/27] feat(desktop): add Pocket voice settings Signed-off-by: John Tennant Signed-off-by: John Tennant Signed-off-by: John Tennant --- desktop/playwright.config.ts | 1 + .../resources/pocket-voices/NOTICE.md | 25 + .../resources/pocket-voices/marius.wav | Bin 0 -> 480044 bytes desktop/src-tauri/src/app_state.rs | 20 +- desktop/src-tauri/src/huddle/mod.rs | 70 +- desktop/src-tauri/src/huddle/models.rs | 26 +- .../src/huddle/models_voice_upgrade.rs | 73 ++ desktop/src-tauri/src/huddle/pipeline.rs | 9 +- desktop/src-tauri/src/huddle/state.rs | 19 + desktop/src-tauri/src/huddle/tts.rs | 107 ++- desktop/src-tauri/src/huddle/tts_settings.rs | 715 ++++++++++++++++++ .../src/huddle/tts_voice_selection_tests.rs | 30 + desktop/src-tauri/src/lib.rs | 16 + .../huddle/lib/ttsLiveMessages.test.mjs | 154 ++++ .../features/huddle/lib/ttsLiveMessages.ts | 124 +++ .../features/huddle/lib/useTtsSubscription.ts | 102 ++- .../features/settings/ui/SettingsPanels.tsx | 11 + .../src/features/settings/ui/SettingsView.tsx | 1 + .../settings/ui/VoiceSettingsCard.tsx | 252 ++++++ .../settings/ui/voiceSettingsLogic.test.mjs | 62 ++ .../settings/ui/voiceSettingsLogic.ts | 57 ++ .../shared/api/relayChannelFilters.test.mjs | 9 + desktop/src/shared/api/relayChannelFilters.ts | 12 + desktop/src/testing/e2eBridge.ts | 87 +++ desktop/tests/e2e/voice-settings.spec.ts | 78 ++ desktop/tests/helpers/bridge.ts | 5 + desktop/tests/helpers/settings.ts | 1 + 27 files changed, 1951 insertions(+), 115 deletions(-) create mode 100644 desktop/src-tauri/resources/pocket-voices/NOTICE.md create mode 100644 desktop/src-tauri/resources/pocket-voices/marius.wav create mode 100644 desktop/src-tauri/src/huddle/models_voice_upgrade.rs create mode 100644 desktop/src-tauri/src/huddle/tts_settings.rs create mode 100644 desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs create mode 100644 desktop/src/features/huddle/lib/ttsLiveMessages.test.mjs create mode 100644 desktop/src/features/huddle/lib/ttsLiveMessages.ts create mode 100644 desktop/src/features/settings/ui/VoiceSettingsCard.tsx create mode 100644 desktop/src/features/settings/ui/voiceSettingsLogic.test.mjs create mode 100644 desktop/src/features/settings/ui/voiceSettingsLogic.ts create mode 100644 desktop/tests/e2e/voice-settings.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 40dd5dd1b1..bbdccd6de3 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -48,6 +48,7 @@ export default defineConfig({ "**/activity-scope-label-screenshots.spec.ts", "**/welcome-agent-modal-screenshots.spec.ts", "**/local-archive-screenshots.spec.ts", + "**/voice-settings.spec.ts", "**/agent-readiness-screenshots.spec.ts", "**/agent-error-state-screenshots.spec.ts", "**/edit-agent.spec.ts", diff --git a/desktop/src-tauri/resources/pocket-voices/NOTICE.md b/desktop/src-tauri/resources/pocket-voices/NOTICE.md new file mode 100644 index 0000000000..ecbee2691d --- /dev/null +++ b/desktop/src-tauri/resources/pocket-voices/NOTICE.md @@ -0,0 +1,25 @@ +# Marius Pocket TTS reference voice + +`marius.wav` is an audio-identical copy of Kyutai's +`voice-donations/Selfie.wav`; only the filename changed. + +- Display name: Marius +- Source: https://huggingface.co/kyutai/tts-voices/blob/main/voice-donations/Selfie.wav +- Upstream revision: `323332d33f997de8394f24a193e1a76df720e01a` +- SHA-256: `076968c3122520f3412eb7090e8c1c3f75fe57be1e24a2f96465583d84c71e16` +- Format: WAV PCM signed 16-bit little-endian, mono, 24 kHz, 10 seconds +- License: CC0 1.0 Universal + (https://creativecommons.org/publicdomain/zero/1.0/) +- Repository provenance: + https://huggingface.co/kyutai/tts-voices/blob/main/README.md +- Donation terms: + https://kyutai.org/next/legal/Terms%20of%20Use%20-%20Unmute%20Voice%20Donation%20Project%20v1.pdf + +The adult contributor submitted their own natural voice through Kyutai's +Unmute Voice Donation Project. Its terms expressly contemplate public reuse, +text-to-speech and voice-replica use, and synthetic speech generated in the +contributed voice. + +Neither Kyutai nor the contributor endorses Buzz. Buzz does not identify or +attempt to identify the contributor, and synthetic output must not be presented +as a genuine recording by them. diff --git a/desktop/src-tauri/resources/pocket-voices/marius.wav b/desktop/src-tauri/resources/pocket-voices/marius.wav new file mode 100644 index 0000000000000000000000000000000000000000..11ab460394eba612faeaea90cfcb104221d9a0b1 GIT binary patch literal 480044 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
    }^JRNZs^7~u|(7G$aIw%qfU&>O(J}P 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�b5@HWejJHE6HyNAd7Mo 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%0AWkJ5XiC3%R$xyO!b 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 a+0Qf03lz+Wl5E~$mE={jkC?AFCiy?lV@9_C literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index 94d162e620..7f7a521141 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -30,9 +30,8 @@ pub struct AppState { /// Workspace-provided relay URL override. Set by `apply_workspace` on app /// init and takes priority over env vars and compile-time defaults. pub relay_url_override: Mutex>, - /// Set during backend setup when managed agents are eligible for launch - /// restore. `apply_workspace` consumes it after installing the workspace - /// relay and identity, so agents never start against the fallback relay. + /// Set during setup when managed agents are eligible for launch restore; + /// consumed after workspace identity/relay install to avoid the fallback. pub managed_agent_restore_pending: AtomicBool, /// Whether desktop may repair managed-agent kind:0 profiles from its local /// records. Disabled by the agent-managed profiles experiment so an agent's @@ -40,19 +39,17 @@ pub struct AppState { pub managed_agent_profile_reconcile_enabled: AtomicBool, /// Shared shutdown signal checked by launch-time agent restoration. pub shutdown_started: AtomicBool, - /// Serializes every managed-runtime transition that changes the protected - /// PID set: spawn/register, adoption, stop, shutdown, and sweep snapshots. + /// Serializes managed-runtime transitions that change the protected PID set: + /// spawn/register, adoption, stop, shutdown, and sweep snapshots. /// Never perform network I/O while holding this lock. pub managed_agent_runtime_transition: Mutex<()>, pub managed_agents_store_lock: Mutex<()>, pub channel_templates_store_lock: Mutex<()>, pub managed_agent_processes: Mutex>, pub huddle_state: Mutex, - /// Tauri app handle — stored after setup so huddle commands can emit - /// `huddle-state-changed` events without needing the handle threaded - /// through every call site. - /// - /// Set once during `setup()` in `lib.rs`; never cleared. + pub tts_settings: Mutex, + pub tts_settings_load_error: Mutex>, + pub tts_settings_transition: tokio::sync::Mutex<()>, pub app_handle: Mutex>, /// Selected audio output device name. `None` = system default. /// Used by `connect_audio_relay` and TTS pipeline when opening sinks. @@ -213,6 +210,9 @@ pub fn build_app_state() -> AppState { managed_agent_processes: Mutex::new(HashMap::new()), session_config_cache: Mutex::new(HashMap::new()), huddle_state: Mutex::new(HuddleState::default()), + tts_settings: Mutex::new(Default::default()), + tts_settings_load_error: Mutex::new(None), + tts_settings_transition: tokio::sync::Mutex::new(()), app_handle: Mutex::new(None), audio_output_device: Mutex::new(None), media_proxy_port: AtomicU16::new(0), diff --git a/desktop/src-tauri/src/huddle/mod.rs b/desktop/src-tauri/src/huddle/mod.rs index a815bf2d06..7eaa5add4d 100644 --- a/desktop/src-tauri/src/huddle/mod.rs +++ b/desktop/src-tauri/src/huddle/mod.rs @@ -37,6 +37,7 @@ pub mod state; pub mod stt; pub mod transcription; pub mod tts; +pub mod tts_settings; pub mod wire; // ── Shared utilities ────────────────────────────────────────────────────────── @@ -63,6 +64,7 @@ pub(super) fn drain_until_shutdown( pub use state::{HuddleJoinInfo, HuddlePhase, HuddleState, VoiceInputMode}; pub use transcription::{set_huddle_transcription_enabled, start_stt_pipeline}; +pub use tts_settings::set_tts_enabled; // ── Imports ─────────────────────────────────────────────────────────────────── @@ -817,47 +819,6 @@ pub fn get_model_status(_state: State<'_, AppState>) -> Result) -> Result<(), String> { - let old_pipeline = { - let mut hs = state.huddle()?; - hs.tts_enabled = enabled; - if !enabled { - hs.tts_pipeline.take() // Take out of lock. - } else { - None - } - }; - // Shut down outside the lock — thread join happens here. - if let Some(ref pipeline) = old_pipeline { - pipeline.shutdown(); - } - drop(old_pipeline); - - if enabled { - // Re-start TTS pipeline if models are available and huddle is active. - let phase = { - let hs = state.huddle()?; - hs.phase.clone() - }; - if matches!(phase, HuddlePhase::Connected | HuddlePhase::Active) { - if let Err(e) = maybe_start_tts_pipeline(&state).await { - eprintln!("buzz-desktop: TTS pipeline restart failed: {e}"); - } - } - } - - Ok(()) -} - /// Speak an agent message via TTS. /// /// Maximum text length accepted for TTS synthesis. @@ -895,13 +856,26 @@ pub async fn speak_agent_message(text: String, state: State<'_, AppState>) -> Re } } - let hs = state.huddle()?; - if hs.tts_enabled { - if let Some(ref pipeline) = hs.tts_pipeline { - pipeline.speak(text)?; - } - } - Ok(()) + let sender = { + let hs = state.huddle()?; + hs.tts_enabled + .then(|| { + hs.tts_pipeline + .as_ref() + .map(|pipeline| pipeline.text_sender()) + }) + .flatten() + }; + let Some(sender) = sender else { + return Ok(()); + }; + tokio::task::spawn_blocking(move || { + sender + .send(text) + .map_err(|error| format!("TTS queue closed while waiting to enqueue: {error}")) + }) + .await + .map_err(|error| format!("TTS enqueue task failed: {error}"))? } /// Add an agent to the active huddle. diff --git a/desktop/src-tauri/src/huddle/models.rs b/desktop/src-tauri/src/huddle/models.rs index 169ddf66c0..e9b4e02a2c 100644 --- a/desktop/src-tauri/src/huddle/models.rs +++ b/desktop/src-tauri/src/huddle/models.rs @@ -24,6 +24,9 @@ use std::sync::{Arc, Mutex, OnceLock}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +#[path = "models_voice_upgrade.rs"] +mod voice_upgrade; + // ── Integrity verification ──────────────────────────────────────────────────── // // All model artifacts are verified against pinned SHA-256 hashes before @@ -77,6 +80,7 @@ const TTS_FILE_HASHES: &[(&str, &str)] = &[ ("token_scores.json", "5be2f278caf9b9800741f0fd82bff677f4943ec764c356f907213434b622d958"), ("LICENSE", "fe7b4ce83b8381cc5b216bbb4af73c570688d1b819c73bbaed8ca401f4677cd6"), ("reference_sample.wav", "a35b0468382218e9f37a9a7494d1e4b74deaf18d7ced22265b4e325bb55c183f"), + ("marius.wav", "076968c3122520f3412eb7090e8c1c3f75fe57be1e24a2f96465583d84c71e16"), ]; // ── Model versioning ────────────────────────────────────────────────────────── @@ -100,7 +104,7 @@ const STT_MODEL_VERSION: &str = "2"; /// reason explicit and skips the failing-then-re-fetching transient state. /// Bumped "2" → "3" for the int8 → fp32 model swap (see `POCKET_HF_BASE`): /// existing int8 installs must re-download the suffixless fp32 sessions. -const TTS_MODEL_VERSION: &str = "3"; +const TTS_MODEL_VERSION: &str = "4"; /// Filename for the version manifest written alongside model files. const MANIFEST_FILENAME: &str = ".buzz-model-manifest"; @@ -194,6 +198,14 @@ https://datashare.ed.ac.uk/handle/10283/3443 (CC-BY-4.0). Recording enhancement (denoise/dereverb) by ai-coustics: https://ai-coustics.com/ +Bundled reference voice (marius.wav): +\"Marius\", an audio-identical copy of Kyutai's +`voice-donations/Selfie.wav`, distributed under CC0 1.0 Universal: +https://huggingface.co/kyutai/tts-voices/blob/main/voice-donations/Selfie.wav +https://creativecommons.org/publicdomain/zero/1.0/ +The contributor submitted their own voice under Kyutai's Unmute Voice Donation +Project terms. Neither Kyutai nor the contributor endorses Buzz. + Buzz ships all ONNX/model artifacts and the reference voice WAV unmodified, renamed only by placement in the local model directory. @@ -212,6 +224,7 @@ const TTS_EXPECTED_FILES: &[&str] = &[ "token_scores.json", "LICENSE", "reference_sample.wav", + "marius.wav", TTS_LICENSE_FILE_NAME, ]; @@ -640,6 +653,9 @@ impl ModelManager { /// Start a background Pocket TTS download (~189 MB). No-op if already ready or downloading. pub fn start_tts_download(&self, http_client: reqwest::Client) { + if let Err(error) = voice_upgrade::install_marius_into_v3_model(&self.models_dir) { + eprintln!("buzz-desktop: could not upgrade existing Pocket voices in place: {error}"); + } let manager = self.clone(); self.tts.start_download( &self.models_dir, @@ -757,7 +773,7 @@ impl ModelManager { /// - five ONNX sessions (Pocket TTS + Mimi codec) /// - `vocab.json` / `token_scores.json` for sherpa-onnx text conditioning /// - upstream `LICENSE` plus Buzz's `MODEL_LICENSE.txt` attribution sidecar - /// - `reference_sample.wav` as the bundled default voice + /// - `reference_sample.wav` and embedded `marius.wav` reference voices /// /// Files are written to a temp directory first, then moved atomically. async fn download_tts_model(&self, http_client: reqwest::Client) -> Result<(), String> { @@ -845,6 +861,12 @@ impl ModelManager { tokio::fs::write(temp_dir.join(TTS_LICENSE_FILE_NAME), TTS_LICENSE_TEXT) .await .map_err(|e| format!("write TTS model license sidecar: {e}"))?; + tokio::fs::write( + temp_dir.join("marius.wav"), + voice_upgrade::POCKET_MARIUS_WAV, + ) + .await + .map_err(|e| format!("install bundled Marius voice: {e}"))?; self.tts.set_status(ModelStatus::Downloading { progress_percent: 90, diff --git a/desktop/src-tauri/src/huddle/models_voice_upgrade.rs b/desktop/src-tauri/src/huddle/models_voice_upgrade.rs new file mode 100644 index 0000000000..28c6d26da8 --- /dev/null +++ b/desktop/src-tauri/src/huddle/models_voice_upgrade.rs @@ -0,0 +1,73 @@ +use super::*; + +pub(super) const POCKET_MARIUS_WAV: &[u8] = + include_bytes!("../../resources/pocket-voices/marius.wav"); +const PRE_MARIUS_TTS_MODEL_VERSION: &str = "3"; + +/// Add Marius to an otherwise-ready v3 install without re-downloading models. +/// +/// The manifest is written last, so interruption leaves v3 intact and the next +/// launch retries. +pub(super) fn install_marius_into_v3_model(models_dir: &Path) -> Result<(), String> { + let model_dir = models_dir.join(TTS_MODEL_DIR_NAME); + let manifest_path = model_dir.join(MANIFEST_FILENAME); + let version = match std::fs::read_to_string(&manifest_path) { + Ok(version) => version, + Err(_) => return Ok(()), + }; + if version.trim() != PRE_MARIUS_TTS_MODEL_VERSION { + return Ok(()); + } + if !TTS_EXPECTED_FILES + .iter() + .filter(|filename| **filename != "marius.wav") + .all(|filename| model_dir.join(filename).is_file()) + { + return Ok(()); + } + + std::fs::write(model_dir.join("marius.wav"), POCKET_MARIUS_WAV) + .map_err(|error| format!("write Marius voice: {error}"))?; + std::fs::write(model_dir.join(TTS_LICENSE_FILE_NAME), TTS_LICENSE_TEXT) + .map_err(|error| format!("update Pocket voice notice: {error}"))?; + std::fs::write(manifest_path, TTS_MODEL_VERSION) + .map_err(|error| format!("update Pocket model manifest: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn v3_install_adds_marius_without_redownloading_models() { + let temp = tempfile::tempdir().expect("tempdir"); + let model_dir = temp.path().join(TTS_MODEL_DIR_NAME); + std::fs::create_dir_all(&model_dir).expect("create model dir"); + for file in TTS_EXPECTED_FILES + .iter() + .filter(|filename| **filename != "marius.wav") + { + std::fs::write(model_dir.join(file), b"existing").expect("write prior file"); + } + std::fs::write( + model_dir.join(MANIFEST_FILENAME), + PRE_MARIUS_TTS_MODEL_VERSION, + ) + .expect("write prior manifest"); + + install_marius_into_v3_model(temp.path()).expect("in-place upgrade"); + + assert_eq!( + std::fs::read(model_dir.join("marius.wav")).expect("Marius installed"), + POCKET_MARIUS_WAV + ); + assert_eq!( + std::fs::read_to_string(model_dir.join(MANIFEST_FILENAME)).expect("updated manifest"), + TTS_MODEL_VERSION + ); + assert!( + ModelSlot::new(TTS_MODEL_DIR_NAME, TTS_EXPECTED_FILES, TTS_MODEL_VERSION) + .is_ready(temp.path()) + ); + } +} diff --git a/desktop/src-tauri/src/huddle/pipeline.rs b/desktop/src-tauri/src/huddle/pipeline.rs index ceccedd8b6..478771712d 100644 --- a/desktop/src-tauri/src/huddle/pipeline.rs +++ b/desktop/src-tauri/src/huddle/pipeline.rs @@ -216,8 +216,15 @@ pub(crate) async fn maybe_start_tts_pipeline(state: &AppState) -> Result, - /// Voice name (e.g. "reference_sample"). Stored for future voice-switching support. - #[allow(dead_code)] - voice: String, + /// Selected manifest voice. The worker reloads only the lightweight style + /// when this changes; the warmed Pocket engine and audio player stay alive. + voice: Arc>, /// Worker thread handle — taken on drop to join cleanly. thread: Option>, } impl TtsPipeline { - /// Spawn the TTS pipeline thread using the default voice. - /// - /// `model_dir` must contain the Pocket TTS files declared by `huddle::models` - /// (the five ONNX sessions, the two JSON tables, and `.wav`). - /// - /// `tts_active` is set to `true` while audio is playing and `false` when idle. - /// Pass the same `Arc` to the STT pipeline to gate microphone input. + /// Spawn the TTS pipeline thread with a manifest-backed voice name. /// - /// `cancel` is the shared barge-in flag from `HuddleState.tts_cancel`. Pass the - /// same `Arc` to the STT pipeline so both sides reference the same flag for the - /// entire huddle session — no stale references after pipeline restarts. - pub fn new( - model_dir: PathBuf, - tts_active: Arc, - cancel: Arc, - output_device: Option, - ) -> Result { - use super::pocket::DEFAULT_VOICE; - Self::new_with_voice(model_dir, tts_active, cancel, DEFAULT_VOICE, output_device) - } - - /// Spawn the TTS pipeline thread with a specific voice name. Today only the - /// bundled default voice (see `pocket::DEFAULT_VOICE`) is shipped; other - /// names will surface a clear error from `load_voice_style`. + /// `cancel` is shared with STT for barge-in. The same handle survives voice + /// changes so the warmed Pocket engine is retained. pub fn new_with_voice( model_dir: PathBuf, tts_active: Arc, @@ -178,7 +158,8 @@ impl TtsPipeline { let shutdown_worker = Arc::clone(&shutdown); let cancel_worker = Arc::clone(&cancel); let tts_active_worker = Arc::clone(&tts_active); - let voice_name = voice.to_string(); + let voice = Arc::new(Mutex::new(voice.to_string())); + let voice_worker = Arc::clone(&voice); let model_dir_worker = model_dir.clone(); let handle = thread::Builder::new() @@ -186,7 +167,7 @@ impl TtsPipeline { .spawn(move || { tts_worker( model_dir_worker, - voice_name, + voice_worker, text_rx, tts_active_worker, shutdown_worker, @@ -201,7 +182,7 @@ impl TtsPipeline { tts_active, shutdown, cancel, - voice: voice.to_string(), + voice, thread: Some(handle), }) } @@ -217,6 +198,23 @@ impl TtsPipeline { }) } + /// Clone the bounded queue sender so callers can apply backpressure without + /// holding the huddle mutex. Disabling TTS drops the receiver and unblocks + /// any waiting sender while the shared cancellation flag stops playback. + pub(crate) fn text_sender(&self) -> SyncSender { + self.text_tx.clone() + } + + /// Select a bundled Pocket voice for subsequent speech. + /// + /// Current playback and queued text are cancelled immediately so content + /// cannot continue in the old voice. The worker keeps its warmed inference + /// engine and reloads only the reference style before the next utterance. + pub fn select_voice(&self, voice: &str) { + *self.voice.lock().unwrap_or_else(|error| error.into_inner()) = voice.to_string(); + self.cancel.store(true, Ordering::Release); + } + /// Signal the worker thread to stop. pub fn shutdown(&self) { self.shutdown.store(true, Ordering::Release); @@ -244,7 +242,7 @@ impl Drop for TtsPipeline { fn tts_worker( model_dir: PathBuf, - voice_name: String, + selected_voice: Arc>, text_rx: mpsc::Receiver, tts_active: Arc, shutdown: Arc, @@ -267,8 +265,12 @@ fn tts_worker( }; // ── 2. Load voice style ─────────────────────────────────────────────────── + let mut voice_name = selected_voice + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone(); let voice_path = model_dir.join(format!("{voice_name}.{VOICE_FILE_EXT}")); - let style = match load_voice_style(&voice_path) { + let mut style = match load_voice_style(&voice_path) { Ok(s) => s, Err(e) => { eprintln!( @@ -441,6 +443,46 @@ fn tts_worker( continue; } + // Voice changes cancel the old utterance/queue and are observed here, + // before receiving subsequent text. A bad bundled asset falls back to + // Mary without discarding the already-warmed Pocket engine. + let requested_voice = selected_voice + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone(); + if requested_voice != voice_name { + let requested_path = model_dir.join(format!("{requested_voice}.{VOICE_FILE_EXT}")); + match load_voice_style(&requested_path) { + Ok(requested_style) => { + style = requested_style; + voice_name = requested_voice; + } + Err(error) => { + use super::pocket::DEFAULT_VOICE; + eprintln!( + "buzz-desktop: Pocket voice {requested_voice} is unavailable ({error}); falling back to Mary" + ); + let fallback_path = model_dir.join(format!("{DEFAULT_VOICE}.{VOICE_FILE_EXT}")); + match load_voice_style(&fallback_path) { + Ok(fallback_style) => { + style = fallback_style; + voice_name = DEFAULT_VOICE.to_string(); + *selected_voice + .lock() + .unwrap_or_else(|lock_error| lock_error.into_inner()) = + DEFAULT_VOICE.to_string(); + } + Err(fallback_error) => { + eprintln!( + "buzz-desktop: Mary voice fallback is unavailable: {fallback_error}" + ); + continue; + } + } + } + } + } + let raw_text = match text_rx.recv_timeout(RECV_TIMEOUT) { Ok(t) => t, Err(mpsc::RecvTimeoutError::Timeout) => { @@ -776,3 +818,6 @@ use super::drain_until_shutdown; #[cfg(test)] #[path = "tts_tests.rs"] mod tests; +#[cfg(test)] +#[path = "tts_voice_selection_tests.rs"] +mod voice_selection_tests; diff --git a/desktop/src-tauri/src/huddle/tts_settings.rs b/desktop/src-tauri/src/huddle/tts_settings.rs new file mode 100644 index 0000000000..9a949d8396 --- /dev/null +++ b/desktop/src-tauri/src/huddle/tts_settings.rs @@ -0,0 +1,715 @@ +//! Installation-global text-to-speech preferences and the local voice registry. +//! +//! Voice keys are backend-qualified (`pocket:mary`, `siri:aaron`) and +//! preferences are ordered. A client resolves the first compatible entry for +//! its one active playback backend. The same [`VoicePreferences`] value can be +//! embedded in installation-global settings or future agent identity without a +//! schema change. Availability is intentionally client-local. + +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Manager, State}; + +use crate::{app_state::AppState, managed_agents::storage::atomic_write_json_restricted}; + +use super::{models, pocket::DEFAULT_VOICE, HuddlePhase}; + +const SETTINGS_FILE: &str = "tts-settings.json"; +const CURRENT_VERSION: u32 = 1; +pub const POCKET_BACKEND_ID: &str = "pocket"; +pub const MARY_VOICE_KEY: &str = "pocket:mary"; +pub const MARIUS_VOICE_KEY: &str = "pocket:marius"; + +const VOICE_AVAILABILITY_BUNDLED: &str = "bundled"; +const VOICE_AVAILABILITY_INSTALLED: &str = "installed"; + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct VoiceRegistryEntry { + /// Stable identity, never derived from or merged by the display name. + /// + /// Built-ins use `backend:slug`. Future imports use + /// `pocket:imported:` so two clips with the same + /// editable label remain distinct. + pub key: String, + pub display_name: String, + pub backend: String, + pub backend_name: String, + /// Client-local state: bundled, installed, downloadable, or unavailable. + pub availability: String, + pub fallback_key: Option, + pub reference_file: Option, + pub provenance: VoiceProvenance, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct VoiceProvenance { + pub source: String, + pub content_hash: Option, + pub license: Option, + pub source_url: Option, +} + +/// Ordered, backend-qualified preferences shared by global and agent settings. +/// +/// Unknown but well-formed keys remain persisted because a different client +/// may have that backend installed. Resolution is always local. +pub type VoicePreferences = Vec; + +/// Cross-backend registry for voices known to this client. +/// +/// V1 contains Pocket entries only. Siri, Kokoro, imported voices, and +/// per-agent assignment can add entries or reuse the preference type without +/// changing the registry/settings boundary. +pub fn voice_registry() -> Vec { + vec![ + VoiceRegistryEntry { + key: MARY_VOICE_KEY.to_string(), + display_name: "Mary".to_string(), + backend: POCKET_BACKEND_ID.to_string(), + backend_name: "Pocket TTS".to_string(), + availability: VOICE_AVAILABILITY_BUNDLED.to_string(), + fallback_key: None, + reference_file: Some("reference_sample.wav".to_string()), + provenance: VoiceProvenance { + source: "bundled".to_string(), + content_hash: Some( + "a35b0468382218e9f37a9a7494d1e4b74deaf18d7ced22265b4e325bb55c183f" + .to_string(), + ), + license: Some("CC-BY-4.0".to_string()), + source_url: Some("https://datashare.ed.ac.uk/handle/10283/3443".to_string()), + }, + }, + VoiceRegistryEntry { + key: MARIUS_VOICE_KEY.to_string(), + display_name: "Marius".to_string(), + backend: POCKET_BACKEND_ID.to_string(), + backend_name: "Pocket TTS".to_string(), + availability: VOICE_AVAILABILITY_BUNDLED.to_string(), + fallback_key: Some(MARY_VOICE_KEY.to_string()), + reference_file: Some("marius.wav".to_string()), + provenance: VoiceProvenance { + source: "bundled".to_string(), + content_hash: Some( + "076968c3122520f3412eb7090e8c1c3f75fe57be1e24a2f96465583d84c71e16" + .to_string(), + ), + license: Some("CC0-1.0".to_string()), + source_url: Some( + "https://huggingface.co/kyutai/tts-voices/blob/323332d33f997de8394f24a193e1a76df720e01a/voice-donations/Selfie.wav" + .to_string(), + ), + }, + }, + ] +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct TtsSettings { + pub version: u32, + pub agent_text_to_speech: bool, + pub voice_preferences: VoicePreferences, +} + +impl Default for TtsSettings { + fn default() -> Self { + Self { + version: CURRENT_VERSION, + agent_text_to_speech: true, + voice_preferences: vec![MARY_VOICE_KEY.to_string()], + } + } +} + +pub fn voice_by_key(key: &str) -> Option { + voice_registry().into_iter().find(|voice| voice.key == key) +} + +fn is_qualified_voice_key(key: &str) -> bool { + key.split_once(':') + .is_some_and(|(backend, voice)| !backend.is_empty() && !voice.is_empty()) +} + +fn is_locally_available(availability: &str) -> bool { + matches!( + availability, + VOICE_AVAILABILITY_BUNDLED | VOICE_AVAILABILITY_INSTALLED + ) +} + +pub fn resolve_voice_for_backend( + preferences: &[String], + backend: &str, +) -> Result { + let registry = voice_registry(); + preferences + .iter() + .filter_map(|key| registry.iter().find(|voice| voice.key == *key)) + .find(|voice| voice.backend == backend && is_locally_available(voice.availability.as_str())) + .or_else(|| { + registry.iter().find(|voice| { + voice.backend == backend + && voice.fallback_key.is_none() + && is_locally_available(voice.availability.as_str()) + }) + }) + .cloned() + .ok_or_else(|| format!("No locally available fallback voice for backend {backend}")) +} + +pub fn pocket_voice_name(preferences: &[String]) -> String { + resolve_voice_for_backend(preferences, POCKET_BACKEND_ID) + .ok() + .and_then(|voice| voice.reference_file) + .and_then(|file| file.strip_suffix(".wav").map(str::to_string)) + .unwrap_or_else(|| DEFAULT_VOICE.to_string()) +} + +pub(crate) fn settings_path(app: &AppHandle) -> Result { + app.path() + .app_data_dir() + .map(|dir| dir.join(SETTINGS_FILE)) + .map_err(|error| format!("could not locate Buzz settings storage: {error}")) +} + +pub(crate) fn load_from_path(path: &Path) -> Result { + if !path.exists() { + return Ok(TtsSettings::default()); + } + let bytes = std::fs::read(path) + .map_err(|error| format!("could not read text-to-speech settings: {error}"))?; + let value: serde_json::Value = serde_json::from_slice(&bytes) + .map_err(|error| format!("text-to-speech settings are not valid JSON: {error}"))?; + + // Pre-V1 experiment builds stored an unversioned, incompatible shape. + // Migrate it to deterministic V1 defaults instead of carrying it over. + if value.get("version").is_none() { + return Ok(TtsSettings::default()); + } + + let version = value + .get("version") + .and_then(serde_json::Value::as_u64) + .ok_or("text-to-speech settings version is invalid")?; + if version > u64::from(CURRENT_VERSION) { + return Err(format!( + "text-to-speech settings version {version} is newer than this Buzz build supports" + )); + } + + // Early experiments used one bare Pocket `voiceId`. Preserve the toggle + // and qualify that value into the ordered cross-backend preference schema. + if value.get("voicePreferences").is_none() { + let legacy_voice = value + .get("voiceId") + .or_else(|| value.get("voice_id")) + .and_then(serde_json::Value::as_str) + .unwrap_or("mary"); + let voice_key = if is_qualified_voice_key(legacy_voice) { + legacy_voice.to_string() + } else { + format!("{POCKET_BACKEND_ID}:{legacy_voice}") + }; + return Ok(TtsSettings { + version: CURRENT_VERSION, + agent_text_to_speech: value + .get("agentTextToSpeech") + .and_then(serde_json::Value::as_bool) + .unwrap_or(true), + voice_preferences: vec![voice_key], + }); + } + + let mut settings: TtsSettings = serde_json::from_value(value) + .map_err(|error| format!("text-to-speech settings are invalid: {error}"))?; + settings.version = CURRENT_VERSION; + if settings.voice_preferences.is_empty() + || settings + .voice_preferences + .iter() + .any(|key| !is_qualified_voice_key(key)) + { + settings.voice_preferences = TtsSettings::default().voice_preferences; + } + Ok(settings) +} + +pub(crate) fn save_to_path(path: &Path, settings: &TtsSettings) -> Result<(), String> { + if settings.voice_preferences.is_empty() { + return Err("At least one voice preference is required".to_string()); + } + if let Some(key) = settings + .voice_preferences + .iter() + .find(|key| !is_qualified_voice_key(key)) + { + return Err(format!( + "Voice preference keys must be backend-qualified: {key}" + )); + } + let payload = serde_json::to_vec_pretty(settings) + .map_err(|error| format!("could not encode text-to-speech settings: {error}"))?; + atomic_write_json_restricted(path, &payload) + .map_err(|error| format!("could not save text-to-speech settings: {error}")) +} + +pub fn load_for_app(app: &AppHandle) -> (TtsSettings, Option) { + let result = settings_path(app).and_then(|path| load_from_path(&path)); + match result { + Ok(settings) => (settings, None), + Err(error) => { + eprintln!("buzz-desktop: {error}; preserving the file and using Mary for this session"); + (TtsSettings::default(), Some(error)) + } + } +} + +#[tauri::command] +pub fn get_tts_settings(state: State<'_, AppState>) -> Result { + if let Some(error) = state + .tts_settings_load_error + .lock() + .map_err(|lock_error| format!("text-to-speech settings lock poisoned: {lock_error}"))? + .clone() + { + return Err(format!( + "Voice settings could not be loaded and were left unchanged: {error}" + )); + } + state + .tts_settings + .lock() + .map(|settings| settings.clone()) + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}")) +} + +#[tauri::command] +pub fn list_voice_registry() -> Vec { + voice_registry() +} + +fn ensure_settings_writable(state: &AppState) -> Result<(), String> { + if let Some(error) = state + .tts_settings_load_error + .lock() + .map_err(|lock_error| format!("text-to-speech settings lock poisoned: {lock_error}"))? + .as_ref() + { + return Err(format!( + "Voice settings were not saved because the existing file could not be loaded: {error}" + )); + } + Ok(()) +} + +fn cancel_huddle_speech( + huddle: &mut super::HuddleState, +) -> Option> { + huddle.tts_enabled = false; + huddle + .tts_cancel + .store(true, std::sync::atomic::Ordering::Release); + huddle.tts_pipeline.take() +} + +fn disable_tts_runtime(state: &AppState) -> Result<(), String> { + let old_pipeline = { + let mut huddle = state.huddle()?; + cancel_huddle_speech(&mut huddle) + }; + if let Some(ref pipeline) = old_pipeline { + pipeline.shutdown(); + } + drop(old_pipeline); + state.emit_huddle_state_changed(); + Ok(()) +} + +fn commit_effective_off(state: &AppState) -> Result<(), String> { + state + .tts_settings + .lock() + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? + .agent_text_to_speech = false; + Ok(()) +} + +async fn apply_tts_settings( + settings: TtsSettings, + app: &AppHandle, + state: &AppState, +) -> Result { + if settings.version != CURRENT_VERSION { + return Err(format!( + "Unsupported text-to-speech settings version: {}", + settings.version + )); + } + + // OFF is safety-sensitive: stop current and queued speech before any disk + // I/O, and never resume it merely because persistence fails. + if !settings.agent_text_to_speech { + disable_tts_runtime(state)?; + commit_effective_off(state)?; + } + + ensure_settings_writable(state)?; + save_to_path(&settings_path(app)?, &settings)?; + + *state + .tts_settings + .lock() + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? = + settings.clone(); + + if settings.agent_text_to_speech { + let active = { + let mut huddle = state.huddle()?; + huddle.tts_enabled = true; + huddle + .tts_cancel + .store(false, std::sync::atomic::Ordering::Release); + if let Some(pipeline) = huddle.tts_pipeline.as_ref() { + pipeline.select_voice(&pocket_voice_name(&settings.voice_preferences)); + } + matches!(huddle.phase, HuddlePhase::Connected | HuddlePhase::Active) + }; + if active { + if let Err(error) = super::pipeline::maybe_start_tts_pipeline(state).await { + eprintln!("buzz-desktop: could not hot-start text to speech: {error}"); + } + } + state.emit_huddle_state_changed(); + } + Ok(settings) +} + +/// Compatibility command for the huddle speaker button. It updates the same +/// installation-global preference as Settings; there is no per-huddle override. +#[tauri::command] +pub async fn set_tts_enabled( + enabled: bool, + app: AppHandle, + state: State<'_, AppState>, +) -> Result { + let _transition = state.tts_settings_transition.lock().await; + let mut settings = state + .tts_settings + .lock() + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? + .clone(); + settings.agent_text_to_speech = enabled; + apply_tts_settings(settings, &app, &state).await +} + +fn settings_with_pocket_voice( + mut settings: TtsSettings, + voice_key: &str, +) -> Result { + let voice = voice_by_key(voice_key).ok_or_else(|| format!("Unknown voice: {voice_key}"))?; + if voice.backend != POCKET_BACKEND_ID || !is_locally_available(&voice.availability) { + return Err("The selected Pocket voice is not available on this device".to_string()); + } + let first_pocket_index = settings + .voice_preferences + .iter() + .position(|key| key.starts_with("pocket:")); + settings + .voice_preferences + .retain(|key| !key.starts_with("pocket:")); + let insert_at = first_pocket_index + .unwrap_or(settings.voice_preferences.len()) + .min(settings.voice_preferences.len()); + settings + .voice_preferences + .insert(insert_at, voice_key.to_string()); + Ok(settings) +} + +#[tauri::command] +pub async fn set_pocket_voice( + voice_key: String, + app: AppHandle, + state: State<'_, AppState>, +) -> Result { + let _transition = state.tts_settings_transition.lock().await; + let settings = state + .tts_settings + .lock() + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? + .clone(); + let settings = settings_with_pocket_voice(settings, &voice_key)?; + apply_tts_settings(settings, &app, &state).await +} + +#[tauri::command] +pub async fn preview_pocket_voice( + voice_key: String, + state: State<'_, AppState>, +) -> Result<(), String> { + let voice = voice_by_key(&voice_key).ok_or_else(|| format!("Unknown voice: {voice_key}"))?; + if voice.backend != POCKET_BACKEND_ID { + return Err("Only Pocket voices can be previewed in this build".to_string()); + } + if !models::is_tts_ready() { + return Err("Voice files are still downloading. Try preview again shortly.".to_string()); + } + let model_dir = models::tts_model_dir().ok_or("Pocket voice files are unavailable")?; + let output_device = state + .audio_output_device + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone(); + let voice_name = voice + .reference_file + .and_then(|file| file.strip_suffix(".wav").map(str::to_string)) + .ok_or_else(|| format!("Voice {voice_key} has no local Pocket reference file"))?; + tokio::task::spawn_blocking(move || { + let active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let pipeline = super::tts::TtsPipeline::new_with_voice( + model_dir, + active.clone(), + cancel, + &voice_name, + output_device, + )?; + pipeline.speak("Hello! This is how I’ll read agent responses.".to_string())?; + let started = std::time::Instant::now(); + let mut heard_audio = false; + while started.elapsed() < std::time::Duration::from_secs(30) { + let is_active = active.load(std::sync::atomic::Ordering::Acquire); + heard_audio |= is_active; + if heard_audio && !is_active { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(25)); + } + Err("Voice preview timed out. Check your audio output and try again.".to_string()) + }) + .await + .map_err(|error| format!("Voice preview task failed: {error}"))? +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_are_backwards_compatible_and_use_mary() { + assert_eq!( + TtsSettings::default(), + TtsSettings { + version: 1, + agent_text_to_speech: true, + voice_preferences: vec!["pocket:mary".to_string()], + } + ); + } + + #[test] + fn v1_registry_has_exactly_the_two_reviewed_bundled_voices() { + assert_eq!( + voice_registry() + .iter() + .map(|voice| { + ( + voice.key.as_str(), + voice.display_name.as_str(), + voice.reference_file.as_deref(), + ) + }) + .collect::>(), + vec![ + ("pocket:mary", "Mary", Some("reference_sample.wav")), + ("pocket:marius", "Marius", Some("marius.wav")), + ] + ); + } + + #[test] + fn local_backend_resolution_uses_first_compatible_preference() { + let preferences = vec![ + "siri:aaron".to_string(), + MARIUS_VOICE_KEY.to_string(), + MARY_VOICE_KEY.to_string(), + "kokoro:af_heart".to_string(), + ]; + assert_eq!( + resolve_voice_for_backend(&preferences, POCKET_BACKEND_ID) + .expect("Pocket fallback") + .key, + MARIUS_VOICE_KEY + ); + } + + #[test] + fn unsupported_or_missing_preferences_fall_back_to_backend_default() { + let preferences = vec![ + "siri:aaron".to_string(), + "pocket:imported:deadbeef".to_string(), + ]; + assert_eq!( + resolve_voice_for_backend(&preferences, POCKET_BACKEND_ID) + .expect("Pocket fallback") + .key, + MARY_VOICE_KEY + ); + } + + #[test] + fn identity_is_qualified_key_not_display_label() { + assert!(is_qualified_voice_key("pocket:imported:audio-content-hash")); + assert_ne!(MARY_VOICE_KEY, MARIUS_VOICE_KEY); + let mut registry = voice_registry(); + registry[0].display_name = "Jim".to_string(); + registry[1].display_name = "Jim".to_string(); + assert_eq!(registry[0].display_name, registry[1].display_name); + assert_ne!(registry[0].key, registry[1].key); + assert_eq!( + registry + .iter() + .map(|voice| voice.key.as_str()) + .collect::>() + .len(), + registry.len() + ); + } + + #[test] + fn bundled_marius_asset_has_reviewed_size_and_wav_container() { + let bytes = include_bytes!("../../resources/pocket-voices/marius.wav"); + assert_eq!(bytes.len(), 480_044); + assert_eq!(&bytes[0..4], b"RIFF"); + assert_eq!(&bytes[8..12], b"WAVE"); + assert_eq!( + hex::encode(::digest(bytes)), + "076968c3122520f3412eb7090e8c1c3f75fe57be1e24a2f96465583d84c71e16" + ); + } + + #[test] + fn migrates_unversioned_experiment_settings_to_v1_defaults() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join(SETTINGS_FILE); + std::fs::write(&path, r#"{"voice":"legacy-experiment"}"#).expect("fixture write"); + assert_eq!( + load_from_path(&path).expect("migration"), + TtsSettings::default() + ); + } + + #[test] + fn migrates_bare_pocket_voice_id_to_qualified_preferences() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join(SETTINGS_FILE); + std::fs::write( + &path, + r#"{"version":1,"agentTextToSpeech":false,"voiceId":"marius"}"#, + ) + .expect("fixture write"); + assert_eq!( + load_from_path(&path).expect("migration"), + TtsSettings { + version: 1, + agent_text_to_speech: false, + voice_preferences: vec![MARIUS_VOICE_KEY.to_string()], + } + ); + } + + #[test] + fn unknown_qualified_preferences_are_preserved_for_other_clients() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join(SETTINGS_FILE); + std::fs::write( + &path, + r#"{"version":1,"agentTextToSpeech":false,"voicePreferences":["siri:aaron","pocket:imported:abc123"]}"#, + ) + .expect("fixture write"); + let settings = load_from_path(&path).expect("load"); + assert!(!settings.agent_text_to_speech); + assert_eq!( + settings.voice_preferences, + vec!["siri:aaron", "pocket:imported:abc123"] + ); + } + + #[test] + fn rejects_future_schema_versions_clearly() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join(SETTINGS_FILE); + std::fs::write( + &path, + r#"{"version":99,"agentTextToSpeech":true,"voicePreferences":["pocket:mary"]}"#, + ) + .expect("fixture write"); + assert!(load_from_path(&path) + .expect_err("future version should fail") + .contains("newer than this Buzz build supports")); + } + + #[test] + fn disabling_cancels_runtime_before_persistence_can_fail() { + let mut huddle = super::super::HuddleState { + tts_enabled: true, + ..super::super::HuddleState::default() + }; + assert!(!huddle.tts_cancel.load(std::sync::atomic::Ordering::Acquire)); + assert!(cancel_huddle_speech(&mut huddle).is_none()); + assert!(!huddle.tts_enabled); + assert!(huddle.tts_cancel.load(std::sync::atomic::Ordering::Acquire)); + } + + #[test] + fn pocket_voice_update_preserves_the_latest_toggle_and_other_backends() { + let current = TtsSettings { + agent_text_to_speech: false, + voice_preferences: vec!["siri:aaron".to_string(), MARY_VOICE_KEY.to_string()], + ..TtsSettings::default() + }; + let updated = + settings_with_pocket_voice(current, MARIUS_VOICE_KEY).expect("available voice"); + assert!(!updated.agent_text_to_speech); + assert_eq!( + updated.voice_preferences, + vec!["siri:aaron", MARIUS_VOICE_KEY] + ); + } + + #[test] + fn failed_off_persistence_cannot_be_undone_by_a_later_voice_update() { + let state = crate::app_state::build_app_state(); + commit_effective_off(&state).expect("commit effective OFF state"); + + // This models the next command after the OFF save fails: it must merge + // from effective memory state, not the stale last-persisted ON value. + let current = state.tts_settings.lock().expect("settings").clone(); + let voice_update = + settings_with_pocket_voice(current, MARIUS_VOICE_KEY).expect("available voice"); + assert!(!voice_update.agent_text_to_speech); + } + + #[test] + fn failed_disabled_voice_save_does_not_change_the_remembered_voice() { + let state = crate::app_state::build_app_state(); + state + .tts_settings + .lock() + .expect("settings") + .agent_text_to_speech = false; + let current = state.tts_settings.lock().expect("settings").clone(); + let unsaved = + settings_with_pocket_voice(current, MARIUS_VOICE_KEY).expect("available voice"); + + // This is the only pre-persistence mutation for an OFF candidate. + commit_effective_off(&state).expect("commit effective OFF state"); + let remembered = state.tts_settings.lock().expect("settings").clone(); + assert_eq!(remembered.voice_preferences, vec![MARY_VOICE_KEY]); + assert_eq!(unsaved.voice_preferences, vec![MARIUS_VOICE_KEY]); + } +} diff --git a/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs b/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs new file mode 100644 index 0000000000..a2a8410bba --- /dev/null +++ b/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs @@ -0,0 +1,30 @@ +use super::*; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +#[test] +fn selecting_a_voice_immediately_raises_cancel_and_retains_the_engine_handle() { + let model_dir = tempfile::tempdir().expect("temp model dir"); + let active = Arc::new(AtomicBool::new(false)); + let cancel = Arc::new(AtomicBool::new(false)); + let pipeline = TtsPipeline::new_with_voice( + model_dir.path().to_path_buf(), + active, + Arc::clone(&cancel), + "reference_sample", + None, + ) + .expect("pipeline handle"); + + pipeline.select_voice("marius"); + + assert!(cancel.load(Ordering::Acquire)); + assert_eq!( + pipeline + .voice + .lock() + .unwrap_or_else(|error| error.into_inner()) + .as_str(), + "marius" + ); +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 5346791ccf..732d7ff84e 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -460,6 +460,18 @@ pub fn run() { *guard = Some(app_handle.clone()); } + let (tts_settings, tts_settings_load_error) = + huddle::tts_settings::load_for_app(&app_handle); + if let Ok(mut guard) = state.tts_settings.lock() { + *guard = tts_settings.clone(); + } + if let Ok(mut guard) = state.tts_settings_load_error.lock() { + *guard = tts_settings_load_error; + } + if let Ok(mut huddle) = state.huddle_state.lock() { + huddle.tts_enabled = tts_settings.agent_text_to_speech; + } + // Bring up the runtime-owned shared-compute coordinator before // saved agents are restored. Its lifetime is tied to the app, not // a UI mount; it publishes discovery and reconciles membership for @@ -877,6 +889,10 @@ pub fn run() { download_voice_models, get_model_status, set_tts_enabled, + huddle::tts_settings::get_tts_settings, + huddle::tts_settings::list_voice_registry, + huddle::tts_settings::set_pocket_voice, + huddle::tts_settings::preview_pocket_voice, speak_agent_message, add_agent_to_huddle, check_pipeline_hotstart, diff --git a/desktop/src/features/huddle/lib/ttsLiveMessages.test.mjs b/desktop/src/features/huddle/lib/ttsLiveMessages.test.mjs new file mode 100644 index 0000000000..088a6d6ebf --- /dev/null +++ b/desktop/src/features/huddle/lib/ttsLiveMessages.test.mjs @@ -0,0 +1,154 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + createInitialMembershipGate, + createLatestStateGate, + createOrderedSpeaker, + speakableAgentText, +} from "./ttsLiveMessages.ts"; + +const agents = new Set(["agent"]); +const base = { + id: "1", + kind: 9, + pubkey: "agent", + content: "Hello there", + tags: [], +}; + +test("speaks only new agent-authored text message events", () => { + assert.equal(speakableAgentText(base, agents, "human"), "Hello there"); + assert.equal( + speakableAgentText({ ...base, kind: 7 }, agents, "human"), + null, + "reactions and other event kinds are excluded", + ); + assert.equal( + speakableAgentText({ ...base, kind: 10 }, agents, "human"), + null, + "edits and status events are excluded", + ); + assert.equal( + speakableAgentText({ ...base, pubkey: "human" }, agents, "human"), + null, + "human-authored messages are excluded", + ); + assert.equal( + speakableAgentText({ ...base, content: " " }, agents, "human"), + null, + "empty and non-text content are excluded", + ); + assert.equal( + speakableAgentText( + { ...base, content: "[System] tool started" }, + agents, + "human", + ), + null, + "legacy system rows are excluded", + ); +}); + +test("strips attachment markup and skips attachment-only events", () => { + const url = "https://cdn.example/voice.png"; + const tags = [["imeta", `url ${url}`, "m image/png"]]; + assert.equal( + speakableAgentText( + { ...base, content: `![image](${url})`, tags }, + agents, + "human", + ), + null, + ); + assert.equal( + speakableAgentText( + { ...base, content: `Here is the diagram.\n\n![image](${url})`, tags }, + agents, + "human", + ), + "Here is the diagram.", + ); + assert.equal( + speakableAgentText( + { ...base, content: `||\n![image](${url})\n||`, tags }, + agents, + "human", + ), + null, + ); +}); + +test("queues agent messages in live thread arrival order", async () => { + const spoken = []; + let releaseFirst; + const firstBlocked = new Promise((resolve) => { + releaseFirst = resolve; + }); + const speaker = createOrderedSpeaker(async (text) => { + if (text === "first") await firstBlocked; + spoken.push(text); + }, assert.fail); + + speaker.enqueue("first"); + speaker.enqueue("second"); + await Promise.resolve(); + assert.deepEqual(spoken, []); + releaseFirst(); + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.deepEqual(spoken, ["first", "second"]); +}); + +test("disabling cancels queued speech and rejects new messages until enabled", async () => { + const invoked = []; + let releaseFirst; + const firstBlocked = new Promise((resolve) => { + releaseFirst = resolve; + }); + const speaker = createOrderedSpeaker(async (text) => { + invoked.push(text); + if (text === "first") await firstBlocked; + }, assert.fail); + + speaker.enqueue("first"); + speaker.enqueue("queued-before-off"); + await Promise.resolve(); + speaker.setEnabled(false); + speaker.enqueue("while-off"); + releaseFirst(); + await new Promise((resolve) => setTimeout(resolve, 0)); + speaker.setEnabled(true); + speaker.enqueue("after-on"); + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.deepEqual(invoked, ["first", "after-on"]); +}); + +test("a live TTS state event supersedes a delayed bootstrap result", () => { + const applied = []; + const gate = createLatestStateGate((enabled) => applied.push(enabled)); + const applyBootstrap = gate.beginSnapshot(); + + gate.applyEvent(false); + applyBootstrap(true); + + assert.deepEqual(applied, [false]); +}); + +test("buffers initial live events until membership resolves in order", () => { + const delivered = []; + const gate = createInitialMembershipGate((event) => delivered.push(event)); + gate.push("first"); + gate.push("second"); + assert.deepEqual(delivered, []); + gate.succeed(); + gate.push("third"); + assert.deepEqual(delivered, ["first", "second", "third"]); +}); + +test("drops the initial buffer fail-closed when membership lookup fails", () => { + const delivered = []; + const gate = createInitialMembershipGate((event) => delivered.push(event)); + gate.push("unverified"); + gate.fail(); + assert.deepEqual(delivered, []); +}); diff --git a/desktop/src/features/huddle/lib/ttsLiveMessages.ts b/desktop/src/features/huddle/lib/ttsLiveMessages.ts new file mode 100644 index 0000000000..1e1aeb8b3f --- /dev/null +++ b/desktop/src/features/huddle/lib/ttsLiveMessages.ts @@ -0,0 +1,124 @@ +export type LiveTtsEvent = { + id: string; + kind: number; + pubkey: string; + content: string; + tags: string[][]; +}; + +function textWithoutAttachments(event: LiveTtsEvent): string { + const urls = new Set( + event.tags + .filter((tag) => tag[0] === "imeta") + .flatMap((tag) => + tag + .slice(1) + .filter((field) => field.startsWith("url ")) + .map((field) => field.slice(4)), + ), + ); + if (urls.size === 0) return event.content; + const withoutMedia = event.content + .split("\n") + .filter( + (line) => !Array.from(urls).some((url) => line.includes(`](${url})`)), + ) + .join("\n"); + return withoutMedia.replace( + /(^|\n)\s*\|\|\s*\n(?:\s*\n)*\s*\|\|\s*(?=\n|$)/gu, + "$1", + ); +} + +export function speakableAgentText( + event: LiveTtsEvent, + agentPubkeys: ReadonlySet, + selfPubkey: string | null, +): string | null { + if (event.kind !== 9) return null; + if (!agentPubkeys.has(event.pubkey)) return null; + if (event.pubkey === selfPubkey) return null; + const content = textWithoutAttachments(event).trim(); + if (content.length <= 1) return null; + if (content.startsWith("[System]")) return null; + return content; +} + +/** + * Serialize native speak calls so live messages enter the bounded Pocket queue + * in thread arrival order even when the bridge resolves calls asynchronously. + */ +export function createOrderedSpeaker( + speak: (text: string) => Promise, + onError: (error: unknown) => void, +): { + enqueue: (text: string) => void; + setEnabled: (enabled: boolean) => void; +} { + let tail = Promise.resolve(); + let enabled = true; + let generation = 0; + return { + enqueue(text) { + if (!enabled) return; + const queuedGeneration = generation; + tail = tail + .then(() => { + if (!enabled || generation !== queuedGeneration) return; + return speak(text); + }) + .catch(onError); + }, + setEnabled(nextEnabled) { + if (!nextEnabled) generation += 1; + enabled = nextEnabled; + }, + }; +} + +/** Ensure a delayed bootstrap snapshot cannot overwrite a newer live event. */ +export function createLatestStateGate(apply: (value: T) => void): { + applyEvent: (value: T) => void; + beginSnapshot: () => (value: T) => void; +} { + let revision = 0; + return { + applyEvent(value) { + revision += 1; + apply(value); + }, + beginSnapshot() { + const snapshotRevision = revision; + return (value) => { + if (revision === snapshotRevision) apply(value); + }; + }, + }; +} + +/** Hold live events until the first authoritative agent-membership lookup. */ +export function createInitialMembershipGate(deliver: (event: T) => void): { + push: (event: T) => void; + succeed: () => void; + fail: () => void; +} { + let settled = false; + let pending: T[] = []; + return { + push(event) { + if (settled) deliver(event); + else pending.push(event); + }, + succeed() { + if (settled) return; + settled = true; + const buffered = pending; + pending = []; + for (const event of buffered) deliver(event); + }, + fail() { + settled = true; + pending = []; + }, + }; +} diff --git a/desktop/src/features/huddle/lib/useTtsSubscription.ts b/desktop/src/features/huddle/lib/useTtsSubscription.ts index d534fc0166..251d7e99b1 100644 --- a/desktop/src/features/huddle/lib/useTtsSubscription.ts +++ b/desktop/src/features/huddle/lib/useTtsSubscription.ts @@ -1,7 +1,15 @@ import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import * as React from "react"; +import { buildHuddleTtsLiveFilter } from "@/shared/api/relayChannelFilters"; import { relayClient } from "@/shared/api/relayClient"; +import { + createInitialMembershipGate, + createLatestStateGate, + createOrderedSpeaker, + speakableAgentText, +} from "./ttsLiveMessages"; const AGENT_PUBKEY_REFRESH_INTERVAL_MS = 30_000; @@ -20,6 +28,7 @@ export function useTtsSubscription( let disposed = false; let cleanup: (() => void) | null = null; + let unlistenHuddleState: (() => void) | null = null; // ── Agent identity (authoritative, fail-closed) ─────────────────────── // @@ -33,42 +42,98 @@ export function useTtsSubscription( let agentsLoaded = false; const agentPubkeys = new Set(); - async function loadAgentPubkeys() { + const speakInOrder = createOrderedSpeaker( + async (text) => { + if (!disposed) { + await invoke("speak_agent_message", { text }); + } + }, + (err) => { + console.warn("[huddle] TTS speak failed:", err); + }, + ); + + const deliver = (event: Parameters[0]) => { + if (!agentsLoaded || disposed) return; + const text = speakableAgentText( + event, + agentPubkeys, + selfPubkeyRef.current, + ); + if (text) speakInOrder.enqueue(text); + }; + const initialMembershipGate = createInitialMembershipGate(deliver); + + async function loadAgentPubkeys(initial = false) { try { const pubkeys = await invoke("get_huddle_agent_pubkeys"); + if (disposed) return; agentPubkeys.clear(); for (const pk of pubkeys) agentPubkeys.add(pk); agentsLoaded = true; + if (initial) { + initialMembershipGate.succeed(); + } } catch (e) { // Fail-closed on ALL failures, including refresh after prior success. // Clear the set and mark as not loaded — TTS goes mute until the // next successful refresh. Stale membership must never authorize speech. agentPubkeys.clear(); agentsLoaded = false; + if (initial) { + initialMembershipGate.fail(); + } console.error("[huddle] Failed to load agent pubkeys:", e); } } // Initial load + periodic refresh (catches mid-huddle agent additions). - void loadAgentPubkeys(); + void loadAgentPubkeys(true); const agentRefreshId = window.setInterval(() => { void loadAgentPubkeys(); }, AGENT_PUBKEY_REFRESH_INTERVAL_MS); + // Install the state listener before requesting a snapshot. If a newer + // event arrives while IPC is pending, it supersedes the stale snapshot. + const ttsStateGate = createLatestStateGate<{ tts_enabled: boolean }>( + (state) => { + if (!disposed) speakInOrder.setEnabled(state.tts_enabled); + }, + ); + void listen<{ tts_enabled: boolean }>("huddle-state-changed", (event) => { + if (!disposed) ttsStateGate.applyEvent(event.payload); + }) + .then((unlisten) => { + if (disposed) { + unlisten(); + return; + } + unlistenHuddleState = unlisten; + const applyBootstrap = ttsStateGate.beginSnapshot(); + void invoke<{ tts_enabled: boolean }>("get_huddle_state") + .then((state) => { + if (!disposed) applyBootstrap(state); + }) + .catch((err) => { + if (!disposed) applyBootstrap({ tts_enabled: false }); + console.warn("[huddle] Failed to load TTS state:", err); + }); + }) + .catch((err) => { + speakInOrder.setEnabled(false); + console.warn("[huddle] Failed to listen for TTS state:", err); + }); + // ── Live-only subscription ─────────────────────────────────────────── - // subscribeToChannelLive uses `since: now` — the relay never sends - // historical backlog. Every event delivered is a live message. + // A kind:9, limit:0 subscription receives future fan-out while the relay + // returns no stored rows, including pre-join rows from the current second. // Event-ID dedup handles reconnect replay (same event arriving twice). const seenEventIds = new Set(); const seenOrder: string[] = []; const MAX_SEEN_EVENTS = 5000; - relayClient - .subscribeToChannelLive(ephemeralChannelId, (event) => { + .subscribeLive(buildHuddleTtsLiveFilter(ephemeralChannelId), (event) => { if (disposed) return; - // Defense-in-depth: subscription already filters to kind:9 only. - if (event.kind !== 9) return; - // Dedup by event ID (covers reconnect replay). if (seenEventIds.has(event.id)) return; seenEventIds.add(event.id); @@ -78,20 +143,9 @@ export function useTtsSubscription( if (oldest !== undefined) seenEventIds.delete(oldest); } - // Fail-closed: don't speak until agent list is loaded. - if (!agentsLoaded) return; - // Only speak agent messages — skip human STT transcripts. - if (!agentPubkeys.has(event.pubkey)) return; - if (event.pubkey === selfPubkeyRef.current) return; - if (event.content.trim().length <= 1) return; - // Legacy: skip [System]-prefixed messages from before kind:48106. - if (event.content.startsWith("[System]")) return; - invoke("speak_agent_message", { text: event.content }).catch((err) => { - console.warn( - "[huddle] TTS speak failed (backpressure or pipeline unavailable):", - err, - ); - }); + // Preserve arrival order while the initial authoritative membership + // lookup is pending. A failed lookup clears this buffer fail-closed. + initialMembershipGate.push(event); }) .then((dispose) => { if (disposed) { @@ -106,7 +160,9 @@ export function useTtsSubscription( return () => { disposed = true; + speakInOrder.setEnabled(false); cleanup?.(); + unlistenHuddleState?.(); window.clearInterval(agentRefreshId); }; }, [ephemeralChannelId, selfPubkeyRef]); diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx index 156be00b72..27a0674f80 100644 --- a/desktop/src/features/settings/ui/SettingsPanels.tsx +++ b/desktop/src/features/settings/ui/SettingsPanels.tsx @@ -21,6 +21,7 @@ import { SunMoon, Ticket, UserRound, + Volume2, type LucideIcon, } from "lucide-react"; import type { @@ -84,10 +85,12 @@ import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; import { ProfileSettingsCard } from "./ProfileSettingsCard"; import { UpdateChecker } from "../UpdateChecker"; import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import { VoiceSettingsCard } from "./VoiceSettingsCard"; export type SettingsSection = | "profile" | "notifications" + | "voice" | "experimental" | "agents" | "channel-templates" @@ -107,6 +110,7 @@ export const DEFAULT_SETTINGS_SECTION: SettingsSection = "profile"; const SETTINGS_SECTION_VALUES: readonly SettingsSection[] = [ "profile", "notifications", + "voice", "experimental", "agents", "channel-templates", @@ -168,6 +172,11 @@ export const settingsSections: SettingsSectionDescriptor[] = [ label: "Notifications", icon: BellRing, }, + { + value: "voice", + label: "Voice", + icon: Volume2, + }, { value: "experimental", label: "Experiments", @@ -808,6 +817,8 @@ export function renderSettingsSection( onSetSoundForSlot={props.onSetSoundForSlot} /> ); + case "voice": + return ; case "experimental": return ; case "agents": diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index 20389b420a..8613880571 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -58,6 +58,7 @@ const settingsNavGroups: Array<{ "profile", "appearance", "notifications", + "voice", "shortcuts", "custom-emoji", "local-archive", diff --git a/desktop/src/features/settings/ui/VoiceSettingsCard.tsx b/desktop/src/features/settings/ui/VoiceSettingsCard.tsx new file mode 100644 index 0000000000..3542d6f817 --- /dev/null +++ b/desktop/src/features/settings/ui/VoiceSettingsCard.tsx @@ -0,0 +1,252 @@ +import * as React from "react"; +import { ChevronDown, Play, Volume2 } from "lucide-react"; + +import { invokeTauri } from "@/shared/api/tauri"; +import { cn } from "@/shared/lib/cn"; +import { Button } from "@/shared/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/shared/ui/dropdown-menu"; +import { Switch } from "@/shared/ui/switch"; +import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; +import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import { + selectedVoiceForBackend, + type VoiceRegistryEntry, + voiceOptionLabel, + voicesForBackend, +} from "./voiceSettingsLogic"; + +export type TtsSettings = { + version: number; + agentTextToSpeech: boolean; + voicePreferences: string[]; +}; + +export function VoiceSettingsCard() { + const [settings, setSettings] = React.useState(null); + const [registry, setRegistry] = React.useState([]); + const [busy, setBusy] = React.useState(false); + const [previewing, setPreviewing] = React.useState(false); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + let disposed = false; + Promise.all([ + invokeTauri("get_tts_settings"), + invokeTauri("list_voice_registry"), + ]) + .then(([nextSettings, nextRegistry]) => { + if (!disposed) { + setSettings(nextSettings); + setRegistry(nextRegistry); + } + }) + .catch((loadError) => { + if (!disposed) { + setError( + loadError instanceof Error + ? loadError.message + : "Voice settings could not be loaded.", + ); + } + }); + return () => { + disposed = true; + }; + }, []); + + const saveEnabled = React.useCallback(async (enabled: boolean) => { + setBusy(true); + setError(null); + try { + const saved = await invokeTauri("set_tts_enabled", { + enabled, + }); + setSettings(saved); + } catch (saveError) { + try { + const state = await invokeTauri<{ tts_enabled: boolean }>( + "get_huddle_state", + ); + setSettings((current) => + current + ? { ...current, agentTextToSpeech: state.tts_enabled } + : current, + ); + } catch { + // Keep the last confirmed state when native reconciliation is + // unavailable; the visible save error makes the failure explicit. + } + setError( + saveError instanceof Error + ? saveError.message + : "Voice settings could not be saved.", + ); + } finally { + setBusy(false); + } + }, []); + + const savePocketVoice = React.useCallback(async (voiceKey: string) => { + setBusy(true); + setError(null); + try { + const saved = await invokeTauri("set_pocket_voice", { + voiceKey, + }); + setSettings(saved); + } catch (saveError) { + setError( + saveError instanceof Error + ? saveError.message + : "Voice settings could not be saved.", + ); + } finally { + setBusy(false); + } + }, []); + + const voices = voicesForBackend(registry, "pocket"); + const selectedVoice = selectedVoiceForBackend( + settings?.voicePreferences ?? [], + voices, + ); + const enabled = settings?.agentTextToSpeech ?? true; + const controlsDisabled = !settings || busy || !enabled; + + return ( +
    + + +
    + + +
    + +

    + Read new agent messages aloud in the order they arrive. +

    +
    + { + if (settings) void saveEnabled(checked); + }} + /> +
    +
    + +
    + + +
    +

    Voice

    +

    + Voice files stay private on this device. +

    +
    + +
    + + + + + + { + if (settings) void savePocketVoice(voiceKey); + }} + value={selectedVoice?.key} + > + {voices.map((voice) => ( + + {voiceOptionLabel(voice, voices)} + + ))} + + + + +
    +
    +
    +
    + + {error && ( +

    + {error} +

    + )} +
    +
    + ); +} diff --git a/desktop/src/features/settings/ui/voiceSettingsLogic.test.mjs b/desktop/src/features/settings/ui/voiceSettingsLogic.test.mjs new file mode 100644 index 0000000000..d9e5ce9d03 --- /dev/null +++ b/desktop/src/features/settings/ui/voiceSettingsLogic.test.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + selectedVoiceForBackend, + voiceOptionLabel, + voicesForBackend, +} from "./voiceSettingsLogic.ts"; + +const voice = (key, displayName, fallbackKey = "pocket:mary") => ({ + key, + displayName, + backend: "pocket", + backendName: "Pocket TTS", + availability: "bundled", + fallbackKey, + referenceFile: `${key}.wav`, + provenance: { + source: "bundled", + contentHash: null, + license: null, + sourceUrl: null, + }, +}); + +test("Pocket-only V1 filters the shared registry by backend", () => { + const registry = [ + voice("pocket:mary", "Mary", null), + { ...voice("siri:aaron", "Aaron"), backend: "siri" }, + ]; + assert.deepEqual( + voicesForBackend(registry, "pocket").map((entry) => entry.key), + ["pocket:mary"], + ); +}); + +test("local selection uses the first compatible qualified preference", () => { + const voices = [ + voice("pocket:mary", "Mary", null), + voice("pocket:marius", "Marius"), + ]; + assert.equal( + selectedVoiceForBackend( + ["siri:aaron", "pocket:marius", "pocket:mary"], + voices, + )?.key, + "pocket:marius", + ); +}); + +test("duplicate display labels remain distinct by content-derived key", () => { + const voices = [ + voice("pocket:imported:aaa", "Jim"), + voice("pocket:imported:bbb", "Jim"), + ]; + assert.equal( + selectedVoiceForBackend(["pocket:imported:bbb"], voices)?.key, + "pocket:imported:bbb", + ); + assert.equal(voiceOptionLabel(voices[0], voices), "Jim · aaa"); + assert.equal(voiceOptionLabel(voices[1], voices), "Jim · bbb"); +}); diff --git a/desktop/src/features/settings/ui/voiceSettingsLogic.ts b/desktop/src/features/settings/ui/voiceSettingsLogic.ts new file mode 100644 index 0000000000..30352f2d0f --- /dev/null +++ b/desktop/src/features/settings/ui/voiceSettingsLogic.ts @@ -0,0 +1,57 @@ +export type VoiceAvailability = + | "bundled" + | "installed" + | "downloadable" + | "unavailable"; + +export type VoiceRegistryEntry = { + key: string; + displayName: string; + backend: string; + backendName: string; + availability: VoiceAvailability; + fallbackKey: string | null; + referenceFile: string | null; + provenance: { + source: string; + contentHash: string | null; + license: string | null; + sourceUrl: string | null; + }; +}; + +export function voicesForBackend( + registry: readonly VoiceRegistryEntry[], + backend: string, +): VoiceRegistryEntry[] { + return registry.filter( + (voice) => + voice.backend === backend && + (voice.availability === "bundled" || voice.availability === "installed"), + ); +} + +export function selectedVoiceForBackend( + preferences: readonly string[], + voices: readonly VoiceRegistryEntry[], +): VoiceRegistryEntry | undefined { + for (const key of preferences) { + const voice = voices.find((candidate) => candidate.key === key); + if (voice) return voice; + } + return voices.find((voice) => voice.fallbackKey === null) ?? voices[0]; +} + +export function voiceOptionLabel( + voice: VoiceRegistryEntry, + voices: readonly VoiceRegistryEntry[], +): string { + const duplicateLabel = voices.some( + (candidate) => + candidate.key !== voice.key && + candidate.displayName === voice.displayName, + ); + if (!duplicateLabel) return voice.displayName; + const identitySuffix = voice.key.split(":").at(-1)?.slice(-8) ?? voice.key; + return `${voice.displayName} · ${identitySuffix}`; +} diff --git a/desktop/src/shared/api/relayChannelFilters.test.mjs b/desktop/src/shared/api/relayChannelFilters.test.mjs index 3519c82146..7b89799049 100644 --- a/desktop/src/shared/api/relayChannelFilters.test.mjs +++ b/desktop/src/shared/api/relayChannelFilters.test.mjs @@ -6,6 +6,7 @@ import { buildChannelAuxFilter, buildChannelReactionAuxFilter, buildChannelStructuralAuxFilter, + buildHuddleTtsLiveFilter, } from "./relayChannelFilters.ts"; const CHANNEL = "36411e44-0e2d-4cfe-bd6e-567eb169db9f"; @@ -14,6 +15,14 @@ const IDS = [ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ]; +test("huddle TTS filter is future-only kind:9 with no same-second replay", () => { + assert.deepEqual(buildHuddleTtsLiveFilter(CHANNEL), { + kinds: [9], + "#h": [CHANNEL], + limit: 0, + }); +}); + // Regression: reaction (kind:7) and reaction-removal (kind:5) events carry only // an `e` tag, no channel `h` tag. An `#h`-scoped aux query never matches them, // so removed historical reactions reappear. The aux filters must key on `#e` diff --git a/desktop/src/shared/api/relayChannelFilters.ts b/desktop/src/shared/api/relayChannelFilters.ts index d0c7e7938e..9acaef45c5 100644 --- a/desktop/src/shared/api/relayChannelFilters.ts +++ b/desktop/src/shared/api/relayChannelFilters.ts @@ -6,6 +6,7 @@ import { KIND_DELETION, KIND_NIP29_DELETE_EVENT, KIND_REACTION, + KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_EDIT, } from "@/shared/constants/kinds"; import type { RelaySubscriptionFilter } from "@/shared/api/relayClientShared"; @@ -40,6 +41,17 @@ export function buildChannelFilter( return filter; } +/** Strictly live huddle message filter: zero stored rows, future kind:9 only. */ +export function buildHuddleTtsLiveFilter( + channelId: string, +): RelaySubscriptionFilter { + return { + kinds: [KIND_STREAM_MESSAGE], + "#h": [channelId], + limit: 0, + }; +} + /** * History filter for cold-load and scrollback: message kinds *only*, so the * `limit` budget buys visible message depth. Auxiliary events (reactions, diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 03c05fe877..d14f99ee12 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -141,6 +141,11 @@ type MockSearchProfileSeed = { type E2eConfig = { mode?: "mock" | "relay"; mock?: { + ttsSettings?: { + version: number; + agentTextToSpeech: boolean; + voicePreferences: string[]; + }; /** Advertised HEAD for the first mock project without adding that branch. */ projectHeadBranch?: string; /** Builderlab account returned by hosted-community onboarding. Null/omitted = signed out. */ @@ -9430,6 +9435,88 @@ export function maybeInstallE2eTauriMocks() { window.__BUZZ_E2E_COMMAND_LOG__?.push({ command, payload }); switch (command) { + case "get_tts_settings": + return ( + activeConfig?.mock?.ttsSettings ?? { + version: 1, + agentTextToSpeech: true, + voicePreferences: ["pocket:mary"], + } + ); + case "list_voice_registry": + return [ + { + key: "pocket:mary", + displayName: "Mary", + backend: "pocket", + backendName: "Pocket TTS", + availability: "bundled", + fallbackKey: null, + referenceFile: "reference_sample.wav", + provenance: { + source: "bundled", + contentHash: + "a35b0468382218e9f37a9a7494d1e4b74deaf18d7ced22265b4e325bb55c183f", + license: "CC-BY-4.0", + sourceUrl: "https://datashare.ed.ac.uk/handle/10283/3443", + }, + }, + { + key: "pocket:marius", + displayName: "Marius", + backend: "pocket", + backendName: "Pocket TTS", + availability: "bundled", + fallbackKey: "pocket:mary", + referenceFile: "marius.wav", + provenance: { + source: "bundled", + contentHash: + "076968c3122520f3412eb7090e8c1c3f75fe57be1e24a2f96465583d84c71e16", + license: "CC0-1.0", + sourceUrl: + "https://huggingface.co/kyutai/tts-voices/blob/323332d33f997de8394f24a193e1a76df720e01a/voice-donations/Selfie.wav", + }, + }, + ]; + case "set_tts_enabled": { + const enabled = (payload as { enabled?: boolean })?.enabled; + if (typeof enabled !== "boolean") + throw new Error("Missing text-to-speech enabled state"); + const settings = { + version: 1, + agentTextToSpeech: enabled, + voicePreferences: activeConfig?.mock?.ttsSettings + ?.voicePreferences ?? ["pocket:mary"], + }; + if (activeConfig?.mock) activeConfig.mock.ttsSettings = settings; + return settings; + } + case "set_pocket_voice": { + const voiceKey = (payload as { voiceKey?: string })?.voiceKey; + if (!voiceKey) throw new Error("Missing Pocket voice key"); + const current = activeConfig?.mock?.ttsSettings ?? { + version: 1, + agentTextToSpeech: true, + voicePreferences: ["pocket:mary"], + }; + const firstPocketIndex = current.voicePreferences.findIndex((key) => + key.startsWith("pocket:"), + ); + const preferences = current.voicePreferences.filter( + (key) => !key.startsWith("pocket:"), + ); + preferences.splice( + firstPocketIndex < 0 ? preferences.length : firstPocketIndex, + 0, + voiceKey, + ); + const settings = { ...current, voicePreferences: preferences }; + if (activeConfig?.mock) activeConfig.mock.ttsSettings = settings; + return settings; + } + case "preview_pocket_voice": + return null; case "get_builderlab_auth": return activeConfig?.mock?.builderlabAuth ?? null; case "start_builderlab_login": { diff --git a/desktop/tests/e2e/voice-settings.spec.ts b/desktop/tests/e2e/voice-settings.spec.ts new file mode 100644 index 0000000000..4cc8e75fea --- /dev/null +++ b/desktop/tests/e2e/voice-settings.spec.ts @@ -0,0 +1,78 @@ +import { expect, test } from "@playwright/test"; + +import { waitForAnimations } from "../helpers/animations"; +import { installMockBridge } from "../helpers/bridge"; +import { openSettings } from "../helpers/settings"; + +const SCREENSHOT_PATH = "test-results/voice-settings/pocket-voices.png"; + +test.describe("Pocket voice settings", () => { + test.use({ viewport: { width: 1100, height: 760 } }); + + test("selects and retains a bundled voice while text to speech is off", async ({ + page, + }) => { + await installMockBridge(page); + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "voice"); + + const card = page.getByTestId("settings-voice"); + await expect(card).toBeVisible(); + await expect( + page.getByText("Agent text to speech", { exact: true }), + ).toBeVisible(); + + await page.getByTestId("pocket-voice-selector").click(); + await page.getByRole("menuitemradio", { name: "Marius" }).click(); + await expect(page.getByTestId("pocket-voice-selector")).toContainText( + "Marius", + ); + + await page.getByTestId("agent-text-to-speech-toggle").click(); + await expect(page.getByTestId("pocket-voice-controls")).toHaveAttribute( + "aria-disabled", + "true", + ); + await expect(page.getByTestId("pocket-voice-selector")).toContainText( + "Marius", + ); + + const savedCommands = await page.evaluate(() => + (window.__BUZZ_E2E_COMMAND_LOG__ ?? []) + .filter((entry) => + ["set_pocket_voice", "set_tts_enabled"].includes(entry.command), + ) + .map((entry) => ({ command: entry.command, payload: entry.payload })), + ); + expect(savedCommands).toEqual([ + { + command: "set_pocket_voice", + payload: { voiceKey: "pocket:marius" }, + }, + { + command: "set_tts_enabled", + payload: { enabled: false }, + }, + ]); + }); + + test("captures the complete two-voice settings surface", async ({ page }) => { + await installMockBridge(page, { + ttsSettings: { + version: 1, + agentTextToSpeech: true, + voicePreferences: ["pocket:marius"], + }, + }); + await page.goto("/", { waitUntil: "domcontentloaded" }); + await openSettings(page, "voice"); + + const card = page.getByTestId("settings-voice"); + await expect(card).toBeVisible(); + await expect(page.getByTestId("pocket-voice-selector")).toContainText( + "Marius", + ); + await waitForAnimations(page); + await card.screenshot({ path: SCREENSHOT_PATH }); + }); +}); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index d65a260f30..5f1ce5ea9d 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -128,6 +128,11 @@ export type MockAgentMemoryListing = { }; type MockBridgeOptions = { + ttsSettings?: { + version: number; + agentTextToSpeech: boolean; + voicePreferences: string[]; + }; /** Advertised HEAD for the first mock project without adding that branch. */ projectHeadBranch?: string; /** Relay NIP-11 identity used to sign authoritative repository state. */ diff --git a/desktop/tests/helpers/settings.ts b/desktop/tests/helpers/settings.ts index a63b52453e..c26c0e9195 100644 --- a/desktop/tests/helpers/settings.ts +++ b/desktop/tests/helpers/settings.ts @@ -3,6 +3,7 @@ import { expect, type Page } from "@playwright/test"; type SettingsSection = | "profile" | "notifications" + | "voice" | "agents" | "channel-templates" | "compute" From 44c0e12d332bc9190b06d92c7b9b80a7143190d2 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 17:19:34 -0400 Subject: [PATCH 04/27] fix(desktop): persist mocked voice settings Signed-off-by: John Tennant --- desktop/src/testing/e2eBridge.ts | 10 ++++++++-- desktop/tests/e2e/voice-settings.spec.ts | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index d14f99ee12..4030471618 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -9489,7 +9489,10 @@ export function maybeInstallE2eTauriMocks() { voicePreferences: activeConfig?.mock?.ttsSettings ?.voicePreferences ?? ["pocket:mary"], }; - if (activeConfig?.mock) activeConfig.mock.ttsSettings = settings; + if (activeConfig) { + activeConfig.mock ??= {}; + activeConfig.mock.ttsSettings = settings; + } return settings; } case "set_pocket_voice": { @@ -9512,7 +9515,10 @@ export function maybeInstallE2eTauriMocks() { voiceKey, ); const settings = { ...current, voicePreferences: preferences }; - if (activeConfig?.mock) activeConfig.mock.ttsSettings = settings; + if (activeConfig) { + activeConfig.mock ??= {}; + activeConfig.mock.ttsSettings = settings; + } return settings; } case "preview_pocket_voice": diff --git a/desktop/tests/e2e/voice-settings.spec.ts b/desktop/tests/e2e/voice-settings.spec.ts index 4cc8e75fea..6904484776 100644 --- a/desktop/tests/e2e/voice-settings.spec.ts +++ b/desktop/tests/e2e/voice-settings.spec.ts @@ -29,6 +29,9 @@ test.describe("Pocket voice settings", () => { ); await page.getByTestId("agent-text-to-speech-toggle").click(); + await expect( + page.getByTestId("agent-text-to-speech-toggle"), + ).toHaveAttribute("aria-checked", "false"); await expect(page.getByTestId("pocket-voice-controls")).toHaveAttribute( "aria-disabled", "true", From f2e9108552bcd0b1b51898586190e4305b5988ec Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 19:13:36 -0400 Subject: [PATCH 05/27] fix(desktop): serialize Pocket voice transitions Signed-off-by: John Tennant --- desktop/src-tauri/src/app_state.rs | 14 +- desktop/src-tauri/src/huddle/pipeline.rs | 149 +++++- desktop/src-tauri/src/huddle/tts.rs | 431 ++++++++++++++---- desktop/src-tauri/src/huddle/tts_settings.rs | 153 ++++++- .../src/huddle/tts_voice_selection_tests.rs | 314 ++++++++++++- 5 files changed, 934 insertions(+), 127 deletions(-) diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index 7f7a521141..1a2101a2d7 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -30,8 +30,9 @@ pub struct AppState { /// Workspace-provided relay URL override. Set by `apply_workspace` on app /// init and takes priority over env vars and compile-time defaults. pub relay_url_override: Mutex>, - /// Set during setup when managed agents are eligible for launch restore; - /// consumed after workspace identity/relay install to avoid the fallback. + /// Set during backend setup when managed agents are eligible for launch + /// restore. `apply_workspace` consumes it after installing the workspace + /// relay and identity, so agents never start against the fallback relay. pub managed_agent_restore_pending: AtomicBool, /// Whether desktop may repair managed-agent kind:0 profiles from its local /// records. Disabled by the agent-managed profiles experiment so an agent's @@ -39,8 +40,8 @@ pub struct AppState { pub managed_agent_profile_reconcile_enabled: AtomicBool, /// Shared shutdown signal checked by launch-time agent restoration. pub shutdown_started: AtomicBool, - /// Serializes managed-runtime transitions that change the protected PID set: - /// spawn/register, adoption, stop, shutdown, and sweep snapshots. + /// Serializes every managed-runtime transition that changes the protected + /// PID set: spawn/register, adoption, stop, shutdown, and sweep snapshots. /// Never perform network I/O while holding this lock. pub managed_agent_runtime_transition: Mutex<()>, pub managed_agents_store_lock: Mutex<()>, @@ -50,6 +51,11 @@ pub struct AppState { pub tts_settings: Mutex, pub tts_settings_load_error: Mutex>, pub tts_settings_transition: tokio::sync::Mutex<()>, + /// Tauri app handle — stored after setup so huddle commands can emit + /// `huddle-state-changed` events without needing the handle threaded + /// through every call site. + /// + /// Set once during `setup()` in `lib.rs`; never cleared. pub app_handle: Mutex>, /// Selected audio output device name. `None` = system default. /// Used by `connect_audio_relay` and TTS pipeline when opening sinks. diff --git a/desktop/src-tauri/src/huddle/pipeline.rs b/desktop/src-tauri/src/huddle/pipeline.rs index 478771712d..8a90d386b7 100644 --- a/desktop/src-tauri/src/huddle/pipeline.rs +++ b/desktop/src-tauri/src/huddle/pipeline.rs @@ -16,7 +16,7 @@ use crate::events; use super::models; use super::relay_api::{self, fetch_channel_members, parse_channel_uuid}; -use super::state::{HuddlePhase, VoiceInputMode}; +use super::state::{HuddlePhase, HuddleState, VoiceInputMode}; use super::stt; use super::tts; @@ -216,7 +216,7 @@ pub(crate) async fn maybe_start_tts_pipeline(state: &AppState) -> Result Result Result Result { + let mut huddle = state.huddle()?; + huddle.tts_starting.store(false, Ordering::Release); + if !huddle.tts_enabled + || !matches!(huddle.phase, HuddlePhase::Connected | HuddlePhase::Active) + || huddle.tts_pipeline.is_some() { - let mut hs = state.huddle()?; - hs.tts_starting.store(false, Ordering::Release); - // Phase check: huddle may have been torn down during construction. - if !matches!(hs.phase, HuddlePhase::Connected | HuddlePhase::Active) { - return Ok(false); - } - // Final check: another path may have created a pipeline while we were constructing. - if hs.tts_pipeline.is_some() { - return Ok(false); - } - hs.tts_pipeline = Some(pipeline); + return Ok(false); } - + let voice = state + .tts_settings + .lock() + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}")) + .map(|settings| { + super::tts_settings::pocket_voice_name(&settings.voice_preferences).to_string() + })?; + publish(&voice, &mut huddle); Ok(true) } @@ -364,3 +388,98 @@ pub(crate) fn spawn_transcription_task( } }); } + +#[cfg(test)] +mod tts_start_race_tests { + use std::sync::{atomic::Ordering, Arc, Barrier, Mutex}; + + use crate::app_state::build_app_state; + + use super::{finalize_tts_pipeline_start, HuddlePhase}; + + #[test] + fn construction_reconciles_a_voice_selected_while_starting() { + let state = Arc::new(build_app_state()); + { + let mut huddle = state.huddle().expect("huddle state"); + huddle.phase = HuddlePhase::Active; + huddle.tts_enabled = true; + huddle.tts_starting.store(true, Ordering::Release); + } + + let constructed = Arc::new(Barrier::new(2)); + let publish = Arc::new(Barrier::new(2)); + let selected_voice = Arc::new(Mutex::new(None)); + let worker_state = Arc::clone(&state); + let worker_constructed = Arc::clone(&constructed); + let worker_publish = Arc::clone(&publish); + let worker_voice = Arc::clone(&selected_voice); + let worker = std::thread::spawn(move || { + worker_constructed.wait(); + worker_publish.wait(); + finalize_tts_pipeline_start(&worker_state, |voice, _| { + *worker_voice.lock().expect("selected voice") = Some(voice.to_string()); + }) + }); + + constructed.wait(); + assert!(state + .huddle() + .expect("huddle state") + .tts_starting + .load(Ordering::Acquire)); + state + .tts_settings + .lock() + .expect("text-to-speech settings") + .voice_preferences = vec!["pocket:marius".to_string()]; + publish.wait(); + + assert!(worker.join().expect("starter thread").expect("finalize")); + assert_eq!( + *selected_voice.lock().expect("selected voice"), + Some("marius".to_string()) + ); + } + + #[test] + fn construction_is_discarded_when_disabled_while_starting() { + let state = Arc::new(build_app_state()); + { + let mut huddle = state.huddle().expect("huddle state"); + huddle.phase = HuddlePhase::Active; + huddle.tts_enabled = true; + huddle.tts_starting.store(true, Ordering::Release); + } + + let constructed = Arc::new(Barrier::new(2)); + let publish = Arc::new(Barrier::new(2)); + let did_publish = Arc::new(Mutex::new(false)); + let worker_state = Arc::clone(&state); + let worker_constructed = Arc::clone(&constructed); + let worker_publish = Arc::clone(&publish); + let worker_did_publish = Arc::clone(&did_publish); + let worker = std::thread::spawn(move || { + worker_constructed.wait(); + worker_publish.wait(); + finalize_tts_pipeline_start(&worker_state, |_, _| { + *worker_did_publish.lock().expect("publish flag") = true; + }) + }); + + constructed.wait(); + { + let mut huddle = state.huddle().expect("huddle state"); + huddle.tts_enabled = false; + } + publish.wait(); + + assert!(!worker.join().expect("starter thread").expect("finalize")); + assert!(!*did_publish.lock().expect("publish flag")); + assert!(!state + .huddle() + .expect("huddle state") + .tts_starting + .load(Ordering::Acquire)); + } +} diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index 15b75b0eeb..7b60df2fb5 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -35,10 +35,11 @@ //! can gate microphone input while the agent is speaking. use std::{ + collections::VecDeque, num::NonZero, path::PathBuf, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, mpsc::{self, SyncSender}, Arc, Mutex, MutexGuard, PoisonError, }, @@ -46,9 +47,50 @@ use std::{ time::Duration, }; -use super::pocket::{load_text_to_speech, load_voice_style, SAMPLE_RATE, VOICE_FILE_EXT}; +use super::pocket::{ + load_text_to_speech, load_voice_style, VoiceStyle, SAMPLE_RATE, VOICE_FILE_EXT, +}; use super::preprocessing::{preprocess_for_tts, split_sentences}; +#[derive(Debug)] +struct PendingVoiceChange { + generation: u64, + acknowledged: tokio::sync::oneshot::Sender<()>, +} + +type VoiceChangeAck = Arc>>; +type WorkerVoiceState = (Arc>, Arc, VoiceChangeAck); +type WorkerCancelSignals = (Arc, Arc); +type CancelTextState<'a> = ( + &'a mpsc::Receiver, + &'a mut VecDeque, + &'a mut Option, +); +type CancelSignals<'a> = (&'a AtomicBool, &'a AtomicBool); + +#[derive(Debug)] +struct QueuedText { + generation: u64, + text: String, +} + +#[derive(Clone, Debug)] +pub(crate) struct TtsTextSender { + text_tx: SyncSender, + generation: u64, +} + +impl TtsTextSender { + pub(crate) fn send(&self, text: String) -> Result<(), String> { + self.text_tx + .send(QueuedText { + generation: self.generation, + text, + }) + .map_err(|error| error.to_string()) + } +} + // ── Constants ───────────────────────────────────────────────────────────────── /// Maximum number of queued text items. @@ -122,7 +164,7 @@ const INTER_SENTENCE_SILENCE: f32 = 0.1; #[derive(Debug)] pub struct TtsPipeline { /// Send preprocessed text into the pipeline. - text_tx: SyncSender, + text_tx: SyncSender, /// `true` while the agent is speaking. Shared with the STT pipeline for gating. #[allow(dead_code)] pub tts_active: Arc, @@ -132,9 +174,16 @@ pub struct TtsPipeline { /// Kept alive here so the Arc isn't dropped — the worker holds a clone. #[allow(dead_code)] cancel: Arc, + /// Internal cancellation used only for voice changes. Kept separate so a + /// concurrent human barge-in always clears every queued message. + voice_cancel: Arc, /// Selected manifest voice. The worker reloads only the lightweight style /// when this changes; the warmed Pocket engine and audio player stay alive. voice: Arc>, + /// Tags messages so a voice change drops only pre-change queue entries. + voice_generation: Arc, + /// Completed after the worker drains pre-change text and installs the new style. + voice_change_ack: VoiceChangeAck, /// Worker thread handle — taken on drop to join cleanly. thread: Option>, } @@ -151,15 +200,21 @@ impl TtsPipeline { voice: &str, output_device: Option, ) -> Result { - let (text_tx, text_rx) = mpsc::sync_channel::(TEXT_QUEUE_DEPTH); + let (text_tx, text_rx) = mpsc::sync_channel::(TEXT_QUEUE_DEPTH); let shutdown = Arc::new(AtomicBool::new(false)); // cancel is passed in from HuddleState.tts_cancel — shared with STT for barge-in. let shutdown_worker = Arc::clone(&shutdown); let cancel_worker = Arc::clone(&cancel); + let voice_cancel = Arc::new(AtomicBool::new(false)); + let worker_voice_cancel = Arc::clone(&voice_cancel); let tts_active_worker = Arc::clone(&tts_active); let voice = Arc::new(Mutex::new(voice.to_string())); let voice_worker = Arc::clone(&voice); + let voice_generation = Arc::new(AtomicU64::new(1)); + let worker_voice_generation = Arc::clone(&voice_generation); + let voice_change_ack = Arc::new(Mutex::new(None)); + let worker_voice_change_ack = Arc::clone(&voice_change_ack); let model_dir_worker = model_dir.clone(); let handle = thread::Builder::new() @@ -167,11 +222,15 @@ impl TtsPipeline { .spawn(move || { tts_worker( model_dir_worker, - voice_worker, + ( + voice_worker, + worker_voice_generation, + worker_voice_change_ack, + ), text_rx, tts_active_worker, shutdown_worker, - cancel_worker, + (cancel_worker, worker_voice_cancel), output_device, ) }) @@ -182,7 +241,10 @@ impl TtsPipeline { tts_active, shutdown, cancel, + voice_cancel, voice, + voice_generation, + voice_change_ack, thread: Some(handle), }) } @@ -192,17 +254,25 @@ impl TtsPipeline { /// Non-blocking. Returns `Err` if the queue is full (bounded at /// `TEXT_QUEUE_DEPTH`) — caller may log and discard. pub fn speak(&self, text: String) -> Result<(), String> { - self.text_tx.try_send(text).map_err(|e| { - eprintln!("buzz-desktop: TTS queue saturated, dropping message: {e}"); - format!("TTS queue full, dropping: {e}") - }) + self.text_tx + .try_send(QueuedText { + generation: self.voice_generation.load(Ordering::Acquire), + text, + }) + .map_err(|e| { + eprintln!("buzz-desktop: TTS queue saturated, dropping message: {e}"); + format!("TTS queue full, dropping: {e}") + }) } /// Clone the bounded queue sender so callers can apply backpressure without /// holding the huddle mutex. Disabling TTS drops the receiver and unblocks /// any waiting sender while the shared cancellation flag stops playback. - pub(crate) fn text_sender(&self) -> SyncSender { - self.text_tx.clone() + pub(crate) fn text_sender(&self) -> TtsTextSender { + TtsTextSender { + text_tx: self.text_tx.clone(), + generation: self.voice_generation.load(Ordering::Acquire), + } } /// Select a bundled Pocket voice for subsequent speech. @@ -210,9 +280,23 @@ impl TtsPipeline { /// Current playback and queued text are cancelled immediately so content /// cannot continue in the old voice. The worker keeps its warmed inference /// engine and reloads only the reference style before the next utterance. - pub fn select_voice(&self, voice: &str) { + pub fn select_voice(&self, voice: &str) -> Option> { + begin_voice_change( + &self.voice, + &self.voice_generation, + &self.voice_cancel, + &self.voice_change_ack, + voice, + ) + } + + /// Reconcile the voice of a pipeline that has not been published yet. + /// + /// No caller can enqueue text before publication, so raising the shared + /// cancellation flag here would create a race that could discard the first + /// message queued immediately after installation. + pub(crate) fn select_voice_before_publish(&self, voice: &str) { *self.voice.lock().unwrap_or_else(|error| error.into_inner()) = voice.to_string(); - self.cancel.store(true, Ordering::Release); } /// Signal the worker thread to stop. @@ -242,13 +326,15 @@ impl Drop for TtsPipeline { fn tts_worker( model_dir: PathBuf, - selected_voice: Arc>, - text_rx: mpsc::Receiver, + voice_state: WorkerVoiceState, + text_rx: mpsc::Receiver, tts_active: Arc, shutdown: Arc, - cancel: Arc, + cancel_signals: WorkerCancelSignals, output_device: Option, ) { + let (selected_voice, voice_generation, voice_change_ack) = voice_state; + let (cancel, voice_cancel) = cancel_signals; // ── 1. Initialise TTS engine ────────────────────────────────────────────── let model_dir_str = model_dir.to_string_lossy().to_string(); @@ -259,7 +345,12 @@ fn tts_worker( "buzz-desktop: TTS engine init failed (model_dir={}): {e}. TTS disabled.", model_dir.display() ); - drain_until_shutdown(text_rx, &shutdown); + drain_tts_until_shutdown( + text_rx, + &shutdown, + (&cancel, &voice_cancel), + &voice_change_ack, + ); return; } }; @@ -276,7 +367,12 @@ fn tts_worker( eprintln!( "buzz-desktop: TTS voice style load failed ({voice_name}): {e}. TTS disabled." ); - drain_until_shutdown(text_rx, &shutdown); + drain_tts_until_shutdown( + text_rx, + &shutdown, + (&cancel, &voice_cancel), + &voice_change_ack, + ); return; } }; @@ -309,7 +405,12 @@ fn tts_worker( Ok(h) => h, Err(e) => { eprintln!("buzz-desktop: TTS audio output failed: {e}. TTS disabled."); - drain_until_shutdown(text_rx, &shutdown); + drain_tts_until_shutdown( + text_rx, + &shutdown, + (&cancel, &voice_cancel), + &voice_change_ack, + ); return; } }; @@ -379,6 +480,7 @@ fn tts_worker( let monitor = { let player = Arc::clone(&player); let cancel = Arc::clone(&cancel); + let voice_cancel = Arc::clone(&voice_cancel); let tts_active = Arc::clone(&tts_active); let stop = Arc::clone(&monitor_stop); let player_ops = Arc::clone(&player_ops); @@ -386,12 +488,12 @@ fn tts_worker( .name("tts-barge-in-monitor".into()) .spawn(move || { while !stop.load(Ordering::Acquire) { - if cancel.load(Ordering::Acquire) { + if cancel.load(Ordering::Acquire) || voice_cancel.load(Ordering::Acquire) { let _ops = lock_player_ops(&player_ops); // Re-check under the lock: the worker may have // consumed this cancel (and appended fresh audio) // between the load above and the lock acquisition. - if cancel.load(Ordering::Acquire) { + if cancel.load(Ordering::Acquire) || voice_cancel.load(Ordering::Acquire) { // clear() pauses the persistent player; play() // un-pauses (see handle_cancel_or_shutdown). // Idempotent — safe to repeat every tick until @@ -425,13 +527,16 @@ fn tts_worker( // idle branch below uses it to decide when to drop `tts_active` and to // arm a fresh lead-in cushion for the next utterance. let mut first_append = true; + let mut deferred_text = VecDeque::new(); loop { + let mut no_current_text = None; if handle_cancel_or_shutdown( - &cancel, + (&cancel, &voice_cancel), &shutdown, &tts_active, - &text_rx, + (&text_rx, &mut deferred_text, &mut no_current_text), + &voice_change_ack, Some((&player, &player_ops)), ) { if shutdown.load(Ordering::Acquire) { @@ -446,65 +551,39 @@ fn tts_worker( // Voice changes cancel the old utterance/queue and are observed here, // before receiving subsequent text. A bad bundled asset falls back to // Mary without discarding the already-warmed Pocket engine. - let requested_voice = selected_voice - .lock() - .unwrap_or_else(|error| error.into_inner()) - .clone(); - if requested_voice != voice_name { - let requested_path = model_dir.join(format!("{requested_voice}.{VOICE_FILE_EXT}")); - match load_voice_style(&requested_path) { - Ok(requested_style) => { - style = requested_style; - voice_name = requested_voice; - } - Err(error) => { - use super::pocket::DEFAULT_VOICE; - eprintln!( - "buzz-desktop: Pocket voice {requested_voice} is unavailable ({error}); falling back to Mary" - ); - let fallback_path = model_dir.join(format!("{DEFAULT_VOICE}.{VOICE_FILE_EXT}")); - match load_voice_style(&fallback_path) { - Ok(fallback_style) => { - style = fallback_style; - voice_name = DEFAULT_VOICE.to_string(); - *selected_voice - .lock() - .unwrap_or_else(|lock_error| lock_error.into_inner()) = - DEFAULT_VOICE.to_string(); - } - Err(fallback_error) => { - eprintln!( - "buzz-desktop: Mary voice fallback is unavailable: {fallback_error}" - ); - continue; - } - } - } - } + let voice_ready = + reconcile_selected_voice(&model_dir, &selected_voice, &mut voice_name, &mut style); + acknowledge_voice_change(&voice_change_ack, &voice_cancel); + if !voice_ready { + continue; } - let raw_text = match text_rx.recv_timeout(RECV_TIMEOUT) { - Ok(t) => t, - Err(mpsc::RecvTimeoutError::Timeout) => { - // Nothing queued. If playback has also finished, the agent - // has gone quiet — release the mic gate and reset the - // lead-in so the next utterance gets a fresh cushion. - if player.empty() && !first_append { - tts_active.store(false, Ordering::Release); - first_append = true; + let mut queued_text = Some(match deferred_text.pop_front() { + Some(text) => text, + None => match text_rx.recv_timeout(RECV_TIMEOUT) { + Ok(text) => text, + Err(mpsc::RecvTimeoutError::Timeout) => { + // Nothing queued. If playback has also finished, the agent + // has gone quiet — release the mic gate and reset the + // lead-in so the next utterance gets a fresh cushion. + if player.empty() && !first_append { + tts_active.store(false, Ordering::Release); + first_append = true; + } + continue; } - continue; - } - Err(mpsc::RecvTimeoutError::Disconnected) => break, - }; + Err(mpsc::RecvTimeoutError::Disconnected) => break, + }, + }); // Check cancel again after unblocking — a cancel may have arrived // while we were waiting. if handle_cancel_or_shutdown( - &cancel, + (&cancel, &voice_cancel), &shutdown, &tts_active, - &text_rx, + (&text_rx, &mut deferred_text, &mut queued_text), + &voice_change_ack, Some((&player, &player_ops)), ) { if shutdown.load(Ordering::Acquire) { @@ -513,6 +592,21 @@ fn tts_worker( first_append = true; continue; } + let Some(queued_text) = queued_text else { + continue; + }; + if queued_text.generation < voice_generation.load(Ordering::Acquire) { + continue; + } + let raw_text = queued_text.text; + + // The selected voice can change while this worker is blocked in + // recv_timeout. Reconcile again after receipt so the first message + // queued after an unpublished pipeline is installed cannot use the + // voice captured when construction began. + if !reconcile_selected_voice(&model_dir, &selected_voice, &mut voice_name, &mut style) { + continue; + } // If playback already drained while we were waiting for this item, // the agent is silent — release the mic gate BEFORE preprocessing/ @@ -547,11 +641,13 @@ fn tts_worker( let chunks = group_sentences_into_chunks(&sentences, MAX_CHUNK_CHARS); for chunk in &chunks { + let mut no_current_text = None; if handle_cancel_or_shutdown( - &cancel, + (&cancel, &voice_cancel), &shutdown, &tts_active, - &text_rx, + (&text_rx, &mut deferred_text, &mut no_current_text), + &voice_change_ack, Some((&player, &player_ops)), ) { first_append = true; @@ -591,7 +687,7 @@ fn tts_worker( // does the full consume (drain queue, reset lead-in) on // the next iteration. let _ops = lock_player_ops(&player_ops); - if cancel.load(Ordering::Acquire) { + if cancel.load(Ordering::Acquire) || voice_cancel.load(Ordering::Acquire) { // Nothing appended; the loop-top consume re-arms // `first_append` (the flag is still set — the worker // is its only consumer). @@ -624,11 +720,133 @@ fn tts_worker( let _ = handle.join(); } + finish_voice_change_ack(&voice_change_ack); tts_active.store(false, Ordering::Release); } // ── Helpers ─────────────────────────────────────────────────────────────────── +fn begin_voice_change( + selected_voice: &Mutex, + voice_generation: &AtomicU64, + voice_cancel: &AtomicBool, + voice_change_ack: &VoiceChangeAck, + voice: &str, +) -> Option> { + let mut pending_ack = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()); + let mut selected = selected_voice + .lock() + .unwrap_or_else(|error| error.into_inner()); + if selected.as_str() == voice { + return None; + } + + let (sender, receiver) = tokio::sync::oneshot::channel(); + voice_cancel.store(true, Ordering::Release); + let generation = voice_generation.fetch_add(1, Ordering::AcqRel) + 1; + if let Some(superseded) = pending_ack.replace(PendingVoiceChange { + generation, + acknowledged: sender, + }) { + let _ = superseded.acknowledged.send(()); + } + *selected = voice.to_string(); + Some(receiver) +} + +fn acknowledge_voice_change(voice_change_ack: &VoiceChangeAck, voice_cancel: &AtomicBool) { + let mut pending_ack = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()); + if voice_cancel.load(Ordering::Acquire) { + return; + } + if let Some(pending) = pending_ack.take() { + let _ = pending.acknowledged.send(()); + } +} + +fn finish_voice_change_ack(voice_change_ack: &VoiceChangeAck) { + if let Some(pending) = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + let _ = pending.acknowledged.send(()); + } +} + +fn drain_tts_until_shutdown( + text_rx: mpsc::Receiver, + shutdown: &AtomicBool, + cancel_signals: CancelSignals<'_>, + voice_change_ack: &VoiceChangeAck, +) { + let (cancel, voice_cancel) = cancel_signals; + loop { + if cancel.swap(false, Ordering::AcqRel) | voice_cancel.swap(false, Ordering::AcqRel) { + while text_rx.try_recv().is_ok() {} + } + acknowledge_voice_change(voice_change_ack, voice_cancel); + if shutdown.load(Ordering::Acquire) { + break; + } + match text_rx.recv_timeout(RECV_TIMEOUT) { + Ok(_) | Err(mpsc::RecvTimeoutError::Timeout) => {} + Err(mpsc::RecvTimeoutError::Disconnected) => break, + } + } + finish_voice_change_ack(voice_change_ack); +} + +fn reconcile_selected_voice( + model_dir: &std::path::Path, + selected_voice: &Mutex, + voice_name: &mut String, + style: &mut VoiceStyle, +) -> bool { + let requested_voice = selected_voice + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone(); + if requested_voice == *voice_name { + return true; + } + + let requested_path = model_dir.join(format!("{requested_voice}.{VOICE_FILE_EXT}")); + match load_voice_style(&requested_path) { + Ok(requested_style) => { + *style = requested_style; + *voice_name = requested_voice; + true + } + Err(error) => { + use super::pocket::DEFAULT_VOICE; + eprintln!( + "buzz-desktop: Pocket voice {requested_voice} is unavailable ({error}); falling back to Mary" + ); + let fallback_path = model_dir.join(format!("{DEFAULT_VOICE}.{VOICE_FILE_EXT}")); + match load_voice_style(&fallback_path) { + Ok(fallback_style) => { + *style = fallback_style; + *voice_name = DEFAULT_VOICE.to_string(); + *selected_voice + .lock() + .unwrap_or_else(|lock_error| lock_error.into_inner()) = + DEFAULT_VOICE.to_string(); + true + } + Err(fallback_error) => { + eprintln!("buzz-desktop: Mary voice fallback is unavailable: {fallback_error}"); + false + } + } + } + } +} + /// Check for cancel or shutdown. Returns `true` if the caller should break/continue. /// On cancel: drains the text queue and clears the cancel flag. /// @@ -637,12 +855,15 @@ fn tts_worker( /// it is serialized with the monitor's stale-branch re-check (see the monitor /// block in `tts_worker`). fn handle_cancel_or_shutdown( - cancel: &AtomicBool, + cancel_signals: CancelSignals<'_>, shutdown: &AtomicBool, tts_active: &AtomicBool, - text_rx: &mpsc::Receiver, + text_state: CancelTextState<'_>, + voice_change_ack: &VoiceChangeAck, player: Option<(&rodio::Player, &Mutex<()>)>, ) -> bool { + let (cancel, voice_cancel) = cancel_signals; + let (text_rx, deferred_text, current_text) = text_state; if shutdown.load(Ordering::Acquire) { if let Some((p, ops)) = player { let _ops = lock_player_ops(ops); @@ -651,7 +872,24 @@ fn handle_cancel_or_shutdown( tts_active.store(false, Ordering::Release); return true; } - if cancel.load(Ordering::Acquire) { + if cancel.load(Ordering::Acquire) || voice_cancel.load(Ordering::Acquire) { + // Serialize with begin_voice_change so the generation boundary and + // cancel consumption are observed as one transition. + let pending_voice_change = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()); + // Consume at the serialization point. A later barge-in remains true + // for the next pass instead of being overwritten after queue cleanup. + let barge_in = cancel.swap(false, Ordering::AcqRel); + voice_cancel.store(false, Ordering::Release); + let preserve_generation = (!barge_in) + .then(|| { + pending_voice_change + .as_ref() + .map(|pending| pending.generation) + }) + .flatten(); + retain_cancelled_text(deferred_text, current_text, text_rx, preserve_generation); if let Some((p, ops)) = player { let _ops = lock_player_ops(ops); // `Player::clear()` removes queued sources AND pauses the player @@ -664,11 +902,6 @@ fn handle_cancel_or_shutdown( // Consume the flag under the lock: once released with // `cancel == false`, the monitor's stale branch no-ops instead // of clearing the fresh post-cancel utterance. - while text_rx.try_recv().is_ok() {} - cancel.store(false, Ordering::Release); - } else { - while text_rx.try_recv().is_ok() {} - cancel.store(false, Ordering::Release); } tts_active.store(false, Ordering::Release); return true; @@ -676,6 +909,31 @@ fn handle_cancel_or_shutdown( false } +fn retain_cancelled_text( + deferred_text: &mut VecDeque, + current_text: &mut Option, + text_rx: &mpsc::Receiver, + preserve_generation: Option, +) { + if let Some(generation) = preserve_generation { + deferred_text.retain(|text| text.generation >= generation); + if let Some(text) = current_text.take() { + if text.generation >= generation { + deferred_text.push_front(text); + } + } + while let Ok(text) = text_rx.try_recv() { + if text.generation >= generation { + deferred_text.push_back(text); + } + } + } else { + deferred_text.clear(); + current_text.take(); + while text_rx.try_recv().is_ok() {} + } +} + /// Acquire the `player_ops` lock, recovering from poison. /// /// The data under the mutex is `()` — it only serializes Player mutations — @@ -810,9 +1068,6 @@ fn group_sentences_into_chunks(sentences: &[String], max_chars: usize) -> Vec, + tokio::sync::oneshot::Receiver<()>, +); + const VOICE_AVAILABILITY_BUNDLED: &str = "bundled"; const VOICE_AVAILABILITY_INSTALLED: &str = "installed"; @@ -338,11 +348,28 @@ fn commit_effective_off(state: &AppState) -> Result<(), String> { Ok(()) } +fn enable_tts_runtime(huddle: &mut HuddleState, voice: &str) -> Option { + huddle.tts_enabled = true; + // OFF removes the pipeline. Clear a prior cancellation only when enabling + // a fresh pipeline; an idempotent ON write must not erase a voice + // transition that the existing worker still needs to drain. + if huddle.tts_pipeline.is_none() { + huddle + .tts_cancel + .store(false, std::sync::atomic::Ordering::Release); + } + huddle.tts_pipeline.as_ref().and_then(|pipeline| { + pipeline + .select_voice(voice) + .map(|acknowledged| (Arc::clone(pipeline), acknowledged)) + }) +} + async fn apply_tts_settings( settings: TtsSettings, app: &AppHandle, state: &AppState, -) -> Result { +) -> Result, String> { if settings.version != CURRENT_VERSION { return Err(format!( "Unsupported text-to-speech settings version: {}", @@ -366,18 +393,18 @@ async fn apply_tts_settings( .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? = settings.clone(); + let mut voice_change_wait = None; if settings.agent_text_to_speech { - let active = { + let (active, voice_change_ack) = { let mut huddle = state.huddle()?; - huddle.tts_enabled = true; - huddle - .tts_cancel - .store(false, std::sync::atomic::Ordering::Release); - if let Some(pipeline) = huddle.tts_pipeline.as_ref() { - pipeline.select_voice(&pocket_voice_name(&settings.voice_preferences)); - } - matches!(huddle.phase, HuddlePhase::Connected | HuddlePhase::Active) + let voice_change_ack = + enable_tts_runtime(&mut huddle, &pocket_voice_name(&settings.voice_preferences)); + ( + matches!(huddle.phase, HuddlePhase::Connected | HuddlePhase::Active), + voice_change_ack, + ) }; + voice_change_wait = voice_change_ack; if active { if let Err(error) = super::pipeline::maybe_start_tts_pipeline(state).await { eprintln!("buzz-desktop: could not hot-start text to speech: {error}"); @@ -385,7 +412,50 @@ async fn apply_tts_settings( } state.emit_huddle_state_changed(); } - Ok(settings) + Ok(voice_change_wait) +} + +fn current_settings(state: &AppState) -> Result { + state + .tts_settings + .lock() + .map_err(|error| format!("text-to-speech settings lock poisoned: {error}")) + .map(|settings| settings.clone()) +} + +async fn finish_voice_change(voice_change: Option) -> Result<(), String> { + let Some((pipeline, acknowledged)) = voice_change else { + return Ok(()); + }; + wait_for_voice_change_ack(acknowledged, VOICE_CHANGE_ACK_TIMEOUT, || { + pipeline.is_finished() + }) + .await +} + +async fn wait_for_voice_change_ack( + mut acknowledged: tokio::sync::oneshot::Receiver<()>, + timeout: Duration, + mut worker_is_finished: impl FnMut() -> bool, +) -> Result<(), String> { + let deadline = tokio::time::sleep(timeout); + tokio::pin!(deadline); + loop { + tokio::select! { + _ = &mut acknowledged => return Ok(()), + _ = &mut deadline => { + return Err( + "Pocket TTS is still finishing the previous voice. Turn Agent text to speech off and try again." + .to_string(), + ); + } + _ = tokio::time::sleep(Duration::from_millis(25)) => { + if worker_is_finished() { + return Ok(()); + } + } + } + } } /// Compatibility command for the huddle speaker button. It updates the same @@ -396,14 +466,17 @@ pub async fn set_tts_enabled( app: AppHandle, state: State<'_, AppState>, ) -> Result { - let _transition = state.tts_settings_transition.lock().await; + let transition = state.tts_settings_transition.lock().await; let mut settings = state .tts_settings .lock() .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? .clone(); settings.agent_text_to_speech = enabled; - apply_tts_settings(settings, &app, &state).await + let voice_change = apply_tts_settings(settings, &app, &state).await?; + drop(transition); + finish_voice_change(voice_change).await?; + current_settings(&state) } fn settings_with_pocket_voice( @@ -436,14 +509,17 @@ pub async fn set_pocket_voice( app: AppHandle, state: State<'_, AppState>, ) -> Result { - let _transition = state.tts_settings_transition.lock().await; + let transition = state.tts_settings_transition.lock().await; let settings = state .tts_settings .lock() .map_err(|error| format!("text-to-speech settings lock poisoned: {error}"))? .clone(); let settings = settings_with_pocket_voice(settings, &voice_key)?; - apply_tts_settings(settings, &app, &state).await + let voice_change = apply_tts_settings(settings, &app, &state).await?; + drop(transition); + finish_voice_change(voice_change).await?; + current_settings(&state) } #[tauri::command] @@ -499,6 +575,49 @@ pub async fn preview_pocket_voice( mod tests { use super::*; + #[tokio::test] + async fn stalled_voice_change_returns_an_actionable_error() { + let (_keep_pending, acknowledged) = tokio::sync::oneshot::channel(); + + let error = wait_for_voice_change_ack(acknowledged, Duration::from_millis(1), || false) + .await + .expect_err("stalled worker should time out"); + + assert!(error.contains("Turn Agent text to speech off")); + } + + #[test] + fn idempotent_enable_preserves_an_existing_pipeline_cancel() { + let state = crate::app_state::build_app_state(); + let model_dir = tempfile::tempdir().expect("temp model dir"); + let cancel = state.huddle().expect("huddle state").tts_cancel.clone(); + let pipeline = Arc::new( + super::super::tts::TtsPipeline::new_with_voice( + model_dir.path().to_path_buf(), + Arc::new(std::sync::atomic::AtomicBool::new(false)), + Arc::clone(&cancel), + "reference_sample", + None, + ) + .expect("pipeline"), + ); + pipeline.shutdown(); + for _ in 0..100 { + if pipeline.is_finished() { + break; + } + std::thread::sleep(Duration::from_millis(1)); + } + assert!(pipeline.is_finished(), "test pipeline should stop"); + + cancel.store(true, std::sync::atomic::Ordering::Release); + let mut huddle = state.huddle().expect("huddle state"); + huddle.tts_pipeline = Some(pipeline); + + assert!(enable_tts_runtime(&mut huddle, "reference_sample").is_none()); + assert!(cancel.load(std::sync::atomic::Ordering::Acquire)); + } + #[test] fn defaults_are_backwards_compatible_and_use_mary() { assert_eq!( diff --git a/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs b/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs index a2a8410bba..04bccd4605 100644 --- a/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs +++ b/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; #[test] -fn selecting_a_voice_immediately_raises_cancel_and_retains_the_engine_handle() { +fn selecting_a_voice_raises_only_the_internal_cancel_and_retains_the_engine_handle() { let model_dir = tempfile::tempdir().expect("temp model dir"); let active = Arc::new(AtomicBool::new(false)); let cancel = Arc::new(AtomicBool::new(false)); @@ -16,9 +16,10 @@ fn selecting_a_voice_immediately_raises_cancel_and_retains_the_engine_handle() { ) .expect("pipeline handle"); - pipeline.select_voice("marius"); + let _acknowledged = pipeline.select_voice("marius"); - assert!(cancel.load(Ordering::Acquire)); + assert!(!cancel.load(Ordering::Acquire)); + assert!(pipeline.voice_cancel.load(Ordering::Acquire)); assert_eq!( pipeline .voice @@ -28,3 +29,310 @@ fn selecting_a_voice_immediately_raises_cancel_and_retains_the_engine_handle() { "marius" ); } + +#[test] +fn reconciling_an_unpublished_pipeline_does_not_cancel_its_first_message() { + let model_dir = tempfile::tempdir().expect("temp model dir"); + let active = Arc::new(AtomicBool::new(false)); + let cancel = Arc::new(AtomicBool::new(false)); + let pipeline = TtsPipeline::new_with_voice( + model_dir.path().to_path_buf(), + active, + Arc::clone(&cancel), + "reference_sample", + None, + ) + .expect("pipeline handle"); + + pipeline.select_voice_before_publish("marius"); + + assert!(!cancel.load(Ordering::Acquire)); + assert_eq!( + pipeline + .voice + .lock() + .unwrap_or_else(|error| error.into_inner()) + .as_str(), + "marius" + ); +} + +#[test] +fn received_text_reconciles_a_voice_changed_while_the_worker_was_waiting() { + let model_dir = tempfile::tempdir().expect("temp model dir"); + let bundled_voice = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/pocket-voices/marius.wav"); + std::fs::copy( + &bundled_voice, + model_dir.path().join("reference_sample.wav"), + ) + .expect("Mary test voice"); + std::fs::copy(&bundled_voice, model_dir.path().join("marius.wav")).expect("Marius test voice"); + + let selected_voice = Arc::new(std::sync::Mutex::new("reference_sample".to_string())); + let mut style = + load_voice_style(&model_dir.path().join("reference_sample.wav")).expect("initial style"); + let waiting = Arc::new(std::sync::Barrier::new(2)); + let (text_tx, text_rx) = std::sync::mpsc::channel(); + let worker_voice = Arc::clone(&selected_voice); + let worker_waiting = Arc::clone(&waiting); + let worker_model_dir = model_dir.path().to_path_buf(); + let worker = std::thread::spawn(move || { + let mut voice_name = "reference_sample".to_string(); + worker_waiting.wait(); + let text = text_rx.recv().expect("first queued text"); + assert!(reconcile_selected_voice( + &worker_model_dir, + &worker_voice, + &mut voice_name, + &mut style, + )); + (text, voice_name) + }); + + waiting.wait(); + *selected_voice.lock().expect("selected voice") = "marius".to_string(); + text_tx + .send("first message".to_string()) + .expect("queue first message"); + + assert_eq!( + worker.join().expect("worker"), + ("first message".to_string(), "marius".to_string()) + ); +} + +#[test] +fn an_in_hand_post_change_message_survives_cancellation() { + let selected_voice = Arc::new(std::sync::Mutex::new("reference_sample".to_string())); + let voice_generation = AtomicU64::new(1); + let barge_in = AtomicBool::new(false); + let voice_cancel = Arc::new(AtomicBool::new(false)); + let voice_change_ack = Arc::new(std::sync::Mutex::new(None)); + let (text_tx, text_rx) = std::sync::mpsc::sync_channel(1); + let mut acknowledged = begin_voice_change( + &selected_voice, + &voice_generation, + &voice_cancel, + &voice_change_ack, + "marius", + ) + .expect("voice changed"); + assert!(voice_cancel.load(Ordering::Acquire)); + assert!(matches!( + acknowledged.try_recv(), + Err(tokio::sync::oneshot::error::TryRecvError::Empty) + )); + acknowledge_voice_change(&voice_change_ack, &voice_cancel); + assert!(matches!( + acknowledged.try_recv(), + Err(tokio::sync::oneshot::error::TryRecvError::Empty) + )); + text_tx + .send(QueuedText { + generation: voice_generation.load(Ordering::Acquire), + text: "new message".to_string(), + }) + .expect("new message"); + let mut current_text = Some(text_rx.recv().expect("in-hand new message")); + + let shutdown = AtomicBool::new(false); + let active = AtomicBool::new(true); + let mut deferred_text = VecDeque::from([ + QueuedText { + generation: 1, + text: "old message".to_string(), + }, + QueuedText { + generation: voice_generation.load(Ordering::Acquire), + text: "later new message".to_string(), + }, + ]); + assert!(handle_cancel_or_shutdown( + (&barge_in, &voice_cancel), + &shutdown, + &active, + (&text_rx, &mut deferred_text, &mut current_text), + &voice_change_ack, + None, + )); + acknowledge_voice_change(&voice_change_ack, &voice_cancel); + acknowledged.blocking_recv().expect("voice change ack"); + + assert_eq!( + deferred_text + .pop_front() + .expect("preserved post-change message") + .text, + "new message" + ); + assert_eq!( + deferred_text + .pop_front() + .expect("later post-change message") + .text, + "later new message" + ); + assert!(text_rx.try_recv().is_err()); +} + +#[test] +fn superseding_voice_change_removes_earlier_deferred_messages() { + let selected_voice = std::sync::Mutex::new("reference_sample".to_string()); + let voice_generation = AtomicU64::new(1); + let barge_in = AtomicBool::new(false); + let voice_cancel = AtomicBool::new(false); + let voice_change_ack = Arc::new(std::sync::Mutex::new(None)); + let (_text_tx, text_rx) = std::sync::mpsc::channel(); + let shutdown = AtomicBool::new(false); + let active = AtomicBool::new(true); + let mut deferred_text = VecDeque::new(); + let mut current_text = None; + + let first = begin_voice_change( + &selected_voice, + &voice_generation, + &voice_cancel, + &voice_change_ack, + "marius", + ) + .expect("first voice change"); + deferred_text.push_back(QueuedText { + generation: voice_generation.load(Ordering::Acquire), + text: "message for Marius".to_string(), + }); + assert!(handle_cancel_or_shutdown( + (&barge_in, &voice_cancel), + &shutdown, + &active, + (&text_rx, &mut deferred_text, &mut current_text), + &voice_change_ack, + None, + )); + acknowledge_voice_change(&voice_change_ack, &voice_cancel); + first.blocking_recv().expect("first acknowledgement"); + + let _second = begin_voice_change( + &selected_voice, + &voice_generation, + &voice_cancel, + &voice_change_ack, + "reference_sample", + ) + .expect("second voice change"); + assert!(handle_cancel_or_shutdown( + (&barge_in, &voice_cancel), + &shutdown, + &active, + (&text_rx, &mut deferred_text, &mut current_text), + &voice_change_ack, + None, + )); + + assert!(deferred_text.is_empty()); +} + +#[test] +fn barge_in_clears_deferred_voice_change_messages() { + let barge_in = AtomicBool::new(true); + let voice_cancel = AtomicBool::new(false); + let shutdown = AtomicBool::new(false); + let active = AtomicBool::new(true); + let voice_change_ack = Arc::new(std::sync::Mutex::new(None)); + let (_text_tx, text_rx) = std::sync::mpsc::channel(); + let mut deferred_text = VecDeque::from([QueuedText { + generation: 2, + text: "deferred message".to_string(), + }]); + let mut current_text = None; + + assert!(handle_cancel_or_shutdown( + (&barge_in, &voice_cancel), + &shutdown, + &active, + (&text_rx, &mut deferred_text, &mut current_text), + &voice_change_ack, + None, + )); + + assert!(deferred_text.is_empty()); +} + +#[test] +fn barge_in_during_a_voice_change_clears_post_change_messages() { + let selected_voice = std::sync::Mutex::new("reference_sample".to_string()); + let voice_generation = AtomicU64::new(1); + let barge_in = AtomicBool::new(false); + let voice_cancel = AtomicBool::new(false); + let voice_change_ack = Arc::new(std::sync::Mutex::new(None)); + let (_text_tx, text_rx) = std::sync::mpsc::channel(); + let shutdown = AtomicBool::new(false); + let active = AtomicBool::new(true); + let mut deferred_text = VecDeque::new(); + let mut current_text = None; + + let _acknowledged = begin_voice_change( + &selected_voice, + &voice_generation, + &voice_cancel, + &voice_change_ack, + "marius", + ) + .expect("voice change"); + deferred_text.push_back(QueuedText { + generation: voice_generation.load(Ordering::Acquire), + text: "post-change message".to_string(), + }); + barge_in.store(true, Ordering::Release); + + assert!(handle_cancel_or_shutdown( + (&barge_in, &voice_cancel), + &shutdown, + &active, + (&text_rx, &mut deferred_text, &mut current_text), + &voice_change_ack, + None, + )); + assert!(deferred_text.is_empty()); +} + +#[test] +fn a_sender_captured_before_voice_change_is_stale_even_if_it_sends_after_drain() { + let selected_voice = std::sync::Mutex::new("reference_sample".to_string()); + let voice_generation = Arc::new(AtomicU64::new(1)); + let barge_in = AtomicBool::new(false); + let voice_cancel = AtomicBool::new(false); + let voice_change_ack = Arc::new(std::sync::Mutex::new(None)); + let (text_tx, text_rx) = std::sync::mpsc::sync_channel(1); + let old_sender = TtsTextSender { + text_tx, + generation: voice_generation.load(Ordering::Acquire), + }; + let shutdown = AtomicBool::new(false); + let active = AtomicBool::new(true); + let mut deferred_text = VecDeque::new(); + let mut current_text = None; + + let _acknowledged = begin_voice_change( + &selected_voice, + &voice_generation, + &voice_cancel, + &voice_change_ack, + "marius", + ) + .expect("voice change"); + assert!(handle_cancel_or_shutdown( + (&barge_in, &voice_cancel), + &shutdown, + &active, + (&text_rx, &mut deferred_text, &mut current_text), + &voice_change_ack, + None, + )); + old_sender + .send("late old message".to_string()) + .expect("late send"); + let late = text_rx.recv().expect("late queued text"); + + assert!(late.generation < voice_generation.load(Ordering::Acquire)); +} From 53648e8d0b064fd7298e2820a4958f8b42052217 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 19:34:09 -0400 Subject: [PATCH 06/27] refactor(desktop): split Pocket voice transitions Signed-off-by: John Tennant --- desktop/scripts/check-file-sizes.mjs | 5 +- desktop/src-tauri/src/huddle/tts.rs | 168 +---------------- .../src/huddle/tts_voice_transition.rs | 175 ++++++++++++++++++ 3 files changed, 183 insertions(+), 165 deletions(-) create mode 100644 desktop/src-tauri/src/huddle/tts_voice_transition.rs diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 5f6f48eb14..63dda2b206 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -485,7 +485,10 @@ const overrides = new Map([ // +4 (1081 -> 1085): mesh recovery keeps one app-scoped state object beside // the embedded runtime and coordinator. Probe/re-arm logic lives in // mesh_llm/recovery.rs rather than growing AppState or command modules. - ["src-tauri/src/app_state.rs", 1085], + // +6 (1085 -> 1091): installation-global TTS settings, load diagnostics, + // and transition serialization live beside the huddle state they govern. + // Voice behavior remains split across the huddle modules. + ["src-tauri/src/app_state.rs", 1091], // multi-slot splitting + no-op suppression (#1309): the ReadStateManager // class grew from ~700 lines to ~1019 with the addition of // splitContextsIntoBudgetedSlots (pure fn + 5 tests), publishSplitSlots, diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index 7b60df2fb5..1f1fee361f 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -47,49 +47,12 @@ use std::{ time::Duration, }; -use super::pocket::{ - load_text_to_speech, load_voice_style, VoiceStyle, SAMPLE_RATE, VOICE_FILE_EXT, -}; +use super::pocket::{load_text_to_speech, load_voice_style, SAMPLE_RATE, VOICE_FILE_EXT}; use super::preprocessing::{preprocess_for_tts, split_sentences}; -#[derive(Debug)] -struct PendingVoiceChange { - generation: u64, - acknowledged: tokio::sync::oneshot::Sender<()>, -} - -type VoiceChangeAck = Arc>>; -type WorkerVoiceState = (Arc>, Arc, VoiceChangeAck); -type WorkerCancelSignals = (Arc, Arc); -type CancelTextState<'a> = ( - &'a mpsc::Receiver, - &'a mut VecDeque, - &'a mut Option, -); -type CancelSignals<'a> = (&'a AtomicBool, &'a AtomicBool); - -#[derive(Debug)] -struct QueuedText { - generation: u64, - text: String, -} - -#[derive(Clone, Debug)] -pub(crate) struct TtsTextSender { - text_tx: SyncSender, - generation: u64, -} - -impl TtsTextSender { - pub(crate) fn send(&self, text: String) -> Result<(), String> { - self.text_tx - .send(QueuedText { - generation: self.generation, - text, - }) - .map_err(|error| error.to_string()) - } -} +#[path = "tts_voice_transition.rs"] +mod voice_transition; +use voice_transition::*; // ── Constants ───────────────────────────────────────────────────────────────── @@ -726,58 +689,6 @@ fn tts_worker( // ── Helpers ─────────────────────────────────────────────────────────────────── -fn begin_voice_change( - selected_voice: &Mutex, - voice_generation: &AtomicU64, - voice_cancel: &AtomicBool, - voice_change_ack: &VoiceChangeAck, - voice: &str, -) -> Option> { - let mut pending_ack = voice_change_ack - .lock() - .unwrap_or_else(|error| error.into_inner()); - let mut selected = selected_voice - .lock() - .unwrap_or_else(|error| error.into_inner()); - if selected.as_str() == voice { - return None; - } - - let (sender, receiver) = tokio::sync::oneshot::channel(); - voice_cancel.store(true, Ordering::Release); - let generation = voice_generation.fetch_add(1, Ordering::AcqRel) + 1; - if let Some(superseded) = pending_ack.replace(PendingVoiceChange { - generation, - acknowledged: sender, - }) { - let _ = superseded.acknowledged.send(()); - } - *selected = voice.to_string(); - Some(receiver) -} - -fn acknowledge_voice_change(voice_change_ack: &VoiceChangeAck, voice_cancel: &AtomicBool) { - let mut pending_ack = voice_change_ack - .lock() - .unwrap_or_else(|error| error.into_inner()); - if voice_cancel.load(Ordering::Acquire) { - return; - } - if let Some(pending) = pending_ack.take() { - let _ = pending.acknowledged.send(()); - } -} - -fn finish_voice_change_ack(voice_change_ack: &VoiceChangeAck) { - if let Some(pending) = voice_change_ack - .lock() - .unwrap_or_else(|error| error.into_inner()) - .take() - { - let _ = pending.acknowledged.send(()); - } -} - fn drain_tts_until_shutdown( text_rx: mpsc::Receiver, shutdown: &AtomicBool, @@ -801,52 +712,6 @@ fn drain_tts_until_shutdown( finish_voice_change_ack(voice_change_ack); } -fn reconcile_selected_voice( - model_dir: &std::path::Path, - selected_voice: &Mutex, - voice_name: &mut String, - style: &mut VoiceStyle, -) -> bool { - let requested_voice = selected_voice - .lock() - .unwrap_or_else(|error| error.into_inner()) - .clone(); - if requested_voice == *voice_name { - return true; - } - - let requested_path = model_dir.join(format!("{requested_voice}.{VOICE_FILE_EXT}")); - match load_voice_style(&requested_path) { - Ok(requested_style) => { - *style = requested_style; - *voice_name = requested_voice; - true - } - Err(error) => { - use super::pocket::DEFAULT_VOICE; - eprintln!( - "buzz-desktop: Pocket voice {requested_voice} is unavailable ({error}); falling back to Mary" - ); - let fallback_path = model_dir.join(format!("{DEFAULT_VOICE}.{VOICE_FILE_EXT}")); - match load_voice_style(&fallback_path) { - Ok(fallback_style) => { - *style = fallback_style; - *voice_name = DEFAULT_VOICE.to_string(); - *selected_voice - .lock() - .unwrap_or_else(|lock_error| lock_error.into_inner()) = - DEFAULT_VOICE.to_string(); - true - } - Err(fallback_error) => { - eprintln!("buzz-desktop: Mary voice fallback is unavailable: {fallback_error}"); - false - } - } - } - } -} - /// Check for cancel or shutdown. Returns `true` if the caller should break/continue. /// On cancel: drains the text queue and clears the cancel flag. /// @@ -909,31 +774,6 @@ fn handle_cancel_or_shutdown( false } -fn retain_cancelled_text( - deferred_text: &mut VecDeque, - current_text: &mut Option, - text_rx: &mpsc::Receiver, - preserve_generation: Option, -) { - if let Some(generation) = preserve_generation { - deferred_text.retain(|text| text.generation >= generation); - if let Some(text) = current_text.take() { - if text.generation >= generation { - deferred_text.push_front(text); - } - } - while let Ok(text) = text_rx.try_recv() { - if text.generation >= generation { - deferred_text.push_back(text); - } - } - } else { - deferred_text.clear(); - current_text.take(); - while text_rx.try_recv().is_ok() {} - } -} - /// Acquire the `player_ops` lock, recovering from poison. /// /// The data under the mutex is `()` — it only serializes Player mutations — diff --git a/desktop/src-tauri/src/huddle/tts_voice_transition.rs b/desktop/src-tauri/src/huddle/tts_voice_transition.rs new file mode 100644 index 0000000000..1839db0ee7 --- /dev/null +++ b/desktop/src-tauri/src/huddle/tts_voice_transition.rs @@ -0,0 +1,175 @@ +use std::{ + collections::VecDeque, + path::Path, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + mpsc::{self, SyncSender}, + Arc, Mutex, + }, +}; + +use crate::huddle::pocket::{load_voice_style, VoiceStyle, DEFAULT_VOICE, VOICE_FILE_EXT}; + +#[derive(Debug)] +pub(super) struct PendingVoiceChange { + pub(super) generation: u64, + acknowledged: tokio::sync::oneshot::Sender<()>, +} + +pub(super) type VoiceChangeAck = Arc>>; +pub(super) type WorkerVoiceState = (Arc>, Arc, VoiceChangeAck); +pub(super) type WorkerCancelSignals = (Arc, Arc); +pub(super) type CancelTextState<'a> = ( + &'a mpsc::Receiver, + &'a mut VecDeque, + &'a mut Option, +); +pub(super) type CancelSignals<'a> = (&'a AtomicBool, &'a AtomicBool); + +#[derive(Debug)] +pub(super) struct QueuedText { + pub(super) generation: u64, + pub(super) text: String, +} + +#[derive(Clone, Debug)] +pub(crate) struct TtsTextSender { + pub(super) text_tx: SyncSender, + pub(super) generation: u64, +} + +impl TtsTextSender { + pub(crate) fn send(&self, text: String) -> Result<(), String> { + self.text_tx + .send(QueuedText { + generation: self.generation, + text, + }) + .map_err(|error| error.to_string()) + } +} + +pub(super) fn begin_voice_change( + selected_voice: &Mutex, + voice_generation: &AtomicU64, + voice_cancel: &AtomicBool, + voice_change_ack: &VoiceChangeAck, + voice: &str, +) -> Option> { + let mut pending_ack = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()); + let mut selected = selected_voice + .lock() + .unwrap_or_else(|error| error.into_inner()); + if selected.as_str() == voice { + return None; + } + + let (sender, receiver) = tokio::sync::oneshot::channel(); + voice_cancel.store(true, Ordering::Release); + let generation = voice_generation.fetch_add(1, Ordering::AcqRel) + 1; + if let Some(superseded) = pending_ack.replace(PendingVoiceChange { + generation, + acknowledged: sender, + }) { + let _ = superseded.acknowledged.send(()); + } + *selected = voice.to_string(); + Some(receiver) +} + +pub(super) fn acknowledge_voice_change( + voice_change_ack: &VoiceChangeAck, + voice_cancel: &AtomicBool, +) { + let mut pending_ack = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()); + if voice_cancel.load(Ordering::Acquire) { + return; + } + if let Some(pending) = pending_ack.take() { + let _ = pending.acknowledged.send(()); + } +} + +pub(super) fn finish_voice_change_ack(voice_change_ack: &VoiceChangeAck) { + if let Some(pending) = voice_change_ack + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + { + let _ = pending.acknowledged.send(()); + } +} + +pub(super) fn reconcile_selected_voice( + model_dir: &Path, + selected_voice: &Mutex, + voice_name: &mut String, + style: &mut VoiceStyle, +) -> bool { + let requested_voice = selected_voice + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone(); + if requested_voice == *voice_name { + return true; + } + + let requested_path = model_dir.join(format!("{requested_voice}.{VOICE_FILE_EXT}")); + match load_voice_style(&requested_path) { + Ok(requested_style) => { + *style = requested_style; + *voice_name = requested_voice; + true + } + Err(error) => { + eprintln!( + "buzz-desktop: Pocket voice {requested_voice} is unavailable ({error}); falling back to Mary" + ); + let fallback_path = model_dir.join(format!("{DEFAULT_VOICE}.{VOICE_FILE_EXT}")); + match load_voice_style(&fallback_path) { + Ok(fallback_style) => { + *style = fallback_style; + *voice_name = DEFAULT_VOICE.to_string(); + *selected_voice + .lock() + .unwrap_or_else(|lock_error| lock_error.into_inner()) = + DEFAULT_VOICE.to_string(); + true + } + Err(fallback_error) => { + eprintln!("buzz-desktop: Mary voice fallback is unavailable: {fallback_error}"); + false + } + } + } + } +} + +pub(super) fn retain_cancelled_text( + deferred_text: &mut VecDeque, + current_text: &mut Option, + text_rx: &mpsc::Receiver, + preserve_generation: Option, +) { + if let Some(generation) = preserve_generation { + deferred_text.retain(|text| text.generation >= generation); + if let Some(text) = current_text.take() { + if text.generation >= generation { + deferred_text.push_front(text); + } + } + while let Ok(text) = text_rx.try_recv() { + if text.generation >= generation { + deferred_text.push_back(text); + } + } + } else { + deferred_text.clear(); + current_text.take(); + while text_rx.try_recv().is_ok() {} + } +} From 3278e64cee1b58be34353d3e910c5257b55b5dd8 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 23:55:11 -0400 Subject: [PATCH 07/27] Make TTS size override comment timeless Signed-off-by: John Tennant --- desktop/scripts/check-file-sizes.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 63dda2b206..7abf14b450 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -485,8 +485,8 @@ const overrides = new Map([ // +4 (1081 -> 1085): mesh recovery keeps one app-scoped state object beside // the embedded runtime and coordinator. Probe/re-arm logic lives in // mesh_llm/recovery.rs rather than growing AppState or command modules. - // +6 (1085 -> 1091): installation-global TTS settings, load diagnostics, - // and transition serialization live beside the huddle state they govern. + // Installation-global TTS settings, load diagnostics, and transition + // serialization live beside the huddle state they govern. // Voice behavior remains split across the huddle modules. ["src-tauri/src/app_state.rs", 1091], // multi-slot splitting + no-op suppression (#1309): the ReadStateManager From 342a87bb8491efb7a93c96485e177991a490e71c Mon Sep 17 00:00:00 2001 From: John Tennant Date: Tue, 28 Jul 2026 00:03:08 -0400 Subject: [PATCH 08/27] Cancel in-flight Pocket synthesis Signed-off-by: John Tennant --- crates/buzz-voice/src/pocket.rs | 67 +++++++++++++++++++++++++---- desktop/src-tauri/src/huddle/tts.rs | 21 +++++++-- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/crates/buzz-voice/src/pocket.rs b/crates/buzz-voice/src/pocket.rs index e7daa3ed96..91ec146f24 100644 --- a/crates/buzz-voice/src/pocket.rs +++ b/crates/buzz-voice/src/pocket.rs @@ -50,8 +50,14 @@ //! (`offline-tts-pocket-impl.h` — zero references), and upstream pocket-tts //! has no speed parameter either. -use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; use sherpa_onnx::{GenerationConfig, OfflineTts, OfflineTtsConfig, Wave}; @@ -193,6 +199,12 @@ pub struct PocketTts { inner: OfflineTts, } +#[derive(Debug, Clone, PartialEq)] +pub enum SynthesisOutcome { + Complete(Vec), + Interrupted, +} + /// Build the Pocket TTS engine from the model directory installed by /// `huddle::models`. Returns `Err` if any expected ONNX or JSON file is /// missing — readiness is normally enforced by `is_tts_ready` upstream, but @@ -372,20 +384,44 @@ impl PocketTts { /// directly and is a one-step consistency model. Returns an empty buffer /// for whitespace-only input. pub fn synth_chunk( + &self, + text: &str, + lang: &str, + style: &VoiceStyle, + steps: usize, + ) -> Result, String> { + match self.synth_chunk_interruptible(text, lang, style, steps, || false)? { + SynthesisOutcome::Complete(samples) => Ok(samples), + SynthesisOutcome::Interrupted => Ok(Vec::new()), + } + } + + /// Synthesise one chunk while allowing the caller to stop in-flight model + /// generation. Interrupted output is discarded, so a partial waveform can + /// never be queued after a barge-in, voice change, or shutdown. + pub fn synth_chunk_interruptible( &self, text: &str, _lang: &str, style: &VoiceStyle, _steps: usize, - ) -> Result, String> { + is_interrupted: F, + ) -> Result + where + F: Fn() -> bool + Send + Sync + 'static, + { // Mirror upstream pocket-tts prompt prep — without this short or // unpunctuated inputs can cause the LM's EOS logit to never trip, // producing up to 40 s of "monster breathing" garbage on the first // utterance. See `prepare_pocket_prompt` for the full recipe. let prepared = match prepare_pocket_prompt(text) { Some(p) => p, - None => return Ok(Vec::new()), + None => return Ok(SynthesisOutcome::Complete(Vec::new())), }; + let is_interrupted = Arc::new(is_interrupted); + if is_interrupted() { + return Ok(SynthesisOutcome::Interrupted); + } // Per-call generation hints sherpa-onnx forwards to // `offline-tts-pocket-impl.h`. We only override `max_frames`, and @@ -406,13 +442,22 @@ impl PocketTts { ..Default::default() }; - // No progress callback — synthesis is fast enough that returning the - // whole buffer at once keeps the lookahead pipelining in `tts.rs` - // simple. `None:: bool>` pins the callback type for the - // `generate_with_config` generic parameter. + let callback_interrupted = Arc::new(AtomicBool::new(false)); + let callback_flag = Arc::clone(&callback_interrupted); + let callback_predicate = Arc::clone(&is_interrupted); let audio = self .inner - .generate_with_config(&prepared.text, &cfg, None:: bool>) + .generate_with_config( + &prepared.text, + &cfg, + Some(move |_samples: &[f32], _progress: f32| { + let interrupted = callback_predicate(); + if interrupted { + callback_flag.store(true, Ordering::Release); + } + !interrupted + }), + ) .ok_or_else(|| { format!( "Pocket TTS synthesis failed for text ({} chars)", @@ -428,7 +473,11 @@ impl PocketTts { ); } - Ok(audio.samples().to_vec()) + if callback_interrupted.load(Ordering::Acquire) || is_interrupted() { + Ok(SynthesisOutcome::Interrupted) + } else { + Ok(SynthesisOutcome::Complete(audio.samples().to_vec())) + } } } diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index 1f1fee361f..08dcc77b5f 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -47,7 +47,9 @@ use std::{ time::Duration, }; -use super::pocket::{load_text_to_speech, load_voice_style, SAMPLE_RATE, VOICE_FILE_EXT}; +use super::pocket::{ + load_text_to_speech, load_voice_style, SynthesisOutcome, SAMPLE_RATE, VOICE_FILE_EXT, +}; use super::preprocessing::{preprocess_for_tts, split_sentences}; #[path = "tts_voice_transition.rs"] @@ -622,8 +624,15 @@ fn tts_worker( continue; } - match engine.synth_chunk(text, "en", &style, SYNTH_STEPS) { - Ok(samples) if !samples.is_empty() => { + let synth_cancel = Arc::clone(&cancel); + let synth_voice_cancel = Arc::clone(&voice_cancel); + let synth_shutdown = Arc::clone(&shutdown); + match engine.synth_chunk_interruptible(text, "en", &style, SYNTH_STEPS, move || { + synth_cancel.load(Ordering::Acquire) + || synth_voice_cancel.load(Ordering::Acquire) + || synth_shutdown.load(Ordering::Acquire) + }) { + Ok(SynthesisOutcome::Complete(samples)) if !samples.is_empty() => { let mut audio = clamp_to_full_scale(samples); // Fade-out only — fading-in would attenuate the consonant // onset (see `apply_fade_out` docstring + the @@ -664,7 +673,11 @@ fn tts_worker( // See crossfire review C3. tts_active.store(true, Ordering::Release); } - Ok(_) => {} + Ok(SynthesisOutcome::Complete(_)) => {} + Ok(SynthesisOutcome::Interrupted) => { + first_append = true; + break; + } Err(e) => { eprintln!("buzz-desktop: TTS synth failed: {e}"); } From 8f19af9fae29d67984af7fb35dc282d38f4963d1 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Tue, 28 Jul 2026 00:00:26 -0400 Subject: [PATCH 09/27] Own Pocket prompt contract in settings layer Signed-off-by: John Tennant --- crates/buzz-voice/src/pocket.rs | 136 ++++++++++++-------------------- 1 file changed, 50 insertions(+), 86 deletions(-) diff --git a/crates/buzz-voice/src/pocket.rs b/crates/buzz-voice/src/pocket.rs index 91ec146f24..ae235b93eb 100644 --- a/crates/buzz-voice/src/pocket.rs +++ b/crates/buzz-voice/src/pocket.rs @@ -110,31 +110,19 @@ const SYNTH_SILENCE_SCALE: f32 = 1.0; #[cfg(test)] const SHERPA_ONNX_MAX_FRAMES_DEFAULT: i32 = 500; -/// Tight `max_frames` we ask for on short, padded prompts to bound the +/// Tight `max_frames` we ask for on short prompts to bound the /// original "monster breathing" runaway. 100 LM steps ≈ 8 s of audio — /// roomy for any one-to-four-word utterance the user is likely to elicit /// while still well short of the 40 s upstream default. Chosen with slack so /// we never *truncate* a legitimate short reply. const SHORT_PROMPT_MAX_FRAMES: i32 = 100; -/// Word-count threshold (inclusive) below which we pad the prompt with -/// leading spaces and cap `max_frames` tighter than the upstream default. -/// Matches upstream `pocket_tts.models.tts_model.prepare_text_prompt`. Above -/// this threshold we leave sherpa-onnx's own defaults in place because -/// dropping `frames_after_eos` below the upstream default of 3 can clip the -/// leading audio of multi-clause sentences. +/// Word-count threshold (inclusive) below which we cap `max_frames` tighter +/// than the upstream default. Above this threshold we leave sherpa-onnx's +/// generation limits in place because dropping `frames_after_eos` below the +/// upstream default of 3 can clip the leading audio of multi-clause sentences. const SHORT_PROMPT_WORD_THRESHOLD: usize = 4; -/// Number of leading spaces prepended to short prompts. The upstream Python -/// uses exactly 8 — keep parity rather than tuning blindly. -/// -/// This is upstream's *only* mitigation for the FlowLM cold-start smear on -/// short utterances (kyutai-labs/pocket-tts #91, #70): the autoregressive -/// generation has a 2–3 step "settle" period where the first phoneme can be -/// smeared. The pad must remain whitespace-only: synthesized sacrificial text -/// requires amplitude-threshold trimming, which can eat soft word starts. -const SHORT_PROMPT_PAD_SPACES: usize = 8; - /// sherpa-onnx's documented `frames_after_eos` default. We deliberately do /// *not* override this knob because values below the upstream default of 3 can /// clip the leading audio of multi-clause sentences. The constant exists only @@ -256,18 +244,17 @@ pub fn load_text_to_speech(model_dir: &str) -> Result { /// per-call generation overrides derived from the original text. /// /// `None` for either override means "leave sherpa-onnx's documented default -/// in place". The pipeline only sets `max_frames` (and only for short -/// padded inputs) so it can bound the original "monster breathing" runaway -/// without disturbing the rest of the LM sampling envelope. +/// in place". The pipeline only sets `max_frames` for short prompts, bounding +/// runaway generation without disturbing the rest of the LM sampling envelope. #[derive(Debug, Clone, PartialEq)] pub(crate) struct PreparedPrompt { /// Text to hand to `OfflineTts::generate_with_config`. Capitalized, - /// punctuation-terminated, and (for short inputs) left-padded with - /// spaces — upstream's mitigation for the FlowLM cold-start smear. + /// punctuation-terminated, and prefixed with the production `. ` cold-start + /// mitigation. pub text: String, /// Value to pass via `GenerationConfig.extra["max_frames"]`, or `None` to /// keep the upstream default of 500 LM steps. We only override on short - /// padded prompts where we have a tight expectation on output length. + /// prompts where we have a tight expectation on output length. pub max_frames: Option, } @@ -275,17 +262,20 @@ pub(crate) struct PreparedPrompt { /// `pocket_tts.models.tts_model.prepare_text_prompt`. Sherpa-onnx's C++ /// Pocket TTS impl does not run these preparation steps, so short / /// unpunctuated / lowercase inputs can trigger up to 40 s of runaway -/// generation when the EOS logit never crosses its threshold. We replicate -/// the upstream Python recipe here: +/// generation when the EOS logit never crosses its threshold. +/// +/// We mirror the upstream Python recipe by: /// /// 1. Collapse interior whitespace (already done by `preprocess_for_tts`, but /// cheap to re-check after sentence splitting). /// 2. Capitalize the first letter. /// 3. Append `.` if the text doesn't end in punctuation. -/// 4. If fewer than five words, prepend `SHORT_PROMPT_PAD_SPACES` spaces -/// (upstream's cold-start mitigation — see the constant's docstring) and -/// return a tight [`SHORT_PROMPT_MAX_FRAMES`] cap so the LM can't run -/// away if EOS still doesn't fire. +/// 4. If fewer than five words, return a tight +/// [`SHORT_PROMPT_MAX_FRAMES`] cap so the LM can't run away if EOS still +/// doesn't fire. +/// +/// Buzz also prepends `". "` to every prompt. This sacrificial punctuation +/// lets the autoregressive model settle before it renders the first real word. /// /// We do **not** override `frames_after_eos` — sherpa-onnx's default of 3 /// is what we want. An earlier version set it to 1 on long inputs, which @@ -338,25 +328,20 @@ pub(crate) fn prepare_pocket_prompt(input: &str) -> Option { cleaned.push('.'); } - // Word count of the *cleaned but not padded* text — padding is whitespace - // only and would just lie to the threshold check below. + // Count words before adding the cold-start prefix. let word_count = cleaned.split_whitespace().count(); - let (final_text, max_frames) = if word_count <= SHORT_PROMPT_WORD_THRESHOLD { - let mut padded = String::with_capacity(cleaned.len() + SHORT_PROMPT_PAD_SPACES); - for _ in 0..SHORT_PROMPT_PAD_SPACES { - padded.push(' '); - } - padded.push_str(&cleaned); - (padded, Some(SHORT_PROMPT_MAX_FRAMES)) + let max_frames = if word_count <= SHORT_PROMPT_WORD_THRESHOLD { + Some(SHORT_PROMPT_MAX_FRAMES) } else { - // For everything ≥5 words, fall back to upstream defaults. Overriding - // these is what caused the "first 'yep' is static" regression — the - // upstream LM has been tuned for `frames_after_eos = 3` and - // `max_frames = 500`, and there's no clear win in second-guessing. - (cleaned, None) + // For everything ≥5 words, keep the upstream generation limits. + None }; + let mut final_text = String::with_capacity(cleaned.len() + 2); + final_text.push_str(". "); + final_text.push_str(&cleaned); + Some(PreparedPrompt { text: final_text, max_frames, @@ -494,21 +479,17 @@ mod tests { assert!(prepare_pocket_prompt("\n\t ").is_none()); } - /// Helper: the exact leading sequence prepended to every short prompt — - /// 8 spaces of padding (upstream's cold-start mitigation). - /// Centralising this keeps the assertions readable. - fn short_prefix() -> String { - " ".repeat(SHORT_PROMPT_PAD_SPACES) + /// Production's cold-start prefix. + fn prompt_prefix() -> &'static str { + ". " } #[test] - fn prepare_prompt_pads_and_capitalizes_one_word() { - // The "yep" case Tyler hit in production — bare lowercase one-word - // utterance with no punctuation. Must be padded with the short-prompt - // space pad, capitalized, terminated, with a tight `max_frames` cap - // to bound runaway gen. + fn prepare_prompt_prefixes_and_capitalizes_one_word() { + // A bare lowercase word is prefixed, capitalized, terminated, and + // capped tightly enough to bound runaway generation. let out = prepare_pocket_prompt("yep").expect("non-empty"); - assert_eq!(out.text, format!("{}Yep.", short_prefix())); + assert_eq!(out.text, format!("{}Yep.", prompt_prefix())); assert_eq!(out.max_frames, Some(SHORT_PROMPT_MAX_FRAMES)); const { assert!( @@ -521,27 +502,28 @@ mod tests { #[test] fn prepare_prompt_preserves_existing_punctuation() { let out = prepare_pocket_prompt("yes!").expect("non-empty"); - assert_eq!(out.text, format!("{}Yes!", short_prefix())); // exclamation kept + assert_eq!(out.text, format!("{}Yes!", prompt_prefix())); // exclamation kept let out = prepare_pocket_prompt("really?").expect("non-empty"); - assert_eq!(out.text, format!("{}Really?", short_prefix())); + assert_eq!(out.text, format!("{}Really?", prompt_prefix())); } #[test] fn prepare_prompt_threshold_is_inclusive_at_four_words() { - // 4 words = short (padded + tight max_frames); 5 words = long - // (no padding, no overrides — upstream defaults stand). + // Both prompts are prefixed. Only the 4-word prompt gets a tight + // max_frames cap; the 5-word prompt keeps upstream generation limits. let four = prepare_pocket_prompt("one two three four").expect("non-empty"); assert_eq!( four.text, - format!("{}One two three four.", short_prefix()), - "four-word input should get exactly the space pad" + format!("{}One two three four.", prompt_prefix()), + "four-word input should get exactly the production prefix" ); assert_eq!(four.max_frames, Some(SHORT_PROMPT_MAX_FRAMES)); let five = prepare_pocket_prompt("one two three four five").expect("non-empty"); - assert!( - !five.text.starts_with(' '), - "five-word input should NOT be padded" + assert_eq!( + five.text, + format!("{}One two three four five.", prompt_prefix()), + "five-word input should also get the cold-start prefix" ); assert_eq!( five.max_frames, None, @@ -550,10 +532,10 @@ mod tests { } #[test] - fn prepare_prompt_does_not_pad_long_text() { + fn prepare_prompt_prefixes_long_text_without_capping_it() { let long = "This is a longer sentence that the model should handle just fine."; let out = prepare_pocket_prompt(long).expect("non-empty"); - assert!(!out.text.starts_with(' ')); + assert_eq!(out.text, format!("{}{}", prompt_prefix(), long)); assert_eq!(out.max_frames, None); assert!(out.text.ends_with('.')); } @@ -561,14 +543,14 @@ mod tests { #[test] fn prepare_prompt_collapses_whitespace() { let out = prepare_pocket_prompt("Hello world\n\nfriend").expect("non-empty"); - // 3 words → short → padded. Interior whitespace collapsed. - assert_eq!(out.text, format!("{}Hello world friend.", short_prefix())); + // 3 words → short → prefixed. Interior whitespace collapsed. + assert_eq!(out.text, format!("{}Hello world friend.", prompt_prefix())); } #[test] fn prepare_prompt_does_not_double_capitalize_already_uppercase() { let out = prepare_pocket_prompt("HELLO there").expect("non-empty"); - assert_eq!(out.text, format!("{}HELLO there.", short_prefix())); + assert_eq!(out.text, format!("{}HELLO there.", prompt_prefix())); } #[test] @@ -579,24 +561,6 @@ mod tests { assert!(out.text.contains("Дa.")); } - /// REGRESSION GUARD: short prompts must receive *only* whitespace - /// padding — no sacrificial text. A previous revision prepended a - /// `". . "` cold-start absorber and trimmed the rendered audio back out - /// with an amplitude threshold that could eat soft word onsets. If - /// non-whitespace ever reappears in the pad, the synth output will - /// contain audio for text the user never wrote. - #[test] - fn prepare_prompt_pad_is_whitespace_only() { - let out = prepare_pocket_prompt("I'm happy.").expect("non-empty"); - let pad_len = out.text.len() - "I'm happy.".len(); - assert!( - out.text[..pad_len].chars().all(|c| c == ' '), - "short-prompt pad must be spaces only, got {:?}", - &out.text[..pad_len] - ); - assert_eq!(out.text, format!("{}I'm happy.", short_prefix())); - } - // ── build_generation_extra ─────────────────────────────────────────────── // // Short prompts override only `max_frames`; long prompts emit no extras. From d0125dfcf9554381ef8589865d07f02da7ab5084 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Tue, 28 Jul 2026 00:20:38 -0400 Subject: [PATCH 10/27] Preserve ordered huddle TTS reconnect replay Signed-off-by: John Tennant --- desktop/src-tauri/src/huddle/tts_settings.rs | 8 ++--- .../features/huddle/lib/useTtsSubscription.ts | 32 +++++++++++-------- desktop/src/shared/api/relayClientSession.ts | 5 ++- desktop/src/shared/api/relayClientShared.ts | 1 + .../shared/api/relayReconnectReplay.test.mjs | 27 ++++++++++++++++ .../src/shared/api/relayReconnectReplay.ts | 11 +++++-- 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/desktop/src-tauri/src/huddle/tts_settings.rs b/desktop/src-tauri/src/huddle/tts_settings.rs index 2a07f0999c..a441056f34 100644 --- a/desktop/src-tauri/src/huddle/tts_settings.rs +++ b/desktop/src-tauri/src/huddle/tts_settings.rs @@ -195,8 +195,8 @@ pub(crate) fn load_from_path(path: &Path) -> Result { let value: serde_json::Value = serde_json::from_slice(&bytes) .map_err(|error| format!("text-to-speech settings are not valid JSON: {error}"))?; - // Pre-V1 experiment builds stored an unversioned, incompatible shape. - // Migrate it to deterministic V1 defaults instead of carrying it over. + // Unversioned settings are incompatible with the V1 schema. Use + // deterministic V1 defaults rather than interpreting ambiguous fields. if value.get("version").is_none() { return Ok(TtsSettings::default()); } @@ -211,8 +211,8 @@ pub(crate) fn load_from_path(path: &Path) -> Result { )); } - // Early experiments used one bare Pocket `voiceId`. Preserve the toggle - // and qualify that value into the ordered cross-backend preference schema. + // Legacy V1 settings may contain one bare Pocket `voiceId`. Preserve the + // toggle and qualify it into the ordered cross-backend preference schema. if value.get("voicePreferences").is_none() { let legacy_voice = value .get("voiceId") diff --git a/desktop/src/features/huddle/lib/useTtsSubscription.ts b/desktop/src/features/huddle/lib/useTtsSubscription.ts index 251d7e99b1..8c6d8f67b5 100644 --- a/desktop/src/features/huddle/lib/useTtsSubscription.ts +++ b/desktop/src/features/huddle/lib/useTtsSubscription.ts @@ -132,21 +132,25 @@ export function useTtsSubscription( const seenOrder: string[] = []; const MAX_SEEN_EVENTS = 5000; relayClient - .subscribeLive(buildHuddleTtsLiveFilter(ephemeralChannelId), (event) => { - if (disposed) return; - // Dedup by event ID (covers reconnect replay). - if (seenEventIds.has(event.id)) return; - seenEventIds.add(event.id); - seenOrder.push(event.id); - if (seenOrder.length > MAX_SEEN_EVENTS) { - const oldest = seenOrder.shift(); - if (oldest !== undefined) seenEventIds.delete(oldest); - } + .subscribeLive( + buildHuddleTtsLiveFilter(ephemeralChannelId), + (event) => { + if (disposed) return; + // Dedup by event ID (covers reconnect replay). + if (seenEventIds.has(event.id)) return; + seenEventIds.add(event.id); + seenOrder.push(event.id); + if (seenOrder.length > MAX_SEEN_EVENTS) { + const oldest = seenOrder.shift(); + if (oldest !== undefined) seenEventIds.delete(oldest); + } - // Preserve arrival order while the initial authoritative membership - // lookup is pending. A failed lookup clears this buffer fail-closed. - initialMembershipGate.push(event); - }) + // Preserve arrival order while the initial authoritative membership + // lookup is pending. A failed lookup clears this buffer fail-closed. + initialMembershipGate.push(event); + }, + { replayMissedHistory: true }, + ) .then((dispose) => { if (disposed) { void dispose(); diff --git a/desktop/src/shared/api/relayClientSession.ts b/desktop/src/shared/api/relayClientSession.ts index 84ee10b68d..239e7305f2 100644 --- a/desktop/src/shared/api/relayClientSession.ts +++ b/desktop/src/shared/api/relayClientSession.ts @@ -445,8 +445,9 @@ export class RelayClient { async subscribeLive( filter: RelaySubscriptionFilter, onEvent: (event: RelayEvent) => void, + options?: { replayMissedHistory?: boolean }, ) { - return this.subscribe(filter, onEvent); + return this.subscribe(filter, onEvent, options); } async subscribeToChannelMentionEvents( @@ -600,6 +601,7 @@ export class RelayClient { private async subscribe( filter: RelaySubscriptionFilter, onEvent: (event: RelayEvent) => void, + options?: { replayMissedHistory?: boolean }, ) { await this.ensureConnected(); @@ -621,6 +623,7 @@ export class RelayClient { mode: "live", filter, onEvent, + replayMissedHistory: options?.replayMissedHistory, resolveReady, }); diff --git a/desktop/src/shared/api/relayClientShared.ts b/desktop/src/shared/api/relayClientShared.ts index 9108f7b6d7..7559478b3d 100644 --- a/desktop/src/shared/api/relayClientShared.ts +++ b/desktop/src/shared/api/relayClientShared.ts @@ -58,6 +58,7 @@ type LiveSubscription = { mode: "live"; filter: RelaySubscriptionFilter; onEvent: (event: RelayEvent) => void; + replayMissedHistory?: boolean; resolveReady?: () => void; lastSeenCreatedAt?: number; closedRetryAttempt?: number; diff --git a/desktop/src/shared/api/relayReconnectReplay.test.mjs b/desktop/src/shared/api/relayReconnectReplay.test.mjs index 54254e89de..922d8ade49 100644 --- a/desktop/src/shared/api/relayReconnectReplay.test.mjs +++ b/desktop/src/shared/api/relayReconnectReplay.test.mjs @@ -5,6 +5,7 @@ import { buildReconnectReplayFilter, replayLiveSubscriptions, REPLAY_BATCH_SIZE, + shouldPageReconnectReplay, } from "./relayReconnectReplay.ts"; import { buildChannelFilter } from "./relayChannelFilters.ts"; @@ -113,6 +114,32 @@ test("reconnect replay caps large steady-state limits", () => { }); }); +test("reconnect replay preserves the live-only zero-history contract", () => { + const filter = { + kinds: [9], + "#h": ["channel-1"], + limit: 0, + }; + + assert.deepEqual(replayFilter(filter, 123), { + kinds: [9], + "#h": ["channel-1"], + limit: 0, + since: 123, + }); +}); + +test("missed-history replay is explicit for live-only subscriptions", () => { + const filter = { + kinds: [9], + "#h": ["channel-1"], + limit: 0, + }; + + assert.equal(shouldPageReconnectReplay(filter), false); + assert.equal(shouldPageReconnectReplay(filter, true), true); +}); + test("reconnect replay keeps the stricter existing since window", () => { const filter = { kinds: [9], diff --git a/desktop/src/shared/api/relayReconnectReplay.ts b/desktop/src/shared/api/relayReconnectReplay.ts index 74b752f666..6a09d1c549 100644 --- a/desktop/src/shared/api/relayReconnectReplay.ts +++ b/desktop/src/shared/api/relayReconnectReplay.ts @@ -69,7 +69,11 @@ export function buildReconnectReplayFilter( return replayFilter; } -export function shouldPageReconnectReplay(filter: RelaySubscriptionFilter) { +export function shouldPageReconnectReplay( + filter: RelaySubscriptionFilter, + replayMissedHistory = false, +) { + if (replayMissedHistory) return true; return ( filter.limit > 0 && Array.isArray(filter["#h"]) && @@ -176,7 +180,10 @@ export async function replayLiveSubscriptions({ ); const shouldPageReplay = replaySince !== undefined && - shouldPageReconnectReplay(subscription.filter); + shouldPageReconnectReplay( + subscription.filter, + subscription.replayMissedHistory, + ); return { subId, subscription, replaySince, shouldPageReplay }; }); From c118f5ae45905c5679af9997246bc418481a8ba9 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Tue, 28 Jul 2026 00:36:25 -0400 Subject: [PATCH 11/27] Fix first-event reconnect replay baseline Signed-off-by: John Tennant --- desktop/src/shared/api/relayClientSession.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/desktop/src/shared/api/relayClientSession.ts b/desktop/src/shared/api/relayClientSession.ts index 239e7305f2..6be7fe04af 100644 --- a/desktop/src/shared/api/relayClientSession.ts +++ b/desktop/src/shared/api/relayClientSession.ts @@ -624,6 +624,9 @@ export class RelayClient { filter, onEvent, replayMissedHistory: options?.replayMissedHistory, + lastSeenCreatedAt: options?.replayMissedHistory + ? Math.floor(Date.now() / 1_000) + : undefined, resolveReady, }); From 7e5cc4b60796a4ceb5dfa10629587417dbbff85e Mon Sep 17 00:00:00 2001 From: John Tennant Date: Tue, 28 Jul 2026 00:50:04 -0400 Subject: [PATCH 12/27] Test agent TTS routing in settings layer Signed-off-by: John Tennant --- .../src/huddle/agent_tts_routing_tests.rs | 26 +++++++++++++++ desktop/src-tauri/src/huddle/mod.rs | 33 ++++++++++++++----- 2 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 desktop/src-tauri/src/huddle/agent_tts_routing_tests.rs diff --git a/desktop/src-tauri/src/huddle/agent_tts_routing_tests.rs b/desktop/src-tauri/src/huddle/agent_tts_routing_tests.rs new file mode 100644 index 0000000000..c6ce686853 --- /dev/null +++ b/desktop/src-tauri/src/huddle/agent_tts_routing_tests.rs @@ -0,0 +1,26 @@ +use super::{enqueue_agent_tts_text, normalize_agent_tts_text, MAX_TTS_TEXT_LEN}; + +#[tokio::test] +async fn assistant_plain_text_routes_unchanged_into_voice_pipeline_boundary() { + let (sender, receiver) = std::sync::mpsc::channel(); + let text = "A newly submitted assistant reply.".to_string(); + + enqueue_agent_tts_text(text.clone(), move |queued| { + sender.send(queued).map_err(|error| error.to_string()) + }) + .await + .expect("route assistant text"); + + assert_eq!(receiver.recv().expect("queued text"), text); +} + +#[test] +fn assistant_text_truncation_is_unicode_safe_before_voice_routing() { + let input = "🦀".repeat(MAX_TTS_TEXT_LEN + 1); + let output = normalize_agent_tts_text(input); + assert_eq!( + output.chars().count(), + MAX_TTS_TEXT_LEN + "... message truncated.".chars().count() + ); + assert!(output.ends_with("... message truncated.")); +} diff --git a/desktop/src-tauri/src/huddle/mod.rs b/desktop/src-tauri/src/huddle/mod.rs index 7eaa5add4d..e274916500 100644 --- a/desktop/src-tauri/src/huddle/mod.rs +++ b/desktop/src-tauri/src/huddle/mod.rs @@ -825,6 +825,25 @@ pub fn get_model_status(_state: State<'_, AppState>) -> Result String { + if text.chars().count() > MAX_TTS_TEXT_LEN { + let mut truncated: String = text.chars().take(MAX_TTS_TEXT_LEN).collect(); + truncated.push_str("... message truncated."); + truncated + } else { + text + } +} + +async fn enqueue_agent_tts_text(text: String, enqueue: F) -> Result<(), String> +where + F: FnOnce(String) -> Result<(), String> + Send + 'static, +{ + tokio::task::spawn_blocking(move || enqueue(text)) + .await + .map_err(|error| format!("TTS enqueue task failed: {error}"))? +} + /// Called by the WebView when it receives an incoming agent kind:9 message. /// Lazily starts the TTS pipeline if models are ready but the pipeline hasn't /// been created yet (e.g. models finished downloading after huddle started). @@ -834,13 +853,7 @@ const MAX_TTS_TEXT_LEN: usize = 2000; pub async fn speak_agent_message(text: String, state: State<'_, AppState>) -> Result<(), String> { // Truncate oversized messages — agents shouldn't monologue in a voice huddle. // Use char count (not byte length) to avoid panicking on multi-byte UTF-8. - let text = if text.chars().count() > MAX_TTS_TEXT_LEN { - let mut truncated: String = text.chars().take(MAX_TTS_TEXT_LEN).collect(); - truncated.push_str("... message truncated."); - truncated - } else { - text - }; + let text = normalize_agent_tts_text(text); let needs_pipeline = { let hs = state.huddle()?; @@ -869,15 +882,17 @@ pub async fn speak_agent_message(text: String, state: State<'_, AppState>) -> Re let Some(sender) = sender else { return Ok(()); }; - tokio::task::spawn_blocking(move || { + enqueue_agent_tts_text(text, move |text| { sender .send(text) .map_err(|error| format!("TTS queue closed while waiting to enqueue: {error}")) }) .await - .map_err(|error| format!("TTS enqueue task failed: {error}"))? } +#[cfg(test)] +mod agent_tts_routing_tests; + /// Add an agent to the active huddle. /// /// Steps: From ba455ff9d0050d733834542bde0cace17a92b773 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 15:02:02 -0400 Subject: [PATCH 13/27] Add pitch-preserving TTS playback speed Signed-off-by: John Tennant --- desktop/src-tauri/Cargo.lock | 147 ++++++- desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/examples/tts_speed_probe.rs | 29 ++ desktop/src-tauri/src/app_state.rs | 2 + desktop/src-tauri/src/huddle/mod.rs | 1 + desktop/src-tauri/src/huddle/pipeline.rs | 2 + .../src-tauri/src/huddle/playback_speed.rs | 382 ++++++++++++++++++ desktop/src-tauri/src/huddle/tts.rs | 49 ++- desktop/src-tauri/src/huddle/tts_settings.rs | 2 + .../src/huddle/tts_voice_selection_tests.rs | 1 + desktop/src-tauri/src/lib.rs | 6 + .../features/settings/ui/SettingsPanels.tsx | 2 + .../settings/ui/SpeechPlaybackSettings.tsx | 109 +++++ desktop/src/shared/api/tauri.ts | 8 + 14 files changed, 716 insertions(+), 25 deletions(-) create mode 100644 desktop/src-tauri/examples/tts_speed_probe.rs create mode 100644 desktop/src-tauri/src/huddle/playback_speed.rs create mode 100644 desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 17421c6447..61a9eeeb06 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1064,6 +1064,7 @@ dependencies = [ "serde_yaml", "sha2 0.11.0", "sherpa-onnx", + "ssstretch", "strip-ansi-escapes", "tar", "tauri", @@ -1499,6 +1500,17 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1517,7 +1529,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1754,7 +1766,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.61.3", + "windows 0.62.2", ] [[package]] @@ -2060,6 +2072,68 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "cxx" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash 0.2.0", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap 2.14.0", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap 2.14.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap 2.14.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "darling" version = "0.20.11" @@ -2158,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.118", ] [[package]] @@ -3108,8 +3182,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", - "windows-result 0.3.4", + "windows-link 0.2.1", + "windows-result 0.4.1", ] [[package]] @@ -3854,7 +3928,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.5.3", + "windows-registry 0.6.1", ] [[package]] @@ -3869,7 +3943,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4704,6 +4778,15 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + [[package]] name = "link-section" version = "0.19.0" @@ -6140,7 +6223,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.118", @@ -6708,7 +6791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -7418,7 +7501,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools", "log", "multimap", @@ -8517,6 +8600,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + [[package]] name = "scrypt" version = "0.11.0" @@ -9274,6 +9363,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "ssstretch" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee31a0a494b76c8d3047aac804c5f4eb4b6e96c75414e3043b2212301bb8c6" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -9554,6 +9653,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -10159,7 +10269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -10174,6 +10284,15 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termina" version = "0.3.3" @@ -11795,7 +11914,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -12424,8 +12543,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.61.3", - "windows-core 0.61.2", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index d3c52235fd..8e867dcb80 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -121,6 +121,7 @@ regex = "1" rusqlite = { version = "0.37", features = ["bundled"] } axum = "0.8" rodio = "0.22" +ssstretch = "0.1.0" earshot = "1.0" rubato = "3.0" audioadapter-buffers = "3.0" diff --git a/desktop/src-tauri/examples/tts_speed_probe.rs b/desktop/src-tauri/examples/tts_speed_probe.rs new file mode 100644 index 0000000000..f2a3aba8b3 --- /dev/null +++ b/desktop/src-tauri/examples/tts_speed_probe.rs @@ -0,0 +1,29 @@ +use std::time::Instant; + +const SAMPLE_RATE: u32 = 24_000; + +fn main() { + let input: Vec = (0..SAMPLE_RATE * 10) + .map(|index| { + (2.0 * std::f32::consts::PI * 220.0 * index as f32 / SAMPLE_RATE as f32).sin() * 0.25 + }) + .collect(); + + for speed in [0.75_f32, 1.25, 1.5] { + let mut processor = ssstretch::Stretch::new(); + processor.preset_default(1, SAMPLE_RATE as f32); + let output_len = (input.len() as f32 / speed).round() as usize; + let inputs = [input.clone()]; + let mut outputs = [Vec::with_capacity(output_len)]; + let latency_ms = processor.output_latency() as f64 * 1_000.0 / SAMPLE_RATE as f64; + let started = Instant::now(); + processor.process_vec(&inputs, input.len() as i32, &mut outputs, output_len as i32); + let elapsed = started.elapsed(); + println!( + "{speed:.2}x: latency={latency_ms:.1}ms, CPU={:.2}ms for 10s ({:.3}% realtime), output={}", + elapsed.as_secs_f64() * 1_000.0, + elapsed.as_secs_f64() / 10.0 * 100.0, + outputs[0].len(), + ); + } +} diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index 1a2101a2d7..c07263ea5c 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -60,6 +60,7 @@ pub struct AppState { /// Selected audio output device name. `None` = system default. /// Used by `connect_audio_relay` and TTS pipeline when opening sinks. pub audio_output_device: Mutex>, + pub tts_playback_speed: crate::huddle::playback_speed::PlaybackSpeedControl, /// Port of the localhost media streaming proxy (set during setup). pub media_proxy_port: AtomicU16, /// Set when identity resolution detected a "keyring-locked" state: the @@ -221,6 +222,7 @@ pub fn build_app_state() -> AppState { tts_settings_transition: tokio::sync::Mutex::new(()), app_handle: Mutex::new(None), audio_output_device: Mutex::new(None), + tts_playback_speed: crate::huddle::playback_speed::PlaybackSpeedControl::default(), media_proxy_port: AtomicU16::new(0), prevent_sleep: Arc::new(Mutex::new( crate::prevent_sleep::PreventSleepState::default(), diff --git a/desktop/src-tauri/src/huddle/mod.rs b/desktop/src-tauri/src/huddle/mod.rs index e274916500..bb1f69d5b3 100644 --- a/desktop/src-tauri/src/huddle/mod.rs +++ b/desktop/src-tauri/src/huddle/mod.rs @@ -28,6 +28,7 @@ pub mod audio_output; pub mod jitter; pub mod models; pub mod pipeline; +pub mod playback_speed; pub mod playout; pub mod pocket; pub mod preprocessing; diff --git a/desktop/src-tauri/src/huddle/pipeline.rs b/desktop/src-tauri/src/huddle/pipeline.rs index 8a90d386b7..68d873f4e1 100644 --- a/desktop/src-tauri/src/huddle/pipeline.rs +++ b/desktop/src-tauri/src/huddle/pipeline.rs @@ -223,6 +223,7 @@ pub(crate) async fn maybe_start_tts_pipeline(state: &AppState) -> Result Result); + +impl Default for PlaybackSpeedControl { + fn default() -> Self { + Self(Arc::new(AtomicU32::new(DEFAULT_PLAYBACK_SPEED.to_bits()))) + } +} + +impl PlaybackSpeedControl { + /// Return the current generated-speech playback speed. + pub fn get(&self) -> f32 { + f32::from_bits(self.0.load(Ordering::Acquire)) + } + + /// Update the in-memory speed after validation. + pub fn set(&self, speed: f32) -> Result<(), String> { + validate_speed(speed)?; + self.0.store(speed.to_bits(), Ordering::Release); + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct PersistedPlaybackSettings { + speed: f32, +} + +/// Load the global playback speed during app setup. +pub fn load_playback_speed(app: &AppHandle, control: &PlaybackSpeedControl) -> Result<(), String> { + let path = settings_path(app)?; + let speed = load_from_path(&path)?; + control.set(speed) +} + +/// Return the globally configured generated-speech playback speed. +#[tauri::command] +pub fn get_tts_playback_speed(state: State<'_, AppState>) -> f32 { + state.tts_playback_speed.get() +} + +/// Persist and apply the global generated-speech playback speed. +#[tauri::command] +pub fn set_tts_playback_speed( + speed: f32, + app: AppHandle, + state: State<'_, AppState>, +) -> Result<(), String> { + validate_speed(speed)?; + save_to_path(&settings_path(&app)?, speed)?; + state.tts_playback_speed.set(speed) +} + +fn settings_path(app: &AppHandle) -> Result { + app.path() + .app_data_dir() + .map(|directory| directory.join(SETTINGS_FILE)) + .map_err(|error| format!("resolve TTS playback settings directory: {error}")) +} + +fn load_from_path(path: &Path) -> Result { + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(DEFAULT_PLAYBACK_SPEED); + } + Err(error) => return Err(format!("read {}: {error}", path.display())), + }; + let settings: PersistedPlaybackSettings = serde_json::from_slice(&bytes) + .map_err(|error| format!("parse {}: {error}", path.display()))?; + validate_speed(settings.speed)?; + Ok(settings.speed) +} + +fn save_to_path(path: &Path, speed: f32) -> Result<(), String> { + validate_speed(speed)?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| format!("create {}: {error}", parent.display()))?; + } + let payload = serde_json::to_vec_pretty(&PersistedPlaybackSettings { speed }) + .map_err(|error| format!("serialize TTS playback settings: {error}"))?; + crate::managed_agents::storage::atomic_write_json(path, &payload) +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProcessorKind { + Bypass, + Signalsmith, +} + +/// Stateful streaming pitch-preserving processor. +/// +/// Calls to [`Self::process`] preserve input order. The current Pocket path +/// uses [`Self::process_complete_chunk`] because Pocket already hands the +/// player one complete synthesis chunk; a future progressive synthesizer can +/// feed deltas through `process` and pay only the one-time reported latency. +pub struct PlaybackSpeedProcessor { + inner: Processor, + speed: f32, + input_samples: usize, + output_samples: usize, + cancelled: bool, +} + +enum Processor { + Bypass, + Signalsmith(ssstretch::Stretch), +} + +impl PlaybackSpeedProcessor { + /// Select bypass at 1x or Signalsmith Stretch at a non-unity speed. + pub fn new(speed: f32, sample_rate: u32) -> Result { + validate_speed(speed)?; + let inner = if (speed - DEFAULT_PLAYBACK_SPEED).abs() <= UNITY_EPSILON { + Processor::Bypass + } else { + let mut stretch = ssstretch::Stretch::new(); + stretch.preset_default(1, sample_rate as f32); + Processor::Signalsmith(stretch) + }; + Ok(Self { + inner, + speed, + input_samples: 0, + output_samples: 0, + cancelled: false, + }) + } + + #[cfg(test)] + fn kind(&self) -> ProcessorKind { + match self.inner { + Processor::Bypass => ProcessorKind::Bypass, + Processor::Signalsmith(_) => ProcessorKind::Signalsmith, + } + } + + /// One-time output-side algorithmic latency in samples. + pub fn output_latency(&self) -> usize { + match &self.inner { + Processor::Bypass => 0, + Processor::Signalsmith(stretch) => stretch.output_latency().max(0) as usize, + } + } + + /// Process a progressive input delta without reordering prior deltas. + pub fn process(&mut self, input: &[f32]) -> Result, String> { + if input.is_empty() || self.cancelled { + return Ok(Vec::new()); + } + match &mut self.inner { + Processor::Bypass => Ok(input.to_vec()), + Processor::Signalsmith(stretch) => { + self.input_samples = self.input_samples.saturating_add(input.len()); + let target_output = + (self.input_samples as f64 / self.speed as f64).round() as usize; + let output_len = target_output.saturating_sub(self.output_samples); + let input_len = i32_len(input.len())?; + let output_len_i32 = i32_len(output_len)?; + let inputs = [input.to_vec()]; + let mut outputs = [Vec::with_capacity(output_len)]; + stretch.process_vec(&inputs, input_len, &mut outputs, output_len_i32); + self.output_samples = target_output; + Ok(std::mem::take(&mut outputs[0])) + } + } + } + + /// Process one already-buffered Pocket chunk and compensate DSP pre-roll. + /// + /// This returns exactly `input.len() / speed` samples and does not buffer + /// any later Pocket chunk or the remainder of the response. + pub fn process_complete_chunk(&mut self, input: &[f32]) -> Result, String> { + if matches!(self.inner, Processor::Bypass) { + return self.process(input); + } + + let expected = (input.len() as f64 / self.speed as f64).round() as usize; + let latency = self.output_latency(); + let mut output = self.process(input)?; + output.extend(self.drain()?); + + let end = latency.saturating_add(expected); + if output.len() < end { + return Err(format!( + "time stretcher produced {} samples, need {end}", + output.len() + )); + } + Ok(output[latency..end].to_vec()) + } + + /// Discard all pending processor output after barge-in. + #[allow(dead_code)] // Used by progressive synthesis integrations; current Pocket chunks are atomic. + pub fn cancel(&mut self) { + self.cancelled = true; + if let Processor::Signalsmith(stretch) = &mut self.inner { + stretch.reset(); + } + } + + fn drain(&mut self) -> Result, String> { + if self.cancelled { + return Ok(Vec::new()); + } + let Processor::Signalsmith(stretch) = &mut self.inner else { + return Ok(Vec::new()); + }; + let input_latency = stretch.input_latency().max(0) as usize; + let output_latency = stretch.output_latency().max(0) as usize; + let drain_output = (input_latency as f64 / self.speed as f64).ceil() as usize; + + let inputs = [vec![0.0; input_latency]]; + let mut processed = [Vec::with_capacity(drain_output)]; + stretch.process_vec( + &inputs, + i32_len(input_latency)?, + &mut processed, + i32_len(drain_output)?, + ); + let mut flushed = [Vec::with_capacity(output_latency)]; + stretch.flush_vec(&mut flushed, i32_len(output_latency)?); + processed[0].extend_from_slice(&flushed[0]); + Ok(std::mem::take(&mut processed[0])) + } +} + +/// Validate a generated-speech playback speed. +pub fn validate_speed(speed: f32) -> Result<(), String> { + if speed.is_finite() && (MIN_PLAYBACK_SPEED..=MAX_PLAYBACK_SPEED).contains(&speed) { + Ok(()) + } else { + Err(format!( + "Speech playback speed must be between {MIN_PLAYBACK_SPEED} and {MAX_PLAYBACK_SPEED}" + )) + } +} + +fn i32_len(length: usize) -> Result { + i32::try_from(length).map_err(|_| "audio chunk is too large to process".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE_RATE: u32 = 24_000; + + #[test] + fn selects_bypass_only_at_unity() { + assert_eq!( + PlaybackSpeedProcessor::new(1.0, SAMPLE_RATE) + .expect("bypass") + .kind(), + ProcessorKind::Bypass + ); + assert_eq!( + PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE) + .expect("stretcher") + .kind(), + ProcessorKind::Signalsmith + ); + } + + #[test] + fn chunked_processing_preserves_length_pitch_and_order() { + let input: Vec = (0..24_000) + .map(|sample| { + let frequency = if sample < 12_000 { 220.0 } else { 440.0 }; + (2.0 * std::f32::consts::PI * frequency * sample as f32 / SAMPLE_RATE as f32).sin() + }) + .collect(); + let mut processor = PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE).expect("processor"); + let output = processor + .process_complete_chunk(&input) + .expect("complete output"); + + assert_eq!(output.len(), 19_200); + let first_frequency = zero_crossing_frequency(&output[2_000..7_000], SAMPLE_RATE); + let second_frequency = zero_crossing_frequency(&output[12_000..17_000], SAMPLE_RATE); + assert!( + (first_frequency - 220.0).abs() < 8.0, + "first segment measured {first_frequency} Hz" + ); + assert!( + (second_frequency - 440.0).abs() <= 10.0, + "second segment measured {second_frequency} Hz" + ); + } + + #[test] + fn cancellation_discards_subsequent_chunks_and_tail() { + let mut processor = PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE).expect("processor"); + assert!(!processor + .process(&vec![0.25; 4_800]) + .expect("first chunk") + .is_empty()); + processor.cancel(); + assert!(processor + .process(&vec![0.5; 4_800]) + .expect("cancelled chunk") + .is_empty()); + assert!(processor.drain().expect("cancelled tail").is_empty()); + } + + #[test] + fn processor_preserves_sine_pitch() { + let frequency = 220.0_f32; + let input: Vec = (0..SAMPLE_RATE * 2) + .map(|sample| { + (2.0 * std::f32::consts::PI * frequency * sample as f32 / SAMPLE_RATE as f32).sin() + }) + .collect(); + let mut processor = PlaybackSpeedProcessor::new(1.5, SAMPLE_RATE).expect("processor"); + let output = processor + .process_complete_chunk(&input) + .expect("complete output"); + let measured = zero_crossing_frequency(&output[2_000..], SAMPLE_RATE); + assert!( + (measured - frequency).abs() < 3.0, + "expected {frequency} Hz, measured {measured} Hz" + ); + } + + #[test] + fn non_unity_latency_stays_below_75_ms() { + let processor = PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE).expect("processor"); + let latency_ms = processor.output_latency() as f64 * 1_000.0 / SAMPLE_RATE as f64; + assert!(latency_ms <= 75.0, "algorithmic latency was {latency_ms}ms"); + } + + #[test] + fn persisted_speed_round_trips_and_rejects_invalid_values() { + let directory = tempfile::tempdir().expect("tempdir"); + let path = directory.path().join(SETTINGS_FILE); + save_to_path(&path, 1.25).expect("save"); + assert_eq!(load_from_path(&path).expect("load"), 1.25); + + std::fs::write(&path, br#"{"speed":2.0}"#).expect("invalid fixture"); + assert!(load_from_path(&path).is_err()); + } + + fn zero_crossing_frequency(samples: &[f32], sample_rate: u32) -> f32 { + let crossings = samples + .windows(2) + .filter(|pair| pair[0] <= 0.0 && pair[1] > 0.0) + .count(); + crossings as f32 * sample_rate as f32 / samples.len() as f32 + } +} diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index 08dcc77b5f..91be2fbf48 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -47,6 +47,7 @@ use std::{ time::Duration, }; +use super::playback_speed::{PlaybackSpeedControl, PlaybackSpeedProcessor}; use super::pocket::{ load_text_to_speech, load_voice_style, SynthesisOutcome, SAMPLE_RATE, VOICE_FILE_EXT, }; @@ -164,6 +165,7 @@ impl TtsPipeline { cancel: Arc, voice: &str, output_device: Option, + playback_speed: PlaybackSpeedControl, ) -> Result { let (text_tx, text_rx) = mpsc::sync_channel::(TEXT_QUEUE_DEPTH); let shutdown = Arc::new(AtomicBool::new(false)); @@ -185,19 +187,20 @@ impl TtsPipeline { let handle = thread::Builder::new() .name("tts-worker".into()) .spawn(move || { - tts_worker( - model_dir_worker, - ( + let config = TtsWorkerConfig { + model_dir: model_dir_worker, + voice_state: ( voice_worker, worker_voice_generation, worker_voice_change_ack, ), - text_rx, - tts_active_worker, - shutdown_worker, - (cancel_worker, worker_voice_cancel), + tts_active: tts_active_worker, + shutdown: shutdown_worker, + cancel_signals: (cancel_worker, worker_voice_cancel), output_device, - ) + playback_speed, + }; + tts_worker(config, text_rx) }) .map_err(|e| format!("failed to spawn tts-worker thread: {e}"))?; @@ -289,15 +292,26 @@ impl Drop for TtsPipeline { // ── Worker thread ───────────────────────────────────────────────────────────── -fn tts_worker( +struct TtsWorkerConfig { model_dir: PathBuf, voice_state: WorkerVoiceState, - text_rx: mpsc::Receiver, tts_active: Arc, shutdown: Arc, cancel_signals: WorkerCancelSignals, output_device: Option, -) { + playback_speed: PlaybackSpeedControl, +} + +fn tts_worker(config: TtsWorkerConfig, text_rx: mpsc::Receiver) { + let TtsWorkerConfig { + model_dir, + voice_state, + tts_active, + shutdown, + cancel_signals, + output_device, + playback_speed, + } = config; let (selected_voice, voice_generation, voice_change_ack) = voice_state; let (cancel, voice_cancel) = cancel_signals; // ── 1. Initialise TTS engine ────────────────────────────────────────────── @@ -646,6 +660,19 @@ fn tts_worker( // every chunk a quiet device warm-up window. let buf = build_sentence_append_buffer(&mut first_append, audio, silence_buf_len); + let speed = playback_speed.get(); + let buf = match PlaybackSpeedProcessor::new(speed, SAMPLE_RATE) + .and_then(|mut processor| processor.process_complete_chunk(&buf)) + { + Ok(processed) => processed, + Err(error) => { + eprintln!( + "buzz-desktop: TTS playback-speed processing failed at {speed:.2}x: \ + {error}; using 1x playback" + ); + buf + } + }; // Check-and-append under `player_ops`, serialized with // the monitor: a barge-in may have arrived during diff --git a/desktop/src-tauri/src/huddle/tts_settings.rs b/desktop/src-tauri/src/huddle/tts_settings.rs index a441056f34..83a1f0148d 100644 --- a/desktop/src-tauri/src/huddle/tts_settings.rs +++ b/desktop/src-tauri/src/huddle/tts_settings.rs @@ -540,6 +540,7 @@ pub async fn preview_pocket_voice( .lock() .unwrap_or_else(|error| error.into_inner()) .clone(); + let playback_speed = state.tts_playback_speed.clone(); let voice_name = voice .reference_file .and_then(|file| file.strip_suffix(".wav").map(str::to_string)) @@ -553,6 +554,7 @@ pub async fn preview_pocket_voice( cancel, &voice_name, output_device, + playback_speed, )?; pipeline.speak("Hello! This is how I’ll read agent responses.".to_string())?; let started = std::time::Instant::now(); diff --git a/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs b/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs index 04bccd4605..311920e3dd 100644 --- a/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs +++ b/desktop/src-tauri/src/huddle/tts_voice_selection_tests.rs @@ -13,6 +13,7 @@ fn selecting_a_voice_raises_only_the_internal_cancel_and_retains_the_engine_hand Arc::clone(&cancel), "reference_sample", None, + PlaybackSpeedControl::default(), ) .expect("pipeline handle"); diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 732d7ff84e..1d07d047aa 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -39,6 +39,7 @@ use deep_link::{ use huddle::audio_output::{ get_audio_output_device, list_audio_output_devices, set_audio_output_device, }; +use huddle::playback_speed::{get_tts_playback_speed, load_playback_speed, set_tts_playback_speed}; use huddle::reconnect::reconnect_huddle_audio; use huddle::{ add_agent_to_huddle, check_pipeline_hotstart, confirm_huddle_active, download_voice_models, @@ -400,6 +401,9 @@ pub fn run() { // that will be lost on restart, as that silently breaks channel // memberships, DMs, and relay identity. let state = app_handle.state::(); + if let Err(error) = load_playback_speed(&app_handle, &state.tts_playback_speed) { + eprintln!("buzz-desktop: failed to load TTS playback speed; using 1x: {error}"); + } if let Err(e) = resolve_persisted_identity(&app_handle, &state) { eprintln!("buzz-desktop: fatal: identity resolution failed: {e}"); std::process::exit(1); @@ -904,6 +908,8 @@ pub fn run() { list_audio_output_devices, set_audio_output_device, get_audio_output_device, + get_tts_playback_speed, + set_tts_playback_speed, start_pairing, confirm_pairing_sas, cancel_pairing, diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx index 27a0674f80..fdcac35a8b 100644 --- a/desktop/src/features/settings/ui/SettingsPanels.tsx +++ b/desktop/src/features/settings/ui/SettingsPanels.tsx @@ -82,6 +82,7 @@ import { ActiveAgentCommunitiesSettingsCard } from "./ActiveAgentCommunitiesSett import { AgentDefaultsSettingsCard } from "./AgentDefaultsSettingsCard"; import { HostedCommunitiesSettingsCard } from "./HostedCommunitiesSettingsCard"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; +import { SpeechPlaybackSettings } from "./SpeechPlaybackSettings"; import { ProfileSettingsCard } from "./ProfileSettingsCard"; import { UpdateChecker } from "../UpdateChecker"; import { SettingsSectionHeader } from "./SettingsSectionHeader"; @@ -656,6 +657,7 @@ function ThemeSettingsCard() { )} + ); diff --git a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx new file mode 100644 index 0000000000..0b60a46754 --- /dev/null +++ b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx @@ -0,0 +1,109 @@ +import { useEffect, useRef, useState } from "react"; +import { getTtsPlaybackSpeed, setTtsPlaybackSpeed } from "@/shared/api/tauri"; +import { Button } from "@/shared/ui/button"; +import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; + +const DEFAULT_SPEED = 1; +const MIN_SPEED = 0.75; +const MAX_SPEED = 1.5; +const SPEED_STEP = 0.05; + +export function SpeechPlaybackSettings() { + const [speed, setSpeed] = useState(DEFAULT_SPEED); + const [error, setError] = useState(null); + const committedSpeed = useRef(DEFAULT_SPEED); + + useEffect(() => { + let active = true; + getTtsPlaybackSpeed() + .then((savedSpeed) => { + if (active) { + committedSpeed.current = savedSpeed; + setSpeed(savedSpeed); + } + }) + .catch((cause) => { + if (active) setError(String(cause)); + }); + return () => { + active = false; + }; + }, []); + + const commitSpeed = async (nextSpeed: number) => { + const previous = committedSpeed.current; + if (nextSpeed === previous) return; + committedSpeed.current = nextSpeed; + setSpeed(nextSpeed); + setError(null); + try { + await setTtsPlaybackSpeed(nextSpeed); + } catch (cause) { + if (committedSpeed.current === nextSpeed) { + committedSpeed.current = previous; + setSpeed(previous); + setError(String(cause)); + } + } + }; + + return ( + + +
    +
    +
    + +

    + Changes generated speech playback without changing voice pitch. +

    +
    +
    + + {speed.toFixed(2)}x + + +
    +
    + commitSpeed(Number(event.currentTarget.value))} + onChange={(event) => setSpeed(Number(event.currentTarget.value))} + onKeyUp={(event) => commitSpeed(Number(event.currentTarget.value))} + onPointerUp={(event) => + commitSpeed(Number(event.currentTarget.value)) + } + step={SPEED_STEP} + type="range" + value={speed} + /> + {error ? ( +

    + Could not save playback speed: {error} +

    + ) : null} +
    +
    +
    + ); +} diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts index c57525480e..6eb43c6197 100644 --- a/desktop/src/shared/api/tauri.ts +++ b/desktop/src/shared/api/tauri.ts @@ -1197,6 +1197,14 @@ export const setPreventSleepActive = (active: boolean) => export const setAgentManagedProfiles = (enabled: boolean) => invokeTauri("set_agent_managed_profiles", { enabled }); +export function getTtsPlaybackSpeed(): Promise { + return invokeTauri("get_tts_playback_speed"); +} + +export function setTtsPlaybackSpeed(speed: number): Promise { + return invokeTauri("set_tts_playback_speed", { speed }); +} + /** Returns true on macOS, Windows, and Linux AppImage installs. * Returns false on Linux non-AppImage packages (e.g. .deb) where * Tauri's updater cannot swap the binary. */ From 51905d0cba6175743a98e5a56090db05000abc2b Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 15:15:35 -0400 Subject: [PATCH 14/27] Fix TTS stretcher pre-roll compensation Signed-off-by: John Tennant --- .../src-tauri/src/huddle/playback_speed.rs | 267 +++++++----------- desktop/src-tauri/src/huddle/tts.rs | 6 +- 2 files changed, 97 insertions(+), 176 deletions(-) diff --git a/desktop/src-tauri/src/huddle/playback_speed.rs b/desktop/src-tauri/src/huddle/playback_speed.rs index e327c13ec8..eb34d1cffd 100644 --- a/desktop/src-tauri/src/huddle/playback_speed.rs +++ b/desktop/src-tauri/src/huddle/playback_speed.rs @@ -115,146 +115,76 @@ fn save_to_path(path: &Path, speed: f32) -> Result<(), String> { #[cfg(test)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ProcessorKind { +enum ProcessorKind { Bypass, Signalsmith, } -/// Stateful streaming pitch-preserving processor. -/// -/// Calls to [`Self::process`] preserve input order. The current Pocket path -/// uses [`Self::process_complete_chunk`] because Pocket already hands the -/// player one complete synthesis chunk; a future progressive synthesizer can -/// feed deltas through `process` and pay only the one-time reported latency. -pub struct PlaybackSpeedProcessor { - inner: Processor, - speed: f32, - input_samples: usize, - output_samples: usize, - cancelled: bool, -} - -enum Processor { - Bypass, - Signalsmith(ssstretch::Stretch), -} - -impl PlaybackSpeedProcessor { - /// Select bypass at 1x or Signalsmith Stretch at a non-unity speed. - pub fn new(speed: f32, sample_rate: u32) -> Result { - validate_speed(speed)?; - let inner = if (speed - DEFAULT_PLAYBACK_SPEED).abs() <= UNITY_EPSILON { - Processor::Bypass - } else { - let mut stretch = ssstretch::Stretch::new(); - stretch.preset_default(1, sample_rate as f32); - Processor::Signalsmith(stretch) - }; - Ok(Self { - inner, - speed, - input_samples: 0, - output_samples: 0, - cancelled: false, - }) - } - - #[cfg(test)] - fn kind(&self) -> ProcessorKind { - match self.inner { - Processor::Bypass => ProcessorKind::Bypass, - Processor::Signalsmith(_) => ProcessorKind::Signalsmith, - } - } - - /// One-time output-side algorithmic latency in samples. - pub fn output_latency(&self) -> usize { - match &self.inner { - Processor::Bypass => 0, - Processor::Signalsmith(stretch) => stretch.output_latency().max(0) as usize, - } - } - - /// Process a progressive input delta without reordering prior deltas. - pub fn process(&mut self, input: &[f32]) -> Result, String> { - if input.is_empty() || self.cancelled { - return Ok(Vec::new()); - } - match &mut self.inner { - Processor::Bypass => Ok(input.to_vec()), - Processor::Signalsmith(stretch) => { - self.input_samples = self.input_samples.saturating_add(input.len()); - let target_output = - (self.input_samples as f64 / self.speed as f64).round() as usize; - let output_len = target_output.saturating_sub(self.output_samples); - let input_len = i32_len(input.len())?; - let output_len_i32 = i32_len(output_len)?; - let inputs = [input.to_vec()]; - let mut outputs = [Vec::with_capacity(output_len)]; - stretch.process_vec(&inputs, input_len, &mut outputs, output_len_i32); - self.output_samples = target_output; - Ok(std::mem::take(&mut outputs[0])) - } - } - } - - /// Process one already-buffered Pocket chunk and compensate DSP pre-roll. - /// - /// This returns exactly `input.len() / speed` samples and does not buffer - /// any later Pocket chunk or the remainder of the response. - pub fn process_complete_chunk(&mut self, input: &[f32]) -> Result, String> { - if matches!(self.inner, Processor::Bypass) { - return self.process(input); - } - - let expected = (input.len() as f64 / self.speed as f64).round() as usize; - let latency = self.output_latency(); - let mut output = self.process(input)?; - output.extend(self.drain()?); - - let end = latency.saturating_add(expected); - if output.len() < end { - return Err(format!( - "time stretcher produced {} samples, need {end}", - output.len() - )); - } - Ok(output[latency..end].to_vec()) +#[cfg(test)] +fn processor_kind(speed: f32) -> ProcessorKind { + if (speed - DEFAULT_PLAYBACK_SPEED).abs() <= UNITY_EPSILON { + ProcessorKind::Bypass + } else { + ProcessorKind::Signalsmith } +} - /// Discard all pending processor output after barge-in. - #[allow(dead_code)] // Used by progressive synthesis integrations; current Pocket chunks are atomic. - pub fn cancel(&mut self) { - self.cancelled = true; - if let Processor::Signalsmith(stretch) = &mut self.inner { - stretch.reset(); - } +/// Pitch-preserve one already-buffered Pocket chunk. +/// +/// The returned buffer has exactly `input.len() / speed` samples. Signalsmith +/// starts a reset processor `input_latency` samples before the supplied audio +/// and emits another `output_latency` samples of pre-roll. Both components are +/// removed after draining, so the returned chunk retains its beginning and +/// tail without buffering any later Pocket chunk. +pub fn process_complete_chunk( + input: &[f32], + speed: f32, + sample_rate: u32, +) -> Result, String> { + validate_speed(speed)?; + if input.is_empty() || (speed - DEFAULT_PLAYBACK_SPEED).abs() <= UNITY_EPSILON { + return Ok(input.to_vec()); } - fn drain(&mut self) -> Result, String> { - if self.cancelled { - return Ok(Vec::new()); - } - let Processor::Signalsmith(stretch) = &mut self.inner else { - return Ok(Vec::new()); - }; - let input_latency = stretch.input_latency().max(0) as usize; - let output_latency = stretch.output_latency().max(0) as usize; - let drain_output = (input_latency as f64 / self.speed as f64).ceil() as usize; - - let inputs = [vec![0.0; input_latency]]; - let mut processed = [Vec::with_capacity(drain_output)]; - stretch.process_vec( - &inputs, - i32_len(input_latency)?, - &mut processed, - i32_len(drain_output)?, - ); - let mut flushed = [Vec::with_capacity(output_latency)]; - stretch.flush_vec(&mut flushed, i32_len(output_latency)?); - processed[0].extend_from_slice(&flushed[0]); - Ok(std::mem::take(&mut processed[0])) + let expected = (input.len() as f64 / speed as f64).round() as usize; + let mut stretch = ssstretch::Stretch::new(); + stretch.preset_default(1, sample_rate as f32); + let input_latency = stretch.input_latency().max(0) as usize; + let output_latency = stretch.output_latency().max(0) as usize; + let reset_pre_roll = (input_latency as f64 / speed as f64).ceil() as usize; + + let inputs = [input.to_vec()]; + let mut output = [Vec::with_capacity(expected)]; + stretch.process_vec( + &inputs, + i32_len(input.len())?, + &mut output, + i32_len(expected)?, + ); + + let latency_input = [vec![0.0; input_latency]]; + let mut latency_output = [Vec::with_capacity(reset_pre_roll)]; + stretch.process_vec( + &latency_input, + i32_len(input_latency)?, + &mut latency_output, + i32_len(reset_pre_roll)?, + ); + output[0].extend_from_slice(&latency_output[0]); + + let mut flushed = [Vec::with_capacity(output_latency)]; + stretch.flush_vec(&mut flushed, i32_len(output_latency)?); + output[0].extend_from_slice(&flushed[0]); + + let start = reset_pre_roll.saturating_add(output_latency); + let end = start.saturating_add(expected); + if output[0].len() < end { + return Err(format!( + "time stretcher produced {} samples, need {end}", + output[0].len() + )); } + Ok(output[0][start..end].to_vec()) } /// Validate a generated-speech playback speed. @@ -280,36 +210,30 @@ mod tests { #[test] fn selects_bypass_only_at_unity() { - assert_eq!( - PlaybackSpeedProcessor::new(1.0, SAMPLE_RATE) - .expect("bypass") - .kind(), - ProcessorKind::Bypass - ); - assert_eq!( - PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE) - .expect("stretcher") - .kind(), - ProcessorKind::Signalsmith - ); + assert_eq!(processor_kind(1.0), ProcessorKind::Bypass); + assert_eq!(processor_kind(1.25), ProcessorKind::Signalsmith); } #[test] - fn chunked_processing_preserves_length_pitch_and_order() { + fn complete_processing_preserves_length_pitch_and_order() { let input: Vec = (0..24_000) .map(|sample| { - let frequency = if sample < 12_000 { 220.0 } else { 440.0 }; + let frequency = if sample < 8_000 { + 220.0 + } else if sample < 16_000 { + 440.0 + } else { + 660.0 + }; (2.0 * std::f32::consts::PI * frequency * sample as f32 / SAMPLE_RATE as f32).sin() }) .collect(); - let mut processor = PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE).expect("processor"); - let output = processor - .process_complete_chunk(&input) - .expect("complete output"); + let output = process_complete_chunk(&input, 1.25, SAMPLE_RATE).expect("complete output"); assert_eq!(output.len(), 19_200); - let first_frequency = zero_crossing_frequency(&output[2_000..7_000], SAMPLE_RATE); - let second_frequency = zero_crossing_frequency(&output[12_000..17_000], SAMPLE_RATE); + let first_frequency = zero_crossing_frequency(&output[1_500..5_000], SAMPLE_RATE); + let second_frequency = zero_crossing_frequency(&output[7_500..11_000], SAMPLE_RATE); + let third_frequency = zero_crossing_frequency(&output[14_000..18_000], SAMPLE_RATE); assert!( (first_frequency - 220.0).abs() < 8.0, "first segment measured {first_frequency} Hz" @@ -318,21 +242,10 @@ mod tests { (second_frequency - 440.0).abs() <= 10.0, "second segment measured {second_frequency} Hz" ); - } - - #[test] - fn cancellation_discards_subsequent_chunks_and_tail() { - let mut processor = PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE).expect("processor"); - assert!(!processor - .process(&vec![0.25; 4_800]) - .expect("first chunk") - .is_empty()); - processor.cancel(); - assert!(processor - .process(&vec![0.5; 4_800]) - .expect("cancelled chunk") - .is_empty()); - assert!(processor.drain().expect("cancelled tail").is_empty()); + assert!( + (third_frequency - 660.0).abs() <= 12.0, + "third segment measured {third_frequency} Hz" + ); } #[test] @@ -343,10 +256,15 @@ mod tests { (2.0 * std::f32::consts::PI * frequency * sample as f32 / SAMPLE_RATE as f32).sin() }) .collect(); - let mut processor = PlaybackSpeedProcessor::new(1.5, SAMPLE_RATE).expect("processor"); - let output = processor - .process_complete_chunk(&input) - .expect("complete output"); + let output = process_complete_chunk(&input, 1.5, SAMPLE_RATE).expect("complete output"); + assert!( + root_mean_square(&output[..480]) > 0.2, + "full reset pre-roll was not removed" + ); + assert!( + root_mean_square(&output[output.len() - 480..]) > 0.2, + "speech tail was truncated" + ); let measured = zero_crossing_frequency(&output[2_000..], SAMPLE_RATE); assert!( (measured - frequency).abs() < 3.0, @@ -356,8 +274,9 @@ mod tests { #[test] fn non_unity_latency_stays_below_75_ms() { - let processor = PlaybackSpeedProcessor::new(1.25, SAMPLE_RATE).expect("processor"); - let latency_ms = processor.output_latency() as f64 * 1_000.0 / SAMPLE_RATE as f64; + let mut stretch = ssstretch::Stretch::new(); + stretch.preset_default(1, SAMPLE_RATE as f32); + let latency_ms = stretch.output_latency().max(0) as f64 * 1_000.0 / SAMPLE_RATE as f64; assert!(latency_ms <= 75.0, "algorithmic latency was {latency_ms}ms"); } @@ -379,4 +298,8 @@ mod tests { .count(); crossings as f32 * sample_rate as f32 / samples.len() as f32 } + + fn root_mean_square(samples: &[f32]) -> f32 { + (samples.iter().map(|sample| sample * sample).sum::() / samples.len() as f32).sqrt() + } } diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index 91be2fbf48..c9faf25a05 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -47,7 +47,7 @@ use std::{ time::Duration, }; -use super::playback_speed::{PlaybackSpeedControl, PlaybackSpeedProcessor}; +use super::playback_speed::{process_complete_chunk, PlaybackSpeedControl}; use super::pocket::{ load_text_to_speech, load_voice_style, SynthesisOutcome, SAMPLE_RATE, VOICE_FILE_EXT, }; @@ -661,9 +661,7 @@ fn tts_worker(config: TtsWorkerConfig, text_rx: mpsc::Receiver) { let buf = build_sentence_append_buffer(&mut first_append, audio, silence_buf_len); let speed = playback_speed.get(); - let buf = match PlaybackSpeedProcessor::new(speed, SAMPLE_RATE) - .and_then(|mut processor| processor.process_complete_chunk(&buf)) - { + let buf = match process_complete_chunk(&buf, speed, SAMPLE_RATE) { Ok(processed) => processed, Err(error) => { eprintln!( From 478101a5f00aa1b10f12469f6e484cb3142e517e Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 15:23:20 -0400 Subject: [PATCH 15/27] Test TTS time-stretch onset alignment Signed-off-by: John Tennant --- .../src-tauri/src/huddle/playback_speed.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/desktop/src-tauri/src/huddle/playback_speed.rs b/desktop/src-tauri/src/huddle/playback_speed.rs index eb34d1cffd..91e2963f85 100644 --- a/desktop/src-tauri/src/huddle/playback_speed.rs +++ b/desktop/src-tauri/src/huddle/playback_speed.rs @@ -272,6 +272,35 @@ mod tests { ); } + #[test] + fn complete_processing_preserves_onset_timing() { + let input: Vec = (0..SAMPLE_RATE) + .map(|sample| { + if (4_800..12_000).contains(&sample) || sample >= 16_800 { + (2.0 * std::f32::consts::PI * 220.0 * sample as f32 / SAMPLE_RATE as f32).sin() + } else { + 0.0 + } + }) + .collect(); + let output = process_complete_chunk(&input, 1.25, SAMPLE_RATE).expect("complete output"); + let active_windows = active_window_starts(&output, 240, 0.15); + + let first_onset = *active_windows.first().expect("first tone onset"); + let second_onset = active_windows + .windows(2) + .find_map(|pair| (pair[1] > pair[0] + 480).then_some(pair[1])) + .expect("second tone onset"); + assert!( + (3_200..=4_400).contains(&first_onset), + "first onset shifted to sample {first_onset}" + ); + assert!( + (12_800..=14_000).contains(&second_onset), + "second onset shifted to sample {second_onset}" + ); + } + #[test] fn non_unity_latency_stays_below_75_ms() { let mut stretch = ssstretch::Stretch::new(); @@ -302,4 +331,14 @@ mod tests { fn root_mean_square(samples: &[f32]) -> f32 { (samples.iter().map(|sample| sample * sample).sum::() / samples.len() as f32).sqrt() } + + fn active_window_starts(samples: &[f32], window: usize, threshold: f32) -> Vec { + samples + .chunks_exact(window) + .enumerate() + .filter_map(|(index, chunk)| { + (root_mean_square(chunk) > threshold).then_some(index * window) + }) + .collect() + } } From 3a117ac08ee959ab19e5885bb6125a746eded850 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 15:31:55 -0400 Subject: [PATCH 16/27] Support TTS speed in desktop screenshot harness Signed-off-by: John Tennant --- desktop/src/testing/e2eBridge.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 4030471618..291763e87e 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -2798,6 +2798,7 @@ let mockClosedChannelLiveSubscription = false; const realSockets = new Map(); let mockManagedAgents: MockManagedAgent[] = []; let mockManagedAgentRuntimes: MockManagedAgentRuntimeRow[] = []; +let mockTtsPlaybackSpeed = 1; // Mutable `save_subscriptions` table mirror — TEST-ONLY. // @@ -9523,6 +9524,11 @@ export function maybeInstallE2eTauriMocks() { } case "preview_pocket_voice": return null; + case "get_tts_playback_speed": + return mockTtsPlaybackSpeed; + case "set_tts_playback_speed": + mockTtsPlaybackSpeed = (payload as { speed: number }).speed; + return null; case "get_builderlab_auth": return activeConfig?.mock?.builderlabAuth ?? null; case "start_builderlab_login": { From 9d308440e0af5a6f6e540631b532472e19ec838d Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 17:08:02 -0400 Subject: [PATCH 17/27] Place playback speed in Voice settings Signed-off-by: John Tennant --- desktop/src/features/settings/ui/SettingsPanels.tsx | 2 -- desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx | 2 +- desktop/src/features/settings/ui/VoiceSettingsCard.tsx | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx index fdcac35a8b..27a0674f80 100644 --- a/desktop/src/features/settings/ui/SettingsPanels.tsx +++ b/desktop/src/features/settings/ui/SettingsPanels.tsx @@ -82,7 +82,6 @@ import { ActiveAgentCommunitiesSettingsCard } from "./ActiveAgentCommunitiesSett import { AgentDefaultsSettingsCard } from "./AgentDefaultsSettingsCard"; import { HostedCommunitiesSettingsCard } from "./HostedCommunitiesSettingsCard"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; -import { SpeechPlaybackSettings } from "./SpeechPlaybackSettings"; import { ProfileSettingsCard } from "./ProfileSettingsCard"; import { UpdateChecker } from "../UpdateChecker"; import { SettingsSectionHeader } from "./SettingsSectionHeader"; @@ -657,7 +656,6 @@ function ThemeSettingsCard() { )} - ); diff --git a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx index 0b60a46754..879915b125 100644 --- a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx +++ b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx @@ -48,7 +48,7 @@ export function SpeechPlaybackSettings() { }; return ( - +
    diff --git a/desktop/src/features/settings/ui/VoiceSettingsCard.tsx b/desktop/src/features/settings/ui/VoiceSettingsCard.tsx index 3542d6f817..0a536d03f8 100644 --- a/desktop/src/features/settings/ui/VoiceSettingsCard.tsx +++ b/desktop/src/features/settings/ui/VoiceSettingsCard.tsx @@ -14,6 +14,7 @@ import { import { Switch } from "@/shared/ui/switch"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import { SpeechPlaybackSettings } from "./SpeechPlaybackSettings"; import { selectedVoiceForBackend, type VoiceRegistryEntry, @@ -237,6 +238,8 @@ export function VoiceSettingsCard() {
    + + {error && (

    Date: Mon, 27 Jul 2026 17:34:30 -0400 Subject: [PATCH 18/27] Serialize playback speed updates Signed-off-by: John Tennant --- desktop/src-tauri/Cargo.lock | 28 +++--- .../src-tauri/src/huddle/playback_speed.rs | 69 +++++++++++++-- .../settings/ui/SpeechPlaybackSettings.tsx | 29 +++--- .../ui/playbackSpeedPersistence.test.mjs | 88 +++++++++++++++++++ .../settings/ui/playbackSpeedPersistence.ts | 62 +++++++++++++ 5 files changed, 237 insertions(+), 39 deletions(-) create mode 100644 desktop/src/features/settings/ui/playbackSpeedPersistence.test.mjs create mode 100644 desktop/src/features/settings/ui/playbackSpeedPersistence.ts diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 61a9eeeb06..923e614312 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1529,7 +1529,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -1766,7 +1766,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.62.2", + "windows 0.61.3", ] [[package]] @@ -2232,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 2.0.118", + "syn 1.0.109", ] [[package]] @@ -3182,8 +3182,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", + "windows-link 0.1.3", + "windows-result 0.3.4", ] [[package]] @@ -3928,7 +3928,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.6.1", + "windows-registry 0.5.3", ] [[package]] @@ -3943,7 +3943,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -6223,7 +6223,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.118", @@ -6791,7 +6791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -7501,7 +7501,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools", "log", "multimap", @@ -10269,7 +10269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -11914,7 +11914,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -12543,8 +12543,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.18", - "windows 0.62.2", - "windows-core 0.62.2", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] diff --git a/desktop/src-tauri/src/huddle/playback_speed.rs b/desktop/src-tauri/src/huddle/playback_speed.rs index 91e2963f85..2cfe11a571 100644 --- a/desktop/src-tauri/src/huddle/playback_speed.rs +++ b/desktop/src-tauri/src/huddle/playback_speed.rs @@ -29,24 +29,38 @@ const UNITY_EPSILON: f32 = 0.000_1; /// Lock-free shared control read by the TTS worker before each synthesis chunk. #[derive(Clone, Debug)] -pub struct PlaybackSpeedControl(Arc); +pub struct PlaybackSpeedControl { + speed: Arc, + transition: Arc>, +} impl Default for PlaybackSpeedControl { fn default() -> Self { - Self(Arc::new(AtomicU32::new(DEFAULT_PLAYBACK_SPEED.to_bits()))) + Self { + speed: Arc::new(AtomicU32::new(DEFAULT_PLAYBACK_SPEED.to_bits())), + transition: Arc::new(tokio::sync::Mutex::new(())), + } } } impl PlaybackSpeedControl { /// Return the current generated-speech playback speed. pub fn get(&self) -> f32 { - f32::from_bits(self.0.load(Ordering::Acquire)) + f32::from_bits(self.speed.load(Ordering::Acquire)) } /// Update the in-memory speed after validation. pub fn set(&self, speed: f32) -> Result<(), String> { validate_speed(speed)?; - self.0.store(speed.to_bits(), Ordering::Release); + self.speed.store(speed.to_bits(), Ordering::Release); + Ok(()) + } + + async fn persist_to_path(&self, path: &Path, speed: f32) -> Result<(), String> { + validate_speed(speed)?; + let _transition = self.transition.lock().await; + save_to_path(path, speed)?; + self.speed.store(speed.to_bits(), Ordering::Release); Ok(()) } } @@ -71,14 +85,15 @@ pub fn get_tts_playback_speed(state: State<'_, AppState>) -> f32 { /// Persist and apply the global generated-speech playback speed. #[tauri::command] -pub fn set_tts_playback_speed( +pub async fn set_tts_playback_speed( speed: f32, app: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { - validate_speed(speed)?; - save_to_path(&settings_path(&app)?, speed)?; - state.tts_playback_speed.set(speed) + state + .tts_playback_speed + .persist_to_path(&settings_path(&app)?, speed) + .await } fn settings_path(app: &AppHandle) -> Result { @@ -320,6 +335,44 @@ mod tests { assert!(load_from_path(&path).is_err()); } + #[tokio::test] + async fn serialized_persistence_finishes_with_the_latest_speed() { + let directory = tempfile::tempdir().expect("tempdir"); + let path = directory.path().join(SETTINGS_FILE); + let control = PlaybackSpeedControl::default(); + let transition = control.transition.lock().await; + + let (first_enqueued_tx, first_enqueued_rx) = tokio::sync::oneshot::channel(); + let first_control = control.clone(); + let first_path = path.clone(); + let first = tokio::spawn(async move { + first_enqueued_tx.send(()).expect("signal first waiter"); + first_control.persist_to_path(&first_path, 1.25).await + }); + first_enqueued_rx.await.expect("first waiter started"); + tokio::task::yield_now().await; + + let (second_enqueued_tx, second_enqueued_rx) = tokio::sync::oneshot::channel(); + let second_control = control.clone(); + let second_path = path.clone(); + let second = tokio::spawn(async move { + second_enqueued_tx.send(()).expect("signal second waiter"); + second_control.persist_to_path(&second_path, 1.5).await + }); + second_enqueued_rx.await.expect("second waiter started"); + tokio::task::yield_now().await; + + drop(transition); + first.await.expect("join first save").expect("first save"); + second + .await + .expect("join second save") + .expect("second save"); + + assert_eq!(load_from_path(&path).expect("load final speed"), 1.5); + assert_eq!(control.get(), 1.5); + } + fn zero_crossing_frequency(samples: &[f32], sample_rate: u32) -> f32 { let crossings = samples .windows(2) diff --git a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx index 879915b125..2e3377073a 100644 --- a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx +++ b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { getTtsPlaybackSpeed, setTtsPlaybackSpeed } from "@/shared/api/tauri"; import { Button } from "@/shared/ui/button"; +import { commitPlaybackSpeed } from "./playbackSpeedPersistence"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; const DEFAULT_SPEED = 1; @@ -11,14 +12,17 @@ const SPEED_STEP = 0.05; export function SpeechPlaybackSettings() { const [speed, setSpeed] = useState(DEFAULT_SPEED); const [error, setError] = useState(null); - const committedSpeed = useRef(DEFAULT_SPEED); + const confirmedSpeed = useRef(DEFAULT_SPEED); + const desiredSpeed = useRef(DEFAULT_SPEED); + const flushPromise = useRef | null>(null); useEffect(() => { let active = true; getTtsPlaybackSpeed() .then((savedSpeed) => { if (active) { - committedSpeed.current = savedSpeed; + confirmedSpeed.current = savedSpeed; + desiredSpeed.current = savedSpeed; setSpeed(savedSpeed); } }) @@ -30,21 +34,12 @@ export function SpeechPlaybackSettings() { }; }, []); - const commitSpeed = async (nextSpeed: number) => { - const previous = committedSpeed.current; - if (nextSpeed === previous) return; - committedSpeed.current = nextSpeed; - setSpeed(nextSpeed); - setError(null); - try { - await setTtsPlaybackSpeed(nextSpeed); - } catch (cause) { - if (committedSpeed.current === nextSpeed) { - committedSpeed.current = previous; - setSpeed(previous); - setError(String(cause)); - } - } + const commitSpeed = (nextSpeed: number) => { + commitPlaybackSpeed( + { confirmedSpeed, desiredSpeed, flushPromise }, + { persist: setTtsPlaybackSpeed, setError, setSpeed }, + nextSpeed, + ); }; return ( diff --git a/desktop/src/features/settings/ui/playbackSpeedPersistence.test.mjs b/desktop/src/features/settings/ui/playbackSpeedPersistence.test.mjs new file mode 100644 index 0000000000..61cc0e4284 --- /dev/null +++ b/desktop/src/features/settings/ui/playbackSpeedPersistence.test.mjs @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { commitPlaybackSpeed } from "./playbackSpeedPersistence.ts"; + +function deferred() { + let resolve; + let reject; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, reject, resolve }; +} + +function harness() { + const calls = []; + const errors = []; + const saves = []; + const speeds = []; + const state = { + confirmedSpeed: { current: 1 }, + desiredSpeed: { current: 1 }, + flushPromise: { current: null }, + }; + const callbacks = { + persist: (speed) => { + calls.push(speed); + const save = deferred(); + saves.push(save); + return save.promise; + }, + setError: (error) => errors.push(error), + setSpeed: (speed) => speeds.push(speed), + }; + return { callbacks, calls, errors, saves, speeds, state }; +} + +async function settle(state) { + while (state.flushPromise.current) { + await state.flushPromise.current; + } +} + +test("rapid intents persist serially and finish at the latest speed", async () => { + const h = harness(); + commitPlaybackSpeed(h.state, h.callbacks, 1.25); + commitPlaybackSpeed(h.state, h.callbacks, 1.5); + assert.deepEqual(h.calls, [1.25]); + + h.saves[0].resolve(); + await Promise.resolve(); + assert.deepEqual(h.calls, [1.25, 1.5]); + h.saves[1].resolve(); + await settle(h.state); + + assert.equal(h.state.confirmedSpeed.current, 1.5); + assert.equal(h.state.desiredSpeed.current, 1.5); +}); + +test("a stale failure does not roll back a newer successful intent", async () => { + const h = harness(); + commitPlaybackSpeed(h.state, h.callbacks, 1.25); + commitPlaybackSpeed(h.state, h.callbacks, 1.5); + + h.saves[0].reject(new Error("first failed")); + await Promise.resolve(); + assert.deepEqual(h.calls, [1.25, 1.5]); + h.saves[1].resolve(); + await settle(h.state); + + assert.equal(h.state.confirmedSpeed.current, 1.5); + assert.equal(h.state.desiredSpeed.current, 1.5); + assert.deepEqual(h.speeds, [1.25, 1.5]); + assert.deepEqual(h.errors, [null, null]); +}); + +test("the latest failure rolls back to the last confirmed speed", async () => { + const h = harness(); + commitPlaybackSpeed(h.state, h.callbacks, 1.25); + h.saves[0].reject(new Error("save failed")); + await settle(h.state); + + assert.equal(h.state.confirmedSpeed.current, 1); + assert.equal(h.state.desiredSpeed.current, 1); + assert.deepEqual(h.speeds, [1.25, 1]); + assert.equal(h.errors.at(-1), "Error: save failed"); +}); diff --git a/desktop/src/features/settings/ui/playbackSpeedPersistence.ts b/desktop/src/features/settings/ui/playbackSpeedPersistence.ts new file mode 100644 index 0000000000..a793a2c94a --- /dev/null +++ b/desktop/src/features/settings/ui/playbackSpeedPersistence.ts @@ -0,0 +1,62 @@ +type MutableValue = { + current: T; +}; + +export type PlaybackSpeedPersistenceState = { + confirmedSpeed: MutableValue; + desiredSpeed: MutableValue; + flushPromise: MutableValue | null>; +}; + +type PlaybackSpeedPersistenceCallbacks = { + persist: (speed: number) => Promise; + setSpeed: (speed: number) => void; + setError: (error: string | null) => void; +}; + +export function commitPlaybackSpeed( + state: PlaybackSpeedPersistenceState, + callbacks: PlaybackSpeedPersistenceCallbacks, + nextSpeed: number, +) { + state.desiredSpeed.current = nextSpeed; + callbacks.setSpeed(nextSpeed); + callbacks.setError(null); + ensurePlaybackSpeedFlush(state, callbacks); +} + +function ensurePlaybackSpeedFlush( + state: PlaybackSpeedPersistenceState, + callbacks: PlaybackSpeedPersistenceCallbacks, +) { + if (state.flushPromise.current) return; + + const flush = flushPlaybackSpeed(state, callbacks).finally(() => { + if (state.flushPromise.current !== flush) return; + state.flushPromise.current = null; + if (state.desiredSpeed.current !== state.confirmedSpeed.current) { + ensurePlaybackSpeedFlush(state, callbacks); + } + }); + state.flushPromise.current = flush; +} + +async function flushPlaybackSpeed( + state: PlaybackSpeedPersistenceState, + callbacks: PlaybackSpeedPersistenceCallbacks, +) { + while (state.desiredSpeed.current !== state.confirmedSpeed.current) { + const target = state.desiredSpeed.current; + try { + await callbacks.persist(target); + state.confirmedSpeed.current = target; + } catch (cause) { + if (state.desiredSpeed.current === target) { + state.desiredSpeed.current = state.confirmedSpeed.current; + callbacks.setSpeed(state.confirmedSpeed.current); + callbacks.setError(String(cause)); + return; + } + } + } +} From 52876c08c7ca9af240cc8dab6b6d061c28834704 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Mon, 27 Jul 2026 17:39:38 -0400 Subject: [PATCH 19/27] Guard playback speed initialization Signed-off-by: John Tennant --- .../settings/ui/SpeechPlaybackSettings.tsx | 28 +++++++++++++++---- .../ui/playbackSpeedPersistence.test.mjs | 20 ++++++++++++- .../settings/ui/playbackSpeedPersistence.ts | 13 +++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx index 2e3377073a..dc5fb58700 100644 --- a/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx +++ b/desktop/src/features/settings/ui/SpeechPlaybackSettings.tsx @@ -1,7 +1,10 @@ import { useEffect, useRef, useState } from "react"; import { getTtsPlaybackSpeed, setTtsPlaybackSpeed } from "@/shared/api/tauri"; import { Button } from "@/shared/ui/button"; -import { commitPlaybackSpeed } from "./playbackSpeedPersistence"; +import { + applyLoadedPlaybackSpeed, + commitPlaybackSpeed, +} from "./playbackSpeedPersistence"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; const DEFAULT_SPEED = 1; @@ -12,22 +15,34 @@ const SPEED_STEP = 0.05; export function SpeechPlaybackSettings() { const [speed, setSpeed] = useState(DEFAULT_SPEED); const [error, setError] = useState(null); + const [loaded, setLoaded] = useState(false); const confirmedSpeed = useRef(DEFAULT_SPEED); const desiredSpeed = useRef(DEFAULT_SPEED); const flushPromise = useRef | null>(null); + const hasLocalIntent = useRef(false); useEffect(() => { let active = true; getTtsPlaybackSpeed() .then((savedSpeed) => { if (active) { - confirmedSpeed.current = savedSpeed; - desiredSpeed.current = savedSpeed; - setSpeed(savedSpeed); + applyLoadedPlaybackSpeed( + { + confirmedSpeed, + desiredSpeed, + flushPromise, + hasLocalIntent, + }, + { setSpeed }, + savedSpeed, + ); } }) .catch((cause) => { if (active) setError(String(cause)); + }) + .finally(() => { + if (active) setLoaded(true); }); return () => { active = false; @@ -36,7 +51,7 @@ export function SpeechPlaybackSettings() { const commitSpeed = (nextSpeed: number) => { commitPlaybackSpeed( - { confirmedSpeed, desiredSpeed, flushPromise }, + { confirmedSpeed, desiredSpeed, flushPromise, hasLocalIntent }, { persist: setTtsPlaybackSpeed, setError, setSpeed }, nextSpeed, ); @@ -66,7 +81,7 @@ export function SpeechPlaybackSettings() { {speed.toFixed(2)}x