aboutsummaryrefslogtreecommitdiff
path: root/.opencode/plans
diff options
context:
space:
mode:
Diffstat (limited to '.opencode/plans')
-rw-r--r--.opencode/plans/1776156239868-shiny-island.md185
1 files changed, 185 insertions, 0 deletions
diff --git a/.opencode/plans/1776156239868-shiny-island.md b/.opencode/plans/1776156239868-shiny-island.md
new file mode 100644
index 0000000..c6c69e7
--- /dev/null
+++ b/.opencode/plans/1776156239868-shiny-island.md
@@ -0,0 +1,185 @@
+# Plan: backend setup states, always-buildable audio.cpp, in-TUI long steps
+
+## Problem summary
+
+1. **Status granularity**: `audiocpp.detect()` sets `installed = (binary built)`. A cloned-but-not-built
+ checkout collapses into "unavailable" (red, dimmed) — indistinguishable from "nothing there".
+2. **Build trap**: once a `server.json` exists, the wizard's `_after_sync()` hits
+ `existing_backend in BACKENDS → build=False` and *skips the build offer forever*. Declining the
+ build on first install → backend stays "unavailable" with no TUI path to build it. The hub's
+ "Download Missing Models" action is also gated on `status.installed`, so it disappears too.
+3. **Console drops**: clone (`audiocpp._wizard`), the whole `_execute` tail (build / whisper
+ transcription / model downloads — all three backends' `setup_screen`), and the hub's
+ "Download Missing Models" action all run under `tui.suspend`, dumping the user to the console.
+
+## Existing machinery to reuse
+
+- `ui/runview.py`: proven pattern for a full-screen curses view driven by a worker thread + event
+ queue + timed `getch` redraw + Esc→confirm-cancel.
+- `app/audio.cpp/tools/model_manager_v2.py install <pkg> --progress --cancel-file F`: emits
+ `AUDIOCPP_PROGRESS downloaded=N total=M` lines and supports graceful cancel (already upstream).
+- `git clone --progress` emits `Receiving objects: NN%`; cmake/make emit `[ NN%]`, ninja `[d/t]`.
+- `tests.test_tui.FakeCurses/FakeScreen`: fake terminal for widget tests (as in test_runview.py).
+
+## Design
+
+### 1. New `partial` state label on `BackendStatus`
+
+`app/backends/__init__.py`: add `partial: str = ""` — an optional, more specific label for a
+backend that is set up only part-way. Documented values used by audio.cpp:
+- checkout exists, no `audiocpp_server` binary → `"downloaded (not built)"`
+- binary built, no `server.json` → `"built (not configured)"`
+
+`app/ui/hub.py::_status_mark` (before the installed/unavailable branches, after running):
+- `status.partial` set → `(status.partial, "warn", "dim" if not status.installed else "body")`
+ (amber text; dimmed name while still unusable, matching the "dim = unusable" convention).
+- `audiocpp.detect()` populates `partial`; details lines already carry specifics.
+ qwen/faster unchanged (they can adopt `partial` later).
+
+### 2. New in-TUI task view: `app/ui/taskview.py`
+
+Full-screen DOS-style view (mirrors runview.py conventions) that runs an ordered list of steps
+inside the hub's curses session:
+
+```python
+@dataclass
+class TaskStep:
+ title: str
+ work: Callable[[emit, cancel], int] # emit(line) streams output; cancel is a threading.Event
+
+def run_steps(scr, title: str, steps: list[TaskStep]) -> int # 0 = all ok
+```
+
+- Worker thread runs steps sequentially; events over a queue: `step_start`, `line`, `step_done`,
+ `done`. Main thread redraws on a 250 ms `getch` timeout.
+- Layout: dialog frame with the step list (`[OK]` / `[..]`+spinner+elapsed / `[FAIL]` / pending),
+ an optional progress bar under the current step, and a dim scrolling log tail (last ~10 lines).
+- Progress parsing per emitted line (bar hidden until first match):
+ - `AUDIOCPP_PROGRESS downloaded=(\d+) total=(\d+)` → exact bytes bar (line not shown in log);
+ - `(\d{1,3})%` (git `Receiving objects: 45%`, make `[ 45%]`) → percent bar;
+ - `[(\d+)/(\d+)]` (ninja) → count bar.
+- Esc/q while running → `tui.confirm("Cancel?")` → sets `cancel` (subprocess runners kill the
+ child; in-process steps check between units of work) → summary shows "cancelled".
+- Terminal state: per-step marks + result line; waits for a keypress so failures never scroll away.
+- `run_steps` returns the first non-zero step rc (0 when all succeed), so callers can flash/raise.
+
+### 3. Streaming subprocess support: `app/backends/common.py`
+
+- Extend `run_console_subprocess(argv, cwd=None, *, emit=None, cancel=None, on_cancel=None)`:
+ - `emit=None` → today's behavior (inherit terminal; used by CLI/non-TUI paths).
+ - `emit` given → `Popen(stdout=PIPE, stderr=STDOUT, start_new_session=True)`; read chunks,
+ split lines on `\n` and `\r` (covers tqdm/git `\r` updates), call `emit(line)`.
+ - On `cancel.is_set()`: call `on_cancel()` if given (e.g. touch a `--cancel-file`), then
+ killpg TERM → grace → KILL (same escalation as `servers.py`); return 130.
+- `git_clone(url, target, *, emit=None, cancel=None)`: adds `--progress` when emitting.
+
+`app/backends/envs.py`: `pip_install(packages, *, emit=None, cancel=None)` threads the args
+through; passes `--progress-bar off` when emitting (clean `Downloading X (12 MB)` lines instead
+of carriage-return spam). `pip_uninstall` unchanged (hub uninstall stays on the console).
+
+### 4. audio.cpp backend (`app/backends/audiocpp.py`)
+
+**Wizard — always offer the build when not built:**
+- `_after_sync()`: drop the `existing_backend in BACKENDS → build=False` early return. The backend
+ menu is skipped only when `detected_backend` is not None (a unique built backend exists) or a
+ flag supplied the value. Otherwise → `screen_backend`.
+- `screen_backend`: default-select `existing_backend` (recorded in server.json) when present;
+ neutral title ("Which inference backend should audiocpp_server use?") since it now serves both
+ the not-built and the multi-build cases.
+- `screen_build`: shown whenever the wizard's chosen backend has no built binary — add
+ `built_server_binary(audiocpp_dir, backend) -> Optional[Path]` (per-backend check under
+ `build/*<backend>*/bin/`) and skip the prompt when it exists (multi-build checkouts picking an
+ already-built backend don't get a pointless build offer). Declining stays possible, but now the
+ hub offers a dedicated build action afterwards (below), so "not now" no longer strands the user.
+
+**Step-based `_execute`:**
+- Refactor `_execute(settings, args, emit=None, cancel=None)` into
+ `_execute_steps(settings, args) -> list[taskview.TaskStep]` plus a thin runner: console paths
+ (`run_tui`, `main`) run the steps with `emit=None` (byte-identical console output to today);
+ `setup_screen` runs them via `taskview.run_steps(stdscr, "Setting up audio.cpp", steps)` — no
+ more `tui.suspend`. Steps: build (if `settings["build"]`), transcribe voices (cancel checked
+ between .wav files), write server.json + config sync, delete unused models (modify flow),
+ download models (if `settings["download"]`); the launch hint is the final log line. A failing
+ step marks `[FAIL]` and the run continues where today it would only warn (build failure still
+ writes server.json); `run_steps`' rc feeds the hub flash.
+- `build_audiocpp(audiocpp_dir, backend, *, emit=None, cancel=None)`: streams via the new helper.
+- `_install_models(..., emit=None, cancel=None)` / `install_models(...)`: when emitting, probe
+ `model_manager_v2.py` once for `--progress` support (source contains `AUDIOCPP_PROGRESS`) and
+ pass `--progress --cancel-file <tmp>`; the cancel file is wired as the runner's `on_cancel`.
+ Without support (older checkout) or emit=None: today's plain streaming.
+- Wizard clone step: `taskview.run_steps(stdscr, "Clone audio.cpp", [clone step])` instead of
+ `tui.suspend`; on rc != 0 raise `_TuiError` exactly as today.
+- `detect()`: set `partial` per the states above.
+
+**New `build_screen(stdscr)`** (exported for the hub): when the checkout has no built binary —
+menu to pick the backend (default: server.json's `backend`, else cuda) → `taskview.run_steps`
+with the build step → on success, if a server.json exists, rewrite its `backend` field to the
+built backend and flash "build complete"; on failure flash the log tail hint. Returns 0/1.
+
+### 5. faster + qwen backends
+
+- Same `setup_screen` switch: `_execute(settings)` → `_execute_steps(settings)` +
+ `taskview.run_steps(...)`; console paths keep `emit=None`.
+ - faster steps: pip install `faster-qwen3-tts[demo]`, git clone, transcribe + write voices.json,
+ sync config.
+ - qwen steps: pip install `qwen-tts`, sync config.
+- `detect()` unchanged (no `partial` states for now).
+
+### 6. Hub (`app/ui/hub.py`)
+
+- `_status_mark`: `partial` handling (above) + docstring.
+- `screen_configure`:
+ - New action **"Build audio.cpp server"** when `audiocpp_status` has a checkout but no built
+ binary (`audiocpp_backend.find_local_checkout()` + `find_audiocpp_server_bin()`), calling
+ `audiocpp_backend.build_screen(self.stdscr)` as an inline action (continue loop).
+ - "Download Missing Models (audio.cpp)": gate on `audiocpp_status.configured` (server.json
+ exists) instead of `.installed` — a not-built backend can still need its models.
+- `_download_models_action`: run `install_models` through `taskview.run_steps` (one step per
+ install id, real byte-progress via `AUDIOCPP_PROGRESS`) instead of `tui.suspend`.
+- Server start/stop + uninstall actions: unchanged (still `tui.suspend`) — out of scope.
+
+### 7. Statuses shown after the change (audio.cpp)
+
+| state | status text | color |
+|---|---|---|
+| no checkout | `unavailable` | red, dim name |
+| checkout, not built | `downloaded (not built)` | amber, dim name |
+| built, no server.json | `built (not configured)` | amber |
+| built + server.json | `installed` / `installed (models missing)` | amber |
+| server up | `running [local]` etc. | green |
+
+### 8. Tests
+
+- `tests/test_hub.py`: `_status_mark` partial cases; configure-menu test for the new Build action
+ presence/absence; Download-Missing-Models gating on `configured`.
+- `tests/test_backends_audiocpp.py`: update `SetupScreenTests` (patch `taskview.run_steps`
+ instead of `tui.suspend`); wizard tests for the new `_after_sync`/`screen_build` flow (modify
+ flow with existing server.json now offers the build; already-built backend skips it);
+ `detect()` partial cases; `built_server_binary`.
+- `tests/test_backends_faster.py` / `tests/test_backends.py` (qwen): same setup_screen update.
+- `tests/test_backends_envs.py`: `pip_install` call now passes emit/cancel kwargs — adjust
+ assertions.
+- New `tests/test_taskview.py`: reuse `FakeCurses`/`FakeScreen` — step sequencing, progress-line
+ parsing (`AUDIOCPP_PROGRESS`, `%`, `[d/t]`), cancel flow, failure marks.
+- New tests for `common.run_console_subprocess` streaming mode (emit lines, cancel → 130).
+
+### 9. Docs
+
+- `app/docs/backend-audiocpp.md`: mention the Build action + the new intermediate statuses.
+- README Quick Start step 4: one sentence that install/clone/build/download run inside the TUI
+ with progress.
+- Module docstrings (`backends/__init__.py` BackendStatus, hub `_status_mark`, audiocpp wizard
+ docstring) updated to match. No AGENTS.md exists.
+
+## Out of scope
+
+- Standalone CLI paths (`python app/backends/audiocpp.py`, non-interactive runs) keep the plain
+ console tail — the TUI improvements target the hub session.
+- Hub server start/stop and uninstall console drops.
+
+## Verification
+
+- `python -m pytest app/tests -x -q` (full suite) plus targeted runs of the touched test files.
+- Manual smoke (described in code review): drive the hub against a fake checkout to see
+ `downloaded (not built)` → Build action → `installed`; decline build in the wizard → status +
+ Build action remain.