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/audio.py | 6 +- app/converter/converter.py | 34 ++++++++---- app/converter/tts.py | 135 ++++++++++++++++++--------------------------- app/tests/test_tts.py | 58 ++++++++++--------- 4 files changed, 113 insertions(+), 120 deletions(-) (limited to 'app') diff --git a/app/converter/audio.py b/app/converter/audio.py index 81431cb..eb970ff 100644 --- a/app/converter/audio.py +++ b/app/converter/audio.py @@ -15,6 +15,10 @@ logger = logging.getLogger(__name__) CHUNKS_FOLDER = Path(__file__).resolve().parent.parent.parent / "app" / "chunks" +# Tolerance for "is this speed 1.0?" comparisons (banner display, atempo +# filter elision); shared by every speed check. +SPEED_EPSILON = 1e-6 + def atempo_filters(speed: float) -> str: """Return a comma-joined ffmpeg ``atempo`` filter chain for ``speed``. @@ -24,7 +28,7 @@ def atempo_filters(speed: float) -> str: """ if speed <= 0: raise ValueError(f"Speed must be a positive number, got {speed}") - if abs(speed - 1.0) < 1e-6: + if abs(speed - 1.0) < SPEED_EPSILON: return "" remaining = float(speed) chain = [] diff --git a/app/converter/converter.py b/app/converter/converter.py index 455c1fb..1abc85c 100644 --- a/app/converter/converter.py +++ b/app/converter/converter.py @@ -64,7 +64,9 @@ def setup_logging(debug: bool = False, console: bool = True) -> None: (DEBUG with --debug) so progress prints are never mirrored as timestamped log lines; httpx/httpcore request logs stay file-only. CONSOLE=False (the TUI run view owns the screen) keeps every record - in the file only. + in the file only. Safe to call repeatedly in one process (the hub + calls it once per conversion run): force=True replaces the previous + handlers instead of silently keeping them. """ LOGS_FOLDER.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler( @@ -82,9 +84,12 @@ def setup_logging(debug: bool = False, console: bool = True) -> None: level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=handlers, + force=True, ) if debug: logging.getLogger("converter").setLevel(logging.DEBUG) + else: + logging.getLogger("converter").setLevel(logging.INFO) def setup_directories() -> None: @@ -215,10 +220,16 @@ class AudiobookConverter: self.instructions = instructions self.request_options = dict(request_options or {}) self._validate_configuration() + # Interactive reporting (the TUI run view): PROGRESS receives an + # event dict per state change and turns the clients' console prints + # off (quiet is set at construction so connect-time lines respect it + # too); CANCEL (a threading.Event) stops the run between requests. + quiet = progress is not None if backend == BACKEND_FASTER: # The faster backend always voice-clones using a reference voice # configured on the server, so no local reference audio is needed. - self.tts = FasterTTSClient(voice=voice, api_url=api_url) + self.tts = FasterTTSClient(voice=voice, api_url=api_url, + quiet=quiet) elif backend == BACKEND_AUDIOCPP: # --voice picks the voice: a built-in speaker name on the # CustomVoice entry, or a server-side preset (cloning) @@ -230,7 +241,7 @@ class AudiobookConverter: model_id=model_id, instructions=instructions, request_options=self.request_options, - api_url=api_url) + api_url=api_url, quiet=quiet) else: self.tts = QwenTTSClient( voice_mode=voice_mode, @@ -239,14 +250,10 @@ class AudiobookConverter: skip_transcription=skip_transcription, language=self.language, api_url=api_url, + quiet=quiet, ) - # Interactive reporting/cancellation (the TUI run view): PROGRESS - # receives an event dict per state change and turns the console - # prints off (the view owns the screen); CANCEL (a - # threading.Event) stops the run between requests. self._progress = progress self.tts.cancel = cancel - self.tts.quiet = progress is not None def _emit(self, event: dict) -> None: """Send one progress event (a no-op without a progress callback).""" @@ -723,11 +730,11 @@ class AudiobookConverter: self._say(f"Output format: {self.output_format}") if self.single_file and self.output_format != "m4b": self._say("Chapter mode: single file (--single-file)") - if abs(self.speed - 1.0) >= 1e-6: + if abs(self.speed - 1.0) >= audio.SPEED_EPSILON: self._say(f"Playback speed: {self.speed:g}x") if self.debug: self._say(f"Debug dumps (per-chunk text + raw audio): {DEBUG_FOLDER}") - print("=" * 70) + self._say("=" * 70) # ------------------------------------------------------------------ # Pre-flight: overwrite checks before connecting to a TTS server @@ -857,11 +864,14 @@ class AudiobookConverter: successful = sum(results.values()) total = len(results) + # A cancelled run is not a successful run on either path (the TUI + # event consumer and the console summary report it consistently). + ok = not cancelled and total > 0 and successful == total self._emit({"kind": "done", "ok": successful, "total": total or len(planned), "cancelled": cancelled}) if self._progress is not None: - return not cancelled and total > 0 and successful == total + return ok print("\n" + "=" * 70) print("CONVERSION SUMMARY") @@ -888,4 +898,4 @@ class AudiobookConverter: print(f"\n[INFO] Generation completed in {duration}") logger.info("Generation completed in %s", duration) - return total > 0 and successful == total + return ok 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) diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py index c17609b..02b7dc4 100644 --- a/app/tests/test_tts.py +++ b/app/tests/test_tts.py @@ -339,18 +339,20 @@ class FasterTTSClientGenerateTests(unittest.TestCase): remaining = sorted(path.name for path in Path(self._tmp.name).glob("chunk_0001.*")) self.assertEqual(remaining, ["chunk_0001.wav"]) - def test_transient_failure_is_retried(self): + def test_transient_failure_fails_the_chunk_attempt(self): + # Retrying is the chunk-level policy's job + # (process_chunk_with_retry); one generate_chunk call makes one + # request attempt per sub-chunk. client = self._make_client() pcm = b"\x01\x00" * 10 with patch.object(client, "_request_pcm", side_effect=[RuntimeError("boom"), pcm]) as mock_pcm: result = client.generate_chunk("Hello.", 1) - self.assertIsNotNone(result) - self.assertEqual(mock_pcm.call_count, 2) + self.assertIsNone(result) + self.assertEqual(mock_pcm.call_count, 1) - def test_empty_pcm_response_is_treated_as_failure(self): + def test_empty_pcm_response_fails_the_chunk(self): client = self._make_client() - pcm = b"\x01\x00" * 10 def _response(body): response = MagicMock() @@ -359,20 +361,18 @@ class FasterTTSClientGenerateTests(unittest.TestCase): return response with patch("converter.tts.urllib.request.urlopen", - side_effect=[_response(b""), _response(pcm)]) as mock_urlopen: + side_effect=[_response(b"")]) as mock_urlopen: result = client.generate_chunk("Hello.", 1) - self.assertIsNotNone(result) - self.assertEqual(mock_urlopen.call_count, 2) - _, _, _, frames = self._read_wav(Path(result)) - self.assertEqual(frames, pcm) + self.assertIsNone(result) + self.assertEqual(mock_urlopen.call_count, 1) - def test_exhausted_subchunk_retries_fail_the_chunk(self): + def test_subchunk_request_failure_fails_the_chunk(self): client = self._make_client() with patch.object(client, "_request_pcm", side_effect=RuntimeError("down")) as mock_pcm: result = client.generate_chunk("Hello.", 1) self.assertIsNone(result) - self.assertEqual(mock_pcm.call_count, config.MAX_RETRIES) + self.assertEqual(mock_pcm.call_count, 1) def test_empty_text_fails_the_chunk(self): client = self._make_client() @@ -1289,22 +1289,25 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): self.assertIn("500", str(ctx.exception)) self.assertIn("bad voice", str(ctx.exception)) - def test_transient_failure_is_retried(self): + def test_transient_failure_fails_the_chunk_attempt(self): + # Retrying is the chunk-level policy's job + # (process_chunk_with_retry); one generate_chunk call makes one + # request attempt per sub-chunk. client = self._make_client() wav = self._wav_bytes() with patch.object(client, "_request_wav", side_effect=[RuntimeError("boom"), wav]) as mock_request: result = client.generate_chunk("Hello.", 1) - self.assertIsNotNone(result) - self.assertEqual(mock_request.call_count, 2) + self.assertIsNone(result) + self.assertEqual(mock_request.call_count, 1) - def test_exhausted_retries_fail_the_chunk(self): + def test_request_failure_fails_the_chunk(self): client = self._make_client() with patch.object(client, "_request_wav", side_effect=RuntimeError("down")) as mock_request: result = client.generate_chunk("Hello.", 1) self.assertIsNone(result) - self.assertEqual(mock_request.call_count, config.MAX_RETRIES) + self.assertEqual(mock_request.call_count, 1) def test_empty_text_fails_the_chunk(self): client = self._make_client() @@ -1396,7 +1399,7 @@ class AudioCppHeartbeatTests(unittest.TestCase): buf = io.StringIO() with patch.object(config, "HEARTBEAT_INTERVAL_SECONDS", 0.03), \ - patch.object(client, "_request_wav_with_retry", + patch.object(client, "_request_wav", side_effect=slow_request), \ redirect_stdout(buf): result = client.generate_chunk("Hello.", 1) @@ -1591,7 +1594,8 @@ class BackendWiringTests(unittest.TestCase): patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, backend=tts.BACKEND_FASTER, voice="narrator") - mock_faster.assert_called_once_with(voice="narrator", api_url=None) + mock_faster.assert_called_once_with(voice="narrator", api_url=None, + quiet=False) mock_qwen.assert_not_called() mock_audiocpp.assert_not_called() @@ -1606,7 +1610,7 @@ class BackendWiringTests(unittest.TestCase): model_id=None, instructions=None, request_options={}, - api_url=None) + api_url=None, quiet=False) mock_faster.assert_not_called() mock_qwen.assert_not_called() @@ -1618,7 +1622,7 @@ class BackendWiringTests(unittest.TestCase): model_id=None, instructions=None, request_options={}, - api_url=None) + api_url=None, quiet=False) def test_audiocpp_backend_model_id_is_wired_through(self): with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: @@ -1628,7 +1632,7 @@ class BackendWiringTests(unittest.TestCase): mock_audiocpp.assert_called_once_with( voice="narrator", language=config.LANGUAGE, model_id="higgs", instructions=None, - request_options={}, api_url=None) + request_options={}, api_url=None, quiet=False) def test_audiocpp_backend_instructions_and_options_are_wired_through(self): with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: @@ -1642,7 +1646,7 @@ class BackendWiringTests(unittest.TestCase): model_id=None, instructions="A warm adult narrator", request_options={"emotion": "neutral", "speed": "1.1"}, - api_url=None) + api_url=None, quiet=False) def test_qwen_backend_uses_qwen_client(self): with patch("converter.converter.FasterTTSClient") as mock_faster, \ @@ -1669,13 +1673,14 @@ class BackendWiringTests(unittest.TestCase): mock_audiocpp.assert_called_once_with( voice="narrator", language=config.LANGUAGE, model_id=None, instructions=None, request_options={}, - api_url="http://10.0.0.5:8080") + api_url="http://10.0.0.5:8080", quiet=False) with patch("converter.converter.FasterTTSClient") as mock_faster: AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, backend=tts.BACKEND_FASTER, voice="narrator", api_url="http://10.0.0.5:8000") mock_faster.assert_called_once_with(voice="narrator", - api_url="http://10.0.0.5:8000") + api_url="http://10.0.0.5:8000", + quiet=False) with patch("converter.converter.QwenTTSClient") as mock_qwen: AudiobookConverter(voice_mode=tts.VOICE_MODE_CUSTOM, backend=tts.BACKEND_QWEN, @@ -1683,7 +1688,8 @@ class BackendWiringTests(unittest.TestCase): mock_qwen.assert_called_once_with( voice_mode=tts.VOICE_MODE_CUSTOM, voice_clone_ref_audio=None, voice_clone_ref_text=None, skip_transcription=False, - language=config.LANGUAGE, api_url="http://10.0.0.5:7860") + language=config.LANGUAGE, api_url="http://10.0.0.5:7860", + quiet=False) def test_audiocpp_clone_mode_does_not_require_reference(self): # Cloning is server-side for the audiocpp backend, so the -- cgit v1.2.3