diff options
Diffstat (limited to 'app/ui/hub.py')
| -rw-r--r-- | app/ui/hub.py | 98 |
1 files changed, 91 insertions, 7 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py index 5b18869..0e227da 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -15,6 +15,7 @@ main menu. import json import re import shutil +import urllib.parse from pathlib import Path from typing import Optional, Tuple @@ -77,12 +78,12 @@ def _hub_menu(stdscr) -> Optional[tuple]: """Show the main menu; return a command tuple, or None to quit.""" while True: statuses = detect_all() - options = [("Set up a backend...", "setup")] + options = [("Set up a backend", "setup")] if any(st.installed or st.running for st in statuses): - options.insert(0, ("Convert books...", "convert")) - options.append(("Configure a backend...", "configure")) - options.append(("Server...", "server")) - options.append(("Settings...", "settings")) + options.insert(0, ("Convert books", "convert")) + options.append(("Configure a backend", "configure")) + options.append(("Start/Stop Backend Servers", "server")) + options.append(("Settings", "settings")) options.append(("Quit", "quit")) rows = [(st.label, *_status_mark(st)) for st in statuses] notice_lines = None @@ -177,13 +178,13 @@ def _convert_menu(stdscr, statuses) -> Optional[tuple]: if not available: choice = tui.menu( stdscr, "No backend is available", - [("Set up a backend...", "__setup__")], + [("Set up a backend", "__setup__")], help_lines=["Set up a backend (clone/build/configure) before " "converting."]) if choice == "__setup__": return _setup_menu(stdscr, statuses) return None - options.append(("Set up a backend...", "__setup__")) + options.append(("Set up a backend", "__setup__")) key = tui.menu(stdscr, "Convert books with...", options, back_value=_GO_BACK) if key is _GO_BACK or key is None: @@ -399,6 +400,22 @@ def _settings_menu(stdscr) -> None: "value": config.LANGUAGE, "validate": _validate_language}, {"key": "chunk_size", "label": "Chunk size (words)", "kind": "text", "value": str(config.CHUNK_SIZE), "validate": _validate_chunk_size}, + {"key": "qwen_custom_port", "label": "qwen-tts CustomVoice port", + "kind": "text", + "value": str(_port_from_url(config.QWEN_API_URL, 7860)), + "validate": _validate_port}, + {"key": "qwen_clone_port", "label": "qwen-tts Base (clone) port", + "kind": "text", + "value": str(_port_from_url(config.CLONE_API_URL, 7861)), + "validate": _validate_port}, + {"key": "faster_port", "label": "faster-qwen3-tts port", + "kind": "text", + "value": str(_port_from_url(config.FASTER_API_URL, 8000)), + "validate": _validate_port}, + {"key": "audiocpp_port", "label": "audio.cpp port", + "kind": "text", + "value": str(_port_from_url(config.AUDIOCPP_API_URL, 8080)), + "validate": _validate_port}, ] result = tui.form(stdscr, "Settings", fields, back_value=_GO_BACK) if result is None or result is _GO_BACK: @@ -438,6 +455,25 @@ def _validate_chunk_size(value: str) -> Optional[str]: return None +def _validate_port(value: str) -> Optional[str]: + """Error message for an invalid port, or None to accept it.""" + try: + number = int(value.strip()) + except ValueError: + return "Enter a port number, e.g. 8080" + if not 1 <= number <= 65535: + return "Port must be between 1 and 65535" + return None + + +def _port_from_url(url: str, default: int) -> int: + """Return the port in URL, or DEFAULT when it has none/unparsable.""" + try: + return urllib.parse.urlsplit(url).port or default + except ValueError: + return default + + def _apply_settings(values: dict) -> None: """Write VALUES to app/converter/config.py and reload them in-memory.""" chunk_size = int(values["chunk_size"].strip()) @@ -448,16 +484,64 @@ def _apply_settings(values: dict) -> None: raise ValueError("Audio bitrate must not be empty") if values["audio_format"] not in AUDIO_FORMATS: raise ValueError(f"Unsupported audio format: {values['audio_format']}") + + ports = { + "qwen_custom_port": _read_port(values, "qwen_custom_port"), + "qwen_clone_port": _read_port(values, "qwen_clone_port"), + "faster_port": _read_port(values, "faster_port"), + "audiocpp_port": _read_port(values, "audiocpp_port"), + } updates = { "AUDIO_FORMAT": values["audio_format"], "AUDIO_BITRATE": bitrate, "LANGUAGE": normalize_language(values["language"]), "CHUNK_SIZE": chunk_size, + "QWEN_API_URL": common.url_with_port( + config.QWEN_API_URL, ports["qwen_custom_port"]), + "CLONE_API_URL": common.url_with_port( + config.CLONE_API_URL, ports["qwen_clone_port"]), + "FASTER_API_URL": common.url_with_port( + config.FASTER_API_URL, ports["faster_port"]), + "AUDIOCPP_API_URL": common.url_with_port( + config.AUDIOCPP_API_URL, ports["audiocpp_port"]), } _write_config(updates) for name, value in updates.items(): setattr(config, name, value) + _sync_audiocpp_server_port(ports["audiocpp_port"]) + + +def _read_port(values: dict, key: str) -> int: + """Parse a port field value, raising ValueError on a bad number.""" + try: + number = int(values[key].strip()) + except (KeyError, ValueError): + raise ValueError(f"Enter a valid port for {key}") + if not 1 <= number <= 65535: + raise ValueError("Port must be between 1 and 65535") + return number + + +def _sync_audiocpp_server_port(port: int) -> None: + """Rewrite the audio.cpp server.json 'port' to PORT when it exists. + + A missing checkout/server.json is a no-op (the config URL still + changes; the file is regenerated on reconfigure). An existing + server.json that cannot be updated raises, so the save is not + reported as successful while the two are out of sync. + """ + checkout = audiocpp_backend.find_local_checkout() + if checkout is None: + return + server_json = checkout / "server.json" + if not server_json.exists(): + return + if not audiocpp_backend.update_server_config_port(port): + raise ValueError( + f"Could not update {server_json}; the audio.cpp port was " + "left as-is") + def _write_config(updates: dict) -> None: """Rewrite the ``NAME = value`` lines for UPDATES in app/converter/config.py. |
