WIP: uncommitted work at pause (session limit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f7d0525423
commit
826bcd2e49
2 files changed
+127
-3
No files matched your search
@@ -10,6 +10,13 @@ import {
|
||||
type LiveTranscribeFn,
|
||||
} from "../src/lib/live";
|
||||
import { resolveDefaultDtype } from "../src/lib/models";
|
||||
import {
|
||||
buildPromptPrefix,
|
||||
installPromptSupport,
|
||||
PROMPT_PREFIX_KWARG,
|
||||
type PromptableModel,
|
||||
type PromptTokenizer,
|
||||
} from "../src/worker/prompt";
|
||||
import { wordErrorRate } from "./dsp";
|
||||
|
||||
/**
|
||||
@@ -266,6 +273,10 @@ describe("live pipeline accuracy (real model, real recordings)", () => {
|
||||
normalize((await transcribe(whole)).text),
|
||||
);
|
||||
|
||||
// File-mode accuracy gate (spec 001): the upload flow is held to the
|
||||
// same per-clip bar as live, so a file-mode regression fails here too.
|
||||
expect(fileWer).toBeLessThanOrEqual(MAX_WER_BY_CLIP[clip.base] ?? MAX_WER);
|
||||
|
||||
// The live session, with the injected clock advancing with the audio
|
||||
// fed so pass scheduling behaves as in real time.
|
||||
let fedSamples = 0;
|
||||
@@ -310,3 +321,103 @@ describe("live pipeline accuracy (real model, real recordings)", () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vocabulary hint (D-WS2a): the worker's real prompt code on real weights
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const HINT = "Maxwell, Voices of the Void, Slenderman, RuneScape, UFO";
|
||||
|
||||
/** Decode the first `seconds` of a clip straight to 16 kHz (no mic path needed). */
|
||||
async function decode16k(file: string, seconds?: number): Promise<Float32Array> {
|
||||
const proc = Bun.spawn(
|
||||
[
|
||||
ffmpegPath as unknown as string,
|
||||
...["-hide_banner", "-loglevel", "error", "-i", join(AUDIO_DIR, file)],
|
||||
...(seconds ? ["-t", String(seconds)] : []),
|
||||
...["-ac", "1", "-ar", String(RATE), "-f", "f32le", "-"],
|
||||
],
|
||||
{ stdout: "pipe", stderr: "pipe" },
|
||||
);
|
||||
const [bytes, exitCode] = await Promise.all([new Response(proc.stdout).arrayBuffer(), proc.exited]);
|
||||
if (exitCode !== 0) throw new Error(`ffmpeg failed on ${file}`);
|
||||
return new Float32Array(bytes);
|
||||
}
|
||||
|
||||
/** One pass with the inference worker's options and (optionally) its prompt. */
|
||||
async function transcribeWithHint(
|
||||
audio: Float32Array,
|
||||
hint: string,
|
||||
language = "en",
|
||||
): Promise<string> {
|
||||
const asr = await loadAsr();
|
||||
installPromptSupport(asr.model as unknown as PromptableModel);
|
||||
const prefix = buildPromptPrefix(asr.tokenizer as unknown as PromptTokenizer, hint);
|
||||
const output = await asr(audio, {
|
||||
chunk_length_s: 30,
|
||||
stride_length_s: 5,
|
||||
return_timestamps: true,
|
||||
max_new_tokens: 440,
|
||||
language,
|
||||
task: "transcribe",
|
||||
...(prefix.length > 0 && { [PROMPT_PREFIX_KWARG]: prefix }),
|
||||
} as Record<string, unknown>);
|
||||
const single = (Array.isArray(output) ? output[0] : output) as { text?: string };
|
||||
return single.text ?? "";
|
||||
}
|
||||
|
||||
describe("vocabulary hint (real model, D-WS2a)", () => {
|
||||
test.skipIf(!available)(
|
||||
"a hint keeps the transcript intact, keeps the hinted names, and never leaks into the text",
|
||||
async () => {
|
||||
// The first 2 min of en_alien: four windows, "Voices of the Void" and
|
||||
// several "Maxwell"s.
|
||||
const audio = await decode16k("en_alien.opus", 120);
|
||||
const plain = await transcribeWithHint(audio, "");
|
||||
const hinted = await transcribeWithHint(audio, HINT);
|
||||
const count = (text: string, term: string) => normalize(text).split(term).length - 1;
|
||||
|
||||
expect(count(hinted, "maxwell")).toBeGreaterThanOrEqual(count(plain, "maxwell"));
|
||||
expect(count(hinted, "voices of the void")).toBeGreaterThanOrEqual(1);
|
||||
// Not garbled: no repetition loop, no dropped windows (measured
|
||||
// difference between the two runs ≈ punctuation-level).
|
||||
expect(wordErrorRate(normalize(plain), normalize(hinted))).toBeLessThanOrEqual(0.15);
|
||||
// The prompt itself is never part of the transcript.
|
||||
expect(normalize(hinted).startsWith(normalize(HINT))).toBe(false);
|
||||
expect(normalize(hinted)).not.toContain("slenderman runescape");
|
||||
},
|
||||
DOWNLOAD_BUDGET_MS + 120 * 2 * MS_PER_AUDIO_SECOND,
|
||||
);
|
||||
|
||||
test.skipIf(!available)(
|
||||
"a hint longer than Whisper's prompt budget is cut, not an error or a garbled window",
|
||||
async () => {
|
||||
const longHint = Array.from({ length: 200 }, (_, i) => `Project${i} Codename${i}`)
|
||||
.join(", ")
|
||||
.slice(0, 1000);
|
||||
const reference = await Bun.file(join(AUDIO_DIR, "en_aiproblem.txt")).text();
|
||||
const text = await transcribeWithHint(await decode16k("en_aiproblem.m4a"), longHint);
|
||||
expect(wordErrorRate(normalize(reference), normalize(text))).toBeLessThanOrEqual(MAX_WER);
|
||||
expect(text).not.toContain("Codename");
|
||||
},
|
||||
DOWNLOAD_BUDGET_MS + 60 * MS_PER_AUDIO_SECOND,
|
||||
);
|
||||
|
||||
test.skipIf(!available)(
|
||||
"a short, near-silent live window with a hint does not transcribe the hint",
|
||||
async () => {
|
||||
// 3 s of faint noise — what a live pass sees in a pause that the
|
||||
// segmenter did not drop (its near-silence gate is far lower).
|
||||
let seed = 1;
|
||||
const noise = new Float32Array(3 * RATE).map(() => {
|
||||
seed = (seed * 1103515245 + 12345) % 2 ** 31;
|
||||
return (seed / 2 ** 31 - 0.5) * 0.01;
|
||||
});
|
||||
const text = normalize(await transcribeWithHint(noise, HINT));
|
||||
for (const term of ["maxwell", "voices of the void", "slenderman", "runescape"]) {
|
||||
expect(text).not.toContain(term);
|
||||
}
|
||||
},
|
||||
DOWNLOAD_BUDGET_MS + 30 * MS_PER_AUDIO_SECOND,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user