aboutsummaryrefslogtreecommitdiff
path: root/tests/test_tts.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-20 22:58:52 -0400
committerhistoria <historiavg@proton.me>2026-08-20 22:58:52 -0400
commit5c3df0a434059bd0d541bda35a51e49e3c44dd55 (patch)
tree1d18e5f41ed9fc1184275a2a2b1a6555dffa4dc4 /tests/test_tts.py
parent0c197324f5444b448c285d2a57bd0a5834c2fc84 (diff)
downloadtts-audiobook-generator-5c3df0a434059bd0d541bda35a51e49e3c44dd55.tar.gz
feat: experimental support for non-qwen models
Diffstat (limited to 'tests/test_tts.py')
-rw-r--r--tests/test_tts.py151
1 files changed, 150 insertions, 1 deletions
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"