Mark server-live sidecar passes as live mode and cover the stream heartbeat with a test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dc64bb8198
commit
afce794cbf
@@ -60,6 +60,8 @@ export function createLiveWebSocket(deps: LiveWebSocketDeps) {
|
||||
audioPath,
|
||||
language: ws.data.language,
|
||||
task: ws.data.task,
|
||||
// Short passes: sequential decode, no batching/VAD (D-WS5a).
|
||||
mode: "live",
|
||||
},
|
||||
(segment) => {
|
||||
text.push(segment.text);
|
||||
|
||||
@@ -316,6 +316,8 @@ export interface TranscribeDeps {
|
||||
maxUploadBytes?: number;
|
||||
/** Directory for uploaded-PCM temp files. Default: the OS temp dir. */
|
||||
tempDir?: string;
|
||||
/** Blank-line heartbeat period on the ndjson stream (D-021). Default 5 s. */
|
||||
heartbeatIntervalMs?: number;
|
||||
}
|
||||
|
||||
function json(body: unknown, status: number): Response {
|
||||
@@ -409,6 +411,7 @@ async function writeBodyToFile(
|
||||
export function createTranscribeHandlers(deps: TranscribeDeps): TranscribeHandlers {
|
||||
const maxUploadBytes = deps.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
||||
const tempDir = deps.tempDir ?? tmpdir();
|
||||
const heartbeatIntervalMs = deps.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
|
||||
|
||||
return {
|
||||
async status(request) {
|
||||
@@ -460,7 +463,7 @@ export function createTranscribeHandlers(deps: TranscribeDeps): TranscribeHandle
|
||||
} catch {
|
||||
clearInterval(heartbeat);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
}, heartbeatIntervalMs);
|
||||
const chunks: TranscriptChunk[] = [];
|
||||
try {
|
||||
await deps.sidecar.transcribe(
|
||||
|
||||
Vendored
+4
-3
@@ -5,7 +5,7 @@ Used by tests/transcribe.test.ts to exercise the real Bun-side supervisor
|
||||
requested model name:
|
||||
|
||||
"ok" two segments (the second reports the audio byte count,
|
||||
language and task for pass-through assertions), then done
|
||||
language, task and mode for pass-through assertions), then done
|
||||
"boom" an error line
|
||||
"die" exit(1) mid-request (crash simulation)
|
||||
"wait-for-cancel" one segment, then block until the next stdin line
|
||||
@@ -53,6 +53,7 @@ for line in sys.stdin:
|
||||
emit({"type": "segment", "id": request_id, "start": 0.0, "end": 1.5,
|
||||
"text": " Hello"})
|
||||
emit({"type": "segment", "id": request_id, "start": 1.5, "end": 3.0,
|
||||
"text": " world. [model=%s bytes=%d lang=%s task=%s]"
|
||||
% (model, size, message.get("language"), message.get("task"))})
|
||||
"text": " world. [model=%s bytes=%d lang=%s task=%s mode=%s]"
|
||||
% (model, size, message.get("language"), message.get("task"),
|
||||
message.get("mode"))})
|
||||
emit({"type": "done", "id": request_id, "cancelled": False})
|
||||
|
||||
@@ -111,7 +111,7 @@ function sessionData(overrides: Partial<LiveSessionData> = {}): LiveSessionData
|
||||
}
|
||||
|
||||
describe("createLiveWebSocket", () => {
|
||||
test("a committed segment writes the exact PCM bytes and passes model/language/task through", async () => {
|
||||
test("a committed segment writes the exact PCM bytes and passes model/language/task/mode through", async () => {
|
||||
const tempDir = await makeTempDir();
|
||||
const sidecar = fakeSidecar();
|
||||
const handler = createLiveWebSocket({ sidecar });
|
||||
@@ -123,7 +123,8 @@ describe("createLiveWebSocket", () => {
|
||||
|
||||
await waitFor(() => sidecar.requests.length > 0);
|
||||
expect(sidecar.requests[0]).toEqual(
|
||||
expect.objectContaining({ model: "base", language: "da", task: "translate" }),
|
||||
// Live passes are short: sequential decoding, no batching/VAD (D-WS5a).
|
||||
expect.objectContaining({ model: "base", language: "da", task: "translate", mode: "live" }),
|
||||
);
|
||||
// The bytes written to the temp file are exactly the committed segment's
|
||||
// samples — not, e.g., a whole shared buffer (see the byteOffset/byteLength fix).
|
||||
|
||||
@@ -52,6 +52,8 @@ function fakeSidecar(
|
||||
segments?: SidecarSegment[];
|
||||
error?: string;
|
||||
waitForAbort?: boolean;
|
||||
/** Quiet time before the first segment (model load, VAD, first batch). */
|
||||
delayMs?: number;
|
||||
} = {},
|
||||
): FakeSidecar {
|
||||
const segments = behavior.segments ?? [
|
||||
@@ -72,6 +74,7 @@ function fakeSidecar(
|
||||
new Uint8Array(await Bun.file(request.audioPath).arrayBuffer()),
|
||||
);
|
||||
if (behavior.error) throw new Error(behavior.error);
|
||||
if (behavior.delayMs) await Bun.sleep(behavior.delayMs);
|
||||
onSegment(segments[0]!);
|
||||
if (behavior.waitForAbort) {
|
||||
await new Promise<void>((resolve) => {
|
||||
@@ -323,6 +326,25 @@ describe("POST /api/transcribe", () => {
|
||||
expect(await readdir(tempDir)).toEqual([]);
|
||||
});
|
||||
|
||||
test("heartbeat blank lines keep a quiet stream alive before the first segment (D-021)", async () => {
|
||||
// The batched pipeline (D-WS5a) runs VAD over the whole file and decodes a
|
||||
// full batch before its first segment, so the quiet stretch is longer.
|
||||
const handlers = createTranscribeHandlers({
|
||||
sidecar: fakeSidecar({ delayMs: 200 }),
|
||||
getSessionUser: sessionFromCookie,
|
||||
tempDir: await makeTempDir(),
|
||||
heartbeatIntervalMs: 20,
|
||||
});
|
||||
const response = await handlers.post(
|
||||
postRequest("?model=onnx-community/whisper-base", pcmBody([0.1]), { cookie: "sid=ok" }),
|
||||
);
|
||||
const text = await response.text();
|
||||
const beforeFirstSegment = text.slice(0, text.indexOf("{"));
|
||||
expect(beforeFirstSegment.length).toBeGreaterThanOrEqual(2);
|
||||
expect(beforeFirstSegment.trim()).toBe("");
|
||||
expect(text).toContain('"type":"result"');
|
||||
});
|
||||
|
||||
test("sidecar errors travel in-stream after the 200 header", async () => {
|
||||
const tempDir = await makeTempDir();
|
||||
const handlers = createTranscribeHandlers({
|
||||
@@ -385,8 +407,12 @@ describe("createSidecar (fake python sidecar)", () => {
|
||||
expect(outcome).toEqual({ cancelled: false });
|
||||
expect(segments).toHaveLength(2);
|
||||
expect(segments[0]).toEqual({ start: 0, end: 1.5, text: " Hello" });
|
||||
// The fixture echoes what it received (audio bytes, language, task).
|
||||
expect(segments[1]!.text).toContain("model=ok bytes=12 lang=da task=translate");
|
||||
// The fixture echoes what it received (audio bytes, language, task, mode).
|
||||
expect(segments[1]!.text).toContain("model=ok bytes=12 lang=da task=translate mode=file");
|
||||
|
||||
const live: SidecarSegment[] = [];
|
||||
await sidecar.transcribe({ model: "ok", audioPath, mode: "live" }, (s) => live.push(s));
|
||||
expect(live[1]!.text).toContain("mode=live");
|
||||
});
|
||||
|
||||
test("sidecar 'error' lines reject that request only", async () => {
|
||||
|
||||
Reference in New Issue
Block a user