-
Notifications
You must be signed in to change notification settings - Fork 212
Improve AgentActivity resource cleanup #891
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
Changes from all commits
863cf20
e1733d6
00716ba
b4602bc
9487096
3eb83ff
cecac00
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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@livekit/agents': patch | ||
| --- | ||
|
|
||
| Fix improper resource cleanup inside AgentActivity by not close global STT / TTS / VAD components |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,9 +113,9 @@ export abstract class STT extends (EventEmitter as new () => TypedEmitter<STTCal | |
| } | ||
|
|
||
| /** Receives an audio buffer and returns transcription in the form of a {@link SpeechEvent} */ | ||
| async recognize(frame: AudioBuffer): Promise<SpeechEvent> { | ||
| async recognize(frame: AudioBuffer, abortSignal?: AbortSignal): Promise<SpeechEvent> { | ||
| const startTime = process.hrtime.bigint(); | ||
| const event = await this._recognize(frame); | ||
| const event = await this._recognize(frame, abortSignal); | ||
| const durationMs = Number((process.hrtime.bigint() - startTime) / BigInt(1000000)); | ||
| this.emit('metrics_collected', { | ||
| type: 'stt_metrics', | ||
|
|
@@ -128,7 +128,11 @@ export abstract class STT extends (EventEmitter as new () => TypedEmitter<STTCal | |
| }); | ||
| return event; | ||
| } | ||
| protected abstract _recognize(frame: AudioBuffer): Promise<SpeechEvent>; | ||
|
|
||
| protected abstract _recognize( | ||
| frame: AudioBuffer, | ||
| abortSignal?: AbortSignal, | ||
| ): Promise<SpeechEvent>; | ||
|
|
||
| /** | ||
| * Returns a {@link SpeechStream} that can be used to push audio frames and receive | ||
|
|
@@ -173,6 +177,8 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> | |
| private logger = log(); | ||
| private _connOptions: APIConnectOptions; | ||
|
|
||
| protected abortController = new AbortController(); | ||
|
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. add an |
||
|
|
||
| constructor( | ||
| stt: STT, | ||
| sampleRate?: number, | ||
|
|
@@ -290,6 +296,10 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> | |
|
|
||
| protected abstract run(): Promise<void>; | ||
|
|
||
| protected get abortSignal(): AbortSignal { | ||
| return this.abortController.signal; | ||
| } | ||
|
|
||
| updateInputStream(audioStream: ReadableStream<AudioFrame>) { | ||
| this.deferredInputStream.setSource(audioStream); | ||
| } | ||
|
|
@@ -354,6 +364,7 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> | |
| if (!this.input.closed) this.input.close(); | ||
| if (!this.queue.closed) this.queue.close(); | ||
| if (!this.output.closed) this.output.close(); | ||
| if (!this.abortController.signal.aborted) this.abortController.abort(); | ||
| this.closed = true; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -260,28 +260,41 @@ export class Agent<UserData = any> { | |
| let wrapped_stt = activity.stt; | ||
|
|
||
| if (!wrapped_stt.capabilities.streaming) { | ||
| if (!agent.vad) { | ||
| const vad = agent.vad || activity.vad; | ||
| if (!vad) { | ||
| throw new Error( | ||
| 'STT does not support streaming, add a VAD to the AgentTask/VoiceAgent to enable streaming', | ||
| ); | ||
| } | ||
| wrapped_stt = new STTStreamAdapter(wrapped_stt, agent.vad); | ||
| wrapped_stt = new STTStreamAdapter(wrapped_stt, vad); | ||
| } | ||
|
|
||
| const connOptions = activity.agentSession.connOptions.sttConnOptions; | ||
| const stream = wrapped_stt.stream({ connOptions }); | ||
| stream.updateInputStream(audio); | ||
|
|
||
| let cleaned = false; | ||
| const cleanup = () => { | ||
| if (cleaned) return; | ||
| cleaned = true; | ||
| stream.detachInputStream(); | ||
| stream.close(); | ||
| }; | ||
|
|
||
| return new ReadableStream({ | ||
| async start(controller) { | ||
| for await (const event of stream) { | ||
| controller.enqueue(event); | ||
| try { | ||
| for await (const event of stream) { | ||
| controller.enqueue(event); | ||
| } | ||
| controller.close(); | ||
| } finally { | ||
| // Always clean up the STT stream, whether it ends naturally or is cancelled | ||
| cleanup(); | ||
| } | ||
| controller.close(); | ||
| }, | ||
| cancel() { | ||
| stream.detachInputStream(); | ||
| stream.close(); | ||
| cleanup(); | ||
| }, | ||
| }); | ||
| }, | ||
|
|
@@ -314,15 +327,27 @@ export class Agent<UserData = any> { | |
| connOptions, | ||
| parallelToolCalls: true, | ||
| }); | ||
|
|
||
| let cleaned = false; | ||
| const cleanup = () => { | ||
| if (cleaned) return; | ||
| cleaned = true; | ||
| stream.close(); | ||
|
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. ^ |
||
| }; | ||
|
|
||
| return new ReadableStream({ | ||
| async start(controller) { | ||
| for await (const chunk of stream) { | ||
| controller.enqueue(chunk); | ||
| try { | ||
| for await (const chunk of stream) { | ||
| controller.enqueue(chunk); | ||
| } | ||
| controller.close(); | ||
| } finally { | ||
| cleanup(); | ||
| } | ||
| controller.close(); | ||
| }, | ||
| cancel() { | ||
| stream.close(); | ||
| cleanup(); | ||
| }, | ||
| }); | ||
| }, | ||
|
|
@@ -347,18 +372,29 @@ export class Agent<UserData = any> { | |
| const stream = wrapped_tts.stream({ connOptions }); | ||
| stream.updateInputStream(text); | ||
|
|
||
| let cleaned = false; | ||
| const cleanup = () => { | ||
| if (cleaned) return; | ||
| cleaned = true; | ||
| stream.close(); | ||
| }; | ||
|
|
||
| return new ReadableStream({ | ||
| async start(controller) { | ||
| for await (const chunk of stream) { | ||
| if (chunk === SynthesizeStream.END_OF_STREAM) { | ||
| break; | ||
| try { | ||
| for await (const chunk of stream) { | ||
| if (chunk === SynthesizeStream.END_OF_STREAM) { | ||
| break; | ||
| } | ||
| controller.enqueue(chunk.frame); | ||
| } | ||
| controller.enqueue(chunk.frame); | ||
| controller.close(); | ||
| } finally { | ||
| cleanup(); | ||
| } | ||
| controller.close(); | ||
| }, | ||
| cancel() { | ||
| stream.close(); | ||
| cleanup(); | ||
| }, | ||
| }); | ||
| }, | ||
|
|
||
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.
plugins can now attach this abortSignal for any external request cancellation