Skip to content

fix(huddle): Pocket TTS quality — grouped chunking + FP32 model bundle#2224

Merged
tlongwell-block merged 5 commits into
mainfrom
eva/pocket-tts-quality
Jul 22, 2026
Merged

fix(huddle): Pocket TTS quality — grouped chunking + FP32 model bundle#2224
tlongwell-block merged 5 commits into
mainfrom
eva/pocket-tts-quality

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Why

Pocket TTS in huddles "sounds janky." Audit (see RESEARCH_POCKET_TTS_AB_PROTOCOL.md on this branch) traced it to two root causes plus one piece of dead code:

  1. Per-sentence synthesis — we called the engine once per sentence (BATCH_SIZE = 1), so every sentence boundary is a prosody reset + audible seam. Upstream sherpa-onnx groups text into ~50-token chunks before synthesis for exactly this reason.
  2. INT8-quantized weights — we ship the INT8 Pocket bundle. sherpa-onnx#3172 A/B'd INT8 vs FP32 on these weights and found INT8 responsible for garbled/degraded output, especially first-synthesis-after-idle.
  3. A speed parameter threaded through our synth path that the sherpa Pocket implementation never reads — a dead knob that only implied tunability we didn't have.

What

Three changes (one commit each) plus a blind evaluation harness:

  • fix(huddle): group TTS sentences into upstream-sized chunks; drop dead speed knob
    tts.rs: BATCH_SIZE = 1MAX_CHUNK_CHARS = 200 (≈60–100 tokens at the vocab's effective ~2–4 chars/token, deliberately at-or-above upstream's 50 — Sami measured the ratio; we don't ship the SentencePiece tokenizer in Rust) with a new group_sentences_into_chunks(): the first sentence always goes alone to preserve time-to-first-audio, then greedy packing; oversized sentences stay unsplit. Deleted SYNTH_SPEED; pocket.rs::synth_chunk loses the never-read speed param, with a doc comment explaining why. Unit tests added for the grouping helper.

  • fix(huddle): switch Pocket TTS to fp32 model bundle
    models.rs: new POCKET_HF_BASE pinned to csukuangfj2/sherpa-onnx-pocket-tts-2026-01-26 @ 96d1e53ce3311ca6c2c6a35e2062d36b4cec6fa3; all TTS_FILE_HASHES swapped to FP32 SHA-256s (Mari verified by download+hash; I independently re-downloaded and re-verified — all match); TTS_MODEL_VERSION "2"→"3" to force re-download; MAX_TTS_FILE_BYTES 200→400 MB (lm_main.onnx is ~303 MB); attribution/license text updated.

  • test(tts): add blind Pocket quality corpus (Max)
    examples/pocket_quality_ab.rs + RESEARCH_POCKET_TTS_AB_PROTOCOL.md: deterministic blind INT8/FP32 × per-sentence/grouped corpus using production preprocessing, warm + genuinely fresh-engine cold cases, real-dormancy probes (--idle-minutes N --only ITEM), opaque filenames, key.json, generated SCORING.md. Sami pre-registered the listening protocol and ship rules before any clips existed.

Kept as-is deliberately: steps=1 (upstream design point), temperature at sherpa default 0.7 (correct for the english_2026-01 weights), frames_after_eos untouched (regression-guard tests still pass).

Tradeoff

FP32 grows the model download 189 MB → 473 MB (one-time, per TTS_MODEL_VERSION bump). We're shipping it as the default rather than a variant because the upstream A/B attributes the worst artifacts directly to quantization. Merge gate: the pre-registered blind listening run must confirm — per Sami's decision rules, FP32 ships iff it wins on garble/post-idle items; a null result means reverting the FP32 commit (chunking stands on its own). Per Sami's review, the FP32 change is a single revertable unitmodels.rs manifest + pocket.rs FILE_* names + doc text all live in the one FP32 commit, verified by revert simulation (git revert of it compiles clean and restores a consistent int8 path).

Testing

  • cargo check + clippy clean; full desktop tauri suite green: 1509 passed / 0 failed / 13 ignored, plus 3/3 mixer tests.
  • Harness release smoke generated and validated 8 nonempty mono 24 kHz WAVs + key/scoring files (mechanics only — real FP32 clips and real 5/15-min idle runs are the remaining gate).
  • Pre-push hooks (branch-skew, desktop-tauri-test, mobile-test) green at push.

Credits

  • Max — blind A/B harness + protocol doc
  • Sami — pre-registered listening protocol and ship/revert decision rules; first reviewer on the chunking diff
  • Mari — FP32 bundle pin verification (repo/commit/SHA-256s), independently re-verified by me

Review record

  • Sami reviewed the chunking diff: logic correct, tests traced, ratio measured (dict avg 4.04 chars/token, effective ~2–4). His blocker — FP32 filename renames split across two commits, breaking the documented git revert fallback — fixed by re-splitting the branch (force-pushed; commit contents otherwise identical, plus his comment-wording fix on MAX_CHUNK_CHARS).

Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
@tlongwell-block
tlongwell-block requested a review from a team as a code owner July 21, 2026 14:51
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 3 commits July 21, 2026 07:57
…d speed knob

Pocket TTS quality fix, part 1 of 2 (see the fp32 model swap commit for
part 2; that commit is a single revertable unit — this one is
int8-compatible on its own). Two changes:

1. Chunk grouping: upstream pocket-tts synthesizes chunks of up to
   MAX_TOKEN_PER_CHUNK=50 tokenizer tokens (typically multi-sentence);
   every generate() call is an independent generation with a cold FlowLM
   start, and each boundary is an exposed prosody seam (kyutai-labs/
   pocket-tts#151 — the Kyutai team names chunk stitching as the
   reliability lever). Our sentence-per-call path created ~2-4x more
   seams than upstream. Sentences after the first now pack greedily up
   to ~200 chars (~60-100 tokens at the vocab's effective ~2-4
   chars/token — deliberately at or above upstream's 50, since erring
   large means fewer seams and stays far below the 500-LM-step ceiling);
   the first sentence stays alone to preserve time-to-first-audio.

2. SYNTH_SPEED=1.05 deleted: sherpa-onnx's Pocket impl never reads
   GenerationConfig.speed (offline-tts-pocket-impl.h — zero references;
   the field is vits-only), and upstream has no speed knob. The constant
   documented an effect that does not exist.

frames_after_eos remains untouched and the short-prompt max_frames cap
stays, per the existing regression guards.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Pocket TTS quality fix, part 2 of 2. Swap the pinned model manifest from
csukuangfj2/sherpa-onnx-pocket-tts-int8-2026-01-26 to the fp32 sibling
repo (same layout minus .int8 suffixes, commit 96d1e53). A direct
same-runtime A/B (k2-fsa/sherpa-onnx#3172) found the int8 ONNX export
audibly degraded output and that fp32 'significantly improved quality
even at 1 step'.

This commit is the complete fp32 change — models.rs manifest, pocket.rs
FILE_* names, and doc text move together — so `git revert` of this one
commit cleanly restores the int8 bundle if the blind A/B returns a null
result.

Cost: runtime bundle grows ~189 MB -> ~473 MB (lm_main.onnx is 303 MB,
so MAX_TTS_FILE_BYTES rises to 400 MB). encoder, text_conditioner, both
JSON tables, and LICENSE are byte-identical between the repos; only the
three quantized sessions change. TTS_MODEL_VERSION bumps 2 -> 3 so
existing int8 installs re-download.

SHA-256 pins verified against local downloads of every file from the
pinned HF commit.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block
tlongwell-block force-pushed the eva/pocket-tts-quality branch from c6284a7 to 2f699d4 Compare July 21, 2026 14:59
@tlongwell-block
tlongwell-block merged commit 8727632 into main Jul 22, 2026
26 checks passed
@tlongwell-block
tlongwell-block deleted the eva/pocket-tts-quality branch July 22, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant