diff options
Diffstat (limited to 'app/ui/hub.py')
| -rw-r--r-- | app/ui/hub.py | 131 |
1 files changed, 83 insertions, 48 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py index 25baf37..172a4ed 100644 --- a/app/ui/hub.py +++ b/app/ui/hub.py @@ -45,11 +45,10 @@ from backends import faster as faster_backend from backends import probe as backend_probe from backends import qwen as qwen_backend from converter import config +from converter import converter as converter_mod from converter.converter import ( - AUDIOBOOKS_FOLDER, AUDIO_FORMATS, AudiobookConverter, - BOOKS_FOLDER, LOGS_FOLDER, voice_mode_for, ) @@ -837,12 +836,15 @@ def _help_lines() -> list: Items are text_viewer rows: "" (a blank line) or a (segments, indent) pair — SEGMENTS are (text, kind) with KIND a theme key (None = body). Numbered steps start at the margin; every other - line is indented two spaces so it reads as part of its step. + line is indented two spaces so it reads as part of its step. The + input/output folders are read from the converter module at call + time, so a Settings change this session is reflected without a + restart. """ return [ ([("1. ", "title"), ("Put your ebooks (epub, txt, or pdf) here:", None)], 0), - ([(str(BOOKS_FOLDER), "input")], 1), + ([(str(converter_mod.BOOKS_FOLDER), "input")], 1), "", ([("2. ", "title"), ("Put any .wavs of voices to clone here:", None)], 0), @@ -875,7 +877,7 @@ def _help_lines() -> list: ([("6. ", "title"), ("Generated audiobooks (m4b, mp3, etc.) will output here:", None)], 0), - ([(str(AUDIOBOOKS_FOLDER), "input")], 1), + ([(str(converter_mod.AUDIOBOOKS_FOLDER), "input")], 1), ] @@ -884,9 +886,10 @@ def _convert_form(stdscr) -> Optional[tuple]: The first field is the Backend picker; the remaining fields are that backend's options (audio.cpp: model/voice/instructions; qwen: - model + speaker / clone .wav / design instruction; faster: voice), - plus the shared output - settings. A backend appears once as a managed entry ("audio.cpp") when + model + speaker / clone .wav / design instruction; faster: voice) + plus the per-run combine-all-chapters toggle (the shared output + settings live in the Settings menu). A backend appears once as a + managed entry ("audio.cpp") when it is installed+configured here, and once as a remote entry ("audio.cpp [remote]") when a running server was found at its remote URL. Managed entries read the local server.json / voices.json; remote @@ -1033,61 +1036,44 @@ def _field_value(fields, key: str, default=None): return default -# Dim hint shown while editing a Language field (Settings and Generate -# Audiobooks): which languages a model accepts varies by backend/model. +# Dim hint shown while editing the Settings Language field: which +# languages a model accepts varies by backend/model. _LANGUAGE_EDIT_HINT = ["Check model documentation for supported languages."] def _common_fields() -> list: - """Field dicts for output format, language, speed, single-file, debug. + """The per-run Generate-form fields (the global output options — + output format, language, speed, debug, stop-server-and-exit — live + in the Settings menu and reach the run via _common_kwargs()). The single-file field is hidden for m4b (always a single file with - embedded chapter markers), so its "visible" callable reads the live - output-format value from the field list. The per-run Language field - mirrors the CLI's --language (a static audio.cpp-menu picker) and is - hidden for faster entries (the faster server owns the language). + embedded chapter markers), so its "visible" callable reads the + configured output format. """ - fmt_default = config.AUDIO_FORMAT \ - if config.AUDIO_FORMAT in AUDIO_FORMATS else AUDIO_FORMATS[0] return [ - {"key": "output_format", "label": "Output format", "kind": "choice", - "value": fmt_default, "choices": list(AUDIO_FORMATS)}, - {"key": "language", "label": "Language", "kind": "choice", - "value": config.LANGUAGE, "choices": list(LANGUAGE_CHOICES), - "help": _LANGUAGE_EDIT_HINT, - "validate": _validate_language, - "visible": lambda fs: not str(_field_value(fs, "backend") or "") - .startswith("faster")}, - {"key": "speed", "label": "Speed", "kind": "text", "value": "1.0", - "validate": lambda s: None if (_is_float(s) and float(s) > 0) - else "Enter a positive number, e.g. 1.0"}, {"key": "single_file", "label": "Combine all chapters", "kind": "bool", "value": False, - "visible": lambda fs: _field_value(fs, "output_format") != "m4b"}, - {"key": "debug", "label": "Debug", "kind": "bool", "value": False}, - {"key": "stop_and_exit", "label": "Stop server and exit", - "kind": "bool", "value": config.STOP_SERVER_AND_EXIT}, + "visible": lambda fs: config.AUDIO_FORMAT != "m4b"}, ] def _common_kwargs(values: dict) -> dict: - """Map the common form fields to converter keyword arguments.""" - output_format = values["output_format"] - # The Language field is hidden for faster entries and may then hold - # stale, unvalidated text; a failed normalization falls back to None - # so convert() applies config.LANGUAGE instead of failing the run. - try: - language = normalize_language(values.get("language")) - except ValueError: - language = None + """Map the common form fields plus the configured output settings to + converter keyword arguments. + + Output format, language, speed, debug, and stop-server-and-exit are + configured once in the Settings menu (config.py) and apply to every + run; only per-run choices (combine-all-chapters) come from the form. + """ + output_format = config.AUDIO_FORMAT return { - "language": language, + "language": config.LANGUAGE, "output_format": output_format, - "speed": float(values["speed"]), + "speed": float(config.SPEED), "single_file": bool(values["single_file"]) and output_format != "m4b", - "debug": bool(values["debug"]), - "stop_and_exit": bool(values["stop_and_exit"]), + "debug": bool(config.DEBUG), + "stop_and_exit": bool(config.STOP_SERVER_AND_EXIT), } @@ -1562,20 +1548,35 @@ def _settings_changed(fields: list, original: dict) -> bool: def _settings_fields() -> list: - """The global output-settings field list (Save writes to config.py).""" + """The global settings field list (Save writes to config.py).""" return [ {"key": "audio_format", "label": "Audio format", "kind": "choice", "value": config.AUDIO_FORMAT, "choices": list(AUDIO_FORMATS)}, {"key": "audio_bitrate", "label": "Audio bitrate", "kind": "text", "value": config.AUDIO_BITRATE, "validate": _validate_bitrate}, - {"key": "language", "label": "Default Language", "kind": "choice", + {"key": "language", "label": "Language", "kind": "choice", "value": config.LANGUAGE, "choices": list(LANGUAGE_CHOICES), "help": _LANGUAGE_EDIT_HINT, "validate": _validate_language}, {"key": "chunk_size", "label": "Chunk size (words)", "kind": "text", "value": str(config.CHUNK_SIZE), "validate": _validate_chunk_size}, - {"key": "stop_and_exit", "label": "Default stop server and exit", + {"key": "input_dir", "label": "Input Directory", "kind": "dir", + "value": converter_mod.resolve_dir(config.INPUT_DIR, "input"), + "validate": _validate_dir, + "note": "Directory the books (.txt/.pdf/.epub) are read from. " + "Relative paths resolve against the project root."}, + {"key": "output_dir", "label": "Output Directory", "kind": "dir", + "value": converter_mod.resolve_dir(config.OUTPUT_DIR, "output"), + "validate": _validate_dir, + "note": "Directory the finished audiobooks are written to."}, + {"key": "speed", "label": "Speed", "kind": "text", + "value": str(config.SPEED), "validate": _validate_speed}, + {"key": "debug", "label": "Debug", "kind": "bool", + "value": config.DEBUG, + "note": "Dump each chunk's raw audio and the exact text sent " + "for it, and log every TTS request and response."}, + {"key": "stop_and_exit", "label": "Stop server and exit", "kind": "bool", "value": config.STOP_SERVER_AND_EXIT, "note": "Automatically stop the TTS server and exit TUI " "after generating audiobooks"}, @@ -1638,6 +1639,20 @@ def _validate_chunk_size(value: str) -> Optional[str]: return None +def _validate_speed(value: str) -> Optional[str]: + """Error message for an invalid SPEED, or None to accept it.""" + if _is_float(value.strip()) and float(value) > 0: + return None + return "Enter a positive number, e.g. 1.0" + + +def _validate_dir(value) -> Optional[str]: + """Error message for a blank directory setting, or None to accept it.""" + if str(value).strip(): + return None + return "Directory must not be empty" + + def _validate_port(value: str) -> Optional[str]: """Error message for an invalid port, or None to accept it.""" try: @@ -1685,6 +1700,15 @@ 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']}") + speed = float(str(values["speed"]).strip()) + if speed <= 0: + raise ValueError("Speed must be a positive number") + input_dir = str(values["input_dir"]).strip() + output_dir = str(values["output_dir"]).strip() + if not input_dir: + raise ValueError("Input Directory must not be empty") + if not output_dir: + raise ValueError("Output Directory must not be empty") ports = { "qwen_port": _read_port(values, "qwen_port"), @@ -1704,6 +1728,10 @@ def _apply_settings(values: dict) -> None: "AUDIO_BITRATE": bitrate, "LANGUAGE": normalize_language(values["language"]), "CHUNK_SIZE": chunk_size, + "INPUT_DIR": input_dir, + "OUTPUT_DIR": output_dir, + "SPEED": speed, + "DEBUG": bool(values["debug"]), "STOP_SERVER_AND_EXIT": bool(values["stop_and_exit"]), "AUDIOCPP_UNLOAD_MODELS": bool(values["unload_models"]), "QWEN_API_URL": common.url_with_port( @@ -1724,6 +1752,13 @@ def _apply_settings(values: dict) -> None: if not common.update_config_value(name, value): raise ValueError(f"Could not save {name} to " f"{common.CONFIG_PATH}") + # The input/output folders changed: re-derive the converter module's + # folder globals so this session's preflight/runs (and the Help text) + # see the new directories without a restart. + converter_mod.BOOKS_FOLDER = converter_mod.resolve_dir( + input_dir, "input") + converter_mod.AUDIOBOOKS_FOLDER = converter_mod.resolve_dir( + output_dir, "output") # Ports/URLs may have changed: the cached backend statuses are stale. invalidate_detect_cache() |
