- Fix: index.html <link> hrefs must be relative for Bun's HTML loader (absolute URLs failed the build and dev serve); ensureIcons() now runs before bundling in both scripts/build.ts and src/index.ts - tests/offline.md: manual airplane-mode verification procedure - WorkBreakdown: M3 checked ([~] on the manual browser run), M4 all done with notes; M5 retargeted to faster-whisper sidecar - Architecture/CLAUDE.md/Decisions: server engine = faster-whisper (CTranslate2 Python sidecar, D-013); browser stays Transformers.js (no WASM build of CTranslate2 exists) - bun test: 83 pass, tsc clean, production curl smoke-test green Co-Authored-By: Claude Fable 5 <[email protected]>
9.0 KiB
9.0 KiB
Work Breakdown
Milestones are ordered so that every milestone ends in a demoable, working app. Within a milestone, tasks are roughly in dependency order. Check items off as they land; add follow-up tasks where they belong rather than in a separate backlog.
Legend: [ ] todo · [x] done · [~] in progress
M0 — Scaffold cleanup & docs (small)
Current state is the raw bun init React template.
Architecture.md,Decisions.md,WorkBreakdown.mdcreated- Remove template cruft:
APITester.tsx, hello routes insrc/index.ts, Bun/React logo branding inApp.tsx/index.html - Add
@huggingface/transformersdependency - Target directory layout from
Architecture.md§3 (empty modules with TODOs are fine) - Smoke test in
tests/provingbun testruns
M1 — Browser transcription MVP (core)
Anonymous user can transcribe an uploaded file with one hardcoded model. This is the riskiest milestone — do it first, learn from it.
src/worker/protocol.ts— typed messages: load / load-progress / transcribe / partial / result / errorsrc/worker/inference.worker.ts— Transformers.js ASR pipeline (onnx-community/whisper-tiny.enhardcoded, WASM only for now), chunked with timestamps, partials streamed viaWhisperTextStreamersrc/lib/audio.ts— decode upload → 16 kHz mono Float32Array (D-012)FileDropzonecomponent (drag-drop + file picker, audio/* and video/*)TranscriptViewcomponent — streaming partials, final text, copy button- Model download progress UI (bytes / %, per-file)
- Wire into
App.tsx; delete remaining template UI - Unit tests for audio resampling and protocol handling
Notes (M1):
- Bun 1.3 does not bundle
new Worker(new URL(...))graphs (verified against bothbun buildand the dev server), sosrc/index.tsbundles the worker withBun.buildat startup and serves it at/inference.worker.js; the UI spawnsnew Worker("/inference.worker.js", { type: "module" }). Revisit if Bun gains native worker bundling. - onnxruntime-web fetches its
.wasmbinaries from the jsdelivr CDN (Transformers.js default). Fine for M1; M3 (offline) must serve/cache them locally.
M2 — Model choice + WebGPU (core)
src/lib/models.ts— curated catalog with sizes and dtype defaults (D-006)ModelPickercomponent — size estimates, English-only vs multilingual, downloaded-state badge (query Cache API)- WebGPU detection in worker,
deviceselection with WASM fallback (D-004); surface active backend in UI - Handle model switching (dispose old pipeline, load new)
- Language / task options for multilingual models (transcribe vs translate)
- [~] Verify quantized variants (q4/q8/fp16) on both backends; record working
combinations in
models.ts
Notes (M2):
- Quantized-variant verification is pending manual in-browser testing (no real
browser in the dev environment).
models.tsencodes the community-known-good combinations — WASM: q8 (tiny→small) / q4 (medium, large-v3-turbo); WebGPU: fp32 encoder+decoder for tiny/base (fp16 encoders known-bad there), fp16 encoder + q4 merged decoder for small/medium/large-v3-turbo — each with averifiedflag and note. Onlywhisper-tiny.en @ wasm/q8(exercised in M1) is markedverified: true; flip the flags as manual testing confirms each. - Downloaded badge (D-005):
src/lib/modelCache.tsinspects Cache Storage for transformers-cache entries with.onnxhits per model id; M3's offline indicator should reusegetDownloadedModels.
M3 — PWA & offline (core)
manifest.webmanifest+ icons; installabilitysrc/pwa/sw.ts— app-shell precache, cache-first static, network-only API (D-007)- Build step: emit precache manifest from
bun buildoutput into the SW - SW registration + update flow (new-version toast)
- [~] Offline verification: airplane-mode transcription with a previously
downloaded model; document the manual test procedure in
tests/offline.md— procedure written (tests/offline.md), pending a manual run in a real browser (none in the dev environment; same caveat as M2's variants) - "Available offline" indicator per model in
ModelPicker
Notes (M3):
sw.tsships with"__PRECACHE_MANIFEST__"/__CACHE_VERSION__string placeholders;src/pwa/precache.ts(pure, unit-tested) splices in the real values. Production:scripts/build.tsinjects the hasheddist/asset list and a content-hash version intodist/sw.js. Dev:src/index.tsinjects an empty list +"dev", so the shell is runtime-cached network-first and HMR keeps working. SW bundles are never minified — the placeholders must survive verbatim.- onnxruntime's
.wasm/.mjsare served from our origin at/ort/*(dev: straight fromnode_modules/onnxruntime-web/dist; prod: copied todist/ort/) instead of the jsdelivr CDN, and the SW runtime-caches them cache-first intowhisper-runtime-*— required for offline inference, too big to precache. Model weights stay in Transformers.js's owntransformers-cache, which the SW never touches (D-005). - Bun's HTML loader must resolve every
<link href>to a real file, soindex.htmlreferences./pwa/manifest.webmanifest/./pwa/icons/...relatively (absolute/...URLs fail the build — bug found while verifying M3). The bundler emits hashed copies; the fixed/manifest.webmanifestand/icons/*URLs are still served for the manifest's internal icon refs.ensureIcons()must run before the HTML is bundled (icons are generated, not checked in). - Production smoke-tested via curl:
/and deep links → shell (no-cache), hashed chunks → immutable,/sw.js→ no-cache,/manifest.webmanifest→application/manifest+json,/ort/*.wasm→application/wasm,/api/*→ 404 network-only. Browser-level behavior (install, airplane mode, update toast) is manual — seetests/offline.md.
M4 — Accounts (Passport.js)
src/server/db.ts— bun:sqlite schema: users, sessions (D-009)- Connect-compat adapter + Passport local strategy in
src/server/auth.ts(D-008) - Routes:
POST /api/auth/register|login|logout,GET /api/auth/me - Password hashing via
Bun.password(Argon2) - Frontend: login/register forms, session-aware header, logged-out ⇒ no behavioral change (anonymous-first invariant)
- Auth tests (register/login/logout, session expiry, wrong password)
Notes (M4):
- DB path env var is
DB_PATH(default./data/app.db, gitignored); tests injectcreateDb(":memory:")and never touch disk. Session ids are 256-bit base64url; expiry is enforced server-side viasessions.expires_at, not just the cookie. Cookie:sid, HttpOnly, SameSite=Lax, Max-Age 7d, Secure in production. - One shared
/api/*dispatcher insrc/index.tsserves both the devroutestable and the productionfetchhandler — new API routes go there. src/server/auth.tsexportsgetSessionUser(request)for M5's auth-guarded/api/transcribe. Frontend auth state lives insrc/lib/useAuth.tsover the framework-freesrc/lib/authClient.ts;/api/auth/mefailures (401 or offline) fail silent to anonymous — the app is offline-first.
M5 — Server engine + toggle
- Server-side pipeline in
src/server/transcribe.ts— faster-whisper (CTranslate2) via a Python sidecar inserver-py/, spawned/supervised by Bun (D-013 supersedes the Transformers.js-under-Bun plan); map the shared model catalog toSystran/faster-whisper-*model ids insrc/lib/models.ts - 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 handlingsrc/lib/engine.ts—Engineinterface;BrowserEngine(wraps worker) andServerEngine(wraps fetch stream) (D-010)EngineTogglecomponent — 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
M6 — Hardening & polish
- Error taxonomy: unsupported file, out-of-memory (suggest smaller model), WebGPU device-lost, network loss mid-download (resume or clear retry)
- Cancel in-flight transcription (both engines)
- Export transcript: .txt and .srt (timestamps already available)
- Storage management UI: list cached models, per-model delete
- Basic a11y pass (keyboard, focus, aria on progress/live transcript)
- Production build +
bun startdeployment check; README for deployers
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.
Promote to a milestone via a new entry in Decisions.md first.