aboutsummaryrefslogtreecommitdiff
path: root/app/tests
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests')
-rw-r--r--app/tests/test_converter.py14
-rw-r--r--app/tests/test_hub.py95
-rw-r--r--app/tests/test_tts.py177
-rw-r--r--app/tests/test_tui.py11
4 files changed, 250 insertions, 47 deletions
diff --git a/app/tests/test_converter.py b/app/tests/test_converter.py
index 9b0ddb4..317e772 100644
--- a/app/tests/test_converter.py
+++ b/app/tests/test_converter.py
@@ -131,6 +131,7 @@ class NarratorTagTests(unittest.TestCase):
converter.voice_clone_ref_audio = ref_audio
converter.backend = tts.BACKEND_QWEN
converter.voice = None
+ converter.speaker = None
converter.instructions = instructions
return converter
@@ -159,11 +160,12 @@ class NarratorTagTests(unittest.TestCase):
self.assertEqual(self._converter(tts.VOICE_MODE_CLONE, "/x/???.wav")._narrator_tag(),
"narrator")
- def _audiocpp_converter(self, voice=None, instructions=None):
+ def _audiocpp_converter(self, voice=None, instructions=None, speaker=None):
converter = self._converter(tts.VOICE_MODE_CUSTOM,
instructions=instructions)
converter.backend = tts.BACKEND_AUDIOCPP
converter.voice = voice
+ converter.speaker = speaker
return converter
def test_audiocpp_design_run_uses_designed_tag(self):
@@ -181,6 +183,15 @@ class NarratorTagTests(unittest.TestCase):
converter = self._audiocpp_converter()
self.assertEqual(converter._narrator_tag(), "Vivian")
+ def test_audiocpp_explicit_speaker_uses_speaker_tag(self):
+ # A chosen CustomVoice speaker names the output, not config.SPEAKER.
+ converter = self._audiocpp_converter(speaker="Ryan")
+ self.assertEqual(converter._narrator_tag(), "Ryan")
+
+ def test_audiocpp_explicit_speaker_normalizes_display_name(self):
+ converter = self._audiocpp_converter(speaker="Uncle_Fu")
+ self.assertEqual(converter._narrator_tag(), "Uncle_Fu")
+
def test_preflight_design_run_uses_designed_tag(self):
with tempfile.TemporaryDirectory() as books_tmp, \
tempfile.TemporaryDirectory() as output_tmp:
@@ -547,6 +558,7 @@ class RunOverwritePromptTests(unittest.TestCase):
self.converter.voice_clone_ref_audio = None
self.converter.backend = tts.BACKEND_QWEN
self.converter.voice = None
+ self.converter.speaker = None
self.converter.instructions = None
self.converter.speed = 1.0
self.converter.single_file = False
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))
diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py
index b43919d..0e35f78 100644
--- a/app/tests/test_tts.py
+++ b/app/tests/test_tts.py
@@ -479,8 +479,10 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
def setUp(self):
# The default AUDIOCPP_MODEL_ID is empty (auto-select); these tests
- # exercise a configured single-model server, so pin a concrete id.
- patcher = patch.object(config, "AUDIOCPP_MODEL_ID", "qwen")
+ # exercise a configured single-model CustomVoice server, so pin a
+ # concrete id whose "customvoice" substring marks it speaker-capable.
+ patcher = patch.object(
+ config, "AUDIOCPP_MODEL_ID", "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF")
patcher.start()
self.addCleanup(patcher.stop)
@@ -500,7 +502,8 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
else {"status": "ok"})
if url.endswith("/v1/models"):
return self._json_response(models if models is not None else
- {"data": [{"id": config.AUDIOCPP_MODEL_ID}]})
+ {"data": [{"id": config.AUDIOCPP_MODEL_ID,
+ "family": "qwen3_tts"}]})
if "/v1/audio/voices" in url:
if voices is Exception:
raise Exception("voices endpoint down")
@@ -509,11 +512,12 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
raise AssertionError(f"unexpected URL: {url}")
return _dispatch
- def _client(self, voice=None, language=None, model_id=None, **kwargs):
+ def _client(self, voice=None, language=None, model_id=None, speaker=None,
+ **kwargs):
with patch("converter.tts.urllib.request.urlopen",
side_effect=self._get_responses(**kwargs)):
return AudioCppTTSClient(voice=voice, language=language,
- model_id=model_id)
+ model_id=model_id, speaker=speaker)
def test_unreachable_server_raises_with_readme_pointer(self):
import urllib.error
@@ -551,6 +555,42 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
client = self._client()
self.assertEqual(client.voice, "Uncle Fu")
+ def test_explicit_speaker_selects_speaker_mode(self):
+ # --speaker picks a CustomVoice speaker; the name is normalized to
+ # its wire (display) form and no preset validation runs.
+ client = self._client(speaker="Uncle_Fu")
+ self.assertEqual(client.voice, "Uncle Fu")
+ self.assertFalse(client.preset_mode)
+
+ def test_explicit_speaker_on_clone_entry_raises(self):
+ # --speaker is meaningless on a clone-only (Base) entry.
+ with self.assertRaises(RuntimeError) as ctx:
+ self._client(speaker="Ryan", model_id="Qwen3-TTS-12Hz-1.7B-Base-GGUF",
+ models={"data": [
+ {"id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF",
+ "family": "qwen3_tts"}]})
+ message = str(ctx.exception)
+ self.assertIn("no built-in speakers", message)
+ self.assertIn("--voice", message)
+
+ def test_voice_and_speaker_are_mutually_exclusive(self):
+ with self.assertRaises(ValueError) as ctx:
+ AudioCppTTSClient(voice="narrator", speaker="Ryan")
+ self.assertIn("mutually exclusive", str(ctx.exception))
+
+ def test_no_voice_on_base_entry_raises_instead_of_silent_speaker(self):
+ # The Base model has no built-in speakers: without --voice the run
+ # fails fast instead of silently sending a speaker name that the
+ # model ignores.
+ with self.assertRaises(RuntimeError) as ctx:
+ self._client(model_id="Qwen3-TTS-12Hz-1.7B-Base-GGUF",
+ models={"data": [
+ {"id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF",
+ "family": "qwen3_tts"}]})
+ message = str(ctx.exception)
+ self.assertIn("Base-GGUF", message)
+ self.assertIn("--voice", message)
+
def test_preset_mode_uses_requested_voice(self):
client = self._client(voice="narrator")
self.assertEqual(client.voice, "narrator")
@@ -598,7 +638,8 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
self.assertLogs("converter.tts", level="WARNING") as logs:
client = self._client(
voice="narrator",
- models={"data": [{"id": "qwen3-tts"}, {"id": "pocket-tts"}]})
+ models={"data": [{"id": "qwen3-tts", "family": "qwen3_tts"},
+ {"id": "pocket-tts"}]})
self.assertEqual(client.model_id, "qwen3-tts")
self.assertTrue(any("qwen3-tts-clone" in line for line in logs.output))
@@ -632,11 +673,17 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
self.assertEqual(client.model_id, "higgs")
def test_clone_model_id_ignored_for_speaker_mode(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
+ # Speaker mode (no --voice on a CustomVoice entry) never reroutes to
+ # AUDIOCPP_CLONE_MODEL_ID — that reroute is a preset-mode concern.
+ with patch.object(config, "AUDIOCPP_MODEL_ID",
+ "Qwen3-TTS-CustomVoice"), \
patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"):
client = self._client(
- models={"data": [{"id": "qwen3-tts"}, {"id": "qwen3-tts-clone"}]})
- self.assertEqual(client.model_id, "qwen3-tts")
+ models={"data": [{"id": "Qwen3-TTS-CustomVoice",
+ "family": "qwen3_tts"},
+ {"id": "qwen3-tts-clone",
+ "family": "qwen3_tts"}]})
+ self.assertEqual(client.model_id, "Qwen3-TTS-CustomVoice")
def test_clone_model_id_equal_to_primary_is_noop(self):
with patch.object(config, "AUDIOCPP_CLONE_MODEL_ID",
@@ -662,9 +709,12 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
self.assertIn("--voice", message)
def test_preset_mode_with_no_matching_model_lists_both_ids(self):
+ # Neither the primary nor the clone id is on the server, so the
+ # family is unknown and no degradation warning is logged — the
+ # requirement error lists both configured ids instead.
with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"), \
- self.assertLogs("converter.tts", level="WARNING"):
+ self.assertNoLogs("converter.tts", level="WARNING"):
with self.assertRaises(RuntimeError) as ctx:
self._client(voice="narrator",
models={"data": [{"id": "pocket-tts"}]})
@@ -678,7 +728,10 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
"""Task auto-detection (tts/clon/vdes) and voice design validation."""
def setUp(self):
- patcher = patch.object(config, "AUDIOCPP_MODEL_ID", "qwen")
+ # Pin a CustomVoice id so the default (no-voice) path is speaker
+ # mode; individual tests override family/task to exercise other paths.
+ patcher = patch.object(
+ config, "AUDIOCPP_MODEL_ID", "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF")
patcher.start()
self.addCleanup(patcher.stop)
@@ -853,18 +906,19 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
self.assertEqual(client.family, "higgs_audio_tts")
self.assertIs(client.profile, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE)
- def test_missing_family_falls_back_to_qwen3_tts(self):
+ def test_missing_family_uses_generic_profile(self):
+ # A missing family is unknown (not guessed as qwen3_tts): it falls
+ # through to the generic clone-only profile.
client = self._client(models={"data": [
{"id": config.AUDIOCPP_MODEL_ID}]})
- self.assertEqual(client.family, "qwen3_tts")
- self.assertTrue(client.profile.builtin_speakers)
+ self.assertEqual(client.family, "")
+ self.assertIs(client.profile, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE)
def test_unknown_family_uses_generic_profile(self):
client = self._client(models={"data": [
{"id": config.AUDIOCPP_MODEL_ID, "family": "future_tts"}]})
self.assertEqual(client.family, "future_tts")
self.assertIs(client.profile, tts.AUDIOCPP_DEFAULT_FAMILY_PROFILE)
- self.assertFalse(client.profile.builtin_speakers)
self.assertEqual(client.profile.language_style, tts.AUDIOCPP_LANG_OMIT)
def test_speaker_mode_rejected_for_clone_only_family(self):
@@ -879,11 +933,31 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
self.assertIn("no built-in speakers", message)
self.assertIsNone(client)
- def test_speaker_mode_allowed_for_qwen_family(self):
- client = self._client(voice=None, models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts"}]})
+ def test_speaker_mode_allowed_for_customvoice_entry(self):
+ # A Qwen3-TTS entry whose id names CustomVoice is speaker-capable;
+ # no --voice is needed.
+ with patch.object(config, "AUDIOCPP_MODEL_ID",
+ "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF"):
+ client = self._client(voice=None, models={"data": [
+ {"id": "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF",
+ "family": "qwen3_tts"}]})
self.assertEqual(client.family, "qwen3_tts")
+ def test_speaker_mode_rejected_for_qwen_base_entry(self):
+ # A Qwen3-TTS entry whose id names Base (not CustomVoice) is
+ # clone-only, even though its family has built-in speakers on other
+ # entries: without --voice it fails fast.
+ client = None
+ try:
+ client = self._client(voice=None, models={"data": [
+ {"id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF",
+ "family": "qwen3_tts"}]})
+ except RuntimeError as exc:
+ message = str(exc)
+ self.assertIn("Base-GGUF", message)
+ self.assertIn("--voice", message)
+ self.assertIsNone(client)
+
def test_clone_model_id_of_different_family_is_ignored(self):
with patch.object(config, "AUDIOCPP_MODEL_ID", "higgs"), \
patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen-clone"), \
@@ -919,6 +993,52 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
self.assertIsNone(tts.LANGUAGE_ISO_CODES.get("Auto"))
+class AudiocppEntryVoiceCapabilityTests(unittest.TestCase):
+ """The per-entry voice capability resolver (speaker/clone/design)."""
+
+ def _cap(self, family="", task="tts", model_id=""):
+ return tts.audiocpp_entry_voice_capability(family, task, model_id)
+
+ def test_vdes_task_is_design(self):
+ self.assertEqual(self._cap("qwen3_tts", "vdes",
+ "Qwen3-TTS-VoiceDesign-GGUF"),
+ tts.AUDIOCPP_VOICE_DESIGN)
+
+ def test_qwen_customvoice_entry_is_speaker(self):
+ self.assertEqual(self._cap("qwen3_tts", "tts",
+ "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF"),
+ tts.AUDIOCPP_VOICE_SPEAKER)
+
+ def test_qwen_base_entry_is_clone(self):
+ self.assertEqual(self._cap("qwen3_tts", "tts",
+ "Qwen3-TTS-12Hz-1.7B-Base-GGUF"),
+ tts.AUDIOCPP_VOICE_CLONE)
+
+ def test_qwen_unidentified_entry_is_clone(self):
+ self.assertEqual(self._cap("qwen3_tts", "tts", "qwen"),
+ tts.AUDIOCPP_VOICE_CLONE)
+
+ def test_other_families_are_clone(self):
+ self.assertEqual(self._cap("higgs_audio_tts", "tts", "higgs"),
+ tts.AUDIOCPP_VOICE_CLONE)
+
+ def test_missing_family_is_clone(self):
+ self.assertEqual(self._cap("", "tts", "legacy"),
+ tts.AUDIOCPP_VOICE_CLONE)
+
+ def test_customvoice_match_is_case_insensitive(self):
+ self.assertEqual(self._cap("qwen3_tts", "tts",
+ "Qwen3-TTS-12Hz-1.7B-CUSTOMVOICE-GGUF"),
+ tts.AUDIOCPP_VOICE_SPEAKER)
+
+ def test_customvoice_id_in_other_family_is_not_speaker(self):
+ # The "customvoice" substring only marks a speaker for the qwen3_tts
+ # family; another family with a lookalike id stays clone-only.
+ self.assertEqual(self._cap("future_tts", "tts",
+ "Qwen3-TTS-CustomVoice"),
+ tts.AUDIOCPP_VOICE_CLONE)
+
+
class AudioCppTTSClientRequestTests(unittest.TestCase):
"""The /v1/audio/speech payload and response validation."""
@@ -943,6 +1063,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase):
client.model_id = config.AUDIOCPP_MODEL_ID
client.preset_mode = preset_mode
client.voice = voice
+ client.speaker = None
client.language = language
client._seed = seed
client.family = family
@@ -953,10 +1074,12 @@ class AudioCppTTSClientRequestTests(unittest.TestCase):
client.request_options = dict(request_options or {})
client.design_mode = task == tts.AUDIOCPP_TASK_VDES
# Mirrors the connect-time rule: an instruction-defined voice on a
- # family without built-in speakers (design mode takes precedence).
+ # clone-capable entry with no --voice (design mode takes precedence).
+ capability = tts.audiocpp_entry_voice_capability(
+ family, task, client.model_id)
client.instruction_voice = (
not preset_mode and not client.design_mode
- and not client.profile.builtin_speakers
+ and capability == tts.AUDIOCPP_VOICE_CLONE
and bool(client.instructions))
return client
@@ -1396,6 +1519,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase):
client.design_mode = False
client.instruction_voice = False
client.instructions = ""
+ client.speaker = None
with patch.object(client, "_check_health"), \
patch.object(client, "_list_models",
return_value=[{"id": client.model_id,
@@ -1425,6 +1549,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase):
client.design_mode = False
client.instruction_voice = False
client.instructions = ""
+ client.speaker = None
with patch.object(client, "_check_health"), \
patch.object(client, "_list_models",
return_value=[{"id": client.model_id,
@@ -1466,7 +1591,8 @@ class BackendWiringTests(unittest.TestCase):
model_id=None,
instructions=None,
request_options={},
- api_url=None)
+ api_url=None,
+ speaker=None)
mock_faster.assert_not_called()
mock_qwen.assert_not_called()
@@ -1478,7 +1604,8 @@ class BackendWiringTests(unittest.TestCase):
model_id=None,
instructions=None,
request_options={},
- api_url=None)
+ api_url=None,
+ speaker=None)
def test_audiocpp_backend_model_id_is_wired_through(self):
with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp:
@@ -1488,7 +1615,7 @@ class BackendWiringTests(unittest.TestCase):
mock_audiocpp.assert_called_once_with(
voice="narrator", language=config.LANGUAGE,
model_id="higgs", instructions=None,
- request_options={}, api_url=None)
+ request_options={}, api_url=None, speaker=None)
def test_audiocpp_backend_instructions_and_options_are_wired_through(self):
with patch("converter.converter.AudioCppTTSClient") as mock_audiocpp:
@@ -1502,7 +1629,7 @@ class BackendWiringTests(unittest.TestCase):
model_id=None,
instructions="A warm adult narrator",
request_options={"emotion": "neutral", "speed": "1.1"},
- api_url=None)
+ api_url=None, speaker=None)
def test_qwen_backend_uses_qwen_client(self):
with patch("converter.converter.FasterTTSClient") as mock_faster, \
@@ -1529,7 +1656,7 @@ class BackendWiringTests(unittest.TestCase):
mock_audiocpp.assert_called_once_with(
voice="narrator", language=config.LANGUAGE, model_id=None,
instructions=None, request_options={},
- api_url="http://10.0.0.5:8080")
+ api_url="http://10.0.0.5:8080", speaker=None)
with patch("converter.converter.FasterTTSClient") as mock_faster:
AudiobookConverter(voice_mode=tts.VOICE_MODE_CLONE,
backend=tts.BACKEND_FASTER, voice="narrator",
diff --git a/app/tests/test_tui.py b/app/tests/test_tui.py
index 964f983..543194e 100644
--- a/app/tests/test_tui.py
+++ b/app/tests/test_tui.py
@@ -204,6 +204,17 @@ class MenuTests(TuiTestCase):
self.assertGreater(help_x, margin)
self.assert_inside_border(screen)
+ def test_blank_line_below_the_title(self):
+ # A titled dialog reserves a blank line between the title row
+ # and the first body row.
+ screen = FakeScreen(keys=[10])
+ tui.menu(screen, "Pick", self.OPTIONS)
+ title_y = next(y for y, _, text, _ in screen.strings
+ if text == " Pick ")
+ first_y = next(y for y, _, text, _ in screen.strings
+ if text == "first option")
+ self.assertEqual(first_y, title_y + 2)
+
def test_up_wraps_around_to_last_option(self):
screen = FakeScreen(keys=[FakeCurses.KEY_UP, 10])
value = tui.menu(screen, "Pick one", self.OPTIONS)