WIP: M3 PWA/offline implementation, M4 auth (backend + frontend), D-013 faster-whisper

- M3: hand-rolled service worker (precache + runtime /ort caching), precache
  manifest injection, SW registration/update toast, manifest + generated icons,
  production build script, offline indicator in ModelPicker (D-005/D-007)
- M4 backend: bun:sqlite users/sessions (D-009), Passport local strategy behind
  connect-compat adapter (D-008), /api/auth routes wired into dev+prod serving,
  Bun.password Argon2id, 38 auth tests
- M4 frontend: AuthForms + useAuth + framework-free authClient, session-aware
  header, anonymous-first fail-silent offline handling, 18 client tests
- D-013: server engine (M5) will use faster-whisper via Python sidecar;
  browser side stays Transformers.js (CTranslate2 has no WASM build)
- bun test: 83 pass / tsc --noEmit clean
- Note: M3 doc closeout (WorkBreakdown/Architecture/tests/offline.md) still in
  flight from a background agent; follow-up commit will finish docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Fable 5 committed 2026-07-10 04:02:28 +02:00
1 parent eb72a2e646
commit 9f39a82982
38 files changed
+3857 -246

No files matched your search

+44 -14
View File
@@ -13,17 +13,26 @@ import {
describe("isUiToWorkerMessage", () => {
test("accepts valid load and transcribe messages", () => {
const load: UiToWorkerMessage = {
type: "load",
modelId: "onnx-community/whisper-tiny.en",
device: "wasm",
};
const transcribe: UiToWorkerMessage = {
type: "transcribe",
audio: new Float32Array(8),
};
expect(isUiToWorkerMessage(load)).toBe(true);
expect(isUiToWorkerMessage(transcribe)).toBe(true);
const loads: UiToWorkerMessage[] = [
{ type: "load", modelId: "onnx-community/whisper-tiny.en", device: "wasm" },
{ type: "load", modelId: "onnx-community/whisper-tiny", device: "webgpu" },
{ type: "load", modelId: "onnx-community/whisper-base", device: "auto" },
{ type: "load", modelId: "onnx-community/whisper-base", device: "auto", dtype: "q4" },
{
type: "load",
modelId: "onnx-community/whisper-large-v3-turbo",
device: "webgpu",
dtype: { encoder_model: "fp16", decoder_model_merged: "q4" },
},
];
const transcribes: UiToWorkerMessage[] = [
{ type: "transcribe", audio: new Float32Array(8) },
{ type: "transcribe", audio: new Float32Array(8), language: "da", task: "transcribe" },
{ type: "transcribe", audio: new Float32Array(8), language: "auto", task: "translate" },
];
for (const message of [...loads, ...transcribes]) {
expect(isUiToWorkerMessage(message)).toBe(true);
}
});
test("rejects malformed messages", () => {
@@ -31,7 +40,19 @@ describe("isUiToWorkerMessage", () => {
expect(isUiToWorkerMessage("load")).toBe(false);
expect(isUiToWorkerMessage({ type: "load" })).toBe(false);
expect(isUiToWorkerMessage({ type: "load", modelId: "x", device: "cuda" })).toBe(false);
expect(isUiToWorkerMessage({ type: "load", modelId: "x", device: "auto", dtype: "int3" })).toBe(false);
expect(
isUiToWorkerMessage({
type: "load",
modelId: "x",
device: "auto",
dtype: { encoder_model: "fp16" },
}),
).toBe(false);
expect(isUiToWorkerMessage({ type: "transcribe", audio: [1, 2, 3] })).toBe(false);
expect(
isUiToWorkerMessage({ type: "transcribe", audio: new Float32Array(4), task: "detect" }),
).toBe(false);
expect(isUiToWorkerMessage({ type: "result", text: "", chunks: [] })).toBe(false);
});
});
@@ -50,7 +71,8 @@ describe("isWorkerToUiMessage", () => {
progress: 10,
},
{ type: "load-progress", status: "done", file: "model.onnx" },
{ type: "load-progress", status: "ready" },
{ type: "load-progress", status: "ready", device: "wasm" },
{ type: "load-progress", status: "ready", device: "webgpu" },
{ type: "partial", text: " hello" },
{ type: "result", text: "hello world", chunks: [{ text: "hello world", timestamp: [0, 1.2] }] },
{ type: "error", stage: "load", message: "boom" },
@@ -64,6 +86,8 @@ describe("isWorkerToUiMessage", () => {
expect(isWorkerToUiMessage(undefined)).toBe(false);
expect(isWorkerToUiMessage({})).toBe(false);
expect(isWorkerToUiMessage({ type: "load-progress", status: "progress", file: "f" })).toBe(false);
expect(isWorkerToUiMessage({ type: "load-progress", status: "ready" })).toBe(false);
expect(isWorkerToUiMessage({ type: "load-progress", status: "ready", device: "cuda" })).toBe(false);
expect(isWorkerToUiMessage({ type: "partial" })).toBe(false);
expect(isWorkerToUiMessage({ type: "result", text: "x", chunks: "nope" })).toBe(false);
expect(isWorkerToUiMessage({ type: "error", stage: "decode", message: "m" })).toBe(false);
@@ -129,14 +153,20 @@ describe("applyLoadProgress", () => {
expect(state.files["a.onnx"]).toEqual({ file: "a.onnx", loaded: 200, total: 200, done: true });
});
test("ready flips the ready flag and keeps file state", () => {
test("ready flips the ready flag, records the device, keeps file state", () => {
let state = applyLoadProgress(initialLoadProgress, {
type: "load-progress",
status: "done",
file: "a.onnx",
});
state = applyLoadProgress(state, { type: "load-progress", status: "ready" });
expect(state.device).toBeNull();
state = applyLoadProgress(state, {
type: "load-progress",
status: "ready",
device: "webgpu",
});
expect(state.ready).toBe(true);
expect(state.device).toBe("webgpu");
expect(state.files["a.onnx"]!.done).toBe(true);
});
});