@livekit/agents 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_exceptions.cjs.map +1 -1
- package/dist/_exceptions.d.ts.map +1 -1
- package/dist/_exceptions.js.map +1 -1
- package/dist/audio.cjs +10 -0
- package/dist/audio.cjs.map +1 -1
- package/dist/audio.d.cts +1 -1
- package/dist/audio.d.ts +1 -1
- package/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +10 -0
- package/dist/audio.js.map +1 -1
- package/dist/beta/workflows/task_group.cjs +7 -4
- package/dist/beta/workflows/task_group.cjs.map +1 -1
- package/dist/beta/workflows/task_group.d.ts.map +1 -1
- package/dist/beta/workflows/task_group.js +7 -4
- package/dist/beta/workflows/task_group.js.map +1 -1
- package/dist/inference/api_protos.d.cts +26 -26
- package/dist/inference/api_protos.d.ts +26 -26
- package/dist/inference/interruption/http_transport.cjs.map +1 -1
- package/dist/inference/interruption/http_transport.d.cts +3 -1
- package/dist/inference/interruption/http_transport.d.ts +3 -1
- package/dist/inference/interruption/http_transport.d.ts.map +1 -1
- package/dist/inference/interruption/http_transport.js.map +1 -1
- package/dist/inference/interruption/ws_transport.cjs +37 -32
- package/dist/inference/interruption/ws_transport.cjs.map +1 -1
- package/dist/inference/interruption/ws_transport.d.ts.map +1 -1
- package/dist/inference/interruption/ws_transport.js +37 -32
- package/dist/inference/interruption/ws_transport.js.map +1 -1
- package/dist/inference/tts.cjs +14 -1
- package/dist/inference/tts.cjs.map +1 -1
- package/dist/inference/tts.d.cts +42 -4
- package/dist/inference/tts.d.ts +42 -4
- package/dist/inference/tts.d.ts.map +1 -1
- package/dist/inference/tts.js +24 -3
- package/dist/inference/tts.js.map +1 -1
- package/dist/inference/tts.test.cjs +72 -0
- package/dist/inference/tts.test.cjs.map +1 -1
- package/dist/inference/tts.test.js +72 -0
- package/dist/inference/tts.test.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +7 -2
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +7 -2
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/ipc/supervised_proc.cjs +4 -1
- package/dist/ipc/supervised_proc.cjs.map +1 -1
- package/dist/ipc/supervised_proc.d.ts.map +1 -1
- package/dist/ipc/supervised_proc.js +4 -1
- package/dist/ipc/supervised_proc.js.map +1 -1
- package/dist/ipc/supervised_proc.test.cjs +82 -0
- package/dist/ipc/supervised_proc.test.cjs.map +1 -1
- package/dist/ipc/supervised_proc.test.js +82 -0
- package/dist/ipc/supervised_proc.test.js.map +1 -1
- package/dist/job.cjs +2 -1
- package/dist/job.cjs.map +1 -1
- package/dist/job.d.ts.map +1 -1
- package/dist/job.js +2 -1
- package/dist/job.js.map +1 -1
- package/dist/llm/chat_context.cjs +102 -31
- package/dist/llm/chat_context.cjs.map +1 -1
- package/dist/llm/chat_context.d.ts.map +1 -1
- package/dist/llm/chat_context.js +102 -31
- package/dist/llm/chat_context.js.map +1 -1
- package/dist/llm/chat_context.test.cjs +123 -5
- package/dist/llm/chat_context.test.cjs.map +1 -1
- package/dist/llm/chat_context.test.js +123 -5
- package/dist/llm/chat_context.test.js.map +1 -1
- package/dist/llm/fallback_adapter.cjs +2 -0
- package/dist/llm/fallback_adapter.cjs.map +1 -1
- package/dist/llm/fallback_adapter.d.ts.map +1 -1
- package/dist/llm/fallback_adapter.js +2 -0
- package/dist/llm/fallback_adapter.js.map +1 -1
- package/dist/llm/index.cjs +2 -0
- package/dist/llm/index.cjs.map +1 -1
- package/dist/llm/index.d.cts +1 -1
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +2 -0
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/utils.cjs +89 -0
- package/dist/llm/utils.cjs.map +1 -1
- package/dist/llm/utils.d.cts +8 -0
- package/dist/llm/utils.d.ts +8 -0
- package/dist/llm/utils.d.ts.map +1 -1
- package/dist/llm/utils.js +88 -0
- package/dist/llm/utils.js.map +1 -1
- package/dist/llm/utils.test.cjs +90 -0
- package/dist/llm/utils.test.cjs.map +1 -1
- package/dist/llm/utils.test.js +98 -2
- package/dist/llm/utils.test.js.map +1 -1
- package/dist/stt/stt.cjs +8 -0
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.d.cts +8 -0
- package/dist/stt/stt.d.ts +8 -0
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +8 -0
- package/dist/stt/stt.js.map +1 -1
- package/dist/tts/fallback_adapter.cjs +6 -0
- package/dist/tts/fallback_adapter.cjs.map +1 -1
- package/dist/tts/fallback_adapter.d.ts.map +1 -1
- package/dist/tts/fallback_adapter.js +6 -0
- package/dist/tts/fallback_adapter.js.map +1 -1
- package/dist/typed_promise.cjs +48 -0
- package/dist/typed_promise.cjs.map +1 -0
- package/dist/typed_promise.d.cts +24 -0
- package/dist/typed_promise.d.ts +24 -0
- package/dist/typed_promise.d.ts.map +1 -0
- package/dist/typed_promise.js +28 -0
- package/dist/typed_promise.js.map +1 -0
- package/dist/utils.cjs +30 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +18 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +27 -2
- package/dist/utils.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/dist/voice/agent_activity.cjs +10 -0
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +11 -0
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +1 -1
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +4 -2
- package/dist/voice/agent_session.d.ts +4 -2
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +1 -1
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/events.cjs +11 -0
- package/dist/voice/events.cjs.map +1 -1
- package/dist/voice/events.d.cts +12 -1
- package/dist/voice/events.d.ts +12 -1
- package/dist/voice/events.d.ts.map +1 -1
- package/dist/voice/events.js +10 -0
- package/dist/voice/events.js.map +1 -1
- package/dist/voice/generation.cjs +23 -4
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +32 -5
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/generation_tts_timeout.test.cjs +85 -0
- package/dist/voice/generation_tts_timeout.test.cjs.map +1 -0
- package/dist/voice/generation_tts_timeout.test.js +84 -0
- package/dist/voice/generation_tts_timeout.test.js.map +1 -0
- package/dist/voice/index.cjs.map +1 -1
- package/dist/voice/index.d.cts +1 -1
- package/dist/voice/index.d.ts +1 -1
- package/dist/voice/index.d.ts.map +1 -1
- package/dist/voice/index.js +3 -1
- package/dist/voice/index.js.map +1 -1
- package/dist/voice/recorder_io/recorder_io.cjs +1 -2
- package/dist/voice/recorder_io/recorder_io.cjs.map +1 -1
- package/dist/voice/recorder_io/recorder_io.d.ts.map +1 -1
- package/dist/voice/recorder_io/recorder_io.js +2 -3
- package/dist/voice/recorder_io/recorder_io.js.map +1 -1
- package/dist/voice/report.cjs +1 -1
- package/dist/voice/report.cjs.map +1 -1
- package/dist/voice/report.js +1 -1
- package/dist/voice/report.js.map +1 -1
- package/dist/voice/report.test.cjs +70 -0
- package/dist/voice/report.test.cjs.map +1 -1
- package/dist/voice/report.test.js +70 -0
- package/dist/voice/report.test.js.map +1 -1
- package/dist/voice/room_io/room_io.cjs +5 -1
- package/dist/voice/room_io/room_io.cjs.map +1 -1
- package/dist/voice/room_io/room_io.d.ts.map +1 -1
- package/dist/voice/room_io/room_io.js +5 -1
- package/dist/voice/room_io/room_io.js.map +1 -1
- package/dist/voice/room_io/room_io.test.cjs +18 -0
- package/dist/voice/room_io/room_io.test.cjs.map +1 -0
- package/dist/voice/room_io/room_io.test.js +17 -0
- package/dist/voice/room_io/room_io.test.js.map +1 -0
- package/package.json +4 -2
- package/src/_exceptions.ts +5 -0
- package/src/audio.ts +12 -1
- package/src/beta/workflows/task_group.ts +14 -5
- package/src/inference/interruption/http_transport.ts +2 -1
- package/src/inference/interruption/ws_transport.ts +44 -34
- package/src/inference/tts.test.ts +87 -0
- package/src/inference/tts.ts +71 -9
- package/src/ipc/job_proc_lazy_main.ts +7 -2
- package/src/ipc/supervised_proc.test.ts +96 -0
- package/src/ipc/supervised_proc.ts +8 -1
- package/src/job.ts +1 -0
- package/src/llm/chat_context.test.ts +137 -5
- package/src/llm/chat_context.ts +119 -38
- package/src/llm/fallback_adapter.ts +5 -2
- package/src/llm/index.ts +2 -0
- package/src/llm/utils.test.ts +103 -2
- package/src/llm/utils.ts +128 -0
- package/src/stt/stt.ts +9 -1
- package/src/tts/fallback_adapter.ts +9 -2
- package/src/typed_promise.ts +67 -0
- package/src/utils.ts +45 -2
- package/src/voice/agent_activity.ts +11 -0
- package/src/voice/agent_session.ts +13 -7
- package/src/voice/events.ts +21 -0
- package/src/voice/generation.ts +35 -8
- package/src/voice/generation_tts_timeout.test.ts +112 -0
- package/src/voice/index.ts +6 -1
- package/src/voice/recorder_io/recorder_io.ts +2 -7
- package/src/voice/report.test.ts +78 -0
- package/src/voice/report.ts +1 -1
- package/src/voice/room_io/room_io.test.ts +38 -0
- package/src/voice/room_io/room_io.ts +7 -2
package/dist/stt/stt.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/stt/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { calculateAudioDurationSeconds } from '../audio.js';\nimport type { LanguageCode } from '../language.js';\nimport { log } from '../log.js';\nimport type { STTMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport type { AudioBuffer } from '../utils.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/index.js';\n\n/** Indicates start/middle/end of speech */\nexport enum SpeechEventType {\n /**\n * Indicate the start of speech.\n * If the STT doesn't support this event, this will be emitted at the same time\n * as the first INTERIM_TRANSCRIPT.\n */\n START_OF_SPEECH = 0,\n /**\n * Interim transcript, useful for real-time transcription.\n */\n INTERIM_TRANSCRIPT = 1,\n /**\n * Final transcript, emitted when the STT is confident enough that a certain\n * portion of the speech will not change.\n */\n FINAL_TRANSCRIPT = 2,\n /**\n * Indicate the end of speech, emitted when the user stops speaking.\n * The first alternative is a combination of all the previous FINAL_TRANSCRIPT events.\n */\n END_OF_SPEECH = 3,\n /** Usage event, emitted periodically to indicate usage metrics. */\n RECOGNITION_USAGE = 4,\n /**\n * Preflight transcript, emitted before final transcript when STT has high confidence\n * but hasn't fully committed yet. Includes all pre-committed transcripts including\n * final transcript from the previous STT run.\n */\n PREFLIGHT_TRANSCRIPT = 5,\n}\n\n/** SpeechData contains metadata about this {@link SpeechEvent}. */\nexport interface SpeechData {\n /** Language code of the speech. */\n language: LanguageCode;\n /** Transcribed text. */\n text: string;\n /** Start time of the speech segment in seconds. */\n startTime: number;\n /** End time of the speech segment in seconds. */\n endTime: number;\n /** Confidence score of the transcription (0-1). */\n confidence: number;\n /** Word-level timing information. */\n words?: TimedString[];\n}\n\nexport interface RecognitionUsage {\n /** Duration of the audio that was recognized in seconds. */\n audioDuration: number;\n /** Input audio tokens (for token-based STT billing). */\n inputTokens?: number;\n /** Output text tokens (for token-based STT billing). */\n outputTokens?: number;\n}\n\n/** SpeechEvent is a packet of speech-to-text data. */\nexport interface SpeechEvent {\n type: SpeechEventType;\n alternatives?: [SpeechData, ...SpeechData[]];\n requestId?: string;\n recognitionUsage?: RecognitionUsage;\n}\n\n/**\n * Describes the capabilities of the STT provider.\n *\n * @remarks\n * At present, the framework only supports providers that have a streaming endpoint.\n */\nexport interface STTCapabilities {\n streaming: boolean;\n interimResults: boolean;\n /**\n * Whether this STT supports aligned transcripts with word/chunk timestamps.\n * - 'word': Provider returns word-level timestamps\n * - 'chunk': Provider returns chunk-level timestamps (e.g., sentence/phrase boundaries)\n * - false: Provider does not support aligned transcripts\n */\n alignedTranscript?: 'word' | 'chunk' | false;\n}\n\nexport interface STTError {\n type: 'stt_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type STTCallbacks = {\n ['metrics_collected']: (metrics: STTMetrics) => void;\n ['error']: (error: STTError) => void;\n};\n\n/**\n * An instance of a speech-to-text adapter.\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child STT class, which inherits this class's methods.\n */\nexport abstract class STT extends (EventEmitter as new () => TypedEmitter<STTCallbacks>) {\n abstract label: string;\n #capabilities: STTCapabilities;\n\n constructor(capabilities: STTCapabilities) {\n super();\n this.#capabilities = capabilities;\n }\n\n /** Returns this STT's capabilities */\n get capabilities(): STTCapabilities {\n return this.#capabilities;\n }\n\n /**\n * Get the model name/identifier for this STT instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Get the provider name for this STT instance.\n *\n * @returns The provider name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their provider information.\n */\n get provider(): string {\n return 'unknown';\n }\n\n /** Receives an audio buffer and returns transcription in the form of a {@link SpeechEvent} */\n async recognize(frame: AudioBuffer, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n const startTime = process.hrtime.bigint();\n const event = await this._recognize(frame, abortSignal);\n const durationMs = Number((process.hrtime.bigint() - startTime) / BigInt(1000000));\n this.emit('metrics_collected', {\n type: 'stt_metrics',\n requestId: event.requestId ?? '',\n timestamp: Date.now(),\n durationMs,\n label: this.label,\n audioDurationMs: Math.round(calculateAudioDurationSeconds(frame) * 1000),\n streamed: false,\n metadata: {\n modelProvider: this.provider,\n modelName: this.model,\n },\n });\n return event;\n }\n\n protected abstract _recognize(\n frame: AudioBuffer,\n abortSignal?: AbortSignal,\n ): Promise<SpeechEvent>;\n\n /**\n * Returns a {@link SpeechStream} that can be used to push audio frames and receive\n * transcriptions\n *\n * @param options - Optional configuration including connection options\n */\n abstract stream(options?: { connOptions?: APIConnectOptions }): SpeechStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a speech-to-text stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * if (event.type === SpeechEventType.FINAL_TRANSCRIPT) {\n * console.log(event.alternatives[0].text)\n * }\n * }\n * ```\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child SpeechStream class, which inherits this class's methods.\n */\nexport abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> {\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n protected input = new AsyncIterableQueue<AudioFrame | typeof SpeechStream.FLUSH_SENTINEL>();\n protected output = new AsyncIterableQueue<SpeechEvent>();\n protected queue = new AsyncIterableQueue<SpeechEvent>();\n protected neededSampleRate?: number;\n protected resampler?: AudioResampler;\n abstract label: string;\n protected closed = false;\n #stt: STT;\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n private logger = log();\n private _connOptions: APIConnectOptions;\n private _startTimeOffset: number = 0;\n\n protected abortController = new AbortController();\n\n constructor(\n stt: STT,\n sampleRate?: number,\n connectionOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n ) {\n this.#stt = stt;\n this._connOptions = connectionOptions;\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n this.neededSampleRate = sampleRate;\n this.monitorMetrics();\n this.pumpInput();\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n private async mainTask() {\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await this.run();\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = intervalForRetry(this._connOptions, i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to recognize speech after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n // Don't emit error event for recoverable errors during retry loop\n // to avoid ERR_UNHANDLED_ERROR or premature session termination\n this.logger.warn(\n { stt: this.#stt.label, attempt: i + 1, error },\n `failed to recognize speech, retrying in ${retryInterval}ms`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#stt.emit('error', {\n type: 'stt_error',\n timestamp: Date.now(),\n label: this.#stt.label,\n error,\n recoverable,\n });\n }\n\n protected async pumpInput() {\n // TODO(AJS-35): Implement STT with webstreams API\n const inputStream = this.deferredInputStream.stream;\n const reader = inputStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n this.pushFrame(value);\n }\n } catch (error) {\n this.logger.error('Error in STTStream mainTask:', error);\n } finally {\n reader.releaseLock();\n }\n }\n\n protected async monitorMetrics() {\n for await (const event of this.queue) {\n if (!this.output.closed) {\n try {\n this.output.put(event);\n } catch (e) {\n if (e instanceof Error && e.message.includes('Queue is closed')) {\n this.logger.warn(\n { err: e },\n 'Queue closed during transcript processing (expected during disconnect)',\n );\n }\n }\n }\n if (event.type !== SpeechEventType.RECOGNITION_USAGE) continue;\n const metrics: STTMetrics = {\n type: 'stt_metrics',\n timestamp: Date.now(),\n requestId: event.requestId!,\n durationMs: 0,\n label: this.#stt.label,\n audioDurationMs: Math.round(event.recognitionUsage!.audioDuration * 1000),\n inputTokens: event.recognitionUsage!.inputTokens ?? 0,\n outputTokens: event.recognitionUsage!.outputTokens ?? 0,\n streamed: true,\n metadata: {\n modelProvider: this.#stt.provider,\n modelName: this.#stt.model,\n },\n };\n this.#stt.emit('metrics_collected', metrics);\n }\n if (!this.output.closed) {\n this.output.close();\n }\n }\n\n protected abstract run(): Promise<void>;\n\n protected get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n get startTimeOffset(): number {\n return this._startTimeOffset;\n }\n\n set startTimeOffset(value: number) {\n if (value < 0) {\n throw new Error('startTimeOffset must be non-negative');\n }\n this._startTimeOffset = value;\n }\n\n updateInputStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputStream() {\n this.deferredInputStream.detachSource();\n }\n\n /** Push an audio frame to the STT */\n pushFrame(frame: AudioFrame) {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n\n if (this.neededSampleRate && frame.sampleRate !== this.neededSampleRate) {\n if (!this.resampler) {\n this.resampler = new AudioResampler(frame.sampleRate, this.neededSampleRate);\n }\n }\n\n if (frame.samplesPerChannel === 0) {\n this.input.put(frame);\n return;\n }\n\n if (this.resampler) {\n const frames = this.resampler.push(frame);\n for (const frame of frames) {\n this.input.put(frame);\n }\n } else {\n this.input.put(frame);\n }\n }\n\n /** Flush the STT, causing it to process all pending text */\n flush() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.put(SpeechStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SpeechEvent>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the STT stream */\n close() {\n if (!this.input.closed) this.input.close();\n if (!this.queue.closed) this.queue.close();\n if (!this.output.closed) this.output.close();\n if (!this.abortController.signal.aborted) this.abortController.abort();\n this.closed = true;\n }\n\n [Symbol.asyncIterator](): SpeechStream {\n return this;\n }\n}\n"],"mappings":"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","names":["SpeechEventType","frame"]}
|
|
1
|
+
{"version":3,"sources":["../../src/stt/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { calculateAudioDurationSeconds } from '../audio.js';\nimport type { LanguageCode } from '../language.js';\nimport { log } from '../log.js';\nimport type { STTMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport type { AudioBuffer } from '../utils.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/index.js';\n\n/** Indicates start/middle/end of speech */\nexport enum SpeechEventType {\n /**\n * Indicate the start of speech.\n * If the STT doesn't support this event, this will be emitted at the same time\n * as the first INTERIM_TRANSCRIPT.\n */\n START_OF_SPEECH = 0,\n /**\n * Interim transcript, useful for real-time transcription.\n */\n INTERIM_TRANSCRIPT = 1,\n /**\n * Final transcript, emitted when the STT is confident enough that a certain\n * portion of the speech will not change.\n */\n FINAL_TRANSCRIPT = 2,\n /**\n * Indicate the end of speech, emitted when the user stops speaking.\n * The first alternative is a combination of all the previous FINAL_TRANSCRIPT events.\n */\n END_OF_SPEECH = 3,\n /** Usage event, emitted periodically to indicate usage metrics. */\n RECOGNITION_USAGE = 4,\n /**\n * Preflight transcript, emitted before final transcript when STT has high confidence\n * but hasn't fully committed yet. Includes all pre-committed transcripts including\n * final transcript from the previous STT run.\n */\n PREFLIGHT_TRANSCRIPT = 5,\n}\n\n/** SpeechData contains metadata about this {@link SpeechEvent}. */\nexport interface SpeechData {\n /** Language code of the speech. */\n language: LanguageCode;\n /** Transcribed text. */\n text: string;\n /** Start time of the speech segment in seconds. */\n startTime: number;\n /** End time of the speech segment in seconds. */\n endTime: number;\n /** Confidence score of the transcription (0-1). */\n confidence: number;\n /** Word-level timing information. */\n words?: TimedString[];\n}\n\nexport interface RecognitionUsage {\n /** Duration of the audio that was recognized in seconds. */\n audioDuration: number;\n /** Input audio tokens (for token-based STT billing). */\n inputTokens?: number;\n /** Output text tokens (for token-based STT billing). */\n outputTokens?: number;\n}\n\n/** SpeechEvent is a packet of speech-to-text data. */\nexport interface SpeechEvent {\n type: SpeechEventType;\n alternatives?: [SpeechData, ...SpeechData[]];\n requestId?: string;\n recognitionUsage?: RecognitionUsage;\n}\n\n/**\n * Describes the capabilities of the STT provider.\n *\n * @remarks\n * At present, the framework only supports providers that have a streaming endpoint.\n */\nexport interface STTCapabilities {\n streaming: boolean;\n interimResults: boolean;\n /**\n * Whether this STT supports aligned transcripts with word/chunk timestamps.\n * - 'word': Provider returns word-level timestamps\n * - 'chunk': Provider returns chunk-level timestamps (e.g., sentence/phrase boundaries)\n * - false: Provider does not support aligned transcripts\n */\n alignedTranscript?: 'word' | 'chunk' | false;\n}\n\nexport interface STTError {\n type: 'stt_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type STTCallbacks = {\n ['metrics_collected']: (metrics: STTMetrics) => void;\n ['error']: (error: STTError) => void;\n};\n\n/**\n * An instance of a speech-to-text adapter.\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child STT class, which inherits this class's methods.\n */\nexport abstract class STT extends (EventEmitter as new () => TypedEmitter<STTCallbacks>) {\n abstract label: string;\n #capabilities: STTCapabilities;\n\n constructor(capabilities: STTCapabilities) {\n super();\n this.#capabilities = capabilities;\n }\n\n /** Returns this STT's capabilities */\n get capabilities(): STTCapabilities {\n return this.#capabilities;\n }\n\n /**\n * Get the model name/identifier for this STT instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Get the provider name for this STT instance.\n *\n * @returns The provider name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their provider information.\n */\n get provider(): string {\n return 'unknown';\n }\n\n /** Receives an audio buffer and returns transcription in the form of a {@link SpeechEvent} */\n async recognize(frame: AudioBuffer, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n const startTime = process.hrtime.bigint();\n const event = await this._recognize(frame, abortSignal);\n const durationMs = Number((process.hrtime.bigint() - startTime) / BigInt(1000000));\n this.emit('metrics_collected', {\n type: 'stt_metrics',\n requestId: event.requestId ?? '',\n timestamp: Date.now(),\n durationMs,\n label: this.label,\n audioDurationMs: Math.round(calculateAudioDurationSeconds(frame) * 1000),\n streamed: false,\n metadata: {\n modelProvider: this.provider,\n modelName: this.model,\n },\n });\n return event;\n }\n\n protected abstract _recognize(\n frame: AudioBuffer,\n abortSignal?: AbortSignal,\n ): Promise<SpeechEvent>;\n\n /**\n * Returns a {@link SpeechStream} that can be used to push audio frames and receive\n * transcriptions\n *\n * @param options - Optional configuration including connection options\n */\n abstract stream(options?: { connOptions?: APIConnectOptions }): SpeechStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a speech-to-text stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * if (event.type === SpeechEventType.FINAL_TRANSCRIPT) {\n * console.log(event.alternatives[0].text)\n * }\n * }\n * ```\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child SpeechStream class, which inherits this class's methods.\n */\nexport abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> {\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n protected input = new AsyncIterableQueue<AudioFrame | typeof SpeechStream.FLUSH_SENTINEL>();\n protected output = new AsyncIterableQueue<SpeechEvent>();\n protected queue = new AsyncIterableQueue<SpeechEvent>();\n protected neededSampleRate?: number;\n protected resampler?: AudioResampler;\n abstract label: string;\n protected closed = false;\n #stt: STT;\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n private logger = log();\n private _connOptions: APIConnectOptions;\n private _startTimeOffset: number = 0;\n\n protected abortController = new AbortController();\n\n constructor(\n stt: STT,\n sampleRate?: number,\n connectionOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n ) {\n this.#stt = stt;\n this._connOptions = connectionOptions;\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n this.neededSampleRate = sampleRate;\n this.monitorMetrics();\n this.pumpInput();\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n /**\n * Runs the STT with retry logic. Errors are emitted via {@link STT} error events\n * and then re-thrown to trigger `.finally()` cleanup.\n *\n * @throws {APIError} When the STT request fails with a non-retryable error\n * @throws {APIConnectionError} When all retry attempts are exhausted\n * @internal Not annotated with Throws<> because this is fire-and-forget via startSoon()\n */\n private async mainTask(): Promise<void> {\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await this.run();\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = intervalForRetry(this._connOptions, i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to recognize speech after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n // Don't emit error event for recoverable errors during retry loop\n // to avoid ERR_UNHANDLED_ERROR or premature session termination\n this.logger.warn(\n { stt: this.#stt.label, attempt: i + 1, error },\n `failed to recognize speech, retrying in ${retryInterval}ms`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#stt.emit('error', {\n type: 'stt_error',\n timestamp: Date.now(),\n label: this.#stt.label,\n error,\n recoverable,\n });\n }\n\n protected async pumpInput() {\n // TODO(AJS-35): Implement STT with webstreams API\n const inputStream = this.deferredInputStream.stream;\n const reader = inputStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n this.pushFrame(value);\n }\n } catch (error) {\n this.logger.error('Error in STTStream mainTask:', error);\n } finally {\n reader.releaseLock();\n }\n }\n\n protected async monitorMetrics() {\n for await (const event of this.queue) {\n if (!this.output.closed) {\n try {\n this.output.put(event);\n } catch (e) {\n if (e instanceof Error && e.message.includes('Queue is closed')) {\n this.logger.warn(\n { err: e },\n 'Queue closed during transcript processing (expected during disconnect)',\n );\n }\n }\n }\n if (event.type !== SpeechEventType.RECOGNITION_USAGE) continue;\n const metrics: STTMetrics = {\n type: 'stt_metrics',\n timestamp: Date.now(),\n requestId: event.requestId!,\n durationMs: 0,\n label: this.#stt.label,\n audioDurationMs: Math.round(event.recognitionUsage!.audioDuration * 1000),\n inputTokens: event.recognitionUsage!.inputTokens ?? 0,\n outputTokens: event.recognitionUsage!.outputTokens ?? 0,\n streamed: true,\n metadata: {\n modelProvider: this.#stt.provider,\n modelName: this.#stt.model,\n },\n };\n this.#stt.emit('metrics_collected', metrics);\n }\n if (!this.output.closed) {\n this.output.close();\n }\n }\n\n protected abstract run(): Promise<void>;\n\n protected get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n get startTimeOffset(): number {\n return this._startTimeOffset;\n }\n\n set startTimeOffset(value: number) {\n if (value < 0) {\n throw new Error('startTimeOffset must be non-negative');\n }\n this._startTimeOffset = value;\n }\n\n updateInputStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputStream() {\n this.deferredInputStream.detachSource();\n }\n\n /** Push an audio frame to the STT */\n pushFrame(frame: AudioFrame) {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n\n if (this.neededSampleRate && frame.sampleRate !== this.neededSampleRate) {\n if (!this.resampler) {\n this.resampler = new AudioResampler(frame.sampleRate, this.neededSampleRate);\n }\n }\n\n if (frame.samplesPerChannel === 0) {\n this.input.put(frame);\n return;\n }\n\n if (this.resampler) {\n const frames = this.resampler.push(frame);\n for (const frame of frames) {\n this.input.put(frame);\n }\n } else {\n this.input.put(frame);\n }\n }\n\n /** Flush the STT, causing it to process all pending text */\n flush() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.put(SpeechStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SpeechEvent>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the STT stream */\n close() {\n if (!this.input.closed) this.input.close();\n if (!this.queue.closed) this.queue.close();\n if (!this.output.closed) this.output.close();\n if (!this.abortController.signal.aborted) this.abortController.abort();\n this.closed = true;\n }\n\n [Symbol.asyncIterator](): SpeechStream {\n return this;\n }\n}\n"],"mappings":"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","names":["SpeechEventType","frame"]}
|
package/dist/stt/stt.d.cts
CHANGED
|
@@ -175,6 +175,14 @@ export declare abstract class SpeechStream implements AsyncIterableIterator<Spee
|
|
|
175
175
|
private _startTimeOffset;
|
|
176
176
|
protected abortController: AbortController;
|
|
177
177
|
constructor(stt: STT, sampleRate?: number, connectionOptions?: APIConnectOptions);
|
|
178
|
+
/**
|
|
179
|
+
* Runs the STT with retry logic. Errors are emitted via {@link STT} error events
|
|
180
|
+
* and then re-thrown to trigger `.finally()` cleanup.
|
|
181
|
+
*
|
|
182
|
+
* @throws {APIError} When the STT request fails with a non-retryable error
|
|
183
|
+
* @throws {APIConnectionError} When all retry attempts are exhausted
|
|
184
|
+
* @internal Not annotated with Throws<> because this is fire-and-forget via startSoon()
|
|
185
|
+
*/
|
|
178
186
|
private mainTask;
|
|
179
187
|
private emitError;
|
|
180
188
|
protected pumpInput(): Promise<void>;
|
package/dist/stt/stt.d.ts
CHANGED
|
@@ -175,6 +175,14 @@ export declare abstract class SpeechStream implements AsyncIterableIterator<Spee
|
|
|
175
175
|
private _startTimeOffset;
|
|
176
176
|
protected abortController: AbortController;
|
|
177
177
|
constructor(stt: STT, sampleRate?: number, connectionOptions?: APIConnectOptions);
|
|
178
|
+
/**
|
|
179
|
+
* Runs the STT with retry logic. Errors are emitted via {@link STT} error events
|
|
180
|
+
* and then re-thrown to trigger `.finally()` cleanup.
|
|
181
|
+
*
|
|
182
|
+
* @throws {APIError} When the STT request fails with a non-retryable error
|
|
183
|
+
* @throws {APIConnectionError} When all retry attempts are exhausted
|
|
184
|
+
* @internal Not annotated with Throws<> because this is fire-and-forget via startSoon()
|
|
185
|
+
*/
|
|
178
186
|
private mainTask;
|
|
179
187
|
private emitError;
|
|
180
188
|
protected pumpInput(): Promise<void>;
|
package/dist/stt/stt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stt.d.ts","sourceRoot":"","sources":["../../src/stt/stt.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"stt.d.ts","sourceRoot":"","sources":["../../src/stt/stt.ts"],"names":[],"mappings":"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"}
|
package/dist/stt/stt.js
CHANGED
|
@@ -94,6 +94,14 @@ class SpeechStream {
|
|
|
94
94
|
this.pumpInput();
|
|
95
95
|
startSoon(() => this.mainTask().finally(() => this.queue.close()));
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Runs the STT with retry logic. Errors are emitted via {@link STT} error events
|
|
99
|
+
* and then re-thrown to trigger `.finally()` cleanup.
|
|
100
|
+
*
|
|
101
|
+
* @throws {APIError} When the STT request fails with a non-retryable error
|
|
102
|
+
* @throws {APIConnectionError} When all retry attempts are exhausted
|
|
103
|
+
* @internal Not annotated with Throws<> because this is fire-and-forget via startSoon()
|
|
104
|
+
*/
|
|
97
105
|
async mainTask() {
|
|
98
106
|
for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
|
|
99
107
|
try {
|
package/dist/stt/stt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/stt/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { calculateAudioDurationSeconds } from '../audio.js';\nimport type { LanguageCode } from '../language.js';\nimport { log } from '../log.js';\nimport type { STTMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport type { AudioBuffer } from '../utils.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/index.js';\n\n/** Indicates start/middle/end of speech */\nexport enum SpeechEventType {\n /**\n * Indicate the start of speech.\n * If the STT doesn't support this event, this will be emitted at the same time\n * as the first INTERIM_TRANSCRIPT.\n */\n START_OF_SPEECH = 0,\n /**\n * Interim transcript, useful for real-time transcription.\n */\n INTERIM_TRANSCRIPT = 1,\n /**\n * Final transcript, emitted when the STT is confident enough that a certain\n * portion of the speech will not change.\n */\n FINAL_TRANSCRIPT = 2,\n /**\n * Indicate the end of speech, emitted when the user stops speaking.\n * The first alternative is a combination of all the previous FINAL_TRANSCRIPT events.\n */\n END_OF_SPEECH = 3,\n /** Usage event, emitted periodically to indicate usage metrics. */\n RECOGNITION_USAGE = 4,\n /**\n * Preflight transcript, emitted before final transcript when STT has high confidence\n * but hasn't fully committed yet. Includes all pre-committed transcripts including\n * final transcript from the previous STT run.\n */\n PREFLIGHT_TRANSCRIPT = 5,\n}\n\n/** SpeechData contains metadata about this {@link SpeechEvent}. */\nexport interface SpeechData {\n /** Language code of the speech. */\n language: LanguageCode;\n /** Transcribed text. */\n text: string;\n /** Start time of the speech segment in seconds. */\n startTime: number;\n /** End time of the speech segment in seconds. */\n endTime: number;\n /** Confidence score of the transcription (0-1). */\n confidence: number;\n /** Word-level timing information. */\n words?: TimedString[];\n}\n\nexport interface RecognitionUsage {\n /** Duration of the audio that was recognized in seconds. */\n audioDuration: number;\n /** Input audio tokens (for token-based STT billing). */\n inputTokens?: number;\n /** Output text tokens (for token-based STT billing). */\n outputTokens?: number;\n}\n\n/** SpeechEvent is a packet of speech-to-text data. */\nexport interface SpeechEvent {\n type: SpeechEventType;\n alternatives?: [SpeechData, ...SpeechData[]];\n requestId?: string;\n recognitionUsage?: RecognitionUsage;\n}\n\n/**\n * Describes the capabilities of the STT provider.\n *\n * @remarks\n * At present, the framework only supports providers that have a streaming endpoint.\n */\nexport interface STTCapabilities {\n streaming: boolean;\n interimResults: boolean;\n /**\n * Whether this STT supports aligned transcripts with word/chunk timestamps.\n * - 'word': Provider returns word-level timestamps\n * - 'chunk': Provider returns chunk-level timestamps (e.g., sentence/phrase boundaries)\n * - false: Provider does not support aligned transcripts\n */\n alignedTranscript?: 'word' | 'chunk' | false;\n}\n\nexport interface STTError {\n type: 'stt_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type STTCallbacks = {\n ['metrics_collected']: (metrics: STTMetrics) => void;\n ['error']: (error: STTError) => void;\n};\n\n/**\n * An instance of a speech-to-text adapter.\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child STT class, which inherits this class's methods.\n */\nexport abstract class STT extends (EventEmitter as new () => TypedEmitter<STTCallbacks>) {\n abstract label: string;\n #capabilities: STTCapabilities;\n\n constructor(capabilities: STTCapabilities) {\n super();\n this.#capabilities = capabilities;\n }\n\n /** Returns this STT's capabilities */\n get capabilities(): STTCapabilities {\n return this.#capabilities;\n }\n\n /**\n * Get the model name/identifier for this STT instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Get the provider name for this STT instance.\n *\n * @returns The provider name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their provider information.\n */\n get provider(): string {\n return 'unknown';\n }\n\n /** Receives an audio buffer and returns transcription in the form of a {@link SpeechEvent} */\n async recognize(frame: AudioBuffer, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n const startTime = process.hrtime.bigint();\n const event = await this._recognize(frame, abortSignal);\n const durationMs = Number((process.hrtime.bigint() - startTime) / BigInt(1000000));\n this.emit('metrics_collected', {\n type: 'stt_metrics',\n requestId: event.requestId ?? '',\n timestamp: Date.now(),\n durationMs,\n label: this.label,\n audioDurationMs: Math.round(calculateAudioDurationSeconds(frame) * 1000),\n streamed: false,\n metadata: {\n modelProvider: this.provider,\n modelName: this.model,\n },\n });\n return event;\n }\n\n protected abstract _recognize(\n frame: AudioBuffer,\n abortSignal?: AbortSignal,\n ): Promise<SpeechEvent>;\n\n /**\n * Returns a {@link SpeechStream} that can be used to push audio frames and receive\n * transcriptions\n *\n * @param options - Optional configuration including connection options\n */\n abstract stream(options?: { connOptions?: APIConnectOptions }): SpeechStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a speech-to-text stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * if (event.type === SpeechEventType.FINAL_TRANSCRIPT) {\n * console.log(event.alternatives[0].text)\n * }\n * }\n * ```\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child SpeechStream class, which inherits this class's methods.\n */\nexport abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> {\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n protected input = new AsyncIterableQueue<AudioFrame | typeof SpeechStream.FLUSH_SENTINEL>();\n protected output = new AsyncIterableQueue<SpeechEvent>();\n protected queue = new AsyncIterableQueue<SpeechEvent>();\n protected neededSampleRate?: number;\n protected resampler?: AudioResampler;\n abstract label: string;\n protected closed = false;\n #stt: STT;\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n private logger = log();\n private _connOptions: APIConnectOptions;\n private _startTimeOffset: number = 0;\n\n protected abortController = new AbortController();\n\n constructor(\n stt: STT,\n sampleRate?: number,\n connectionOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n ) {\n this.#stt = stt;\n this._connOptions = connectionOptions;\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n this.neededSampleRate = sampleRate;\n this.monitorMetrics();\n this.pumpInput();\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n private async mainTask() {\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await this.run();\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = intervalForRetry(this._connOptions, i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to recognize speech after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n // Don't emit error event for recoverable errors during retry loop\n // to avoid ERR_UNHANDLED_ERROR or premature session termination\n this.logger.warn(\n { stt: this.#stt.label, attempt: i + 1, error },\n `failed to recognize speech, retrying in ${retryInterval}ms`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#stt.emit('error', {\n type: 'stt_error',\n timestamp: Date.now(),\n label: this.#stt.label,\n error,\n recoverable,\n });\n }\n\n protected async pumpInput() {\n // TODO(AJS-35): Implement STT with webstreams API\n const inputStream = this.deferredInputStream.stream;\n const reader = inputStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n this.pushFrame(value);\n }\n } catch (error) {\n this.logger.error('Error in STTStream mainTask:', error);\n } finally {\n reader.releaseLock();\n }\n }\n\n protected async monitorMetrics() {\n for await (const event of this.queue) {\n if (!this.output.closed) {\n try {\n this.output.put(event);\n } catch (e) {\n if (e instanceof Error && e.message.includes('Queue is closed')) {\n this.logger.warn(\n { err: e },\n 'Queue closed during transcript processing (expected during disconnect)',\n );\n }\n }\n }\n if (event.type !== SpeechEventType.RECOGNITION_USAGE) continue;\n const metrics: STTMetrics = {\n type: 'stt_metrics',\n timestamp: Date.now(),\n requestId: event.requestId!,\n durationMs: 0,\n label: this.#stt.label,\n audioDurationMs: Math.round(event.recognitionUsage!.audioDuration * 1000),\n inputTokens: event.recognitionUsage!.inputTokens ?? 0,\n outputTokens: event.recognitionUsage!.outputTokens ?? 0,\n streamed: true,\n metadata: {\n modelProvider: this.#stt.provider,\n modelName: this.#stt.model,\n },\n };\n this.#stt.emit('metrics_collected', metrics);\n }\n if (!this.output.closed) {\n this.output.close();\n }\n }\n\n protected abstract run(): Promise<void>;\n\n protected get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n get startTimeOffset(): number {\n return this._startTimeOffset;\n }\n\n set startTimeOffset(value: number) {\n if (value < 0) {\n throw new Error('startTimeOffset must be non-negative');\n }\n this._startTimeOffset = value;\n }\n\n updateInputStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputStream() {\n this.deferredInputStream.detachSource();\n }\n\n /** Push an audio frame to the STT */\n pushFrame(frame: AudioFrame) {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n\n if (this.neededSampleRate && frame.sampleRate !== this.neededSampleRate) {\n if (!this.resampler) {\n this.resampler = new AudioResampler(frame.sampleRate, this.neededSampleRate);\n }\n }\n\n if (frame.samplesPerChannel === 0) {\n this.input.put(frame);\n return;\n }\n\n if (this.resampler) {\n const frames = this.resampler.push(frame);\n for (const frame of frames) {\n this.input.put(frame);\n }\n } else {\n this.input.put(frame);\n }\n }\n\n /** Flush the STT, causing it to process all pending text */\n flush() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.put(SpeechStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SpeechEvent>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the STT stream */\n close() {\n if (!this.input.closed) this.input.close();\n if (!this.queue.closed) this.queue.close();\n if (!this.output.closed) this.output.close();\n if (!this.abortController.signal.aborted) this.abortController.abort();\n this.closed = true;\n }\n\n [Symbol.asyncIterator](): SpeechStream {\n return this;\n }\n}\n"],"mappings":"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","names":["SpeechEventType","frame"]}
|
|
1
|
+
{"version":3,"sources":["../../src/stt/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type AudioFrame, AudioResampler } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { calculateAudioDurationSeconds } from '../audio.js';\nimport type { LanguageCode } from '../language.js';\nimport { log } from '../log.js';\nimport type { STTMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport type { AudioBuffer } from '../utils.js';\nimport { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/index.js';\n\n/** Indicates start/middle/end of speech */\nexport enum SpeechEventType {\n /**\n * Indicate the start of speech.\n * If the STT doesn't support this event, this will be emitted at the same time\n * as the first INTERIM_TRANSCRIPT.\n */\n START_OF_SPEECH = 0,\n /**\n * Interim transcript, useful for real-time transcription.\n */\n INTERIM_TRANSCRIPT = 1,\n /**\n * Final transcript, emitted when the STT is confident enough that a certain\n * portion of the speech will not change.\n */\n FINAL_TRANSCRIPT = 2,\n /**\n * Indicate the end of speech, emitted when the user stops speaking.\n * The first alternative is a combination of all the previous FINAL_TRANSCRIPT events.\n */\n END_OF_SPEECH = 3,\n /** Usage event, emitted periodically to indicate usage metrics. */\n RECOGNITION_USAGE = 4,\n /**\n * Preflight transcript, emitted before final transcript when STT has high confidence\n * but hasn't fully committed yet. Includes all pre-committed transcripts including\n * final transcript from the previous STT run.\n */\n PREFLIGHT_TRANSCRIPT = 5,\n}\n\n/** SpeechData contains metadata about this {@link SpeechEvent}. */\nexport interface SpeechData {\n /** Language code of the speech. */\n language: LanguageCode;\n /** Transcribed text. */\n text: string;\n /** Start time of the speech segment in seconds. */\n startTime: number;\n /** End time of the speech segment in seconds. */\n endTime: number;\n /** Confidence score of the transcription (0-1). */\n confidence: number;\n /** Word-level timing information. */\n words?: TimedString[];\n}\n\nexport interface RecognitionUsage {\n /** Duration of the audio that was recognized in seconds. */\n audioDuration: number;\n /** Input audio tokens (for token-based STT billing). */\n inputTokens?: number;\n /** Output text tokens (for token-based STT billing). */\n outputTokens?: number;\n}\n\n/** SpeechEvent is a packet of speech-to-text data. */\nexport interface SpeechEvent {\n type: SpeechEventType;\n alternatives?: [SpeechData, ...SpeechData[]];\n requestId?: string;\n recognitionUsage?: RecognitionUsage;\n}\n\n/**\n * Describes the capabilities of the STT provider.\n *\n * @remarks\n * At present, the framework only supports providers that have a streaming endpoint.\n */\nexport interface STTCapabilities {\n streaming: boolean;\n interimResults: boolean;\n /**\n * Whether this STT supports aligned transcripts with word/chunk timestamps.\n * - 'word': Provider returns word-level timestamps\n * - 'chunk': Provider returns chunk-level timestamps (e.g., sentence/phrase boundaries)\n * - false: Provider does not support aligned transcripts\n */\n alignedTranscript?: 'word' | 'chunk' | false;\n}\n\nexport interface STTError {\n type: 'stt_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type STTCallbacks = {\n ['metrics_collected']: (metrics: STTMetrics) => void;\n ['error']: (error: STTError) => void;\n};\n\n/**\n * An instance of a speech-to-text adapter.\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child STT class, which inherits this class's methods.\n */\nexport abstract class STT extends (EventEmitter as new () => TypedEmitter<STTCallbacks>) {\n abstract label: string;\n #capabilities: STTCapabilities;\n\n constructor(capabilities: STTCapabilities) {\n super();\n this.#capabilities = capabilities;\n }\n\n /** Returns this STT's capabilities */\n get capabilities(): STTCapabilities {\n return this.#capabilities;\n }\n\n /**\n * Get the model name/identifier for this STT instance.\n *\n * @returns The model name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their model information.\n */\n get model(): string {\n return 'unknown';\n }\n\n /**\n * Get the provider name for this STT instance.\n *\n * @returns The provider name if available, \"unknown\" otherwise.\n *\n * @remarks\n * Plugins should override this property to provide their provider information.\n */\n get provider(): string {\n return 'unknown';\n }\n\n /** Receives an audio buffer and returns transcription in the form of a {@link SpeechEvent} */\n async recognize(frame: AudioBuffer, abortSignal?: AbortSignal): Promise<SpeechEvent> {\n const startTime = process.hrtime.bigint();\n const event = await this._recognize(frame, abortSignal);\n const durationMs = Number((process.hrtime.bigint() - startTime) / BigInt(1000000));\n this.emit('metrics_collected', {\n type: 'stt_metrics',\n requestId: event.requestId ?? '',\n timestamp: Date.now(),\n durationMs,\n label: this.label,\n audioDurationMs: Math.round(calculateAudioDurationSeconds(frame) * 1000),\n streamed: false,\n metadata: {\n modelProvider: this.provider,\n modelName: this.model,\n },\n });\n return event;\n }\n\n protected abstract _recognize(\n frame: AudioBuffer,\n abortSignal?: AbortSignal,\n ): Promise<SpeechEvent>;\n\n /**\n * Returns a {@link SpeechStream} that can be used to push audio frames and receive\n * transcriptions\n *\n * @param options - Optional configuration including connection options\n */\n abstract stream(options?: { connOptions?: APIConnectOptions }): SpeechStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a speech-to-text stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * if (event.type === SpeechEventType.FINAL_TRANSCRIPT) {\n * console.log(event.alternatives[0].text)\n * }\n * }\n * ```\n *\n * @remarks\n * This class is abstract, and as such cannot be used directly. Instead, use a provider plugin that\n * exports its own child SpeechStream class, which inherits this class's methods.\n */\nexport abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent> {\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n protected input = new AsyncIterableQueue<AudioFrame | typeof SpeechStream.FLUSH_SENTINEL>();\n protected output = new AsyncIterableQueue<SpeechEvent>();\n protected queue = new AsyncIterableQueue<SpeechEvent>();\n protected neededSampleRate?: number;\n protected resampler?: AudioResampler;\n abstract label: string;\n protected closed = false;\n #stt: STT;\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n private logger = log();\n private _connOptions: APIConnectOptions;\n private _startTimeOffset: number = 0;\n\n protected abortController = new AbortController();\n\n constructor(\n stt: STT,\n sampleRate?: number,\n connectionOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n ) {\n this.#stt = stt;\n this._connOptions = connectionOptions;\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n this.neededSampleRate = sampleRate;\n this.monitorMetrics();\n this.pumpInput();\n\n // this is a hack to immitate asyncio.create_task so that mainTask\n // is run **after** the constructor has finished. Otherwise we get\n // runtime error when trying to access class variables in the\n // `run` method.\n startSoon(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n /**\n * Runs the STT with retry logic. Errors are emitted via {@link STT} error events\n * and then re-thrown to trigger `.finally()` cleanup.\n *\n * @throws {APIError} When the STT request fails with a non-retryable error\n * @throws {APIConnectionError} When all retry attempts are exhausted\n * @internal Not annotated with Throws<> because this is fire-and-forget via startSoon()\n */\n private async mainTask(): Promise<void> {\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await this.run();\n } catch (error) {\n if (error instanceof APIError) {\n const retryInterval = intervalForRetry(this._connOptions, i);\n\n if (this._connOptions.maxRetry === 0 || !error.retryable) {\n this.emitError({ error, recoverable: false });\n throw error;\n } else if (i === this._connOptions.maxRetry) {\n this.emitError({ error, recoverable: false });\n throw new APIConnectionError({\n message: `failed to recognize speech after ${this._connOptions.maxRetry + 1} attempts`,\n options: { retryable: false },\n });\n } else {\n // Don't emit error event for recoverable errors during retry loop\n // to avoid ERR_UNHANDLED_ERROR or premature session termination\n this.logger.warn(\n { stt: this.#stt.label, attempt: i + 1, error },\n `failed to recognize speech, retrying in ${retryInterval}ms`,\n );\n }\n\n if (retryInterval > 0) {\n await delay(retryInterval);\n }\n } else {\n this.emitError({ error: toError(error), recoverable: false });\n throw error;\n }\n }\n }\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#stt.emit('error', {\n type: 'stt_error',\n timestamp: Date.now(),\n label: this.#stt.label,\n error,\n recoverable,\n });\n }\n\n protected async pumpInput() {\n // TODO(AJS-35): Implement STT with webstreams API\n const inputStream = this.deferredInputStream.stream;\n const reader = inputStream.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n this.pushFrame(value);\n }\n } catch (error) {\n this.logger.error('Error in STTStream mainTask:', error);\n } finally {\n reader.releaseLock();\n }\n }\n\n protected async monitorMetrics() {\n for await (const event of this.queue) {\n if (!this.output.closed) {\n try {\n this.output.put(event);\n } catch (e) {\n if (e instanceof Error && e.message.includes('Queue is closed')) {\n this.logger.warn(\n { err: e },\n 'Queue closed during transcript processing (expected during disconnect)',\n );\n }\n }\n }\n if (event.type !== SpeechEventType.RECOGNITION_USAGE) continue;\n const metrics: STTMetrics = {\n type: 'stt_metrics',\n timestamp: Date.now(),\n requestId: event.requestId!,\n durationMs: 0,\n label: this.#stt.label,\n audioDurationMs: Math.round(event.recognitionUsage!.audioDuration * 1000),\n inputTokens: event.recognitionUsage!.inputTokens ?? 0,\n outputTokens: event.recognitionUsage!.outputTokens ?? 0,\n streamed: true,\n metadata: {\n modelProvider: this.#stt.provider,\n modelName: this.#stt.model,\n },\n };\n this.#stt.emit('metrics_collected', metrics);\n }\n if (!this.output.closed) {\n this.output.close();\n }\n }\n\n protected abstract run(): Promise<void>;\n\n protected get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n get startTimeOffset(): number {\n return this._startTimeOffset;\n }\n\n set startTimeOffset(value: number) {\n if (value < 0) {\n throw new Error('startTimeOffset must be non-negative');\n }\n this._startTimeOffset = value;\n }\n\n updateInputStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputStream() {\n this.deferredInputStream.detachSource();\n }\n\n /** Push an audio frame to the STT */\n pushFrame(frame: AudioFrame) {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n\n if (this.neededSampleRate && frame.sampleRate !== this.neededSampleRate) {\n if (!this.resampler) {\n this.resampler = new AudioResampler(frame.sampleRate, this.neededSampleRate);\n }\n }\n\n if (frame.samplesPerChannel === 0) {\n this.input.put(frame);\n return;\n }\n\n if (this.resampler) {\n const frames = this.resampler.push(frame);\n for (const frame of frames) {\n this.input.put(frame);\n }\n } else {\n this.input.put(frame);\n }\n }\n\n /** Flush the STT, causing it to process all pending text */\n flush() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.put(SpeechStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n if (this.input.closed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SpeechEvent>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the STT stream */\n close() {\n if (!this.input.closed) this.input.close();\n if (!this.queue.closed) this.queue.close();\n if (!this.output.closed) this.output.close();\n if (!this.abortController.signal.aborted) this.abortController.abort();\n this.closed = true;\n }\n\n [Symbol.asyncIterator](): SpeechStream {\n return this;\n }\n}\n"],"mappings":"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","names":["SpeechEventType","frame"]}
|
|
@@ -223,6 +223,9 @@ class FallbackChunkedStream extends import_tts.ChunkedStream {
|
|
|
223
223
|
this.adapter = adapter;
|
|
224
224
|
this.connOptions = connOptions;
|
|
225
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* @throws {APIConnectionError} When all TTS providers have been exhausted
|
|
228
|
+
*/
|
|
226
229
|
async run() {
|
|
227
230
|
const allTTSFailed = this.adapter.status.every((s) => !s.available);
|
|
228
231
|
let lastRequestId = "";
|
|
@@ -309,6 +312,9 @@ class FallbackSynthesizeStream extends import_tts.SynthesizeStream {
|
|
|
309
312
|
super(adapter, connOptions);
|
|
310
313
|
this.adapter = adapter;
|
|
311
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* @throws {APIConnectionError} When all TTS providers have been exhausted
|
|
317
|
+
*/
|
|
312
318
|
async run() {
|
|
313
319
|
const allTTSFailed = this.adapter.status.every((s) => !s.available);
|
|
314
320
|
if (allTTSFailed) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tts/fallback_adapter.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { AudioResampler } from '@livekit/rtc-node';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport { basic } from '../tokenize/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS } from '../types.js';\nimport { Task, cancelAndWait } from '../utils.js';\nimport { StreamAdapter } from './stream_adapter.js';\nimport { ChunkedStream, SynthesizeStream, TTS, type TTSCapabilities } from './tts.js';\n\n/**\n * Internal status tracking for each TTS instance.\n * @internal\n */\ninterface TTSStatus {\n available: boolean;\n recoveringTask: Task<void> | null;\n}\n\n/**\n * Options for creating a FallbackAdapter.\n */\nexport interface FallbackAdapterOptions {\n /** List of TTS instances to use for fallback (in priority order). At least one is required. */\n ttsInstances: TTS[];\n /** Number of internal retries per TTS instance before moving to the next one. Defaults to 2. */\n maxRetryPerTTS?: number;\n /** Delay in milliseconds before attempting to recover a failed TTS instance. Defaults to 1000. */\n recoveryDelayMs?: number;\n}\n\n/**\n * Event emitted when a TTS instance's availability changes.\n */\nexport interface AvailabilityChangedEvent {\n /** The TTS instance whose availability changed. */\n tts: TTS;\n /** Whether the TTS instance is now available. */\n available: boolean;\n}\n\nconst DEFAULT_FALLBACK_API_CONNECT_OPTIONS: APIConnectOptions = {\n maxRetry: 0,\n timeoutMs: DEFAULT_API_CONNECT_OPTIONS.timeoutMs,\n retryIntervalMs: DEFAULT_API_CONNECT_OPTIONS.retryIntervalMs,\n};\n\nconst FORWARD_POLL_MS = 10;\n\n/**\n * FallbackAdapter is a TTS wrapper that provides automatic failover between multiple TTS providers.\n *\n * When the primary TTS fails, it automatically switches to the next available provider in the list.\n * Failed providers are monitored in the background and restored when they recover.\n *\n * Features:\n * - Automatic failover to backup TTS providers on failure\n * - Background health checks to restore recovered providers\n * - Automatic audio resampling when TTS providers have different sample rates\n * - Support for both streaming and non-streaming TTS providers\n *\n * @example\n * ```typescript\n * import { FallbackAdapter } from '@livekit/agents';\n * import { TTS as OpenAITTS } from '@livekit/agents-plugin-openai';\n * import { TTS as ElevenLabsTTS } from '@livekit/agents-plugin-elevenlabs';\n *\n * const fallbackTTS = new FallbackAdapter({\n * ttsInstances: [\n * new OpenAITTS(), // Primary\n * new ElevenLabsTTS(), // Fallback\n * ],\n * maxRetryPerTTS: 2, // Retry each TTS twice before moving to next\n * recoveryDelayMs: 1000, // Check recovery every 1 second\n * });\n *\n * ```\n */\nexport class FallbackAdapter extends TTS {\n /** The list of TTS instances used for fallback (in priority order). */\n readonly ttsInstances: TTS[];\n /** Number of retries per TTS instance before falling back to the next one. */\n readonly maxRetryPerTTS: number;\n /** Delay in milliseconds before attempting to recover a failed TTS instance. */\n readonly recoveryDelayMs: number;\n\n private _status: TTSStatus[] = [];\n private _logger = log();\n private _recoveryTimeouts: Map<number, NodeJS.Timeout> = new Map();\n\n label: string = `tts.FallbackAdapter`;\n\n constructor(opts: FallbackAdapterOptions) {\n if (!opts.ttsInstances || opts.ttsInstances.length < 1) {\n throw new Error('at least one TTS instance must be provided.');\n }\n const numChannels = opts.ttsInstances[0]!.numChannels;\n const allNumChannelsMatch = opts.ttsInstances.every((tts) => tts.numChannels === numChannels);\n if (!allNumChannelsMatch) {\n throw new Error('All TTS instances should have the same number of channels');\n }\n const sampleRate = Math.max(...opts.ttsInstances.map((t) => t.sampleRate));\n const capabilities = FallbackAdapter.aggregateCapabilities(opts.ttsInstances);\n super(sampleRate, numChannels, capabilities);\n this.ttsInstances = opts.ttsInstances;\n this.maxRetryPerTTS = opts.maxRetryPerTTS ?? 2;\n this.recoveryDelayMs = opts.recoveryDelayMs ?? 1000;\n this._status = opts.ttsInstances.map(() => ({\n available: true,\n recoveringTask: null,\n }));\n this.setupEventForwarding();\n }\n private static aggregateCapabilities(instances: TTS[]): TTSCapabilities {\n const streaming = instances.some((tts) => tts.capabilities.streaming);\n const alignedTranscript = instances.every((tts) => tts.capabilities.alignedTranscript === true);\n return { streaming, alignedTranscript };\n }\n\n private setupEventForwarding(): void {\n this.ttsInstances.forEach((tts) => {\n tts.on('metrics_collected', (metrics) => {\n this.emit('metrics_collected', metrics);\n });\n tts.on('error', (error) => {\n this.emit('error', error);\n });\n });\n }\n\n /**\n * Returns the current status of all TTS instances, including availability and recovery state.\n */\n get status(): TTSStatus[] {\n return this._status;\n }\n\n getStreamingInstance(index: number): TTS {\n const tts = this.ttsInstances[index]!;\n if (tts.capabilities.streaming) {\n return tts;\n }\n // Wrap non-streaming TTS with StreamAdapter\n return new StreamAdapter(tts, new basic.SentenceTokenizer());\n }\n\n /**\n * Creates a new AudioResampler for the given TTS index if needed.\n * Returns null if the TTS sample rate matches the adapter's output rate.\n * Each stream should create its own resampler to avoid concurrency issues.\n * @internal\n */\n createResamplerForTTS(index: number): AudioResampler | null {\n const tts = this.ttsInstances[index]!;\n if (this.sampleRate !== tts.sampleRate) {\n this._logger.debug(\n `resampling ${tts.label} from ${tts.sampleRate}Hz to ${this.sampleRate}Hz`,\n );\n return new AudioResampler(tts.sampleRate, this.sampleRate, tts.numChannels);\n }\n return null;\n }\n\n private emitAvailabilityChanged(tts: TTS, available: boolean): void {\n const event: AvailabilityChangedEvent = { tts, available };\n (this as unknown as { emit: (event: string, data: AvailabilityChangedEvent) => void }).emit(\n 'tts_availability_changed',\n event,\n );\n }\n\n private tryRecovery(index: number): void {\n const status = this._status[index]!;\n const tts = this.ttsInstances[index]!;\n if (status.recoveringTask && !status.recoveringTask.done) {\n return;\n }\n status.recoveringTask = Task.from(async (controller) => {\n try {\n const testStream = tts.synthesize(\n 'Hello world, this is a recovery test.',\n {\n maxRetry: 0,\n timeoutMs: 10000,\n retryIntervalMs: 1000,\n },\n controller.signal,\n );\n let audioReceived = false;\n for await (const _ of testStream) {\n audioReceived = true;\n }\n if (!audioReceived) {\n throw new Error('Recovery test completed but no audio was received');\n }\n\n status.available = true;\n status.recoveringTask = null;\n this._logger.info({ tts: tts.label }, 'TTS recovered');\n this.emitAvailabilityChanged(tts, true);\n } catch (error) {\n status.recoveringTask = null;\n // Don't schedule retry if we're shutting down\n if (controller.signal.aborted) {\n return;\n }\n this._logger.debug({ tts: tts.label, error }, 'TTS recovery failed, will retry');\n // Retry recovery after delay (matches Python's retry behavior)\n const timeoutId = setTimeout(() => {\n this._recoveryTimeouts.delete(index);\n this.tryRecovery(index);\n }, this.recoveryDelayMs);\n this._recoveryTimeouts.set(index, timeoutId);\n }\n });\n }\n\n markUnAvailable(index: number): void {\n const status = this._status[index]!;\n if (status.recoveringTask && !status.recoveringTask.done) {\n return;\n }\n if (status.available) {\n status.available = false;\n this.emitAvailabilityChanged(this.ttsInstances[index]!, false);\n }\n this.tryRecovery(index);\n }\n\n /**\n * Receives text and returns synthesis in the form of a {@link ChunkedStream}\n */\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): ChunkedStream {\n return new FallbackChunkedStream(\n this,\n text,\n connOptions ?? DEFAULT_FALLBACK_API_CONNECT_OPTIONS,\n abortSignal,\n );\n }\n\n /**\n * Returns a {@link SynthesizeStream} that can be used to push text and receive audio data\n *\n * @param options - Optional configuration including connection options\n */\n stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream {\n return new FallbackSynthesizeStream(\n this,\n options?.connOptions ?? DEFAULT_FALLBACK_API_CONNECT_OPTIONS,\n );\n }\n\n /**\n * Close the FallbackAdapter and all underlying TTS instances.\n * This cancels any ongoing recovery tasks and cleans up resources.\n */\n async close(): Promise<void> {\n // clear all recovery timeouts so that it does not cause issue\n this._recoveryTimeouts.forEach((timeoutId) => {\n clearTimeout(timeoutId);\n });\n this._recoveryTimeouts.clear();\n\n // Cancel all recovery tasks\n const recoveryTasks = this._status\n .map((s) => s.recoveringTask)\n .filter((t): t is Task<void> => t !== null);\n\n if (recoveryTasks.length > 0) {\n await cancelAndWait(recoveryTasks, 1000);\n }\n\n // Remove event listeners\n for (const tts of this.ttsInstances) {\n tts.removeAllListeners('metrics_collected');\n tts.removeAllListeners('error');\n }\n\n // Close all TTS instances\n await Promise.all(this.ttsInstances.map((tts) => tts.close()));\n }\n}\n\nclass FallbackChunkedStream extends ChunkedStream {\n private adapter: FallbackAdapter;\n private connOptions: APIConnectOptions;\n private _logger = log();\n\n label: string = 'tts.FallbackChunkedStream';\n\n constructor(\n adapter: FallbackAdapter,\n text: string,\n connOptions: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, adapter, connOptions, abortSignal);\n this.adapter = adapter;\n this.connOptions = connOptions;\n }\n\n protected async run(): Promise<void> {\n const allTTSFailed = this.adapter.status.every((s) => !s.available);\n let lastRequestId: string = '';\n let lastSegmentId: string = '';\n if (allTTSFailed) {\n this._logger.warn('All fallback TTS instances failed, retrying from first...');\n }\n for (let i = 0; i < this.adapter.ttsInstances.length; i++) {\n const tts = this.adapter.ttsInstances[i]!;\n const status = this.adapter.status[i]!;\n if (!status.available && !allTTSFailed) {\n this.adapter.markUnAvailable(i);\n continue;\n }\n try {\n this._logger.debug({ tts: tts.label }, 'attempting TTS synthesis');\n const connOptions: APIConnectOptions = {\n ...this.connOptions,\n maxRetry: this.adapter.maxRetryPerTTS,\n };\n const stream = tts.synthesize(this.inputText, connOptions, this.abortSignal);\n let audioReceived = false;\n const resampler = this.adapter.createResamplerForTTS(i);\n for await (const audio of stream) {\n if (this.abortController.signal.aborted) {\n stream.close();\n return;\n }\n\n if (resampler) {\n for (const frame of resampler.push(audio.frame)) {\n this.queue.put({\n ...audio,\n frame,\n });\n audioReceived = true;\n }\n } else {\n this.queue.put(audio);\n audioReceived = true;\n }\n lastRequestId = audio.requestId;\n lastSegmentId = audio.segmentId;\n }\n\n // Flush any remaining resampled frames\n if (resampler) {\n for (const frame of resampler.flush()) {\n this.queue.put({\n requestId: lastRequestId || '',\n segmentId: lastSegmentId || '',\n frame,\n final: true,\n });\n audioReceived = true;\n }\n }\n\n // Verify audio was actually received - silent failures should trigger fallback\n if (!audioReceived) {\n throw new APIConnectionError({\n message: 'TTS synthesis completed but no audio was received',\n });\n }\n\n this._logger.debug({ tts: tts.label }, 'TTS synthesis succeeded');\n return;\n } catch (error) {\n if (error instanceof APIError || error instanceof APIConnectionError) {\n this._logger.warn({ tts: tts.label, error }, 'TTS failed, switching to next instance');\n this.adapter.markUnAvailable(i);\n } else {\n throw error;\n }\n }\n }\n const labels = this.adapter.ttsInstances.map((t) => t.label).join(', ');\n throw new APIConnectionError({\n message: `all TTS instances failed (${labels})`,\n });\n }\n}\n\nclass FallbackSynthesizeStream extends SynthesizeStream {\n private adapter: FallbackAdapter;\n private tokenBuffer: (\n | string\n | typeof SynthesizeStream.FLUSH_SENTINEL\n | typeof SynthesizeStream.END_OF_STREAM\n )[] = [];\n private audioPushed = false;\n private _logger = log();\n\n label: string = 'tts.FallbackSynthesizeStream';\n\n constructor(adapter: FallbackAdapter, connOptions: APIConnectOptions) {\n super(adapter, connOptions);\n this.adapter = adapter;\n }\n\n protected async run(): Promise<void> {\n const allTTSFailed = this.adapter.status.every((s) => !s.available);\n if (allTTSFailed) {\n this._logger.warn('All fallback TTS instances failed, retrying from first...');\n }\n const readInputLLMStream = (async () => {\n try {\n for await (const input of this.input) {\n if (this.abortController.signal.aborted) break;\n this.tokenBuffer.push(input);\n }\n } catch (error) {\n this._logger.debug({ error }, 'Error reading input LLM stream');\n throw error;\n } finally {\n this.tokenBuffer.push(SynthesizeStream.END_OF_STREAM);\n }\n })();\n\n for (let i = 0; i < this.adapter.ttsInstances.length; i++) {\n const tts = this.adapter.getStreamingInstance(i);\n const originalTts = this.adapter.ttsInstances[i]!;\n const status = this.adapter.status[i]!;\n let lastRequestId: string = '';\n let lastSegmentId: string = '';\n\n if (!status.available && !allTTSFailed) {\n this.adapter.markUnAvailable(i);\n continue;\n }\n\n try {\n this._logger.debug({ tts: originalTts.label }, 'attempting TTS stream');\n\n const connOptions: APIConnectOptions = {\n ...this.connOptions,\n maxRetry: this.adapter.maxRetryPerTTS,\n };\n\n const stream = tts.stream({ connOptions });\n const resampler = this.adapter.createResamplerForTTS(i);\n let bufferIndex = 0;\n let streamOutputCompleted = false;\n const forwardBufferToTTS = async () => {\n while (true) {\n while (bufferIndex < this.tokenBuffer.length) {\n const token = this.tokenBuffer[bufferIndex++]!;\n if (token === SynthesizeStream.FLUSH_SENTINEL) {\n stream.flush();\n } else if (token === SynthesizeStream.END_OF_STREAM) {\n stream.endInput();\n return;\n } else {\n stream.pushText(token);\n }\n }\n await new Promise((resolve) => setTimeout(resolve, FORWARD_POLL_MS));\n if (this.abortController.signal.aborted || streamOutputCompleted) {\n stream.endInput();\n return;\n }\n }\n };\n\n const processOutput = async () => {\n try {\n for await (const audio of stream) {\n if (this.abortController.signal.aborted) {\n stream.close();\n return;\n }\n\n if (audio === SynthesizeStream.END_OF_STREAM) {\n // Don't forward END_OF_STREAM yet — only emit after we verify audio\n // was received. Otherwise a silent failure would signal completion\n // to consumers before fallback can try the next TTS.\n continue;\n }\n\n if (resampler) {\n for (const frame of resampler.push(audio.frame)) {\n this.queue.put({\n ...audio,\n frame,\n });\n this.audioPushed = true;\n }\n } else {\n this.queue.put(audio);\n this.audioPushed = true;\n }\n lastRequestId = audio.requestId;\n lastSegmentId = audio.segmentId;\n }\n\n // Flush resampler\n if (resampler) {\n for (const frame of resampler.flush()) {\n this.queue.put({\n requestId: lastRequestId || '',\n segmentId: lastSegmentId || '',\n frame,\n final: true,\n });\n this.audioPushed = true;\n }\n }\n } finally {\n // processOutput and forwardBufferToTTS run in parallel.\n // forwardBufferToTTS polls tokenBuffer and only exits when it sees END_OF_STREAM.\n // But END_OF_STREAM is only added when the LLM finishes streaming (line 417).\n // If the TTS fails while the LLM is still streaming, forwardBufferToTTS would\n // keep polling indefinitely, blocking fallback to the next TTS.\n // This flag tells it to exit early.\n streamOutputCompleted = true;\n }\n };\n const [outputResult, forwardBufferResult] = await Promise.allSettled([\n processOutput(),\n forwardBufferToTTS().catch((err) => {\n stream.close(); // Close stream so processOutput can exit\n throw err;\n }),\n ]);\n if (outputResult.status === 'rejected') {\n stream.close();\n throw outputResult.reason;\n }\n if (forwardBufferResult.status === 'rejected') {\n stream.close();\n throw forwardBufferResult.reason;\n }\n\n // Verify audio was actually received - if not, the TTS failed silently\n if (!this.audioPushed) {\n throw new APIConnectionError({\n message: 'TTS stream completed but no audio was received',\n });\n }\n\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n this._logger.debug({ tts: originalTts.label }, 'TTS stream succeeded');\n await readInputLLMStream.catch(() => {});\n return;\n } catch (error) {\n if (this.audioPushed) {\n this._logger.error(\n { tts: originalTts.label },\n 'TTS failed after audio pushed, cannot fallback mid-utterance',\n );\n throw error;\n }\n\n if (error instanceof APIError || error instanceof APIConnectionError) {\n this._logger.warn(\n { tts: originalTts.label, error },\n 'TTS failed, switching to next instance',\n );\n this.adapter.markUnAvailable(i);\n } else {\n throw error;\n }\n }\n }\n await readInputLLMStream.catch(() => {});\n const labels = this.adapter.ttsInstances.map((t) => t.label).join(', ');\n throw new APIConnectionError({\n message: `all TTS instances failed (${labels})`,\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/tts/fallback_adapter.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { AudioResampler } from '@livekit/rtc-node';\nimport type { Throws } from '@livekit/throws-transformer/throws';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport { basic } from '../tokenize/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS } from '../types.js';\nimport { Task, cancelAndWait } from '../utils.js';\nimport { StreamAdapter } from './stream_adapter.js';\nimport { ChunkedStream, SynthesizeStream, TTS, type TTSCapabilities } from './tts.js';\n\n/**\n * Internal status tracking for each TTS instance.\n * @internal\n */\ninterface TTSStatus {\n available: boolean;\n recoveringTask: Task<void> | null;\n}\n\n/**\n * Options for creating a FallbackAdapter.\n */\nexport interface FallbackAdapterOptions {\n /** List of TTS instances to use for fallback (in priority order). At least one is required. */\n ttsInstances: TTS[];\n /** Number of internal retries per TTS instance before moving to the next one. Defaults to 2. */\n maxRetryPerTTS?: number;\n /** Delay in milliseconds before attempting to recover a failed TTS instance. Defaults to 1000. */\n recoveryDelayMs?: number;\n}\n\n/**\n * Event emitted when a TTS instance's availability changes.\n */\nexport interface AvailabilityChangedEvent {\n /** The TTS instance whose availability changed. */\n tts: TTS;\n /** Whether the TTS instance is now available. */\n available: boolean;\n}\n\nconst DEFAULT_FALLBACK_API_CONNECT_OPTIONS: APIConnectOptions = {\n maxRetry: 0,\n timeoutMs: DEFAULT_API_CONNECT_OPTIONS.timeoutMs,\n retryIntervalMs: DEFAULT_API_CONNECT_OPTIONS.retryIntervalMs,\n};\n\nconst FORWARD_POLL_MS = 10;\n\n/**\n * FallbackAdapter is a TTS wrapper that provides automatic failover between multiple TTS providers.\n *\n * When the primary TTS fails, it automatically switches to the next available provider in the list.\n * Failed providers are monitored in the background and restored when they recover.\n *\n * Features:\n * - Automatic failover to backup TTS providers on failure\n * - Background health checks to restore recovered providers\n * - Automatic audio resampling when TTS providers have different sample rates\n * - Support for both streaming and non-streaming TTS providers\n *\n * @example\n * ```typescript\n * import { FallbackAdapter } from '@livekit/agents';\n * import { TTS as OpenAITTS } from '@livekit/agents-plugin-openai';\n * import { TTS as ElevenLabsTTS } from '@livekit/agents-plugin-elevenlabs';\n *\n * const fallbackTTS = new FallbackAdapter({\n * ttsInstances: [\n * new OpenAITTS(), // Primary\n * new ElevenLabsTTS(), // Fallback\n * ],\n * maxRetryPerTTS: 2, // Retry each TTS twice before moving to next\n * recoveryDelayMs: 1000, // Check recovery every 1 second\n * });\n *\n * ```\n */\nexport class FallbackAdapter extends TTS {\n /** The list of TTS instances used for fallback (in priority order). */\n readonly ttsInstances: TTS[];\n /** Number of retries per TTS instance before falling back to the next one. */\n readonly maxRetryPerTTS: number;\n /** Delay in milliseconds before attempting to recover a failed TTS instance. */\n readonly recoveryDelayMs: number;\n\n private _status: TTSStatus[] = [];\n private _logger = log();\n private _recoveryTimeouts: Map<number, NodeJS.Timeout> = new Map();\n\n label: string = `tts.FallbackAdapter`;\n\n constructor(opts: FallbackAdapterOptions) {\n if (!opts.ttsInstances || opts.ttsInstances.length < 1) {\n throw new Error('at least one TTS instance must be provided.');\n }\n const numChannels = opts.ttsInstances[0]!.numChannels;\n const allNumChannelsMatch = opts.ttsInstances.every((tts) => tts.numChannels === numChannels);\n if (!allNumChannelsMatch) {\n throw new Error('All TTS instances should have the same number of channels');\n }\n const sampleRate = Math.max(...opts.ttsInstances.map((t) => t.sampleRate));\n const capabilities = FallbackAdapter.aggregateCapabilities(opts.ttsInstances);\n super(sampleRate, numChannels, capabilities);\n this.ttsInstances = opts.ttsInstances;\n this.maxRetryPerTTS = opts.maxRetryPerTTS ?? 2;\n this.recoveryDelayMs = opts.recoveryDelayMs ?? 1000;\n this._status = opts.ttsInstances.map(() => ({\n available: true,\n recoveringTask: null,\n }));\n this.setupEventForwarding();\n }\n private static aggregateCapabilities(instances: TTS[]): TTSCapabilities {\n const streaming = instances.some((tts) => tts.capabilities.streaming);\n const alignedTranscript = instances.every((tts) => tts.capabilities.alignedTranscript === true);\n return { streaming, alignedTranscript };\n }\n\n private setupEventForwarding(): void {\n this.ttsInstances.forEach((tts) => {\n tts.on('metrics_collected', (metrics) => {\n this.emit('metrics_collected', metrics);\n });\n tts.on('error', (error) => {\n this.emit('error', error);\n });\n });\n }\n\n /**\n * Returns the current status of all TTS instances, including availability and recovery state.\n */\n get status(): TTSStatus[] {\n return this._status;\n }\n\n getStreamingInstance(index: number): TTS {\n const tts = this.ttsInstances[index]!;\n if (tts.capabilities.streaming) {\n return tts;\n }\n // Wrap non-streaming TTS with StreamAdapter\n return new StreamAdapter(tts, new basic.SentenceTokenizer());\n }\n\n /**\n * Creates a new AudioResampler for the given TTS index if needed.\n * Returns null if the TTS sample rate matches the adapter's output rate.\n * Each stream should create its own resampler to avoid concurrency issues.\n * @internal\n */\n createResamplerForTTS(index: number): AudioResampler | null {\n const tts = this.ttsInstances[index]!;\n if (this.sampleRate !== tts.sampleRate) {\n this._logger.debug(\n `resampling ${tts.label} from ${tts.sampleRate}Hz to ${this.sampleRate}Hz`,\n );\n return new AudioResampler(tts.sampleRate, this.sampleRate, tts.numChannels);\n }\n return null;\n }\n\n private emitAvailabilityChanged(tts: TTS, available: boolean): void {\n const event: AvailabilityChangedEvent = { tts, available };\n (this as unknown as { emit: (event: string, data: AvailabilityChangedEvent) => void }).emit(\n 'tts_availability_changed',\n event,\n );\n }\n\n private tryRecovery(index: number): void {\n const status = this._status[index]!;\n const tts = this.ttsInstances[index]!;\n if (status.recoveringTask && !status.recoveringTask.done) {\n return;\n }\n status.recoveringTask = Task.from(async (controller) => {\n try {\n const testStream = tts.synthesize(\n 'Hello world, this is a recovery test.',\n {\n maxRetry: 0,\n timeoutMs: 10000,\n retryIntervalMs: 1000,\n },\n controller.signal,\n );\n let audioReceived = false;\n for await (const _ of testStream) {\n audioReceived = true;\n }\n if (!audioReceived) {\n throw new Error('Recovery test completed but no audio was received');\n }\n\n status.available = true;\n status.recoveringTask = null;\n this._logger.info({ tts: tts.label }, 'TTS recovered');\n this.emitAvailabilityChanged(tts, true);\n } catch (error) {\n status.recoveringTask = null;\n // Don't schedule retry if we're shutting down\n if (controller.signal.aborted) {\n return;\n }\n this._logger.debug({ tts: tts.label, error }, 'TTS recovery failed, will retry');\n // Retry recovery after delay (matches Python's retry behavior)\n const timeoutId = setTimeout(() => {\n this._recoveryTimeouts.delete(index);\n this.tryRecovery(index);\n }, this.recoveryDelayMs);\n this._recoveryTimeouts.set(index, timeoutId);\n }\n });\n }\n\n markUnAvailable(index: number): void {\n const status = this._status[index]!;\n if (status.recoveringTask && !status.recoveringTask.done) {\n return;\n }\n if (status.available) {\n status.available = false;\n this.emitAvailabilityChanged(this.ttsInstances[index]!, false);\n }\n this.tryRecovery(index);\n }\n\n /**\n * Receives text and returns synthesis in the form of a {@link ChunkedStream}\n */\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): ChunkedStream {\n return new FallbackChunkedStream(\n this,\n text,\n connOptions ?? DEFAULT_FALLBACK_API_CONNECT_OPTIONS,\n abortSignal,\n );\n }\n\n /**\n * Returns a {@link SynthesizeStream} that can be used to push text and receive audio data\n *\n * @param options - Optional configuration including connection options\n */\n stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream {\n return new FallbackSynthesizeStream(\n this,\n options?.connOptions ?? DEFAULT_FALLBACK_API_CONNECT_OPTIONS,\n );\n }\n\n /**\n * Close the FallbackAdapter and all underlying TTS instances.\n * This cancels any ongoing recovery tasks and cleans up resources.\n */\n async close(): Promise<void> {\n // clear all recovery timeouts so that it does not cause issue\n this._recoveryTimeouts.forEach((timeoutId) => {\n clearTimeout(timeoutId);\n });\n this._recoveryTimeouts.clear();\n\n // Cancel all recovery tasks\n const recoveryTasks = this._status\n .map((s) => s.recoveringTask)\n .filter((t): t is Task<void> => t !== null);\n\n if (recoveryTasks.length > 0) {\n await cancelAndWait(recoveryTasks, 1000);\n }\n\n // Remove event listeners\n for (const tts of this.ttsInstances) {\n tts.removeAllListeners('metrics_collected');\n tts.removeAllListeners('error');\n }\n\n // Close all TTS instances\n await Promise.all(this.ttsInstances.map((tts) => tts.close()));\n }\n}\n\nclass FallbackChunkedStream extends ChunkedStream {\n private adapter: FallbackAdapter;\n private connOptions: APIConnectOptions;\n private _logger = log();\n\n label: string = 'tts.FallbackChunkedStream';\n\n constructor(\n adapter: FallbackAdapter,\n text: string,\n connOptions: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, adapter, connOptions, abortSignal);\n this.adapter = adapter;\n this.connOptions = connOptions;\n }\n\n /**\n * @throws {APIConnectionError} When all TTS providers have been exhausted\n */\n protected async run(): Promise<Throws<void, APIConnectionError>> {\n const allTTSFailed = this.adapter.status.every((s) => !s.available);\n let lastRequestId: string = '';\n let lastSegmentId: string = '';\n if (allTTSFailed) {\n this._logger.warn('All fallback TTS instances failed, retrying from first...');\n }\n for (let i = 0; i < this.adapter.ttsInstances.length; i++) {\n const tts = this.adapter.ttsInstances[i]!;\n const status = this.adapter.status[i]!;\n if (!status.available && !allTTSFailed) {\n this.adapter.markUnAvailable(i);\n continue;\n }\n try {\n this._logger.debug({ tts: tts.label }, 'attempting TTS synthesis');\n const connOptions: APIConnectOptions = {\n ...this.connOptions,\n maxRetry: this.adapter.maxRetryPerTTS,\n };\n const stream = tts.synthesize(this.inputText, connOptions, this.abortSignal);\n let audioReceived = false;\n const resampler = this.adapter.createResamplerForTTS(i);\n for await (const audio of stream) {\n if (this.abortController.signal.aborted) {\n stream.close();\n return;\n }\n\n if (resampler) {\n for (const frame of resampler.push(audio.frame)) {\n this.queue.put({\n ...audio,\n frame,\n });\n audioReceived = true;\n }\n } else {\n this.queue.put(audio);\n audioReceived = true;\n }\n lastRequestId = audio.requestId;\n lastSegmentId = audio.segmentId;\n }\n\n // Flush any remaining resampled frames\n if (resampler) {\n for (const frame of resampler.flush()) {\n this.queue.put({\n requestId: lastRequestId || '',\n segmentId: lastSegmentId || '',\n frame,\n final: true,\n });\n audioReceived = true;\n }\n }\n\n // Verify audio was actually received - silent failures should trigger fallback\n if (!audioReceived) {\n throw new APIConnectionError({\n message: 'TTS synthesis completed but no audio was received',\n });\n }\n\n this._logger.debug({ tts: tts.label }, 'TTS synthesis succeeded');\n return;\n } catch (error) {\n if (error instanceof APIError || error instanceof APIConnectionError) {\n this._logger.warn({ tts: tts.label, error }, 'TTS failed, switching to next instance');\n this.adapter.markUnAvailable(i);\n } else {\n throw error;\n }\n }\n }\n const labels = this.adapter.ttsInstances.map((t) => t.label).join(', ');\n throw new APIConnectionError({\n message: `all TTS instances failed (${labels})`,\n });\n }\n}\n\nclass FallbackSynthesizeStream extends SynthesizeStream {\n private adapter: FallbackAdapter;\n private tokenBuffer: (\n | string\n | typeof SynthesizeStream.FLUSH_SENTINEL\n | typeof SynthesizeStream.END_OF_STREAM\n )[] = [];\n private audioPushed = false;\n private _logger = log();\n\n label: string = 'tts.FallbackSynthesizeStream';\n\n constructor(adapter: FallbackAdapter, connOptions: APIConnectOptions) {\n super(adapter, connOptions);\n this.adapter = adapter;\n }\n\n /**\n * @throws {APIConnectionError} When all TTS providers have been exhausted\n */\n protected async run(): Promise<Throws<void, APIConnectionError>> {\n const allTTSFailed = this.adapter.status.every((s) => !s.available);\n if (allTTSFailed) {\n this._logger.warn('All fallback TTS instances failed, retrying from first...');\n }\n const readInputLLMStream = (async () => {\n try {\n for await (const input of this.input) {\n if (this.abortController.signal.aborted) break;\n this.tokenBuffer.push(input);\n }\n } catch (error) {\n this._logger.debug({ error }, 'Error reading input LLM stream');\n throw error;\n } finally {\n this.tokenBuffer.push(SynthesizeStream.END_OF_STREAM);\n }\n })();\n\n for (let i = 0; i < this.adapter.ttsInstances.length; i++) {\n const tts = this.adapter.getStreamingInstance(i);\n const originalTts = this.adapter.ttsInstances[i]!;\n const status = this.adapter.status[i]!;\n let lastRequestId: string = '';\n let lastSegmentId: string = '';\n\n if (!status.available && !allTTSFailed) {\n this.adapter.markUnAvailable(i);\n continue;\n }\n\n try {\n this._logger.debug({ tts: originalTts.label }, 'attempting TTS stream');\n\n const connOptions: APIConnectOptions = {\n ...this.connOptions,\n maxRetry: this.adapter.maxRetryPerTTS,\n };\n\n const stream = tts.stream({ connOptions });\n const resampler = this.adapter.createResamplerForTTS(i);\n let bufferIndex = 0;\n let streamOutputCompleted = false;\n const forwardBufferToTTS = async () => {\n while (true) {\n while (bufferIndex < this.tokenBuffer.length) {\n const token = this.tokenBuffer[bufferIndex++]!;\n if (token === SynthesizeStream.FLUSH_SENTINEL) {\n stream.flush();\n } else if (token === SynthesizeStream.END_OF_STREAM) {\n stream.endInput();\n return;\n } else {\n stream.pushText(token);\n }\n }\n await new Promise((resolve) => setTimeout(resolve, FORWARD_POLL_MS));\n if (this.abortController.signal.aborted || streamOutputCompleted) {\n stream.endInput();\n return;\n }\n }\n };\n\n const processOutput = async () => {\n try {\n for await (const audio of stream) {\n if (this.abortController.signal.aborted) {\n stream.close();\n return;\n }\n\n if (audio === SynthesizeStream.END_OF_STREAM) {\n // Don't forward END_OF_STREAM yet — only emit after we verify audio\n // was received. Otherwise a silent failure would signal completion\n // to consumers before fallback can try the next TTS.\n continue;\n }\n\n if (resampler) {\n for (const frame of resampler.push(audio.frame)) {\n this.queue.put({\n ...audio,\n frame,\n });\n this.audioPushed = true;\n }\n } else {\n this.queue.put(audio);\n this.audioPushed = true;\n }\n lastRequestId = audio.requestId;\n lastSegmentId = audio.segmentId;\n }\n\n // Flush resampler\n if (resampler) {\n for (const frame of resampler.flush()) {\n this.queue.put({\n requestId: lastRequestId || '',\n segmentId: lastSegmentId || '',\n frame,\n final: true,\n });\n this.audioPushed = true;\n }\n }\n } finally {\n // processOutput and forwardBufferToTTS run in parallel.\n // forwardBufferToTTS polls tokenBuffer and only exits when it sees END_OF_STREAM.\n // But END_OF_STREAM is only added when the LLM finishes streaming (line 417).\n // If the TTS fails while the LLM is still streaming, forwardBufferToTTS would\n // keep polling indefinitely, blocking fallback to the next TTS.\n // This flag tells it to exit early.\n streamOutputCompleted = true;\n }\n };\n const [outputResult, forwardBufferResult] = await Promise.allSettled([\n processOutput(),\n forwardBufferToTTS().catch((err) => {\n stream.close(); // Close stream so processOutput can exit\n throw err;\n }),\n ]);\n if (outputResult.status === 'rejected') {\n stream.close();\n throw outputResult.reason;\n }\n if (forwardBufferResult.status === 'rejected') {\n stream.close();\n throw forwardBufferResult.reason;\n }\n\n // Verify audio was actually received - if not, the TTS failed silently\n if (!this.audioPushed) {\n throw new APIConnectionError({\n message: 'TTS stream completed but no audio was received',\n });\n }\n\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n this._logger.debug({ tts: originalTts.label }, 'TTS stream succeeded');\n await readInputLLMStream.catch(() => {});\n return;\n } catch (error) {\n if (this.audioPushed) {\n this._logger.error(\n { tts: originalTts.label },\n 'TTS failed after audio pushed, cannot fallback mid-utterance',\n );\n throw error;\n }\n\n if (error instanceof APIError || error instanceof APIConnectionError) {\n this._logger.warn(\n { tts: originalTts.label, error },\n 'TTS failed, switching to next instance',\n );\n this.adapter.markUnAvailable(i);\n } else {\n throw error;\n }\n }\n }\n await readInputLLMStream.catch(() => {});\n const labels = this.adapter.ttsInstances.map((t) => t.label).join(', ');\n throw new APIConnectionError({\n message: `all TTS instances failed (${labels})`,\n });\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback_adapter.d.ts","sourceRoot":"","sources":["../../src/tts/fallback_adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"fallback_adapter.d.ts","sourceRoot":"","sources":["../../src/tts/fallback_adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAKnD,OAAO,EAAE,KAAK,iBAAiB,EAA+B,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,IAAI,EAAiB,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,GAAG,EAAwB,MAAM,UAAU,CAAC;AAEtF;;;GAGG;AACH,UAAU,SAAS;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB,gGAAgG;IAChG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kGAAkG;IAClG,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,GAAG,EAAE,GAAG,CAAC;IACT,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,eAAgB,SAAQ,GAAG;IACtC,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAEjC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,iBAAiB,CAA0C;IAEnE,KAAK,EAAE,MAAM,CAAyB;gBAE1B,IAAI,EAAE,sBAAsB;IAqBxC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAMpC,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,IAAI,MAAM,IAAI,SAAS,EAAE,CAExB;IAED,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG;IASxC;;;;;OAKG;IACH,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAW3D,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,WAAW;IA8CnB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAYpC;;OAEG;IACH,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW,GACxB,aAAa;IAShB;;;;OAIG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,gBAAgB;IAOvE;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAyB7B"}
|
|
@@ -200,6 +200,9 @@ class FallbackChunkedStream extends ChunkedStream {
|
|
|
200
200
|
this.adapter = adapter;
|
|
201
201
|
this.connOptions = connOptions;
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* @throws {APIConnectionError} When all TTS providers have been exhausted
|
|
205
|
+
*/
|
|
203
206
|
async run() {
|
|
204
207
|
const allTTSFailed = this.adapter.status.every((s) => !s.available);
|
|
205
208
|
let lastRequestId = "";
|
|
@@ -286,6 +289,9 @@ class FallbackSynthesizeStream extends SynthesizeStream {
|
|
|
286
289
|
super(adapter, connOptions);
|
|
287
290
|
this.adapter = adapter;
|
|
288
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* @throws {APIConnectionError} When all TTS providers have been exhausted
|
|
294
|
+
*/
|
|
289
295
|
async run() {
|
|
290
296
|
const allTTSFailed = this.adapter.status.every((s) => !s.available);
|
|
291
297
|
if (allTTSFailed) {
|