aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-31 14:08:36 -0400
committerhistoria <historiavg@proton.me>2026-08-31 14:08:36 -0400
commit3b109185d642319c2c1870815b25ae1c0ad49445 (patch)
tree475bdac0f98de68d99b1c8ce251c74a092d507cb /app/tests/test_hub.py
parenta0e3050c6e1e43df3941077afa4ade9a1c4d6ce4 (diff)
downloadtts-audiobook-generator-3b109185d642319c2c1870815b25ae1c0ad49445.tar.gz
feat: model capabilities lined up in tui menu
Diffstat (limited to 'app/tests/test_hub.py')
-rw-r--r--app/tests/test_hub.py111
1 files changed, 90 insertions, 21 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"))