aboutsummaryrefslogtreecommitdiff
path: root/app/ui/hub.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-26 20:43:05 -0400
committerhistoria <historiavg@proton.me>2026-08-26 20:43:05 -0400
commit65c6f737f1545ef225768af897acd20f163a4fb4 (patch)
tree9c0974944191e50b9caa90591dfcb7f97c659770 /app/ui/hub.py
parent975bd960fd07e75799b8e3adc4c0033046b34792 (diff)
downloadtts-audiobook-generator-65c6f737f1545ef225768af897acd20f163a4fb4.tar.gz
fix: settings menu only prompts to save after change
Diffstat (limited to 'app/ui/hub.py')
-rw-r--r--app/ui/hub.py69
1 files changed, 66 insertions, 3 deletions
diff --git a/app/ui/hub.py b/app/ui/hub.py
index d9d291b..0fd1b4d 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -414,6 +414,7 @@ class _Hub:
def screen_settings(self):
fields = _settings_fields()
+ original = {field["key"]: field["value"] for field in fields}
while True:
result = tui.form(self.stdscr, "Settings", fields,
back_value=tui.Wizard.BACK)
@@ -425,7 +426,10 @@ class _Hub:
tui.flash(self.stdscr, str(exc), "err")
return tui.Wizard.BACK
# q/Esc (or the Cancel button) left the form without saving:
- # ask whether the edits should be kept before discarding them.
+ # with no edits there is nothing to keep, so go straight back;
+ # otherwise ask whether the edits should be preserved.
+ if not _settings_changed(fields, original):
+ return tui.Wizard.BACK
answer = tui.confirm_yn_cancel(self.stdscr, "Save settings?")
if answer == "cancel":
continue # back into the form, edits intact
@@ -1022,6 +1026,17 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
if data.get("voice_dir") else []
voice_cache: dict = {} # model id -> voices (local: shared list)
+ # Per-family request-option support comes from this machine's audio.cpp
+ # checkout model_specs, best effort for both entries: the server's HTTP
+ # API does not report it. A "[remote]" entry is classified from the same
+ # local specs when a family matches; with no checkout every family counts
+ # as unknown and the Request options field stays hidden.
+ specs_checkout = checkout if local \
+ else audiocpp_backend.find_local_checkout()
+ option_families = (
+ audiocpp_backend.request_options_families(specs_checkout)
+ if specs_checkout is not None else {})
+
def voices_for(model_id: str) -> list:
if local:
return local_voices
@@ -1119,6 +1134,31 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
entry.get("id") or "")
return f"{entry.get('id') or '':<{id_width}} ({capability})"
+ def entry_supports_options(fs) -> bool:
+ """True when the selected entry's family defines request options.
+
+ Resolved strictly from this machine's model_specs: a family the
+ specs prove unable to read options, or cannot classify at all,
+ keeps the field hidden (unknown support is treated as no).
+ """
+ family = model_entry(fs).get("family") or ""
+ return audiocpp_backend.supports_request_options(
+ option_families, family) is True
+
+ # Edit-dialog help lines: short, and identical for every capability
+ # (design-model validation already explains its own requirement).
+ INSTRUCTIONS_HELP = [
+ "TTS style instructions. Supported by some clone models. Example:",
+ '"Speak in a calm, soothing, and happy tone."',
+ ]
+ # Edit-dialog help for the Request options field — at most 2 lines;
+ # unsupported keys are ignored server-side, so nothing else needs
+ # spelling out here.
+ OPTIONS_HELP = [
+ "KEY=VALUE items, comma/space separated; unsupported keys ignored.",
+ "Examples: emotion=neutral, speed=1.1, temperature=0.8",
+ ]
+
fields = [
{"key": prefix + "model_id", "label": "Model", "kind": "choice",
"value": default_model,
@@ -1142,13 +1182,17 @@ def _audiocpp_fields(stdscr, api_url: Optional[str] = None,
# that read instructions, the voice itself (instruction-voice mode).
{"key": prefix + "instructions", "label": "Instructions", "kind": "text",
"value": config.AUDIOCPP_INSTRUCTIONS,
+ "help": INSTRUCTIONS_HELP,
"validate": lambda value: None
if (model_capability(fields) != AUDIOCPP_VOICE_DESIGN or str(value).strip())
else "Describe the voice, e.g. 'A warm female narrator'"},
- # Free-form per-model controls (--option KEY=VALUE on the CLI),
- # e.g. "emotion=neutral, speed=1.1".
+ # Free-form per-model controls (--option KEY=VALUE on the CLI).
+ # Shown only for families whose audio.cpp spec declares request
+ # options; unknown-support families keep it hidden.
{"key": prefix + "request_options", "label": "Request options",
"kind": "text", "value": "",
+ "visible": entry_supports_options,
+ "help": OPTIONS_HELP,
"validate": _validate_request_options},
]
@@ -1314,6 +1358,25 @@ def _faster_fields(stdscr, api_url: Optional[str] = None,
# Settings menu (global output options -> app/converter/config.py)
# ---------------------------------------------------------------------------
+def _settings_changed(fields: list, original: dict) -> bool:
+ """True when any field's current value differs from its ORIGINAL.
+
+ Text values compare whitespace-stripped (the form's editor and
+ _apply_settings trim them anyway), so retyping a setting with stray
+ spaces does not count as a change.
+ """
+ for field in fields:
+ value = field["value"]
+ base = original[field["key"]]
+ if isinstance(value, str) and isinstance(base, str):
+ changed = value.strip() != base.strip()
+ else:
+ changed = value != base
+ if changed:
+ return True
+ return False
+
+
def _settings_fields() -> list:
"""The global output-settings field list (Save writes to config.py)."""
return [