diff options
| author | historia <historiavg@proton.me> | 2026-08-29 13:57:40 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-29 13:57:40 -0400 |
| commit | df4a81c6101d33fe745b6ac249c736e088760c85 (patch) | |
| tree | 7103e8e2d33a1c2b9afecd47d309147bbeed3c34 /app/tests/test_hub.py | |
| parent | 8a128b3859b8f398e162d3168ff328ab3199d307 (diff) | |
| download | tts-audiobook-generator-df4a81c6101d33fe745b6ac249c736e088760c85.tar.gz | |
fix: crash on bad model_specs from audio.cpp, sanitized
Diffstat (limited to 'app/tests/test_hub.py')
| -rw-r--r-- | app/tests/test_hub.py | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index 39b8c5a..de7e4ea 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -1031,6 +1031,115 @@ class ConvertFlowTests(unittest.TestCase): instr = self._field("instructions") self.assertTrue(instr["visible"](fields)) + def test_audiocpp_model_switch_keeps_the_picked_voice(self): + # Switching models whose voice list is unchanged (two clone + # entries sharing one server's voices) keeps the picked voice + # instead of snapping back to the list's first entry. + self._patch_remote( + [{"id": "alpha", "family": "higgs_audio_tts", "task": "tts"}, + {"id": "beta", "family": "qwen3_tts", "task": "tts"}], + voices=["narrator", "second"]) + self._answer_form(backend="audiocpp-remote", model_id="alpha", + audiocpp_voice="second", instructions="") + self._convert(None, [self._remote("audiocpp", "audio.cpp")]) + fields = self.tui.forms_seen[0][1] + model_field = self._field("model_id") + voice_field = self._field("audiocpp_voice") + # The form opens on the list's first voice; the user picks another. + self.assertEqual(voice_field["value"], "narrator") + voice_field["value"] = "second" + model_field["value"] = "beta" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], "second") + model_field["value"] = "alpha" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], "second") + + def test_audiocpp_local_model_switch_keeps_the_picked_voice(self): + # The managed entry's voice list is shared by every model in + # server.json, so switching models keeps the picked voice. + with tempfile.TemporaryDirectory() as td: + root = Path(td) + (root / "server.json").write_text(json.dumps({ + "models": [{"id": "qwen-1_7b", "family": "qwen3_tts", + "task": "tts"}, + {"id": "qwen-0_6b", "family": "qwen3_tts", + "task": "tts"}], + "voice_dir": str(root), + }), encoding="utf-8") + (root / "Narrator.wav").write_bytes(b"x") + (root / "Second.wav").write_bytes(b"x") + with patch.object(hub.audiocpp_backend, "find_local_checkout", + return_value=root): + self._answer_form(backend="audiocpp", model_id="qwen-1_7b", + audiocpp_voice="Second", instructions="") + self._convert(None, + [self._ready("audiocpp", "audio.cpp")]) + fields = self.tui.forms_seen[0][1] + model_field = self._field("model_id") + voice_field = self._field("audiocpp_voice") + self.assertEqual(voice_field["value"], "Narrator") + voice_field["value"] = "Second" + model_field["value"] = "qwen-0_6b" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], "Second") + + def test_audiocpp_model_switch_resets_when_the_pick_is_gone(self): + # A remote server may host different voices per model: switching + # to a model whose list no longer offers the pick falls back to + # that model's first voice (and re-points again on the way back). + models = patch.object( + hub.audiocpp_backend, "fetch_server_models", + lambda url: [{"id": "alpha", "family": "higgs_audio_tts", + "task": "tts"}, + {"id": "beta", "family": "qwen3_tts", + "task": "tts"}]) + voices = patch.object( + hub.audiocpp_backend, "fetch_server_voices", + lambda url, model_id: {"alpha": ["narrator", "second"], + "beta": ["other"]}[model_id]) + with models, voices: + self._answer_form(backend="audiocpp-remote", model_id="alpha", + audiocpp_voice="second", instructions="") + self._convert(None, [self._remote("audiocpp", "audio.cpp")]) + fields = self.tui.forms_seen[0][1] + model_field = self._field("model_id") + voice_field = self._field("audiocpp_voice") + model_field["value"] = "beta" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], "other") + model_field["value"] = "alpha" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], "narrator") + + def test_audiocpp_model_switch_resets_across_capabilities(self): + # Keep-the-pick only applies within one voice list: a clone pick + # never survives a move to a built-in-speaker entry (and vice + # versa), and a design entry clears the voice again. + self._patch_remote( + [{"id": "clone", "family": "higgs_audio_tts", "task": "tts"}, + {"id": "Qwen3-TTS-CustomVoice-GGUF", "family": "qwen3_tts", + "task": "tts"}, + {"id": "design", "family": "qwen3_tts", "task": "vdes"}], + voices=["narrator", "second"]) + self._answer_form(backend="audiocpp-remote", model_id="clone", + audiocpp_voice="second", instructions="") + self._convert(None, [self._remote("audiocpp", "audio.cpp")]) + fields = self.tui.forms_seen[0][1] + model_field = self._field("model_id") + voice_field = self._field("audiocpp_voice") + voice_field["value"] = "second" + model_field["value"] = "Qwen3-TTS-CustomVoice-GGUF" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], hub.QWEN3_TTS_SPEAKERS[0]) + voice_field["value"] = "Ryan" + model_field["value"] = "clone" + model_field["on_change"](fields) + self.assertEqual(voice_field["value"], "narrator") + model_field["value"] = "design" + model_field["on_change"](fields) + self.assertIsNone(voice_field["value"]) + def test_audiocpp_remote_without_voices_refuses_generate_with_hint(self): # A clone-capable entry (e.g. Qwen Base) whose server lists no # voices at all: the Voice picker stays visible but is empty — |
