#1108·voicebox

trim_tts_output silently discards correct speech after any pause over 1s

Author: ariellerCreated Sep 16, 2026Updated Sep 16, 2026

Summary

trim_tts_output discards correct speech. It cuts the audio at the first internal silence gap longer than max_internal_silence_ms (default 1000ms) and throws away everything after it, without testing whether what follows is a hallucinated tail or the rest of the utterance. A generation that reports completed with a valid WAV can be missing most of its content.

This is data loss rather than a quality setting, and it is not reachable from the HTTP API — neither trim_fn nor its thresholds are exposed, so a client cannot opt out.

Impact

Expressive speech pauses for well over a second, so the exposed case is precisely the well-delivered line — a beat after a dramatic sentence, or any paralinguistic tag ([sigh], [laugh]) that renders as low-energy audio.

Reproduction

Chatterbox Turbo, cloned profile, one 416-character text containing a [sigh], six seeds. Generated directly against the model, then passed through trim_tts_output as a pure function call:

seed raw after trim_tts_output
7000 21.80s 21.78s
7001 25.48s 25.44s
7002 24.48s 24.38s
7003 25.00s 10.52s
7004 25.52s 4.02s
7005 24.00s 8.84s

The model produced full, correct audio in all six. Three were damaged by the trim, one reduced to 16% of its length. Supporting evidence: 0 truncations in 10 direct (untrimmed) renders of the same text, against roughly 7 anomalies in 16 renders through /generate.

Root cause

backend/utils/audio.py, in trim_tts_output:

consecutive_silence += 1
if consecutive_silence >= max_silence_frames:
    cut_frame = i - consecutive_silence + 1
    break

The docstring states the intent correctly — strip a [speech][silence][hallucinated noise] tail — but the implementation infers "hallucinated noise" from the gap alone. An RMS threshold cannot distinguish a pause from a hallucination, so the length of what follows has to decide.

Proposed fix

Only cut when the audio after the gap is not a substantial continuation. Minimal patch, preserving the original intent:

     min_silence_ms: int = 200,
     max_internal_silence_ms: int = 1000,
     fade_ms: int = 30,
+    min_continuation_ms: int = 1500,
 ) -> np.ndarray:
@@
             consecutive_silence += 1
             if consecutive_silence >= max_silence_frames:
+                speech_after_frames = int(np.count_nonzero(is_speech[i + 1 :]))
+                if speech_after_frames * frame_ms >= min_continuation_ms:
+                    consecutive_silence = 0
+                    continue
                 cut_frame = i - consecutive_silence + 1
                 break

Verification

Same six seeds, patched: 21.78 / 25.44 / 24.38 / 24.84 / 25.40 / 23.96 — all preserved, trailing-silence trim and fade still applied.

Synthetic cases confirming the original behaviour is intact:

case before -> after expected
3s speech, 1.5s gap, 0.4s noise 4.90s -> 3.00s tail still cut
3s speech, 1.5s gap, 4s speech 8.50s -> 8.50s pause preserved
5s speech, no gap 5.00s -> 5.00s unchanged
3s speech, gap, 4s speech, gap, 0.3s noise 10.30s -> 8.50s speech kept, tail cut

Happy to open a PR if the approach looks right. An alternative (or addition) would be exposing trim/thresholds through GenerationRequest so clients can opt out.