aboutsummaryrefslogtreecommitdiff
path: root/app/backends/faster.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-24 20:17:11 -0400
committerhistoria <historiavg@proton.me>2026-08-24 20:17:11 -0400
commitcf24fa74188cee498eeb7b94422371c952278d4a (patch)
tree302244a8b09b9173fbde542874cfa549f26be7a2 /app/backends/faster.py
parent0522e73b68291af62e43c387ab8f9b8ffa2cab47 (diff)
downloadtts-audiobook-generator-cf24fa74188cee498eeb7b94422371c952278d4a.tar.gz
fix: stepping back steps in tui
Diffstat (limited to 'app/backends/faster.py')
-rwxr-xr-xapp/backends/faster.py221
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