From 65a0ef67cf027b32b798eaf8af3700d9dc25da80 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 1 Jun 2026 23:52:05 -0700 Subject: [PATCH] fix(chatterbox-fast): correct-length WAV header for one-shot responses stream=false + format=wav emitted the streaming 0xFFFFFFFF-length header, so a buffered consumer reading a complete wav got bogus RIFF/data sizes. One-shot knows the full length, so emit correct sizes; streaming keeps the open-ended header (length genuinely unknown up front). Verified remote: one-shot wav data size == bytes-44, python wave.open() reads 2.20s cleanly; streaming still 0xFFFFFFFF. --- stacks/chatterbox-fast/app.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/stacks/chatterbox-fast/app.py b/stacks/chatterbox-fast/app.py index 91f79d4..758456b 100644 --- a/stacks/chatterbox-fast/app.py +++ b/stacks/chatterbox-fast/app.py @@ -217,12 +217,15 @@ def _pcm16(wav: torch.Tensor) -> bytes: return (a * 32767.0).astype(" bytes: - """Streaming WAV header with unknown length (0xFFFFFFFF sizes).""" +def _wav_header(sr: int, data_len: int | None = None) -> bytes: + """WAV header. data_len=None → streaming (length unknown, 0xFFFFFFFF sizes, + player reads to EOF); an int → correct RIFF/data sizes for a complete file.""" + data_size = 0xFFFFFFFF if data_len is None else data_len + riff_size = 0xFFFFFFFF if data_len is None else 36 + data_len return ( - b"RIFF" + struct.pack(" StreamingResponse: # workload); concurrent callers queue rather than corrupt conditionals. with engine.lock: engine._prepare(voice_path, exaggeration=req.exaggeration) - if req.format == "wav": - yield _wav_header(engine.sr) - t_req = time.perf_counter() first_audio_ms: float | None = None if not req.stream: + # One-shot: the full length is known, so emit a correct-sized WAV + # header (a buffered consumer wants well-formed sizes). wav, audio_sec = engine.generate(req.text, req) first_audio_ms = (time.perf_counter() - t_req) * 1000 log.info("oneshot: %.0fms gen, %.2fs audio", first_audio_ms, audio_sec) - yield _pcm16(wav) + pcm = _pcm16(wav) + if req.format == "wav": + yield _wav_header(engine.sr, len(pcm)) + yield pcm return + # Streaming: length is unknown up front → open-ended WAV header. + if req.format == "wav": + yield _wav_header(engine.sr) + def _gen(text: str) -> tuple[torch.Tensor, float]: return engine.generate(text, req)