aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests/test_hub.py')
-rw-r--r--app/tests/test_hub.py109
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 —