aboutsummaryrefslogtreecommitdiff
path: root/app/ui/hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-26 19:12:11 -0400
committerhistoria <historiavg@proton.me>2026-08-26 19:12:11 -0400
commit5af37bbd575eec89c6ac2fecf2d8c2eda4c1728d (patch)
tree4f8fe9b79d339695ca83968e03301b853eb2e2ef /app/ui/hub.py
parentdf95e7034df683c38fde67890430ab4c2abfa4ba (diff)
downloadtts-audiobook-generator-5af37bbd575eec89c6ac2fecf2d8c2eda4c1728d.tar.gz
fix: crash when menu of clone voices is empty
Diffstat (limited to 'app/ui/hub.py')
-rw-r--r--app/ui/hub.py31
1 files changed, 28 insertions, 3 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 6a82c83..80fe843 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -927,6 +927,13 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
the field keys ("" for the managed entry) so two entries of this
backend can share one form without overwriting each other.
+ The Voice field tracks the selected entry's capability — built-in
+ speakers on CustomVoice, the server's clone voices on every other
+ entry. A clone-capable entry whose server lists no voices cannot be
+ picked from (empty menu) and refuses Generate! with a hint pointing
+ at the voice-clone .wav directory instead of crashing or producing a
+ run that fails at model-load time.
+
With API_URL None (the managed entry) the model list is fed from the
local checkout's server.json — the config of the server this tool
manages. With API_URL set (the "[remote]" entry) the models and voices
@@ -1045,6 +1052,25 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
fields, prefix + "model_id"))]
return [] # design: the field is hidden
+ def no_voices_hint() -> str:
+ """Why a clone-capable entry has no selectable voices."""
+ if local:
+ return ("No .wav files available to clone — run Configure "
+ "backends → audio.cpp and add voices to its "
+ "voice-clone .wav directory.")
+ return ("No .wav files available to clone — the audio.cpp server "
+ f"at {url} hosts none. Configure its voice-clone .wav "
+ "directory on that machine.")
+
+ def voice_validate(value):
+ """Refuse Generate! when this entry's clone voice is unavailable."""
+ if model_capability(fields) != AUDIOCPP_VOICE_CLONE:
+ return None
+ if not voices_for(_field_value(fields, prefix + "model_id")):
+ return no_voices_hint()
+ return None if value \
+ else "This model needs a voice — pick one or switch models"
+
model_ids = [m.get("id") for m in models]
default_model = config.AUDIOCPP_MODEL_ID \
if config.AUDIOCPP_MODEL_ID in model_ids else model_ids[0]
@@ -1080,9 +1106,8 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
"value": initial_voice,
"choices": lambda fs: voice_choices(fs),
"visible": lambda fs: model_capability(fs) != AUDIOCPP_VOICE_DESIGN,
- "validate": lambda value: None
- if (model_capability(fields) != AUDIOCPP_VOICE_CLONE or value)
- else "This model needs a voice — pick one or switch models"},
+ "on_empty_choices": no_voices_hint,
+ "validate": voice_validate},
{"key": prefix + "instructions", "label": "Instructions", "kind": "text",
"value": config.AUDIOCPP_INSTRUCTIONS,
"visible": lambda fs: model_capability(fs) in (AUDIOCPP_VOICE_DESIGN,