From 4db8ea7a63107297450819d227497ebbb121ff38 Mon Sep 17 00:00:00 2001 From: historia Date: Mon, 24 Aug 2026 06:04:38 -0400 Subject: fix: don't expect/use local config for remote servers --- app/ui/hub.py | 153 +++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 43 deletions(-) (limited to 'app/ui/hub.py') 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 -- cgit v1.2.3