aboutsummaryrefslogtreecommitdiff
path: root/app/ui/hub.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/ui/hub.py')
-rw-r--r--app/ui/hub.py98
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.