aboutsummaryrefslogtreecommitdiff
path: root/app/tests
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests')
-rw-r--r--app/tests/test_audiobook_cli.py74
-rw-r--r--app/tests/test_backends.py40
-rw-r--r--app/tests/test_backends_audiocpp.py98
-rw-r--r--app/tests/test_converter.py57
-rw-r--r--app/tests/test_converter_progress.py1
-rw-r--r--app/tests/test_hub.py323
-rw-r--r--app/tests/test_tts.py372
7 files changed, 428 insertions, 537 deletions
diff --git a/app/tests/test_audiobook_cli.py b/app/tests/test_audiobook_cli.py
index b06a715..d947c5d 100644
--- a/app/tests/test_audiobook_cli.py
+++ b/app/tests/test_audiobook_cli.py
@@ -43,13 +43,17 @@ class MainTestCase(unittest.TestCase):
self.tmp = Path(tempfile.mkdtemp(prefix="audiobook_cli_"))
self.addCleanup(shutil.rmtree, self.tmp, True)
- def run_main(self, argv):
+ def run_main(self, argv, backend="audiocpp"):
"""Run main() with the given argv; returns (code, stderr, convert mock).
The envs bootstrap (which re-execs into the managed venv via
os.execv when active) and convert() are stubbed, so no TTS work
- happens and the process survives.
+ happens and the process survives. BACKEND (a --backend value, or
+ None to omit the required flag) is prepended unless the argv
+ already carries --backend.
"""
+ if backend is not None and "--backend" not in argv:
+ argv = ["--backend", backend, *argv]
err = io.StringIO()
convert = MagicMock(return_value=0)
with patch.object(sys, "argv", ["audiobook.py", *argv]), \
@@ -64,6 +68,71 @@ class MainTestCase(unittest.TestCase):
return code, err.getvalue(), convert
+class MainBackendTests(MainTestCase):
+ """The backend/model/voice are per-run choices with no config defaults."""
+
+ def test_backend_is_required(self):
+ code, err, convert = self.run_main(["--debug"], backend=None)
+ self.assertEqual(code, 2)
+ self.assertIn("--backend", err)
+ self.assertIn("required", err)
+ convert.assert_not_called()
+
+ def test_backend_reaches_convert(self):
+ code, _, convert = self.run_main([])
+ self.assertEqual(code, 0)
+ self.assertEqual(convert.call_args.kwargs["backend"], "audiocpp")
+
+ def test_qwen_accepts_a_builtin_speaker_voice(self):
+ code, _, convert = self.run_main(
+ ["--backend", "qwen", "--voice", "Vivian"], backend=None)
+ self.assertEqual(code, 0)
+ self.assertEqual(convert.call_args.kwargs["backend"], "qwen")
+ self.assertEqual(convert.call_args.kwargs["voice"], "Vivian")
+
+ def test_qwen_rejects_a_non_speaker_voice(self):
+ code, err, convert = self.run_main(
+ ["--backend", "qwen", "--voice", "narrator"], backend=None)
+ self.assertEqual(code, 2)
+ self.assertIn("not a built-in speaker", err)
+ convert.assert_not_called()
+
+ def test_qwen_requires_a_voice_without_clone_or_instructions(self):
+ code, err, convert = self.run_main(["--backend", "qwen"], backend=None)
+ self.assertEqual(code, 2)
+ self.assertIn("--backend qwen needs a voice", err)
+ convert.assert_not_called()
+
+ def test_qwen_clone_run_needs_no_voice(self):
+ code, _, convert = self.run_main(
+ ["--backend", "qwen", "--clone", "ref.wav"], backend=None)
+ self.assertEqual(code, 0)
+ convert.assert_called_once()
+
+ def test_faster_requires_a_voice(self):
+ code, err, convert = self.run_main(["--backend", "faster"],
+ backend=None)
+ self.assertEqual(code, 2)
+ self.assertIn("--backend faster requires --voice", err)
+ convert.assert_not_called()
+
+ def test_noninteractive_no_args_stops_with_guidance(self):
+ # No args in a non-interactive session cannot guess a backend:
+ # point the user at --backend / the TUI instead of converting.
+ out, err = io.StringIO(), io.StringIO()
+ with patch.object(sys, "argv", ["audiobook.py"]), \
+ patch.object(sys, "stdin", io.StringIO()), \
+ patch.object(sys, "stdout", io.StringIO()), \
+ contextlib.redirect_stdout(out), \
+ contextlib.redirect_stderr(err), \
+ patch.object(audiobook._envs, "bootstrap"):
+ with self.assertRaises(SystemExit) as ctx:
+ audiobook.main()
+ self.assertEqual(ctx.exception.code, 2)
+ self.assertIn("No --backend given", out.getvalue())
+ self.assertIn("No --backend given", err.getvalue() + out.getvalue())
+
+
class MainFlagConflictTests(MainTestCase):
"""Mixing the directory and single-book flag pairs stops with an error."""
@@ -200,6 +269,7 @@ class ConvertWiringTests(unittest.TestCase):
self._old_folders
def _convert(self, **kwargs):
+ kwargs.setdefault("backend", "audiocpp")
preflight = MagicMock(
return_value=([self.book], [(self.book, "dune")]))
fake_instance = MagicMock()
diff --git a/app/tests/test_backends.py b/app/tests/test_backends.py
index 09ad3cc..64fd483 100644
--- a/app/tests/test_backends.py
+++ b/app/tests/test_backends.py
@@ -145,31 +145,33 @@ class DetectAllTests(unittest.TestCase):
self.assertEqual(status.remote_models, [model])
self.assertEqual(status.running_models, [model])
- def test_qwen_detect_uses_one_spec_for_the_configured_model(self):
- # One demo server hosts one model on the single port: the spec's
- # argv launches config.QWEN_MODEL's repo, and its identity matches.
+ def test_qwen_detect_builds_one_spec_for_the_default_model(self):
+ # One demo server hosts one model on the single port: the detect()
+ # spec launches the default model's repo (CustomVoice), and its
+ # identity matches; runs wanting another model boot their own spec.
from backends import qwen
from backends.probe import (IDENTITY_QWEN_CLONE,
IDENTITY_QWEN_CUSTOM,
IDENTITY_QWEN_DESIGN)
- cases = {"CustomVoice": IDENTITY_QWEN_CUSTOM,
- "Base": IDENTITY_QWEN_CLONE,
- "VoiceDesign": IDENTITY_QWEN_DESIGN}
- for model, identity in cases.items():
+ identity = IDENTITY_QWEN_CUSTOM
+ model = qwen.DEFAULT_MODEL
+ with patch.object(qwen, "_is_installed", return_value=True), \
+ patch("backends.common.server_running",
+ return_value=False):
+ status = qwen.detect()
+ self.assertEqual([spec.name for spec in status.servers], ["qwen"])
+ spec = status.servers[0]
+ self.assertEqual(spec.identity, identity)
+ self.assertIn(qwen.MODEL_REPOS[model], spec.argv)
+ self.assertIn(qwen.MODEL_REPOS[model], status.launch_hint)
+
+ # An explicit per-run spec can target any of the three models.
+ for model, wanted in (("Base", IDENTITY_QWEN_CLONE),
+ ("VoiceDesign", IDENTITY_QWEN_DESIGN)):
with self.subTest(model=model):
- with patch.object(qwen.config, "QWEN_MODEL", model), \
- patch.object(qwen, "_is_installed",
- return_value=True), \
- patch("backends.common.server_running",
- return_value=False):
- status = qwen.detect()
- self.assertEqual([spec.name for spec in status.servers],
- ["qwen"])
- spec = status.servers[0]
- self.assertEqual(spec.identity, identity)
+ spec = qwen._build_spec(model)
+ self.assertEqual(spec.identity, wanted)
self.assertIn(qwen.MODEL_REPOS[model], spec.argv)
- self.assertIn(qwen.MODEL_REPOS[model],
- status.launch_hint)
def test_qwen_detect_marks_our_server_as_managed(self):
from backends import qwen
diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py
index 95f8bec..82d6b88 100644
--- a/app/tests/test_backends_audiocpp.py
+++ b/app/tests/test_backends_audiocpp.py
@@ -27,14 +27,6 @@ FAKE_CONFIG = (
"CHUNK_SIZE = 250\n"
)
-FAKE_CONFIG_WITH_MODEL_IDS = (
- 'AUDIOCPP_API_URL = "http://127.0.0.1:9999" # audio.cpp audiocpp_server\n'
- "\n"
- 'AUDIOCPP_MODEL_ID = "qwen" # server entry for speaker mode\n'
- 'AUDIOCPP_CLONE_MODEL_ID = "qwen-clone"\n'
-)
-
-
def _write_spec(checkout: Path, family: str, *, display_name=None,
tasks=("tts", "clone"), languages=("en",), packages=None,
category="tts"):
@@ -278,59 +270,6 @@ class UpdateConfigPortTests(unittest.TestCase):
8080, config_path=Path(self._tmp.name) / "nope.py"))
-class UpdateConfigModelIdsTests(unittest.TestCase):
- def setUp(self):
- self._tmp = tempfile.TemporaryDirectory()
- self.config_path = Path(self._tmp.name) / "config.py"
- self.config_path.write_text(FAKE_CONFIG_WITH_MODEL_IDS,
- encoding="utf-8")
- # The shared helper also mirrors values onto converter.config.
- self._saved_ids = (config.AUDIOCPP_MODEL_ID,
- config.AUDIOCPP_CLONE_MODEL_ID)
-
- def tearDown(self):
- (config.AUDIOCPP_MODEL_ID,
- config.AUDIOCPP_CLONE_MODEL_ID) = self._saved_ids
- self._tmp.cleanup()
-
- def test_rewrites_both_ids_preserving_lines(self):
- changed = make_server.configsync.update_config_model_ids(
- "higgs", "higgs", config_path=self.config_path)
- self.assertTrue(changed)
- text = self.config_path.read_text(encoding="utf-8")
- self.assertIn('AUDIOCPP_MODEL_ID = "higgs" # server entry for speaker mode',
- text)
- self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "higgs"', text)
- self.assertIn('AUDIOCPP_API_URL = "http://127.0.0.1:9999"', text)
-
- def test_clone_id_optional(self):
- changed = make_server.configsync.update_config_model_ids(
- "voxcpm2", config_path=self.config_path)
- self.assertTrue(changed)
- text = self.config_path.read_text(encoding="utf-8")
- self.assertIn('AUDIOCPP_MODEL_ID = "voxcpm2"', text)
- self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "qwen-clone"', text)
-
- def test_ids_unchanged_is_a_success_noop(self):
- # Both ids already hold their values: success, nothing rewritten.
- changed = make_server.configsync.update_config_model_ids(
- "qwen", "qwen-clone", config_path=self.config_path)
- self.assertTrue(changed)
- self.assertEqual(self.config_path.read_text(encoding="utf-8"),
- FAKE_CONFIG_WITH_MODEL_IDS)
-
- def test_returns_false_when_lines_missing(self):
- path = Path(self._tmp.name) / "other.py"
- path.write_text('CHUNK_SIZE = 250\n', encoding="utf-8")
- self.assertFalse(make_server.configsync.update_config_model_ids(
- "higgs", "higgs", config_path=path))
-
- def test_returns_false_when_file_missing(self):
- self.assertFalse(make_server.configsync.update_config_model_ids(
- "higgs", "higgs",
- config_path=Path(self._tmp.name) / "nope.py"))
-
-
class ResolveWavDirArgTests(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
@@ -1934,7 +1873,7 @@ class NonInteractiveMainTests(unittest.TestCase):
def test_default_run_hosts_recommended_entry(self):
exit_code = self._run(
- self._args("--families", "higgs_audio_tts", "--no-sync-model-ids"))
+ self._args("--families", "higgs_audio_tts"))
self.assertEqual(exit_code, 0)
data = json.loads(self.output.read_text(encoding="utf-8"))
self.assertEqual(data["host"], "127.0.0.1")
@@ -1952,8 +1891,7 @@ class NonInteractiveMainTests(unittest.TestCase):
with patch.object(config, "AUDIOCPP_API_URL",
"http://127.0.0.1:9999"):
exit_code = self._run(
- self._args("--families", "higgs_audio_tts",
- "--no-sync-model-ids"))
+ self._args("--families", "higgs_audio_tts"))
self.assertEqual(exit_code, 0)
self.assertIn('"http://127.0.0.1:9999"',
self.fake_config.read_text(encoding="utf-8"))
@@ -1968,21 +1906,10 @@ class NonInteractiveMainTests(unittest.TestCase):
with self.assertRaises(SystemExit):
parser.parse_args([flag, "x"])
- def test_model_id_sync_accepted_updates_config(self):
- self.fake_config.write_text(FAKE_CONFIG_WITH_MODEL_IDS,
- encoding="utf-8")
- exit_code = self._run(self._args("--families", "higgs_audio_tts"))
- self.assertEqual(exit_code, 0)
- text = self.fake_config.read_text(encoding="utf-8")
- self.assertIn('AUDIOCPP_MODEL_ID = "Higgs-Audio-v3-TTS-4B-GGUF"', text)
- self.assertIn('AUDIOCPP_CLONE_MODEL_ID = "Higgs-Audio-v3-TTS-4B-GGUF"',
- text)
-
def test_multi_family_lazy_with_voice_dir(self):
(self.folder / "narrator.wav").write_bytes(b"x")
exit_code = self._run(
- self._args("--families", "qwen3_tts,higgs_audio_tts",
- "--no-sync-model-ids"),
+ self._args("--families", "qwen3_tts,higgs_audio_tts"),
transcribe=lambda path, model_name="base": "a transcript")
self.assertEqual(exit_code, 0)
data = json.loads(self.output.read_text(encoding="utf-8"))
@@ -1998,8 +1925,7 @@ class NonInteractiveMainTests(unittest.TestCase):
def test_force_overwrites_existing_output(self):
self.output.write_text('{"old": true}', encoding="utf-8")
exit_code = self._run(
- self._args("--families", "higgs_audio_tts", "--force",
- "--no-sync-model-ids"))
+ self._args("--families", "higgs_audio_tts", "--force"))
self.assertEqual(exit_code, 0)
data = json.loads(self.output.read_text(encoding="utf-8"))
self.assertEqual(len(data["models"]), 1)
@@ -2007,15 +1933,14 @@ class NonInteractiveMainTests(unittest.TestCase):
def test_existing_output_declined_keeps_file(self):
self.output.write_text('{"old": true}', encoding="utf-8")
exit_code = self._run(
- self._args("--families", "higgs_audio_tts", "--no-sync-model-ids"))
+ self._args("--families", "higgs_audio_tts"))
self.assertEqual(exit_code, 1)
self.assertEqual(json.loads(self.output.read_text(encoding="utf-8")),
{"old": True})
def test_all_packages_hosts_design_as_vdes(self):
exit_code = self._run(
- self._args("--families", "qwen3_tts", "--all-packages",
- "--no-sync-model-ids"))
+ self._args("--families", "qwen3_tts", "--all-packages"))
self.assertEqual(exit_code, 0)
data = json.loads(self.output.read_text(encoding="utf-8"))
by_id = {m["id"]: m for m in data["models"]}
@@ -2029,14 +1954,13 @@ class NonInteractiveMainTests(unittest.TestCase):
def test_unknown_family_rejected(self):
with self.assertRaises(SystemExit) as ctx:
- self._run(self._args("--families", "not_a_family",
- "--no-sync-model-ids"))
+ self._run(self._args("--families", "not_a_family"))
self.assertEqual(ctx.exception.code, 2)
def test_missing_checkout_rejected(self):
with self.assertRaises(SystemExit) as ctx:
self._run(["--families", "higgs_audio_tts", "--output",
- str(self.output), "--no-sync-model-ids"],
+ str(self.output)],
no_checkout=True)
self.assertEqual(ctx.exception.code, 2)
@@ -2044,12 +1968,12 @@ class NonInteractiveMainTests(unittest.TestCase):
missing = self.root / "nope"
with self.assertRaises(SystemExit) as ctx:
self._run(["--wavs", str(missing), "--output", str(self.output),
- "--families", "higgs_audio_tts", "--no-sync-model-ids"])
+ "--families", "higgs_audio_tts"])
self.assertEqual(ctx.exception.code, 2)
def test_families_required_in_noninteractive_run(self):
with self.assertRaises(SystemExit) as ctx:
- self._run(self._args("--no-sync-model-ids"))
+ self._run(self._args())
self.assertEqual(ctx.exception.code, 2)
@@ -2666,7 +2590,6 @@ class WizardNavigationTests(unittest.TestCase):
self.assertEqual(settings["backend"], "cuda") # default choice
self.assertTrue(settings["build"]) # not built yet → offered (default Yes)
self.assertFalse(settings["download"]) # no manager script here
- self.assertTrue(settings["sync_model_ids"])
def test_tree_screen_starts_on_confirm(self):
# The model-tree screen opens with focus on Confirm so Enter
@@ -2895,7 +2818,6 @@ class ExecuteLanesTests(unittest.TestCase):
"wav_dir": None,
"plan": None,
"sync_port": None,
- "sync_model_ids": None,
"delete_unused": False,
"unused_entries": [],
"model_entries": [],
diff --git a/app/tests/test_converter.py b/app/tests/test_converter.py
index 4d7a064..eaf96ef 100644
--- a/app/tests/test_converter.py
+++ b/app/tests/test_converter.py
@@ -64,20 +64,29 @@ class ConfigurationValidationTests(unittest.TestCase):
def test_language_defaults_to_config(self):
with patch("converter.converter.QwenTTSClient") as mock_tts:
- AudiobookConverter(backend=BACKEND_QWEN)
+ AudiobookConverter(backend=BACKEND_QWEN, voice="Vivian")
self.assertEqual(mock_tts.call_args.kwargs["language"], config.LANGUAGE)
def test_output_format_defaults_to_config(self):
with patch("converter.converter.QwenTTSClient"):
- converter = AudiobookConverter(backend=BACKEND_QWEN)
+ converter = AudiobookConverter(backend=BACKEND_QWEN,
+ voice="Vivian")
self.assertEqual(converter.output_format, config.AUDIO_FORMAT)
def test_language_normalized_before_tts_client(self):
with patch("converter.converter.QwenTTSClient") as mock_tts:
- converter = AudiobookConverter(language="ja", backend=BACKEND_QWEN)
+ converter = AudiobookConverter(language="ja", backend=BACKEND_QWEN,
+ voice="Vivian")
self.assertEqual(converter.language, "Japanese")
self.assertEqual(mock_tts.call_args.kwargs["language"], "Japanese")
+ def test_qwen_custom_voice_requires_a_speaker(self):
+ # There is no configured default speaker: a qwen built-in-speaker
+ # run must be told which one to use.
+ with self.assertRaises(ValueError) as ctx:
+ AudiobookConverter(backend=BACKEND_QWEN)
+ self.assertIn("requires a speaker", str(ctx.exception))
+
class FindExistingOutputsTests(unittest.TestCase):
def setUp(self):
@@ -133,23 +142,23 @@ class FindExistingOutputsTests(unittest.TestCase):
class NarratorTagTests(unittest.TestCase):
- def _converter(self, voice_mode, ref_audio=None, instructions=None):
+ def _converter(self, voice_mode, ref_audio=None, instructions=None,
+ voice=None):
converter = AudiobookConverter.__new__(AudiobookConverter)
converter.voice_mode = voice_mode
converter.voice_clone_ref_audio = ref_audio
converter.backend = BACKEND_QWEN
- converter.voice = None
+ converter.voice = voice
converter.instructions = instructions
return converter
def test_custom_voice_uses_speaker_display_name(self):
- self.assertEqual(self._converter(VOICE_MODE_CUSTOM)._narrator_tag(),
- "Vivian")
+ converter = self._converter(VOICE_MODE_CUSTOM, voice="Vivian")
+ self.assertEqual(converter._narrator_tag(), "Vivian")
def test_multi_word_display_name_gets_underscores(self):
- with patch.object(config, "SPEAKER", "uncle_fu"):
- self.assertEqual(self._converter(VOICE_MODE_CUSTOM)._narrator_tag(),
- "Uncle_Fu")
+ converter = self._converter(VOICE_MODE_CUSTOM, voice="uncle_fu")
+ self.assertEqual(converter._narrator_tag(), "Uncle_Fu")
def test_clone_uses_reference_audio_stem(self):
self.assertEqual(self._converter(VOICE_MODE_CLONE, "/x/ref.wav")._narrator_tag(),
@@ -186,14 +195,21 @@ class NarratorTagTests(unittest.TestCase):
self.assertEqual(converter._narrator_tag(), "narrator")
def test_audiocpp_speaker_mode_keeps_speaker_tag(self):
- converter = self._audiocpp_converter()
+ converter = self._audiocpp_converter(voice="Vivian")
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.
+ # A chosen CustomVoice speaker names the output.
converter = self._audiocpp_converter(voice="Ryan")
self.assertEqual(converter._narrator_tag(), "Ryan")
+ def test_audiocpp_without_voice_or_instruction_uses_fallback_tag(self):
+ # A run like this fails at connect time (the client refuses a
+ # speaker-capable entry without --voice); the pre-flight still
+ # needs a stable tag for it.
+ converter = self._audiocpp_converter()
+ self.assertEqual(converter._narrator_tag(), "narrator")
+
def test_audiocpp_explicit_speaker_normalizes_display_name(self):
converter = self._audiocpp_converter(voice="Uncle_Fu")
self.assertEqual(converter._narrator_tag(), "Uncle_Fu")
@@ -332,8 +348,11 @@ class DebugDumpTests(unittest.TestCase):
def test_debug_flag_wiring(self):
with patch("converter.converter.QwenTTSClient"):
- self.assertFalse(AudiobookConverter(backend=BACKEND_QWEN).debug)
- self.assertTrue(AudiobookConverter(debug=True, backend=BACKEND_QWEN).debug)
+ self.assertFalse(AudiobookConverter(backend=BACKEND_QWEN,
+ voice="Vivian").debug)
+ self.assertTrue(AudiobookConverter(debug=True,
+ backend=BACKEND_QWEN,
+ voice="Vivian").debug)
class SetupLoggingTests(unittest.TestCase):
@@ -549,14 +568,14 @@ class PreflightOverwritesTests(unittest.TestCase):
(converter_mod.BOOKS_FOLDER / "book.txt").unlink()
with patch("builtins.input", side_effect=AssertionError("should not prompt")):
book_files, planned = AudiobookConverter.preflight_overwrites(
- BACKEND_QWEN, None, VOICE_MODE_CUSTOM, None, "mp3")
+ BACKEND_QWEN, "Vivian", VOICE_MODE_CUSTOM, None, "mp3")
self.assertEqual(book_files, [])
self.assertEqual(planned, [])
def test_new_book_planned_without_prompt(self):
with patch("builtins.input", side_effect=AssertionError("should not prompt")):
book_files, planned = AudiobookConverter.preflight_overwrites(
- BACKEND_QWEN, None, VOICE_MODE_CUSTOM, None, "mp3")
+ BACKEND_QWEN, "Vivian", VOICE_MODE_CUSTOM, None, "mp3")
self.assertEqual(len(book_files), 1)
self.assertEqual(planned, [(book_files[0], "book_Vivian")])
@@ -564,14 +583,14 @@ class PreflightOverwritesTests(unittest.TestCase):
(converter_mod.AUDIOBOOKS_FOLDER / "book_Vivian.mp3").write_bytes(b"existing")
with patch("builtins.input", return_value=""):
book_files, planned = AudiobookConverter.preflight_overwrites(
- BACKEND_QWEN, None, VOICE_MODE_CUSTOM, None, "mp3")
+ BACKEND_QWEN, "Vivian", VOICE_MODE_CUSTOM, None, "mp3")
self.assertEqual(planned, [(book_files[0], "book_Vivian")])
def test_existing_output_declined_is_skipped(self):
(converter_mod.AUDIOBOOKS_FOLDER / "book_Vivian.mp3").write_bytes(b"existing")
with patch("builtins.input", return_value="n"):
book_files, planned = AudiobookConverter.preflight_overwrites(
- BACKEND_QWEN, None, VOICE_MODE_CUSTOM, None, "mp3")
+ BACKEND_QWEN, "Vivian", VOICE_MODE_CUSTOM, None, "mp3")
self.assertEqual(len(book_files), 1)
self.assertEqual(planned, [])
@@ -590,7 +609,7 @@ class RunOverwritePromptTests(unittest.TestCase):
self.converter.voice_mode = VOICE_MODE_CUSTOM
self.converter.voice_clone_ref_audio = None
self.converter.backend = BACKEND_QWEN
- self.converter.voice = None
+ self.converter.voice = "Vivian"
self.converter.instructions = None
self.converter.speed = 1.0
self.converter.single_file = False
diff --git a/app/tests/test_converter_progress.py b/app/tests/test_converter_progress.py
index 9a1147c..d77e9e0 100644
--- a/app/tests/test_converter_progress.py
+++ b/app/tests/test_converter_progress.py
@@ -116,6 +116,7 @@ class _ConvertFixture:
return_value=MagicMock()):
converter = AudiobookConverter(
voice_mode=VOICE_MODE_CUSTOM, backend=BACKEND_QWEN,
+ voice="Vivian",
output_format="mp3", language="English",
progress=progress, cancel=cancel)
converter.tts.process_chunk_with_retry.return_value = "chunk_0001.wav"
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index ee1425d..86dd9e3 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -916,8 +916,7 @@ class ConvertFlowTests(unittest.TestCase):
patch.object(hub.config, "AUDIO_FORMAT", "m4b"), \
patch.object(hub.config, "LANGUAGE", "English"), \
patch.object(hub.config, "SPEED", 1.25), \
- patch.object(hub.config, "DEBUG", False), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ patch.object(hub.config, "DEBUG", False):
self._answer_form(backend="audiocpp-remote", model_id="higgs",
audiocpp_voice="narrator", instructions="")
cmd = self._convert(
@@ -964,11 +963,10 @@ class ConvertFlowTests(unittest.TestCase):
[{"id": "short", "family": "higgs_audio_tts", "task": "tts"},
{"id": "a-much-longer-model-id", "family": "qwen3_tts",
"task": "tts"}])
- with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
- self._answer_form(backend="audiocpp-remote", model_id="short",
- audiocpp_voice="", instructions="")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="short",
+ audiocpp_voice="", instructions="")
+ cmd = self._convert(
+ 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
@@ -990,13 +988,12 @@ class ConvertFlowTests(unittest.TestCase):
self._patch_remote(
[{"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="Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF",
- audiocpp_voice="Ryan", instructions="")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ 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 picked speaker is passed as --voice; no separate speaker kwarg.
self.assertEqual(cmd[2]["voice"], "Ryan")
self.assertNotIn("speaker", cmd[2])
@@ -1016,13 +1013,12 @@ class ConvertFlowTests(unittest.TestCase):
[{"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._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.assertNotIn("speaker", cmd[2])
self.assertIsNone(cmd[2]["instructions"])
@@ -1044,12 +1040,11 @@ class ConvertFlowTests(unittest.TestCase):
[{"id": "Qwen3-TTS-12Hz-1.7B-Base-GGUF",
"family": "qwen3_tts", "task": "tts"}],
voices=[])
- with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
- self._answer_form(
- backend="audiocpp-remote",
- model_id="Qwen3-TTS-12Hz-1.7B-Base-GGUF",
- audiocpp_voice="", instructions="")
- self._convert(None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(
+ backend="audiocpp-remote",
+ model_id="Qwen3-TTS-12Hz-1.7B-Base-GGUF",
+ audiocpp_voice="", instructions="")
+ self._convert(None, [self._remote("audiocpp", "audio.cpp")])
fields = self.tui.forms_seen[0][1]
voice_field = self._field("audiocpp_voice")
self.assertTrue(voice_field["visible"](fields))
@@ -1069,9 +1064,7 @@ class ConvertFlowTests(unittest.TestCase):
"models": [{"id": "qwen", "family": "qwen3_tts",
"task": "tts"}],
}), encoding="utf-8")
- with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=root), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ with patch.object(hub.audiocpp_backend, "find_local_checkout", return_value=root):
self._answer_form(backend="audiocpp", model_id="qwen",
audiocpp_voice="", instructions="")
self._convert(None,
@@ -1091,11 +1084,10 @@ class ConvertFlowTests(unittest.TestCase):
# 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="", instructions="")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="legacy",
+ audiocpp_voice="", instructions="")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
self.assertIsNotNone(cmd)
self.assertIsNone(cmd[2]["voice"])
self.assertNotIn("speaker", cmd[2])
@@ -1106,12 +1098,11 @@ class ConvertFlowTests(unittest.TestCase):
def test_audiocpp_vdes_hides_voice_and_requires_instructions(self):
self._patch_remote(
[{"id": "design", "family": "qwen3_tts", "task": "vdes"}])
- with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
- self._answer_form(backend="audiocpp-remote", model_id="design",
- audiocpp_voice=None,
- instructions="A warm British narrator")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="design",
+ audiocpp_voice=None,
+ instructions="A warm British narrator")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
self.assertIsNone(cmd[2]["voice"])
self.assertEqual(cmd[2]["instructions"], "A warm British narrator")
fields = self.tui.forms_seen[0][1]
@@ -1129,12 +1120,11 @@ class ConvertFlowTests(unittest.TestCase):
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._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator",
+ instructions="stale description")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
self.assertEqual(cmd[2]["instructions"], "stale description")
def test_audiocpp_required_voice_validates(self):
@@ -1142,11 +1132,10 @@ class ConvertFlowTests(unittest.TestCase):
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="")
- self._convert(None,
- [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
voice_field = self._field("audiocpp_voice")
self.assertIsNotNone(voice_field["validate"](""))
self.assertIsNone(voice_field["validate"]("narrator"))
@@ -1157,14 +1146,12 @@ class ConvertFlowTests(unittest.TestCase):
self._patch_remote(
[{"id": "Qwen3-TTS-CustomVoice-GGUF", "family": "qwen3_tts",
"task": "tts"}])
- with patch.object(hub.config, "SPEAKER", "Vivian"), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
- self._answer_form(
- backend="audiocpp-remote",
- model_id="Qwen3-TTS-CustomVoice-GGUF",
- audiocpp_voice="Vivian", instructions="")
- self._convert(None,
- [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(
+ backend="audiocpp-remote",
+ model_id="Qwen3-TTS-CustomVoice-GGUF",
+ audiocpp_voice="Vivian", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
fields = self.tui.forms_seen[0][1]
label = self._field("audiocpp_voice")["label"]
self.assertEqual(label(fields), "Built-in voice")
@@ -1175,12 +1162,11 @@ class ConvertFlowTests(unittest.TestCase):
[{"id": "Qwen3-TTS-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-Base-GGUF",
- audiocpp_voice="narrator", instructions="")
- self._convert(None,
- [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote",
+ model_id="Qwen3-TTS-Base-GGUF",
+ audiocpp_voice="narrator", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
fields = self.tui.forms_seen[0][1]
label = self._field("audiocpp_voice")["label"]
self.assertEqual(label(fields), "Voice to clone")
@@ -1192,11 +1178,10 @@ class ConvertFlowTests(unittest.TestCase):
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="", instructions="")
- self._convert(None,
- [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="", instructions="")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
fields = self.tui.forms_seen[0][1]
voice = self._field("audiocpp_voice")
instr = self._field("instructions")
@@ -1211,11 +1196,10 @@ class ConvertFlowTests(unittest.TestCase):
self._patch_remote(
[{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
voices=[])
- with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
- self._answer_form(backend="audiocpp-remote", model_id="higgs",
- audiocpp_voice="", instructions="")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="", instructions="")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
fields = self.tui.forms_seen[0][1]
voice = self._field("audiocpp_voice")
instr = self._field("instructions")
@@ -1230,13 +1214,12 @@ class ConvertFlowTests(unittest.TestCase):
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="",
- request_options="emotion=neutral, speed=1.1")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(
+ backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="",
+ request_options="emotion=neutral, speed=1.1")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
self.assertEqual(cmd[2]["request_options"],
{"emotion": "neutral", "speed": "1.1"})
@@ -1244,12 +1227,11 @@ class ConvertFlowTests(unittest.TestCase):
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="",
- request_options="oops")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="",
+ request_options="oops")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
options_field = self._field("request_options")
self.assertIsNone(options_field["validate"]("emotion=neutral"))
self.assertIsNotNone(options_field["validate"]("oops"))
@@ -1284,9 +1266,7 @@ class ConvertFlowTests(unittest.TestCase):
[{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
voices=["narrator"])
with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=self._specs_checkout(
- ("higgs_audio_tts",))), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ return_value=self._specs_checkout(("higgs_audio_tts",))):
self._answer_form(backend="audiocpp-remote", model_id="higgs",
audiocpp_voice="narrator", instructions="")
cmd = self._convert(
@@ -1302,9 +1282,7 @@ class ConvertFlowTests(unittest.TestCase):
# A checkout exists but only qwen3_tts declares request options:
# higgs is provably unsupported -> hidden.
with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=self._specs_checkout(
- ("qwen3_tts",))), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ return_value=self._specs_checkout(("qwen3_tts",))):
self._answer_form(backend="audiocpp-remote", model_id="higgs",
audiocpp_voice="narrator", instructions="")
self._convert(None,
@@ -1317,9 +1295,7 @@ class ConvertFlowTests(unittest.TestCase):
self._patch_remote(
[{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
voices=["narrator"])
- with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=None), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ with patch.object(hub.audiocpp_backend, "find_local_checkout", return_value=None):
self._answer_form(backend="audiocpp-remote", model_id="higgs",
audiocpp_voice="narrator", instructions="")
self._convert(None,
@@ -1335,12 +1311,11 @@ class ConvertFlowTests(unittest.TestCase):
{"id": "design", "family": "qwen3_tts", "task": "vdes"},
{"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="design",
- audiocpp_voice=None,
- instructions="A warm British narrator")
- self._convert(None,
- [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="design",
+ audiocpp_voice=None,
+ instructions="A warm British narrator")
+ self._convert(None,
+ [self._remote("audiocpp", "audio.cpp")])
instr = self._field("instructions")
self.assertEqual(instr["help"], [
"TTS style instructions. Supported by some clone models. Example:",
@@ -1352,9 +1327,7 @@ class ConvertFlowTests(unittest.TestCase):
[{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
voices=["narrator"])
with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=self._specs_checkout(
- ("qwen3_tts", "higgs_audio_tts"))), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ return_value=self._specs_checkout(("qwen3_tts", "higgs_audio_tts"))):
self._answer_form(backend="audiocpp-remote", model_id="higgs",
audiocpp_voice="narrator", instructions="")
self._convert(None,
@@ -1371,7 +1344,6 @@ class ConvertFlowTests(unittest.TestCase):
# The Settings Language setting travels on the run kwargs as-is;
# the converter normalizes it (short codes included).
with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
- patch.object(hub.config, "SPEAKER", "Vivian"), \
patch.object(hub.config, "LANGUAGE", "en"):
self._answer_form(backend="qwen", mode="custom",
speaker="Vivian", clone="")
@@ -1399,11 +1371,10 @@ class ConvertFlowTests(unittest.TestCase):
self._patch_remote(
[{"id": "higgs", "family": "higgs_audio_tts", "task": "tts"}],
voices=[])
- with patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
- self._answer_form(backend="audiocpp-remote", model_id="higgs",
- audiocpp_voice="", instructions="")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp")])
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="", instructions="")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp")])
self.assertIsNotNone(cmd)
self.assertIsNone(cmd[2]["voice"])
fields = self.tui.forms_seen[0][1]
@@ -1429,11 +1400,8 @@ class ConvertFlowTests(unittest.TestCase):
"voice_dir": str(root),
}), encoding="utf-8")
(root / "Narrator.wav").write_bytes(b"x")
- with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=root), \
- patch.object(hub.audiocpp_backend, "fetch_server_models",
- must_not_query), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ with patch.object(hub.audiocpp_backend, "find_local_checkout", return_value=root), \
+ patch.object(hub.audiocpp_backend, "fetch_server_models", must_not_query):
self._answer_form(backend="audiocpp", model_id="qwen",
audiocpp_voice="Narrator", instructions="")
cmd = self._convert(None,
@@ -1456,9 +1424,7 @@ class ConvertFlowTests(unittest.TestCase):
"models": [{"id": "qwen", "family": "qwen3_tts",
"task": "tts"}],
}), encoding="utf-8")
- with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=root), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ with patch.object(hub.audiocpp_backend, "find_local_checkout", return_value=root):
self._answer_form(backend="audiocpp", model_id="qwen",
audiocpp_voice="", instructions="")
cmd = self._convert(None, [
@@ -1496,9 +1462,7 @@ class ConvertFlowTests(unittest.TestCase):
"models": [{"id": "qwen", "family": "qwen3_tts",
"task": "tts"}],
}), encoding="utf-8")
- with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=root), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ with patch.object(hub.audiocpp_backend, "find_local_checkout", return_value=root):
# Managed selected: its picks must survive next to the
# remote entry's same-shaped fields.
self.tui.form_script.append({
@@ -1529,12 +1493,11 @@ class ConvertFlowTests(unittest.TestCase):
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="")
- cmd = self._convert(
- None, [self._remote("audiocpp", "audio.cpp",
- url="http://10.0.0.5:8080")])
+ self._answer_form(backend="audiocpp-remote", model_id="higgs",
+ audiocpp_voice="narrator", instructions="")
+ cmd = self._convert(
+ None, [self._remote("audiocpp", "audio.cpp",
+ url="http://10.0.0.5:8080")])
self.assertEqual(cmd[2]["api_url"], "http://10.0.0.5:8080")
# ------------------------------------------------------------------
@@ -1556,29 +1519,17 @@ class ConvertFlowTests(unittest.TestCase):
# ------------------------------------------------------------------
def test_qwen_builds_speaker_and_clone_form(self):
- with patch.object(hub.qwen_backend, "QWEN_SPEAKERS",
- ["Vivian", "Serena"]), \
- patch.object(hub.config, "SPEAKER", "Vivian"), \
- patch.object(hub.qwen_backend.config, "QWEN_MODEL",
- "CustomVoice"):
+ with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian", "Serena"]):
self._answer_form(backend="qwen", mode="custom", speaker="Serena",
clone="")
- # The fake mirrors the real update_config_value contract:
- # persisting a value also lands it on the imported module.
- def fake_update(key, value, config_path=None):
- setattr(hub.config, key, value)
- return True
-
- with patch.object(hub.common, "update_config_value",
- fake_update):
- cmd = self._convert(None,
- [self._ready("qwen", "qwen-tts")])
- speaker_in_memory = hub.config.SPEAKER
+ cmd = self._convert(None,
+ [self._ready("qwen", "qwen-tts")])
self.assertEqual(cmd[0], "convert")
self.assertEqual(cmd[1], hub.BACKEND_QWEN)
self.assertIsNone(cmd[2]["clone"])
self.assertIsNone(cmd[2].get("instructions"))
- self.assertEqual(speaker_in_memory, "Serena")
+ # The picked speaker travels with the run; nothing is persisted.
+ self.assertEqual(cmd[2]["voice"], "Serena")
fields = self.tui.forms_seen[0][1]
self.assertEqual([f["key"] for f in fields],
["backend", "mode", "speaker", "clone_dir",
@@ -1617,11 +1568,8 @@ class ConvertFlowTests(unittest.TestCase):
self.assertFalse(clone_field["visible"](fields))
self.assertTrue(design_field["visible"](fields))
- def test_qwen_design_mode_passes_instructions_and_persists_model(self):
- with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
- patch.object(hub.config, "SPEAKER", "Vivian"), \
- patch.object(hub.qwen_backend.config, "QWEN_MODEL",
- "CustomVoice"):
+ def test_qwen_design_mode_passes_instructions(self):
+ with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]):
self._answer_form(backend="qwen", mode="design",
qwen_instructions="A warm narrator")
with patch.object(hub.common, "update_config_value") as mk_update:
@@ -1629,51 +1577,37 @@ class ConvertFlowTests(unittest.TestCase):
[self._ready("qwen", "qwen-tts")])
self.assertEqual(cmd[2]["clone"], None)
self.assertEqual(cmd[2]["instructions"], "A warm narrator")
- # The model switch is persisted (CustomVoice -> VoiceDesign).
- mk_update.assert_called_once_with("QWEN_MODEL", "VoiceDesign")
+ # Per-run choices are not persisted to the config file.
+ mk_update.assert_not_called()
- def test_qwen_clone_mode_passes_path_and_persists_model(self):
- with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
- patch.object(hub.config, "SPEAKER", "Vivian"), \
- patch.object(hub.qwen_backend.config, "QWEN_MODEL",
- "CustomVoice"):
+ def test_qwen_clone_mode_passes_path(self):
+ with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]):
self._answer_form(backend="qwen", mode="clone", speaker="Vivian",
clone="/tmp/ref.wav")
with patch.object(hub.common, "update_config_value") as mk_update:
cmd = self._convert(None,
- [self._ready("qwen", "qwen-tts")])
+ [self._ready("qwen", "qwen-tts")])
self.assertEqual(cmd[2]["clone"], "/tmp/ref.wav")
- # Clone mode does not touch the global speaker.
- keys = [c.args[0] for c in mk_update.call_args_list]
- self.assertNotIn("SPEAKER", keys)
- # ...but remembers the switch to the Base model.
- self.assertEqual(keys, ["QWEN_MODEL"])
- self.assertEqual(mk_update.call_args.args[1], "Base")
-
- def test_qwen_same_model_run_persists_nothing_new(self):
- with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
- patch.object(hub.config, "SPEAKER", "Vivian"), \
- patch.object(hub.qwen_backend.config, "QWEN_MODEL",
- "CustomVoice"):
+ mk_update.assert_not_called()
+
+ def test_qwen_custom_mode_passes_the_speaker(self):
+ with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]):
self._answer_form(backend="qwen", mode="custom", speaker="Vivian")
with patch.object(hub.common, "update_config_value") as mk_update:
cmd = self._convert(None,
[self._ready("qwen", "qwen-tts")])
self.assertIsNotNone(cmd)
+ self.assertEqual(cmd[2]["voice"], "Vivian")
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).
+ def test_qwen_form_defaults_to_the_first_model(self):
+ # No persisted default: the Model picker opens on CustomVoice.
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")
+ self.assertEqual(self._field("mode")["value"], "custom")
def test_qwen_clone_dir_defaults_to_the_project_voices(self):
# The Clone .wav directory is the shared directory widget, seeded
@@ -1689,9 +1623,7 @@ class ConvertFlowTests(unittest.TestCase):
("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.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
patch.object(hub.common, "update_config_value"):
self._answer_form(backend="qwen", mode="custom",
speaker="Vivian")
@@ -1720,9 +1652,7 @@ class ConvertFlowTests(unittest.TestCase):
(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.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
patch.object(hub.common, "update_config_value"):
self._answer_form(backend="qwen", mode="clone", clone="")
self._convert(None, [self._ready("qwen", "qwen-tts")])
@@ -1746,9 +1676,7 @@ class ConvertFlowTests(unittest.TestCase):
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.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
patch.object(hub.common, "update_config_value"):
self._answer_form(backend="qwen", mode="clone", clone="")
cmd = self._convert(None,
@@ -1846,8 +1774,7 @@ class ConvertFlowTests(unittest.TestCase):
"qwen", "qwen-tts",
remote_urls={"qwen": "http://10.0.0.5:7861"},
remote_models=["Base"])
- with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]), \
- patch.object(hub.config, "SPEAKER", "Vivian"):
+ with patch.object(hub.qwen_backend, "QWEN_SPEAKERS", ["Vivian"]):
self._answer_form(backend="qwen-remote", mode="clone",
speaker="Vivian", clone="/tmp/ref.wav")
cmd = self._convert(None, [st])
@@ -1870,9 +1797,7 @@ class ConvertFlowTests(unittest.TestCase):
"models": [{"id": "higgs", "family": "higgs_audio_tts",
"task": "tts"}],
}), encoding="utf-8")
- with patch.object(hub.audiocpp_backend, "find_local_checkout",
- return_value=root), \
- patch.object(hub.config, "AUDIOCPP_INSTRUCTIONS", ""):
+ with patch.object(hub.audiocpp_backend, "find_local_checkout", return_value=root):
self._answer_form(backend="qwen", mode="custom",
speaker="Vivian", clone="")
cmd = self._convert(None, [
@@ -1974,7 +1899,11 @@ class PrepareRunConfigTests(unittest.TestCase):
with patch.object(hub, "detect_all", return_value=[]), \
patch.object(hub, "_find_spec", return_value=spec):
cfg = hub._prepare_run_config("qwen", kwargs)
- self.assertIs(cfg.autostart_spec, spec)
+ # The qwen spec is rebuilt for the model this run selected, so an
+ # autostart boots exactly what the conversion needs.
+ self.assertEqual(cfg.autostart_spec.name, "qwen")
+ self.assertEqual(cfg.autostart_spec.identity, "qwen-custom")
+ self.assertEqual(cfg.autostart_spec.url, spec.url)
self.assertFalse(cfg.restart_first)
self.assertEqual(cfg.server_name, "qwen")
self.assertNotIn("autostart", kwargs)
@@ -1985,13 +1914,15 @@ class PrepareRunConfigTests(unittest.TestCase):
spec = self._spec()
status = BackendStatus("qwen", "qwen-tts", installed=True,
configured=True, servers=[spec])
- kwargs = {"restart_server": "qwen"}
+ kwargs = {"restart_server": "qwen", "clone": "/tmp/ref.wav"}
with patch.object(hub, "detect_all", return_value=[status]), \
patch("backends.common.server_running",
return_value=True), \
patch.object(hub.servers, "alive", return_value=True):
cfg = hub._prepare_run_config("qwen", kwargs)
- self.assertIs(cfg.autostart_spec, spec)
+ self.assertEqual(cfg.autostart_spec.name, spec.name)
+ # The restart spec hosts the Base model (the run clones a voice).
+ self.assertEqual(cfg.autostart_spec.identity, "qwen-clone")
self.assertTrue(cfg.restart_first)
self.assertNotIn("restart_server", kwargs)
self.assertEqual(cfg.server_url, spec.url)
diff --git a/app/tests/test_tts.py b/app/tests/test_tts.py
index ce2dbb6..3538d8b 100644
--- a/app/tests/test_tts.py
+++ b/app/tests/test_tts.py
@@ -48,6 +48,10 @@ from converter.converter import AudiobookConverter
# Chunks folder handed to clients whose tests never write chunk files.
_DUMMY_CHUNKS = Path(tempfile.gettempdir()) / "audiobook_tts_test_chunks"
+# A concrete audio.cpp model entry id (no config default anymore): the
+# tests request it explicitly, the way --model / the Generate form does.
+_AUDIOCPP_MODEL_ID = "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF"
+
class NormalizeLanguageTests(unittest.TestCase):
def test_display_names_case_insensitive(self):
@@ -116,14 +120,16 @@ class QwenTTSClientLanguageTests(unittest.TestCase):
return QwenTTSClient(_DUMMY_CHUNKS, **kwargs)
def test_default_follows_config_for_each_mode(self):
- custom = self._make_client(voice_mode=VOICE_MODE_CUSTOM)
+ custom = self._make_client(voice_mode=VOICE_MODE_CUSTOM,
+ voice="Vivian")
self.assertEqual(custom.language, config.LANGUAGE)
clone = self._make_client(voice_mode=VOICE_MODE_CLONE,
voice_clone_ref_audio="ref.wav")
self.assertEqual(clone.language, config.LANGUAGE)
def test_explicit_language_normalized(self):
- client = self._make_client(voice_mode=VOICE_MODE_CUSTOM, language="ja")
+ client = self._make_client(voice_mode=VOICE_MODE_CUSTOM,
+ language="ja", voice="Vivian")
self.assertEqual(client.language, "Japanese")
def test_invalid_language_fails_before_connect(self):
@@ -134,7 +140,8 @@ class QwenTTSClientLanguageTests(unittest.TestCase):
def test_api_url_override_stored(self):
client = self._make_client(voice_mode=VOICE_MODE_CUSTOM,
- api_url="http://10.0.0.5:7860")
+ api_url="http://10.0.0.5:7860",
+ voice="Vivian")
self.assertEqual(client.api_url, "http://10.0.0.5:7860")
def test_api_url_override_used_by_connect(self):
@@ -158,19 +165,22 @@ class SeedResolutionTests(unittest.TestCase):
def test_constant_seed_draws_one_nonnegative_seed(self):
with patch.object(config, "CONSTANT_SEED", True), \
patch.object(config, "SEED", -1):
- client = self._make_client(voice_mode=VOICE_MODE_CUSTOM)
+ client = self._make_client(voice_mode=VOICE_MODE_CUSTOM,
+ voice="Vivian")
self.assertGreaterEqual(client._seed, 0)
def test_explicit_seed_wins_over_constant_seed(self):
with patch.object(config, "CONSTANT_SEED", True), \
patch.object(config, "SEED", 42):
- client = self._make_client(voice_mode=VOICE_MODE_CUSTOM)
+ client = self._make_client(voice_mode=VOICE_MODE_CUSTOM,
+ voice="Vivian")
self.assertEqual(client._seed, 42)
def test_without_constant_seed_minus_one_is_forwarded(self):
with patch.object(config, "CONSTANT_SEED", False), \
patch.object(config, "SEED", -1):
- client = self._make_client(voice_mode=VOICE_MODE_CUSTOM)
+ client = self._make_client(voice_mode=VOICE_MODE_CUSTOM,
+ voice="Vivian")
self.assertEqual(client._seed, -1)
def test_resolved_seed_is_reused_across_requests(self):
@@ -183,6 +193,7 @@ class SeedResolutionTests(unittest.TestCase):
}
client = QwenTTSClient.__new__(QwenTTSClient)
client.voice_mode = VOICE_MODE_CUSTOM
+ client.speaker = "Vivian"
client.language = "English"
client._seed = 1234
client.api_info = api_info
@@ -208,6 +219,7 @@ class PayloadLanguageTests(unittest.TestCase):
def _custom_client(self, language, endpoint, api_info=None):
client = QwenTTSClient.__new__(QwenTTSClient)
client.voice_mode = VOICE_MODE_CUSTOM
+ client.speaker = "Vivian"
client.language = language
client._seed = config.SEED
client.api_info = api_info if api_info is not None else {
@@ -287,7 +299,7 @@ class FasterTTSClientHealthTests(unittest.TestCase):
with patch("converter.clients.faster.urllib.request.urlopen",
side_effect=urllib.error.URLError("Connection refused")):
with self.assertRaises(RuntimeError) as ctx:
- FasterTTSClient(_DUMMY_CHUNKS)
+ FasterTTSClient(_DUMMY_CHUNKS, voice="narrator")
message = str(ctx.exception)
self.assertIn("not reachable", message)
self.assertIn("README", message)
@@ -296,14 +308,25 @@ class FasterTTSClientHealthTests(unittest.TestCase):
with patch("converter.clients.faster.urllib.request.urlopen",
return_value=self._health_response(model_loaded=False)):
with self.assertRaises(RuntimeError) as ctx:
- FasterTTSClient(_DUMMY_CHUNKS)
+ FasterTTSClient(_DUMMY_CHUNKS, voice="narrator")
self.assertIn("not loaded", str(ctx.exception))
- def test_healthy_server_defaults_from_config(self):
+ def test_missing_voice_raises_before_connecting(self):
+ # There is no configured default voice: a faster run names its
+ # voice per run (the server silently falls back when the key is
+ # not in its voices.json).
+ with patch("converter.clients.faster.urllib.request.urlopen") \
+ as mock_urlopen:
+ with self.assertRaises(RuntimeError) as ctx:
+ FasterTTSClient(_DUMMY_CHUNKS)
+ self.assertIn("requires a voice", str(ctx.exception))
+ mock_urlopen.assert_not_called()
+
+ def test_healthy_server_uses_the_requested_voice(self):
with patch("converter.clients.faster.urllib.request.urlopen",
return_value=self._health_response()):
- client = FasterTTSClient(_DUMMY_CHUNKS)
- self.assertEqual(client.voice, config.FASTER_VOICE)
+ client = FasterTTSClient(_DUMMY_CHUNKS, voice="narrator")
+ self.assertEqual(client.voice, "narrator")
self.assertEqual(client.api_url, config.FASTER_API_URL.rstrip("/"))
def test_explicit_voice_and_url_override_config(self):
@@ -533,8 +556,7 @@ class QwenTTSClientVoiceDesignTests(unittest.TestCase):
client.chunks_dir = Path(self._tmp.name)
client.voice_mode = VOICE_MODE_DESIGN
client.language = config.LANGUAGE
- client.instructions = (instructions if instructions is not None
- else config.INSTRUCT).strip()
+ client.instructions = (instructions or "").strip()
client.api_info = {"named_endpoints": {"/run_voice_design": {
"parameters": [
{"parameter_name": "text"},
@@ -577,10 +599,11 @@ class QwenTTSClientVoiceDesignTests(unittest.TestCase):
self.assertNotIn("seed", captured) # not accepted by this endpoint
self.assertEqual(result, (self._fake_output(),))
- def test_payload_defaults_instructions_to_config(self):
+ def test_payload_uses_empty_design_field_when_no_instructions_given(self):
+ # There is no configured default instruction: the client sends
+ # whatever the run provided (empty when none).
client = self._client(instructions=None)
- self.assertEqual(client.instructions,
- (config.INSTRUCT or "").strip())
+ self.assertEqual(client.instructions, "")
def test_unknown_api_falls_back_to_the_requested_name(self):
client = self._client()
@@ -596,15 +619,6 @@ class QwenTTSClientVoiceDesignTests(unittest.TestCase):
class AudioCppTTSClientHealthTests(unittest.TestCase):
"""Connection behavior of the audio.cpp client."""
- def setUp(self):
- # The default AUDIOCPP_MODEL_ID is empty (auto-select); these tests
- # 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)
-
@staticmethod
def _json_response(payload):
response = MagicMock()
@@ -621,7 +635,7 @@ 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": _AUDIOCPP_MODEL_ID,
"family": "qwen3_tts"}]})
if "/v1/audio/voices" in url:
if voices is Exception:
@@ -631,7 +645,8 @@ 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=_AUDIOCPP_MODEL_ID, **kwargs):
with patch("converter.clients.faster.urllib.request.urlopen",
side_effect=self._get_responses(**kwargs)):
return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice,
@@ -656,25 +671,33 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
with self.assertRaises(RuntimeError) as ctx:
self._client(models={"data": [{"id": "pocket-tts"}, {"id": "other"}]})
message = str(ctx.exception)
- self.assertIn(config.AUDIOCPP_MODEL_ID, message)
+ self.assertIn(_AUDIOCPP_MODEL_ID, message)
self.assertIn("pocket-tts", message)
self.assertIn("other", message)
def test_healthy_server_speaker_mode_defaults(self):
- client = self._client()
+ client = self._client(voice="Vivian")
self.assertEqual(client.api_url, config.AUDIOCPP_API_URL.rstrip("/"))
- self.assertEqual(client.model_id, config.AUDIOCPP_MODEL_ID)
+ self.assertEqual(client.model_id, _AUDIOCPP_MODEL_ID)
self.assertEqual(client.language, config.LANGUAGE)
self.assertEqual(client.voice, "Vivian")
self.assertFalse(client.preset_mode)
self.assertTrue(client.speaker_mode)
- def test_speaker_mode_uses_configured_speaker(self):
- with patch.object(config, "SPEAKER", "uncle_fu"):
- client = self._client()
+ def test_speaker_mode_normalizes_the_speaker_name(self):
+ client = self._client(voice="uncle_fu")
self.assertEqual(client.voice, "Uncle Fu")
self.assertTrue(client.speaker_mode)
+ def test_no_voice_on_speaker_entry_raises(self):
+ # There is no configured default speaker: a CustomVoice entry
+ # without --voice fails fast instead of guessing one.
+ with self.assertRaises(RuntimeError) as ctx:
+ self._client()
+ message = str(ctx.exception)
+ self.assertIn("built-in speakers", message)
+ self.assertIn("--voice", message)
+
def test_voice_speaker_name_selects_speaker_mode(self):
# --voice naming a built-in CustomVoice speaker selects speaker
# mode; the name is normalized to its wire (display) form and no
@@ -697,18 +720,15 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
self.assertIn("'Ryan'", message)
self.assertIn("--voice", message)
- def test_voice_speaker_name_does_not_reroute_to_clone_model(self):
- # A built-in speaker name on a CustomVoice primary selects speaker
- # mode without the AUDIOCPP_CLONE_MODEL_ID reroute.
- with patch.object(config, "AUDIOCPP_MODEL_ID",
- "Qwen3-TTS-CustomVoice"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"):
- client = self._client(
- voice="Ryan",
- models={"data": [{"id": "Qwen3-TTS-CustomVoice",
- "family": "qwen3_tts"},
- {"id": "qwen3-tts-clone",
- "family": "qwen3_tts"}]})
+ def test_speaker_mode_stays_on_the_selected_entry(self):
+ # A built-in speaker name selects speaker mode on the entry the
+ # run picked; no second-entry rerouting exists anymore.
+ client = self._client(
+ voice="Ryan", model_id="Qwen3-TTS-CustomVoice",
+ models={"data": [{"id": "Qwen3-TTS-CustomVoice",
+ "family": "qwen3_tts"},
+ {"id": "qwen3-tts-clone",
+ "family": "qwen3_tts"}]})
self.assertEqual(client.model_id, "Qwen3-TTS-CustomVoice")
self.assertTrue(client.speaker_mode)
self.assertFalse(client.preset_mode)
@@ -750,33 +770,23 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
mock_urlopen.assert_not_called()
def test_explicit_language_normalized(self):
- client = self._client(language="ja")
+ client = self._client(language="ja", voice="Vivian")
self.assertEqual(client.language, "Japanese")
def test_seed_resolved_once_per_run(self):
with patch.object(config, "CONSTANT_SEED", True), \
patch.object(config, "SEED", -1):
- client = self._client()
+ client = self._client(voice="Vivian")
self.assertGreaterEqual(client._seed, 0)
- def test_preset_mode_routes_to_clone_model_when_configured(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"):
- client = self._client(
- voice="narrator",
- models={"data": [{"id": "qwen3-tts"}, {"id": "qwen3-tts-clone"}]})
- self.assertEqual(client.model_id, "qwen3-tts-clone")
-
- def test_preset_mode_falls_back_when_clone_model_not_on_server(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"), \
- self.assertLogs("converter.clients.audiocpp", level="WARNING") as logs:
- client = self._client(
- voice="narrator",
- models={"data": [{"id": "qwen3-tts", "family": "qwen3_tts"},
- {"id": "pocket-tts"}]})
+ def test_preset_mode_stays_on_the_requested_entry(self):
+ # Preset (cloning) requests synthesize with the entry the run
+ # selected; pick the Base entry with --model to clone on it.
+ client = self._client(
+ voice="narrator", model_id="qwen3-tts",
+ models={"data": [{"id": "qwen3-tts"}, {"id": "qwen3-tts-clone"}]})
self.assertEqual(client.model_id, "qwen3-tts")
- self.assertTrue(any("qwen3-tts-clone" in line for line in logs.output))
+ self.assertTrue(client.preset_mode)
def test_empty_model_id_auto_picks_single_server_entry(self):
# A multi-model server used without editing config.py: an empty
@@ -798,78 +808,47 @@ class AudioCppTTSClientHealthTests(unittest.TestCase):
self.assertIn("higgs", message)
self.assertIn("voxcpm2", message)
- def test_model_id_override_reaches_request(self):
- # --model overrides AUDIOCPP_MODEL_ID for the run.
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen"):
- client = self._client(
- voice="narrator", model_id="higgs",
- models={"data": [{"id": "higgs", "family": "higgs_audio_tts"}]},
- voices={"voices": ["narrator"]})
+ def test_model_id_reaches_request(self):
+ # The per-run --model value is what the client requests.
+ client = self._client(
+ voice="narrator", model_id="higgs",
+ models={"data": [{"id": "higgs", "family": "higgs_audio_tts"}]},
+ voices={"voices": ["narrator"]})
self.assertEqual(client.model_id, "higgs")
- def test_clone_model_id_ignored_for_speaker_mode(self):
- # 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-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",
- config.AUDIOCPP_MODEL_ID):
- client = self._client(voice="narrator")
- self.assertEqual(client.model_id, config.AUDIOCPP_MODEL_ID)
-
- def test_preset_mode_with_clone_only_server_uses_clone_model(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"):
- client = self._client(
- voice="narrator",
- models={"data": [{"id": "qwen3-tts-clone"}]})
+ def test_preset_mode_on_a_single_clone_entry_server(self):
+ # A server hosting only the Base (cloning) entry: select it with
+ # --model and a preset voice works.
+ client = self._client(
+ voice="narrator", model_id="qwen3-tts-clone",
+ models={"data": [{"id": "qwen3-tts-clone"}]})
self.assertEqual(client.model_id, "qwen3-tts-clone")
+ self.assertTrue(client.preset_mode)
- def test_speaker_mode_with_clone_only_server_suggests_voice(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"):
- with self.assertRaises(RuntimeError) as ctx:
- self._client(models={"data": [{"id": "qwen3-tts-clone"}]})
+ def test_unknown_model_id_error_suggests_a_model(self):
+ # Requesting an id the server does not host fails fast and names
+ # both the requested and the hosted ids.
+ with self.assertRaises(RuntimeError) as ctx:
+ self._client(voice="narrator", model_id="qwen3-tts",
+ models={"data": [{"id": "qwen3-tts-clone"}]})
message = str(ctx.exception)
self.assertIn("qwen3-tts", message)
- self.assertIn("--voice", message)
+ self.assertIn("qwen3-tts-clone", message)
+ self.assertIn("--model", 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.assertNoLogs("converter.clients.audiocpp", level="WARNING"):
+ with self.assertNoLogs("converter.clients.audiocpp", level="WARNING"):
with self.assertRaises(RuntimeError) as ctx:
- self._client(voice="narrator",
+ self._client(voice="narrator", model_id="qwen3-tts",
models={"data": [{"id": "pocket-tts"}]})
message = str(ctx.exception)
self.assertIn("qwen3-tts", message)
- self.assertIn("qwen3-tts-clone", message)
self.assertIn("pocket-tts", message)
class AudioCppTaskDetectionTests(unittest.TestCase):
"""Task auto-detection (tts/clon/vdes) and voice design validation."""
- def setUp(self):
- # 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)
-
@staticmethod
def _json_response(payload):
response = MagicMock()
@@ -880,7 +859,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
def _client(self, voice=None, instructions=None, request_options=None,
models=None):
if models is None:
- models = {"data": [{"id": config.AUDIOCPP_MODEL_ID,
+ models = {"data": [{"id": _AUDIOCPP_MODEL_ID,
"family": "qwen3_tts"}]}
def _dispatch(request, **_kwargs):
@@ -897,18 +876,19 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
side_effect=_dispatch):
return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice,
instructions=instructions,
- request_options=request_options)
+ request_options=request_options,
+ model_id=_AUDIOCPP_MODEL_ID)
def test_missing_task_falls_back_to_tts(self):
# Servers that predate the task field hosted plain TTS models.
- client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts"}]})
+ client = self._client(voice="Vivian", models={"data": [
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts"}]})
self.assertEqual(client.task, AUDIOCPP_TASK_TTS)
self.assertFalse(client.design_mode)
def test_task_detected_from_models_endpoint(self):
client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
"task": "vdes"}]},
instructions="A warm adult narrator")
self.assertEqual(client.task, AUDIOCPP_TASK_VDES)
@@ -916,7 +896,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
def test_clon_task_entry_connects_in_preset_mode(self):
client = self._client(voice="narrator", models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "chatterbox",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "chatterbox",
"task": "clon"}]})
self.assertEqual(client.task, "clon")
self.assertFalse(client.design_mode)
@@ -925,7 +905,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
def test_unsupported_task_rejected_with_available_entries(self):
with self.assertRaises(RuntimeError) as ctx:
self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_asr",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_asr",
"task": "asr"},
{"id": "tts-1", "family": "qwen3_tts", "task": "tts"}]},
instructions="unused")
@@ -937,7 +917,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
def test_vdes_without_instructions_requires_description(self):
with self.assertRaises(RuntimeError) as ctx:
self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
"task": "vdes"}]})
message = str(ctx.exception)
self.assertIn("voice design", message)
@@ -946,7 +926,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
def test_vdes_with_voice_rejected(self):
with self.assertRaises(RuntimeError) as ctx:
self._client(voice="narrator", models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
"task": "vdes"}]},
instructions="A warm adult narrator")
self.assertIn("--voice", str(ctx.exception))
@@ -956,7 +936,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
buf = io.StringIO()
with redirect_stdout(buf):
client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
"task": "vdes"}]},
instructions="A warm adult narrator")
self.assertTrue(client.design_mode)
@@ -971,7 +951,7 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
buf = io.StringIO()
with redirect_stdout(buf):
client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "omnivoice",
+ {"id": _AUDIOCPP_MODEL_ID, "family": "omnivoice",
"task": "tts"}]},
instructions="female, young adult, moderate pitch")
self.assertFalse(client.design_mode)
@@ -981,39 +961,28 @@ class AudioCppTaskDetectionTests(unittest.TestCase):
def test_instructions_with_builtin_speaker_family_stays_speaker_mode(self):
buf = io.StringIO()
with redirect_stdout(buf):
- client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
- "task": "tts"}]},
+ client = self._client(
+ voice="Vivian",
+ models={"data": [
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
+ "task": "tts"}]},
instructions="Very happy.")
self.assertFalse(client.design_mode)
self.assertFalse(client.instruction_voice)
self.assertIn("speaker 'Vivian'", buf.getvalue())
- def test_config_instructions_used_when_flag_omitted(self):
- with patch.object(config, "AUDIOCPP_INSTRUCTIONS",
- "A calm elderly storyteller"):
- client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
- "task": "vdes"}]})
- self.assertEqual(client.instructions, "A calm elderly storyteller")
-
- def test_explicit_instructions_override_config_default(self):
- with patch.object(config, "AUDIOCPP_INSTRUCTIONS", "from config"):
- client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
+ def test_instructions_reach_the_client(self):
+ client = self._client(
+ models={"data": [
+ {"id": _AUDIOCPP_MODEL_ID, "family": "qwen3_tts",
"task": "vdes"}]},
- instructions="from flag")
+ instructions="from flag")
self.assertEqual(client.instructions, "from flag")
class AudioCppFamilyDetectionTests(unittest.TestCase):
"""Family auto-detection and per-family adaptations."""
- def setUp(self):
- patcher = patch.object(config, "AUDIOCPP_MODEL_ID", "qwen")
- patcher.start()
- self.addCleanup(patcher.stop)
-
@staticmethod
def _json_response(payload):
response = MagicMock()
@@ -1034,11 +1003,12 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
with patch("converter.clients.faster.urllib.request.urlopen",
side_effect=_dispatch):
- return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice)
+ return AudioCppTTSClient(_DUMMY_CHUNKS, voice=voice,
+ model_id=_AUDIOCPP_MODEL_ID)
def test_family_detected_from_models_endpoint(self):
client = self._client(models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "higgs_audio_tts"}]})
+ {"id": _AUDIOCPP_MODEL_ID, "family": "higgs_audio_tts"}]})
self.assertEqual(client.family, "higgs_audio_tts")
self.assertIs(client.profile, AUDIOCPP_DEFAULT_FAMILY_PROFILE)
@@ -1046,13 +1016,13 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
# 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}]})
+ {"id": _AUDIOCPP_MODEL_ID}]})
self.assertEqual(client.family, "")
self.assertIs(client.profile, 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"}]})
+ {"id": _AUDIOCPP_MODEL_ID, "family": "future_tts"}]})
self.assertEqual(client.family, "future_tts")
self.assertIs(client.profile, AUDIOCPP_DEFAULT_FAMILY_PROFILE)
self.assertEqual(client.profile.language_style, AUDIOCPP_LANG_OMIT)
@@ -1061,7 +1031,7 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
client = None
try:
client = self._client(voice=None, models={"data": [
- {"id": config.AUDIOCPP_MODEL_ID, "family": "voxcpm2"}]})
+ {"id": _AUDIOCPP_MODEL_ID, "family": "voxcpm2"}]})
except RuntimeError as exc:
message = str(exc)
self.assertIn("voxcpm2", message)
@@ -1071,13 +1041,13 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
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": [
+ # a built-in speaker name selects speaker mode on it.
+ client = self._client(
+ voice="Vivian", models={"data": [
{"id": "Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF",
"family": "qwen3_tts"}]})
self.assertEqual(client.family, "qwen3_tts")
+ self.assertTrue(client.speaker_mode)
def test_speaker_mode_rejected_for_qwen_base_entry(self):
# A Qwen3-TTS entry whose id names Base (not CustomVoice) is
@@ -1094,36 +1064,6 @@ class AudioCppFamilyDetectionTests(unittest.TestCase):
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"), \
- self.assertLogs("converter.clients.audiocpp", level="WARNING") as logs:
- client = self._client(models={"data": [
- {"id": "higgs", "family": "higgs_audio_tts"},
- {"id": "qwen-clone", "family": "qwen3_tts"}]})
- self.assertEqual(client.model_id, "higgs")
- self.assertTrue(any("different family" in line.lower() or
- "hosts family" in line.lower()
- for line in logs.output))
-
- def test_clone_model_id_missing_on_non_qwen_server_is_debug_only(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "higgs"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen-clone"), \
- self.assertNoLogs("converter.clients.audiocpp", level="WARNING"):
- client = self._client(models={"data": [
- {"id": "higgs", "family": "higgs_audio_tts"}]})
- self.assertEqual(client.model_id, "higgs")
-
- def test_clone_model_id_missing_on_qwen_server_still_warns(self):
- with patch.object(config, "AUDIOCPP_MODEL_ID", "qwen3-tts"), \
- patch.object(config, "AUDIOCPP_CLONE_MODEL_ID", "qwen3-tts-clone"), \
- self.assertLogs("converter.clients.audiocpp", level="WARNING") as logs:
- client = self._client(models={"data": [
- {"id": "qwen3-tts", "family": "qwen3_tts"},
- {"id": "pocket-tts", "family": "pocket_tts"}]})
- self.assertEqual(client.model_id, "qwen3-tts")
- self.assertTrue(any("qwen3-tts-clone" in line for line in logs.output))
-
def test_iso_language_code_helper(self):
self.assertEqual(LANGUAGE_ISO_CODES["English"], "en")
self.assertIsNone(LANGUAGE_ISO_CODES.get("Auto"))
@@ -1198,7 +1138,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase):
client = AudioCppTTSClient.__new__(AudioCppTTSClient)
client.chunks_dir = Path(self._tmp.name)
client.api_url = "http://127.0.0.1:8080"
- client.model_id = config.AUDIOCPP_MODEL_ID
+ client.model_id = _AUDIOCPP_MODEL_ID
client.preset_mode = preset_mode
client.voice = voice
client.language = language
@@ -1246,7 +1186,7 @@ class AudioCppTTSClientRequestTests(unittest.TestCase):
self.assertEqual(request.full_url,
"http://127.0.0.1:8080/v1/audio/speech")
payload = json.loads(request.data.decode("utf-8"))
- self.assertEqual(payload["model"], config.AUDIOCPP_MODEL_ID)
+ self.assertEqual(payload["model"], _AUDIOCPP_MODEL_ID)
self.assertEqual(payload["input"], "Hello world.")
self.assertEqual(payload["voice"], "narrator")
self.assertEqual(payload["language"], "Japanese")
@@ -1270,16 +1210,17 @@ class AudioCppTTSClientRequestTests(unittest.TestCase):
timeout = mock_urlopen.call_args[1]["timeout"]
self.assertEqual(timeout, config.API_TIMEOUT)
- def test_speaker_mode_sends_instruct(self):
+ def test_speaker_mode_without_instructions_omits_the_field(self):
+ # There is no configured style instruction: speaker mode sends no
+ # instructions field unless the run provides one.
client = self._make_client(preset_mode=False)
with patch("converter.clients.faster.urllib.request.urlopen",
return_value=self._post_response(self._wav_bytes())) as mock_urlopen:
client._request_wav("Hello.")
payload = json.loads(mock_urlopen.call_args[0][0].data.decode("utf-8"))
- self.assertEqual(payload["instructions"], config.INSTRUCT)
+ self.assertNotIn("instructions", payload)
- def test_explicit_instructions_replace_config_instruct(self):
- # --instructions overrides the INSTRUCT default in speaker mode.
+ def test_explicit_instructions_reach_the_payload(self):
client = self._make_client(preset_mode=False,
instructions="Read whisper quiet.")
with patch("converter.clients.faster.urllib.request.urlopen",
@@ -1677,7 +1618,7 @@ class AudioCppHeartbeatTests(unittest.TestCase):
client = AudioCppTTSClient.__new__(AudioCppTTSClient)
client.chunks_dir = Path(self._tmp.name)
client.api_url = "http://127.0.0.1:8080"
- client.model_id = config.AUDIOCPP_MODEL_ID
+ client.model_id = _AUDIOCPP_MODEL_ID
client.preset_mode = False
client.voice = "Vivian"
client.language = "English"
@@ -1730,7 +1671,7 @@ class AudioCppTTSClientTruncationTests(unittest.TestCase):
client = AudioCppTTSClient.__new__(AudioCppTTSClient)
client.chunks_dir = Path(self._tmp.name)
client.api_url = "http://127.0.0.1:8080"
- client.model_id = config.AUDIOCPP_MODEL_ID
+ client.model_id = _AUDIOCPP_MODEL_ID
client.preset_mode = True
client.voice = "narrator"
client.language = "English"
@@ -1830,7 +1771,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase):
def test_connect_unloads_before_returning(self):
client = AudioCppTTSClient.__new__(AudioCppTTSClient)
client.api_url = "http://127.0.0.1:8080"
- client.model_id = config.AUDIOCPP_MODEL_ID
+ client.model_id = _AUDIOCPP_MODEL_ID
client.preset_mode = True
client.voice = "narrator"
client.language = "English"
@@ -1848,7 +1789,6 @@ class AudioCppUnloadModelsTests(unittest.TestCase):
"family": "qwen3_tts",
"task": "tts"}]), \
patch.object(client, "_auto_pick_model_id"), \
- patch.object(client, "_select_model"), \
patch.object(client, "_require_model_id"), \
patch.object(client, "_resolve_family"), \
patch.object(client, "_resolve_task"), \
@@ -1860,7 +1800,7 @@ class AudioCppUnloadModelsTests(unittest.TestCase):
def test_connect_skips_unload_when_disabled(self):
client = AudioCppTTSClient.__new__(AudioCppTTSClient)
client.api_url = "http://127.0.0.1:8080"
- client.model_id = config.AUDIOCPP_MODEL_ID
+ client.model_id = _AUDIOCPP_MODEL_ID
client.preset_mode = True
client.voice = "narrator"
client.language = "English"
@@ -1878,7 +1818,6 @@ class AudioCppUnloadModelsTests(unittest.TestCase):
"family": "qwen3_tts",
"task": "tts"}]), \
patch.object(client, "_auto_pick_model_id"), \
- patch.object(client, "_select_model"), \
patch.object(client, "_require_model_id"), \
patch.object(client, "_resolve_family"), \
patch.object(client, "_resolve_task"), \
@@ -1962,8 +1901,9 @@ class BackendWiringTests(unittest.TestCase):
patch("converter.converter.QwenTTSClient") as mock_qwen, \
patch("converter.converter.AudioCppTTSClient") as mock_audiocpp:
AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM,
- backend=BACKEND_QWEN)
- mock_qwen.assert_called_once()
+ backend=BACKEND_QWEN, voice="Vivian")
+ _, kwargs = mock_qwen.call_args
+ self.assertEqual(kwargs["voice"], "Vivian")
mock_faster.assert_not_called()
mock_audiocpp.assert_not_called()
@@ -2016,14 +1956,14 @@ class BackendWiringTests(unittest.TestCase):
quiet=False)
with patch("converter.converter.QwenTTSClient") as mock_qwen:
AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM,
- backend=BACKEND_QWEN,
+ backend=BACKEND_QWEN, voice="Vivian",
api_url="http://10.0.0.5:7860")
mock_qwen.assert_called_once_with(
chunks_dir=converter_mod.CHUNKS_FOLDER,
voice_mode=VOICE_MODE_CUSTOM, voice_clone_ref_audio=None,
voice_clone_ref_text=None, skip_transcription=False,
language=config.LANGUAGE, instructions=None,
- api_url="http://10.0.0.5:7860", quiet=False)
+ api_url="http://10.0.0.5:7860", quiet=False, voice="Vivian")
def test_audiocpp_clone_mode_does_not_require_reference(self):
# Cloning is server-side for the audiocpp backend, so the
@@ -2045,7 +1985,8 @@ class BackendWiringTests(unittest.TestCase):
def test_chapter_chunks_qwen_always_splits(self):
with patch("converter.converter.QwenTTSClient"):
converter = AudiobookConverter(voice_mode=VOICE_MODE_CUSTOM,
- backend=BACKEND_QWEN)
+ backend=BACKEND_QWEN,
+ voice="Vivian")
text = " ".join(f"word{i}" for i in range(50))
with patch.object(config, "CHUNK_SIZE", 10):
chunks = converter._chapter_chunks(text)
@@ -2070,27 +2011,32 @@ class BackendWiringTests(unittest.TestCase):
return AudiobookConverter(voice_mode=VOICE_MODE_CLONE,
backend=BACKEND_FASTER, voice=voice)
- def _audiocpp_converter(self, voice=None):
+ def _audiocpp_converter(self, voice=None, instructions=None):
with patch("converter.converter.AudioCppTTSClient"):
return AudiobookConverter(
voice_mode=VOICE_MODE_CLONE if voice else VOICE_MODE_CUSTOM,
- backend=BACKEND_AUDIOCPP, voice=voice)
+ backend=BACKEND_AUDIOCPP, voice=voice,
+ instructions=instructions)
def test_narrator_tag_uses_faster_voice_name(self):
converter = self._faster_converter(voice="male_richard_poe")
self.assertEqual(converter._narrator_tag(), "male_richard_poe")
- def test_narrator_tag_falls_back_to_config_voice(self):
+ def test_narrator_tag_faster_without_voice_uses_default_key(self):
+ # Unreachable in a valid run (--voice is required); the tag stays
+ # stable for pre-flights of runs that will fail client-side.
converter = self._faster_converter()
- self.assertEqual(converter._narrator_tag(), config.FASTER_VOICE)
+ self.assertEqual(converter._narrator_tag(), "default")
def test_narrator_tag_audiocpp_uses_voice_name(self):
converter = self._audiocpp_converter(voice="female_narrator")
self.assertEqual(converter._narrator_tag(), "female_narrator")
- def test_narrator_tag_audiocpp_falls_back_to_speaker(self):
+ def test_narrator_tag_audiocpp_without_voice_uses_fallback(self):
+ # Unreachable in a valid run (the client refuses a speaker-capable
+ # entry without --voice); the tag stays stable for pre-flights.
converter = self._audiocpp_converter()
- self.assertEqual(converter._narrator_tag(), "Vivian")
+ self.assertEqual(converter._narrator_tag(), "narrator")
def test_banner_and_narrator_work_without_reference_audio(self):
converter = self._faster_converter(voice="male_richard_poe")
@@ -2100,7 +2046,7 @@ class BackendWiringTests(unittest.TestCase):
def test_audiocpp_banner_prints_without_reference_audio(self):
converter = self._audiocpp_converter(voice="narrator")
converter._print_banner() # must not raise
- converter = self._audiocpp_converter()
+ converter = self._audiocpp_converter(instructions="Calm and warm.")
converter._print_banner()
def test_audiocpp_banner_prints_model_family(self):