aboutsummaryrefslogtreecommitdiff
path: root/app/converter
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-31 19:45:57 -0400
committerhistoria <historiavg@proton.me>2026-08-31 19:45:57 -0400
commit10e72d4960e865acf5346ab8cf518ed5844fe45c (patch)
treeadf8c10386b9da6280c247f1fed137ef1a514157 /app/converter
parent4bd0282da65db9f118ef5250582ab67079fad538 (diff)
downloadtts-audiobook-generator-10e72d4960e865acf5346ab8cf518ed5844fe45c.tar.gz
feat: generate a book with all installed models to compare
Diffstat (limited to 'app/converter')
-rw-r--r--app/converter/clients/audiocpp.py17
-rw-r--r--app/converter/converter.py33
2 files changed, 42 insertions, 8 deletions
diff --git a/app/converter/clients/audiocpp.py b/app/converter/clients/audiocpp.py
index a1888bc..df1884d 100644
--- a/app/converter/clients/audiocpp.py
+++ b/app/converter/clients/audiocpp.py
@@ -406,7 +406,8 @@ class AudioCppTTSClient(BaseTTSClient):
model_id: Optional[str] = None,
instructions: Optional[str] = None,
request_options: Optional[Dict[str, str]] = None,
- quiet: bool = False):
+ quiet: bool = False,
+ unload_models: Optional[bool] = None):
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 (or the Generate
@@ -415,6 +416,12 @@ class AudioCppTTSClient(BaseTTSClient):
# don't require --model.
self.model_id = (model_id or "").strip()
self._model_id_explicit = bool(self.model_id)
+ # Unload previously-loaded server models at connect time: None
+ # follows the AUDIOCPP_UNLOAD_MODELS setting (read at connect, so
+ # a Settings change this session is honored); True/False force it
+ # regardless of the setting ("All (multiple generation)" runs pass
+ # True so each per-model conversion starts with a clean VRAM).
+ self._unload_models_override = unload_models
# Validate before connecting so bad values fail fast without a server.
self.language = normalize_language(
language if language is not None else config.LANGUAGE)
@@ -570,7 +577,10 @@ class AudioCppTTSClient(BaseTTSClient):
self._report(f"[INFO] Sending instruction with every request: {self.instructions}")
self._report("[INFO] Its effect (style, emotion, delivery) depends on the "
"model family; models without instruction support ignore it.")
- if config.AUDIOCPP_UNLOAD_MODELS:
+ unload = (config.AUDIOCPP_UNLOAD_MODELS
+ if self._unload_models_override is None
+ else self._unload_models_override)
+ if unload:
self._unload_server_models()
def _require_synthesis_task(self, models: List[Dict[str, str]]) -> None:
@@ -595,7 +605,8 @@ class AudioCppTTSClient(BaseTTSClient):
on its first request. Failures only warn: an older server without
the endpoint, or a busy one, must not block a working setup.
Controlled by config.AUDIOCPP_UNLOAD_MODELS (the TUI Settings
- "Unload models" option).
+ "Unload models" option), or forced per run via the unload_models
+ override ("All (multiple generation)" runs unload between models).
"""
request = urllib.request.Request(
f"{self.api_url}/v1/tasks/unload_all_models", data=b"",
diff --git a/app/converter/converter.py b/app/converter/converter.py
index bd5e477..a10a57d 100644
--- a/app/converter/converter.py
+++ b/app/converter/converter.py
@@ -212,6 +212,7 @@ class AudiobookConverter:
instructions: Optional[str] = None,
request_options: Optional[Dict[str, str]] = None,
api_url: Optional[str] = None,
+ unload_models: Optional[bool] = None,
progress: Optional[Callable[[dict], None]] = None,
cancel=None):
if speed <= 0:
@@ -243,6 +244,9 @@ class AudiobookConverter:
# them against the server-hosted model at connect time.
self.instructions = instructions
self.request_options = dict(request_options or {})
+ # audio.cpp only: force unloading previously-loaded server models
+ # at connect time (None follows the AUDIOCPP_UNLOAD_MODELS setting).
+ self.unload_models = unload_models
self._validate_configuration()
# Interactive reporting (the TUI run view): PROGRESS receives an
# event dict per state change and turns the clients' console prints
@@ -261,13 +265,15 @@ class AudiobookConverter:
# elsewhere. model_id picks the server entry per run
# (auto-selected on single-entry servers); instructions
# describe or style the voice, request_options pass
- # per-model controls through to the server.
+ # per-model controls through to the server. unload_models
+ # forces a pre-run model unload when not None ("All" runs).
self.tts = AudioCppTTSClient(chunks_dir=CHUNKS_FOLDER,
voice=voice, language=self.language,
model_id=model_id,
instructions=instructions,
request_options=self.request_options,
- api_url=api_url, quiet=quiet)
+ api_url=api_url, quiet=quiet,
+ unload_models=unload_models)
else:
# Qwen: the voice mode picks the request shape (built-in
# speaker, clone from a reference .wav, or a designed voice);
@@ -355,7 +361,6 @@ class AudiobookConverter:
voice_clone_ref_audio: Optional[str],
instructions: Optional[str] = None) -> str:
"""Narrator name used in output file names, without a server connection.
-
Custom voice mode uses the built-in speaker's display name; voice
clone mode uses the reference audio file's stem; the faster and
audiocpp backends use the server-side voice name (for audiocpp's
@@ -392,6 +397,19 @@ class AudiobookConverter:
return AudiobookConverter._sanitize_filename(
narrator, fallback="narrator").replace(" ", "_")
+ @staticmethod
+ def compute_model_tag(model_id: Optional[str]) -> str:
+ """Model id used in output file names, without a server connection.
+
+ "All (multiple generation)" runs name every output with the
+ generating model's id so the per-model files never collide
+ (e.g. ``dune_qwen3_tts_1_7b_base_q8_0_Vivian.m4b``). Pure (no I/O,
+ no server) so the pre-flight can compute the exact output names a
+ run would produce before spending time connecting to a TTS server.
+ """
+ return AudiobookConverter._sanitize_filename(
+ model_id or "", fallback="model").replace(" ", "_")
+
# ------------------------------------------------------------------
# Debug dumps (--debug)
# ------------------------------------------------------------------
@@ -804,6 +822,7 @@ class AudiobookConverter:
confirm: Optional[Callable[[str, bool], bool]] = None,
book_files: Optional[List[Path]] = None,
output_name: Optional[str] = None,
+ name_tag: Optional[str] = None,
) -> Tuple[List[Path], List[Tuple[Path, str]]]:
"""Discover books and ask every overwrite question up front.
@@ -822,7 +841,10 @@ class AudiobookConverter:
(a single --input-file book; still filtered to supported formats),
and OUTPUT_NAME overrides the computed output name with a verbatim
base name (--output-file's stem, no narrator tag or stem-collision
- suffix). Both default to the directory-scan behavior.
+ suffix). Both default to the directory-scan behavior. NAME_TAG, when
+ given, is inserted between the book stem and the narrator tag
+ ("All (multiple generation)" runs pass the sanitized model id, so
+ each model's outputs are named and planned separately).
"""
if book_files is None:
book_files = sorted(
@@ -854,7 +876,8 @@ class AudiobookConverter:
name = book_file.stem
if stem_counts[book_file.stem] > 1:
name = f"{book_file.stem}_{book_file.suffix.lstrip('.')}"
- names.append((book_file, f"{name}_{narrator_tag}"))
+ tag = f"{name_tag}_{narrator_tag}" if name_tag else narrator_tag
+ names.append((book_file, f"{name}_{tag}"))
# Ask every overwrite question up front, before any conversion
# starts, so the rest of the run is unattended.