aboutsummaryrefslogtreecommitdiff
path: root/converter
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-21 02:31:54 -0400
committerhistoria <historiavg@proton.me>2026-08-21 02:31:54 -0400
commitf7021704b6b26ee747558d9ad701c2b25baedd2a (patch)
treefeda79e97ad0d6601558123bfe9093cfc4ac66e9 /converter
parentfea9222740da007f1d7befcd7dee035265c0e5d1 (diff)
downloadtts-audiobook-generator-f7021704b6b26ee747558d9ad701c2b25baedd2a.tar.gz
feat: catalog-driven audio.cpp server.json creation
Diffstat (limited to 'converter')
-rw-r--r--converter/config.py5
-rw-r--r--converter/converter.py6
-rw-r--r--converter/tts.py51
3 files changed, 53 insertions, 9 deletions
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 <id> 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: