Add cancel + progress UI, EngineToggle, and Docker deployment

This commit is contained in:
Lucas Winther
2026-07-14 19:32:10 +02:00
parent be82ecfe57
commit 0084e0435a
16 changed files with 972 additions and 60 deletions
+128
View File
@@ -0,0 +1,128 @@
# AU Whisper Web
Browser-first speech-to-text powered by OpenAI Whisper. Anonymous users run
Whisper entirely in their browser via
[Transformers.js](https://github.com/huggingface/transformers) (WebNN → WebGPU
→ WASM, installable PWA, works offline once a model is cached). Logged-in
users can optionally run inference on the **server** instead, backed by
[faster-whisper](https://github.com/SYSTRAN/faster-whisper) (CTranslate2)
through a small Python sidecar (`server-py/`, Decisions.md D-013).
See `Architecture.md` for the design, `Decisions.md` for the decision log, and
`CLAUDE.md` for the toolchain rules (Bun everywhere — no npm/node).
## Development
```sh
bun install
bun dev # dev server with HMR at http://localhost:3000
bun test # unit tests
```
## Production build (bare metal)
```sh
bun install
bun run build # emits self-contained dist/ (app shell, worker, sw.js, /ort/*)
bun start # NODE_ENV=production bun src/index.ts — serves dist/ + /api/*
```
To enable the server engine on bare metal, additionally install Python ≥ 3.9
and the sidecar dependencies: `python3 -m pip install -r server-py/requirements.txt`.
Nothing else changes — the sidecar is spawned lazily on the first server-engine
request.
## Deployment (Docker)
The `Dockerfile` is multi-stage and produces **two image flavors**:
| Flavor | Build command | Contents |
| --- | --- | --- |
| **Bun-only** (default) | `docker build -t whisper-web .` | Bun runtime + built `dist/`. No Python. |
| **With server engine** | `docker build --target with-server-engine -t whisper-web:server-engine .` | Everything above + Debian `python3` and `server-py/requirements.txt` (faster-whisper, CPU-only) in a virtualenv at `/opt/faster-whisper`. |
`--target` was chosen over a build-arg because the optional layer is a strict
superset of the runtime stage: `with-server-engine` extends `runtime`, so the
Bun layers are shared byte-for-byte between both flavors and there is no
conditional logic inside the Dockerfile. A plain `docker build .` (what
`.gitlab-ci.yml` runs) intentionally resolves to the Bun-only flavor.
Run either flavor the same way:
```sh
docker run -d -p 3000:3000 -v whisper-data:/app/data whisper-web
# or, with the server engine:
docker run -d -p 3000:3000 -v whisper-data:/app/data whisper-web:server-engine
```
`docker compose up --build` builds and runs the Bun-only flavor with the
`whisper-data` volume already wired up (see `docker-compose.yml`).
### Graceful degradation (why the Bun-only image is safe to deploy)
The server engine is optional by design (D-013):
- **Bun-only image:** everything anonymous users see works in full — browser
inference, model downloads, PWA/offline, and (if `AUTH_ENABLED` stays on)
accounts. The first server-engine probe finds no `python3`, the supervisor
records that as a permanent condition (no respawn storms), and
`GET /api/transcribe/status` answers `{"available": false, "reason": ...}`;
`POST /api/transcribe` answers `503` with the same reason. The UI simply
doesn't offer the server engine.
- **with-server-engine image:** `status` reports `available: true` and
logged-in users get the server/device toggle. The first request for a model
downloads its CTranslate2 weights from the Hugging Face Hub into
`FASTER_WHISPER_DOWNLOAD_ROOT` (inside the data volume, so restarts don't
re-download); the last-used model stays loaded between requests.
### Runtime environment variables
All configuration is runtime-only (D-015) — one image serves every
configuration; flip flags with `docker run -e` / compose `environment:`.
| Variable | Default | Meaning |
| --- | --- | --- |
| `PORT` | `3000` | Port the Bun server listens on (also honored by the image's `EXPOSE`/healthcheck). |
| `NODE_ENV` | `production` (set in the image) | `production` serves the prebuilt `dist/`; anything else runs the dev server. Leave as-is in Docker. |
| `AUTH_ENABLED` | `true` | D-015 feature flag. `false`/`0`/`no`/`off` disables login/register: `/api/auth/*` **and** `/api/transcribe*` return 404 (the server engine is session-gated, so it disappears with auth) and the client hides the Sign in UI. |
| `DB_PATH` | `/app/data/app.db` (image) / `./data/app.db` (bare metal) | SQLite database (users + sessions, D-009). Keep it inside the mounted volume. |
| `TRANSCRIBE_MAX_UPLOAD_BYTES` | `536870912` (512 MiB ≈ 2¼ h of audio) | Upload cap for `POST /api/transcribe` (raw float32 mono 16 kHz PCM), enforced via Content-Length and again while streaming. |
| `SIDECAR_PYTHON` | `python3` (with-server-engine image: `/opt/faster-whisper/bin/python3`) | Python interpreter used to spawn `server-py/sidecar.py`. Only override for exotic setups. |
| `FASTER_WHISPER_DEVICE` | `auto` | CTranslate2 device for the sidecar (`auto`, `cpu`, `cuda`). In the stock image `auto` resolves to CPU. |
| `FASTER_WHISPER_COMPUTE` | `int8` | CTranslate2 compute type (`int8` is the fast, low-memory CPU choice; e.g. `float16` on GPU). |
| `FASTER_WHISPER_DOWNLOAD_ROOT` | unset (with-server-engine image: `/app/data/faster-whisper-models`) | Where the sidecar caches downloaded model weights. |
There are no session-secret style variables: sessions are random 256-bit ids
stored server-side in SQLite; the cookie is `HttpOnly`, `SameSite=Lax`, and
`Secure` whenever `NODE_ENV=production`.
### Persistence
Mount a volume at **`/app/data`**. It holds:
- `app.db` — the SQLite database (users + sessions). Without the volume every
redeploy logs everyone out and drops accounts.
- `faster-whisper-models/` — (server-engine flavor) downloaded Whisper
weights, from ~75 MB (`tiny`) to ~1.6 GB (`large-v3-turbo`).
Browser-engine model weights never touch the server — they live in each
visitor's browser cache.
### HTTPS / reverse proxy
Run the container behind a TLS-terminating reverse proxy (Caddy, nginx,
Traefik…). HTTPS is **functionally required** in production, not just good
practice: service workers (PWA/offline), microphone capture
(`getUserMedia` for live transcription), WebGPU, and the `Secure` session
cookie all require a secure context. Plain HTTP only works on `localhost`.
The app uses relative URLs and also tolerates being served under a subpath.
### GPU note
The `with-server-engine` target is CPU-only on purpose (small-ish image, no
CUDA runtime). For GPU inference you would build your own image on an
`nvidia/cuda` base with cuDNN 9 + cuBLAS, install Bun and
`server-py/requirements.txt` the same way, run with `docker run --gpus all`,
and set `FASTER_WHISPER_DEVICE=cuda` (plus e.g.
`FASTER_WHISPER_COMPUTE=float16`). Not shipped here — CPU int8 already makes
the server engine several times faster than real time on modest hardware.