@livekit/agents 1.0.44 → 1.0.45
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/ipc/supervised_proc.cjs +1 -1
- package/dist/ipc/supervised_proc.cjs.map +1 -1
- package/dist/ipc/supervised_proc.js +1 -1
- package/dist/ipc/supervised_proc.js.map +1 -1
- package/dist/llm/llm.cjs +1 -1
- package/dist/llm/llm.cjs.map +1 -1
- package/dist/llm/llm.js +1 -1
- package/dist/llm/llm.js.map +1 -1
- package/dist/log.cjs +13 -9
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.cts +1 -1
- package/dist/log.d.ts +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -9
- package/dist/log.js.map +1 -1
- package/dist/stt/stt.cjs +2 -2
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.js +2 -2
- package/dist/stt/stt.js.map +1 -1
- package/dist/tts/fallback_adapter.cjs +466 -0
- package/dist/tts/fallback_adapter.cjs.map +1 -0
- package/dist/tts/fallback_adapter.d.cts +110 -0
- package/dist/tts/fallback_adapter.d.ts +110 -0
- package/dist/tts/fallback_adapter.d.ts.map +1 -0
- package/dist/tts/fallback_adapter.js +442 -0
- package/dist/tts/fallback_adapter.js.map +1 -0
- package/dist/tts/index.cjs +3 -0
- package/dist/tts/index.cjs.map +1 -1
- package/dist/tts/index.d.cts +1 -0
- package/dist/tts/index.d.ts +1 -0
- package/dist/tts/index.d.ts.map +1 -1
- package/dist/tts/index.js +2 -0
- package/dist/tts/index.js.map +1 -1
- package/dist/tts/tts.cjs +2 -2
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.js +2 -2
- package/dist/tts/tts.js.map +1 -1
- package/dist/vad.cjs +11 -10
- package/dist/vad.cjs.map +1 -1
- package/dist/vad.d.cts +5 -3
- package/dist/vad.d.ts +5 -3
- package/dist/vad.d.ts.map +1 -1
- package/dist/vad.js +11 -10
- package/dist/vad.js.map +1 -1
- package/dist/voice/room_io/_input.cjs +6 -3
- package/dist/voice/room_io/_input.cjs.map +1 -1
- package/dist/voice/room_io/_input.d.ts.map +1 -1
- package/dist/voice/room_io/_input.js +6 -3
- package/dist/voice/room_io/_input.js.map +1 -1
- package/package.json +1 -1
- package/src/ipc/supervised_proc.ts +1 -1
- package/src/llm/llm.ts +1 -1
- package/src/log.ts +22 -11
- package/src/stt/stt.ts +2 -2
- package/src/tts/fallback_adapter.ts +579 -0
- package/src/tts/index.ts +1 -0
- package/src/tts/tts.ts +2 -2
- package/src/vad.ts +12 -11
- package/src/voice/room_io/_input.ts +5 -3
|
@@ -0,0 +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":[]}
|
package/dist/tts/index.cjs
CHANGED
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var tts_exports = {};
|
|
20
20
|
__export(tts_exports, {
|
|
21
21
|
ChunkedStream: () => import_tts.ChunkedStream,
|
|
22
|
+
FallbackAdapter: () => import_fallback_adapter.FallbackAdapter,
|
|
22
23
|
StreamAdapter: () => import_stream_adapter.StreamAdapter,
|
|
23
24
|
StreamAdapterWrapper: () => import_stream_adapter.StreamAdapterWrapper,
|
|
24
25
|
SynthesizeStream: () => import_tts.SynthesizeStream,
|
|
@@ -27,9 +28,11 @@ __export(tts_exports, {
|
|
|
27
28
|
module.exports = __toCommonJS(tts_exports);
|
|
28
29
|
var import_tts = require("./tts.cjs");
|
|
29
30
|
var import_stream_adapter = require("./stream_adapter.cjs");
|
|
31
|
+
var import_fallback_adapter = require("./fallback_adapter.cjs");
|
|
30
32
|
// Annotate the CommonJS export names for ESM import in node:
|
|
31
33
|
0 && (module.exports = {
|
|
32
34
|
ChunkedStream,
|
|
35
|
+
FallbackAdapter,
|
|
33
36
|
StreamAdapter,
|
|
34
37
|
StreamAdapterWrapper,
|
|
35
38
|
SynthesizeStream,
|
package/dist/tts/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tts/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport {\n type SynthesizedAudio,\n type TTSCapabilities,\n type TTSCallbacks,\n TTS,\n SynthesizeStream,\n ChunkedStream,\n} from './tts.js';\nexport { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,iBAOO;AACP,4BAAoD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/tts/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport {\n type SynthesizedAudio,\n type TTSCapabilities,\n type TTSCallbacks,\n TTS,\n SynthesizeStream,\n ChunkedStream,\n} from './tts.js';\nexport { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';\nexport { FallbackAdapter, type AvailabilityChangedEvent } from './fallback_adapter.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,iBAOO;AACP,4BAAoD;AACpD,8BAA+D;","names":[]}
|
package/dist/tts/index.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { type SynthesizedAudio, type TTSCapabilities, type TTSCallbacks, TTS, SynthesizeStream, ChunkedStream, } from './tts.js';
|
|
2
2
|
export { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';
|
|
3
|
+
export { FallbackAdapter, type AvailabilityChangedEvent } from './fallback_adapter.js';
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/tts/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { type SynthesizedAudio, type TTSCapabilities, type TTSCallbacks, TTS, SynthesizeStream, ChunkedStream, } from './tts.js';
|
|
2
2
|
export { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';
|
|
3
|
+
export { FallbackAdapter, type AvailabilityChangedEvent } from './fallback_adapter.js';
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/tts/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tts/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,GAAG,EACH,gBAAgB,EAChB,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tts/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,GAAG,EACH,gBAAgB,EAChB,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/tts/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
ChunkedStream
|
|
5
5
|
} from "./tts.js";
|
|
6
6
|
import { StreamAdapter, StreamAdapterWrapper } from "./stream_adapter.js";
|
|
7
|
+
import { FallbackAdapter } from "./fallback_adapter.js";
|
|
7
8
|
export {
|
|
8
9
|
ChunkedStream,
|
|
10
|
+
FallbackAdapter,
|
|
9
11
|
StreamAdapter,
|
|
10
12
|
StreamAdapterWrapper,
|
|
11
13
|
SynthesizeStream,
|
package/dist/tts/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tts/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport {\n type SynthesizedAudio,\n type TTSCapabilities,\n type TTSCallbacks,\n TTS,\n SynthesizeStream,\n ChunkedStream,\n} from './tts.js';\nexport { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';\n"],"mappings":"AAGA;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe,4BAA4B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/tts/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport {\n type SynthesizedAudio,\n type TTSCapabilities,\n type TTSCallbacks,\n TTS,\n SynthesizeStream,\n ChunkedStream,\n} from './tts.js';\nexport { StreamAdapter, StreamAdapterWrapper } from './stream_adapter.js';\nexport { FallbackAdapter, type AvailabilityChangedEvent } from './fallback_adapter.js';\n"],"mappings":"AAGA;AAAA,EAIE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe,4BAA4B;AACpD,SAAS,uBAAsD;","names":[]}
|
package/dist/tts/tts.cjs
CHANGED
|
@@ -120,7 +120,7 @@ class SynthesizeStream {
|
|
|
120
120
|
} else {
|
|
121
121
|
this.logger.warn(
|
|
122
122
|
{ tts: this.#tts.label, attempt: i + 1, error },
|
|
123
|
-
`failed to synthesize speech, retrying in
|
|
123
|
+
`failed to synthesize speech, retrying in ${retryInterval}ms`
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
if (retryInterval > 0) {
|
|
@@ -331,7 +331,7 @@ class ChunkedStream {
|
|
|
331
331
|
} else {
|
|
332
332
|
this.logger.warn(
|
|
333
333
|
{ tts: this.#tts.label, attempt: i + 1, error },
|
|
334
|
-
`failed to generate TTS completion, retrying in ${retryInterval}
|
|
334
|
+
`failed to generate TTS completion, retrying in ${retryInterval}ms`
|
|
335
335
|
);
|
|
336
336
|
}
|
|
337
337
|
if (retryInterval > 0) {
|
package/dist/tts/tts.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tts/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { TTSMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport { AsyncIterableQueue, delay, mergeFrames, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/io.js';\n\n/**\n * SynthesizedAudio is a packet of speech synthesis as returned by the TTS.\n */\nexport interface SynthesizedAudio {\n /** Request ID (one segment could be made up of multiple requests) */\n requestId: string;\n /** Segment ID, each segment is separated by a flush */\n segmentId: string;\n /** Synthesized audio frame */\n frame: AudioFrame;\n /** Current segment of the synthesized audio */\n deltaText?: string;\n /** Whether this is the last frame of the segment (streaming only) */\n final: boolean;\n /**\n * Timed transcripts associated with this audio packet (word-level timestamps).\n */\n timedTranscripts?: TimedString[];\n}\n\n/**\n * Describes the capabilities of the TTS provider.\n *\n * @remarks\n * At present, only `streaming` is supplied to this interface, and the framework only supports\n * providers that do have a streaming endpoint.\n */\nexport interface TTSCapabilities {\n streaming: boolean;\n // Whether this TTS supports aligned transcripts (word-level timestamps).\n alignedTranscript?: boolean;\n}\n\nexport interface TTSError {\n type: 'tts_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type TTSCallbacks = {\n ['metrics_collected']: (metrics: TTSMetrics) => void;\n ['error']: (error: TTSError) => void;\n};\n\n/**\n * An instance of a text-to-speech 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 TTS class, which inherits this class's methods.\n */\nexport abstract class TTS extends (EventEmitter as new () => TypedEmitter<TTSCallbacks>) {\n #capabilities: TTSCapabilities;\n #sampleRate: number;\n #numChannels: number;\n abstract label: string;\n\n constructor(sampleRate: number, numChannels: number, capabilities: TTSCapabilities) {\n super();\n this.#capabilities = capabilities;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n }\n\n /** Returns this TTS's capabilities */\n get capabilities(): TTSCapabilities {\n return this.#capabilities;\n }\n\n /** Returns the sample rate of audio frames returned by this TTS */\n get sampleRate(): number {\n return this.#sampleRate;\n }\n\n /** Returns the channel count of audio frames returned by this TTS */\n get numChannels(): number {\n return this.#numChannels;\n }\n\n /**\n * Receives text and returns synthesis in the form of a {@link ChunkedStream}\n */\n abstract synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): ChunkedStream;\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 abstract stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a text-to-speech stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 SynthesizeStream class, which inherits this class's methods.\n */\nexport abstract class SynthesizeStream\n implements AsyncIterableIterator<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>\n{\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n static readonly END_OF_STREAM = Symbol('END_OF_STREAM');\n protected input = new AsyncIterableQueue<string | typeof SynthesizeStream.FLUSH_SENTINEL>();\n protected queue = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected output = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected closed = false;\n protected connOptions: APIConnectOptions;\n protected abortController = new AbortController();\n\n private deferredInputStream: DeferredReadableStream<\n string | typeof SynthesizeStream.FLUSH_SENTINEL\n >;\n private logger = log();\n\n abstract label: string;\n\n #tts: TTS;\n #metricsPendingTexts: string[] = [];\n #metricsText = '';\n #monitorMetricsTask?: Promise<void>;\n #ttsRequestSpan?: Span;\n\n constructor(tts: TTS, connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS) {\n this.#tts = tts;\n this.connOptions = connOptions;\n this.deferredInputStream = new DeferredReadableStream();\n this.pumpInput();\n\n this.abortController.signal.addEventListener('abort', () => {\n this.deferredInputStream.detachSource();\n // TODO (AJS-36) clean this up when we refactor with streams\n if (!this.input.closed) this.input.close();\n if (!this.output.closed) this.output.close();\n this.closed = true;\n });\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 _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: true,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this.connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to synthesize speech, retrying in ${retryInterval}s`,\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 mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n // NOTE(AJS-37): The implementation below uses an AsyncIterableQueue (`this.input`)\n // bridged from a DeferredReadableStream (`this.deferredInputStream`) rather than\n // consuming the stream directly.\n //\n // A full refactor to native Web Streams was considered but is currently deferred.\n // The primary reason is to maintain architectural parity with the Python SDK,\n // which is a key design goal for the project. This ensures a consistent developer\n // experience across both platforms.\n //\n // For more context, see the discussion in GitHub issue # 844.\n protected async pumpInput() {\n const reader = this.deferredInputStream.stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done || value === SynthesizeStream.FLUSH_SENTINEL) {\n break;\n }\n this.pushText(value);\n }\n this.endInput();\n } catch (error) {\n this.logger.error(error, 'Error reading deferred input stream');\n } finally {\n reader.releaseLock();\n // Ensure output is closed when the stream ends\n if (!this.#monitorMetricsTask) {\n // No text was received, close the output directly\n this.output.close();\n }\n }\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n const emit = () => {\n if (this.#metricsPendingTexts.length) {\n const text = this.#metricsPendingTexts.shift()!;\n const duration = process.hrtime.bigint() - startTime;\n const roundedAudioDurationMs = Math.round(audioDurationMs);\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: text.length,\n audioDurationMs: roundedAudioDurationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#tts.label,\n streamed: false,\n };\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n }\n this.#tts.emit('metrics_collected', metrics);\n }\n };\n\n for await (const audio of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(audio);\n if (audio === SynthesizeStream.END_OF_STREAM) continue;\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n if (audio.final) {\n emit();\n }\n }\n\n if (requestId) {\n emit();\n }\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n }\n\n protected abstract run(): Promise<void>;\n\n updateInputStream(text: ReadableStream<string>) {\n this.deferredInputStream.setSource(text);\n }\n\n /** Push a string of text to the TTS */\n /** @deprecated Use `updateInputStream` instead */\n pushText(text: string) {\n if (!this.#monitorMetricsTask) {\n this.#monitorMetricsTask = this.monitorMetrics();\n // Close output when metrics task completes\n this.#monitorMetricsTask.finally(() => this.output.close());\n }\n this.#metricsText += text;\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(text);\n }\n\n /** Flush the TTS, causing it to process all pending text */\n flush() {\n if (this.#metricsText) {\n this.#metricsPendingTexts.push(this.#metricsText);\n this.#metricsText = '';\n }\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(SynthesizeStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n this.flush();\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>> {\n return this.output.next();\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n /** Close both the input and output of the TTS stream */\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): SynthesizeStream {\n return this;\n }\n}\n\n/**\n * An instance of a text-to-speech response, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 ChunkedStream class, which inherits this class's methods.\n */\nexport abstract class ChunkedStream implements AsyncIterableIterator<SynthesizedAudio> {\n protected queue = new AsyncIterableQueue<SynthesizedAudio>();\n protected output = new AsyncIterableQueue<SynthesizedAudio>();\n protected closed = false;\n abstract label: string;\n #text: string;\n #tts: TTS;\n #ttsRequestSpan?: Span;\n private _connOptions: APIConnectOptions;\n private logger = log();\n\n protected abortController = new AbortController();\n\n constructor(\n text: string,\n tts: TTS,\n connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n abortSignal?: AbortSignal,\n ) {\n this.#text = text;\n this.#tts = tts;\n this._connOptions = connOptions;\n\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => this.abortController.abort(), { once: true });\n }\n\n this.monitorMetrics();\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 Promise.resolve().then(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: false,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to generate TTS completion, retrying in ${retryInterval}s`,\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 async mainTask() {\n return tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n protected abstract run(): Promise<void>;\n\n get inputText(): string {\n return this.#text;\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n for await (const audio of this.queue) {\n this.output.put(audio);\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: this.#text.length,\n audioDurationMs: Math.round(audioDurationMs),\n cancelled: false, // TODO(AJS-186): support ChunkedStream with 1.0 - add this.abortController.signal.aborted here\n label: this.#tts.label,\n streamed: false,\n };\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n\n this.#tts.emit('metrics_collected', metrics);\n }\n\n /** Collect every frame into one in a single call */\n async collect(): Promise<AudioFrame> {\n const frames = [];\n for await (const event of this) {\n frames.push(event.frame);\n }\n return mergeFrames(frames);\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the TTS stream */\n 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](): ChunkedStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/tts/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { TTSMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport { AsyncIterableQueue, delay, mergeFrames, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/io.js';\n\n/**\n * SynthesizedAudio is a packet of speech synthesis as returned by the TTS.\n */\nexport interface SynthesizedAudio {\n /** Request ID (one segment could be made up of multiple requests) */\n requestId: string;\n /** Segment ID, each segment is separated by a flush */\n segmentId: string;\n /** Synthesized audio frame */\n frame: AudioFrame;\n /** Current segment of the synthesized audio */\n deltaText?: string;\n /** Whether this is the last frame of the segment (streaming only) */\n final: boolean;\n /**\n * Timed transcripts associated with this audio packet (word-level timestamps).\n */\n timedTranscripts?: TimedString[];\n}\n\n/**\n * Describes the capabilities of the TTS provider.\n *\n * @remarks\n * At present, only `streaming` is supplied to this interface, and the framework only supports\n * providers that do have a streaming endpoint.\n */\nexport interface TTSCapabilities {\n streaming: boolean;\n // Whether this TTS supports aligned transcripts (word-level timestamps).\n alignedTranscript?: boolean;\n}\n\nexport interface TTSError {\n type: 'tts_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type TTSCallbacks = {\n ['metrics_collected']: (metrics: TTSMetrics) => void;\n ['error']: (error: TTSError) => void;\n};\n\n/**\n * An instance of a text-to-speech 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 TTS class, which inherits this class's methods.\n */\nexport abstract class TTS extends (EventEmitter as new () => TypedEmitter<TTSCallbacks>) {\n #capabilities: TTSCapabilities;\n #sampleRate: number;\n #numChannels: number;\n abstract label: string;\n\n constructor(sampleRate: number, numChannels: number, capabilities: TTSCapabilities) {\n super();\n this.#capabilities = capabilities;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n }\n\n /** Returns this TTS's capabilities */\n get capabilities(): TTSCapabilities {\n return this.#capabilities;\n }\n\n /** Returns the sample rate of audio frames returned by this TTS */\n get sampleRate(): number {\n return this.#sampleRate;\n }\n\n /** Returns the channel count of audio frames returned by this TTS */\n get numChannels(): number {\n return this.#numChannels;\n }\n\n /**\n * Receives text and returns synthesis in the form of a {@link ChunkedStream}\n */\n abstract synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): ChunkedStream;\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 abstract stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a text-to-speech stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 SynthesizeStream class, which inherits this class's methods.\n */\nexport abstract class SynthesizeStream\n implements AsyncIterableIterator<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>\n{\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n static readonly END_OF_STREAM = Symbol('END_OF_STREAM');\n protected input = new AsyncIterableQueue<string | typeof SynthesizeStream.FLUSH_SENTINEL>();\n protected queue = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected output = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected closed = false;\n protected connOptions: APIConnectOptions;\n protected abortController = new AbortController();\n\n private deferredInputStream: DeferredReadableStream<\n string | typeof SynthesizeStream.FLUSH_SENTINEL\n >;\n private logger = log();\n\n abstract label: string;\n\n #tts: TTS;\n #metricsPendingTexts: string[] = [];\n #metricsText = '';\n #monitorMetricsTask?: Promise<void>;\n #ttsRequestSpan?: Span;\n\n constructor(tts: TTS, connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS) {\n this.#tts = tts;\n this.connOptions = connOptions;\n this.deferredInputStream = new DeferredReadableStream();\n this.pumpInput();\n\n this.abortController.signal.addEventListener('abort', () => {\n this.deferredInputStream.detachSource();\n // TODO (AJS-36) clean this up when we refactor with streams\n if (!this.input.closed) this.input.close();\n if (!this.output.closed) this.output.close();\n this.closed = true;\n });\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 _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: true,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this.connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to synthesize 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 mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n // NOTE(AJS-37): The implementation below uses an AsyncIterableQueue (`this.input`)\n // bridged from a DeferredReadableStream (`this.deferredInputStream`) rather than\n // consuming the stream directly.\n //\n // A full refactor to native Web Streams was considered but is currently deferred.\n // The primary reason is to maintain architectural parity with the Python SDK,\n // which is a key design goal for the project. This ensures a consistent developer\n // experience across both platforms.\n //\n // For more context, see the discussion in GitHub issue # 844.\n protected async pumpInput() {\n const reader = this.deferredInputStream.stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done || value === SynthesizeStream.FLUSH_SENTINEL) {\n break;\n }\n this.pushText(value);\n }\n this.endInput();\n } catch (error) {\n this.logger.error(error, 'Error reading deferred input stream');\n } finally {\n reader.releaseLock();\n // Ensure output is closed when the stream ends\n if (!this.#monitorMetricsTask) {\n // No text was received, close the output directly\n this.output.close();\n }\n }\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n const emit = () => {\n if (this.#metricsPendingTexts.length) {\n const text = this.#metricsPendingTexts.shift()!;\n const duration = process.hrtime.bigint() - startTime;\n const roundedAudioDurationMs = Math.round(audioDurationMs);\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: text.length,\n audioDurationMs: roundedAudioDurationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#tts.label,\n streamed: false,\n };\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n }\n this.#tts.emit('metrics_collected', metrics);\n }\n };\n\n for await (const audio of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(audio);\n if (audio === SynthesizeStream.END_OF_STREAM) continue;\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n if (audio.final) {\n emit();\n }\n }\n\n if (requestId) {\n emit();\n }\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n }\n\n protected abstract run(): Promise<void>;\n\n updateInputStream(text: ReadableStream<string>) {\n this.deferredInputStream.setSource(text);\n }\n\n /** Push a string of text to the TTS */\n /** @deprecated Use `updateInputStream` instead */\n pushText(text: string) {\n if (!this.#monitorMetricsTask) {\n this.#monitorMetricsTask = this.monitorMetrics();\n // Close output when metrics task completes\n this.#monitorMetricsTask.finally(() => this.output.close());\n }\n this.#metricsText += text;\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(text);\n }\n\n /** Flush the TTS, causing it to process all pending text */\n flush() {\n if (this.#metricsText) {\n this.#metricsPendingTexts.push(this.#metricsText);\n this.#metricsText = '';\n }\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(SynthesizeStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n this.flush();\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>> {\n return this.output.next();\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n /** Close both the input and output of the TTS stream */\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): SynthesizeStream {\n return this;\n }\n}\n\n/**\n * An instance of a text-to-speech response, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 ChunkedStream class, which inherits this class's methods.\n */\nexport abstract class ChunkedStream implements AsyncIterableIterator<SynthesizedAudio> {\n protected queue = new AsyncIterableQueue<SynthesizedAudio>();\n protected output = new AsyncIterableQueue<SynthesizedAudio>();\n protected closed = false;\n abstract label: string;\n #text: string;\n #tts: TTS;\n #ttsRequestSpan?: Span;\n private _connOptions: APIConnectOptions;\n private logger = log();\n\n protected abortController = new AbortController();\n\n constructor(\n text: string,\n tts: TTS,\n connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n abortSignal?: AbortSignal,\n ) {\n this.#text = text;\n this.#tts = tts;\n this._connOptions = connOptions;\n\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => this.abortController.abort(), { once: true });\n }\n\n this.monitorMetrics();\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 Promise.resolve().then(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: false,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to generate TTS completion, 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 async mainTask() {\n return tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n protected abstract run(): Promise<void>;\n\n get inputText(): string {\n return this.#text;\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n for await (const audio of this.queue) {\n this.output.put(audio);\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: this.#text.length,\n audioDurationMs: Math.round(audioDurationMs),\n cancelled: false, // TODO(AJS-186): support ChunkedStream with 1.0 - add this.abortController.signal.aborted here\n label: this.#tts.label,\n streamed: false,\n };\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n\n this.#tts.emit('metrics_collected', metrics);\n }\n\n /** Collect every frame into one in a single call */\n async collect(): Promise<AudioFrame> {\n const frames = [];\n for await (const event of this) {\n frames.push(event.frame);\n }\n return mergeFrames(frames);\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the TTS stream */\n 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](): ChunkedStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/tts/tts.js
CHANGED
|
@@ -95,7 +95,7 @@ class SynthesizeStream {
|
|
|
95
95
|
} else {
|
|
96
96
|
this.logger.warn(
|
|
97
97
|
{ tts: this.#tts.label, attempt: i + 1, error },
|
|
98
|
-
`failed to synthesize speech, retrying in
|
|
98
|
+
`failed to synthesize speech, retrying in ${retryInterval}ms`
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
101
|
if (retryInterval > 0) {
|
|
@@ -306,7 +306,7 @@ class ChunkedStream {
|
|
|
306
306
|
} else {
|
|
307
307
|
this.logger.warn(
|
|
308
308
|
{ tts: this.#tts.label, attempt: i + 1, error },
|
|
309
|
-
`failed to generate TTS completion, retrying in ${retryInterval}
|
|
309
|
+
`failed to generate TTS completion, retrying in ${retryInterval}ms`
|
|
310
310
|
);
|
|
311
311
|
}
|
|
312
312
|
if (retryInterval > 0) {
|
package/dist/tts/tts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tts/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { TTSMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport { AsyncIterableQueue, delay, mergeFrames, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/io.js';\n\n/**\n * SynthesizedAudio is a packet of speech synthesis as returned by the TTS.\n */\nexport interface SynthesizedAudio {\n /** Request ID (one segment could be made up of multiple requests) */\n requestId: string;\n /** Segment ID, each segment is separated by a flush */\n segmentId: string;\n /** Synthesized audio frame */\n frame: AudioFrame;\n /** Current segment of the synthesized audio */\n deltaText?: string;\n /** Whether this is the last frame of the segment (streaming only) */\n final: boolean;\n /**\n * Timed transcripts associated with this audio packet (word-level timestamps).\n */\n timedTranscripts?: TimedString[];\n}\n\n/**\n * Describes the capabilities of the TTS provider.\n *\n * @remarks\n * At present, only `streaming` is supplied to this interface, and the framework only supports\n * providers that do have a streaming endpoint.\n */\nexport interface TTSCapabilities {\n streaming: boolean;\n // Whether this TTS supports aligned transcripts (word-level timestamps).\n alignedTranscript?: boolean;\n}\n\nexport interface TTSError {\n type: 'tts_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type TTSCallbacks = {\n ['metrics_collected']: (metrics: TTSMetrics) => void;\n ['error']: (error: TTSError) => void;\n};\n\n/**\n * An instance of a text-to-speech 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 TTS class, which inherits this class's methods.\n */\nexport abstract class TTS extends (EventEmitter as new () => TypedEmitter<TTSCallbacks>) {\n #capabilities: TTSCapabilities;\n #sampleRate: number;\n #numChannels: number;\n abstract label: string;\n\n constructor(sampleRate: number, numChannels: number, capabilities: TTSCapabilities) {\n super();\n this.#capabilities = capabilities;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n }\n\n /** Returns this TTS's capabilities */\n get capabilities(): TTSCapabilities {\n return this.#capabilities;\n }\n\n /** Returns the sample rate of audio frames returned by this TTS */\n get sampleRate(): number {\n return this.#sampleRate;\n }\n\n /** Returns the channel count of audio frames returned by this TTS */\n get numChannels(): number {\n return this.#numChannels;\n }\n\n /**\n * Receives text and returns synthesis in the form of a {@link ChunkedStream}\n */\n abstract synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): ChunkedStream;\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 abstract stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a text-to-speech stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 SynthesizeStream class, which inherits this class's methods.\n */\nexport abstract class SynthesizeStream\n implements AsyncIterableIterator<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>\n{\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n static readonly END_OF_STREAM = Symbol('END_OF_STREAM');\n protected input = new AsyncIterableQueue<string | typeof SynthesizeStream.FLUSH_SENTINEL>();\n protected queue = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected output = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected closed = false;\n protected connOptions: APIConnectOptions;\n protected abortController = new AbortController();\n\n private deferredInputStream: DeferredReadableStream<\n string | typeof SynthesizeStream.FLUSH_SENTINEL\n >;\n private logger = log();\n\n abstract label: string;\n\n #tts: TTS;\n #metricsPendingTexts: string[] = [];\n #metricsText = '';\n #monitorMetricsTask?: Promise<void>;\n #ttsRequestSpan?: Span;\n\n constructor(tts: TTS, connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS) {\n this.#tts = tts;\n this.connOptions = connOptions;\n this.deferredInputStream = new DeferredReadableStream();\n this.pumpInput();\n\n this.abortController.signal.addEventListener('abort', () => {\n this.deferredInputStream.detachSource();\n // TODO (AJS-36) clean this up when we refactor with streams\n if (!this.input.closed) this.input.close();\n if (!this.output.closed) this.output.close();\n this.closed = true;\n });\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 _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: true,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this.connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to synthesize speech, retrying in ${retryInterval}s`,\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 mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n // NOTE(AJS-37): The implementation below uses an AsyncIterableQueue (`this.input`)\n // bridged from a DeferredReadableStream (`this.deferredInputStream`) rather than\n // consuming the stream directly.\n //\n // A full refactor to native Web Streams was considered but is currently deferred.\n // The primary reason is to maintain architectural parity with the Python SDK,\n // which is a key design goal for the project. This ensures a consistent developer\n // experience across both platforms.\n //\n // For more context, see the discussion in GitHub issue # 844.\n protected async pumpInput() {\n const reader = this.deferredInputStream.stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done || value === SynthesizeStream.FLUSH_SENTINEL) {\n break;\n }\n this.pushText(value);\n }\n this.endInput();\n } catch (error) {\n this.logger.error(error, 'Error reading deferred input stream');\n } finally {\n reader.releaseLock();\n // Ensure output is closed when the stream ends\n if (!this.#monitorMetricsTask) {\n // No text was received, close the output directly\n this.output.close();\n }\n }\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n const emit = () => {\n if (this.#metricsPendingTexts.length) {\n const text = this.#metricsPendingTexts.shift()!;\n const duration = process.hrtime.bigint() - startTime;\n const roundedAudioDurationMs = Math.round(audioDurationMs);\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: text.length,\n audioDurationMs: roundedAudioDurationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#tts.label,\n streamed: false,\n };\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n }\n this.#tts.emit('metrics_collected', metrics);\n }\n };\n\n for await (const audio of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(audio);\n if (audio === SynthesizeStream.END_OF_STREAM) continue;\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n if (audio.final) {\n emit();\n }\n }\n\n if (requestId) {\n emit();\n }\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n }\n\n protected abstract run(): Promise<void>;\n\n updateInputStream(text: ReadableStream<string>) {\n this.deferredInputStream.setSource(text);\n }\n\n /** Push a string of text to the TTS */\n /** @deprecated Use `updateInputStream` instead */\n pushText(text: string) {\n if (!this.#monitorMetricsTask) {\n this.#monitorMetricsTask = this.monitorMetrics();\n // Close output when metrics task completes\n this.#monitorMetricsTask.finally(() => this.output.close());\n }\n this.#metricsText += text;\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(text);\n }\n\n /** Flush the TTS, causing it to process all pending text */\n flush() {\n if (this.#metricsText) {\n this.#metricsPendingTexts.push(this.#metricsText);\n this.#metricsText = '';\n }\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(SynthesizeStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n this.flush();\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>> {\n return this.output.next();\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n /** Close both the input and output of the TTS stream */\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): SynthesizeStream {\n return this;\n }\n}\n\n/**\n * An instance of a text-to-speech response, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 ChunkedStream class, which inherits this class's methods.\n */\nexport abstract class ChunkedStream implements AsyncIterableIterator<SynthesizedAudio> {\n protected queue = new AsyncIterableQueue<SynthesizedAudio>();\n protected output = new AsyncIterableQueue<SynthesizedAudio>();\n protected closed = false;\n abstract label: string;\n #text: string;\n #tts: TTS;\n #ttsRequestSpan?: Span;\n private _connOptions: APIConnectOptions;\n private logger = log();\n\n protected abortController = new AbortController();\n\n constructor(\n text: string,\n tts: TTS,\n connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n abortSignal?: AbortSignal,\n ) {\n this.#text = text;\n this.#tts = tts;\n this._connOptions = connOptions;\n\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => this.abortController.abort(), { once: true });\n }\n\n this.monitorMetrics();\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 Promise.resolve().then(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: false,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to generate TTS completion, retrying in ${retryInterval}s`,\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 async mainTask() {\n return tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n protected abstract run(): Promise<void>;\n\n get inputText(): string {\n return this.#text;\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n for await (const audio of this.queue) {\n this.output.put(audio);\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: this.#text.length,\n audioDurationMs: Math.round(audioDurationMs),\n cancelled: false, // TODO(AJS-186): support ChunkedStream with 1.0 - add this.abortController.signal.aborted here\n label: this.#tts.label,\n streamed: false,\n };\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n\n this.#tts.emit('metrics_collected', metrics);\n }\n\n /** Collect every frame into one in a single call */\n async collect(): Promise<AudioFrame> {\n const frames = [];\n for await (const event of this) {\n frames.push(event.frame);\n }\n return mergeFrames(frames);\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the TTS stream */\n 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](): ChunkedStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/tts/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport type { Span } from '@opentelemetry/api';\nimport { EventEmitter } from 'node:events';\nimport type { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError } from '../_exceptions.js';\nimport { log } from '../log.js';\nimport type { TTSMetrics } from '../metrics/base.js';\nimport { DeferredReadableStream } from '../stream/deferred_stream.js';\nimport { recordException, traceTypes, tracer } from '../telemetry/index.js';\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, intervalForRetry } from '../types.js';\nimport { AsyncIterableQueue, delay, mergeFrames, startSoon, toError } from '../utils.js';\nimport type { TimedString } from '../voice/io.js';\n\n/**\n * SynthesizedAudio is a packet of speech synthesis as returned by the TTS.\n */\nexport interface SynthesizedAudio {\n /** Request ID (one segment could be made up of multiple requests) */\n requestId: string;\n /** Segment ID, each segment is separated by a flush */\n segmentId: string;\n /** Synthesized audio frame */\n frame: AudioFrame;\n /** Current segment of the synthesized audio */\n deltaText?: string;\n /** Whether this is the last frame of the segment (streaming only) */\n final: boolean;\n /**\n * Timed transcripts associated with this audio packet (word-level timestamps).\n */\n timedTranscripts?: TimedString[];\n}\n\n/**\n * Describes the capabilities of the TTS provider.\n *\n * @remarks\n * At present, only `streaming` is supplied to this interface, and the framework only supports\n * providers that do have a streaming endpoint.\n */\nexport interface TTSCapabilities {\n streaming: boolean;\n // Whether this TTS supports aligned transcripts (word-level timestamps).\n alignedTranscript?: boolean;\n}\n\nexport interface TTSError {\n type: 'tts_error';\n timestamp: number;\n label: string;\n error: Error;\n recoverable: boolean;\n}\n\nexport type TTSCallbacks = {\n ['metrics_collected']: (metrics: TTSMetrics) => void;\n ['error']: (error: TTSError) => void;\n};\n\n/**\n * An instance of a text-to-speech 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 TTS class, which inherits this class's methods.\n */\nexport abstract class TTS extends (EventEmitter as new () => TypedEmitter<TTSCallbacks>) {\n #capabilities: TTSCapabilities;\n #sampleRate: number;\n #numChannels: number;\n abstract label: string;\n\n constructor(sampleRate: number, numChannels: number, capabilities: TTSCapabilities) {\n super();\n this.#capabilities = capabilities;\n this.#sampleRate = sampleRate;\n this.#numChannels = numChannels;\n }\n\n /** Returns this TTS's capabilities */\n get capabilities(): TTSCapabilities {\n return this.#capabilities;\n }\n\n /** Returns the sample rate of audio frames returned by this TTS */\n get sampleRate(): number {\n return this.#sampleRate;\n }\n\n /** Returns the channel count of audio frames returned by this TTS */\n get numChannels(): number {\n return this.#numChannels;\n }\n\n /**\n * Receives text and returns synthesis in the form of a {@link ChunkedStream}\n */\n abstract synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): ChunkedStream;\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 abstract stream(options?: { connOptions?: APIConnectOptions }): SynthesizeStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\n/**\n * An instance of a text-to-speech stream, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 SynthesizeStream class, which inherits this class's methods.\n */\nexport abstract class SynthesizeStream\n implements AsyncIterableIterator<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>\n{\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n static readonly END_OF_STREAM = Symbol('END_OF_STREAM');\n protected input = new AsyncIterableQueue<string | typeof SynthesizeStream.FLUSH_SENTINEL>();\n protected queue = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected output = new AsyncIterableQueue<\n SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM\n >();\n protected closed = false;\n protected connOptions: APIConnectOptions;\n protected abortController = new AbortController();\n\n private deferredInputStream: DeferredReadableStream<\n string | typeof SynthesizeStream.FLUSH_SENTINEL\n >;\n private logger = log();\n\n abstract label: string;\n\n #tts: TTS;\n #metricsPendingTexts: string[] = [];\n #metricsText = '';\n #monitorMetricsTask?: Promise<void>;\n #ttsRequestSpan?: Span;\n\n constructor(tts: TTS, connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS) {\n this.#tts = tts;\n this.connOptions = connOptions;\n this.deferredInputStream = new DeferredReadableStream();\n this.pumpInput();\n\n this.abortController.signal.addEventListener('abort', () => {\n this.deferredInputStream.detachSource();\n // TODO (AJS-36) clean this up when we refactor with streams\n if (!this.input.closed) this.input.close();\n if (!this.output.closed) this.output.close();\n this.closed = true;\n });\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 _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: true,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this.connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to synthesize 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 mainTask = async () =>\n tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n // NOTE(AJS-37): The implementation below uses an AsyncIterableQueue (`this.input`)\n // bridged from a DeferredReadableStream (`this.deferredInputStream`) rather than\n // consuming the stream directly.\n //\n // A full refactor to native Web Streams was considered but is currently deferred.\n // The primary reason is to maintain architectural parity with the Python SDK,\n // which is a key design goal for the project. This ensures a consistent developer\n // experience across both platforms.\n //\n // For more context, see the discussion in GitHub issue # 844.\n protected async pumpInput() {\n const reader = this.deferredInputStream.stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done || value === SynthesizeStream.FLUSH_SENTINEL) {\n break;\n }\n this.pushText(value);\n }\n this.endInput();\n } catch (error) {\n this.logger.error(error, 'Error reading deferred input stream');\n } finally {\n reader.releaseLock();\n // Ensure output is closed when the stream ends\n if (!this.#monitorMetricsTask) {\n // No text was received, close the output directly\n this.output.close();\n }\n }\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n const emit = () => {\n if (this.#metricsPendingTexts.length) {\n const text = this.#metricsPendingTexts.shift()!;\n const duration = process.hrtime.bigint() - startTime;\n const roundedAudioDurationMs = Math.round(audioDurationMs);\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: text.length,\n audioDurationMs: roundedAudioDurationMs,\n cancelled: this.abortController.signal.aborted,\n label: this.#tts.label,\n streamed: false,\n };\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n }\n this.#tts.emit('metrics_collected', metrics);\n }\n };\n\n for await (const audio of this.queue) {\n if (this.abortController.signal.aborted) {\n break;\n }\n this.output.put(audio);\n if (audio === SynthesizeStream.END_OF_STREAM) continue;\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n // TODO(AJS-102): use frame.durationMs once available in rtc-node\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n if (audio.final) {\n emit();\n }\n }\n\n if (requestId) {\n emit();\n }\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n }\n\n protected abstract run(): Promise<void>;\n\n updateInputStream(text: ReadableStream<string>) {\n this.deferredInputStream.setSource(text);\n }\n\n /** Push a string of text to the TTS */\n /** @deprecated Use `updateInputStream` instead */\n pushText(text: string) {\n if (!this.#monitorMetricsTask) {\n this.#monitorMetricsTask = this.monitorMetrics();\n // Close output when metrics task completes\n this.#monitorMetricsTask.finally(() => this.output.close());\n }\n this.#metricsText += text;\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(text);\n }\n\n /** Flush the TTS, causing it to process all pending text */\n flush() {\n if (this.#metricsText) {\n this.#metricsPendingTexts.push(this.#metricsText);\n this.#metricsText = '';\n }\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.put(SynthesizeStream.FLUSH_SENTINEL);\n }\n\n /** Mark the input as ended and forbid additional pushes */\n endInput() {\n this.flush();\n\n if (this.input.closed || this.closed) {\n // Stream was aborted/closed, silently skip\n return;\n }\n\n this.input.close();\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio | typeof SynthesizeStream.END_OF_STREAM>> {\n return this.output.next();\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n /** Close both the input and output of the TTS stream */\n close() {\n this.abortController.abort();\n }\n\n [Symbol.asyncIterator](): SynthesizeStream {\n return this;\n }\n}\n\n/**\n * An instance of a text-to-speech response, as an asynchronous iterable iterator.\n *\n * @example Looping through frames\n * ```ts\n * for await (const event of stream) {\n * await source.captureFrame(event.frame);\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 ChunkedStream class, which inherits this class's methods.\n */\nexport abstract class ChunkedStream implements AsyncIterableIterator<SynthesizedAudio> {\n protected queue = new AsyncIterableQueue<SynthesizedAudio>();\n protected output = new AsyncIterableQueue<SynthesizedAudio>();\n protected closed = false;\n abstract label: string;\n #text: string;\n #tts: TTS;\n #ttsRequestSpan?: Span;\n private _connOptions: APIConnectOptions;\n private logger = log();\n\n protected abortController = new AbortController();\n\n constructor(\n text: string,\n tts: TTS,\n connOptions: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,\n abortSignal?: AbortSignal,\n ) {\n this.#text = text;\n this.#tts = tts;\n this._connOptions = connOptions;\n\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => this.abortController.abort(), { once: true });\n }\n\n this.monitorMetrics();\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 Promise.resolve().then(() => this.mainTask().finally(() => this.queue.close()));\n }\n\n private _mainTaskImpl = async (span: Span) => {\n this.#ttsRequestSpan = span;\n span.setAttributes({\n [traceTypes.ATTR_TTS_STREAMING]: false,\n [traceTypes.ATTR_TTS_LABEL]: this.#tts.label,\n });\n\n for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {\n try {\n return await tracer.startActiveSpan(\n async (attemptSpan) => {\n attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);\n try {\n return await this.run();\n } catch (error) {\n recordException(attemptSpan, toError(error));\n throw error;\n }\n },\n { name: 'tts_request_run' },\n );\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 generate TTS completion 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 { tts: this.#tts.label, attempt: i + 1, error },\n `failed to generate TTS completion, 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 async mainTask() {\n return tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {\n name: 'tts_request',\n endOnExit: false,\n });\n }\n\n private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {\n this.#tts.emit('error', {\n type: 'tts_error',\n timestamp: Date.now(),\n label: this.#tts.label,\n error,\n recoverable,\n });\n }\n\n protected abstract run(): Promise<void>;\n\n get inputText(): string {\n return this.#text;\n }\n\n get abortSignal(): AbortSignal {\n return this.abortController.signal;\n }\n\n protected async monitorMetrics() {\n const startTime = process.hrtime.bigint();\n let audioDurationMs = 0;\n let ttfb: bigint = BigInt(-1);\n let requestId = '';\n\n for await (const audio of this.queue) {\n this.output.put(audio);\n requestId = audio.requestId;\n if (ttfb === BigInt(-1)) {\n ttfb = process.hrtime.bigint() - startTime;\n }\n audioDurationMs += (audio.frame.samplesPerChannel / audio.frame.sampleRate) * 1000;\n }\n this.output.close();\n\n const duration = process.hrtime.bigint() - startTime;\n const metrics: TTSMetrics = {\n type: 'tts_metrics',\n timestamp: Date.now(),\n requestId,\n ttfbMs: ttfb === BigInt(-1) ? -1 : Math.trunc(Number(ttfb / BigInt(1000000))),\n durationMs: Math.trunc(Number(duration / BigInt(1000000))),\n charactersCount: this.#text.length,\n audioDurationMs: Math.round(audioDurationMs),\n cancelled: false, // TODO(AJS-186): support ChunkedStream with 1.0 - add this.abortController.signal.aborted here\n label: this.#tts.label,\n streamed: false,\n };\n\n if (this.#ttsRequestSpan) {\n this.#ttsRequestSpan.setAttribute(traceTypes.ATTR_TTS_METRICS, JSON.stringify(metrics));\n this.#ttsRequestSpan.end();\n this.#ttsRequestSpan = undefined;\n }\n\n this.#tts.emit('metrics_collected', metrics);\n }\n\n /** Collect every frame into one in a single call */\n async collect(): Promise<AudioFrame> {\n const frames = [];\n for await (const event of this) {\n frames.push(event.frame);\n }\n return mergeFrames(frames);\n }\n\n next(): Promise<IteratorResult<SynthesizedAudio>> {\n return this.output.next();\n }\n\n /** Close both the input and output of the TTS stream */\n 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](): ChunkedStream {\n return this;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/vad.cjs
CHANGED
|
@@ -57,13 +57,14 @@ class VADStream {
|
|
|
57
57
|
outputReader;
|
|
58
58
|
closed = false;
|
|
59
59
|
inputClosed = false;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
logger
|
|
60
|
+
vad;
|
|
61
|
+
lastActivityTime = BigInt(0);
|
|
62
|
+
logger;
|
|
63
63
|
deferredInputStream;
|
|
64
64
|
metricsStream;
|
|
65
65
|
constructor(vad) {
|
|
66
|
-
this
|
|
66
|
+
this.logger = (0, import_log.log)();
|
|
67
|
+
this.vad = vad;
|
|
67
68
|
this.deferredInputStream = new import_deferred_stream.DeferredReadableStream();
|
|
68
69
|
this.inputWriter = this.input.writable.getWriter();
|
|
69
70
|
this.inputReader = this.input.readable.getReader();
|
|
@@ -108,16 +109,16 @@ class VADStream {
|
|
|
108
109
|
switch (value.type) {
|
|
109
110
|
case 0 /* START_OF_SPEECH */:
|
|
110
111
|
inferenceCount++;
|
|
111
|
-
if (inferenceCount >= 1e3 / this
|
|
112
|
-
this
|
|
112
|
+
if (inferenceCount >= 1e3 / this.vad.capabilities.updateInterval) {
|
|
113
|
+
this.vad.emit("metrics_collected", {
|
|
113
114
|
type: "vad_metrics",
|
|
114
115
|
timestamp: Date.now(),
|
|
115
116
|
idleTimeMs: Math.trunc(
|
|
116
|
-
Number((process.hrtime.bigint() - this
|
|
117
|
+
Number((process.hrtime.bigint() - this.lastActivityTime) / BigInt(1e6))
|
|
117
118
|
),
|
|
118
119
|
inferenceDurationTotalMs,
|
|
119
120
|
inferenceCount,
|
|
120
|
-
label: this
|
|
121
|
+
label: this.vad.label
|
|
121
122
|
});
|
|
122
123
|
inferenceCount = 0;
|
|
123
124
|
inferenceDurationTotalMs = 0;
|
|
@@ -125,10 +126,10 @@ class VADStream {
|
|
|
125
126
|
break;
|
|
126
127
|
case 1 /* INFERENCE_DONE */:
|
|
127
128
|
inferenceDurationTotalMs += Math.round(value.inferenceDuration);
|
|
128
|
-
this
|
|
129
|
+
this.lastActivityTime = process.hrtime.bigint();
|
|
129
130
|
break;
|
|
130
131
|
case 2 /* END_OF_SPEECH */:
|
|
131
|
-
this
|
|
132
|
+
this.lastActivityTime = process.hrtime.bigint();
|
|
132
133
|
break;
|
|
133
134
|
}
|
|
134
135
|
}
|
package/dist/vad.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vad.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport { EventEmitter } from 'node:events';\nimport type {\n ReadableStream,\n ReadableStreamDefaultReader,\n WritableStreamDefaultWriter,\n} from 'node:stream/web';\nimport { log } from './log.js';\nimport type { VADMetrics } from './metrics/base.js';\nimport { DeferredReadableStream } from './stream/deferred_stream.js';\nimport { IdentityTransform } from './stream/identity_transform.js';\n\nexport enum VADEventType {\n START_OF_SPEECH,\n INFERENCE_DONE,\n END_OF_SPEECH,\n METRICS_COLLECTED,\n}\n\nexport interface VADEvent {\n /** Type of the VAD event (e.g., start of speech, end of speech, inference done). */\n type: VADEventType;\n /**\n * Index of the audio sample where the event occurred, relative to the inference sample rate.\n */\n samplesIndex: number;\n /** Timestamp when the event was fired. */\n timestamp: number;\n /** Duration of the speech segment in seconds. */\n speechDuration: number;\n /** Duration of the silence segment in seconds. */\n silenceDuration: number;\n /**\n * List of audio frames associated with the speech.\n *\n * @remarks\n * - For `start_of_speech` events, this contains the audio chunks that triggered the detection.\n * - For `inference_done` events, this contains the audio chunks that were processed.\n * - For `end_of_speech` events, this contains the complete user speech.\n */\n frames: AudioFrame[];\n /** Probability that speech is present (only for `INFERENCE_DONE` events). */\n probability: number;\n /** Time taken to perform the inference, in seconds (only for `INFERENCE_DONE` events). */\n inferenceDuration: number;\n /** Indicates whether speech was detected in the frames. */\n speaking: boolean;\n /** Threshold used to detect silence. */\n rawAccumulatedSilence: number;\n /** Threshold used to detect speech. */\n rawAccumulatedSpeech: number;\n}\n\nexport interface VADCapabilities {\n /** Duration of each VAD inference window in milliseconds. Used to batch metrics emissions to roughly once per second. */\n updateInterval: number;\n}\n\nexport type VADCallbacks = {\n ['metrics_collected']: (metrics: VADMetrics) => void;\n};\n\nexport abstract class VAD extends (EventEmitter as new () => TypedEmitter<VADCallbacks>) {\n #capabilities: VADCapabilities;\n abstract label: string;\n\n constructor(capabilities: VADCapabilities) {\n super();\n this.#capabilities = capabilities;\n }\n\n get capabilities(): VADCapabilities {\n return this.#capabilities;\n }\n\n /**\n * Returns a {@link VADStream} that can be used to push audio frames and receive VAD events.\n */\n abstract stream(): VADStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\nexport abstract class VADStream implements AsyncIterableIterator<VADEvent> {\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n protected input = new IdentityTransform<AudioFrame | typeof VADStream.FLUSH_SENTINEL>();\n protected output = new IdentityTransform<VADEvent>();\n protected inputWriter: WritableStreamDefaultWriter<AudioFrame | typeof VADStream.FLUSH_SENTINEL>;\n protected inputReader: ReadableStreamDefaultReader<AudioFrame | typeof VADStream.FLUSH_SENTINEL>;\n protected outputWriter: WritableStreamDefaultWriter<VADEvent>;\n protected outputReader: ReadableStreamDefaultReader<VADEvent>;\n protected closed = false;\n protected inputClosed = false;\n\n #vad: VAD;\n #lastActivityTime = BigInt(0);\n private logger = log();\n private deferredInputStream: DeferredReadableStream<AudioFrame>;\n\n private metricsStream: ReadableStream<VADEvent>;\n constructor(vad: VAD) {\n this.#vad = vad;\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n\n this.inputWriter = this.input.writable.getWriter();\n this.inputReader = this.input.readable.getReader();\n this.outputWriter = this.output.writable.getWriter();\n\n const [outputStream, metricsStream] = this.output.readable.tee();\n this.metricsStream = metricsStream;\n this.outputReader = outputStream.getReader();\n\n this.pumpDeferredStream();\n this.monitorMetrics();\n }\n\n /**\n * Reads from the deferred input stream and forwards chunks to the input writer.\n *\n * Note: we can't just do this.deferredInputStream.stream.pipeTo(this.input.writable)\n * because the inputWriter locks the this.input.writable stream. All writes must go through\n * the inputWriter.\n */\n private async pumpDeferredStream() {\n const reader = this.deferredInputStream.stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await this.inputWriter.write(value);\n }\n } catch (e) {\n this.logger.error(`Error pumping deferred stream: ${e}`);\n throw e;\n } finally {\n reader.releaseLock();\n }\n }\n\n protected async monitorMetrics() {\n let inferenceDurationTotalMs = 0;\n let inferenceCount = 0;\n const metricsReader = this.metricsStream.getReader();\n while (true) {\n const { done, value } = await metricsReader.read();\n if (done) {\n break;\n }\n switch (value.type) {\n case VADEventType.START_OF_SPEECH:\n inferenceCount++;\n if (inferenceCount >= 1000 / this.#vad.capabilities.updateInterval) {\n this.#vad.emit('metrics_collected', {\n type: 'vad_metrics',\n timestamp: Date.now(),\n idleTimeMs: Math.trunc(\n Number((process.hrtime.bigint() - this.#lastActivityTime) / BigInt(1000000)),\n ),\n inferenceDurationTotalMs,\n inferenceCount,\n label: this.#vad.label,\n });\n\n inferenceCount = 0;\n inferenceDurationTotalMs = 0;\n }\n break;\n case VADEventType.INFERENCE_DONE:\n inferenceDurationTotalMs += Math.round(value.inferenceDuration);\n this.#lastActivityTime = process.hrtime.bigint();\n break;\n case VADEventType.END_OF_SPEECH:\n this.#lastActivityTime = process.hrtime.bigint();\n break;\n }\n }\n }\n\n /**\n * Safely send a VAD event to the output stream, handling writer release errors during shutdown.\n * @returns true if the event was sent, false if the stream is closing\n * @throws Error if an unexpected error occurs\n */\n protected sendVADEvent(event: VADEvent): boolean {\n if (this.closed) {\n return false;\n }\n\n try {\n this.outputWriter.write(event);\n return true;\n } catch (e) {\n throw e;\n }\n }\n\n updateInputStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputStream() {\n this.deferredInputStream.detachSource();\n }\n\n /** @deprecated Use `updateInputStream` instead */\n pushFrame(frame: AudioFrame) {\n // TODO(AJS-395): remove this method\n if (this.inputClosed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.inputWriter.write(frame);\n }\n\n flush() {\n if (this.inputClosed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.inputWriter.write(VADStream.FLUSH_SENTINEL);\n }\n\n endInput() {\n if (this.inputClosed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.inputClosed = true;\n this.input.writable.close();\n }\n\n async next(): Promise<IteratorResult<VADEvent>> {\n return this.outputReader.read().then(({ done, value }) => {\n if (done) {\n return { done: true, value: undefined };\n }\n return { done: false, value };\n });\n }\n\n close() {\n this.outputWriter.releaseLock();\n this.outputReader.cancel();\n this.output.writable.close();\n this.closed = true;\n }\n\n [Symbol.asyncIterator](): VADStream {\n return this;\n }\n}\n"],"mappings":"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","names":["VADEventType"]}
|
|
1
|
+
{"version":3,"sources":["../src/vad.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';\nimport { EventEmitter } from 'node:events';\nimport type {\n ReadableStream,\n ReadableStreamDefaultReader,\n WritableStreamDefaultWriter,\n} from 'node:stream/web';\nimport { log } from './log.js';\nimport type { VADMetrics } from './metrics/base.js';\nimport { DeferredReadableStream } from './stream/deferred_stream.js';\nimport { IdentityTransform } from './stream/identity_transform.js';\n\nexport enum VADEventType {\n START_OF_SPEECH,\n INFERENCE_DONE,\n END_OF_SPEECH,\n METRICS_COLLECTED,\n}\n\nexport interface VADEvent {\n /** Type of the VAD event (e.g., start of speech, end of speech, inference done). */\n type: VADEventType;\n /**\n * Index of the audio sample where the event occurred, relative to the inference sample rate.\n */\n samplesIndex: number;\n /** Timestamp when the event was fired. */\n timestamp: number;\n /** Duration of the speech segment in seconds. */\n speechDuration: number;\n /** Duration of the silence segment in seconds. */\n silenceDuration: number;\n /**\n * List of audio frames associated with the speech.\n *\n * @remarks\n * - For `start_of_speech` events, this contains the audio chunks that triggered the detection.\n * - For `inference_done` events, this contains the audio chunks that were processed.\n * - For `end_of_speech` events, this contains the complete user speech.\n */\n frames: AudioFrame[];\n /** Probability that speech is present (only for `INFERENCE_DONE` events). */\n probability: number;\n /** Time taken to perform the inference, in seconds (only for `INFERENCE_DONE` events). */\n inferenceDuration: number;\n /** Indicates whether speech was detected in the frames. */\n speaking: boolean;\n /** Threshold used to detect silence. */\n rawAccumulatedSilence: number;\n /** Threshold used to detect speech. */\n rawAccumulatedSpeech: number;\n}\n\nexport interface VADCapabilities {\n /** Duration of each VAD inference window in milliseconds. Used to batch metrics emissions to roughly once per second. */\n updateInterval: number;\n}\n\nexport type VADCallbacks = {\n ['metrics_collected']: (metrics: VADMetrics) => void;\n};\n\nexport abstract class VAD extends (EventEmitter as new () => TypedEmitter<VADCallbacks>) {\n #capabilities: VADCapabilities;\n abstract label: string;\n\n constructor(capabilities: VADCapabilities) {\n super();\n this.#capabilities = capabilities;\n }\n\n get capabilities(): VADCapabilities {\n return this.#capabilities;\n }\n\n /**\n * Returns a {@link VADStream} that can be used to push audio frames and receive VAD events.\n */\n abstract stream(): VADStream;\n\n async close(): Promise<void> {\n return;\n }\n}\n\nexport abstract class VADStream implements AsyncIterableIterator<VADEvent> {\n protected static readonly FLUSH_SENTINEL = Symbol('FLUSH_SENTINEL');\n protected input = new IdentityTransform<AudioFrame | typeof VADStream.FLUSH_SENTINEL>();\n protected output = new IdentityTransform<VADEvent>();\n protected inputWriter: WritableStreamDefaultWriter<AudioFrame | typeof VADStream.FLUSH_SENTINEL>;\n protected inputReader: ReadableStreamDefaultReader<AudioFrame | typeof VADStream.FLUSH_SENTINEL>;\n protected outputWriter: WritableStreamDefaultWriter<VADEvent>;\n protected outputReader: ReadableStreamDefaultReader<VADEvent>;\n protected closed = false;\n protected inputClosed = false;\n\n protected vad: VAD;\n protected lastActivityTime = BigInt(0);\n protected logger;\n protected deferredInputStream: DeferredReadableStream<AudioFrame>;\n\n private metricsStream: ReadableStream<VADEvent>;\n constructor(vad: VAD) {\n this.logger = log();\n this.vad = vad;\n this.deferredInputStream = new DeferredReadableStream<AudioFrame>();\n\n this.inputWriter = this.input.writable.getWriter();\n this.inputReader = this.input.readable.getReader();\n this.outputWriter = this.output.writable.getWriter();\n\n const [outputStream, metricsStream] = this.output.readable.tee();\n this.metricsStream = metricsStream;\n this.outputReader = outputStream.getReader();\n\n this.pumpDeferredStream();\n this.monitorMetrics();\n }\n\n /**\n * Reads from the deferred input stream and forwards chunks to the input writer.\n *\n * Note: we can't just do this.deferredInputStream.stream.pipeTo(this.input.writable)\n * because the inputWriter locks the this.input.writable stream. All writes must go through\n * the inputWriter.\n */\n private async pumpDeferredStream() {\n const reader = this.deferredInputStream.stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await this.inputWriter.write(value);\n }\n } catch (e) {\n this.logger.error(`Error pumping deferred stream: ${e}`);\n throw e;\n } finally {\n reader.releaseLock();\n }\n }\n\n protected async monitorMetrics() {\n let inferenceDurationTotalMs = 0;\n let inferenceCount = 0;\n const metricsReader = this.metricsStream.getReader();\n while (true) {\n const { done, value } = await metricsReader.read();\n if (done) {\n break;\n }\n switch (value.type) {\n case VADEventType.START_OF_SPEECH:\n inferenceCount++;\n if (inferenceCount >= 1000 / this.vad.capabilities.updateInterval) {\n this.vad.emit('metrics_collected', {\n type: 'vad_metrics',\n timestamp: Date.now(),\n idleTimeMs: Math.trunc(\n Number((process.hrtime.bigint() - this.lastActivityTime) / BigInt(1000000)),\n ),\n inferenceDurationTotalMs,\n inferenceCount,\n label: this.vad.label,\n });\n\n inferenceCount = 0;\n inferenceDurationTotalMs = 0;\n }\n break;\n case VADEventType.INFERENCE_DONE:\n inferenceDurationTotalMs += Math.round(value.inferenceDuration);\n this.lastActivityTime = process.hrtime.bigint();\n break;\n case VADEventType.END_OF_SPEECH:\n this.lastActivityTime = process.hrtime.bigint();\n break;\n }\n }\n }\n\n /**\n * Safely send a VAD event to the output stream, handling writer release errors during shutdown.\n * @returns true if the event was sent, false if the stream is closing\n * @throws Error if an unexpected error occurs\n */\n protected sendVADEvent(event: VADEvent): boolean {\n if (this.closed) {\n return false;\n }\n\n try {\n this.outputWriter.write(event);\n return true;\n } catch (e) {\n throw e;\n }\n }\n\n updateInputStream(audioStream: ReadableStream<AudioFrame>) {\n this.deferredInputStream.setSource(audioStream);\n }\n\n detachInputStream() {\n this.deferredInputStream.detachSource();\n }\n\n /** @deprecated Use `updateInputStream` instead */\n pushFrame(frame: AudioFrame) {\n // TODO(AJS-395): remove this method\n if (this.inputClosed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.inputWriter.write(frame);\n }\n\n flush() {\n if (this.inputClosed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.inputWriter.write(VADStream.FLUSH_SENTINEL);\n }\n\n endInput() {\n if (this.inputClosed) {\n throw new Error('Input is closed');\n }\n if (this.closed) {\n throw new Error('Stream is closed');\n }\n this.inputClosed = true;\n this.input.writable.close();\n }\n\n async next(): Promise<IteratorResult<VADEvent>> {\n return this.outputReader.read().then(({ done, value }) => {\n if (done) {\n return { done: true, value: undefined };\n }\n return { done: false, value };\n });\n }\n\n close() {\n this.outputWriter.releaseLock();\n this.outputReader.cancel();\n this.output.writable.close();\n this.closed = true;\n }\n\n [Symbol.asyncIterator](): VADStream {\n return this;\n }\n}\n"],"mappings":"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","names":["VADEventType"]}
|
package/dist/vad.d.cts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AudioFrame } from '@livekit/rtc-node';
|
|
|
3
3
|
import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
|
|
4
4
|
import type { ReadableStream, ReadableStreamDefaultReader, WritableStreamDefaultWriter } from 'node:stream/web';
|
|
5
5
|
import type { VADMetrics } from './metrics/base.js';
|
|
6
|
+
import { DeferredReadableStream } from './stream/deferred_stream.js';
|
|
6
7
|
import { IdentityTransform } from './stream/identity_transform.js';
|
|
7
8
|
export declare enum VADEventType {
|
|
8
9
|
START_OF_SPEECH = 0,
|
|
@@ -63,7 +64,6 @@ export declare abstract class VAD extends VAD_base {
|
|
|
63
64
|
close(): Promise<void>;
|
|
64
65
|
}
|
|
65
66
|
export declare abstract class VADStream implements AsyncIterableIterator<VADEvent> {
|
|
66
|
-
#private;
|
|
67
67
|
protected static readonly FLUSH_SENTINEL: unique symbol;
|
|
68
68
|
protected input: IdentityTransform<AudioFrame | typeof VADStream.FLUSH_SENTINEL>;
|
|
69
69
|
protected output: IdentityTransform<VADEvent>;
|
|
@@ -73,8 +73,10 @@ export declare abstract class VADStream implements AsyncIterableIterator<VADEven
|
|
|
73
73
|
protected outputReader: ReadableStreamDefaultReader<VADEvent>;
|
|
74
74
|
protected closed: boolean;
|
|
75
75
|
protected inputClosed: boolean;
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
protected vad: VAD;
|
|
77
|
+
protected lastActivityTime: bigint;
|
|
78
|
+
protected logger: import("pino").Logger;
|
|
79
|
+
protected deferredInputStream: DeferredReadableStream<AudioFrame>;
|
|
78
80
|
private metricsStream;
|
|
79
81
|
constructor(vad: VAD);
|
|
80
82
|
/**
|