From f7021704b6b26ee747558d9ad701c2b25baedd2a Mon Sep 17 00:00:00 2001 From: historia Date: Fri, 21 Aug 2026 02:31:54 -0400 Subject: feat: catalog-driven audio.cpp server.json creation --- converter/config.py | 5 ++++- converter/converter.py | 6 ++++-- converter/tts.py | 51 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 9 deletions(-) (limited to 'converter') diff --git a/converter/config.py b/converter/config.py index 5cf4a8f..8e60250 100644 --- a/converter/config.py +++ b/converter/config.py @@ -62,6 +62,9 @@ AUDIOCPP_API_URL = "http://127.0.0.1:8080" # audio.cpp audiocpp_server # server-side voice preset. For single-model servers, set # AUDIOCPP_CLONE_MODEL_ID to the same id as AUDIOCPP_MODEL_ID (or leave it # empty); for Qwen3-TTS it typically names a second entry with the Base -# (cloning) model. +# (cloning) model. A multi-model server (one server.json hosting several +# lazily-loaded entries) does not need editing here: leave AUDIOCPP_MODEL_ID +# unset to auto-select when only one entry is hosted, or pick the entry per +# run with the --model CLI flag. AUDIOCPP_MODEL_ID = "qwen" AUDIOCPP_CLONE_MODEL_ID = "qwen-clone" diff --git a/converter/converter.py b/converter/converter.py index c5f1788..4da3626 100644 --- a/converter/converter.py +++ b/converter/converter.py @@ -138,7 +138,7 @@ class AudiobookConverter: speed: float = 1.0, single_file: bool = False, output_format: str = config.AUDIO_FORMAT, language: Optional[str] = None, backend: str = BACKEND_GRADIO, voice: Optional[str] = None, debug: bool = False, - chunk: bool = False): + chunk: bool = False, model_id: Optional[str] = None): if speed <= 0: raise ValueError(f"Speed must be a positive number, got {speed}") if output_format not in AUDIO_FORMATS: @@ -172,8 +172,10 @@ class AudiobookConverter: elif backend == BACKEND_AUDIOCPP: # Speaker mode (no voice) uses a built-in CustomVoice speaker; # an explicit voice selects a server-side preset (cloning). + # model_id overrides AUDIOCPP_MODEL_ID for multi-model servers. self.tts = AudioCppTTSClient(voice=voice, language=self.language, - chunk_text=self.client_chunks) + chunk_text=self.client_chunks, + model_id=model_id) else: self.tts = QwenTTSClient( voice_mode=voice_mode, diff --git a/converter/tts.py b/converter/tts.py index 0803bb9..83284a7 100644 --- a/converter/tts.py +++ b/converter/tts.py @@ -783,9 +783,15 @@ class AudioCppTTSClient(_BaseTTSClient): """ def __init__(self, voice: Optional[str] = None, language: Optional[str] = None, - api_url: Optional[str] = None, chunk_text: bool = False): + api_url: Optional[str] = None, chunk_text: bool = False, + model_id: Optional[str] = None): self.api_url = (api_url or config.AUDIOCPP_API_URL).rstrip("/") - self.model_id = config.AUDIOCPP_MODEL_ID + # 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 "" + self._model_id_explicit = bool(self.model_id) # Validate before connecting so bad values fail fast without a server. self.language = normalize_language( language if language is not None else config.LANGUAGE) @@ -820,6 +826,7 @@ class AudioCppTTSClient(_BaseTTSClient): """ self._check_health() models = self._list_models() + self._auto_pick_model_id(models) if self.preset_mode: self._select_model(models) self._require_model_id(models) @@ -894,6 +901,29 @@ class AudioCppTTSClient(_BaseTTSClient): }) return models + 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 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 + synthesizing a whole book with the wrong family. + """ + if self.model_id: + return + if len(models) == 1: + self.model_id = models[0]["id"] + logger.info( + "AUDIOCPP_MODEL_ID is unset; 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", + 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. @@ -902,9 +932,17 @@ class AudioCppTTSClient(_BaseTTSClient): server hosting only a cloning model works for --voice. """ model_ids = [model["id"] for model in models] - if self.model_id in model_ids: + if self.model_id and self.model_id in model_ids: return configured = ", ".join(model_ids) or "none" + if not self.model_id: + 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 when converting, or set " + "AUDIOCPP_MODEL_ID in converter/config.py to one of them " + "(see README)." + ) if self.preset_mode: raise RuntimeError( f"The audio.cpp server at {self.api_url} has no model id " @@ -912,15 +950,16 @@ class AudioCppTTSClient(_BaseTTSClient): 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 converter/config.py to its id (see README)." + "in converter/config.py to its id, or select it per run with " + "--model (see README)." ) raise RuntimeError( f"The audio.cpp server at {self.api_url} has no model id " f"'{self.model_id}' (configured: {configured}). Speaker mode needs " "the Qwen3-TTS CustomVoice model: add a qwen3_tts model entry to " "the server config and match AUDIOCPP_MODEL_ID in converter/config.py to its " - "id, or rerun with --voice to use a voice preset on any TTS " - "model (see README)." + "id (or pass --model), or rerun with --voice to use a voice preset " + "on any TTS model (see README)." ) def _select_model(self, models: List[Dict[str, str]]) -> None: -- cgit v1.2.3