diff options
Diffstat (limited to 'app/converter/clients/audiocpp.py')
| -rw-r--r-- | app/converter/clients/audiocpp.py | 166 |
1 files changed, 46 insertions, 120 deletions
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. |
