aboutsummaryrefslogtreecommitdiff
path: root/app/converter
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-25 16:40:30 -0400
committerhistoria <historiavg@proton.me>2026-08-25 16:40:30 -0400
commit867866f131b0b6c76c54272791e7f7dea01db990 (patch)
treeb57ecdd66eeaf7ad73742d2f2bbe15d3a5498fa3 /app/converter
parentfca3431721a55277f139efc83df2438207917448 (diff)
downloadtts-audiobook-generator-867866f131b0b6c76c54272791e7f7dea01db990.tar.gz
feat: better tui menu option gating for models that support custom voices (qwen) and models that do not support instructions
Diffstat (limited to 'app/converter')
-rw-r--r--app/converter/converter.py43
-rw-r--r--app/converter/tts.py218
2 files changed, 185 insertions, 76 deletions
diff --git a/app/converter/converter.py b/app/converter/converter.py
index b2c2923..6e677c8 100644
--- a/app/converter/converter.py
+++ b/app/converter/converter.py
@@ -185,6 +185,7 @@ class AudiobookConverter:
instructions: Optional[str] = None,
request_options: Optional[Dict[str, str]] = None,
api_url: Optional[str] = None,
+ speaker: Optional[str] = None,
progress: Optional[Callable[[dict], None]] = None,
cancel=None):
if speed <= 0:
@@ -205,6 +206,7 @@ class AudiobookConverter:
self.output_format = output_format
self.backend = backend
self.voice = voice
+ self.speaker = speaker
self.debug = bool(debug)
# Voice design / style instruction and free-form request options
# (audio.cpp only): forwarded to AudioCppTTSClient, which validates
@@ -217,16 +219,18 @@ class AudiobookConverter:
# configured on the server, so no local reference audio is needed.
self.tts = FasterTTSClient(voice=voice, api_url=api_url)
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;
- # instructions describe or style the voice, request_options pass
- # per-model controls through to the server.
+ # Speaker mode (--speaker or no flag on a CustomVoice entry) uses
+ # a built-in speaker name; an explicit --voice selects a
+ # server-side preset (cloning). 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.
self.tts = AudioCppTTSClient(voice=voice, language=self.language,
model_id=model_id,
instructions=instructions,
request_options=self.request_options,
- api_url=api_url)
+ api_url=api_url,
+ speaker=speaker)
else:
self.tts = QwenTTSClient(
voice_mode=voice_mode,
@@ -290,22 +294,24 @@ class AudiobookConverter:
"""Narrator name used in output file names (see compute_narrator_tag)."""
return self.compute_narrator_tag(
self.backend, self.voice, self.voice_mode,
- self.voice_clone_ref_audio, self.instructions)
+ self.voice_clone_ref_audio, self.instructions, self.speaker)
@staticmethod
def compute_narrator_tag(backend: str, voice: Optional[str],
voice_mode: str,
voice_clone_ref_audio: Optional[str],
- instructions: Optional[str] = None) -> str:
+ instructions: Optional[str] = None,
+ speaker: 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 (falling back to
- the built-in speaker for the audiocpp backend's speaker mode). 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 (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").
Pure (no I/O, no server) so the pre-flight overwrite check can
compute the exact output names a run would produce before spending
@@ -316,6 +322,8 @@ class AudiobookConverter:
elif backend == BACKEND_AUDIOCPP:
if voice:
narrator = voice
+ elif speaker:
+ narrator = speaker
elif instructions:
# The voice comes from the instruction, not a speaker name.
narrator = "designed"
@@ -672,6 +680,9 @@ class AudiobookConverter:
if self.voice:
self._say("Backend: audio.cpp (voice cloning, reference configured on server)")
self._say(f"Voice: {self.voice}")
+ elif self.speaker:
+ self._say("Backend: audio.cpp (custom voice, built-in speaker)")
+ self._say(f"Speaker: {self.speaker}")
elif self.instructions:
self._say("Backend: audio.cpp (voice from --instructions description)")
self._say(f"Instruction: {self.instructions}")
@@ -715,6 +726,7 @@ class AudiobookConverter:
voice_clone_ref_audio: Optional[str],
output_format: str,
instructions: Optional[str] = None,
+ speaker: Optional[str] = None,
confirm: Optional[Callable[[str, bool], bool]] = None,
) -> Tuple[List[Path], List[Tuple[Path, str]]]:
"""Discover books and ask every overwrite question up front.
@@ -746,7 +758,8 @@ class AudiobookConverter:
# starts, so the rest of the run is unattended.
planned: List[Tuple[Path, str]] = []
narrator_tag = AudiobookConverter.compute_narrator_tag(
- backend, voice, voice_mode, voice_clone_ref_audio, instructions)
+ backend, voice, voice_mode, voice_clone_ref_audio, instructions,
+ speaker)
for book_file in book_files:
output_name = book_file.stem
if stem_counts[book_file.stem] > 1:
@@ -782,7 +795,7 @@ class AudiobookConverter:
book_files, planned = AudiobookConverter.preflight_overwrites(
self.backend, self.voice, self.voice_mode,
self.voice_clone_ref_audio, self.output_format,
- self.instructions)
+ self.instructions, self.speaker)
if not book_files:
self._say(f"[INFO] No supported files found in {BOOKS_FOLDER}")
diff --git a/app/converter/tts.py b/app/converter/tts.py
index 0667ec3..a88697e 100644
--- a/app/converter/tts.py
+++ b/app/converter/tts.py
@@ -121,32 +121,51 @@ AUDIOCPP_LANG_DISPLAY = "display" # Qwen display names, e.g. "English"
AUDIOCPP_LANG_ISO = "iso" # ISO 639-1 codes, e.g. "en"
AUDIOCPP_LANG_OMIT = "omit" # no language field; the model detects it
-# The only family with a built-in speaker mode (CustomVoice speaker names
-# plus the INSTRUCT style prompt). Every other family is clone-only: the
-# voice comes from a server-side preset requested with --voice.
+# The Qwen3-TTS family. Unlike every other family (one model type each),
+# qwen3_tts hosts several model *types* under one family id, distinguished
+# only by the server entry's id/task: the CustomVoice model (built-in
+# speakers, e.g. Vivian/Ryan), the Base model (voice cloning via a
+# server-side preset), and the VoiceDesign model (task "vdes"). The
+# per-entry voice capability below (audiocpp_entry_voice_capability)
+# resolves which is which, driving both the Convert form (which voice
+# list to show) and the converter's mode selection.
AUDIOCPP_FAMILY_QWEN3_TTS = "qwen3_tts"
# Server model entry tasks this client can synthesize audiobooks with,
-# taken from GET /v1/models (the "task" field of each entry; servers that
-# predate the field reported TTS models only, so a missing task is treated
-# as "tts"). "vdes" entries are voice design models: the voice is described
-# with --instructions instead of coming from a speaker or a reference clip.
-# Entries with any other task (asr, vc, diar, ...) are rejected at connect
-# time with a hint to pick a synthesis entry.
+# taken from GET /v1/models (the "task" field of each entry; a missing task
+# is treated as "tts" — a harmless generic default). "vdes" entries are
+# voice design models: the voice is described with --instructions instead
+# of coming from a speaker or a reference clip. Entries with any other task
+# (asr, vc, diar, ...) are rejected at connect time with a hint to pick a
+# synthesis entry.
AUDIOCPP_TASK_TTS = "tts"
AUDIOCPP_TASK_VDES = "vdes"
AUDIOCPP_SYNTHESIS_TASKS = (AUDIOCPP_TASK_TTS, "clon", AUDIOCPP_TASK_VDES)
+# The voice capability of a server model entry — how its voice is supplied.
+# Resolved per entry from (family, task, id) by
+# audiocpp_entry_voice_capability; drives both the Convert form (which
+# voice list to show) and the converter (speaker vs preset vs design mode).
+# Most families are clone-only; only the Qwen3-TTS CustomVoice entry has
+# built-in speakers, and only VoiceDesign entries take a description.
+AUDIOCPP_VOICE_SPEAKER = "speaker" # built-in speaker name (Qwen CustomVoice)
+AUDIOCPP_VOICE_CLONE = "clone" # server-side preset / voice_dir (Base, others)
+AUDIOCPP_VOICE_DESIGN = "design" # voice described by --instructions (vdes)
+
class AudioCppFamilyProfile:
- """Request conventions of one audio.cpp model family."""
+ """Request conventions of one audio.cpp model family.
+
+ Language style and whether the family reads a style/instruction prompt;
+ these are family-level (every entry of a family shares them). Whether a
+ *specific entry* has built-in speakers is an entry-level concern, decided
+ by audiocpp_entry_voice_capability, not this profile.
+ """
def __init__(self, language_style: str = AUDIOCPP_LANG_OMIT,
- sends_instructions: bool = False,
- builtin_speakers: bool = False):
+ sends_instructions: bool = False):
self.language_style = language_style
self.sends_instructions = sends_instructions
- self.builtin_speakers = builtin_speakers
# Generic profile for families not listed in AUDIOCPP_FAMILY_PROFILES:
@@ -161,7 +180,6 @@ AUDIOCPP_FAMILY_PROFILES = {
AUDIOCPP_FAMILY_QWEN3_TTS: AudioCppFamilyProfile(
language_style=AUDIOCPP_LANG_DISPLAY,
sends_instructions=True,
- builtin_speakers=True,
),
# Families whose language option takes a code (e.g. "en") instead of
# a Qwen display name; otherwise clone-only like the default profile.
@@ -172,6 +190,34 @@ AUDIOCPP_FAMILY_PROFILES = {
"supertonic": AudioCppFamilyProfile(language_style=AUDIOCPP_LANG_ISO),
}
+
+def audiocpp_entry_voice_capability(family: str, task: str,
+ model_id: str) -> str:
+ """How a server model entry's voice is supplied — speaker/clone/design.
+
+ Resolved from the entry's family, task and id — the same {id, family,
+ task} triple GET /v1/models reports, so it works for local server.json
+ entries and remote live-queried entries alike. Qwen3-TTS is the one
+ family hosting several model *types* under one family id: the
+ CustomVoice model (id contains "customvoice") has built-in speakers, the
+ Base model and any other entry are clone-only, and VoiceDesign entries
+ (task "vdes") take a description. Every other family is clone-only.
+ """
+ if task == AUDIOCPP_TASK_VDES:
+ return AUDIOCPP_VOICE_DESIGN
+ if family == AUDIOCPP_FAMILY_QWEN3_TTS \
+ and "customvoice" in (model_id or "").lower():
+ return AUDIOCPP_VOICE_SPEAKER
+ return AUDIOCPP_VOICE_CLONE
+
+# 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.
+QWEN3_TTS_SPEAKERS = ("Vivian", "Serena", "Uncle_Fu", "Dylan", "Eric",
+ "Ryan", "Aiden", "Ono_Anna", "Sohee")
+
# Canonical speaker names -> display names used by the qwen-tts demo.
SPEAKER_DISPLAY_NAMES = {
"ryan": "Ryan",
@@ -209,10 +255,21 @@ def _resolve_request_seed() -> int:
return seed
+def speaker_display_name_for(name: str) -> str:
+ """Return the wire (display) form of a Qwen3-TTS CustomVoice speaker NAME.
+
+ Accepts either the canonical/config form (e.g. "uncle_fu", "Uncle_Fu")
+ or the display form ("Uncle Fu"), case-insensitively; unknown names pass
+ through unchanged. Used by AudioCppTTSClient to normalize the --speaker /
+ Speaker-picker value into what audiocpp_server expects in the request's
+ voice field.
+ """
+ return SPEAKER_DISPLAY_NAMES.get((name or "").lower(), name)
+
+
def speaker_display_name() -> str:
"""Return the display name for the configured custom speaker."""
- return SPEAKER_DISPLAY_NAMES.get(
- config.SPEAKER.lower(), config.SPEAKER)
+ return speaker_display_name_for(config.SPEAKER)
def normalize_language(value: Optional[str]) -> str:
@@ -808,30 +865,39 @@ class AudioCppTTSClient(_BaseTTSClient):
serving stack). The server API is family-agnostic; the family and task
of the configured model entry are read from GET /v1/models at startup
and adapt the request payload (language field style, style instructions)
- through AUDIOCPP_FAMILY_PROFILES. Three voice modes, all resolved
- server-side from the request's "voice"/"instructions" fields:
-
- - Speaker mode (no ``voice``): Qwen3-TTS only. A built-in CustomVoice
- speaker name (e.g. "Vivian") is passed through, plus the INSTRUCT
- style prompt. The server must be configured with the CustomVoice
- model for this. Families without built-in speakers reject this mode
- with a hint to pass --voice (or --instructions, see below).
- - Preset mode (``voice=NAME``): a voice configured on the server
+ through AUDIOCPP_FAMILY_PROFILES. The entry's voice capability
+ (audiocpp_entry_voice_capability: speaker / clone / design) decides how
+ its voice is supplied; all three are resolved server-side from the
+ request's "voice"/"instructions" fields:
+
+ - Speaker mode (--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); selecting it
+ on a non-speaker entry fails fast with a hint.
+ - 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. Only the entry actually used needs to exist on the server: a
- clone-only (Base) server works for --voice runs, while speaker mode
- on such a server fails with a hint to pass --voice.
+ to it. Selecting --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.
- 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).
A constant per-run seed keeps the designed voice consistent across
chunk boundaries.
+ With neither --voice nor --speaker 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.
+
``instructions`` also works on non-design entries, where it acts as a
generic style/delivery instruction (voice control): families that read
it (OmniVoice, Qwen3-TTS CustomVoice, ...) shape the voice or delivery
@@ -852,7 +918,13 @@ class AudioCppTTSClient(_BaseTTSClient):
api_url: Optional[str] = None,
model_id: Optional[str] = None,
instructions: Optional[str] = None,
- request_options: Optional[Dict[str, str]] = None):
+ request_options: Optional[Dict[str, str]] = None,
+ speaker: Optional[str] = None):
+ if voice and speaker:
+ raise ValueError(
+ "--voice and --speaker are mutually exclusive: --voice selects "
+ "a server-side preset (cloning), --speaker a built-in "
+ "CustomVoice speaker (see README).")
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
@@ -868,8 +940,15 @@ class AudioCppTTSClient(_BaseTTSClient):
# negative "randomize" seed, so a negative value means "send no seed
# at all" (see _request_wav) and the server randomizes.
self._seed = _resolve_request_seed()
+ # Voice/picker selection. preset_mode is True iff a server-side
+ # preset (--voice) was requested: it gates _select_model's reroute
+ # to AUDIOCPP_CLONE_MODEL_ID and the INSTRUCT style-prompt logic.
+ # speaker is a built-in CustomVoice speaker name; the entry's
+ # capability (resolved in _connect) validates it. The request's
+ # "voice" field (self.voice) is filled in _connect per the mode.
self.preset_mode = bool(voice)
- self.voice = voice or speaker_display_name()
+ self.speaker = speaker or None
+ self.voice = voice or None
# Style/voice-design instruction sent with every request (the CLI
# --instructions flag overrides AUDIOCPP_INSTRUCTIONS in config.py).
# For task "vdes" entries it describes the voice to design; for other
@@ -879,9 +958,10 @@ class AudioCppTTSClient(_BaseTTSClient):
# 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 {})
- # Both set during _connect once the entry's task is known: design_mode
- # for "vdes" entries, instruction_voice when a family without built-in
- # speakers gets its voice from the instruction alone (no voice field).
+ # 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).
self.design_mode = False
self.instruction_voice = False
# Family and task of the selected model entry and the family's request
@@ -898,12 +978,15 @@ class AudioCppTTSClient(_BaseTTSClient):
def _connect(self) -> None:
"""Health-check the server and resolve the model, family, task, and voice.
- Speaker mode is only offered to families with built-in speakers
- (Qwen3-TTS); every other family must select a server-side voice
- with --voice or describe one with --instructions, so it fails fast
- with a hint instead of silently synthesizing with a random default
- voice. Voice design entries (task "vdes") require --instructions
- and reject --voice.
+ The entry's voice capability (audiocpp_entry_voice_capability, from
+ family/task/id) plus the caller's --voice/--speaker/--instructions
+ pick the mode: design entries require --instructions and reject
+ --voice/--speaker; a --speaker name requires a speaker-capable
+ (CustomVoice) entry; a --voice preset validates against the server's
+ voice library (and reroutes to AUDIOCPP_CLONE_MODEL_ID when set);
+ with neither flag, speaker-capable entries use the built-in
+ config.SPEAKER and clone entries fail fast with a hint instead of
+ silently synthesizing with a random default voice.
"""
self._check_health()
models = self._list_models()
@@ -921,11 +1004,13 @@ class AudioCppTTSClient(_BaseTTSClient):
f"model entries (tasks {', '.join(AUDIOCPP_SYNTHESIS_TASKS)}). "
f"Pick a synthesis entry with --model (available: {available})."
)
+ capability = audiocpp_entry_voice_capability(
+ self.family, self.task, self.model_id)
if self.design_mode:
- if self.preset_mode:
+ if self.voice or self.speaker:
raise RuntimeError(
- f"--voice cannot be used with the voice design model "
- f"'{self.model_id}': the voice is described by the "
+ f"--voice/--speaker cannot be used with the voice design "
+ f"model '{self.model_id}': the voice is described by the "
"--instructions text instead (see README).")
if not self.instructions:
raise RuntimeError(
@@ -938,18 +1023,31 @@ class AudioCppTTSClient(_BaseTTSClient):
f"(model '{self.model_id}', family '{self.family}', "
"voice design)")
print(f"[INFO] Designing the voice from: {self.instructions}")
- elif self.preset_mode:
+ elif self.speaker is not None:
+ if capability != AUDIOCPP_VOICE_SPEAKER:
+ raise RuntimeError(
+ f"The audio.cpp model '{self.model_id}' (family "
+ f"'{self.family}') has no built-in speakers; --speaker "
+ "selects a CustomVoice speaker, so choose the "
+ "CustomVoice model entry, or use --voice for a "
+ "server-side preset (see README).")
+ self.voice = speaker_display_name_for(self.speaker)
+ print(f"[OK] Connected to audio.cpp server at {self.api_url} "
+ f"(model '{self.model_id}', family '{self.family}', "
+ f"speaker '{self.voice}')")
+ elif self.voice is not None:
+ # Explicit server-side preset (--voice): validate it and route
+ # to the clone model entry when AUDIOCPP_CLONE_MODEL_ID is set.
self._check_voice()
print(f"[OK] Connected to audio.cpp server at {self.api_url} "
f"(model '{self.model_id}', family '{self.family}', "
f"voice '{self.voice}')")
- elif self.profile.builtin_speakers:
+ elif capability == AUDIOCPP_VOICE_SPEAKER:
+ # No flag on a CustomVoice entry: the built-in config.SPEAKER.
+ self.voice = speaker_display_name()
print(f"[OK] Connected to audio.cpp server at {self.api_url} "
f"(model '{self.model_id}', family '{self.family}', "
f"speaker '{self.voice}')")
- print("[INFO] Speaker mode expects the server to be configured with the "
- "CustomVoice model; with the Base model the speaker name is ignored "
- "and a random default voice is used (see README).")
elif self.instructions:
# Families without built-in speakers can still get their voice
# from the instruction alone (e.g. OmniVoice voice design).
@@ -965,7 +1063,8 @@ class AudioCppTTSClient(_BaseTTSClient):
"must come from the server: rerun with --voice NAME "
"matching a voice_preset or voice_dir entry in the server "
"config, or describe a voice with --instructions for "
- "families that support it (see README).")
+ "families that support it, or select the CustomVoice entry "
+ "for built-in speakers (see README).")
if self.instructions and not self.design_mode and not self.instruction_voice:
print(f"[INFO] Sending instruction with every request: {self.instructions}")
print("[INFO] Its effect (style, emotion, delivery) depends on the "
@@ -1135,10 +1234,8 @@ class AudioCppTTSClient(_BaseTTSClient):
# (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_is_qwen = (families.get(self.model_id)
- or AUDIOCPP_FAMILY_QWEN3_TTS) \
- == AUDIOCPP_FAMILY_QWEN3_TTS
- if primary_is_qwen:
+ 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",
@@ -1166,22 +1263,21 @@ class AudioCppTTSClient(_BaseTTSClient):
def _resolve_family(self, models: List[Dict[str, str]]) -> None:
"""Resolve the selected model's family and its request profile.
- The family comes from GET /v1/models. Servers that predate the
- family field served Qwen3-TTS only, so a missing family is treated
- as qwen3_tts, which also preserves this client's legacy behavior
- against those versions.
+ The family comes from GET /v1/models; a missing family is an unknown
+ family that falls through to the generic (clone-only) profile rather
+ than guessing a specific one — audiocpp_server always reports family
+ for entries its server.json describes.
"""
entry = next(
(model for model in models if model["id"] == self.model_id), None)
family = (entry["family"] if entry is not None else "") or ""
- if not family:
- family = AUDIOCPP_FAMILY_QWEN3_TTS
- logger.debug("Model '%s' reported no family; assuming qwen3_tts",
- self.model_id)
self.family = family
self.profile = AUDIOCPP_FAMILY_PROFILES.get(
family, AUDIOCPP_DEFAULT_FAMILY_PROFILE)
- if family not in AUDIOCPP_FAMILY_PROFILES:
+ if not family:
+ logger.debug("Model '%s' reported no family; using the generic "
+ "profile", self.model_id)
+ elif family not in AUDIOCPP_FAMILY_PROFILES:
logger.info(
"audio.cpp family '%s' has no dedicated profile; using the "
"generic profile (voice cloning via --voice, model-detected "