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:
1 parent
eb72a2e646
commit
9f39a82982
38 files changed
+3857
-246
No files matched your search
+44
-14
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user