feat: chunked TTS generation for long text (engine-agnostic)

Text exceeding max_chunk_chars (default 800) is automatically split at
sentence boundaries, generated per-chunk, and concatenated with a 50ms
crossfade.  Works with all engines (Qwen, LuxTTS, Chatterbox, Turbo).

- Abbreviation-aware sentence splitter (Dr., Mr., e.g., decimals)
- CJK sentence-ending punctuation support
- Paralinguistic tag preservation ([laugh], [cough], etc.)
- Per-chunk seed variation to avoid correlated RNG artefacts
- Per-chunk Chatterbox trim (catches hallucination at each boundary)
- max_chunk_chars exposed as per-request param on GenerationRequest
- Text max_length raised to 50,000 characters

Closes #99
This commit is contained in:
James Pine
2026-03-13 06:21:34 -07:00
parent c12b5d6f0a
commit 70ca7f66cb
3 changed files with 329 additions and 19 deletions
+29 -18
View File
@@ -824,18 +824,24 @@ async def generate_speech(
engine=engine,
)
audio, sample_rate = await tts_model.generate(
data.text,
voice_prompt,
data.language,
data.seed,
data.instruct,
)
from .utils.chunked_tts import generate_chunked
# Trim trailing silence/hallucination for Chatterbox output
# Resolve per-chunk trim function for engines that need it
trim_fn = None
if engine in ("chatterbox", "chatterbox_turbo"):
from .utils.audio import trim_tts_output
audio = trim_tts_output(audio, sample_rate)
trim_fn = trim_tts_output
audio, sample_rate = await generate_chunked(
tts_model,
data.text,
voice_prompt,
language=data.language,
seed=data.seed,
instruct=data.instruct,
max_chunk_chars=data.max_chunk_chars,
trim_fn=trim_fn,
)
# Calculate duration
duration = len(audio) / sample_rate
@@ -949,18 +955,23 @@ async def stream_speech(
data.profile_id, db, engine=engine,
)
audio, sample_rate = await tts_model.generate(
data.text,
voice_prompt,
data.language,
data.seed,
data.instruct,
)
from .utils.chunked_tts import generate_chunked
# Trim trailing silence/hallucination for Chatterbox output
trim_fn = None
if engine in ("chatterbox", "chatterbox_turbo"):
from .utils.audio import trim_tts_output
audio = trim_tts_output(audio, sample_rate)
trim_fn = trim_tts_output
audio, sample_rate = await generate_chunked(
tts_model,
data.text,
voice_prompt,
language=data.language,
seed=data.seed,
instruct=data.instruct,
max_chunk_chars=data.max_chunk_chars,
trim_fn=trim_fn,
)
wav_bytes = tts.audio_to_wav_bytes(audio, sample_rate)