Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
43 changes: 35 additions & 8 deletions samples/js/live-audio-transcription/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//
// Usage: node app.js

import { FoundryLocalManager } from 'foundry-local-sdk';
import { FoundryLocalManager, CoreError } from 'foundry-local-sdk';
Comment thread
rui-ren marked this conversation as resolved.
Outdated

console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ Foundry Local — Live Audio Transcription (JS SDK) ║');
Expand Down Expand Up @@ -48,14 +48,19 @@ session.settings.channels = 1;
session.settings.bitsPerSample = 16;
session.settings.language = 'en';

// Graceful-shutdown coordinator. Passed to start() / append() / stop() /
// getTranscriptionStream() so Ctrl+C can cancel any in-flight async work
// (e.g., a backpressured append()) instead of waiting for stop() to drain.
const shutdown = new AbortController();

console.log('Starting streaming session...');
await session.start();
await session.start(shutdown.signal);
console.log('✓ Session started');

// Read transcription results in background
const readPromise = (async () => {
try {
for await (const result of session.getTranscriptionStream()) {
for await (const result of session.getTranscriptionStream(shutdown.signal)) {
const text = result.content?.[0]?.text;
if (!text) continue;

Expand All @@ -67,9 +72,22 @@ const readPromise = (async () => {
}
}
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Stream error:', err.message);
// AbortError is expected on Ctrl+C; ignore quietly.
if (err.name === 'AbortError') return;

// CoreError surfaces native-core failure metadata (code + isTransient).
// Use it to retry quietly on transient blips instead of dying on the
// first hiccup. Without CoreError the only signal would be err.message.
if (err instanceof CoreError) {
if (err.isTransient) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it expected for FoundryLocalCore to throw transient errors? What do transient errors mean? If we detect transient errors, and there is no action that the application/user should take during such errors, can we catch them before they reach the application layer and continue as necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right the transient branch was speculative (no documented list of transient codes from FLC core today, no SDK-internal retry policy yet). Pushed dfc9dee that drops the transient handling from both the JS and Python samples.

What changed:

  • Removed the if (err.isTransient) ... branch from both samples
  • Added a NOTE comment in each sample saying SDK-internal retry on transient errors is future work
  • Samples still demonstrate the value of the structured error type customers can log/route on LiveAudioStreamError.code (JS) or CoreErrorResponse.try_parse(...).code (Python) instead of regex-matching err.message

What stayed:

  • LiveAudioStreamError.isTransient and CoreErrorResponse.is_transient are still on the SDK API surface (no breaking change). Advanced users can still inspect them. We just don't lead the typical sample reader to expect recovery behavior the SDK doesn't deliver.

When the SDK gets a real retry policy + a documented set of transient codes, we can re-introduce internal handling and the field becomes meaningful end-to-end. Happy to file a follow-up issue tracking that work let me know.

console.warn(`\n⚠ Transient ASR error (${err.code}): ${err.message}. Continuing...`);
return;
}
console.error(`\n✗ Stream error [${err.code}]: ${err.message}`);
return;
}

console.error('\n✗ Stream error:', err.message);
}
})();

Expand Down Expand Up @@ -108,14 +126,18 @@ try {
try {
while (appendQueue.length > 0) {
const pcm = appendQueue.shift();
await session.append(pcm);
// Pass the shutdown signal so a backpressured append() resolves
// promptly on Ctrl+C instead of blocking the pump.
await session.append(pcm, shutdown.signal);
}
} catch (err) {
// Aborted via Ctrl+C — exit quietly.
if (err.name === 'AbortError') return;
console.error('append error:', err.message);
} finally {
pumping = false;
// Handle race where new data arrived after loop exit.
if (appendQueue.length > 0) {
if (appendQueue.length > 0 && !shutdown.signal.aborted) {
void pumpAudio();
}
}
Expand Down Expand Up @@ -182,9 +204,14 @@ try {
process.exit(0);
}

// Handle graceful shutdown
// Handle graceful shutdown.
//
// The AbortController fires the shared `shutdown` signal so any in-flight
// session.append() / getTranscriptionStream() resolves promptly with an
// AbortError instead of waiting for stop() to finish draining the queue.
process.on('SIGINT', async () => {
console.log('\n\nStopping...');
shutdown.abort();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is my understanding correct that shutdown and session.stop do the same thing but shutdown is similar to session.stop(hard=true) so we avoid draining the queue? Should we use the same API to represent that, or do we need separate handlers for shutdown vs session.stop()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They're not the same thing — they're orthogonal:

  • shutdown.abort() cancels in-flight calls (e.g., append() blocked on backpressure, getTranscriptionStream() waiting for
    results).
  • session.stop() signals end-of-audio to native core, drains the queue, and releases the native handle.

if (audioInput) {
audioInput.quit();
}
Expand Down
1 change: 1 addition & 0 deletions sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { AudioClient, AudioClientSettings } from './openai/audioClient.js';
export { EmbeddingClient } from './openai/embeddingClient.js';
export { LiveAudioTranscriptionSession, LiveAudioTranscriptionOptions } from './openai/liveAudioTranscriptionClient.js';
export type { LiveAudioTranscriptionResponse, TranscriptionContentPart } from './openai/liveAudioTranscriptionTypes.js';
export { CoreError } from './openai/liveAudioTranscriptionTypes.js';
export { ResponsesClient, ResponsesClientSettings, getOutputText } from './openai/responsesClient.js';
export { ModelLoadManager } from './detail/modelLoadManager.js';
/** @internal */
Expand Down
Loading
Loading