aboutsummaryrefslogtreecommitdiff
path: root/app/ui
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 06:04:38 -0400
committerhistoria <historiavg@proton.me>2026-08-24 06:04:38 -0400
commit4db8ea7a63107297450819d227497ebbb121ff38 (patch)
tree250b646195f98a4b4c42b04589c0ca8951d13fbb /app/ui
parent4f3d16e9d532efd6a6bda283925efeb8c93b51c7 (diff)
downloadtts-audiobook-generator-4db8ea7a63107297450819d227497ebbb121ff38.tar.gz
fix: don't expect/use local config for remote servers
Diffstat (limited to 'app/ui')
-rw-r--r--app/ui/hub.py153
1 files changed, 110 insertions, 43 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index ef396e5..b603dd0 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -232,25 +232,50 @@ def _convert_menu(stdscr, statuses) -> Optional[tuple]:
def _convert_audiocpp(stdscr, statuses) -> Optional[tuple]:
- """Collect audio.cpp run settings by reading app/audio.cpp/server.json."""
+ """Collect audio.cpp run settings for a managed or remote server.
+
+ With a local checkout configured (its server.json), the menus are fed
+ from that file — the config of the server this tool manages. Without
+ one, the running server is external and nothing is known about it
+ locally, so its model and voice lists are queried live instead (the
+ same GET /v1/models and GET /v1/audio/voices endpoints the converter
+ resolves at run time).
+ """
checkout = audiocpp_backend.find_local_checkout()
server_json = checkout / "server.json" if checkout else None
- if not server_json or not server_json.exists():
- tui.flash(stdscr, "No server.json found in the audio.cpp checkout. "
- "Run 'Set up a backend' first.")
- return None
- try:
- data = json.loads(server_json.read_text(encoding="utf-8"))
- except (OSError, ValueError):
- tui.flash(stdscr, f"Could not read {server_json}.")
- return None
- models = data.get("models") or []
- if not models:
- tui.flash(stdscr, "No model entries in server.json. Reconfigure "
- "audio.cpp first.")
- return None
- model_options = [(f"{m.get('id')} ({m.get('family')}, {m.get('task', 'tts')})",
- m.get("id")) for m in models]
+ local = bool(server_json and server_json.exists())
+ url = config.AUDIOCPP_API_URL
+
+ if local:
+ try:
+ data = json.loads(server_json.read_text(encoding="utf-8"))
+ except (OSError, ValueError):
+ tui.flash(stdscr, f"Could not read {server_json}.")
+ return None
+ models = data.get("models") or []
+ if not models:
+ tui.flash(stdscr, "No model entries in server.json. Reconfigure "
+ "audio.cpp first.")
+ return None
+ else:
+ # Remote flow: the backend only reaches the convert menu while a
+ # server is running, so query it — the local config says nothing
+ # about an external server.
+ models = audiocpp_backend.fetch_server_models(url)
+ if models is None:
+ tui.flash(stdscr, f"Could not list models from the audio.cpp "
+ f"server at {url}. Is an audiocpp_server answering "
+ "there?")
+ return None
+ if not models:
+ tui.flash(stdscr, f"The audio.cpp server at {url} hosts no "
+ "model entries.")
+ return None
+ data = {}
+
+ model_options = [(f"{m.get('id')} ({m.get('family') or '?'}, "
+ f"{m.get('task') or 'tts'})", m.get("id"))
+ for m in models]
model_id = tui.menu(stdscr, "Select the audio.cpp model to use",
model_options, back_value=_GO_BACK)
if model_id is _GO_BACK or model_id is None:
@@ -258,16 +283,30 @@ def _convert_audiocpp(stdscr, statuses) -> Optional[tuple]:
entry = next((m for m in models if m.get("id") == model_id), {})
family = entry.get("family")
task = entry.get("task", "tts")
+ if not local:
+ # Servers predating the family/task fields omit them; mirror the
+ # converter's defaults (_resolve_family/_resolve_task): unknown
+ # family means qwen3_tts, a missing task plain tts.
+ family = family or AUDIOCPP_FAMILY_QWEN3_TTS
+ task = task or "tts"
+
+ def _voices() -> Optional[list]:
+ """Voice names for MODEL_ID, or None when a remote server's voice
+ list cannot be queried. Locally: the voice_dir's .wav stems;
+ remotely: GET /v1/audio/voices."""
+ if local:
+ voice_dir = data.get("voice_dir")
+ return _list_voices(voice_dir) if voice_dir else []
+ return audiocpp_backend.fetch_server_voices(url, model_id)
# Voice: optional for qwen3_tts (built-in speaker), required otherwise.
voice = None
- voice_dir = data.get("voice_dir")
- voices = _list_voices(voice_dir) if voice_dir else []
if task == "vdes":
# Voice design: no voice, instructions required.
pass
elif family == AUDIOCPP_FAMILY_QWEN3_TTS:
# Speaker mode available; voice optional.
+ voices = _voices() or []
if voices:
opts = [("(built-in speaker)", None)] + [(v, v) for v in voices]
voice = tui.menu(stdscr, "Voice", opts, back_value=_GO_BACK)
@@ -276,10 +315,21 @@ def _convert_audiocpp(stdscr, statuses) -> Optional[tuple]:
else:
voice = None
else:
+ voices = _voices()
+ if voices is None:
+ tui.flash(stdscr, f"Could not list voices from the audio.cpp "
+ f"server at {url}.")
+ return None
if not voices:
- tui.flash(stdscr, f"This model needs a --voice but voice_dir "
- f"{voice_dir} has no .wav voices. Reconfigure "
- "audio.cpp or add voices.")
+ if local:
+ tui.flash(stdscr, f"This model needs a --voice but voice_dir "
+ f"{data.get('voice_dir')} has no .wav voices. "
+ "Reconfigure audio.cpp or add voices.")
+ else:
+ tui.flash(stdscr, f"This model needs a --voice but the "
+ f"server at {url} lists none for '{model_id}'. "
+ "Configure voice presets or a voice_dir on the "
+ "server.")
return None
voice = tui.menu(stdscr, "Select the voice to clone", [(v, v) for v in voices],
back_value=_GO_BACK)
@@ -344,29 +394,46 @@ def _convert_qwen(stdscr) -> Optional[tuple]:
def _convert_faster(stdscr) -> Optional[tuple]:
- """Collect faster run settings: pick a voice from voices.json."""
+ """Collect faster run settings: pick or type a voice name.
+
+ With a local checkout's voices.json the picker lists it (the config of
+ the server this tool manages). Without one, the running server was
+ configured elsewhere and its voice names are unknown here, so the name
+ is typed instead — safe for any value, since the server falls back to
+ its first configured voice when the name is not defined.
+ """
checkout = faster_backend._checkout()
voices_json = checkout / "voices.json"
- if not voices_json.exists():
- tui.flash(stdscr, f"No voices.json at {voices_json}. Run 'Set up a "
- "backend' for faster first.")
- return None
- try:
- voices = json.loads(voices_json.read_text(encoding="utf-8"))
- except (OSError, ValueError):
- tui.flash(stdscr, f"Could not read {voices_json}.")
- return None
- if not voices:
- tui.flash(stdscr, "voices.json has no voices. Reconfigure faster.")
- return None
- default = config.FASTER_VOICE if config.FASTER_VOICE in voices else \
- next(iter(voices))
- voice = tui.menu(
- stdscr, "Select the voice to clone",
- [(k, k) for k in voices],
- default_index=list(voices).index(default), back_value=_GO_BACK)
- if voice is _GO_BACK or voice is None:
- return None
+ voices = None
+ if voices_json.exists():
+ try:
+ voices = json.loads(voices_json.read_text(encoding="utf-8"))
+ except (OSError, ValueError):
+ tui.flash(stdscr, f"Could not read {voices_json}.")
+ return None
+ if not voices:
+ tui.flash(stdscr, "voices.json has no voices. Reconfigure faster.")
+ return None
+ if voices is None:
+ # No local voices.json: prompt for a server-side voice name.
+ voice_text = tui.line_edit(
+ stdscr, "Server-side voice to clone with "
+ "(blank uses the server's first voice)",
+ config.FASTER_VOICE,
+ validate=lambda s: None if s.strip() else "Enter a voice name",
+ back_value=_GO_BACK)
+ if voice_text is _GO_BACK:
+ return None
+ voice = voice_text.strip()
+ else:
+ default = config.FASTER_VOICE if config.FASTER_VOICE in voices else \
+ next(iter(voices))
+ voice = tui.menu(
+ stdscr, "Select the voice to clone",
+ [(k, k) for k in voices],
+ default_index=list(voices).index(default), back_value=_GO_BACK)
+ if voice is _GO_BACK or voice is None:
+ return None
common_kw = _common_options(stdscr)
if common_kw is None:
return None