aboutsummaryrefslogtreecommitdiff
path: root/app/converter/clients
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 14:57:48 -0400
committerhistoria <historiavg@proton.me>2026-08-28 14:57:48 -0400
commitdb38085d07ce75f8961eecdc1919e98748254c53 (patch)
treebeaea2ba84de05bfc2fa90e9b92a24568d3b4cb9 /app/converter/clients
parentafb2c2d5b297c5aa28bcced0e3f90e207d799c2a (diff)
downloadtts-audiobook-generator-db38085d07ce75f8961eecdc1919e98748254c53.tar.gz
refactor: overhaul config.py, remove cli default options
Diffstat (limited to 'app/converter/clients')
-rw-r--r--app/converter/clients/__init__.py4
-rw-r--r--app/converter/clients/audiocpp.py166
-rw-r--r--app/converter/clients/faster.py8
-rw-r--r--app/converter/clients/qwen.py29
-rw-r--r--app/converter/clients/speakers.py15
5 files changed, 76 insertions, 146 deletions
diff --git a/app/converter/clients/__init__.py b/app/converter/clients/__init__.py
index d02fd9f..16fc99c 100644
--- a/app/converter/clients/__init__.py
+++ b/app/converter/clients/__init__.py
@@ -19,7 +19,7 @@ from .base import BaseTTSClient, ConversionCancelled, VOICE_MODE_CLONE, \
from .languages import LANGUAGE_CHOICES, LANGUAGE_ISO_CODES, TTS_LANGUAGES, \
TTS_LANGUAGE_ALIASES, normalize_language
from .speakers import QWEN3_TTS_SPEAKERS, SPEAKER_DISPLAY_NAMES, \
- is_builtin_speaker, speaker_display_name, speaker_display_name_for
+ is_builtin_speaker, speaker_display_name_for
from .transcribe import (transcribe_reference_audio,
transcribe_reference_audio_detailed,
whisper_backend_available, whisper_backend_problem)
@@ -57,7 +57,7 @@ __all__ = [
"LANGUAGE_CHOICES", "normalize_language",
# speakers
"QWEN3_TTS_SPEAKERS", "SPEAKER_DISPLAY_NAMES",
- "speaker_display_name", "speaker_display_name_for", "is_builtin_speaker",
+ "speaker_display_name_for", "is_builtin_speaker",
# transcription
"transcribe_reference_audio", "transcribe_reference_audio_detailed",
"whisper_backend_available", "whisper_backend_problem",
diff --git a/app/converter/clients/audiocpp.py b/app/converter/clients/audiocpp.py
index 8c446d3..da8d364 100644
--- a/app/converter/clients/audiocpp.py
+++ b/app/converter/clients/audiocpp.py
@@ -16,8 +16,7 @@ from ..chunking import split_into_chunks
from .base import (BaseTTSClient, ConversionCancelled,
NonRetryableTTSError, resolve_request_seed)
from .languages import LANGUAGE_ISO_CODES, normalize_language
-from .speakers import (is_builtin_speaker, speaker_display_name,
- speaker_display_name_for)
+from .speakers import is_builtin_speaker, speaker_display_name_for
logger = logging.getLogger(__name__)
@@ -215,23 +214,18 @@ class AudioCppTTSClient(BaseTTSClient):
its voice is supplied; all three are resolved server-side from the
request's "voice"/"instructions" fields:
- - Speaker mode (--voice with a built-in speaker name, or no flag on a
- CustomVoice entry): Qwen3-TTS CustomVoice only. A built-in speaker
- name (e.g. "Vivian") is passed through, plus the INSTRUCT style
- prompt. The selected entry must be the CustomVoice model (capability
+ - Speaker mode (--voice with a built-in speaker name): Qwen3-TTS
+ CustomVoice only. A built-in speaker name (e.g. "Vivian") is passed
+ through. The selected entry must be the CustomVoice model (capability
== speaker); a speaker name on a non-speaker entry is treated as a
server-side preset instead.
- Preset mode (--voice NAME): a voice configured on the server
(``voice_presets`` or ``voice_dir`` in its config, e.g. a cloning
reference). The name is validated against GET /v1/audio/voices at
startup because an unresolvable name would silently fall back to
- plain TTS on a clone-based model instead of failing. When
- AUDIOCPP_CLONE_MODEL_ID names a second server entry of the same
- family (typically the Qwen Base model), preset requests are routed
- to it. Selecting a non-speaker --voice on a CustomVoice primary with
- a clone id configured is the documented way to switch a speaker setup
- to cloning; without a clone id the voice is validated against the
- server's voice library.
+ plain TTS on a clone-based model instead of failing. To clone on a
+ Qwen3-TTS setup, select the Base model entry with --model and pass
+ a preset voice.
- Voice design (task "vdes" entries, e.g. Qwen3-TTS VoiceDesign): the
voice is described in natural language through ``instructions``,
which is required and sent with every request (no ``voice`` field).
@@ -240,12 +234,13 @@ class AudioCppTTSClient(BaseTTSClient):
The entry's capability decides how an explicit --voice is read: on a
speaker-capable entry a name that matches a built-in speaker selects
- speaker mode, and every other name is a server-side preset. With no
- --voice the entry's capability picks the mode: design entries require
- --instructions; speaker entries use the built-in CustomVoice speaker
- in config.SPEAKER; clone entries (the Base model, and every other
- family) fail fast with a hint to pass --voice, instead of silently
- synthesizing with a random default voice.
+ speaker mode, and every other name is a server-side preset. The
+ entry's capability picks the mode: design entries require
+ --instructions; speaker entries require --voice naming a built-in
+ CustomVoice speaker; clone entries (the Base model, and every other
+ family) require --voice with a server-side preset — all fail fast
+ with a hint instead of silently synthesizing with a random default
+ voice.
``instructions`` also works on non-design entries, where it acts as a
generic style/delivery instruction (voice control): families that read
@@ -272,11 +267,11 @@ class AudioCppTTSClient(BaseTTSClient):
quiet: bool = False):
super().__init__(chunks_dir, quiet=quiet)
self.api_url = (api_url or config.AUDIOCPP_API_URL).rstrip("/")
- # Per-run model selection: the --model CLI flag overrides config; an
- # empty value is resolved at connect time when the server hosts exactly
- # one entry, so multi-model servers don't require editing config.py.
- self.model_id = (model_id if model_id is not None
- else config.AUDIOCPP_MODEL_ID) or ""
+ # Per-run model selection: the --model CLI flag (or the Generate
+ # form's Model pick). An empty value is resolved at connect time
+ # when the server hosts exactly one entry, so single-model servers
+ # don't require --model.
+ self.model_id = (model_id or "").strip()
self._model_id_explicit = bool(self.model_id)
# Validate before connecting so bad values fail fast without a server.
self.language = normalize_language(
@@ -290,26 +285,23 @@ class AudioCppTTSClient(BaseTTSClient):
# resolved in _connect: a --voice that names a built-in CustomVoice
# speaker on a speaker-capable entry selects speaker mode; every
# other name (and any name on a clone-capable entry) is a server-side
- # preset. preset_mode gates _select_model's reroute to
- # AUDIOCPP_CLONE_MODEL_ID and the INSTRUCT style-prompt logic. The
- # request's "voice" field (self.voice) is filled in _connect per the
- # mode.
+ # preset. The request's "voice" field (self.voice) is filled in
+ # _connect per the mode.
self.preset_mode = False
self.speaker_mode = False
self.voice = voice or None
# Style/voice-design instruction sent with every request (the CLI
- # --instructions flag overrides AUDIOCPP_INSTRUCTIONS in config.py).
+ # --instructions flag / the Generate form's Instructions field).
# For task "vdes" entries it describes the voice to design; for other
# families it is a generic style instruction when the model reads one.
- self.instructions = (instructions if instructions is not None
- else config.AUDIOCPP_INSTRUCTIONS or "").strip()
+ self.instructions = (instructions or "").strip()
# Free-form per-request options (--option KEY=VALUE) forwarded in the
# request's "options" object; models ignore keys they don't know.
self.request_options: Dict[str, str] = dict(request_options or {})
# Set during _connect: design_mode for "vdes" entries, instruction_voice
# when a family without built-in speakers gets its voice from the
# instruction alone (no voice field). self.voice is also finalized
- # there (the speaker/preset name, or config.SPEAKER for the default).
+ # there (the speaker/preset name).
self.design_mode = False
self.instruction_voice = False
# Family and task of the selected model entry and the family's request
@@ -337,10 +329,9 @@ class AudioCppTTSClient(BaseTTSClient):
mode. An explicit --voice on a speaker-capable (CustomVoice) entry
that names a built-in speaker selects speaker mode; every other
--voice is a server-side preset, validated against the server's
- voice library (and rerouted to AUDIOCPP_CLONE_MODEL_ID when set).
- With no --voice, design entries require --instructions, speaker-
- capable entries use the built-in config.SPEAKER, and clone entries
- fail fast with a hint instead of silently synthesizing with a
+ voice library. Without a --voice, design entries require
+ --instructions and every other capability requires --voice — the
+ run fails fast with a hint instead of silently synthesizing with a
random default voice.
"""
self._check_health()
@@ -351,8 +342,7 @@ class AudioCppTTSClient(BaseTTSClient):
# preset. A name matching a built-in CustomVoice speaker on a
# speaker-capable primary selects speaker mode; every other name
# (and any name when the primary entry is absent) is a preset,
- # validated against the server's voice library and rerouted to
- # AUDIOCPP_CLONE_MODEL_ID when configured.
+ # validated against the server's voice library.
primary = next((m for m in models if m["id"] == self.model_id),
None)
if primary is not None:
@@ -367,10 +357,8 @@ class AudioCppTTSClient(BaseTTSClient):
self.speaker_mode = True
self._connected(f"speaker '{self.voice}'")
if not self.speaker_mode:
- # Server-side preset (--voice): validate it and route to
- # the clone model entry when AUDIOCPP_CLONE_MODEL_ID is set.
+ # Server-side preset (--voice): validate it.
self.preset_mode = True
- self._select_model(models)
self._require_model_id(models)
self._resolve_family(models)
self._resolve_task(models)
@@ -401,10 +389,13 @@ class AudioCppTTSClient(BaseTTSClient):
self._connected("voice design")
self._report(f"[INFO] Designing the voice from: {self.instructions}")
elif capability == AUDIOCPP_VOICE_SPEAKER:
- # No flag on a CustomVoice entry: the built-in config.SPEAKER.
- self.voice = speaker_display_name()
- self.speaker_mode = True
- self._connected(f"speaker '{self.voice}'")
+ # No --voice on a CustomVoice entry: refuse instead of
+ # guessing a built-in speaker.
+ raise RuntimeError(
+ f"The audio.cpp model '{self.model_id}' (family "
+ f"'{self.family}') serves built-in speakers: pass "
+ "--voice NAME with one of them (e.g. Vivian, Ryan, "
+ "Uncle Fu) to synthesize with it (see README).")
elif self.instructions:
# Families without built-in speakers can still get their voice
# from the instruction alone (e.g. OmniVoice voice design).
@@ -523,11 +514,8 @@ class AudioCppTTSClient(BaseTTSClient):
def _auto_pick_model_id(self, models: List[Dict[str, str]]) -> None:
"""Resolve an empty model id when the server hosts exactly one entry.
- Multi-model servers generated with several lazily-loaded entries can
- be used without editing app/converter/config.py: leave AUDIOCPP_MODEL_ID
- (and ``--model``) unset, and the single hosted entry is chosen
- automatically. With more than one entry an explicit choice is required
- (via ``--model`` or AUDIOCPP_MODEL_ID), since guessing would risk
+ Multi-model servers generated with several lazily-loaded entries
+ need an explicit ``--model``, since guessing would risk
synthesizing a whole book with the wrong family.
"""
if self.model_id:
@@ -535,22 +523,16 @@ class AudioCppTTSClient(BaseTTSClient):
if len(models) == 1:
self.model_id = models[0]["id"]
logger.info(
- "AUDIOCPP_MODEL_ID is unset; using the only server entry '%s'",
+ "No --model given; using the only server entry '%s'",
self.model_id)
else:
logger.debug(
- "AUDIOCPP_MODEL_ID is unset and the server hosts %d entries; "
- "an explicit --model or config id is required",
+ "No --model given and the server hosts %d entries; "
+ "an explicit --model is required",
len(models))
def _require_model_id(self, models: List[Dict[str, str]]) -> None:
- """Verify the model id chosen for this run exists on the server.
-
- Speaker mode needs AUDIOCPP_MODEL_ID (the CustomVoice entry).
- Preset mode validates whichever id _select_model resolved, so a
- server hosting only a cloning model works for --voice. The default
- error distinguishes the two so the fix is obvious.
- """
+ """Verify the model id chosen for this run exists on the server."""
model_ids = [model["id"] for model in models]
if self.model_id and self.model_id in model_ids:
return
@@ -559,19 +541,14 @@ class AudioCppTTSClient(BaseTTSClient):
raise RuntimeError(
f"The audio.cpp server at {self.api_url} hosts {len(model_ids)} "
f"model entries ({configured}); audiobook.py needs to know which "
- "one to use. Pass --model <id> when converting, or set "
- "AUDIOCPP_MODEL_ID in app/converter/config.py to one of them "
- "(see README)."
+ "one to use. Pass --model <id> when converting (see README)."
)
if self.preset_mode:
raise RuntimeError(
f"The audio.cpp server at {self.api_url} has no model id "
- f"'{self.model_id}' or clone model id "
- f"'{config.AUDIOCPP_CLONE_MODEL_ID}' (configured: {configured}). "
- "Add a TTS model entry for the family you want to the server "
- "config and match AUDIOCPP_MODEL_ID / AUDIOCPP_CLONE_MODEL_ID "
- "in app/converter/config.py to its id, or select it per run with "
- "--model (see README)."
+ f"'{self.model_id}' (configured: {configured}). Pass "
+ "--model <id> naming one of the hosted TTS model entries "
+ "(see README)."
)
raise RuntimeError(
f"The audio.cpp server at {self.api_url} has no model id "
@@ -581,52 +558,6 @@ class AudioCppTTSClient(BaseTTSClient):
"on any TTS model (see README)."
)
- def _select_model(self, models: List[Dict[str, str]]) -> None:
- """Pick the model for preset (cloning) requests.
-
- Defaults to the primary model id. When AUDIOCPP_CLONE_MODEL_ID is
- configured and present on the server, preset requests are routed
- to it instead, so one server can host the CustomVoice model for
- speaker mode and the Base model for cloning (Qwen3-TTS setups).
- A clone id that names a model of a different family is ignored
- with a warning, since preset requests must synthesize with the
- family the run is configured for.
- """
- clone_model_id = config.AUDIOCPP_CLONE_MODEL_ID
- if not clone_model_id or clone_model_id == self.model_id:
- return
- families = {model["id"]: model["family"] for model in models}
- if clone_model_id not in families:
- # A qwen3_tts primary without its clone entry silently degrades
- # (presets are ignored on the CustomVoice model), so that case
- # keeps the warning; single-model servers of other families are
- # the normal configuration and only get a debug note.
- primary_family = families.get(self.model_id) or ""
- if primary_family == AUDIOCPP_FAMILY_QWEN3_TTS:
- logger.warning(
- "AUDIOCPP_CLONE_MODEL_ID %r is not configured on the audio.cpp "
- "server; preset requests use '%s' instead",
- clone_model_id, self.model_id)
- else:
- logger.debug(
- "AUDIOCPP_CLONE_MODEL_ID %r is not configured on the audio.cpp "
- "server; preset requests use '%s' instead",
- clone_model_id, self.model_id)
- return
- primary_family = families.get(self.model_id)
- clone_family = families[clone_model_id]
- if primary_family and clone_family and primary_family != clone_family:
- logger.warning(
- "AUDIOCPP_CLONE_MODEL_ID %r hosts family %r, but "
- "AUDIOCPP_MODEL_ID %r hosts %r; preset requests stay on "
- "'%s'. Point both ids at the same model entry in "
- "app/converter/config.py (single-model servers use the same id "
- "for both)",
- clone_model_id, clone_family, self.model_id, primary_family,
- self.model_id)
- return
- self.model_id = clone_model_id
-
def _resolve_family(self, models: List[Dict[str, str]]) -> None:
"""Resolve the selected model's family and its request profile.
@@ -730,11 +661,6 @@ class AudioCppTTSClient(BaseTTSClient):
# Explicit voice-design or style instruction (required for task
# "vdes" entries; a Ctrl/style control on families that read it).
payload["instructions"] = self.instructions
- elif not self.preset_mode and config.INSTRUCT \
- and self.profile.sends_instructions:
- # Style instruction for the Qwen3-TTS CustomVoice speakers;
- # ignored by the Base (cloning) model and other families.
- payload["instructions"] = config.INSTRUCT
if self.request_options:
# Generic per-model controls (--option KEY=VALUE): forwarded
# verbatim; the model ignores keys it does not know.
diff --git a/app/converter/clients/faster.py b/app/converter/clients/faster.py
index f0874e0..48c26ab 100644
--- a/app/converter/clients/faster.py
+++ b/app/converter/clients/faster.py
@@ -33,7 +33,13 @@ class FasterTTSClient(BaseTTSClient):
voice: Optional[str] = None, api_url: Optional[str] = None,
quiet: bool = False):
super().__init__(chunks_dir, quiet=quiet)
- self.voice = voice or config.FASTER_VOICE
+ # The voice is per-run (--voice / the Generate form's Voice pick);
+ # there is no configured default.
+ self.voice = (voice or "").strip()
+ if not self.voice:
+ raise RuntimeError(
+ "The faster backend requires a voice: pass --voice NAME "
+ "naming a key in the server's voices.json (see README).")
self.api_url = (api_url or config.FASTER_API_URL).rstrip("/")
self._check_health()
diff --git a/app/converter/clients/qwen.py b/app/converter/clients/qwen.py
index ed3149b..17f14c5 100644
--- a/app/converter/clients/qwen.py
+++ b/app/converter/clients/qwen.py
@@ -15,7 +15,7 @@ from .base import (BaseTTSClient, ConversionCancelled, resolve_request_seed,
VOICE_MODE_CLONE, VOICE_MODE_CUSTOM, VOICE_MODE_DESIGN,
VOICE_MODES)
from .languages import normalize_language
-from .speakers import speaker_display_name
+from .speakers import QWEN3_TTS_SPEAKERS, speaker_display_name_for
logger = logging.getLogger(__name__)
@@ -32,7 +32,8 @@ class QwenTTSClient(BaseTTSClient):
voice_mode: str = "custom_voice", voice_clone_ref_audio: Optional[str] = None,
voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False,
language: Optional[str] = None, api_url: Optional[str] = None,
- instructions: Optional[str] = None, quiet: bool = False):
+ instructions: Optional[str] = None, quiet: bool = False,
+ voice: Optional[str] = None):
super().__init__(chunks_dir, quiet=quiet)
if voice_mode not in VOICE_MODES:
raise ValueError(
@@ -42,11 +43,17 @@ class QwenTTSClient(BaseTTSClient):
self.voice_clone_ref_audio = voice_clone_ref_audio
self.voice_clone_ref_text = (voice_clone_ref_text or "").strip()
self.skip_transcription = skip_transcription
+ # Built-in CustomVoice speaker (VOICE_MODE_CUSTOM): the --voice
+ # value / the Generate form's Speaker pick. Required there — there
+ # is no configured default speaker.
+ self.speaker = (voice or "").strip() or None
+ if voice_mode == VOICE_MODE_CUSTOM and not self.speaker:
+ raise ValueError(
+ "CustomVoice mode requires a speaker: pass --voice SPEAKER "
+ f"(one of {', '.join(QWEN3_TTS_SPEAKERS)})")
# Voice design / style instruction (VoiceDesign mode): describes the
- # voice to design. Defaults to the configured CustomVoice INSTRUCT so
- # a run never sends an empty design prompt.
- self.instructions = (instructions if instructions is not None
- else config.INSTRUCT).strip()
+ # voice to design. Required there (validated by the converter).
+ self.instructions = (instructions or "").strip()
# api_url overrides the configured endpoint for the active voice mode
# (used by the hub's "[remote]" backend entries and --api-url).
self.api_url = (api_url or "").strip() or None
@@ -261,21 +268,19 @@ class QwenTTSClient(BaseTTSClient):
# ------------------------------------------------------------------
def _generate_custom_voice(self, text: str) -> Tuple:
- """Generate audio using CustomVoice mode."""
+ """Generate audio using CustomVoice mode with the run's speaker."""
custom_api = self._resolve_api_name("/run_instruct", "/run_custom_voice", "/generate_custom_voice")
if custom_api == "/run_instruct":
payload = dict(
text=text,
lang_disp=self.language,
- spk_disp=speaker_display_name(),
- instruct=config.INSTRUCT,
+ spk_disp=speaker_display_name_for(self.speaker),
)
else:
payload = dict(
text=text,
language=self.language,
- speaker=config.SPEAKER,
- instruct=config.INSTRUCT,
+ speaker=self.speaker,
)
if self._endpoint_accepts_param(custom_api, "model_id_cv"):
payload["model_id_cv"] = CUSTOM_VOICE_MODEL_ID
@@ -323,7 +328,7 @@ class QwenTTSClient(BaseTTSClient):
clone_api = self._resolve_api_name("/run_voice_clone", "/generate_voice_clone",
api_info=self.clone_api_info)
- use_xvector = config.XVECTOR_ONLY or not self.voice_clone_ref_text
+ use_xvector = not self.voice_clone_ref_text
if clone_api == "/run_voice_clone":
payload = dict(
diff --git a/app/converter/clients/speakers.py b/app/converter/clients/speakers.py
index eecd52a..a68af68 100644
--- a/app/converter/clients/speakers.py
+++ b/app/converter/clients/speakers.py
@@ -2,13 +2,11 @@
from typing import Optional
-from .. import config
-
# Built-in CustomVoice speaker names for the Qwen3-TTS family. Shared by the
-# qwen-tts demo backend (config.SPEAKER, the qwen setup/form) and the
-# audio.cpp audiocpp backend's CustomVoice entry (the Convert form's Speaker
-# picker). Entries are the canonical/config form; speaker_display_name()
-# maps them to the wire (display) form via SPEAKER_DISPLAY_NAMES below.
+# qwen-tts demo backend (the qwen setup/form) and the audio.cpp audiocpp
+# backend's CustomVoice entry (the Convert form's Speaker picker). Entries
+# are the canonical form; speaker_display_name_for() maps them to the wire
+# (display) form via SPEAKER_DISPLAY_NAMES below.
QWEN3_TTS_SPEAKERS = ("Vivian", "Serena", "Uncle_Fu", "Dylan", "Eric",
"Ryan", "Aiden", "Ono_Anna", "Sohee")
@@ -50,8 +48,3 @@ def is_builtin_speaker(name: Optional[str]) -> bool:
norm = name.lower().replace("_", " ").replace("-", " ")
return any(norm == speaker.lower().replace("_", " ")
for speaker in QWEN3_TTS_SPEAKERS)
-
-
-def speaker_display_name() -> str:
- """Return the display name for the configured custom speaker."""
- return speaker_display_name_for(config.SPEAKER)