aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_tts.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests/test_tts.py')
-rw-r--r--app/tests/test_tts.py372
1 files changed, 159 insertions, 213 deletions
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):