aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 02:27:10 -0400
committerhistoria <historiavg@proton.me>2026-08-28 02:27:10 -0400
commita7c653313d2bb1e185cfbc3f0f52c2fe33218600 (patch)
treef28f57fa5509ddb54176116cc4cc1198d034e9ff /app/tests/test_hub.py
parent975053f1789771ba5cb9dbe50ba7fe0aa396f0ab (diff)
downloadtts-audiobook-generator-a7c653313d2bb1e185cfbc3f0f52c2fe33218600.tar.gz
feat: qwen-tts backend widgets updated to same style as audio.cpp
Diffstat (limited to 'app/tests/test_hub.py')
-rw-r--r--app/tests/test_hub.py142
1 files changed, 129 insertions, 13 deletions
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index fbede75..f0af74c 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -1581,26 +1581,36 @@ class ConvertFlowTests(unittest.TestCase):
self.assertEqual(speaker_in_memory, "Serena")
fields = self.tui.forms_seen[0][1]
self.assertEqual([f["key"] for f in fields],
- ["backend", "mode", "speaker", "clone",
- "qwen_instructions", "single_file"])
+ ["backend", "mode", "speaker", "clone_dir",
+ "clone", "qwen_instructions", "single_file"])
mode_field = self._field("mode")
+ # Model names are padded to the widest ("CustomVoice"/"VoiceDesign"
+ # are 11 columns) plus a two-space gutter, so every (purpose) opens
+ # on the same column and the picker reads as a two-column table.
self.assertEqual(mode_field["choices"],
- [("CustomVoice (built-in voices)", "custom"),
- ("Base (voice cloning)", "clone"),
- ("VoiceDesign (design)", "design")])
+ [("CustomVoice".ljust(11) + " (built-in voices)",
+ "custom"),
+ ("Base".ljust(11) + " (voice cloning)", "clone"),
+ ("VoiceDesign".ljust(11) + " (design)", "design")])
+ self.assertEqual({label.index("(") for label, _ in
+ mode_field["choices"]}, {13})
speaker_field = self._field("speaker")
+ clone_dir_field = self._field("clone_dir")
clone_field = self._field("clone")
design_field = self._field("qwen_instructions")
- # Speaker shows in custom mode; the .wav path in clone mode and the
- # instruction in design mode.
+ # Speaker shows in custom mode; the .wav directory browser and
+ # picker in clone mode and the instruction in design mode.
self.assertTrue(speaker_field["visible"](fields))
+ self.assertFalse(clone_dir_field["visible"](fields))
self.assertFalse(clone_field["visible"](fields))
self.assertFalse(design_field["visible"](fields))
mode_field["value"] = "clone"
self.assertFalse(speaker_field["visible"](fields))
+ self.assertTrue(clone_dir_field["visible"](fields))
self.assertTrue(clone_field["visible"](fields))
mode_field["value"] = "design"
self.assertFalse(speaker_field["visible"](fields))
+ self.assertFalse(clone_dir_field["visible"](fields))
self.assertFalse(clone_field["visible"](fields))
self.assertTrue(design_field["visible"](fields))
@@ -1649,6 +1659,107 @@ class ConvertFlowTests(unittest.TestCase):
self.assertIsNotNone(cmd)
mk_update.assert_not_called()
+ def test_qwen_form_opens_on_the_configured_model(self):
+ # The persisted QWEN_MODEL seeds the Model picker's default, so the
+ # form opens on what the last run chose (not always the first row).
+ with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
+ patch.object(hub.config, "SPEAKER", "Vivian"), \
+ patch.object(hub.qwen_backend.config, "QWEN_MODEL",
+ "VoiceDesign"), \
+ patch.object(hub.common, "update_config_value"):
+ self._answer_form(backend="qwen", mode="design",
+ qwen_instructions="A warm narrator")
+ self._convert(None, [self._ready("qwen", "qwen-tts")])
+ self.assertEqual(self._field("mode")["value"], "design")
+
+ def test_qwen_clone_dir_defaults_to_the_project_voices(self):
+ # The Clone .wav directory is the shared directory widget, seeded
+ # with the project's ./voices; the picker starts on its first .wav
+ # (alphabetically), ignoring non-.wav files.
+ with tempfile.TemporaryDirectory() as td:
+ root = Path(td)
+ (root / "narrator.wav").write_bytes(b"")
+ (root / "alice.wav").write_bytes(b"")
+ (root / "notes.txt").write_text("", encoding="utf-8")
+ expected_choices = [
+ ("alice.wav", str(root / "alice.wav")),
+ ("narrator.wav", str(root / "narrator.wav")),
+ ]
+ with patch.object(hub.common, "VOICES_DIR", root), \
+ patch.object(hub.qwen_backend, "QWEN_SPEAKERS",
+ ["Vivian"]), \
+ patch.object(hub.config, "SPEAKER", "Vivian"), \
+ patch.object(hub.common, "update_config_value"):
+ self._answer_form(backend="qwen", mode="custom",
+ speaker="Vivian")
+ cmd = self._convert(None,
+ [self._ready("qwen", "qwen-tts")])
+ fields = self.tui.forms_seen[0][1]
+ clone_dir_field = self._field("clone_dir")
+ clone_field = self._field("clone")
+ self.assertIsNone(cmd[2]["clone"]) # custom mode: no clone
+ self.assertEqual(clone_dir_field["kind"], "dir")
+ self.assertEqual(clone_dir_field["value"], root)
+ self.assertEqual(clone_field["kind"], "choice")
+ self.assertEqual(clone_field["choices"](fields),
+ expected_choices)
+ self.assertEqual(clone_field["value"],
+ expected_choices[0][1])
+
+ def test_qwen_clone_picker_resets_when_the_directory_changes(self):
+ # Changing the directory browser re-points the picker at the new
+ # directory's first .wav; an empty directory clears the pick.
+ with tempfile.TemporaryDirectory() as td, \
+ tempfile.TemporaryDirectory() as other, \
+ tempfile.TemporaryDirectory() as nowhere:
+ root, other = Path(td), Path(other)
+ (root / "one.wav").write_bytes(b"")
+ (other / "beta.wav").write_bytes(b"")
+ (other / "alpha.wav").write_bytes(b"")
+ with patch.object(hub.common, "VOICES_DIR", root), \
+ patch.object(hub.qwen_backend, "QWEN_SPEAKERS",
+ ["Vivian"]), \
+ patch.object(hub.config, "SPEAKER", "Vivian"), \
+ patch.object(hub.common, "update_config_value"):
+ self._answer_form(backend="qwen", mode="clone", clone="")
+ self._convert(None, [self._ready("qwen", "qwen-tts")])
+ fields = self.tui.forms_seen[0][1]
+ clone_dir_field = self._field("clone_dir")
+ clone_field = self._field("clone")
+ # The picker seeds itself with the default directory's first
+ # .wav, and follows the directory browser from there.
+ self.assertEqual(clone_field["value"], str(root / "one.wav"))
+ clone_dir_field["value"] = other
+ clone_dir_field["on_change"](fields)
+ self.assertEqual(clone_field["value"], str(other / "alpha.wav"))
+ clone_dir_field["value"] = Path(nowhere) / "no-such-dir"
+ clone_dir_field["on_change"](fields)
+ self.assertEqual(clone_field["value"], "")
+
+ def test_qwen_clone_picker_refuses_generate_without_wavs(self):
+ # No .wav files in the directory: the picker stays empty, opening
+ # it flashes the hint, and Generate! is refused with the same
+ # message naming the directory.
+ with tempfile.TemporaryDirectory() as td:
+ empty = Path(td)
+ with patch.object(hub.common, "VOICES_DIR", empty), \
+ patch.object(hub.qwen_backend, "QWEN_SPEAKERS",
+ ["Vivian"]), \
+ patch.object(hub.config, "SPEAKER", "Vivian"), \
+ patch.object(hub.common, "update_config_value"):
+ self._answer_form(backend="qwen", mode="clone", clone="")
+ cmd = self._convert(None,
+ [self._ready("qwen", "qwen-tts")])
+ self.assertIsNone(cmd[2]["clone"])
+ fields = self.tui.forms_seen[0][1]
+ clone_field = self._field("clone")
+ self.assertEqual(clone_field["choices"](fields), [])
+ message = clone_field["on_empty_choices"](fields)
+ self.assertIn(str(empty), message)
+ self.assertIn("No .wav files", message)
+ self.assertEqual(clone_field["validate"](""), message)
+ self.assertIsNone(clone_field["validate"](str(empty / "x.wav")))
+
# ------------------------------------------------------------------
# faster: remote server (no local voices.json)
# ------------------------------------------------------------------
@@ -1741,7 +1852,7 @@ class ConvertFlowTests(unittest.TestCase):
self.assertEqual(cmd[2]["clone"], "/tmp/ref.wav")
self.assertEqual(cmd[2]["api_url"], "http://10.0.0.5:7861")
self.assertEqual(self._field("mode")["choices"],
- [("Base (voice cloning)", "clone")])
+ [("Base".ljust(11) + " (voice cloning)", "clone")])
# ------------------------------------------------------------------
# multiple backends: the Backend picker gates which options show
@@ -1773,8 +1884,8 @@ class ConvertFlowTests(unittest.TestCase):
self.assertEqual(
[f["key"] for f in fields],
["backend", "model_id", "audiocpp_voice", "instructions",
- "request_options", "mode", "speaker", "clone",
- "qwen_instructions", "single_file"])
+ "request_options", "mode", "speaker", "clone_dir",
+ "clone", "qwen_instructions", "single_file"])
# The form opens on the configured default (audio.cpp): its fields
# show, the other backend's hide. Instructions shows too (optional
# style/delivery control even on the clone-only higgs entry), while
@@ -1783,16 +1894,20 @@ class ConvertFlowTests(unittest.TestCase):
for key in ("model_id", "audiocpp_voice", "instructions"):
self.assertTrue(self._field(key)["visible"](fields))
self.assertFalse(self._field("request_options")["visible"](fields))
- for key in ("mode", "speaker", "clone", "qwen_instructions"):
+ for key in ("mode", "speaker", "clone_dir", "clone",
+ "qwen_instructions"):
self.assertFalse(self._field(key)["visible"](fields))
# Picking qwen in the Backend field swaps which options show.
fields[0]["value"] = "qwen"
self.assertTrue(self._field("mode")["visible"](fields))
self.assertTrue(self._field("speaker")["visible"](fields))
+ self.assertFalse(self._field("clone_dir")["visible"](fields))
self.assertFalse(self._field("clone")["visible"](fields))
- # qwen's clone mode hides the speaker and shows the .wav path.
+ # qwen's clone mode hides the speaker and shows the .wav directory
+ # browser and the picker of the .wavs inside it.
self._field("mode")["value"] = "clone"
self.assertFalse(self._field("speaker")["visible"](fields))
+ self.assertTrue(self._field("clone_dir")["visible"](fields))
self.assertTrue(self._field("clone")["visible"](fields))
for key in ("model_id", "audiocpp_voice", "instructions",
"request_options"):
@@ -1801,7 +1916,8 @@ class ConvertFlowTests(unittest.TestCase):
fields[0]["value"] = "audiocpp"
for key in ("model_id", "audiocpp_voice"):
self.assertTrue(self._field(key)["visible"](fields))
- for key in ("mode", "speaker", "clone", "qwen_instructions"):
+ for key in ("mode", "speaker", "clone_dir", "clone",
+ "qwen_instructions"):
self.assertFalse(self._field(key)["visible"](fields))