1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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.
|