diff options
| author | historia <historiavg@proton.me> | 2026-08-25 16:40:30 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-25 16:40:30 -0400 |
| commit | 867866f131b0b6c76c54272791e7f7dea01db990 (patch) | |
| tree | b57ecdd66eeaf7ad73742d2f2bbe15d3a5498fa3 /app/tests/test_hub.py | |
| parent | fca3431721a55277f139efc83df2438207917448 (diff) | |
| download | tts-audiobook-generator-867866f131b0b6c76c54272791e7f7dea01db990.tar.gz | |
feat: better tui menu option gating for models that support custom voices (qwen) and models that do not support instructions
Diffstat (limited to 'app/tests/test_hub.py')
| -rw-r--r-- | app/tests/test_hub.py | 95 |
1 files changed, 74 insertions, 21 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index 9842148..c5ca346 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -750,40 +750,75 @@ class ConvertFlowTests(unittest.TestCase): [("audio.cpp [remote]", "audiocpp-remote")]) # The model menu was fed from the live query (label, id). self.assertEqual(self._field("model_id")["choices"], - [("higgs (higgs_audio_tts, tts)", "higgs")]) + [("higgs (higgs_audio_tts, clone)", "higgs")]) - def test_audiocpp_qwen3_tts_voice_choices_lead_with_builtin_speaker(self): + def test_audiocpp_customvoice_entry_lists_builtin_speakers(self): + # A CustomVoice entry populates the Voice menu with the Qwen3-TTS + # built-in speakers and maps the pick to --speaker. self._patch_remote( - [{"id": "qwen", "family": "qwen3_tts", "task": "tts"}], - voices=["narrator"]) + [{"id": "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF", + "family": "qwen3_tts", "task": "tts"}]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): - self._answer_form(backend="audiocpp-remote", model_id="qwen", - audiocpp_voice="(built-in speaker)", - instructions="") + self._answer_form( + backend="audiocpp-remote", + model_id="Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF", + audiocpp_voice="Ryan", instructions="") cmd = self._convert( None, [self._remote("audiocpp", "audio.cpp")]) - # The sentinel maps to "no voice" (built-in speaker). + # The speaker is passed as --speaker, not --voice. self.assertIsNone(cmd[2]["voice"]) + self.assertEqual(cmd[2]["speaker"], "Ryan") fields = self.tui.forms_seen[0][1] voice_field = self._field("audiocpp_voice") - choices = voice_field["choices"](fields) - self.assertEqual(choices, - [("(built-in speaker)", "(built-in speaker)"), - ("narrator", "narrator")]) - - def test_audiocpp_remote_missing_family_treated_as_qwen3_tts(self): - # Legacy servers omit family/task; the converter defaults them to - # qwen3_tts/tts and so must the form (voice optional). + self.assertEqual(voice_field["choices"](fields), + [(s, s) for s in hub.QWEN3_TTS_SPEAKERS]) + # CustomVoice reads a style instruction, so the field stays visible. + instr = self._field("instructions") + self.assertTrue(instr["visible"](fields)) + self.assertIsNone(instr["validate"]("")) + + def test_audiocpp_qwen3_tts_base_entry_lists_clone_voices(self): + # A Base entry populates the Voice menu with the server's clone + # voices only (no built-in speakers) and maps the pick to --voice. + self._patch_remote( + [{"id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF", + "family": "qwen3_tts", "task": "tts"}], + voices=["narrator"]) + with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): + self._answer_form( + backend="audiocpp-remote", + model_id="Qwen3-TTS-12Hz-1.7B-Base-GGUF", + audiocpp_voice="narrator", instructions="") + cmd = self._convert( + None, [self._remote("audiocpp", "audio.cpp")]) + self.assertEqual(cmd[2]["voice"], "narrator") + self.assertIsNone(cmd[2]["speaker"]) + self.assertIsNone(cmd[2]["instructions"]) + fields = self.tui.forms_seen[0][1] + voice_field = self._field("audiocpp_voice") + self.assertEqual(voice_field["choices"](fields), + [("narrator", "narrator")]) + # Base (clone) ignores instructions, so the field is hidden. + instr = self._field("instructions") + self.assertFalse(instr["visible"](fields)) + + def test_audiocpp_remote_missing_family_is_clone_capable(self): + # A missing family is unknown — not guessed as qwen3_tts — so the + # entry is clone-only: it needs a --voice rather than offering a + # built-in speaker. self._patch_remote([{"id": "legacy", "family": "", "task": ""}], voices=[]) with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): self._answer_form(backend="audiocpp-remote", model_id="legacy", - audiocpp_voice="(built-in speaker)", - instructions="") + audiocpp_voice="", instructions="") cmd = self._convert( None, [self._remote("audiocpp", "audio.cpp")]) self.assertIsNotNone(cmd) self.assertIsNone(cmd[2]["voice"]) + self.assertIsNone(cmd[2]["speaker"]) + voice_field = self._field("audiocpp_voice") + # Clone-only: an empty voice is refused (no built-in speaker option). + self.assertIsNotNone(voice_field["validate"]("")) def test_audiocpp_vdes_hides_voice_and_requires_instructions(self): self._patch_remote( @@ -800,9 +835,24 @@ class ConvertFlowTests(unittest.TestCase): voice_field = self._field("audiocpp_voice") self.assertFalse(voice_field["visible"](fields)) instr = self._field("instructions") + self.assertTrue(instr["visible"](fields)) self.assertIsNotNone(instr["validate"]("")) self.assertIsNone(instr["validate"]("describe me")) + def test_audiocpp_clone_drops_stale_instructions(self): + # A Base/clone entry ignores instructions: even if the form held a + # leftover value, the mapper must not send it to the model. + self._patch_remote( + [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}], + voices=["narrator"]) + with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""): + self._answer_form(backend="audiocpp-remote", model_id="higgs", + audiocpp_voice="narrator", + instructions="stale description") + cmd = self._convert( + None, [self._remote("audiocpp", "audio.cpp")]) + self.assertIsNone(cmd[2]["instructions"]) + def test_audiocpp_required_voice_validates(self): # A non-qwen3_tts family needs a --voice; a blank value refuses. self._patch_remote( @@ -1082,9 +1132,11 @@ class ConvertFlowTests(unittest.TestCase): "mode", "speaker", "clone", "output_format", "speed", "single_file", "debug"]) # The form opens on the configured default (audio.cpp): its fields - # show, the other backend's hide. - for key in ("model_id", "audiocpp_voice", "instructions"): + # show, the other backend's hide. (Instructions is hidden too: the + # default higgs entry is clone-only, which ignores instructions.) + for key in ("model_id", "audiocpp_voice"): self.assertTrue(self._field(key)["visible"](fields)) + self.assertFalse(self._field("instructions")["visible"](fields)) for key in ("mode", "speaker", "clone"): self.assertFalse(self._field(key)["visible"](fields)) # Picking qwen in the Backend field swaps which options show. @@ -1100,8 +1152,9 @@ class ConvertFlowTests(unittest.TestCase): self.assertFalse(self._field(key)["visible"](fields)) # And back to audio.cpp. fields[0]["value"] = "audiocpp" - for key in ("model_id", "audiocpp_voice", "instructions"): + for key in ("model_id", "audiocpp_voice"): self.assertTrue(self._field(key)["visible"](fields)) + self.assertFalse(self._field("instructions")["visible"](fields)) for key in ("mode", "speaker", "clone"): self.assertFalse(self._field(key)["visible"](fields)) |
