diff options
| author | historia <historiavg@proton.me> | 2026-08-20 18:06:22 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-20 18:06:22 -0400 |
| commit | 5ca77f86b70718b4ef1a07299efbd6431268d546 (patch) | |
| tree | e50a439ea7a92bd628f78240fdbc2d4093df6928 /converter/tts.py | |
| parent | b873844f7eb681119542661ef588c5b452f88763 (diff) | |
| download | tts-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.py | 57 |
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") |
