Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 48 additions & 15 deletions samples/js/live-audio-transcription-example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ console.log('Loading model...');
await model.load();
console.log('✓ Model loaded');

// Create live transcription session
// Create live transcription session (same pattern as C# sample).
const audioClient = model.createAudioClient();
const session = audioClient.createLiveTranscriptionSession();

session.settings.sampleRate = 16000; // Default is 16000; shown here for clarity
session.settings.channels = 1;
session.settings.bitsPerSample = 16;
Expand All @@ -56,10 +57,12 @@ const readPromise = (async () => {
try {
for await (const result of session.getTranscriptionStream()) {
const text = result.content?.[0]?.text;
if (!text) continue;

// `is_final` is a transcript-state marker only. It should not stop the app.
if (result.is_final) {
console.log();
console.log(` [FINAL] ${text}`);
} else if (text) {
process.stdout.write(`\n [FINAL] ${text}\n`);
} else {
process.stdout.write(text);
}
}
Expand Down Expand Up @@ -88,22 +91,52 @@ try {
? portAudio.SampleFormat16Bit
: portAudio.SampleFormat32Bit,
sampleRate: session.settings.sampleRate,
framesPerBuffer: 1600, // 100ms chunks
maxQueue: 15 // buffer during event-loop blocks from sync FFI calls
// Larger chunk size lowers callback frequency and reduces overflow risk.
framesPerBuffer: 3200,
// Allow deeper native queue during occasional event-loop stalls.
maxQueue: 64
}
});

let appendPending = false;
const appendQueue = [];
let pumping = false;
let warnedQueueDrop = false;

const pumpAudio = async () => {
if (pumping) return;
pumping = true;
try {
while (appendQueue.length > 0) {
const pcm = appendQueue.shift();
await session.append(pcm);
}
} catch (err) {
console.error('append error:', err.message);
} finally {
pumping = false;
// Handle race where new data arrived after loop exit.
if (appendQueue.length > 0) {
void pumpAudio();
}
}
};

audioInput.on('data', (buffer) => {
if (appendPending) return; // drop frame while backpressured
const pcm = new Uint8Array(buffer);
appendPending = true;
session.append(pcm).then(() => {
appendPending = false;
}).catch((err) => {
appendPending = false;
console.error('append error:', err.message);
});
const copy = new Uint8Array(pcm.length);
copy.set(pcm);
Comment on lines 125 to +127
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the audio data handler you’re copying the PCM buffer twice (new Uint8Array(buffer) already copies when buffer is a Node Buffer/TypedArray, and then you allocate/copy again into copy). This adds avoidable work on a hot path and can increase overflow risk under load. Consider doing a single copy into the queued Uint8Array (or creating a view + one explicit copy if you need to detach from a reused native buffer).

Copilot uses AI. Check for mistakes.

// Keep a bounded queue to avoid unbounded memory growth.
if (appendQueue.length >= 100) {
appendQueue.shift();
if (!warnedQueueDrop) {
warnedQueueDrop = true;
console.warn('Audio append queue overflow; dropping oldest chunk to keep stream alive.');
}
}

appendQueue.push(copy);
void pumpAudio();
});

console.log();
Expand Down
Loading