aboutsummaryrefslogtreecommitdiff
path: root/app/converter
diff options
context:
space:
mode:
Diffstat (limited to 'app/converter')
-rw-r--r--app/converter/clients/__init__.py4
-rw-r--r--app/converter/clients/audiocpp.py166
-rw-r--r--app/converter/clients/faster.py8
-rw-r--r--app/converter/clients/qwen.py29
-rw-r--r--app/converter/clients/speakers.py15
-rw-r--r--app/converter/config.py104
-rw-r--r--app/converter/converter.py50
7 files changed, 120 insertions, 256 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)
diff --git a/app/converter/config.py b/app/converter/config.py
index e37269b..3511033 100644
--- a/app/converter/config.py
+++ b/app/converter/config.py
@@ -3,119 +3,43 @@ AUDIO_FORMAT = "m4b"
AUDIO_BITRATE = "128k"
LANGUAGE = "English"
-API_TIMEOUT = 600 # Timeout per chunk request in seconds
-MAX_RETRIES = 3 # Attempts per chunk request
+API_TIMEOUT = 600 # Timeout per chunk request in seconds
+MAX_RETRIES = 3 # Attempts per chunk request
HEARTBEAT_INTERVAL_SECONDS = 30 # Print "still working" in console logs every N seconds
# Words per TTS generation request (client-side chunking).
CHUNK_SIZE = 250
# Where books are read from and where finished audiobooks are written.
-# Relative paths resolve against the project root (the folder containing
-# audiobook.py). The --input/--output CLI flags override these per run.
+# Relative paths resolve against the project root.
INPUT_DIR = "./input"
OUTPUT_DIR = "./output"
-# Playback speed factor for the final audiobook (1.0 = normal).
-# Pitch-preserving. The --speed CLI flag overrides this per run.
+# Output audiobook file at a different tempo.
SPEED = 1.0
-# Dump each chunk's raw audio and the exact text sent for it under the
-# debug/ folder (organized per book and chapter), and log every TTS
-# request and response to the console and log file. The --debug CLI flag
-# forces this on for a single run.
+# Dump each chunk's raw audio and the text sent for it into debug/
DEBUG = False
-# Default for "Stop server and exit" (TUI Settings menu: "Stop server
-# and exit"): automatically stop the TTS server and exit the TUI after
-# generating audiobooks.
+# Default option for stop and exit TTS server after generating with TUI
STOP_SERVER_AND_EXIT = True
-# Default TTS backend.
-# audiocpp: audiocpp_server
-# qwen: qwen-tts-demo
-# faster: faster-qwen-tts
-# The --backend CLI flag overrides this
-BACKEND = "audiocpp"
+# The IP/port that locally-managed TTS server instances run on
+QWEN_API_URL = "http://127.0.0.1:7860"
+FASTER_API_URL = "http://127.0.0.1:8000"
+AUDIOCPP_API_URL = "http://127.0.0.1:8080"
-###############################################################################
-# BACKEND 1: qwen-tts-demo (qwen) options #
-###############################################################################
-
-# The qwen backend runs ONE demo server at a time, on this port. Which model
-# the server hosts is chosen per run on the Generate Audiobooks screen and
-# persisted below (see QWEN_MODEL); switching models restarts the server.
-QWEN_API_URL = "http://127.0.0.1:7860" # single qwen-tts-demo server
-
-# Remote (externally-run) server URL. The hub probes it and offers a
-# "[remote]" backend entry when it answers with a known qwen-tts demo (any
-# of the three models), so an externally-started server can be used alongside
-# a locally-managed one. Leave empty to disable remote probing. The default
-# matches the local port so an external server squatting the local port is
-# found without any configuration.
+# The URI used to discover externally-run instances
QWEN_REMOTE_URL = "http://127.0.0.1:7860"
-
-# Which model the managed demo server runs (one server hosts one model):
-# CustomVoice - built-in speakers (see SPEAKER)
-# Base - voice cloning from a reference .wav
-# VoiceDesign - voice described by an instruction
-# Chosen per run in the Generate-audiobooks form; edited here only as the
-# default for the next run.
-QWEN_MODEL = "CustomVoice"
-
-# Custom voice options
-SPEAKER = "Vivian" #Vivian, Serena, Uncle_Fu, Dylan, Eric, Ryan, Aiden, Ono_Anna, Sohee
-# Style/delivery instruction for CustomVoice runs; also the default design
-# instruction when a VoiceDesign run does not override it.
-INSTRUCT = "Speak naturally and clearly, as if reading a dramatic book to an adult audience."
-
-# Don't clone with transcription, only use x-vector-only cloning. Generally "worse"
-XVECTOR_ONLY = False
+FASTER_REMOTE_URL = "http://127.0.0.1:8000"
+AUDIOCPP_REMOTE_URL = "http://127.0.0.1:8080"
# Randomization seed. -1 means randomize with every generation
# With SEED = -1 and CONSTANT_SEED = True, one random seed will be used for the entire audiobook.
-# This may keep the voice slightly more consistent across chunk boundaries
+# This MAY keep the voice slightly more consistent across chunk boundaries
SEED = -1
CONSTANT_SEED = False
-###############################################################################
-# BACKEND 2: faster-qwen-tts options #
-###############################################################################
-FASTER_API_URL = "http://127.0.0.1:8000" # faster-qwen3-tts server (Base model only)
-FASTER_REMOTE_URL = "http://127.0.0.1:8000" # externally-run faster-qwen3-tts server ("" disables probing)
-
-# Default voice if no --voice is passed
-FASTER_VOICE = "narrator"
-
-###############################################################################
-# BACKEND 3: audio.cpp options #
-###############################################################################
-AUDIOCPP_API_URL = "http://127.0.0.1:8082" # audio.cpp audiocpp_server
-AUDIOCPP_REMOTE_URL = "http://127.0.0.1:8080" # externally-run audiocpp_server ("" disables probing)
-
-# Model ids in the audio.cpp server.json config. AUDIOCPP_MODEL_ID may point
-# at any TTS model entry the server hosts; the family is detected from the
-# server at startup and adapts the request automatically. Only qwen3_tts has
-# built-in speakers (speaker mode); every other family needs --voice with a
-# server-side voice preset. The server entry id is the model package's
-# target_directory name (e.g. "Qwen3-TTS-12Hz-1.7B-Base-GGUF"). 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. Both default to empty so a
-# single-entry server is auto-selected; a multi-model server (one server.json
-# hosting several lazily-loaded entries) needs no editing here either: 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 = "Qwen3-TTS-12Hz-1.7B-Base-GGUF"
-AUDIOCPP_CLONE_MODEL_ID = "Qwen3-TTS-12Hz-1.7B-Base-GGUF"
-
-# Voice design / style instruction sent with every audio.cpp request when
-# the --instructions CLI flag is not given. Required for server entries
-# hosted with task "vdes" (voice design models such as Qwen3-TTS
-# VoiceDesign); on other families it acts as a style/delivery instruction
-# when the model supports one and is ignored otherwise. Empty by default.
-AUDIOCPP_INSTRUCTIONS = ""
-
# Ask the audio.cpp server to unload all currently loaded models before
# converting, so models left resident by earlier runs free their memory
# (e.g. VRAM on GPU backends) and only the selected entry loads. Set to
diff --git a/app/converter/converter.py b/app/converter/converter.py
index 5235991..a9ea7eb 100644
--- a/app/converter/converter.py
+++ b/app/converter/converter.py
@@ -31,7 +31,7 @@ from .clients import (
FasterTTSClient,
QwenTTSClient,
normalize_language,
- speaker_display_name,
+ speaker_display_name_for,
)
logger = logging.getLogger(__name__)
@@ -207,7 +207,7 @@ class AudiobookConverter:
def __init__(self, voice_mode: str = VOICE_MODE_CUSTOM, voice_clone_ref_audio: Optional[str] = None,
voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False,
speed: float = 1.0, single_file: bool = False, output_format: str = config.AUDIO_FORMAT,
- language: Optional[str] = None, backend: str = config.BACKEND,
+ language: Optional[str] = None, backend: str = None,
voice: Optional[str] = None, debug: bool = False,
model_id: Optional[str] = None,
instructions: Optional[str] = None,
@@ -219,6 +219,8 @@ class AudiobookConverter:
raise ValueError(f"Speed must be a positive number, got {speed}")
if output_format not in AUDIO_FORMATS:
raise ValueError(f"Unsupported output format: {output_format}")
+ if backend is None:
+ raise ValueError("backend is required (pass --backend)")
if backend not in BACKENDS:
raise ValueError(
f"Unknown backend: {backend!r} (expected one of {BACKENDS})"
@@ -257,10 +259,10 @@ class AudiobookConverter:
elif backend == BACKEND_AUDIOCPP:
# --voice picks the voice: a built-in speaker name on the
# CustomVoice entry, or a server-side preset (cloning)
- # elsewhere. model_id overrides AUDIOCPP_MODEL_ID for
- # multi-model servers; instructions describe or style the
- # voice, request_options pass per-model controls through to
- # the server.
+ # 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.
self.tts = AudioCppTTSClient(chunks_dir=CHUNKS_FOLDER,
voice=voice, language=self.language,
model_id=model_id,
@@ -270,6 +272,7 @@ class AudiobookConverter:
else:
# Qwen: the voice mode picks the request shape (built-in
# speaker, clone from a reference .wav, or a designed voice);
+ # --voice names the built-in speaker in speaker mode and
# instructions describe the voice in design mode.
self.tts = QwenTTSClient(
chunks_dir=CHUNKS_FOLDER,
@@ -281,6 +284,7 @@ class AudiobookConverter:
instructions=self.instructions,
api_url=api_url,
quiet=quiet,
+ voice=voice,
)
self._progress = progress
self.tts.cancel = cancel
@@ -314,6 +318,13 @@ class AudiobookConverter:
"Voice Design mode requires a voice description. "
"Use --instructions \"...\" to describe the voice to synthesize with."
)
+ if self.backend == BACKEND_QWEN and self.voice_mode == VOICE_MODE_CUSTOM \
+ and not (self.voice or "").strip():
+ raise ValueError(
+ "CustomVoice mode requires a speaker. Use --voice SPEAKER "
+ "(e.g. Vivian) to pick one, or --clone / --instructions "
+ "for the other voice modes."
+ )
if self.voice_mode == VOICE_MODE_CLONE and self.backend == BACKEND_QWEN:
if not self.voice_clone_ref_audio:
raise ValueError(
@@ -348,19 +359,18 @@ class AudiobookConverter:
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 (or, for audiocpp's
- speaker mode, the selected built-in CustomVoice speaker, falling back
- to the configured one). An instruction without a voice (voice design,
- or instruction-defined voices on families without built-in speakers)
- uses "designed". Spaces become underscores (e.g. "Uncle Fu" ->
- "Uncle_Fu").
+ audiocpp backends use the server-side voice name (for audiocpp's
+ speaker mode, the selected built-in CustomVoice speaker). An
+ instruction without a voice (voice design, or instruction-defined
+ voices on families without built-in speakers) uses "designed".
+ Spaces become underscores (e.g. "Uncle Fu" -> "Uncle_Fu").
Pure (no I/O, no server) so the pre-flight overwrite check can
compute the exact output names a run would produce before spending
time connecting to a TTS server.
"""
if backend == BACKEND_FASTER:
- narrator = voice or config.FASTER_VOICE
+ narrator = voice or "default"
elif backend == BACKEND_AUDIOCPP:
if voice:
narrator = voice
@@ -368,7 +378,10 @@ class AudiobookConverter:
# The voice comes from the instruction, not a speaker name.
narrator = "designed"
else:
- narrator = speaker_display_name()
+ # Unreachable in a valid run (the audiocpp client refuses a
+ # speaker-capable entry without --voice); keep a stable tag
+ # for the pre-flight of runs that will fail at connect time.
+ narrator = "narrator"
elif voice_mode == VOICE_MODE_DESIGN:
# Qwen's VoiceDesign model: the voice is described by an
# instruction and has no speaker name.
@@ -376,7 +389,7 @@ class AudiobookConverter:
elif voice_mode == VOICE_MODE_CLONE:
narrator = Path(voice_clone_ref_audio).stem
else:
- narrator = speaker_display_name()
+ narrator = speaker_display_name_for(voice or "")
return AudiobookConverter._sanitize_filename(
narrator, fallback="narrator").replace(" ", "_")
@@ -736,7 +749,7 @@ class AudiobookConverter:
if self.backend == BACKEND_FASTER:
self._say(f"Faster TTS endpoint: {config.FASTER_API_URL}")
self._say("Backend: faster (voice cloning, reference configured on server)")
- self._say(f"Voice: {self.voice or config.FASTER_VOICE}")
+ self._say(f"Voice: {self.voice}")
elif self.backend == BACKEND_AUDIOCPP:
self._say(f"audio.cpp endpoint: {config.AUDIOCPP_API_URL}")
self._say(f"Model id: {self.tts.model_id}")
@@ -750,9 +763,6 @@ class AudiobookConverter:
elif self.instructions:
self._say("Backend: audio.cpp (voice from --instructions description)")
self._say(f"Instruction: {self.instructions}")
- else:
- self._say("Backend: audio.cpp (custom voice, built-in speaker)")
- self._say(f"Speaker: {config.SPEAKER}")
if self.request_options:
self._say(f"Request options: {self.request_options}")
self._say(f"Language: {self.language}")
@@ -764,7 +774,7 @@ class AudiobookConverter:
self._say(f"Voice mode: {self.voice_mode}")
self._say(f"Model size: {MODEL_SIZE} (always)")
if self.voice_mode == VOICE_MODE_CUSTOM:
- self._say(f"Speaker: {config.SPEAKER}")
+ self._say(f"Speaker: {self.voice}")
self._say(f"Language: {self.language}")
elif self.voice_mode == VOICE_MODE_CLONE:
self._say(f"Reference audio: {Path(self.voice_clone_ref_audio).name}")