From 5c3df0a434059bd0d541bda35a51e49e3c44dd55 Mon Sep 17 00:00:00 2001 From: historia Date: Thu, 20 Aug 2026 22:58:52 -0400 Subject: feat: experimental support for non-qwen models --- tests/test_tts.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) (limited to 'tests/test_tts.py') diff --git a/tests/test_tts.py b/tests/test_tts.py index ec357c1..e2fe921 100644 --- a/tests/test_tts.py +++ b/tests/test_tts.py @@ -687,6 +687,103 @@ class AudioCppTTSClientHealthTests(unittest.TestCase): self.assertIn("pocket-tts", message) +class AudioCppFamilyDetectionTests(unittest.TestCase): + """Family auto-detection and per-family adaptations.""" + + @staticmethod + def _json_response(payload): + response = MagicMock() + response.__enter__.return_value = response + response.read.return_value = json.dumps(payload).encode("utf-8") + return response + + def _client(self, voice="narrator", models=None): + def _dispatch(request, **_kwargs): + url = request if isinstance(request, str) else request.full_url + if url.endswith("/health"): + return self._json_response({"status": "ok"}) + if url.endswith("/v1/models"): + return self._json_response(models) + if "/v1/audio/voices" in url: + return self._json_response({"voices": [voice] if voice else []}) + raise AssertionError(f"unexpected URL: {url}") + + with patch("converter.tts.urllib.request.urlopen", + side_effect=_dispatch): + return AudioCppTTSClient(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) + + def test_missing_family_falls_back_to_qwen3_tts(self): + client = self._client(models={"data": [ + {"id": config.AUDIOCPP_MODEL_ID}]}) + self.assertEqual(client.family, "qwen3_tts") + self.assertTrue(client.profile.builtin_speakers) + + 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.assertFalse(client.profile.builtin_speakers) + self.assertEqual(client.profile.language_style, tts.AUDIOCPP_LANG_OMIT) + + def test_speaker_mode_rejected_for_clone_only_family(self): + client = None + try: + client = self._client(voice=None, models={"data": [ + {"id": config.AUDIOCPP_MODEL_ID, "family": "voxcpm2"}]}) + except RuntimeError as exc: + message = str(exc) + self.assertIn("voxcpm2", message) + self.assertIn("--voice", message) + self.assertIn("no built-in speakers", message) + self.assertIsNone(client) + + def test_speaker_mode_allowed_for_qwen_family(self): + client = self._client(voice=None, models={"data": [ + {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts"}]}) + self.assertEqual(client.family, "qwen3_tts") + + 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: + client = self._client(models={"data": [ + {"id": "higgs", "family": "higgs_audio_tts"}, + {"id": "qwen-clone", "family": "qwen3_tts"}]}) + self.assertEqual(client.model_id, "higgs") + self.assertTrue(any("different family" in line.lower() or + "hosts family" in line.lower() + for line in logs.output)) + + 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"): + client = self._client(models={"data": [ + {"id": "higgs", "family": "higgs_audio_tts"}]}) + self.assertEqual(client.model_id, "higgs") + + 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: + client = self._client(models={"data": [ + {"id": "qwen3-tts", "family": "qwen3_tts"}, + {"id": "pocket-tts", "family": "pocket_tts"}]}) + self.assertEqual(client.model_id, "qwen3-tts") + 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")) + + class AudioCppTTSClientRequestTests(unittest.TestCase): """The /v1/audio/speech payload and response validation.""" @@ -704,7 +801,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): @staticmethod def _make_client(preset_mode=False, voice="Vivian", language="English", seed=-1, - chunk_text=True): + chunk_text=True, family="qwen3_tts"): client = AudioCppTTSClient.__new__(AudioCppTTSClient) client.api_url = "http://127.0.0.1:8080" client.model_id = config.AUDIOCPP_MODEL_ID @@ -713,6 +810,9 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): client.language = language client._seed = seed client.chunk_text = chunk_text + client.family = family + client.profile = tts.AUDIOCPP_FAMILY_PROFILES.get( + family, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE) return client @staticmethod @@ -795,6 +895,44 @@ class AudioCppTTSClientRequestTests(unittest.TestCase): payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8")) self.assertEqual(payload["instructions"], config.INSTRUCT) + def test_generic_family_omits_language_and_instructions(self): + # 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", + 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")) + self.assertNotIn("language", payload) + self.assertNotIn("instructions", payload) + + 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", + 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")) + self.assertEqual(payload["language"], "ja") + + 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", + 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")) + self.assertNotIn("language", payload) + + 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", + 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")) + self.assertEqual(payload["language"], "Japanese") + 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"): @@ -899,6 +1037,8 @@ class AudioCppTTSClientTruncationTests(unittest.TestCase): client.language = "English" client._seed = -1 client.chunk_text = True + client.family = "qwen3_tts" + client.profile = tts.AUDIOCPP_FAMILY_PROFILES["qwen3_tts"] return client @staticmethod @@ -1073,6 +1213,15 @@ class BackendWiringTests(unittest.TestCase): converter = self._audiocpp_converter() converter._print_banner() + def test_audiocpp_banner_prints_model_family(self): + from contextlib import redirect_stdout + converter = self._audiocpp_converter(voice="narrator") + converter.tts.family = "higgs_audio_tts" + buffer = io.StringIO() + with redirect_stdout(buffer): + converter._print_banner() + self.assertIn("higgs_audio_tts", buffer.getvalue()) + def test_non_faster_narrator_tag_unchanged(self): with tempfile.TemporaryDirectory() as tmp: ref = Path(tmp) / "ref.wav" -- cgit v1.2.3