aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-25 16:40:30 -0400
committerhistoria <historiavg@proton.me>2026-08-25 16:40:30 -0400
commit867866f131b0b6c76c54272791e7f7dea01db990 (patch)
treeb57ecdd66eeaf7ad73742d2f2bbe15d3a5498fa3 /app/tests/test_hub.py
parentfca3431721a55277f139efc83df2438207917448 (diff)
downloadtts-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.py95
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))