Record the batched-VAD measurements and check heartbeat, bursts and cancel against the real sidecar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 00:23:55 +01:00
1 parent 1c1806840e
commit 104c1c4e4d
5 files changed
+143 -2

No files matched your search

+5 -1
View File
@@ -31,6 +31,9 @@ faster-whisper isn't installed for `python3` itself.
Without faster-whisper at all, `bun test tests/transcribe.test.ts` still runs
the real `server-py/sidecar.py` against a stub (`tests/fixtures/stub_fw`) to
check the mode routing, the one-model cache and cancel between segments.
With `FW_INTEGRATION=1` it also pushes the whole 15-min `en_alien` clip
(`tiny.en`) through `POST /api/transcribe` — heartbeat lines before the first
burst, segments in bursts — and cancels a batched run after its first burst.
- [ ] The `real faster-whisper sidecar` describe block passes.
@@ -52,7 +55,8 @@ check the mode routing, the one-model cache and cancel between segments.
batched pipeline (D-WS5a) they arrive in bursts — one burst per 8
speech chunks (≈ up to 4 min of audio) — with blank heartbeat lines
every 5 s in between; on a clip under ~30 s of speech, everything
arrives as a single segment at the end.
arrives as a single segment at the end. Each segment covers one VAD
speech chunk (up to 30 s), not one sentence — expected (D-WS5a).
- [ ] Cancelling a long server transcription (abort / page refresh) stops
the sidecar within one burst; the next request then starts normally.
- [ ] Both transcripts agree in content (minor punctuation/casing drift
+87
View File
@@ -624,4 +624,91 @@ describe.skipIf(!process.env.FW_INTEGRATION)("real faster-whisper sidecar", () =
},
{ timeout: 300_000 },
);
// The whole 15-min en_alien clip (≈ 17 VAD chunks ⇒ 3 batches) as uploaded PCM.
async function wholeAlienPcm(): Promise<string> {
const clip = join(import.meta.dir, "audio_files/en_alien.opus");
const pcm = await Bun.$`ffmpeg -v error -i ${clip} -f f32le -ac 1 -ar 16000 -`.arrayBuffer();
const audioPath = join(await makeTempDir(), "alien-full.f32");
await Bun.write(audioPath, pcm);
return audioPath;
}
test(
"a long file streams in bursts behind heartbeats over HTTP (D-021, D-WS5a)",
async () => {
const handlers = createTranscribeHandlers({
sidecar: createSidecar(),
getSessionUser: sessionFromCookie,
tempDir: await makeTempDir(),
heartbeatIntervalMs: 100,
});
const body = await Bun.file(await wholeAlienPcm()).arrayBuffer();
const response = await handlers.post(
postRequest("?model=onnx-community/whisper-tiny.en&language=en", body, {
cookie: "sid=ok",
}),
);
expect(response.status).toBe(200);
// Timestamp every line as it arrives.
const started = performance.now();
const lines: { at: number; line: string }[] = [];
const decoder = new TextDecoder();
let buffered = "";
for await (const chunk of response.body!) {
buffered += decoder.decode(chunk, { stream: true });
let newline: number;
while ((newline = buffered.indexOf("\n")) >= 0) {
lines.push({ at: performance.now() - started, line: buffered.slice(0, newline) });
buffered = buffered.slice(newline + 1);
}
}
const segmentLines = lines.filter(({ line }) => line.includes('"type":"segment"'));
const firstSegment = lines.findIndex(({ line }) => line.includes('"type":"segment"'));
// VAD over 15 min of audio + the first batch: heartbeats fill the gap.
expect(lines.slice(0, firstSegment).filter(({ line }) => line === "").length).toBeGreaterThan(0);
expect(segmentLines.length).toBeGreaterThan(3);
// Streamed, not one blob at the end: later bursts arrive measurably later.
expect(segmentLines.at(-1)!.at - segmentLines[0]!.at).toBeGreaterThan(100);
expect(lines.at(-1)!.line).toContain('"type":"result"');
console.log(
"[D-WS5a] segment arrival (ms):",
segmentLines.map(({ at }) => Math.round(at)).join(" "),
);
},
{ timeout: 600_000 },
);
test(
"cancelling a batched file run stops it early and the next request still works",
async () => {
const sidecar = createSidecar();
const audioPath = await wholeAlienPcm();
const controller = new AbortController();
const cancelledRun: SidecarSegment[] = [];
const outcome = await sidecar.transcribe(
{ model: "tiny.en", audioPath, language: "en" },
(segment) => {
cancelledRun.push(segment);
controller.abort();
},
controller.signal,
);
expect(outcome.cancelled).toBe(true);
// At most the first burst (8 chunks) got out; the full clip is ≈ 17 chunks.
expect(cancelledRun.length).toBeGreaterThan(0);
expect(cancelledRun.length).toBeLessThanOrEqual(8);
expect(cancelledRun.at(-1)!.end).toBeLessThan(800);
const again: SidecarSegment[] = [];
const next = await sidecar.transcribe(
{ model: "tiny.en", audioPath, language: "en" },
(segment) => again.push(segment),
);
expect(next.cancelled).toBe(false);
expect(again.length).toBeGreaterThan(cancelledRun.length);
},
{ timeout: 600_000 },
);
});