diff options
| author | historia <historiavg@proton.me> | 2026-08-24 20:17:11 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-24 20:17:11 -0400 |
| commit | cf24fa74188cee498eeb7b94422371c952278d4a (patch) | |
| tree | 302244a8b09b9173fbde542874cfa549f26be7a2 /app/backends/faster.py | |
| parent | 0522e73b68291af62e43c387ab8f9b8ffa2cab47 (diff) | |
| download | tts-audiobook-generator-cf24fa74188cee498eeb7b94422371c952278d4a.tar.gz | |
fix: stepping back steps in tui
Diffstat (limited to 'app/backends/faster.py')
| -rwxr-xr-x | app/backends/faster.py | 221 |
1 files changed, 156 insertions, 65 deletions
diff --git a/app/backends/faster.py b/app/backends/faster.py index 7e1be74..e209ff2 100755 --- a/app/backends/faster.py +++ b/app/backends/faster.py @@ -185,42 +185,37 @@ def _write_voices_json(output_path: Path, wav_dir: Path, language: str, def _wizard(stdscr, args: argparse.Namespace) -> Optional[dict]: - """Linear TUI wizard collecting every faster-setup decision.""" + """Linear TUI wizard collecting every faster-setup decision. + + Driven by ``tui.Wizard`` as a stack of screen closures: each screen + shows one widget and returns the next screen, ``Wizard.BACK`` (Esc/q — + pop to the previous screen), or the settings dict. Steps whose value is + already provided by a flag (``--wavs``, ``--language``, + ``--whisper-model``, ``--port``, ``--skip-install``, ``--skip-clone``) + or that do not apply (the transcription plan when there is nothing to + decide) are folded into the ``_after_*`` guards and never become + screens, so Esc always lands on the previous real screen. Esc on the + first screen aborts the wizard. + """ _GO_BACK = object() + s: dict = {} - def confirm(question: str, default: bool = True) -> Optional[bool]: + def _confirm(question: str, default: bool = True) -> Optional[bool]: res = tui.confirm(stdscr, question, default=default, cancel_value=_GO_BACK) return None if res is _GO_BACK else res - # Step 0: pip install (if not installed and not skipped). - do_install = False - if not _is_installed() and not args.skip_install: - choice = confirm("faster-qwen3-tts is not installed. " - "pip install it now?", default=True) - if choice is None: - return None - do_install = choice - - # Step 1: clone (if not cloned and not skipped). - do_clone = False - if not _is_cloned() and not args.skip_clone: - choice = confirm(f"faster-qwen3-tts repo not cloned. Clone it into " - f"./app/{FASTER_DIR_NAME}?", default=True) - if choice is None: - return None - do_clone = choice - - # Step 2: voices.json — an existing one seeds the defaults (modify flow) - # instead of an overwrite prompt. - existing_voices = {} + # An existing voices.json seeds the defaults (modify flow) instead of an + # overwrite prompt; its voices also seed the wav-directory browser. default_output = args.output if default_output is None and _is_cloned(): default_output = _checkout() / "voices.json" + existing_voices = {} if default_output is not None and default_output.exists() \ and not args.force: existing_voices = load_voices(default_output) - + s["default_output"] = default_output + s["existing_voices"] = existing_voices wav_start = VOICES_DIR if existing_voices: ref_dirs = {Path(voice["ref_audio"]).parent @@ -228,17 +223,58 @@ def _wizard(stdscr, args: argparse.Namespace) -> Optional[dict]: if isinstance(voice, dict) and voice.get("ref_audio")} if len(ref_dirs) == 1: wav_start = next(iter(ref_dirs)) - - wav_dir = args.input_dir - if wav_dir is None: + s["wav_start"] = wav_start + + def screen_install(): + choice = tui.confirm(stdscr, "faster-qwen3-tts is not installed. " + "pip install it now?", default=True, + cancel_value=_GO_BACK) + if choice is _GO_BACK: + return tui.Wizard.BACK + s["do_install"] = choice + return _after_install() + + def _after_install(): + if not _is_cloned() and not args.skip_clone: + return screen_clone + s["do_clone"] = False + return _after_clone() + + def screen_clone(): + choice = tui.confirm( + stdscr, f"faster-qwen3-tts repo not cloned. Clone it into " + f"./app/{FASTER_DIR_NAME}?", default=True, + cancel_value=_GO_BACK) + if choice is _GO_BACK: + return tui.Wizard.BACK + s["do_clone"] = choice + return _after_clone() + + def _after_clone(): + if args.input_dir is None: + return screen_wav + s["wav_dir"] = args.input_dir + return _after_wav() + + def screen_wav(): wav_dir = tui.browse_directory( stdscr, "Select the directory with your .wav voices", info=common.wav_dir_info, preview=common.wav_dir_preview, - start=wav_start) - language = args.language - if language is None: + start=s["wav_start"], back_value=_GO_BACK) + if wav_dir is _GO_BACK: + return tui.Wizard.BACK + s["wav_dir"] = wav_dir + return _after_wav() + + def _after_wav(): + if args.language is None: + return screen_language + s["language"] = args.language + return _after_language() + + def screen_language(): default_language = config.LANGUAGE - for voice in existing_voices.values(): + for voice in s["existing_voices"].values(): if isinstance(voice, dict) and voice.get("language"): default_language = voice["language"] break @@ -247,51 +283,89 @@ def _wizard(stdscr, args: argparse.Namespace) -> Optional[dict]: validate=lambda s: None if _try_language(s) else "Unknown language (e.g. English, en)", help_lines=["Language for every voice, as passed to the TTS " - "model (names or short codes accepted)"]) - language = lang_text - whisper_model = args.whisper_model - if whisper_model is None: + "model (names or short codes accepted)"], + back_value=_GO_BACK) + if lang_text is _GO_BACK: + return tui.Wizard.BACK + s["language"] = lang_text + return _after_language() + + def _after_language(): + if args.whisper_model is None: + return screen_whisper + s["whisper_model"] = args.whisper_model + return _after_whisper() + + def screen_whisper(): whisper_model = tui.menu( stdscr, "Whisper model for transcription", [(m, m) for m in WHISPER_MODELS], - default_index=WHISPER_MODELS.index("base")) - output_path = args.output - if output_path is None: + default_index=WHISPER_MODELS.index("base"), + back_value=_GO_BACK) + if whisper_model is _GO_BACK: + return tui.Wizard.BACK + s["whisper_model"] = whisper_model + return _after_whisper() + + def _after_whisper(): # Default into the cloned checkout; fall back to the wav directory # when the checkout is not present (so a flag-only run still works). - output_path = (_checkout() / "voices.json") if _is_cloned() \ - else (wav_dir / "voices.json") - - # Transcription plan: re-transcribe only new voices (or all of them) — - # the "re-transcribe anyway?" offer appears even when nothing is new. - plan: Optional[dict] = {"mode": "all", "missing": [], "existing": {}} - wav_files = find_wav_files(wav_dir) - if wav_files and existing_voices and not args.force: - plan = _decide_faster_transcription(wav_files, existing_voices, - confirm) + s["output_path"] = args.output + if s["output_path"] is None: + s["output_path"] = (_checkout() / "voices.json") if _is_cloned() \ + else (s["wav_dir"] / "voices.json") + wav_files = find_wav_files(s["wav_dir"]) + if wav_files and s["existing_voices"] and not args.force: + return screen_transcription + s["plan"] = {"mode": "all", "missing": [], "existing": {}} + return _after_transcription() + + def screen_transcription(): + # Re-transcribe only new voices (or all of them) — the + # "re-transcribe anyway?" offer appears even when nothing is new. + wav_files = find_wav_files(s["wav_dir"]) + plan = _decide_faster_transcription( + wav_files, s["existing_voices"], _confirm) if plan is None: - return None + return tui.Wizard.BACK + s["plan"] = plan + return _after_transcription() + + def _after_transcription(): + if args.port is None: + return screen_port + s["port"] = args.port + return _finalize() - # Step 3: port + default voice. - port = args.port - if port is None: + def screen_port(): port_text = tui.line_edit( stdscr, "Server port", str(_config_port()), validate=lambda s: None if (s.isdigit() and 1 <= int(s) <= 65535) - else "Enter a port number between 1 and 65535") - port = int(port_text) + else "Enter a port number between 1 and 65535", + back_value=_GO_BACK) + if port_text is _GO_BACK: + return tui.Wizard.BACK + s["port"] = int(port_text) + return _finalize() + + def _finalize() -> dict: + return { + "do_install": s.get("do_install", False), + "do_clone": s.get("do_clone", False), + "wav_dir": s["wav_dir"], + "language": s["language"], + "whisper_model": s["whisper_model"], + "output_path": s["output_path"], + "port": s["port"], + "force": args.force, + "plan": s["plan"], + } - return { - "do_install": do_install, - "do_clone": do_clone, - "wav_dir": wav_dir, - "language": language, - "whisper_model": whisper_model, - "output_path": output_path, - "port": port, - "force": args.force, - "plan": plan, - } + if not _is_installed() and not args.skip_install: + first = screen_install + else: + first = _after_install() + return tui.Wizard().run(first) def _try_language(value: str) -> bool: @@ -359,6 +433,23 @@ def _print_launch_hint(voices_path: Path, port: int) -> None: print(f" then run it with --voices {voices_path} --port {port}") +def setup_screen(stdscr) -> int: + """Run the setup wizard on an existing curses screen (the hub's). + + The hub drives this as one screen of its own ``tui.Wizard`` stack, so + Esc on the wizard's first screen simply returns here and the hub pops + back to the menu that launched it. The console tail (install/clone/ + transcribe/write) runs under ``tui.suspend`` so the hub's curses + session stays intact. Returns 0 on completion, 1 when the user aborted. + """ + args = build_parser().parse_args([]) + settings = _wizard(stdscr, args) + if settings is None: + return 1 + with tui.suspend(stdscr): + return _execute(settings) + + def run_tui(args: Optional[argparse.Namespace] = None) -> int: """Run the faster setup wizard end-to-end.""" import curses |
