diff options
| author | historia <historiavg@proton.me> | 2026-08-26 20:43:05 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-26 20:43:05 -0400 |
| commit | 65c6f737f1545ef225768af897acd20f163a4fb4 (patch) | |
| tree | 9c0974944191e50b9caa90591dfcb7f97c659770 /app/ui/hub.py | |
| parent | 975bd960fd07e75799b8e3adc4c0033046b34792 (diff) | |
| download | tts-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.py | 69 |
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 [ |
