aboutsummaryrefslogtreecommitdiff
path: root/converter/tts.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-20 18:06:22 -0400
committerhistoria <historiavg@proton.me>2026-08-20 18:06:22 -0400
commit5ca77f86b70718b4ef1a07299efbd6431268d546 (patch)
treee50a439ea7a92bd628f78240fdbc2d4093df6928 /converter/tts.py
parentb873844f7eb681119542661ef588c5b452f88763 (diff)
downloadtts-audiobook-generator-5ca77f86b70718b4ef1a07299efbd6431268d546.tar.gz
fix: do not chunk with audio.cpp backend (double chunking)
Diffstat (limited to 'converter/tts.py')
-rw-r--r--converter/tts.py57
1 files changed, 45 insertions, 12 deletions
diff --git a/converter/tts.py b/converter/tts.py
index 1a5b1a0..afb0655 100644
--- a/converter/tts.py
+++ b/converter/tts.py
@@ -724,22 +724,35 @@ class AudioCppTTSClient(_BaseTTSClient):
server works for --voice runs, while speaker mode on such a server
fails with a hint to pass --voice.
+ Chunking: the server does its own long-form text chunking (its
+ ``text_chunk_size`` option, 8192 chars by default for qwen3_tts), so by
+ default each chapter is sent as a single request and the audio comes
+ back already stitched. With ``chunk_text=True`` (the --chunk CLI flag),
+ text is instead split client-side into CHUNK_SIZE-word sub-requests,
+ which may needlessly double-chunk — the warning is printed by the CLI.
+
Each response is a complete WAV file, so sub-request audio is
concatenated with the same lossless path used for the Gradio client.
"""
def __init__(self, voice: Optional[str] = None, language: Optional[str] = None,
- api_url: Optional[str] = None):
+ api_url: Optional[str] = None, chunk_text: bool = False):
self.api_url = (api_url or config.AUDIOCPP_API_URL).rstrip("/")
self.model_id = config.AUDIOCPP_MODEL_ID
# Validate before connecting so bad values fail fast without a server.
self.language = normalize_language(
language if language is not None else config.LANGUAGE)
- # Same seed convention as the Gradio client: one value per run,
- # reused for every request (see _resolve_request_seed).
+ # One seed value per run, reused for every request (see
+ # _resolve_request_seed). Unlike the Gradio demo, audio.cpp has no
+ # negative "randomize" seed, so a negative value means "send no seed
+ # at all" (see _request_wav) and the server randomizes.
self._seed = _resolve_request_seed()
self.preset_mode = bool(voice)
self.voice = voice or speaker_display_name()
+ # When False (default), each chapter is sent as one request and the
+ # server does its own long-form chunking (text_chunk_size); when True,
+ # text is split client-side into CHUNK_SIZE-word sub-requests first.
+ self.chunk_text = bool(chunk_text)
self._check_health()
model_ids = self._list_model_ids()
if self.preset_mode:
@@ -887,15 +900,23 @@ class AudioCppTTSClient(_BaseTTSClient):
# ------------------------------------------------------------------
def _request_wav(self, text: str) -> bytes:
- """POST one sub-chunk and return the raw WAV bytes."""
+ """POST one sub-chunk and return the raw WAV bytes.
+
+ The request timeout scales with the text length when a whole
+ chapter is sent in one request (no client-side chunking), since a
+ long chapter means many minutes of audio generated in one go.
+ """
url = f"{self.api_url}/v1/audio/speech"
payload: Dict[str, Any] = {
"model": self.model_id,
"input": text,
"voice": self.voice,
"language": self.language,
- "seed": self._seed,
}
+ if self._seed >= 0:
+ # audio.cpp has no negative "randomize" seed; a negative seed
+ # means "let the server randomize", so the field is omitted.
+ payload["seed"] = self._seed
if not self.preset_mode and config.INSTRUCT:
# Style instruction for the CustomVoice speakers; ignored by
# the Base (cloning) model.
@@ -903,8 +924,14 @@ class AudioCppTTSClient(_BaseTTSClient):
request = urllib.request.Request(
url, data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}, method="POST")
+ timeout = config.API_TIMEOUT
+ if not self.chunk_text:
+ # Estimated audio duration at 150 wpm, doubled plus a minute of
+ # slack, bounded below by the configured per-request timeout.
+ estimated_seconds = 60.0 * len(text.split()) / _ESTIMATED_WORDS_PER_MINUTE
+ timeout = max(timeout, int(estimated_seconds * 2) + 60)
try:
- with urllib.request.urlopen(request, timeout=config.API_TIMEOUT) as response:
+ with urllib.request.urlopen(request, timeout=timeout) as response:
wav = response.read()
except urllib.error.HTTPError as exc:
detail = ""
@@ -940,14 +967,20 @@ class AudioCppTTSClient(_BaseTTSClient):
def generate_chunk(self, text: str, chunk_num: int) -> Optional[str]:
"""Generate one audio chunk; returns its path in the chunks folder.
- The text is split into sub-requests of at most
- ``config.CHUNK_SIZE`` words each (defense in depth against
- pathological input, matching the Gradio client), each sub-request
- returns a complete WAV file, and the parts are concatenated into
- one chunk file.
+ By default the whole text goes out as a single request and the
+ server does its own long-form chunking (see the class docstring).
+ With ``chunk_text=True`` (--chunk), the text is split into
+ sub-requests of at most ``config.CHUNK_SIZE`` words each; each
+ sub-request returns a complete WAV file and the parts are
+ concatenated into one chunk file.
"""
try:
- sub_texts = split_into_chunks(text, max_words=config.CHUNK_SIZE)
+ if self.chunk_text:
+ sub_texts = split_into_chunks(text, max_words=config.CHUNK_SIZE)
+ elif text.strip():
+ sub_texts = [text]
+ else:
+ sub_texts = []
if not sub_texts:
raise RuntimeError("No text to synthesize")