aboutsummaryrefslogtreecommitdiff
path: root/app/ui/hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-27 18:41:10 -0400
committerhistoria <historiavg@proton.me>2026-08-27 18:41:10 -0400
commitb6795046bf20023fd7b2e083ead20e7362f0c0d7 (patch)
tree20c8fd1cde5ed7fbb2c630e8250af098f547be05 /app/ui/hub.py
parent0047a875d0a969005f3cea3df18de909d285f910 (diff)
downloadtts-audiobook-generator-b6795046bf20023fd7b2e083ead20e7362f0c0d7.tar.gz
feat: simply generate audiobooks menu, move more config to settings menu
Diffstat (limited to 'app/ui/hub.py')
-rw-r--r--app/ui/hub.py131
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()