diff options
Diffstat (limited to 'app/tests/test_tts.py')
| -rw-r--r-- | app/tests/test_tts.py | 355 |
1 files changed, 188 insertions, 167 deletions
diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py index 02b7dc4..2b2ac1c 100644 --- a/app/tests/test_tts.py +++ b/app/tests/test_tts.py @@ -4,20 +4,43 @@ import io import json import tempfile import time +import urllib.error import unittest import wave from contextlib import redirect_stdout from pathlib import Path from unittest.mock import MagicMock, patch -from converter import config, tts -from converter.converter import AudiobookConverter -from converter.tts import ( +from converter import config +from converter import converter as converter_mod +from converter.clients import ( + AUDIOCPP_DEFAULT_FAMILY_PROFILE, + AUDIOCPP_FAMILY_PROFILES, + AUDIOCPP_LANG_OMIT, + AUDIOCPP_TASK_TTS, + AUDIOCPP_TASK_VDES, + AUDIOCPP_VOICE_CLONE, + AUDIOCPP_VOICE_DESIGN, + AUDIOCPP_VOICE_SPEAKER, + BACKEND_AUDIOCPP, + BACKEND_FASTER, + BACKEND_QWEN, + LANGUAGE_ISO_CODES, + MODEL_SIZE, + SAMPLE_RATE, + TTS_LANGUAGES, + VOICE_MODE_CLONE, + VOICE_MODE_CUSTOM, AudioCppTTSClient, FasterTTSClient, QwenTTSClient, + audiocpp_entry_voice_capability, normalize_language, ) +from converter.converter import AudiobookConverter + +# Chunks folder handed to clients whose tests never write chunk files. +_DUMMY_CHUNKS = Path(tempfile.gettempdir()) / "audiobook_tts_test_chunks" class NormalizeLanguageTests(unittest.TestCase): @@ -43,7 +66,7 @@ class NormalizeLanguageTests(unittest.TestCase): self.assertEqual(normalize_language("it"), "Italian") def test_all_supported_languages_round_trip(self): - for name in tts.TTS_LANGUAGES: + for name in TTS_LANGUAGES: self.assertEqual(normalize_language(name.lower()), name) def test_unknown_language_rejected_with_guidance(self): @@ -65,34 +88,34 @@ class QwenTTSClientLanguageTests(unittest.TestCase): def _make_client(self, **kwargs): with patch.object(QwenTTSClient, "_connect"): - return QwenTTSClient(**kwargs) + return QwenTTSClient(_DUMMY_CHUNKS, **kwargs) def test_default_follows_config_for_each_mode(self): - custom = self._make_client(voice_mode=tts.VOICE_MODE_CUSTOM) + custom = self._make_client(voice_mode=VOICE_MODE_CUSTOM) self.assertEqual(custom.language, config.LANGUAGE) - clone = self._make_client(voice_mode=tts.VOICE_MODE_CLONE, + clone = self._make_client(voice_mode=VOICE_MODE_CLONE, voice_clone_ref_audio="ref.wav") self.assertEqual(clone.language, config.LANGUAGE) def test_explicit_language_normalized(self): - client = self._make_client(voice_mode=tts.VOICE_MODE_CUSTOM, language="ja") + client = self._make_client(voice_mode=VOICE_MODE_CUSTOM, language="ja") self.assertEqual(client.language, "Japanese") def test_invalid_language_fails_before_connect(self): with patch.object(QwenTTSClient, "_connect") as mock_connect: with self.assertRaises(ValueError): - QwenTTSClient(language="klingon") + QwenTTSClient(_DUMMY_CHUNKS, language="klingon") mock_connect.assert_not_called() def test_api_url_override_stored(self): - client = self._make_client(voice_mode=tts.VOICE_MODE_CUSTOM, + client = self._make_client(voice_mode=VOICE_MODE_CUSTOM, api_url="http://10.0.0.5:7860") self.assertEqual(client.api_url, "http://10.0.0.5:7860") def test_api_url_override_used_by_connect(self): with patch.object(QwenTTSClient, "_init_client") as mk_init: client = QwenTTSClient.__new__(QwenTTSClient) - client.voice_mode = tts.VOICE_MODE_CUSTOM + client.voice_mode = VOICE_MODE_CUSTOM client.api_url = "http://10.0.0.5:7860" client._connect() mk_init.assert_called_once_with("http://10.0.0.5:7860", clone=False) @@ -105,24 +128,24 @@ class SeedResolutionTests(unittest.TestCase): def _make_client(self, **kwargs): with patch.object(QwenTTSClient, "_connect"): - return QwenTTSClient(**kwargs) + return QwenTTSClient(_DUMMY_CHUNKS, **kwargs) def test_constant_seed_draws_one_nonnegative_seed(self): with patch.object(config, "CONSTANT_SEED", True), \ patch.object(config, "SEED", -1): - client = self._make_client(voice_mode=tts.VOICE_MODE_CUSTOM) + client = self._make_client(voice_mode=VOICE_MODE_CUSTOM) self.assertGreaterEqual(client._seed, 0) def test_explicit_seed_wins_over_constant_seed(self): with patch.object(config, "CONSTANT_SEED", True), \ patch.object(config, "SEED", 42): - client = self._make_client(voice_mode=tts.VOICE_MODE_CUSTOM) + client = self._make_client(voice_mode=VOICE_MODE_CUSTOM) self.assertEqual(client._seed, 42) def test_without_constant_seed_minus_one_is_forwarded(self): with patch.object(config, "CONSTANT_SEED", False), \ patch.object(config, "SEED", -1): - client = self._make_client(voice_mode=tts.VOICE_MODE_CUSTOM) + client = self._make_client(voice_mode=VOICE_MODE_CUSTOM) self.assertEqual(client._seed, -1) def test_resolved_seed_is_reused_across_requests(self): @@ -134,7 +157,7 @@ class SeedResolutionTests(unittest.TestCase): } } client = QwenTTSClient.__new__(QwenTTSClient) - client.voice_mode = tts.VOICE_MODE_CUSTOM + client.voice_mode = VOICE_MODE_CUSTOM client.language = "English" client._seed = 1234 client.api_info = api_info @@ -159,7 +182,7 @@ class PayloadLanguageTests(unittest.TestCase): def _custom_client(self, language, endpoint, api_info=None): client = QwenTTSClient.__new__(QwenTTSClient) - client.voice_mode = tts.VOICE_MODE_CUSTOM + client.voice_mode = VOICE_MODE_CUSTOM client.language = language client._seed = config.SEED client.api_info = api_info if api_info is not None else { @@ -170,7 +193,7 @@ class PayloadLanguageTests(unittest.TestCase): def _clone_client(self, language, endpoint, api_info=None, ref_text="hello"): client = QwenTTSClient.__new__(QwenTTSClient) - client.voice_mode = tts.VOICE_MODE_CLONE + client.voice_mode = VOICE_MODE_CLONE client.language = language client._seed = config.SEED client.voice_clone_ref_audio = str(self.ref_audio) @@ -220,7 +243,7 @@ class PayloadLanguageTests(unittest.TestCase): client = self._clone_client("English", "/generate_voice_clone", api_info=api_info) client._generate_voice_clone("text") kwargs = client.clone_client.predict.call_args.kwargs - self.assertEqual(kwargs["model_size"], tts.MODEL_SIZE) + self.assertEqual(kwargs["model_size"], MODEL_SIZE) self.assertEqual(kwargs["seed"], config.SEED) @@ -236,32 +259,33 @@ class FasterTTSClientHealthTests(unittest.TestCase): def test_unreachable_server_raises_with_readme_pointer(self): import urllib.error - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=urllib.error.URLError("Connection refused")): with self.assertRaises(RuntimeError) as ctx: - FasterTTSClient() + FasterTTSClient(_DUMMY_CHUNKS) message = str(ctx.exception) self.assertIn("not reachable", message) self.assertIn("README", message) def test_model_not_loaded_raises(self): - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._health_response(model_loaded=False)): with self.assertRaises(RuntimeError) as ctx: - FasterTTSClient() + FasterTTSClient(_DUMMY_CHUNKS) self.assertIn("not loaded", str(ctx.exception)) def test_healthy_server_defaults_from_config(self): - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._health_response()): - client = FasterTTSClient() + client = FasterTTSClient(_DUMMY_CHUNKS) self.assertEqual(client.voice, config.FASTER_VOICE) self.assertEqual(client.api_url, config.FASTER_API_URL.rstrip("/")) def test_explicit_voice_and_url_override_config(self): - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._health_response()): - client = FasterTTSClient(voice="narrator", api_url="http://10.0.0.5:9000/") + client = FasterTTSClient(_DUMMY_CHUNKS, + voice="narrator", api_url="http://10.0.0.5:9000/") self.assertEqual(client.voice, "narrator") self.assertEqual(client.api_url, "http://10.0.0.5:9000") @@ -271,18 +295,16 @@ class FasterTTSClientGenerateTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() - self._chunks = patch.object(tts, "CHUNKS_FOLDER", Path(self._tmp.name)) - self._chunks.start() - self._sleep = patch("converter.tts.time.sleep") + self._sleep = patch("converter.clients.base.time.sleep") self._sleep.start() def tearDown(self): self._sleep.stop() - self._chunks.stop() self._tmp.cleanup() def _make_client(self): client = FasterTTSClient.__new__(FasterTTSClient) + client.chunks_dir = Path(self._tmp.name) client.voice = "default" client.api_url = "http://127.0.0.1:8000" return client @@ -303,7 +325,7 @@ class FasterTTSClientGenerateTests(unittest.TestCase): channels, sampwidth, framerate, frames = self._read_wav(path) self.assertEqual(channels, 1) self.assertEqual(sampwidth, 2) - self.assertEqual(framerate, tts.SAMPLE_RATE) + self.assertEqual(framerate, SAMPLE_RATE) self.assertEqual(frames, pcm) def test_long_text_is_subchunked_and_concatenated_in_order(self): @@ -360,7 +382,7 @@ class FasterTTSClientGenerateTests(unittest.TestCase): response.read.return_value = body return response - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=[_response(b"")]) as mock_urlopen: result = client.generate_chunk("Hello.", 1) self.assertIsNone(result) @@ -386,7 +408,7 @@ class FasterTTSClientGenerateTests(unittest.TestCase): response = MagicMock() response.__enter__.return_value = response response.read.return_value = b"\x01\x00" * 10 - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=response) as mock_urlopen: pcm = client._request_pcm("Hello world.") self.assertEqual(pcm, b"\x01\x00" * 10) @@ -401,7 +423,7 @@ class FasterTTSClientGenerateTests(unittest.TestCase): client = self._make_client() text = " ".join(f"word{i}" for i in range(12)) # 12 words -> expected 4.8s, half is 2.4s -> 2.5s of audio passes. - pcm = b"\x01\x00" * int(2.5 * tts.SAMPLE_RATE) + pcm = b"\x01\x00" * int(2.5 * SAMPLE_RATE) with patch.object(client, "_request_pcm", return_value=pcm): result = client.generate_chunk(text, 1) self.assertIsNotNone(result) @@ -412,16 +434,14 @@ class QwenTTSClientGenerateTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() - self._chunks = patch.object(tts, "CHUNKS_FOLDER", Path(self._tmp.name)) - self._chunks.start() def tearDown(self): - self._chunks.stop() self._tmp.cleanup() def _make_client(self): client = QwenTTSClient.__new__(QwenTTSClient) - client.voice_mode = tts.VOICE_MODE_CUSTOM + client.chunks_dir = Path(self._tmp.name) + client.voice_mode = VOICE_MODE_CUSTOM return client @staticmethod @@ -429,7 +449,7 @@ class QwenTTSClientGenerateTests(unittest.TestCase): with wave.open(str(path), "wb") as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) - wav_file.setframerate(tts.SAMPLE_RATE) + wav_file.setframerate(SAMPLE_RATE) wav_file.writeframes(frames) return path @@ -513,17 +533,17 @@ class AudioCppTTSClientHealthTests(unittest.TestCase): return _dispatch def _client(self, voice=None, language=None, model_id=None, **kwargs): - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=self._get_responses(**kwargs)): - return AudioCppTTSClient(voice=voice, language=language, - model_id=model_id) + return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice, + language=language, model_id=model_id) def test_unreachable_server_raises_with_readme_pointer(self): import urllib.error - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=urllib.error.URLError("Connection refused")): with self.assertRaises(RuntimeError) as ctx: - AudioCppTTSClient() + AudioCppTTSClient(_DUMMY_CHUNKS) message = str(ctx.exception) self.assertIn("not reachable", message) self.assertIn("README", message) @@ -625,9 +645,9 @@ class AudioCppTTSClientHealthTests(unittest.TestCase): self.assertEqual(client.voice, "narrator") def test_invalid_language_fails_before_connect(self): - with patch("converter.tts.urllib.request.urlopen") as mock_urlopen: + with patch("converter.clients.faster.urllib.request.urlopen") as mock_urlopen: with self.assertRaises(ValueError): - AudioCppTTSClient(language="klingon") + AudioCppTTSClient(_DUMMY_CHUNKS, language="klingon") mock_urlopen.assert_not_called() def test_explicit_language_normalized(self): @@ -651,7 +671,7 @@ class AudioCppTTSClientHealthTests(unittest.TestCase): def test_preset_mode_falls_back_when_clone_model_not_on_server(self): with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \ patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"), \ - self.assertLogs("converter.tts", level="WARNING") as logs: + self.assertLogs("converter.clients.audiocpp", level="WARNING") as logs: client = self._client( voice="narrator", models={"data": [{"id": "qwen3-tts", "family": "qwen3_tts"}, @@ -730,7 +750,7 @@ class AudioCppTTSClientHealthTests(unittest.TestCase): # requirement error lists both configured ids instead. with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \ patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"), \ - self.assertNoLogs("converter.tts", level="WARNING"): + self.assertNoLogs("converter.clients.audiocpp", level="WARNING"): with self.assertRaises(RuntimeError) as ctx: self._client(voice="narrator", models={"data": [{"id": "pocket-tts"}]}) @@ -774,16 +794,17 @@ class AudioCppTaskDetectionTests(unittest.TestCase): return self._json_response({"voices": ["narrator"]}) raise AssertionError(f"unexpected URL: {url}") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=_dispatch): - return AudioCppTTSClient(voice=voice, instructions=instructions, + return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice, + instructions=instructions, request_options=request_options) def test_missing_task_falls_back_to_tts(self): # Servers that predate the task field hosted plain TTS models. client = self._client(models={"data": [ {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts"}]}) - self.assertEqual(client.task, tts.AUDIOCPP_TASK_TTS) + self.assertEqual(client.task, AUDIOCPP_TASK_TTS) self.assertFalse(client.design_mode) def test_task_detected_from_models_endpoint(self): @@ -791,7 +812,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase): {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts", "task": "vdes"}]}, instructions="A warm adult narrator") - self.assertEqual(client.task, tts.AUDIOCPP_TASK_VDES) + self.assertEqual(client.task, AUDIOCPP_TASK_VDES) self.assertTrue(client.design_mode) def test_clon_task_entry_connects_in_preset_mode(self): @@ -912,15 +933,15 @@ class AudioCppFamilyDetectionTests(unittest.TestCase): return self._json_response({"voices": [voice] if voice else []}) raise AssertionError(f"unexpected URL: {url}") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=_dispatch): - return AudioCppTTSClient(voice=voice) + return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice) def test_family_detected_from_models_endpoint(self): client = self._client(models={"data": [ {"id": config.AUDIOCPP_MODEL_ID, "family": "higgs_audio_tts"}]}) self.assertEqual(client.family, "higgs_audio_tts") - self.assertIs(client.profile, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE) + self.assertIs(client.profile, AUDIOCPP_DEFAULT_FAMILY_PROFILE) def test_missing_family_uses_generic_profile(self): # A missing family is unknown (not guessed as qwen3_tts): it falls @@ -928,14 +949,14 @@ class AudioCppFamilyDetectionTests(unittest.TestCase): client = self._client(models={"data": [ {"id": config.AUDIOCPP_MODEL_ID}]}) self.assertEqual(client.family, "") - self.assertIs(client.profile, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE) + self.assertIs(client.profile, AUDIOCPP_DEFAULT_FAMILY_PROFILE) def test_unknown_family_uses_generic_profile(self): client = self._client(models={"data": [ {"id": config.AUDIOCPP_MODEL_ID, "family": "future_tts"}]}) self.assertEqual(client.family, "future_tts") - self.assertIs(client.profile, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE) - self.assertEqual(client.profile.language_style, tts.AUDIOCPP_LANG_OMIT) + self.assertIs(client.profile, AUDIOCPP_DEFAULT_FAMILY_PROFILE) + self.assertEqual(client.profile.language_style, AUDIOCPP_LANG_OMIT) def test_speaker_mode_rejected_for_clone_only_family(self): client = None @@ -977,7 +998,7 @@ class AudioCppFamilyDetectionTests(unittest.TestCase): def test_clone_model_id_of_different_family_is_ignored(self): with patch.object(config, "AUDIOCPP_MODEL_ID", "higgs"), \ patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen-clone"), \ - self.assertLogs("converter.tts", level="WARNING") as logs: + self.assertLogs("converter.clients.audiocpp", level="WARNING") as logs: client = self._client(models={"data": [ {"id": "higgs", "family": "higgs_audio_tts"}, {"id": "qwen-clone", "family": "qwen3_tts"}]}) @@ -989,7 +1010,7 @@ class AudioCppFamilyDetectionTests(unittest.TestCase): def test_clone_model_id_missing_on_non_qwen_server_is_debug_only(self): with patch.object(config, "AUDIOCPP_MODEL_ID", "higgs"), \ patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen-clone"), \ - self.assertNoLogs("converter.tts", level="WARNING"): + self.assertNoLogs("converter.clients.audiocpp", level="WARNING"): client = self._client(models={"data": [ {"id": "higgs", "family": "higgs_audio_tts"}]}) self.assertEqual(client.model_id, "higgs") @@ -997,7 +1018,7 @@ class AudioCppFamilyDetectionTests(unittest.TestCase): def test_clone_model_id_missing_on_qwen_server_still_warns(self): with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \ patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"), \ - self.assertLogs("converter.tts", level="WARNING") as logs: + self.assertLogs("converter.clients.audiocpp", level="WARNING") as logs: client = self._client(models={"data": [ {"id": "qwen3-tts", "family": "qwen3_tts"}, {"id": "pocket-tts", "family": "pocket_tts"}]}) @@ -1005,54 +1026,54 @@ class AudioCppFamilyDetectionTests(unittest.TestCase): self.assertTrue(any("qwen3-tts-clone" in line for line in logs.output)) def test_iso_language_code_helper(self): - self.assertEqual(tts.LANGUAGE_ISO_CODES["English"], "en") - self.assertIsNone(tts.LANGUAGE_ISO_CODES.get("Auto")) + self.assertEqual(LANGUAGE_ISO_CODES["English"], "en") + self.assertIsNone(LANGUAGE_ISO_CODES.get("Auto")) class AudiocppEntryVoiceCapabilityTests(unittest.TestCase): """The per-entry voice capability resolver (speaker/clone/design).""" def _cap(self, family="", task="tts", model_id=""): - return tts.audiocpp_entry_voice_capability(family, task, model_id) + return audiocpp_entry_voice_capability(family, task, model_id) def test_vdes_task_is_design(self): self.assertEqual(self._cap("qwen3_tts", "vdes", "Qwen3-TTS-VoiceDesign-GGUF"), - tts.AUDIOCPP_VOICE_DESIGN) + AUDIOCPP_VOICE_DESIGN) def test_qwen_customvoice_entry_is_speaker(self): self.assertEqual(self._cap("qwen3_tts", "tts", "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF"), - tts.AUDIOCPP_VOICE_SPEAKER) + AUDIOCPP_VOICE_SPEAKER) def test_qwen_base_entry_is_clone(self): self.assertEqual(self._cap("qwen3_tts", "tts", "Qwen3-TTS-12Hz-1.7B-Base-GGUF"), - tts.AUDIOCPP_VOICE_CLONE) + AUDIOCPP_VOICE_CLONE) def test_qwen_unidentified_entry_is_clone(self): self.assertEqual(self._cap("qwen3_tts", "tts", "qwen"), - tts.AUDIOCPP_VOICE_CLONE) + AUDIOCPP_VOICE_CLONE) def test_other_families_are_clone(self): self.assertEqual(self._cap("higgs_audio_tts", "tts", "higgs"), - tts.AUDIOCPP_VOICE_CLONE) + AUDIOCPP_VOICE_CLONE) def test_missing_family_is_clone(self): self.assertEqual(self._cap("", "tts", "legacy"), - tts.AUDIOCPP_VOICE_CLONE) + AUDIOCPP_VOICE_CLONE) def test_customvoice_match_is_case_insensitive(self): self.assertEqual(self._cap("qwen3_tts", "tts", "Qwen3-TTS-12Hz-1.7B-CUSTOMVOICE-GGUF"), - tts.AUDIOCPP_VOICE_SPEAKER) + AUDIOCPP_VOICE_SPEAKER) def test_customvoice_id_in_other_family_is_not_speaker(self): # The "customvoice" substring only marks a speaker for the qwen3_tts # family; another family with a lookalike id stays clone-only. self.assertEqual(self._cap("future_tts", "tts", "Qwen3-TTS-CustomVoice"), - tts.AUDIOCPP_VOICE_CLONE) + AUDIOCPP_VOICE_CLONE) class AudioCppTTSClientRequestTests(unittest.TestCase): @@ -1060,21 +1081,18 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() - self._chunks = patch.object(tts, "CHUNKS_FOLDER", Path(self._tmp.name)) - self._chunks.start() - self._sleep = patch("converter.tts.time.sleep") + self._sleep = patch("converter.clients.base.time.sleep") self._sleep.start() def tearDown(self): self._sleep.stop() - self._chunks.stop() self._tmp.cleanup() - @staticmethod - def _make_client(preset_mode=False, voice="Vivian", language="English", seed=-1, + def _make_client(self, preset_mode=False, voice="Vivian", language="English", seed=-1, family="qwen3_tts", task="tts", instructions=None, request_options=None): client = AudioCppTTSClient.__new__(AudioCppTTSClient) + client.chunks_dir = Path(self._tmp.name) client.api_url = "http://127.0.0.1:8080" client.model_id = config.AUDIOCPP_MODEL_ID client.preset_mode = preset_mode @@ -1083,23 +1101,23 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): client._seed = seed client.family = family client.task = task - client.profile = tts.AUDIOCPP_FAMILY_PROFILES.get( - family, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE) + client.profile = AUDIOCPP_FAMILY_PROFILES.get( + family, AUDIOCPP_DEFAULT_FAMILY_PROFILE) client.instructions = instructions or "" client.request_options = dict(request_options or {}) - client.design_mode = task == tts.AUDIOCPP_TASK_VDES + client.design_mode = task == AUDIOCPP_TASK_VDES # Mirrors the connect-time rule: an instruction-defined voice on a # clone-capable entry with no --voice (design mode takes precedence). - capability = tts.audiocpp_entry_voice_capability( + capability = audiocpp_entry_voice_capability( family, task, client.model_id) client.instruction_voice = ( not preset_mode and not client.design_mode - and capability == tts.AUDIOCPP_VOICE_CLONE + and capability == AUDIOCPP_VOICE_CLONE and bool(client.instructions)) return client @staticmethod - def _wav_bytes(frames=b"\x01\x00" * 10, rate=tts.SAMPLE_RATE): + def _wav_bytes(frames=b"\x01\x00" * 10, rate=SAMPLE_RATE): buffer = io.BytesIO() with wave.open(buffer, "wb") as wav_file: wav_file.setnchannels(1) @@ -1117,7 +1135,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_payload_includes_model_input_voice_language_and_seed(self): client = self._make_client(preset_mode=True, voice="narrator", language="Japanese", seed=1234) - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello world.") request = mock_urlopen.call_args[0][0] @@ -1133,7 +1151,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_negative_seed_omitted_from_payload(self): client = self._make_client(preset_mode=True, voice="narrator", seed=-1) - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello world.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1142,7 +1160,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_request_timeout_is_the_configured_api_timeout(self): client = self._make_client() long_text = " ".join(f"word{i}" for i in range(1500)) - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav(long_text) timeout = mock_urlopen.call_args[1]["timeout"] @@ -1150,7 +1168,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_speaker_mode_sends_instruct(self): client = self._make_client(preset_mode=False) - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1160,7 +1178,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): # --instructions overrides the INSTRUCT default in speaker mode. client = self._make_client(preset_mode=False, instructions="Read whisper quiet.") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1171,7 +1189,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): # instruction reach the model. client = self._make_client(preset_mode=True, voice="narrator", instructions="Calm and steady.") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1181,7 +1199,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_design_mode_payload_omits_voice_and_sends_instructions(self): client = self._make_client(task="vdes", instructions="A warm adult narrator") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1193,7 +1211,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): # takes Qwen display names like the other variants. client = self._make_client(task="vdes", language="Japanese", instructions="A warm adult narrator") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1204,7 +1222,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): # no speaker name is invented, the instruction carries the voice. client = self._make_client(family="omnivoice", instructions="female, young adult") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1216,7 +1234,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): client = self._make_client(preset_mode=True, voice="narrator", request_options={"emotion": "neutral", "speed": "1.1"}) - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1225,7 +1243,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_empty_request_options_omit_options_field(self): client = self._make_client(preset_mode=True, voice="narrator") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1235,7 +1253,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): # Clone-only families (higgs_audio_tts, voxcpm2, ...) detect the # language themselves and take no style instruction. client = self._make_client(preset_mode=False, family="higgs_audio_tts") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1245,7 +1263,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_iso_family_sends_language_code(self): client = self._make_client(preset_mode=True, voice="narrator", language="Japanese", family="index_tts2") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1254,7 +1272,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_iso_family_auto_omits_language(self): client = self._make_client(preset_mode=True, voice="narrator", language="Auto", family="index_tts2") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1263,7 +1281,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_qwen_language_display_name_still_sent(self): client = self._make_client(preset_mode=True, voice="narrator", language="Japanese", family="qwen3_tts") - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(self._wav_bytes())) as mock_urlopen: client._request_wav("Hello.") payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) @@ -1272,7 +1290,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): def test_non_wav_response_rejected(self): client = self._make_client() for body in (b"", b"RIFFxxxx", b"MP3DATA-MP3DATA", b"RIFF\x00\x00\x00\x00mpeg"): - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.faster.urllib.request.urlopen", return_value=self._post_response(body)): with self.assertRaises(RuntimeError): client._request_wav("Hello.") @@ -1283,7 +1301,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): error = urllib.error.HTTPError( "http://127.0.0.1:8080/v1/audio/speech", 500, "Server Error", {}, io.BytesIO(b'{"error":"bad voice"}')) - with patch("converter.tts.urllib.request.urlopen", side_effect=error): + with patch("converter.clients.faster.urllib.request.urlopen", side_effect=error): with self.assertRaises(RuntimeError) as ctx: client._request_wav("Hello.") self.assertIn("500", str(ctx.exception)) @@ -1327,7 +1345,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): with wave.open(str(path), "rb") as wav_file: self.assertEqual(wav_file.getnchannels(), 1) self.assertEqual(wav_file.getsampwidth(), 2) - self.assertEqual(wav_file.getframerate(), tts.SAMPLE_RATE) + self.assertEqual(wav_file.getframerate(), SAMPLE_RATE) self.assertEqual(wav_file.readframes(wav_file.getnframes()), frames) def test_long_text_is_subchunked_and_concatenated_in_order(self): @@ -1360,16 +1378,13 @@ class AudioCppHeartbeatTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() - self._chunks = patch.object(tts, "CHUNKS_FOLDER", Path(self._tmp.name)) - self._chunks.start() def tearDown(self): - self._chunks.stop() self._tmp.cleanup() - @staticmethod - def _client(): + def _client(self): client = AudioCppTTSClient.__new__(AudioCppTTSClient) + client.chunks_dir = Path(self._tmp.name) client.api_url = "http://127.0.0.1:8080" client.model_id = config.AUDIOCPP_MODEL_ID client.preset_mode = False @@ -1377,7 +1392,7 @@ class AudioCppHeartbeatTests(unittest.TestCase): client.language = "English" client._seed = -1 client.family = "qwen3_tts" - client.profile = tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE + client.profile = AUDIOCPP_DEFAULT_FAMILY_PROFILE return client @staticmethod @@ -1386,7 +1401,7 @@ class AudioCppHeartbeatTests(unittest.TestCase): with wave.open(buffer, "wb") as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) - wav_file.setframerate(tts.SAMPLE_RATE) + wav_file.setframerate(SAMPLE_RATE) wav_file.writeframes(b"\x01\x00" * 10) return buffer.getvalue() @@ -1416,15 +1431,13 @@ class AudioCppTTSClientTruncationTests(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() - self._chunks = patch.object(tts, "CHUNKS_FOLDER", Path(self._tmp.name)) - self._chunks.start() def tearDown(self): - self._chunks.stop() self._tmp.cleanup() def _make_client(self): client = AudioCppTTSClient.__new__(AudioCppTTSClient) + client.chunks_dir = Path(self._tmp.name) client.api_url = "http://127.0.0.1:8080" client.model_id = config.AUDIOCPP_MODEL_ID client.preset_mode = True @@ -1432,7 +1445,7 @@ class AudioCppTTSClientTruncationTests(unittest.TestCase): client.language = "English" client._seed = -1 client.family = "qwen3_tts" - client.profile = tts.AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] + client.profile = AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] return client @staticmethod @@ -1441,7 +1454,7 @@ class AudioCppTTSClientTruncationTests(unittest.TestCase): with wave.open(buffer, "wb") as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) - wav_file.setframerate(tts.SAMPLE_RATE) + wav_file.setframerate(SAMPLE_RATE) wav_file.writeframes(frames) return buffer.getvalue() @@ -1449,7 +1462,7 @@ class AudioCppTTSClientTruncationTests(unittest.TestCase): client = self._make_client() text = " ".join(f"word{i}" for i in range(12)) # 12 words -> expected 4.8s, half is 2.4s -> 2.5s of audio passes. - wav = self._wav_bytes(b"\x01\x00" * int(2.5 * tts.SAMPLE_RATE)) + wav = self._wav_bytes(b"\x01\x00" * int(2.5 * SAMPLE_RATE)) with patch.object(client, "_request_wav", return_value=wav): result = client.generate_chunk(text, 1) self.assertIsNotNone(result) @@ -1473,7 +1486,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase): def test_posts_to_unload_all_models(self): client = self._client() - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.audiocpp.urllib.request.urlopen", return_value=self._response(b'{"unloaded": ["qwen"]}')) as mock_urlopen: client._unload_server_models() request = mock_urlopen.call_args[0][0] @@ -1485,7 +1498,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase): def test_reports_unloaded_ids(self): client = self._client() buf = io.StringIO() - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.audiocpp.urllib.request.urlopen", return_value=self._response(b'{"unloaded": ["a", "b"]}')), \ redirect_stdout(buf): client._unload_server_models() @@ -1495,7 +1508,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase): def test_no_loaded_models_is_silent(self): client = self._client() buf = io.StringIO() - with patch("converter.tts.urllib.request.urlopen", + with patch("converter.clients.audiocpp.urllib.request.urlopen", return_value=self._response(b'{"unloaded": []}')), \ redirect_stdout(buf): client._unload_server_models() @@ -1504,8 +1517,8 @@ class AudioCppUnloadModelsTests(unittest.TestCase): def test_http_error_warns_and_continues(self): client = self._client() buf = io.StringIO() - with patch("converter.tts.urllib.request.urlopen", - side_effect=tts.urllib.error.HTTPError( + with patch("converter.clients.audiocpp.urllib.request.urlopen", + side_effect=urllib.error.HTTPError( "http://127.0.0.1:8080/v1/tasks/unload_all_models", 404, "Not Found", None, io.BytesIO())), \ redirect_stdout(buf): @@ -1517,8 +1530,8 @@ class AudioCppUnloadModelsTests(unittest.TestCase): def test_connection_error_warns_and_continues(self): client = self._client() buf = io.StringIO() - with patch("converter.tts.urllib.request.urlopen", - side_effect=tts.urllib.error.URLError("refused")), \ + with patch("converter.clients.audiocpp.urllib.request.urlopen", + side_effect=urllib.error.URLError("refused")), \ redirect_stdout(buf): client._unload_server_models() self.assertIn("[WARNING]", buf.getvalue()) @@ -1532,8 +1545,8 @@ class AudioCppUnloadModelsTests(unittest.TestCase): client.language = "English" client._seed = -1 client.family = "qwen3_tts" - client.task = tts.AUDIOCPP_TASK_TTS - client.profile = tts.AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] + client.task = AUDIOCPP_TASK_TTS + client.profile = AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] client.design_mode = False client.instruction_voice = False client.speaker_mode = False @@ -1562,8 +1575,8 @@ class AudioCppUnloadModelsTests(unittest.TestCase): client.language = "English" client._seed = -1 client.family = "qwen3_tts" - client.task = tts.AUDIOCPP_TASK_TTS - client.profile = tts.AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] + client.task = AUDIOCPP_TASK_TTS + client.profile = AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] client.design_mode = False client.instruction_voice = False client.speaker_mode = False @@ -1579,7 +1592,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase): patch.object(client, "_resolve_family"), \ patch.object(client, "_resolve_task"), \ patch.object(client, "_check_voice"), \ - patch.object(tts.config, "AUDIOCPP_UNLOAD_MODELS", False), \ + patch.object(config, "AUDIOCPP_UNLOAD_MODELS", False), \ patch.object(client, "_unload_server_models") as mock_unload: client._connect() mock_unload.assert_not_called() @@ -1592,9 +1605,10 @@ class BackendWiringTests(unittest.TestCase): with patch("converter.converter.FasterTTSClient") as mock_faster, \ patch("converter.converter.QwenTTSClient") as mock_qwen, \ 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, + AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_FASTER, voice="narrator") + mock_faster.assert_called_once_with(chunks_dir=converter_mod.CHUNKS_FOLDER, + voice="narrator", api_url=None, quiet=False) mock_qwen.assert_not_called() mock_audiocpp.assert_not_called() @@ -1603,10 +1617,11 @@ class BackendWiringTests(unittest.TestCase): with patch("converter.converter.FasterTTSClient") as mock_faster, \ patch("converter.converter.QwenTTSClient") as mock_qwen, \ patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: - AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, - backend=tts.BACKEND_AUDIOCPP, voice="narrator", + AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_AUDIOCPP, voice="narrator", language="ja") - mock_audiocpp.assert_called_once_with(voice="narrator", language="Japanese", + mock_audiocpp.assert_called_once_with(chunks_dir=converter_mod.CHUNKS_FOLDER, + voice="narrator", language="Japanese", model_id=None, instructions=None, request_options={}, @@ -1616,9 +1631,10 @@ class BackendWiringTests(unittest.TestCase): def test_audiocpp_backend_without_voice_uses_audiocpp_client(self): with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: - AudiobookConverter(voice_mode=tts.VOICE_MODE_CUSTOM, - backend=tts.BACKEND_AUDIOCPP) - mock_audiocpp.assert_called_once_with(voice=None, language=config.LANGUAGE, + AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM, + backend=BACKEND_AUDIOCPP) + mock_audiocpp.assert_called_once_with(chunks_dir=converter_mod.CHUNKS_FOLDER, + voice=None, language=config.LANGUAGE, model_id=None, instructions=None, request_options={}, @@ -1626,22 +1642,24 @@ class BackendWiringTests(unittest.TestCase): def test_audiocpp_backend_model_id_is_wired_through(self): with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: - AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, - backend=tts.BACKEND_AUDIOCPP, voice="narrator", + AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_AUDIOCPP, voice="narrator", model_id="higgs") mock_audiocpp.assert_called_once_with( + chunks_dir=converter_mod.CHUNKS_FOLDER, voice="narrator", language=config.LANGUAGE, model_id="higgs", instructions=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: - AudiobookConverter(voice_mode=tts.VOICE_MODE_CUSTOM, - backend=tts.BACKEND_AUDIOCPP, + AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM, + backend=BACKEND_AUDIOCPP, instructions="A warm adult narrator", request_options={"emotion": "neutral", "speed": "1.1"}) mock_audiocpp.assert_called_once_with( + chunks_dir=converter_mod.CHUNKS_FOLDER, voice=None, language=config.LANGUAGE, model_id=None, instructions="A warm adult narrator", @@ -1652,8 +1670,8 @@ class BackendWiringTests(unittest.TestCase): with patch("converter.converter.FasterTTSClient") as mock_faster, \ patch("converter.converter.QwenTTSClient") as mock_qwen, \ patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: - AudiobookConverter(voice_mode=tts.VOICE_MODE_CUSTOM, - backend=tts.BACKEND_QWEN) + AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM, + backend=BACKEND_QWEN) mock_qwen.assert_called_once() mock_faster.assert_not_called() mock_audiocpp.assert_not_called() @@ -1661,32 +1679,35 @@ class BackendWiringTests(unittest.TestCase): def test_qwen_clone_mode_still_requires_reference(self): with patch("converter.converter.QwenTTSClient"): with self.assertRaises(ValueError): - AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, - backend=tts.BACKEND_QWEN) + AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_QWEN) def test_api_url_override_reaches_each_client(self): # A remote conversion threads api_url through to the selected client. with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp: - AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, - backend=tts.BACKEND_AUDIOCPP, voice="narrator", + AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_AUDIOCPP, voice="narrator", api_url="http://10.0.0.5:8080") mock_audiocpp.assert_called_once_with( + chunks_dir=converter_mod.CHUNKS_FOLDER, voice="narrator", language=config.LANGUAGE, model_id=None, instructions=None, request_options={}, 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", + AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_FASTER, voice="narrator", api_url="http://10.0.0.5:8000") - mock_faster.assert_called_once_with(voice="narrator", + mock_faster.assert_called_once_with(chunks_dir=converter_mod.CHUNKS_FOLDER, + voice="narrator", 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, + AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM, + backend=BACKEND_QWEN, api_url="http://10.0.0.5:7860") mock_qwen.assert_called_once_with( - voice_mode=tts.VOICE_MODE_CUSTOM, voice_clone_ref_audio=None, + chunks_dir=converter_mod.CHUNKS_FOLDER, + voice_mode=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", quiet=False) @@ -1695,8 +1716,8 @@ class BackendWiringTests(unittest.TestCase): # Cloning is server-side for the audiocpp backend, so the # clone-mode voice can be selected without local reference audio. with patch("converter.converter.AudioCppTTSClient"): - converter = AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, - backend=tts.BACKEND_AUDIOCPP, + converter = AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_AUDIOCPP, voice="narrator") self.assertIsNone(converter.voice_clone_ref_audio) @@ -1710,8 +1731,8 @@ class BackendWiringTests(unittest.TestCase): def test_chapter_chunks_qwen_always_splits(self): with patch("converter.converter.QwenTTSClient"): - converter = AudiobookConverter(voice_mode=tts.VOICE_MODE_CUSTOM, - backend=tts.BACKEND_QWEN) + converter = AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM, + backend=BACKEND_QWEN) text = " ".join(f"word{i}" for i in range(50)) with patch.object(config, "CHUNK_SIZE", 10): chunks = converter._chapter_chunks(text) @@ -1720,27 +1741,27 @@ class BackendWiringTests(unittest.TestCase): def test_faster_backend_still_validates_other_settings(self): with patch("converter.converter.FasterTTSClient"): with self.assertRaises(ValueError): - AudiobookConverter(backend=tts.BACKEND_FASTER, speed=0) + AudiobookConverter(backend=BACKEND_FASTER, speed=0) with self.assertRaises(ValueError): - AudiobookConverter(backend=tts.BACKEND_FASTER, language="klingon") + AudiobookConverter(backend=BACKEND_FASTER, language="klingon") def test_audiocpp_backend_still_validates_other_settings(self): with patch("converter.converter.AudioCppTTSClient"): with self.assertRaises(ValueError): - AudiobookConverter(backend=tts.BACKEND_AUDIOCPP, speed=0) + AudiobookConverter(backend=BACKEND_AUDIOCPP, speed=0) with self.assertRaises(ValueError): - AudiobookConverter(backend=tts.BACKEND_AUDIOCPP, language="klingon") + AudiobookConverter(backend=BACKEND_AUDIOCPP, language="klingon") def _faster_converter(self, voice=None): with patch("converter.converter.FasterTTSClient"): - return AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, - backend=tts.BACKEND_FASTER, voice=voice) + return AudiobookConverter(voice_mode=VOICE_MODE_CLONE, + backend=BACKEND_FASTER, voice=voice) def _audiocpp_converter(self, voice=None): with patch("converter.converter.AudioCppTTSClient"): return AudiobookConverter( - voice_mode=tts.VOICE_MODE_CLONE if voice else tts.VOICE_MODE_CUSTOM, - backend=tts.BACKEND_AUDIOCPP, voice=voice) + voice_mode=VOICE_MODE_CLONE if voice else VOICE_MODE_CUSTOM, + backend=BACKEND_AUDIOCPP, voice=voice) def test_narrator_tag_uses_faster_voice_name(self): converter = self._faster_converter(voice="male_richard_poe") @@ -1783,9 +1804,9 @@ class BackendWiringTests(unittest.TestCase): ref = Path(tmp) / "ref.wav" ref.write_bytes(b"x") with patch("converter.converter.QwenTTSClient"): - converter = AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE, + converter = AudiobookConverter(voice_mode=VOICE_MODE_CLONE, voice_clone_ref_audio=str(ref), - backend=tts.BACKEND_QWEN) + backend=BACKEND_QWEN) self.assertEqual(converter._narrator_tag(), "ref") |
