The Whisper microphone lesson uses transcribeStream() for live captions. Parakeet TDT does not need a streaming session, so we'll batch it instead: drain the mic into a buffer, slice a 3 s window, and fire transcribe() on each slice. Each call returns a plain string.
The trade is latency for simplicity. Whisper's streaming path runs a VAD and gives back partial segments; Parakeet's batch path waits for the full chunk before transcribing. For a fixed-window live transcription, batch is enough.
Start with the same Parakeet addon used by the filesystem lesson. modelType: "parakeet-transcription" selects the engine, and this recording path needs no modelConfig overrides:
const modelId = await loadModel({
modelSrc: PARAKEET_TDT_0_6B_V3_Q8_0,
modelType: "parakeet-transcription",
});The model is ready, so feed it live bytes. Capture 16 kHz mono s16le from the same ffmpeg child-process pattern as the Whisper lesson, then accumulate each stdout chunk as a frame. Here's how it would look like:
const CHUNK_SIZE = 16000 * 2 * 3; // 3 s at 16 kHz s16le mono
ffmpeg.stdout.on("data", (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk]);
if (buffer.length < CHUNK_SIZE || processing) return;
const audioChunk = buffer.subarray(0, CHUNK_SIZE);
buffer = buffer.subarray(CHUNK_SIZE);
processing = true;
void (async () => {
try {
const text = await transcribe({ modelId, audioChunk });
const trimmed = text.trim();
if (trimmed.length > 0 && !trimmed.includes("[No speech detected]")) {
console.log(trimmed);
}
} finally {
processing = false;
}
})();
});The processing flag prevents overlapping chunks. While one transcribe() is in flight, we drop new audio until it returns. The Buffer.subarray calls trim the consumed bytes off the front without copying. The transcribe() call returns a string, not an array, so we filter the empty string and the [No speech detected] placeholder before logging.
Before leaving the microphone open, make missing ffmpeg an immediate error and put both the child process and model behind one cleanup() handler:
async function cleanup() {
if (shuttingDown) return;
shuttingDown = true;
ffmpeg.kill();
await unloadModel({ modelId }).catch(() => {});
process.exit(0);
}
process.on("SIGINT", () => void cleanup());
process.on("SIGTERM", () => void cleanup());unloadModel().catch(() => {}) swallows the WorkerShutdownError that fires on the in-flight RPC drain after Ctrl+C. The try / finally around transcribe() flips processing back to false even on a thrown error, so a failed chunk doesn't leave the mic muted.
Note: chunking by bytes only works because the input format is fixed. If you switch to
f32le, the byte-per-sample ratio changes, and 3 s of audio stops beingCHUNK_SIZEbytes. Either recomputeBYTES_PER_SAMPLEfor the chosen format, or work in sample counts and multiply at the boundary.
Question 1 of 2
How does Parakeet's batch mode differ from Whisper's streaming session when it comes to latency?
Question 2 of 2
What is the primary purpose of checking the processing flag before starting a new transcribe() call on incoming audio?
Run your code, check your answer, or ask a question. It all shows up here.