From 0b8485a5c8a87d3975cf03cd2a4af965848eb030 Mon Sep 17 00:00:00 2001 From: historia Date: Wed, 26 Aug 2026 00:22:15 -0400 Subject: fix(tts): honor quiet mode and drop request-level retries --- app/converter/tts.py | 135 +++++++++++++++++++++------------------------------ 1 file changed, 54 insertions(+), 81 deletions(-) (limited to 'app/converter/tts.py') diff --git a/app/converter/tts.py b/app/converter/tts.py index 7204e0c..8130a44 100644 --- a/app/converter/tts.py +++ b/app/converter/tts.py @@ -370,6 +370,11 @@ class _BaseTTSClient: cancel = None quiet = False + def _report(self, message: str) -> None: + """Print a console line unless quiet (the run view owns the screen).""" + if not self.quiet: + print(message) + def generate_chunk(self, text: str, chunk_num: int) -> Optional[str]: """Generate one audio chunk; returns its path in the chunks folder.""" raise NotImplementedError @@ -463,7 +468,11 @@ class QwenTTSClient(_BaseTTSClient): def __init__(self, voice_mode: str = "custom_voice", voice_clone_ref_audio: Optional[str] = None, voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False, - language: Optional[str] = None, api_url: Optional[str] = None): + language: Optional[str] = None, api_url: Optional[str] = None, + quiet: bool = False): + # Quiet before connecting so connect-time status lines never reach + # a screen the TUI run view owns. + self.quiet = bool(quiet) if voice_mode not in VOICE_MODES: raise ValueError( f"Unknown voice mode: {voice_mode!r} (expected one of {VOICE_MODES})" @@ -505,11 +514,11 @@ class QwenTTSClient(_BaseTTSClient): # Voice clone uses the Base-model demo, which is a separate server # from the CustomVoice demo (that one only exposes /run_instruct). self._init_client(api_url, clone=True) - print(f"[OK] Connected to Voice Clone API at {api_url}") + self._report(f"[OK] Connected to Voice Clone API at {api_url}") self._resolve_reference_text() else: self._init_client(api_url, clone=False) - print("[OK] Connected to Qwen API") + self._report("[OK] Connected to Qwen API") except Exception as exc: raise RuntimeError( f"Qwen API initialization failed at {api_url}: {exc}. " @@ -523,15 +532,16 @@ class QwenTTSClient(_BaseTTSClient): transcription, then x-vector-only mode.""" if not self.voice_clone_ref_text and self.voice_clone_ref_audio: if self.skip_transcription: - print("[INFO] Skipping reference audio transcription (--no-transcription).") + self._report("[INFO] Skipping reference audio transcription (--no-transcription).") else: - print("[INFO] Transcribing reference audio for voice cloning...") + self._report("[INFO] Transcribing reference audio for voice cloning...") self.voice_clone_ref_text = self.transcribe_audio(self.voice_clone_ref_audio) or "" if not self.voice_clone_ref_text: - print("[WARNING] No reference text available; using x-vector-only clone mode (lower quality).") - print(' Pass --transcription "..." for higher-quality in-context cloning.') + self._report("[WARNING] No reference text available; using " + "x-vector-only clone mode (lower quality).") + self._report(' Pass --transcription "..." for higher-quality in-context cloning.') else: - print(f"[OK] Reference text:\n{self.voice_clone_ref_text}") + self._report(f"[OK] Reference text:\n{self.voice_clone_ref_text}") def _init_client(self, url: str, clone: bool = False) -> None: """Initialize a Gradio client and store its API metadata. @@ -762,7 +772,11 @@ class FasterTTSClient(_BaseTTSClient): request, so long chunks are sub-chunked client-side. """ - def __init__(self, voice: Optional[str] = None, api_url: Optional[str] = None): + def __init__(self, voice: Optional[str] = None, api_url: Optional[str] = None, + quiet: bool = False): + # Quiet before connecting so connect-time status lines never reach + # a screen the TUI run view owns. + self.quiet = bool(quiet) self.voice = voice or config.FASTER_VOICE self.api_url = (api_url or config.FASTER_API_URL).rstrip("/") self._check_health() @@ -784,9 +798,9 @@ class FasterTTSClient(_BaseTTSClient): "The faster TTS server is running but its model is not loaded yet; " "wait for model download and startup to finish, then retry." ) - print(f"[OK] Connected to faster TTS API at {self.api_url} (voice '{self.voice}')") - print(f"[INFO] The server silently falls back to its first configured voice if " - f"'{self.voice}' is not defined in its voice config (see README).") + self._report(f"[OK] Connected to faster TTS API at {self.api_url} (voice '{self.voice}')") + self._report(f"[INFO] The server silently falls back to its first configured voice if " + f"'{self.voice}' is not defined in its voice config (see README).") # ------------------------------------------------------------------ # HTTP requests @@ -819,23 +833,6 @@ class FasterTTSClient(_BaseTTSClient): raise RuntimeError("Faster TTS server returned empty audio") return pcm - def _request_pcm_with_retry(self, text: str, chunk_num: int, sub_num: int, - sub_total: int) -> bytes: - """Request one sub-chunk, retrying transient failures.""" - for attempt in range(config.MAX_RETRIES): - self._check_cancelled() - try: - return self._request_pcm(text) - except ConversionCancelled: - raise - except Exception as exc: - logger.warning("Chunk %d sub-chunk %d/%d attempt %d failed: %s", - chunk_num, sub_num, sub_total, attempt + 1, exc) - if attempt < config.MAX_RETRIES - 1: - self._sleep(2 + 2 * attempt) - raise RuntimeError(f"Sub-chunk {sub_num}/{sub_total} failed after " - f"{config.MAX_RETRIES} attempts") - # ------------------------------------------------------------------ # Chunk generation # ------------------------------------------------------------------ @@ -850,8 +847,7 @@ class FasterTTSClient(_BaseTTSClient): pcm_parts: List[bytes] = [] with self._chunk_heartbeat(chunk_num): for sub_num, sub_text in enumerate(sub_chunks, 1): - pcm = self._request_pcm_with_retry( - sub_text, chunk_num, sub_num, len(sub_chunks)) + pcm = self._request_pcm(sub_text) pcm_parts.append(pcm) output_path = self._chunk_path(chunk_num, ".wav") @@ -936,7 +932,11 @@ class AudioCppTTSClient(_BaseTTSClient): api_url: Optional[str] = None, model_id: Optional[str] = None, instructions: Optional[str] = None, - request_options: Optional[Dict[str, str]] = None): + request_options: Optional[Dict[str, str]] = None, + quiet: bool = False): + # Quiet before connecting so connect-time status lines never reach + # a screen the TUI run view owns. + self.quiet = bool(quiet) self.api_url = (api_url or config.AUDIOCPP_API_URL).rstrip("/") # Per-run model selection: the --model CLI flag overrides config; an # empty value is resolved at connect time when the server hosts exactly @@ -989,6 +989,12 @@ class AudioCppTTSClient(_BaseTTSClient): # Connection # ------------------------------------------------------------------ + def _connected(self, mode: str) -> None: + """Report the resolved connection (MODE: speaker/voice/... label).""" + self._report(f"[OK] Connected to audio.cpp server at {self.api_url} " + f"(model '{self.model_id}', family '{self.family}', " + f"{mode})") + def _connect(self) -> None: """Health-check the server and resolve the model, family, task, and voice. @@ -1025,9 +1031,7 @@ class AudioCppTTSClient(_BaseTTSClient): self._require_synthesis_task(models) self.voice = speaker_display_name_for(self.voice) self.speaker_mode = True - print(f"[OK] Connected to audio.cpp server at {self.api_url} " - f"(model '{self.model_id}', family '{self.family}', " - f"speaker '{self.voice}')") + self._connected(f"speaker '{self.voice}'") if not self.speaker_mode: # Server-side preset (--voice): validate it and route to # the clone model entry when AUDIOCPP_CLONE_MODEL_ID is set. @@ -1043,9 +1047,7 @@ class AudioCppTTSClient(_BaseTTSClient): f"'{self.model_id}': the voice is described by the " "--instructions text instead (see README).") self._check_voice() - print(f"[OK] Connected to audio.cpp server at {self.api_url} " - f"(model '{self.model_id}', family '{self.family}', " - f"voice '{self.voice}')") + self._connected(f"voice '{self.voice}'") else: # No flag: the entry's capability picks the default mode. self._require_model_id(models) @@ -1062,25 +1064,19 @@ class AudioCppTTSClient(_BaseTTSClient): "description of the voice to synthesize with, e.g. " '--instructions "A warm adult female narrator with a ' 'British accent" (see README).') - print(f"[OK] Connected to audio.cpp server at {self.api_url} " - f"(model '{self.model_id}', family '{self.family}', " - "voice design)") - print(f"[INFO] Designing the voice from: {self.instructions}") + self._connected("voice design") + self._report(f"[INFO] Designing the voice from: {self.instructions}") elif capability == AUDIOCPP_VOICE_SPEAKER: # No flag on a CustomVoice entry: the built-in config.SPEAKER. self.voice = speaker_display_name() self.speaker_mode = True - print(f"[OK] Connected to audio.cpp server at {self.api_url} " - f"(model '{self.model_id}', family '{self.family}', " - f"speaker '{self.voice}')") + self._connected(f"speaker '{self.voice}'") elif self.instructions: # Families without built-in speakers can still get their voice # from the instruction alone (e.g. OmniVoice voice design). self.instruction_voice = True - print(f"[OK] Connected to audio.cpp server at {self.api_url} " - f"(model '{self.model_id}', family '{self.family}', " - "instruction voice)") - print(f"[INFO] Designing the voice from: {self.instructions}") + self._connected("instruction voice") + self._report(f"[INFO] Designing the voice from: {self.instructions}") else: raise RuntimeError( f"The audio.cpp model '{self.model_id}' (family " @@ -1091,9 +1087,9 @@ class AudioCppTTSClient(_BaseTTSClient): "families that support it, or select the CustomVoice entry " "for built-in speakers (see README).") if self.instructions and not self.design_mode and not self.instruction_voice: - print(f"[INFO] Sending instruction with every request: {self.instructions}") - print("[INFO] Its effect (style, emotion, delivery) depends on the " - "model family; models without instruction support ignore it.") + self._report(f"[INFO] Sending instruction with every request: {self.instructions}") + self._report("[INFO] Its effect (style, emotion, delivery) depends on the " + "model family; models without instruction support ignore it.") if config.AUDIOCPP_UNLOAD_MODELS: self._unload_server_models() @@ -1128,14 +1124,14 @@ class AudioCppTTSClient(_BaseTTSClient): with urllib.request.urlopen(request, timeout=10) as response: payload = json.loads(response.read().decode("utf-8")) except Exception as exc: - print(f"[WARNING] Could not unload previously loaded models at " - f"{self.api_url}: {exc}") + self._report(f"[WARNING] Could not unload previously loaded models at " + f"{self.api_url}: {exc}") return unloaded = [entry for entry in (payload.get("unloaded") or []) if isinstance(entry, str)] if unloaded: - print(f"[OK] Unloaded {len(unloaded)} model(s) from server memory: " - f"{', '.join(unloaded)}") + self._report(f"[OK] Unloaded {len(unloaded)} model(s) from server memory: " + f"{', '.join(unloaded)}") else: logger.debug("No loaded audio.cpp models to unload at %s", self.api_url) @@ -1371,12 +1367,7 @@ class AudioCppTTSClient(_BaseTTSClient): # ------------------------------------------------------------------ def _request_wav(self, text: str) -> 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. - """ + """POST one sub-chunk and return the raw WAV bytes.""" url = f"{self.api_url}/v1/audio/speech" payload: Dict[str, Any] = { "model": self.model_id, @@ -1434,23 +1425,6 @@ class AudioCppTTSClient(_BaseTTSClient): raise RuntimeError("audio.cpp server returned audio that is not a WAV file") return wav - def _request_wav_with_retry(self, text: str, chunk_num: int, sub_num: int, - sub_total: int) -> bytes: - """Request one sub-chunk, retrying transient failures.""" - for attempt in range(config.MAX_RETRIES): - self._check_cancelled() - try: - return self._request_wav(text) - except ConversionCancelled: - raise - except Exception as exc: - logger.warning("Chunk %d sub-chunk %d/%d attempt %d failed: %s", - chunk_num, sub_num, sub_total, attempt + 1, exc) - if attempt < config.MAX_RETRIES - 1: - self._sleep(2 + 2 * attempt) - raise RuntimeError(f"Sub-chunk {sub_num}/{sub_total} failed after " - f"{config.MAX_RETRIES} attempts") - # ------------------------------------------------------------------ # Chunk generation # ------------------------------------------------------------------ @@ -1472,8 +1446,7 @@ class AudioCppTTSClient(_BaseTTSClient): self._chunk_heartbeat(chunk_num): part_paths = [] for sub_num, sub_text in enumerate(sub_texts, 1): - wav = self._request_wav_with_retry( - sub_text, chunk_num, sub_num, len(sub_texts)) + wav = self._request_wav(sub_text) destination = Path(parts_dir) / f"part_{sub_num:02d}.wav" destination.write_bytes(wav) part_paths.append(destination) -- cgit v1.2.3