diff options
| author | historia <historiavg@proton.me> | 2026-08-31 14:08:36 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-31 14:08:36 -0400 |
| commit | 3b109185d642319c2c1870815b25ae1c0ad49445 (patch) | |
| tree | 475bdac0f98de68d99b1c8ce251c74a092d507cb /app/tests | |
| parent | a0e3050c6e1e43df3941077afa4ade9a1c4d6ce4 (diff) | |
| download | tts-audiobook-generator-3b109185d642319c2c1870815b25ae1c0ad49445.tar.gz | |
feat: model capabilities lined up in tui menu
Diffstat (limited to 'app/tests')
| -rw-r--r-- | app/tests/test_hub.py | 111 | ||||
| -rw-r--r-- | app/tests/test_tts.py | 8 |
2 files changed, 94 insertions, 25 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py index 6ce1943..a822f1a 100644 --- a/app/tests/test_hub.py +++ b/app/tests/test_hub.py @@ -827,11 +827,11 @@ class ConvertFlowTests(unittest.TestCase): # checkout is downloaded by setup): seed the client's spec cache # with the classifications these tests rely on, so they stay # hermetic. Unknown families keep the clone-only default. - spec_cache = audiocpp_client._FAMILY_SPEC_TASKS + spec_cache = audiocpp_client._FAMILY_SPECS spec_cache.clear() spec_cache.update({ - "higgs_audio_tts": {"tts", "clone"}, - "supertonic": {"tts"}, + "higgs_audio_tts": {"tasks": ["tts", "clone"]}, + "supertonic": {"tasks": ["tts"]}, }) self.addCleanup(spec_cache.clear) @@ -965,14 +965,15 @@ class ConvertFlowTests(unittest.TestCase): self.assertEqual(fields[0]["choices"], [("audio.cpp [remote]", "audiocpp-remote")]) # The model menu was fed from the live query (label, id); ids are - # padded so the type column lines up across entries. A mixed - # tts+clone family reads as "(tts/clone)". + # padded so the capability columns line up across entries. A mixed + # tts+clone family reads as the "tts" and "clone" columns. self.assertEqual(self._field("model_id")["choices"], - [("higgs (tts/clone)", "higgs")]) + [("higgs tts clone", "higgs")]) def test_model_menu_lines_the_type_column_up(self): - # Ids are padded to the widest id: every (type) starts on the same - # column, so the picker reads as a two-column table. + # Ids are padded to the widest id, and every capability word sits + # in its own fixed column (tts | clone | design): the picker reads + # as a table where "tts", "clone" (and "design") line up. self._patch_remote( [{"id": "short", "family": "higgs_audio_tts", "task": "tts"}, {"id": "a-much-longer-model-id", "family": "qwen3_tts", @@ -983,17 +984,84 @@ class ConvertFlowTests(unittest.TestCase): None, [self._remote("audiocpp", "audio.cpp")]) self.assertIsNotNone(cmd) choices = self._field("model_id")["choices"] - # "a-much-longer-model-id" is 22 columns wide; both types open at - # column 24 ("(" right after the two-space gutter). + # "a-much-longer-model-id" is 22 columns wide; both capabilities + # start at the same offsets: "tts" at 24, "clone" at 29. The + # clone-only qwen3_tts entry leaves the tts column blank. self.assertEqual(choices[0], - ("short".ljust(22) + " (tts/clone)", "short")) + ("short".ljust(22) + " tts clone", "short")) self.assertEqual(choices[1], - ("a-much-longer-model-id (clone)", - "a-much-longer-model-id")) - self.assertEqual({label.index("(") for label, _ in choices}, {24}) + ("a-much-longer-model-id".ljust(22) + + " clone", "a-much-longer-model-id")) + self.assertEqual({label.index("clone") for label, _ in choices}, + {29}) + self.assertEqual({label.index("tts") for label, _ in choices + if "tts" in label}, {24}) # A plain qwen3_tts entry (no CustomVoice in the id) is clone-only. self.assertEqual(choices[1][1], "a-much-longer-model-id") - self.assertTrue(choices[1][0].endswith("(clone)")) + self.assertTrue(choices[1][0].endswith("clone")) + self.assertNotIn("design", choices[1][0]) + + def test_model_menu_shows_design_for_design_capable_families(self): + # A family whose spec advertises a design task designs on the + # regular entry from the Instructions text: the model menu grows + # a "design" column entry behind tts/clone. + spec_cache = audiocpp_client._FAMILY_SPECS + spec_cache["omnivoice"] = {"tasks": ["tts", "clone", "design"]} + self.addCleanup(spec_cache.pop, "omnivoice", None) + self._patch_remote( + [{"id": "omnivoice", "family": "omnivoice", "task": "tts"}], + voices=["narrator"]) + self._answer_form(backend="audiocpp-remote", model_id="omnivoice", + audiocpp_voice="", instructions="") + cmd = self._convert( + None, [self._remote("audiocpp", "audio.cpp")]) + self.assertIsNotNone(cmd) + self.assertEqual(self._field("model_id")["choices"], + [("omnivoice tts clone design", "omnivoice")]) + + def test_model_menu_shows_design_only_on_vdes_entries(self): + # A task-"vdes" entry is a design model: its row shows only the + # design column (no tts, no clone), aligned with other rows. + self._patch_remote( + [{"id": "moss_voicegen", "family": "moss_voicegen", + "task": "vdes"}]) + self._answer_form(backend="audiocpp-remote", model_id="moss_voicegen", + audiocpp_voice=None, instructions="a warm narrator") + cmd = self._convert( + None, [self._remote("audiocpp", "audio.cpp")]) + self.assertIsNotNone(cmd) + self.assertEqual( + self._field("model_id")["choices"], + [("moss_voicegen design", "moss_voicegen")]) + + def test_model_menu_keeps_design_off_qwen3_tts_nondesign_entries(self): + # Qwen3-TTS designs only through its separate VoiceDesign entry: + # the Base row stays "clone" while the vdes row shows "design", + # both words on the same columns. + self._patch_remote( + [{"id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF", "family": "qwen3_tts", + "task": "tts"}, + {"id": "Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF", + "family": "qwen3_tts", "task": "vdes"}]) + self._answer_form( + backend="audiocpp-remote", model_id="Qwen3-TTS-12Hz-1.7B-Base-GGUF", + audiocpp_voice="", instructions="") + cmd = self._convert( + None, [self._remote("audiocpp", "audio.cpp")]) + self.assertIsNotNone(cmd) + choices = self._field("model_id")["choices"] + base = "Qwen3-TTS-12Hz-1.7B-Base-GGUF" + design = "Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF" + labels = {value: label for label, value in choices} + self.assertEqual(labels, + {base: base.ljust(len(design)) + " clone", + design: design.ljust(len(design)) + + " design"}) + # "clone" and "design" each sit on one shared column. + self.assertEqual({label.index("clone") for label, _ in choices + if "clone" in label}, + {label.index("design") - 7 + for label, _ in choices if "design" in label}) def test_audiocpp_customvoice_entry_lists_builtin_speakers(self): # A CustomVoice entry populates the Voice menu with the Qwen3-TTS @@ -1270,7 +1338,7 @@ class ConvertFlowTests(unittest.TestCase): voice_field["value"] = "narrator" model_field["value"] = "higgs" model_field["on_change"](fields) - # Mixed family: the blank (built-in) pick is valid. + # Mixed family: the blank <built-in> (no clone) pick is valid. self.assertIsNone(voice_field["validate"]("")) def test_audiocpp_builtin_speaker_entry_labels_the_field_built_in(self): @@ -1346,7 +1414,7 @@ class ConvertFlowTests(unittest.TestCase): def test_audiocpp_pure_tts_entry_hides_the_voice_menu(self): # Pure-TTS families (spec tasks without "clone") synthesize with # no voice at all: the Voice menu is hidden entirely, the model - # menu reads "(tts)", and Generate! sends no voice. + # menu reads "tts", and Generate! sends no voice. self._patch_remote( [{"id": "supertonic", "family": "supertonic", "task": "tts"}]) self._answer_form(backend="audiocpp-remote", model_id="supertonic", @@ -1359,12 +1427,12 @@ class ConvertFlowTests(unittest.TestCase): voice_field = self._field("audiocpp_voice") self.assertFalse(voice_field["visible"](fields)) self.assertEqual(self._field("model_id")["choices"], - [("supertonic (tts)", "supertonic")]) + [("supertonic tts", "supertonic")]) def test_audiocpp_mixed_family_offers_a_built_in_blank_pick(self): # Mixed tts+clone families lead the Voice menu with a blank - # "(built-in)" pick meaning plain TTS (no reference voice), and - # the blank pick is the default. + # "<built-in> (no clone)" pick meaning plain TTS (the model's own + # default voice, no reference cloned), and it is the default. self._patch_remote( [{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}], voices=["narrator"]) @@ -1378,7 +1446,8 @@ class ConvertFlowTests(unittest.TestCase): voice_field = self._field("audiocpp_voice") self.assertTrue(voice_field["visible"](fields)) self.assertEqual(voice_field["choices"](fields), - [("", "(built-in)"), ("narrator", "narrator")]) + [("<built-in> (no clone)", ""), + ("narrator", "narrator")]) # A kept clone pick survives a mixed-family switch; blank is valid. voice_field["value"] = "narrator" self.assertIsNone(voice_field["validate"]("narrator")) diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py index 9067443..67c81f8 100644 --- a/app/tests/test_tts.py +++ b/app/tests/test_tts.py @@ -1135,12 +1135,12 @@ class AudioCppFamilyVoicePolicyTests(unittest.TestCase): def setUp(self): # Seed the spec cache instead of reading the (gitignored, setup- # downloaded) checkout's model_specs, so the tests are hermetic. - cache = audiocpp_client._FAMILY_SPEC_TASKS + cache = audiocpp_client._FAMILY_SPECS cache.clear() cache.update({ - "higgs_audio_tts": {"tts", "clone"}, - "supertonic": {"tts"}, - "confucius4_tts": {"clone"}, + "higgs_audio_tts": {"tasks": ["tts", "clone"]}, + "supertonic": {"tasks": ["tts"]}, + "confucius4_tts": {"tasks": ["clone"]}, }) self.addCleanup(cache.clear) |
