Merge feat/longform-pipeline: VAD long-form pipeline, opt-in (spec 001 workstream 3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/Architecture.md
#	docs/Decisions.md
#	src/App.tsx
#	src/index.css
#	src/lib/engine.ts
#	src/worker/inference.worker.ts
#	src/worker/protocol.ts
#	tests/engine.test.ts
#	tests/live-real-model.test.ts
#	tests/offline.md
#	tests/protocol.test.ts
This commit is contained in:
Lucas Winther committed 2026-10-30 01:39:17 +01:00
commit 9c89ab047f
31 files changed
+2264 -16

No files matched your search

+50
View File
@@ -4,6 +4,7 @@ import {
deviceAttempts,
dtypeRequiresF16,
initialLoadProgress,
choosePipeline,
isUiToWorkerMessage,
isWorkerToUiMessage,
TranscribeProgressTracker,
@@ -36,7 +37,9 @@ describe("isUiToWorkerMessage", () => {
{ type: "transcribe", audio: new Float32Array(8), requestId: "engine-1-1", stream: true },
{ type: "transcribe", audio: new Float32Array(8), stream: false },
{ type: "transcribe", audio: new Float32Array(8), prompt: "Maxwell, Voices of the Void" },
{ type: "transcribe", audio: new Float32Array(8), stream: true, longForm: true },
];
loads.push({ type: "load", modelId: "onnx-community/whisper-base", device: "auto", vad: true });
for (const message of [...loads, ...transcribes]) {
expect(isUiToWorkerMessage(message)).toBe(true);
}
@@ -69,10 +72,26 @@ describe("isUiToWorkerMessage", () => {
expect(
isUiToWorkerMessage({ type: "transcribe", audio: new Float32Array(4), prompt: 5 }),
).toBe(false);
expect(
isUiToWorkerMessage({ type: "transcribe", audio: new Float32Array(4), longForm: 1 }),
).toBe(false);
expect(isUiToWorkerMessage({ type: "load", modelId: "x", device: "auto", vad: "yes" })).toBe(
false,
);
expect(isUiToWorkerMessage({ type: "result", text: "", chunks: [] })).toBe(false);
});
});
describe("choosePipeline (D-WS3a/D-WS3b)", () => {
test("long-form only when asked for and the VAD model loaded", () => {
expect(choosePipeline({ longForm: true, vadReady: true })).toBe("longform");
// Offline / never downloaded: the old path, never an error (Review Focus 3).
expect(choosePipeline({ longForm: true, vadReady: false })).toBe("chunked");
expect(choosePipeline({ longForm: false, vadReady: true })).toBe("chunked");
expect(choosePipeline({ longForm: undefined, vadReady: true })).toBe("chunked");
});
});
describe("isWorkerToUiMessage", () => {
test("accepts every worker → UI message shape", () => {
const messages: WorkerToUiMessage[] = [
@@ -278,6 +297,37 @@ describe("TranscribeProgressTracker", () => {
expect(tracker.timestamp(8)).toBe(8);
expect(tracker.finalizeWindow()).toBeNull(); // 20 clamps to 8, not an advance
});
describe("forWindows (variable-length VAD chunks, D-WS3a)", () => {
// Two speech chunks with a skipped 15 s silence between them: [0–20 s]
// and [35–60 s] of a 70 s file.
const windows = [
{ toAbsolute: (t: number) => t, endSeconds: 20, nextStartSeconds: 35 },
{ toAbsolute: (t: number) => 35 + t, endSeconds: 60, nextStartSeconds: 70 },
];
test("maps chunk-relative times through each window", () => {
const tracker = TranscribeProgressTracker.forWindows(70, windows);
expect(tracker.timestamp(5)).toBe(5);
// Finishing a chunk counts the skipped silence up to the next one.
expect(tracker.finalizeWindow()).toBe(35);
expect(tracker.timestamp(10)).toBe(45);
expect(tracker.timestamp(2)).toBeNull(); // 37 < 45: no regression
expect(tracker.finalizeWindow()).toBe(70);
expect(tracker.finalizeWindow()).toBeNull(); // past the last window
});
test("clamps a stray timestamp to the window's end", () => {
const tracker = TranscribeProgressTracker.forWindows(70, windows);
expect(tracker.timestamp(400)).toBe(20);
});
test("no windows (no speech) → nothing to report but the end", () => {
const tracker = TranscribeProgressTracker.forWindows(70, []);
expect(tracker.timestamp(3)).toBeNull();
expect(tracker.finalizeWindow()).toBe(70);
});
});
});
describe("applyLoadProgress", () => {