feat(huddle): cut voice-turn time-to-first-audio from ~1.0 s to ~0.35 s (env-gated latency levers) - #5671
feat(huddle): cut voice-turn time-to-first-audio from ~1.0 s to ~0.35 s (env-gated latency levers)#5671tlongwell-block wants to merge 20 commits into
Conversation
…y levers Problem: speech-end -> first TTS audio through the desktop huddle pipeline measures 924-1087 ms on an M4 Max with a 0 ms LLM leg, which makes voice turns feel sluggish regardless of how fast the agent replies. Baseline breakdown: ~300 ms hardcoded VAD silence flush, ~150-250 ms Parakeet decode, ~380-550 ms TTS synthesis before the first player append. This change adds three independently measurable latency levers plus one always-on exact optimization, and a bench harness that drives the real production pipelines to attribute every millisecond. All levers are env-gated and default to production behavior; with all of them enabled, e2e time-to-first-audio measures 347-384 ms (307-357 ms on a longer utterance) on the same hardware and harness. Levers (all default-off): - BUZZ_STT_FLUSH_MS overrides the 300 ms VAD silence flush window (SILENCE_FLUSH_FRAMES stays the default). At 150 ms, natural intra-sentence pauses become segment boundaries, so this is an A/B candidate, not a new default. - BUZZ_STT_SPECULATIVE=1 starts the Parakeet decode at the FIRST silent VAD frame, overlapping decode with the flush window. Resumed speech invalidates the speculative result (voiced-frame-count check); held silence emits it instantly at the flush boundary, collapsing the STT leg to ~max(flush window, decode). - BUZZ_TTS_STREAMING=1 streams PCM deltas out of Pocket as they are generated: new synth_chunk_streaming interleaves the Flow LM frame loop with incremental stateful Mimi decoding and appends deltas to the player via the existing PlaybackChunkAudio decoration. BUZZ_TTS_EMIT_FRAMES (default 12 = the decoder's native chunk) keeps streamed audio bit-identical to the batch path, verified by the ignored test incremental_stateful_decode_matches_batch_decode; smaller deltas are faster but diverge (~23 dB SNR, decoder intra-chunk lookahead). - BUZZ_STT_THREADS / BUZZ_TTS_THREADS override the ONNX intra-op thread counts (defaults stay 1 pending a min-spec A/B). Always-on (exact): the Flow LM state produced by voice conditioning is snapshotted per reference voice and restored on subsequent chunks, removing a fixed ~160 ms condition_voice pass from every chunk after the first. Deterministic, keyed identically to the existing voice embedding cache; the default path's TTS leg drops from 380-550 ms to 225-355 ms with no configuration. The harness (huddle::latency_bench, #[cfg(test)] + #[ignore]) feeds a 48 kHz WAV through SttPipeline in real-time 100 ms batches, substitutes a configurable fake LLM for the relay leg, and times speech-end -> transcript -> speak() -> first accepted player append. Verification: desktop lib suite 2383 passed / 0 failed with the changes in place; defaults-only bench rerun stays at baseline behavior everywhere except the exact conditioning-cache win; bit-exactness test passes against the production batch decode. Measurements and lever-by- lever numbers: RESEARCH notes in the working tree accompanying PR. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…the-ear rules
Two additions to the kind:48106 huddle guidelines, from live huddle
feedback:
1. Agents must ALWAYS send a short spoken acknowledgment ("Got it, let
me take a look at that.") as their very first action — before any
tool call or answer composition — whenever a request needs tools or
more than one sentence of thought. Silence while an agent works
sounds like the agent never heard the human.
2. A "write for the ear" block: no abbreviations, acronyms, or symbols;
numbers and units spelled out as spoken; no file paths, URLs, or
identifiers (describe them, post exact details to the main channel);
rewrite anything that would sound wrong read aloud.
Also scopes the existing tool-narration bullet to mid-reply tool use so
it composes with the new always-ack-first rule instead of conflicting
with it.
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> * origin/main: Refine channel settings and profile panels (#5574) Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Review finding (Wren): both the voice-embedding cache and the new conditioning cache keyed on (samples pointer, length, rate). Voice switching clones and drops sample buffers, so the allocator can hand a DIFFERENT voice of the same length and rate the same address — the caches would then replay the previous voice's state and the huddle would speak with the wrong voice. Both caches now key on a VoiceKey: a content hash of the sample bits plus length and rate. Addresses are out of the key entirely. Tests: - voice_key_is_content_based_not_address_based (fast, no model): equal-length equal-rate distinct content keys differently; identical content in a distinct allocation keys identically; rate changes key. - switching_between_equal_length_voices_reconditions_the_flow_state (ignored, model-gated): conditions voice A, switches to an equal-length voice B, asserts the resulting Flow LM state is bitwise-equal to a fresh-process conditioning of B and distinct from A's cached state. Snapshot comparison is bitwise on f32 because the state tensors carry NaN fill. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
tlongwell-block
left a comment
There was a problem hiding this comment.
Review at head eabd537cc. Built it, ran the tests, and mutation-tested the central claim rather than reading it. (Security CI check ignored per Tyler — unrelated to this PR.)
Rating: 7.5/10 — minimalness 7, elegance 7.5, correctness 7.
Verified independently (not just read)
The bit-exactness claim holds. I ran incremental_stateful_decode_matches_batch_decode against the real bundle in ~/.buzz/models/pocket-tts and instrumented the assertion to print the actual value:
DAWN_PROBE delta_frames=12: max|diff|=0.000000000
Exactly zero, not merely under the 1e-4 threshold the assert uses. Then I mutation-tested the guard — patched decode_frames to reinitialize the Mimi state on every call (killing the state carry the design depends on):
DAWN_PROBE delta_frames=12: max|diff|=0.461152434
panicked: incremental decode diverged from batch decode
So the test is a live detector, not decoration. Good.
Main objection: the proven part is dark, the live part is unproven
Every lever here is default-off except the voice-conditioning cache, which is always-on and reaches every user on merge. It is the only change with production blast radius — and it is the only one with no test. Nothing asserts that synthesis with a warm conditioning cache equals synthesis that recomputes it (grep for conditioned_flow_state|cached_conditioning|snapshot_state|restore_state across crates/ and desktop/ returns only definitions plus one call inside the streaming test).
The bit-exactness test everyone will point at guards decode_frames, which is off by default. That inversion is the thing to fix before merge.
Cache key aliasing
conditioned_flow_state keys on style.samples.as_ptr() as usize + len + rate. A freed VoiceStyle whose allocation is reused by a different voice with equal len/rate is a silent false hit → wrong conditioning. In fairness this pattern is inherited from the existing voice_embeddings cache, not invented here — but this change doubles what rides on it and promotes it to always-on. Keying by voice name or a sample hash costs nothing.
Off-by-one in the flush override (measurement hazard)
.map(|ms| (ms / 16).max(1))Production is SILENCE_FLUSH_FRAMES = 19 ≈ 304 ms. BUZZ_STT_FLUSH_MS=300 yields 18 frames, not 19 — so a control arm set to "the production value" is not the production path. It didn't affect the reported baseline (that used real defaults), but it will void a control later.
Speculative decode: unbounded work under stutter (promotion-gate, not ship-risk)
decode_speech runs a full transcription of the whole buffer on the audio worker at the first silent frame. Speech that resumes invalidates it, and the next pause kicks another full decode. Speech/pause/speech/pause multiplies transcription cost for a single sentence — invisible on an M4 Max, potentially not on the 4-core min-spec target. Off by default, so not a merge risk; wants a rate/count guard alongside the zero-padding fix already flagged.
Credit where due
Cancellation inside the streaming callback is strictly better than the batch path — it aborts mid-chunk instead of only at chunk boundaries, a real barge-in latency win that's undersold in the writeup. The bench harness excludes its own segment-wait artifact from the timings and documents why; that's more rigor than most benchmarks get.
Weakest axis is minimalness: synth_chunk_streaming duplicates ~90 lines of generate_latents, and decode_frames duplicates decode_latents. Defensible for a don't-touch-the-prod-path experiment, and stated as such — but it's what caps the score.
Recommendation
Split the conditioning cache into its own small PR with one equivalence test (same text, warm vs cold cache, assert identical audio) and fix the key. That ships the exact win fast with the best benefit/risk ratio. Keep the rest here as the env-gated experiment it already is, fix the flush conversion, and it merges as a research artifact without concern.
Process note: BUZZ_POCKET_TEST_MODEL_DIR appears nowhere in .github/workflows/ — the bit-exactness guard never runs in CI. It protects only when a human runs it by hand, as I did here. A green build does not cover it.
…arm cache hit equals cold recompute Review findings (Dawn): 1. BUZZ_STT_FLUSH_MS converted ms to frames with a truncating divide, so an override of 300 (the production window) yielded 18 frames instead of the production 19 — a control arm set to the production value was silently one frame faster than production, skewing any comparison. Now rounds up (300 -> 19, exact multiples unchanged) and the conversion is a pure function with a unit test pinning 300 ms to SILENCE_FLUSH_FRAMES. 2. The always-on conditioning cache had no equivalence coverage: nothing asserted a warm cache hit reproduces the cold computation. The model-gated voice-switch regression test now also conditions the same voice twice and asserts the warm restore is bitwise-equal to a fresh-process recompute. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
…wn modules The pre-push ratchet caps huddle sources at 1000 lines; the latency levers pushed tts.rs to 1072 and mod.rs to 1001. Extract along the existing module seams instead of trimming comments: - tts_streaming.rs: the env-gated streaming synthesis path (BUZZ_TTS_STREAMING / BUZZ_TTS_EMIT_FRAMES) moves out of the worker loop into synthesize_streaming(), same behavior, single call site. - handle_cancel_or_shutdown + lock_player_ops move to tts_voice_transition.rs beside the cancellation state they operate on (speaker_cancellation already used them through that module). - add_agent_to_huddle moves from mod.rs to commands.rs with the other huddle-mutating tauri commands; mod.rs keeps the re-export so lib.rs's import surface is unchanged. No functional change. tts.rs 1072 -> 934, mod.rs 1001 -> 920, ratchet green. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Review response — all findings addressed at
|
| Commit | What it addresses |
|---|---|
49ec564 |
Tyler's huddle-prompt update: agents must speak a short ack ("Got it, let me take a look at that.") as their very first action before any tool call, plus write-for-the-ear rules from the huddle feedback channel |
a3e6407 |
Merge of origin/main (sync merge as requested — no rebase, no force) |
88d80af |
Wren's blocker: both voice caches (conditioning + the older embedding cache, which shared the flawed key shape) now key on a content hash of the reference samples + length + rate. Buffer addresses are out of the key entirely. Regression tests: a fast content-vs-address key test, and a model-gated equal-length voice-switch test asserting the switched Flow state is bitwise-equal to a fresh-process conditioning of the new voice |
7445169 |
Dawn's findings: (1) BUZZ_STT_FLUSH_MS now rounds up to whole VAD frames — an override of 300 yields the production 19 frames, not 18, so a production-value control arm is actually the production path; unit test pins 300 → SILENCE_FLUSH_FRAMES. (2) The conditioning cache now has warm/cold equivalence coverage: the voice-switch test also conditions the same voice twice and asserts the warm cache restore is bitwise-equal to a fresh recompute |
a978b6f |
Restores the desktop file-size ratchet the levers had broken (tts.rs 1072, mod.rs 1001, cap 1000) by extracting along existing seams: streaming path → tts_streaming.rs, cancel helpers → tts_voice_transition.rs, add_agent_to_huddle → commands.rs. No functional change |
On Dawn's remaining notes: the speculative-decode restart guard and zero-padding fix stay parked until promotion (the lever is off by default), and splitting the conditioning cache into its own PR is Tyler's call — the cache now carries the key fix, a wrong-voice regression test, and warm/cold equivalence coverage in this PR either way. The BUZZ_POCKET_TEST_MODEL_DIR tests still don't run in CI (noted and true); I ran them by hand at this exact head.
Verification at a978b6f (git rev-parse HEAD confirmed in the same shell):
- desktop lib suite: 2408 passed / 0 failed
cargo test -p buzz-voice: green; model-gated pocket_april suite (incl. bit-exactness + both new cache tests): 5/5 passed against the real April bundle- clippy
-D warnings+ fmt: clean for both crates - pre-push hooks (ratchet, typecheck, desktop tests, rust tests): all green on the successful push
…ile push-to-talk is held The BUZZ_STT_FLUSH_MS override let the silence window drop below natural mid-sentence pauses (150 ms in the fast-path recipe), splitting one spoken sentence into multiple messages and confusing the listening agents. The window is a turn-taking quality knob, not a latency lever — remove the override and fix the window at the production 300 ms value. Also treat a held push-to-talk shortcut as an explicit "I am not done talking" signal: silence never ends the utterance while the shortcut is held, even when the microphone is also manually open. The utterance still flushes on shortcut release via the existing transmit-edge flush; a manually open mic with the shortcut up keeps normal VAD pause flushing. The speculative-decode kick follows the same gate so it cannot burn decodes inside a held transmission. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Summary - promote owner-signed channel huddle instructions into the agent's real system prompt at session creation - bind the instruction event to the exact channel and verify its Nostr signature before granting system authority - trim the desktop huddle instructions to six voice-first lines centered on replying immediately when addressed - preserve legacy adapters through the existing standing-context fallback ## Why this seam This reuses the existing per-channel `session/new` prompt assembly and the already-posted kind `48106` event. It adds no new desktop-to-harness transport and performs one filtered relay query only when a new channel session is created. Failures are fail-open to no huddle section. Promoting ordinary channel messages would let arbitrary members inject system-level instructions. This path accepts only a valid event authored by the configured owner with the exact channel `h` tag. ## Testing - `cargo test -p buzz-acp` (767 unit + 9 lifecycle tests) - `cargo clippy -p buzz-acp --all-targets --all-features -- -D warnings` - `cargo fmt --all -- --check` - push hook with the repository Hermit toolchain: branch-skew, desktop-check, desktop-typecheck, mobile-test, desktop-test, rust-tests, desktop-tauri-checks all passed Stacked on #5671. Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
The STT worker discarded all mic input while tts_active was set and for a 150 ms cooldown after, so a human talking over an agent was never heard. The huddle UI already instructs users to wear headphones, which is the accepted mitigation for speaker bleed — drop the gate entirely and let transcription continue whenever the human is talking. Local mic frames still never cancel TTS; push-to-talk and remote participant speech remain the explicit barge-in paths. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
* origin/main: Harden shared agent instruction review (#4220) Signed-off-by: tlongwell-block <tlongwell@block.xyz>
The Ctrl+Shift+Space huddle shortcut listener registered every app-shell shortcut in the window capture phase, stealing Cmd+K (and friends) from more specific handlers like the composer's add-link dialog. Split the handlers: only the huddle shortcut stays capture-phase (it must fire even when a focused control stops propagation); the rest return to bubble phase. Also update the Push to Talk e2e test to assert huddles start muted (matching the manual_mic_unmuted default) and cover the manual unmute flow. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Summary - preserve the first sentence as the highest-priority Pocket TTS synthesis unit for minimal time to first audio - replace Desktop's coarse character-based grouping with tokenizer-aware sentence/clause packing - keep subsequent synthesis pipelined with playback, preserving output order without racing the mutable Pocket engine - remove the unconditional 100 ms of inter-unit silence so Pocket's generated pauses determine cadence ## Why Buzz currently resets prosody at coarse boundaries, fades each unit, and adds fixed silence even when the model has already generated a natural pause. This makes speech sound more segmented than the Pocket TTS web demo. This keeps PR #5671's latency strategy: sentence one is isolated and generated first. Once its PCM is queued, the remaining naturally packed units continue synthesizing while playback drains the queue. This PR does **not** add parallel ONNX generation. Pocket owns one mutable engine and synthesis remains sequential; true parallel generation requires another resident model/session and should be evaluated separately as an explicit memory/latency tradeoff. ## Dependency Stacked on #5671 (`eva/huddle-first-audio-latency`) as requested. The merge base is its exact head at the time of implementation: `f9451f1f502b12db2c8972edc934d6f2138182b9`. ## Verification At `6ba487ecf69e9504505eb0449c3c929e606461d4`: - `buzz-voice` library tests: 19 passed, 7 model-dependent tests ignored - full Desktop library tests: 2,412 passed, 15 ignored - `buzz-voice` and Desktop clippy clean for all targets with `-D warnings` - full pre-push hooks passed using the repo's Hermit toolchain (`PATH="$PWD/bin:$PATH"`) - regression coverage pins first-sentence playback isolation and verifies the inner model split does not isolate later packed sentences again --------- Signed-off-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
tlongwell-block
left a comment
There was a problem hiding this comment.
Approve — reviewed at head fbf773a3e6bb03539e18c828fe3120e45281ed80
Head derived with gh pr view 5671 --json headRefOid + git ls-remote origin refs/pull/5671/head (both agree); eabd537cc is an ancestor, not the tip. Diff scope against merge-base origin/main: 27 files, +2489/-873. All gates below were run in a shell where git rev-parse HEAD equals that SHA, on toolchain cargo 1.95.0, tracked tree clean.
What I ran
cargo test -p buzz-voice— 21 passed, 0 failed, 7 ignored.- Full
desktop/src-taurisuite — 2413 passed, 0 failed, 15 ignored (52.3s). Default features; stating the flags because a count without them is not comparable. - Model-gated pair with
BUZZ_POCKET_TEST_MODEL_DIR:incremental_stateful_decode_matches_batch_decodeandswitching_between_equal_length_voices_reconditions_the_flow_state— 2 passed, 0 failed, 26 filtered out. - Mutation on the new conditioning-cache test (it is the test that closes my main round-8 objection, so it needed to be proven live, not merely present): replacing the
cached.key == keyguard withtrue— i.e. always replay the cache — fails withswitching voices must recondition, not replay the cacheatpocket_april.rs:1670. Source restored byte-identical (cmpclean) afterwards.
My three prior findings are closed at this head
- Always-on conditioning cache had no equivalence test →
switching_between_equal_length_voices_reconditions_the_flow_statenow asserts recondition-not-replay, equal-length-distinct-voices differ, and warm-hit ≡ cold-recompute. Mutation-proven above. - Address-based cache key →
VoiceKey { content_hash, samples_len, sample_rate }withvoice_key()hashing sample bits, covering bothcached_voiceandcached_conditioning. - Flush-window override off-by-one measurement hazard → the override is gone entirely;
flush_frames = SILENCE_FLUSH_FRAMES(19) is fixed at the production value, so the control arm is the production path by construction. The comment atstt.rs:165records why it was reverted.
Also checked: the capture-phase keydown listener is scoped to only the huddle shortcut (handleHuddleShortcut with {capture:true}, all other shortcuts still on bubble-phase handleKeyDown), so the composer's ⌘K link dialog is not stolen — that was the shape I asked for. manual_mic_unmuted now defaults to false in HuddleState::default() with the Rust and Playwright tests updated to match.
Two non-blocking promotion gates (record, don't withhold)
- Speculative decode cost.
BUZZ_STT_SPECULATIVE=1(note: not..._DECODE) runs a fulldecode_speechon the audio worker thread at the first silent frame.speculative.is_none()caps it at one decode per silence onset anddecoded_at == *voiced_framescorrectly discards a stale result, so cost is bounded per transition — but stuttery speech multiplies transitions, and on a 4-core minimum-spec box that is real contention. Default-off, so it ships safely; measure before promoting to default-on. - Model-gated tests are not in CI.
rg -n BUZZ_POCKET_TEST_MODEL_DIR .github/workflows→ rc=1 (positive control: the same search does find otherBUZZ_vars in those files). The exactness and cache-equivalence guards are human-run only, so a green board does not cover them.
One failure that is not a regression
imported::tests::imports_common_audio_format_fixtures panics fixture directory: NotPresent — it reads BUZZ_VOICE_IMPORT_TEST_DIR, which is unset on my box. It is #[ignore]d and present verbatim on origin/main (imported.rs:650-653), so it is environmental and pre-existing, not from this PR. Flagging it so nobody mistakes it for one.
Minimalness remains the weakest axis (the streaming path duplicates the frame loop and a decode helper) but that is the documented price of leaving the production path untouched, and I'd rather have that than a shared path threading a mode flag. Approving.
…ixture fix The PR #5671 merge ref was pinned at pre-fix main (45f4b91), so shard-3 Desktop Smoke E2E replayed the already-fixed compact link preview geometry failure. Updating the base recomputes refs/pull/5671/merge against main at b269e8d, which includes the #5799 fixture fix. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> * origin/main: fix(desktop): route compact preview geometry fixture through media proxy (#5799) Make workflow run history authoritative in Desktop (#5780) fix(desktop): more compact "compact" link previews (#5629) Fix mobile composer input regressions (#5594) Add mobile community invites (#5641) Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Overview
Category: feat (env-gated experiment + one exact always-on optimization)
Problem: Speech-end -> first TTS audio through the desktop huddle pipeline measures 924–1087 ms on an M4 Max with a 0 ms LLM leg. Voice turns feel sluggish no matter how fast the agent replies. Baseline breakdown: ~300 ms hardcoded VAD silence flush + ~150–250 ms Parakeet decode + ~380–550 ms TTS synthesis before the first player append.
Outcome: With all levers enabled, e2e time-to-first-audio measures 347–384 ms (307–357 ms on a longer utterance) on the same hardware, harness, and production pipelines. Defaults preserve production behavior everywhere except one deterministic, bit-exact cache win.
What's in here
Levers (all default-off, env-gated)
BUZZ_STT_SPECULATIVE=1BUZZ_TTS_STREAMING=1,BUZZ_TTS_EMIT_FRAMESBUZZ_STT_THREADS,BUZZ_TTS_THREADSsynth_chunk_streaming(buzz-voice) interleaves the Flow LM frame loop with incremental stateful Mimi decoding, emitting PCM deltas to the player via the existingPlaybackChunkAudiodecoration. Atemit_frames=12(the decoder's native chunk) streamed audio is bit-identical to the batch path — verified by the ignored testincremental_stateful_decode_matches_batch_decode(max|diff|=0). Smaller deltas are faster but diverge (~23 dB SNR; decoder intra-chunk lookahead), hence the default of 12.Push-to-talk grouping fix (always-on)
A held push-to-talk shortcut is an explicit "I am not done talking" signal, so silence never ends the utterance while it is held — even when the microphone is also manually open. The utterance flushes on shortcut release (existing transmit-edge flush); a manually open mic with the shortcut up keeps normal VAD pause flushing. Gate is the pure
vad_flush_allowedfunction with a unit-test truth table.Always-on (exact): voice-conditioning cache
Phase profiling (
BUZZ_TTS_PHASE_LOG=1) showed a fixed ~160 mscondition_voiceFlow-LM pass on every chunk, re-deriving the same post-conditioning state for the same reference voice. The state is now snapshotted after first computation and restored per chunk (dtype-tagged tensor copies, keyed identically to the existingcached_voice). Deterministic — same tensors in, same tensors out. The default path's TTS leg drops from 380–550 ms to 225–355 ms with no configuration.Bench harness
huddle::latency_bench(#[cfg(test)]+#[ignore]) drives the realSttPipelineandTtsPipeline, feeding a 48 kHz WAV in real-time 100 ms batches (AudioWorklet cadence) with a configurable fake LLM in place of the relay leg, timing speech-end -> transcript -> speak() -> first accepted player append.Tradeoffs to weigh before promoting any lever to a default
STT_NUM_THREADScomment.Validation
18fab2e1c).cargo clippy --workspace --all-targets -- -D warnings+ fmt clean (pre-push hook battery green).Measurement notes with per-lever logs: Eva's workspace,
RESEARCH/HUDDLE_E2E_LATENCY_OPTIMIZATION_2026_08_12.md+RESEARCH/HUDDLE_E2E_STT_FAKELLM_TTS_BASELINE_2026_08_12.md.Suggested promotion order