diff options
| author | historia <historiavg@proton.me> | 2026-08-28 14:57:48 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-28 14:57:48 -0400 |
| commit | db38085d07ce75f8961eecdc1919e98748254c53 (patch) | |
| tree | beaea2ba84de05bfc2fa90e9b92a24568d3b4cb9 /app/converter/clients | |
| parent | afb2c2d5b297c5aa28bcced0e3f90e207d799c2a (diff) | |
| download | tts-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__.py | 4 | ||||
| -rw-r--r-- | app/converter/clients/audiocpp.py | 166 | ||||
| -rw-r--r-- | app/converter/clients/faster.py | 8 | ||||
| -rw-r--r-- | app/converter/clients/qwen.py | 29 | ||||
| -rw-r--r-- | app/converter/clients/speakers.py | 15 |
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) |
