96 lines
5.2 KiB
Markdown
96 lines
5.2 KiB
Markdown
# Decisions
|
||
|
||
Lightweight ADR log. One entry per decision that shapes the codebase. Statuses:
|
||
**Accepted** (act on it), **Proposed** (default choice, revisit before building on it),
|
||
**Superseded** (kept for history, points to replacement).
|
||
|
||
Add new entries at the bottom with the next `D-###` id. Never rewrite an Accepted
|
||
entry to mean something else — supersede it.
|
||
|
||
---
|
||
|
||
## D-001 — Bun as the only runtime and toolchain
|
||
**Status:** Accepted (mandated by CLAUDE.md)
|
||
Bun replaces Node, vite/webpack (via `bun build` + HTML imports), jest/vitest
|
||
(`bun test`), express (`Bun.serve`), better-sqlite3 (`bun:sqlite`), and dotenv.
|
||
**Consequence:** no vite plugin ecosystem; PWA assets (service worker, manifest) are
|
||
wired by hand instead of via a plugin.
|
||
|
||
## D-002 — Transformers.js (`@huggingface/transformers`) for all Whisper inference
|
||
**Status:** Accepted (mandated by CLAUDE.md)
|
||
Same library in the browser (WebGPU/WASM) and on the server (ONNX Runtime under Bun).
|
||
**Why:** one model catalog, one pipeline API, no native whisper.cpp build step.
|
||
**Trade-off:** server CPU inference is slower than whisper.cpp; acceptable — the server
|
||
engine is a convenience for logged-in users, not the primary path.
|
||
|
||
## D-003 — Inference runs in a dedicated Web Worker
|
||
**Status:** Accepted
|
||
Model load + transcription block for seconds-to-minutes; running them on the main
|
||
thread would freeze the UI. Communication via a small typed postMessage protocol
|
||
(`src/worker/protocol.ts`) shared between UI and worker.
|
||
|
||
## D-004 — WebGPU with automatic WASM fallback
|
||
**Status:** Accepted
|
||
Detect via `navigator.gpu?.requestAdapter()` inside the worker; pass
|
||
`device: "webgpu" | "wasm"` to the pipeline. The active backend is surfaced in the UI
|
||
but users don't have to configure anything. Encoder-heavy Whisper benefits ~10-40× from
|
||
WebGPU where available (Chromium); WASM keeps Firefox/Safari working.
|
||
|
||
## D-005 — Model weights cached via the browser Cache API (Transformers.js default)
|
||
**Status:** Accepted
|
||
Transformers.js already persists Hub downloads in the Cache API (`useBrowserCache`).
|
||
We do **not** re-cache weights in the service worker — the SW precaches only the app
|
||
shell. **Consequence:** offline = installed PWA + at least one previously loaded model;
|
||
the UI must show per-model "downloaded / not downloaded" state (query
|
||
`caches` for hub URLs) so users can prepare for offline use.
|
||
|
||
## D-006 — Quantized model catalog, curated not exhaustive
|
||
**Status:** Proposed
|
||
Expose a curated list (`src/lib/models.ts`): `whisper-tiny(.en)`, `base(.en)`,
|
||
`small(.en)`, `medium(.en)`, `large-v3-turbo`, each with sensible dtype defaults
|
||
(e.g. q4/q8 on WASM, fp16/q4 on WebGPU) and a size estimate shown before download.
|
||
**Why not free-form model ids:** predictable UX, tested combinations, honest
|
||
size/VRAM warnings. Revisit if users ask for arbitrary Hub ids.
|
||
|
||
## D-007 — PWA: hand-rolled service worker, app-shell precache
|
||
**Status:** Accepted
|
||
No Workbox/vite-plugin-pwa (conflicts with D-001 tooling). `bun build` emits hashed
|
||
assets to `dist/`; a small build step writes the precache manifest into `sw.ts`.
|
||
Strategy: precache app shell, cache-first for static assets, network-only for `/api/*`.
|
||
|
||
## D-008 — Passport.js via a connect-compat adapter over Bun.serve
|
||
**Status:** Accepted (Passport mandated by CLAUDE.md; adapter is the how)
|
||
Passport expects connect/express-style `(req, res, next)` and `req.login()`. We are
|
||
not allowed to use express (D-001), so `src/server/auth.ts` provides a thin adapter:
|
||
wrap Bun's `Request` in a minimal req/res shim, invoke the passport authenticate
|
||
chain, translate the outcome back to a `Response`. Local (username/password)
|
||
strategy first; other strategies plug into the same adapter later.
|
||
**Alternative rejected:** dropping Passport for hand-rolled auth — CLAUDE.md names
|
||
Passport explicitly.
|
||
|
||
## D-009 — Sessions + users in `bun:sqlite`
|
||
**Status:** Accepted
|
||
Single-file SQLite DB (path from env, default `./data/app.db`). Tables: `users`
|
||
(id, username, password_hash — Argon2 via `Bun.password`), `sessions`
|
||
(id, user_id, expires_at). Cookie: HttpOnly, SameSite=Lax, Secure in production.
|
||
No Redis/Postgres until there's a scaling reason (would be `Bun.redis` / `Bun.sql`).
|
||
|
||
## D-010 — Engine abstraction: `BrowserEngine` and `ServerEngine` behind one interface
|
||
**Status:** Accepted
|
||
`src/lib/engine.ts` defines `Engine { load(model), transcribe(audio), onProgress }`.
|
||
The UI never branches on engine type beyond the toggle. Keeps the server engine
|
||
(milestone M5) from leaking into UI code built in M1–M4.
|
||
|
||
## D-011 — Server engine transport: streaming HTTP response first, WebSocket if needed
|
||
**Status:** Proposed
|
||
`POST /api/transcribe` returns a streamed (chunked JSON-lines) response with progress
|
||
and partial transcripts. Simpler than WebSockets (no connection lifecycle, works with
|
||
plain fetch + ReadableStream). Switch to `Bun.serve` websockets only if we need
|
||
bidirectional control (e.g. cancel mid-run) that abort signals can't cover.
|
||
|
||
## D-012 — Audio decoding on the main thread via Web Audio API
|
||
**Status:** Accepted
|
||
`decodeAudioData` (handles most audio and audio-in-video containers) → mono downmix →
|
||
resample to 16 kHz → transfer Float32Array to worker (zero-copy). No ffmpeg.wasm
|
||
unless we hit real container-support gaps; note in `src/lib/audio.ts` if we do.
|