Start on live transcription and python sidecar (faster-whisper)

This commit is contained in:
Lucas Winther committed 2026-07-10 09:28:42 +02:00
1 parent 15bf14728a
commit 033ea32968
14 files changed
+1376 -61

No files matched your search

+79 -10
View File
@@ -128,19 +128,57 @@ Notes (M4):
offline) fail silent to anonymous — the app is offline-first.
## M5 — Server engine + toggle
- [ ] Server-side pipeline in `src/server/transcribe.ts` — **faster-whisper**
- [x] Server-side pipeline in `src/server/transcribe.ts` — **faster-whisper**
(CTranslate2) via a Python sidecar in `server-py/`, spawned/supervised by
Bun (D-013 supersedes the Transformers.js-under-Bun plan); map the shared
model catalog to `Systran/faster-whisper-*` model ids in `src/lib/models.ts`
- [ ] Degrade gracefully when Python/the sidecar is absent (server engine
model catalog to faster-whisper model ids in `src/lib/models.ts`
- [x] Degrade gracefully when Python/the sidecar is absent (server engine
unavailable; browser engine untouched — anonymous path deploys with Bun alone)
- [ ] `POST /api/transcribe` — auth-guarded, streamed JSON-lines response (D-011),
upload size limit, request abort handling
- [ ] `src/lib/engine.ts` — `Engine` interface; `BrowserEngine` (wraps worker) and
- [x] `POST /api/transcribe` — auth-guarded, streamed JSON-lines response (D-011),
upload size limit, request abort handling; plus `GET /api/transcribe/status`
(availability probe for the toggle)
- [x] `src/lib/engine.ts` — `Engine` interface; `BrowserEngine` (wraps worker) and
`ServerEngine` (wraps fetch stream) (D-010)
- [ ] `EngineToggle` component — visible only with a session; persists choice
- [ ] Server model caching/warm-up strategy (don't reload weights per request)
- [ ] Integration test: same fixture audio through both engines, transcripts agree
(persistence helpers + status probe already in `lib/engine.ts`; remaining:
the component, lifting `useAuth` out of `AuthForms` into `App` so both see
one session state, and routing the file flow through the selected engine —
live stays on `BrowserEngine` per D-016)
- [x] Server model caching/warm-up strategy (don't reload weights per request)
- [~] Integration test: same fixture audio through both engines, transcripts agree
— supervisor/HTTP layers covered in `tests/transcribe.test.ts` (real
`Bun.spawn` against `tests/fixtures/fake_sidecar.py`); real inference is
the opt-in `FW_INTEGRATION=1` test + manual run in `tests/server-engine.md`
(no pip/browser in the dev environment; same caveat as M2/M3)
- [ ] Deployment: optional Python/faster-whisper layer for the Docker image +
deployer note (base image stays Bun-only; install
`server-py/requirements.txt` to enable the server engine)
Notes (M5):
- Sidecar protocol (`server-py/sidecar.py`): JSON-lines over stdio —
`ready`/`fatal` handshake, `transcribe` → `segment`* → `done`, `cancel`
between segments, `shutdown`/EOF exits. Audio is handed over as a temp file
of raw float32 mono 16 kHz PCM (exactly what the browser engine consumes),
read with `np.fromfile` — no container decoding server-side. The last-used
model stays loaded (replaced on change, so memory is bounded to one model);
device/compute via `FASTER_WHISPER_DEVICE`/`FASTER_WHISPER_COMPUTE`
(default auto/int8).
- Supervision (`createSidecar`): lazy spawn on first use; a crash rejects
in-flight requests and the next request respawns; a missing runtime
(no python3 / `fatal` handshake) is remembered as a *permanent* unavailable
reason — surfaced verbatim by `GET /api/transcribe/status` and 503s — so a
Bun-only deployment never respawn-storms. Tests force this path with
`python3 -S -I` (site-packages off) against the real sidecar.
- Upload limit: `TRANSCRIBE_MAX_UPLOAD_BYTES` (default 512 MiB ≈ 2¼ h of raw
f32 PCM), enforced pre-read via Content-Length *and* while streaming the
body to the temp file. Errors after the 200 header travel in-stream as an
`{"type":"error"}` line (wire format shared with the client via
`src/lib/engine.ts`, so server and `ServerEngine` cannot drift).
- `AUTH_ENABLED=false` (D-015) 404s `/api/transcribe*` along with the auth
routes — the server engine is session-gated, so it disappears with auth.
- Smoke-tested live against the dev server: `/` + auth unchanged; status
401 → (register) → 200 `{available:false, reason:"faster-whisper is not
installed…"}`; POST → 503 with the same reason, body untouched.
## M6 — Hardening & polish
- [ ] Error taxonomy: unsupported file, out-of-memory (suggest smaller model),
@@ -151,9 +189,40 @@ Notes (M4):
- [ ] Basic a11y pass (keyboard, focus, aria on progress/live transcript)
- [ ] Production build + `bun start` deployment check; README for deployers
## M7 — Live microphone transcription (browser engine, D-016)
Promoted out of the out-of-scope list via D-016. Browser engine only: audio
never touches the network, anonymous-first, works offline once a model is
cached.
- [x] Protocol: optional `requestId` on `transcribe`, echoed on
`partial`/`result`/`error` so the live loop can correlate responses
(file flow unchanged without ids)
- [x] `src/lib/live.ts` — pure, unit-tested segment loop: RMS energy gate,
`LiveSegmenter` (speech onset + pre-roll, silence/max-length commits,
pure silence dropped), `LiveTranscriber` (single-flight scheduling,
final passes before throttled tentative refreshes, stop/drain,
absolute-timestamped chunks)
- [x] `src/lib/mic.ts` — `getUserMedia` + AudioWorklet chunker from a Blob URL
(ScriptProcessorNode fallback), native-rate capture resampled to 16 kHz
on the main thread (D-016)
- [x] `LiveControls` component — start/stop button + recording indicator;
mutually exclusive with the file flow
- [x] Live transcript UI: committed text + visually-distinct tentative tail in
`TranscriptView`; final stop() result reuses the normal transcript view
(copy button, timestamps)
- [x] Unit tests: segmenter behavior, transcriber scheduling with a fake
engine and injected clock, protocol `requestId` guards
- [~] Manual in-browser verification (mic permission, live text, offline run)
— procedure in `tests/live.md`, pending a real browser (same dev-env
caveat as M2/M3)
- [ ] **Future want:** live transcription on the **server engine** — stream
mic audio over a WebSocket (D-011's escape hatch) to faster-whisper
(D-013) for logged-in users on weak devices. Needs its own decision
entry; blocked on M5 landing first.
---
## Deliberately out of scope (for now)
Microphone/live transcription, arbitrary Hub model ids, OAuth providers,
multi-user transcript storage, whisper.cpp server backend, i18n of the UI.
Arbitrary Hub model ids, OAuth providers, multi-user transcript storage,
whisper.cpp server backend, i18n of the UI.
Promote to a milestone via a new entry in `Decisions.md` first.