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.
This commit is contained in:
2026-06-01 23:52:05 -07:00
parent 02e5ea5894
commit 65a0ef67cf
+17 -8
View File
@@ -217,12 +217,15 @@ def _pcm16(wav: torch.Tensor) -> bytes:
return (a * 32767.0).astype("<i2").tobytes()
def _wav_header(sr: int) -> 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("<I", 0xFFFFFFFF) + b"WAVE"
b"RIFF" + struct.pack("<I", riff_size) + b"WAVE"
+ b"fmt " + struct.pack("<IHHIIHH", 16, 1, 1, sr, sr * 2, 2, 16)
+ b"data" + struct.pack("<I", 0xFFFFFFFF)
+ b"data" + struct.pack("<I", data_size)
)
@@ -276,19 +279,25 @@ def tts(req: TTSRequest) -> 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)