aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_tts.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-26 21:22:40 -0400
committerhistoria <historiavg@proton.me>2026-08-26 21:22:40 -0400
commit477ac3e827e3bdc9f14583fc3aa8db1fa2d27c52 (patch)
tree1e21fa5af1d7a95ffc62fb03eed153056c38ae9e /app/tests/test_tts.py
parent65c6f737f1545ef225768af897acd20f163a4fb4 (diff)
downloadtts-audiobook-generator-477ac3e827e3bdc9f14583fc3aa8db1fa2d27c52.tar.gz
feat: design model support for qwen-tts backend. remove unnecessary port split for qwen models
Diffstat (limited to 'app/tests/test_tts.py')
-rw-r--r--app/tests/test_tts.py102
1 files changed, 100 insertions, 2 deletions
diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py
index 2b2ac1c..49eb6f6 100644
--- a/app/tests/test_tts.py
+++ b/app/tests/test_tts.py
@@ -31,6 +31,8 @@ from converter.clients import (
TTS_LANGUAGES,
VOICE_MODE_CLONE,
VOICE_MODE_CUSTOM,
+ VOICE_MODE_DESIGN,
+ VOICE_MODES,
AudioCppTTSClient,
FasterTTSClient,
QwenTTSClient,
@@ -494,6 +496,80 @@ class QwenTTSClientGenerateTests(unittest.TestCase):
mock_generate.assert_not_called()
+class QwenTTSClientVoiceDesignTests(unittest.TestCase):
+ """Qwen VoiceDesign mode: instructions and the /run_voice_design call."""
+
+ def setUp(self):
+ self._tmp = tempfile.TemporaryDirectory()
+
+ def tearDown(self):
+ self._tmp.cleanup()
+
+ def _client(self, instructions=None):
+ client = QwenTTSClient.__new__(QwenTTSClient)
+ client.chunks_dir = Path(self._tmp.name)
+ client.voice_mode = VOICE_MODE_DESIGN
+ client.language = config.LANGUAGE
+ client.instructions = (instructions if instructions is not None
+ else config.INSTRUCT).strip()
+ client.api_info = {"named_endpoints": {"/run_voice_design": {
+ "parameters": [
+ {"parameter_name": "text"},
+ {"parameter_name": "lang_disp"},
+ {"parameter_name": "design"},
+ ]}}}
+ return client
+
+ def _fake_output(self) -> str:
+ out = Path(self._tmp.name) / "server_out.wav"
+ out.write_bytes(b"\x01\x00")
+ return str(out)
+
+ def test_voice_mode_design_is_valid(self):
+ self.assertIn(VOICE_MODE_DESIGN, VOICE_MODES)
+
+ def test_generate_payload_and_return(self):
+ client = self._client(instructions="A warm narrator")
+ fake = MagicMock(return_value=(self._fake_output(),))
+ with patch.object(client, "_generate_voice_design", fake):
+ result = client._generate_sub_request(
+ "Hello there.", self._tmp.name, 1, 1, 1)
+ fake.assert_called_once_with("Hello there.")
+ self.assertEqual(Path(result).name, "part_01.wav")
+
+ def test_payload_uses_design_field_language_and_instruction(self):
+ client = self._client(instructions="A warm narrator")
+ captured = {}
+
+ def fake_predict(**payload):
+ captured.update(payload)
+ return (self._fake_output(),)
+
+ client.client = MagicMock()
+ client.client.predict.side_effect = fake_predict
+ result = client._generate_voice_design("Hi.")
+ self.assertEqual(captured["text"], "Hi.")
+ self.assertEqual(captured["lang_disp"], config.LANGUAGE)
+ self.assertEqual(captured["design"], "A warm narrator")
+ self.assertNotIn("seed", captured) # not accepted by this endpoint
+ self.assertEqual(result, (self._fake_output(),))
+
+ def test_payload_defaults_instructions_to_config(self):
+ client = self._client(instructions=None)
+ self.assertEqual(client.instructions,
+ (config.INSTRUCT or "").strip())
+
+ def test_unknown_api_falls_back_to_the_requested_name(self):
+ client = self._client()
+ client.api_info = {"named_endpoints": {}}
+ client.client = MagicMock()
+ client.client.predict.return_value = (self._fake_output(),)
+ client._generate_voice_design("Hi.")
+ _, kwargs = client.client.predict.call_args
+ self.assertEqual(kwargs["api_name"], "/run_voice_design")
+
+
+
class AudioCppTTSClientHealthTests(unittest.TestCase):
"""Connection behavior of the audio.cpp client."""
@@ -1682,6 +1758,28 @@ class BackendWiringTests(unittest.TestCase):
AudiobookConverter(voice_mode=VOICE_MODE_CLONE,
backend=BACKEND_QWEN)
+ def test_qwen_design_mode_without_instructions_rejected(self):
+ # A VoiceDesign run needs a description; an empty instructions
+ # value (not even the config default) is refused up front.
+ with patch("converter.converter.QwenTTSClient"):
+ with self.assertRaises(ValueError):
+ AudiobookConverter(voice_mode=VOICE_MODE_DESIGN,
+ backend=BACKEND_QWEN, instructions=" ")
+
+ def test_qwen_design_mode_threads_instructions_to_the_client(self):
+ with patch("converter.converter.QwenTTSClient") as mock_qwen:
+ AudiobookConverter(voice_mode=VOICE_MODE_DESIGN,
+ backend=BACKEND_QWEN,
+ instructions="A warm adult female narrator")
+ _, kwargs = mock_qwen.call_args
+ self.assertEqual(kwargs["instructions"],
+ "A warm adult female narrator")
+
+ def test_qwen_design_narrator_tag_uses_designed(self):
+ self.assertEqual(AudiobookConverter.compute_narrator_tag(
+ BACKEND_QWEN, None, VOICE_MODE_DESIGN, None,
+ "A warm adult female narrator"), "designed")
+
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:
@@ -1709,8 +1807,8 @@ class BackendWiringTests(unittest.TestCase):
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)
+ language=config.LANGUAGE, instructions=None,
+ 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