-
Notifications
You must be signed in to change notification settings - Fork 309
[JS & Python] Add cancellation and structured error handling to live audio streaming #690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0da8ab7
21194a3
1d340ab
83e8dc4
2cf7df8
df4260c
eee2cbe
42b40e5
6ab38c4
143a8b6
d5683c3
187810b
26e2079
2ed1074
7cc8353
dfc9dee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |
| // | ||
| // Usage: node app.js | ||
|
|
||
| import { FoundryLocalManager } from 'foundry-local-sdk'; | ||
| import { FoundryLocalManager, LiveAudioStreamError } from 'foundry-local-sdk'; | ||
|
|
||
| console.log('╔══════════════════════════════════════════════════════════╗'); | ||
| console.log('║ Foundry Local — Live Audio Transcription (JS SDK) ║'); | ||
|
|
@@ -39,9 +39,16 @@ console.log('Loading model...'); | |
| await model.load(); | ||
| console.log('✓ Model loaded'); | ||
|
|
||
| // Graceful-shutdown coordinator. Set ONCE on the session via | ||
| // createLiveTranscriptionSession(signal) — every subsequent | ||
| // start() / append() / getStream() / stop() call picks it | ||
| // up automatically, so we don't have to thread the signal through every | ||
| // callsite. | ||
| const shutdown = new AbortController(); | ||
|
|
||
| // Create live transcription session (same pattern as C# sample). | ||
| const audioClient = model.createAudioClient(); | ||
| const session = audioClient.createLiveTranscriptionSession(); | ||
| const session = audioClient.createLiveTranscriptionSession(shutdown.signal); | ||
|
|
||
| session.settings.sampleRate = 16000; // Default is 16000; shown here for clarity | ||
| session.settings.channels = 1; | ||
|
|
@@ -67,9 +74,23 @@ 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; | ||
|
|
||
| // LiveAudioStreamError surfaces the structured error code from the | ||
| // native core — use it to log/route on a machine-readable code rather | ||
| // than regex-matching err.message. | ||
| // | ||
| // NOTE: the SDK does not currently retry on `isTransient=true`. The | ||
| // set of transient codes is not yet documented; SDK-internal retry is | ||
| // future work. Until then, treat all stream errors as terminal in | ||
| // application code. | ||
| if (err instanceof LiveAudioStreamError) { | ||
| console.error(`\n✗ Stream error [${err.code}]: ${err.message}`); | ||
| return; | ||
| } | ||
|
|
||
| console.error('\n✗ Stream error:', err.message); | ||
| } | ||
| })(); | ||
|
|
||
|
|
@@ -108,14 +129,18 @@ try { | |
| try { | ||
| while (appendQueue.length > 0) { | ||
| const pcm = appendQueue.shift(); | ||
| // Session-level signal (set in createLiveTranscriptionSession) | ||
| // applies automatically — no need to pass it here. | ||
| await session.append(pcm); | ||
| } | ||
| } 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(); | ||
| } | ||
| } | ||
|
|
@@ -182,9 +207,14 @@ try { | |
| process.exit(0); | ||
| } | ||
|
|
||
| // Handle graceful shutdown | ||
| // Handle graceful shutdown. | ||
| // | ||
| // The AbortController fires the shared `shutdown` signal so any in-flight | ||
| // session.append() / getStream() 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(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They're not the same thing — they're orthogonal:
|
||
| if (audioInput) { | ||
| audioInput.quit(); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating a new class to represent the parameters passed into the session, can we instead make an optional parameter for
signalincreateLiveTranscriptionSession? Then, we can remove the need for theLiveAudioTranscriptionSessionOptionsclass. The new optional parameter forsignalcan behave similar to aCancellationTokenparameter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, I will update the
cancellationpattern. ThanksThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 6ab38c4. Dropped
LiveAudioTranscriptionSessionOptionsand madesignala plain optional parameter oncreateLiveTranscriptionSession, matching C#'sCancellationTokenshape:(was
createLiveTranscriptionSession({ signal: shutdown.signal }))JS 19/19 tests pass; sample updated.