@livekit/agents-plugin-deepgram 1.5.0 → 1.5.1
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/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/stt.cjs +54 -3
- package/dist/stt.cjs.map +1 -1
- package/dist/stt.d.cts +5 -0
- package/dist/stt.d.ts +5 -0
- package/dist/stt.d.ts.map +1 -1
- package/dist/stt.js +54 -3
- package/dist/stt.js.map +1 -1
- package/dist/stt_v2.cjs +53 -3
- package/dist/stt_v2.cjs.map +1 -1
- package/dist/stt_v2.d.cts +1 -0
- package/dist/stt_v2.d.ts +1 -0
- package/dist/stt_v2.d.ts.map +1 -1
- package/dist/stt_v2.js +53 -3
- package/dist/stt_v2.js.map +1 -1
- package/dist/tts.cjs +1 -0
- package/dist/tts.cjs.map +1 -1
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +1 -0
- package/dist/tts.js.map +1 -1
- package/package.json +8 -8
- package/src/stt.ts +62 -2
- package/src/stt_v2.ts +60 -2
- package/src/tts.ts +1 -0
package/dist/stt_v2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stt_v2.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n AudioByteStream,\n Event,\n calculateAudioDurationSeconds,\n createTimedString,\n log,\n normalizeLanguage,\n stt,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport * as queryString from 'node:querystring';\nimport { WebSocket } from 'ws';\nimport { PeriodicCollector } from './_utils.js';\nimport type { V2Models } from './models.js';\n\nconst _CLOSE_MSG = JSON.stringify({ type: 'CloseStream' });\n\n// --- Configuration ---\n\n/**\n * Configuration options for STTv2 (Deepgram Flux model).\n */\nexport interface STTv2Options {\n apiKey?: string;\n model: V2Models | string;\n sampleRate: number;\n keyterms: string[];\n endpointUrl: string;\n language?: string;\n eagerEotThreshold?: number;\n eotThreshold?: number;\n eotTimeoutMs?: number;\n mipOptOut?: boolean;\n tags?: string[];\n /**\n * List of language hints to bias the model for improved accuracy.\n * Only usable with `flux-general-multi`.\n */\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 61 line\n languageHint?: string[];\n}\n\nconst defaultSTTv2Options: Omit<STTv2Options, 'apiKey'> = {\n model: 'flux-general-en',\n sampleRate: 16000,\n keyterms: [],\n endpointUrl: 'wss://api.deepgram.com/v2/listen',\n language: 'en',\n mipOptOut: false,\n};\n\nfunction validateTags(tags: string[]): string[] {\n for (const tag of tags) {\n if (tag.length > 128) {\n throw new Error('tag must be no more than 128 characters');\n }\n }\n return tags;\n}\n\n/**\n * Deepgram STTv2 using the Flux model for streaming speech-to-text.\n *\n * This uses Deepgram's V2 API (`/v2/listen`) which provides turn-based\n * transcription with support for preemptive generation.\n *\n * @remarks\n * Key differences from STT (V1):\n * - Uses `TurnInfo` events instead of `SpeechStarted`/`Results`\n * - Supports `eagerEotThreshold` for preemptive LLM generation\n * - Sends `PREFLIGHT_TRANSCRIPT` events when eager end-of-turn is detected\n *\n * @example\n * ```typescript\n * import { STTv2 } from '@livekit/agents-plugin-deepgram';\n *\n * const stt = new STTv2({\n * model: 'flux-general-en',\n * eagerEotThreshold: 0.5, // Enable preemptive generation\n * });\n *\n * const stream = stt.stream();\n * stream.pushFrame(audioFrame);\n *\n * for await (const event of stream) {\n * if (event.type === SpeechEventType.FINAL_TRANSCRIPT) {\n * console.log(event.alternatives?.[0]?.text);\n * }\n * }\n * ```\n */\nexport class STTv2 extends stt.STT {\n readonly label = 'deepgram.STTv2';\n #opts: STTv2Options;\n #apiKey: string;\n #logger = log();\n\n /**\n * Create a new Deepgram STTv2 instance.\n *\n * @param opts - Configuration options\n * @param opts.apiKey - Deepgram API key (defaults to `DEEPGRAM_API_KEY` env var)\n * @param opts.model - Model to use (default: `flux-general-en`)\n * @param opts.eagerEotThreshold - Threshold (0.3-0.9) for preemptive generation\n * @param opts.eotThreshold - End-of-turn detection threshold (default: 0.7)\n * @param opts.eotTimeoutMs - End-of-turn timeout in ms (default: 3000)\n * @param opts.keyterms - List of key terms to improve recognition\n * @param opts.tags - Tags for usage reporting (max 128 chars each)\n * @param opts.languageHint - List of language hints to bias the model for improved accuracy.\n * Only usable with `flux-general-multi`.\n *\n * @throws Error if no API key is provided\n */\n constructor(opts: Partial<STTv2Options> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: 'word',\n });\n\n this.#opts = {\n ...defaultSTTv2Options,\n ...opts,\n language: opts.language ? normalizeLanguage(opts.language) : defaultSTTv2Options.language,\n };\n\n const apiKey = opts.apiKey || process.env.DEEPGRAM_API_KEY;\n if (!apiKey) {\n throw new Error('Deepgram API key is required');\n }\n this.#apiKey = apiKey;\n\n if (this.#opts.tags) {\n this.#opts.tags = validateTags(this.#opts.tags);\n }\n\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 134-138 lines\n if (\n this.#opts.languageHint &&\n this.#opts.languageHint.length > 0 &&\n this.#opts.model !== 'flux-general-multi'\n ) {\n this.#logger.warn(\n { model: this.#opts.model },\n '`languageHint` is only supported by `flux-general-multi` and will be ignored for this model',\n );\n }\n }\n\n /** The model being used for transcription */\n get model(): string {\n return this.#opts.model;\n }\n\n /** The STT provider name */\n get provider(): string {\n return 'Deepgram';\n }\n\n protected async _recognize(\n _frame: AudioFrame | AudioFrame[],\n _abortSignal?: AbortSignal,\n ): Promise<stt.SpeechEvent> {\n throw new Error('V2 API does not support non-streaming recognize. Use .stream()');\n }\n\n /**\n * Create a new streaming transcription session.\n *\n * @param options - Stream options\n * @returns A SpeechStream that emits transcription events\n */\n stream(options?: { connOptions?: APIConnectOptions }): stt.SpeechStream {\n const streamOpts = { ...this.#opts, apiKey: this.#apiKey };\n return new SpeechStreamv2(this, streamOpts, options?.connOptions);\n }\n\n /**\n * Update STT options. Changes will take effect on the next stream.\n *\n * @param opts - Partial options to update\n */\n updateOptions(opts: Partial<STTv2Options>) {\n this.#opts = {\n ...this.#opts,\n ...opts,\n language:\n opts.language !== undefined ? normalizeLanguage(opts.language) : this.#opts.language,\n };\n if (opts.tags) this.#opts.tags = validateTags(opts.tags);\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 244-249 lines\n if (\n this.#opts.languageHint &&\n this.#opts.languageHint.length > 0 &&\n this.#opts.model !== 'flux-general-multi'\n ) {\n this.#logger.warn(\n { model: this.#opts.model },\n '`languageHint` is only supported by `flux-general-multi` and will be ignored for this model',\n );\n }\n this.#logger.debug('Updated STTv2 options');\n }\n}\n\n// --- Stream Implementation ---\n\nclass SpeechStreamv2 extends stt.SpeechStream {\n readonly label = 'deepgram.SpeechStreamv2';\n #opts: STTv2Options & { apiKey: string };\n #logger = log();\n #ws: WebSocket | null = null;\n\n #audioDurationCollector: PeriodicCollector<number>;\n #requestId = '';\n #speaking = false;\n\n // Parity: _reconnect_event - using existing Event class from @livekit/agents\n #reconnectEvent = new Event();\n\n constructor(\n sttInstance: STTv2,\n opts: STTv2Options & { apiKey: string },\n connOptions?: APIConnectOptions,\n ) {\n super(sttInstance, opts.sampleRate, connOptions);\n this.#opts = opts;\n\n this.#audioDurationCollector = new PeriodicCollector(\n (duration) => this.#onAudioDurationReport(duration),\n { duration: 5.0 },\n );\n }\n\n updateOptions(opts: Partial<STTv2Options>) {\n this.#logger.debug('Stream received option update', opts);\n this.#opts = {\n ...this.#opts,\n ...opts,\n language:\n opts.language !== undefined ? normalizeLanguage(opts.language) : this.#opts.language,\n };\n if (opts.tags) this.#opts.tags = validateTags(opts.tags);\n\n // Trigger reconnection loop\n this.#reconnectEvent.set();\n }\n\n protected async run() {\n // Outer Loop: Handles reconnections (Configuration updates)\n while (!this.closed) {\n try {\n this.#reconnectEvent.clear();\n\n const url = this.#getDeepgramUrl();\n this.#logger.debug(`Connecting to Deepgram: ${url}`);\n\n this.#ws = new WebSocket(url, {\n headers: { Authorization: `Token ${this.#opts.apiKey}` },\n });\n\n // 1. Wait for Connection Open\n await new Promise<void>((resolve, reject) => {\n if (!this.#ws) return reject(new Error('WebSocket not initialized'));\n\n const onOpen = () => {\n this.#ws?.off('error', onError);\n resolve();\n };\n const onError = (err: Error) => {\n this.#ws?.off('open', onOpen);\n reject(err);\n };\n\n this.#ws.once('open', onOpen);\n this.#ws.once('error', onError);\n });\n\n // 2. Run Concurrent Tasks (Send & Receive)\n const sendPromise = this.#sendTask();\n const recvPromise = this.#recvTask();\n const reconnectWait = this.#reconnectEvent.wait();\n\n // 3. Race: Normal Completion vs Reconnect Signal\n const result = await Promise.race([\n Promise.all([sendPromise, recvPromise]),\n reconnectWait.then(() => 'RECONNECT'),\n ]);\n\n if (result === 'RECONNECT') {\n this.#logger.debug('Reconnecting stream due to option update...');\n // Close current socket; loop will restart and open a new one\n this.#ws.close();\n } else {\n // Normal finish (Stream ended or Error thrown)\n break;\n }\n } catch (error) {\n this.#logger.error('Deepgram stream error', { error });\n throw error; // Let Base Class handle retry logic\n } finally {\n if (this.#ws?.readyState === WebSocket.OPEN) {\n this.#ws.close();\n }\n }\n }\n this.close();\n }\n\n async #sendTask() {\n if (!this.#ws) return;\n\n // Buffer audio into 50ms chunks (Parity)\n const samples50ms = Math.floor(this.#opts.sampleRate / 20);\n const audioBstream = new AudioByteStream(this.#opts.sampleRate, 1, samples50ms);\n\n let hasEnded = false;\n\n // Manual Iterator to allow racing against Reconnect Signal\n const iterator = this.input[Symbol.asyncIterator]();\n\n while (true) {\n const nextPromise = iterator.next();\n // If reconnect signal fires, abort the wait\n const abortPromise = this.#reconnectEvent.wait().then(() => ({ abort: true }) as const);\n\n const result = await Promise.race([nextPromise, abortPromise]);\n\n // Check if we need to abort (Reconnect) or if stream is done\n if ('abort' in result || result.done) {\n if (!('abort' in result) && result.done) {\n // Normal stream end\n hasEnded = true;\n } else {\n // Reconnect triggered - break loop immediately\n break;\n }\n }\n\n // If we broke above, we don't process data. If not, 'result' is IteratorResult\n if (hasEnded && result.value === undefined) {\n // Process flush below\n } else if ('value' in result) {\n const data = result.value;\n const frames: AudioFrame[] = [];\n\n if (data === SpeechStreamv2.FLUSH_SENTINEL) {\n frames.push(...audioBstream.flush());\n hasEnded = true;\n } else {\n frames.push(...audioBstream.write((data as AudioFrame).data.buffer as ArrayBuffer));\n }\n\n for (const frame of frames) {\n this.#audioDurationCollector.push(calculateAudioDurationSeconds(frame));\n\n if (this.#ws!.readyState === WebSocket.OPEN) {\n this.#ws!.send(frame.data);\n }\n\n if (hasEnded) {\n this.#audioDurationCollector.flush();\n hasEnded = false;\n }\n }\n }\n\n if (hasEnded) break;\n }\n\n // Only send CloseStream if we are exiting normally (not reconnecting)\n if (!this.#reconnectEvent.isSet && this.#ws!.readyState === WebSocket.OPEN) {\n this.#logger.debug('Sending CloseStream message to Deepgram');\n this.#ws!.send(_CLOSE_MSG);\n }\n }\n\n async #recvTask() {\n if (!this.#ws) return;\n\n return new Promise<void>((resolve) => {\n if (!this.#ws) return resolve();\n\n this.#ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n this.#logger.warn('Received unexpected binary message from Deepgram');\n return;\n }\n try {\n const msg = JSON.parse(data.toString());\n this.#processStreamEvent(msg);\n } catch (error) {\n this.#logger.error('Failed to parse Deepgram message', { error });\n }\n });\n\n this.#ws.on('close', (code, reason) => {\n this.#logger.debug(`Deepgram WebSocket closed: ${code} ${reason}`);\n resolve();\n });\n\n // Errors are caught by run() listener, resolve here to clean up task\n this.#ws.on('error', () => resolve());\n });\n }\n\n #processStreamEvent(data: Record<string, unknown>) {\n if (data.request_id) {\n this.#requestId = data.request_id as string;\n }\n\n if (data.type === 'TurnInfo') {\n const eventType = data.event;\n\n if (eventType === 'StartOfTurn') {\n if (this.#speaking) return;\n\n this.#speaking = true;\n this.queue.put({\n type: stt.SpeechEventType.START_OF_SPEECH,\n requestId: this.#requestId,\n });\n\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, data);\n } else if (eventType === 'Update') {\n if (!this.#speaking) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, data);\n } else if (eventType === 'EagerEndOfTurn') {\n if (!this.#speaking) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, data);\n } else if (eventType === 'TurnResumed') {\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, data);\n } else if (eventType === 'EndOfTurn') {\n if (!this.#speaking) return;\n\n this.#speaking = false;\n this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, data);\n\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n }\n } else if (data.type === 'Error') {\n this.#logger.warn('deepgram sent an error', { data });\n const desc = (data.description as string) || 'unknown error from deepgram';\n throw new Error(`Deepgram API Error: ${desc}`);\n }\n }\n\n #sendTranscriptEvent(eventType: stt.SpeechEventType, data: Record<string, unknown>) {\n const alts = parseTranscription(this.#opts.language || 'en', data, this.startTimeOffset);\n\n if (alts.length > 0) {\n this.queue.put({\n type: eventType,\n requestId: this.#requestId,\n alternatives: [alts[0]!, ...alts.slice(1)],\n });\n }\n }\n\n #onAudioDurationReport(duration: number) {\n const usageEvent: stt.SpeechEvent = {\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: {\n audioDuration: duration,\n },\n };\n this.queue.put(usageEvent);\n }\n\n #getDeepgramUrl(): string {\n const params: Record<string, string | string[]> = {\n model: this.#opts.model,\n sample_rate: this.#opts.sampleRate.toString(),\n encoding: 'linear16',\n mip_opt_out: String(this.#opts.mipOptOut),\n };\n\n // Note: v2 API does NOT include 'language' parameter\n if (this.#opts.eagerEotThreshold)\n params.eager_eot_threshold = this.#opts.eagerEotThreshold.toString();\n if (this.#opts.eotThreshold) params.eot_threshold = this.#opts.eotThreshold.toString();\n if (this.#opts.eotTimeoutMs) params.eot_timeout_ms = this.#opts.eotTimeoutMs.toString();\n\n if (this.#opts.keyterms.length > 0) params.keyterm = this.#opts.keyterms;\n if (this.#opts.tags && this.#opts.tags.length > 0) params.tag = this.#opts.tags;\n\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 480-481 lines\n if (this.#opts.languageHint && this.#opts.languageHint.length > 0) {\n params.language_hint = this.#opts.languageHint;\n }\n\n const baseUrl = this.#opts.endpointUrl.replace(/^http/, 'ws');\n const qs = queryString.stringify(params);\n return `${baseUrl}?${qs}`;\n }\n\n override close() {\n super.close();\n this.#ws?.close();\n }\n}\n\n// --- Helpers ---\n\nfunction parseTranscription(\n language: string,\n data: Record<string, unknown>,\n startTimeOffset: number,\n): stt.SpeechData[] {\n const transcript = data.transcript as string | undefined;\n const wordsData = (data.words as Array<Record<string, unknown>>) || [];\n\n if (!wordsData || wordsData.length === 0) {\n return [];\n }\n\n let confidence = 0;\n if (wordsData.length > 0) {\n const sum = wordsData.reduce((acc: number, w) => acc + ((w.confidence as number) || 0), 0);\n confidence = sum / wordsData.length;\n }\n\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 587-591 lines\n const detectedLanguagesRaw = Array.isArray(data.languages) ? (data.languages as string[]) : [];\n const detectedLanguages = detectedLanguagesRaw.map((lang) => normalizeLanguage(lang));\n const primaryLanguage =\n detectedLanguages.length > 0 ? detectedLanguages[0]! : normalizeLanguage(language);\n\n const sd: stt.SpeechData = {\n language: primaryLanguage,\n startTime: ((data.audio_window_start as number) || 0) + startTimeOffset,\n endTime: ((data.audio_window_end as number) || 0) + startTimeOffset,\n confidence: confidence,\n text: transcript || '',\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 598 line\n sourceLanguages: detectedLanguages.length > 0 ? detectedLanguages : undefined,\n // Note: Deepgram V2 (Flux) API does not provide word-level timing (start/end).\n // Words only contain 'word' and 'confidence' fields, so startTime/endTime will be 0.\n // See: https://developers.deepgram.com/docs/flux/nova-3-migration\n words: wordsData.map((word) =>\n createTimedString({\n text: (word.word as string) ?? '',\n startTime: ((word.start as number) ?? 0) + startTimeOffset,\n endTime: ((word.end as number) ?? 0) + startTimeOffset,\n confidence: (word.confidence as number) ?? 0.0,\n startTimeOffset,\n }),\n ),\n };\n\n return [sd];\n}\n"],"mappings":"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","names":["_a"]}
|
|
1
|
+
{"version":3,"sources":["../src/stt_v2.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n AudioByteStream,\n Event,\n calculateAudioDurationSeconds,\n createTimedString,\n log,\n normalizeLanguage,\n stt,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport * as queryString from 'node:querystring';\nimport { WebSocket } from 'ws';\nimport { PeriodicCollector } from './_utils.js';\nimport type { V2Models } from './models.js';\n\nconst _CLOSE_MSG = JSON.stringify({ type: 'CloseStream' });\n\n// --- Configuration ---\n\n/**\n * Configuration options for STTv2 (Deepgram Flux model).\n */\nexport interface STTv2Options {\n apiKey?: string;\n model: V2Models | string;\n sampleRate: number;\n keyterms: string[];\n endpointUrl: string;\n language?: string;\n eagerEotThreshold?: number;\n eotThreshold?: number;\n eotTimeoutMs?: number;\n mipOptOut?: boolean;\n tags?: string[];\n /**\n * List of language hints to bias the model for improved accuracy.\n * Only usable with `flux-general-multi`.\n */\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 61 line\n languageHint?: string[];\n}\n\nconst defaultSTTv2Options: Omit<STTv2Options, 'apiKey'> = {\n model: 'flux-general-en',\n sampleRate: 16000,\n keyterms: [],\n endpointUrl: 'wss://api.deepgram.com/v2/listen',\n language: 'en',\n mipOptOut: false,\n};\n\nfunction validateTags(tags: string[]): string[] {\n for (const tag of tags) {\n if (tag.length > 128) {\n throw new Error('tag must be no more than 128 characters');\n }\n }\n return tags;\n}\n\n/**\n * Deepgram STTv2 using the Flux model for streaming speech-to-text.\n *\n * This uses Deepgram's V2 API (`/v2/listen`) which provides turn-based\n * transcription with support for preemptive generation.\n *\n * @remarks\n * Key differences from STT (V1):\n * - Uses `TurnInfo` events instead of `SpeechStarted`/`Results`\n * - Supports `eagerEotThreshold` for preemptive LLM generation\n * - Sends `PREFLIGHT_TRANSCRIPT` events when eager end-of-turn is detected\n *\n * @example\n * ```typescript\n * import { STTv2 } from '@livekit/agents-plugin-deepgram';\n *\n * const stt = new STTv2({\n * model: 'flux-general-en',\n * eagerEotThreshold: 0.5, // Enable preemptive generation\n * });\n *\n * const stream = stt.stream();\n * stream.pushFrame(audioFrame);\n *\n * for await (const event of stream) {\n * if (event.type === SpeechEventType.FINAL_TRANSCRIPT) {\n * console.log(event.alternatives?.[0]?.text);\n * }\n * }\n * ```\n */\nexport class STTv2 extends stt.STT {\n readonly label = 'deepgram.STTv2';\n #opts: STTv2Options;\n #apiKey: string;\n #logger = log();\n // session keyterm propagation)\n #streams = new Set<WeakRef<SpeechStreamv2>>();\n #userKeyterms: string[];\n #sessionKeyterms: string[] = [];\n\n /**\n * Create a new Deepgram STTv2 instance.\n *\n * @param opts - Configuration options\n * @param opts.apiKey - Deepgram API key (defaults to `DEEPGRAM_API_KEY` env var)\n * @param opts.model - Model to use (default: `flux-general-en`)\n * @param opts.eagerEotThreshold - Threshold (0.3-0.9) for preemptive generation\n * @param opts.eotThreshold - End-of-turn detection threshold (default: 0.7)\n * @param opts.eotTimeoutMs - End-of-turn timeout in ms (default: 3000)\n * @param opts.keyterms - List of key terms to improve recognition\n * @param opts.tags - Tags for usage reporting (max 128 chars each)\n * @param opts.languageHint - List of language hints to bias the model for improved accuracy.\n * Only usable with `flux-general-multi`.\n *\n * @throws Error if no API key is provided\n */\n constructor(opts: Partial<STTv2Options> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: 'word',\n keyterms: true,\n });\n\n this.#opts = {\n ...defaultSTTv2Options,\n ...opts,\n language: opts.language ? normalizeLanguage(opts.language) : defaultSTTv2Options.language,\n };\n this.#userKeyterms = [...this.#opts.keyterms];\n\n const apiKey = opts.apiKey || process.env.DEEPGRAM_API_KEY;\n if (!apiKey) {\n throw new Error('Deepgram API key is required');\n }\n this.#apiKey = apiKey;\n\n if (this.#opts.tags) {\n this.#opts.tags = validateTags(this.#opts.tags);\n }\n\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 134-138 lines\n if (\n this.#opts.languageHint &&\n this.#opts.languageHint.length > 0 &&\n this.#opts.model !== 'flux-general-multi'\n ) {\n this.#logger.warn(\n { model: this.#opts.model },\n '`languageHint` is only supported by `flux-general-multi` and will be ignored for this model',\n );\n }\n }\n\n /** The model being used for transcription */\n get model(): string {\n return this.#opts.model;\n }\n\n /** The STT provider name */\n get provider(): string {\n return 'Deepgram';\n }\n\n protected async _recognize(\n _frame: AudioFrame | AudioFrame[],\n _abortSignal?: AbortSignal,\n ): Promise<stt.SpeechEvent> {\n throw new Error('V2 API does not support non-streaming recognize. Use .stream()');\n }\n\n /**\n * Create a new streaming transcription session.\n *\n * @param options - Stream options\n * @returns A SpeechStream that emits transcription events\n */\n stream(options?: { connOptions?: APIConnectOptions }): stt.SpeechStream {\n const streamOpts = { ...this.#opts, apiKey: this.#apiKey };\n const stream = new SpeechStreamv2(this, streamOpts, options?.connOptions);\n this.#streams.add(new WeakRef(stream));\n return stream;\n }\n\n /**\n * Update STT options. Changes will take effect on the next stream.\n *\n * @param opts - Partial options to update\n */\n updateOptions(opts: Partial<STTv2Options>) {\n const nextOpts = { ...opts };\n if (nextOpts.keyterms !== undefined) {\n this.#userKeyterms = [...nextOpts.keyterms];\n nextOpts.keyterms = [...new Set([...this.#userKeyterms, ...this.#sessionKeyterms])];\n }\n this.#opts = {\n ...this.#opts,\n ...nextOpts,\n language:\n opts.language !== undefined ? normalizeLanguage(opts.language) : this.#opts.language,\n };\n if (opts.tags) this.#opts.tags = validateTags(opts.tags);\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 244-249 lines\n if (\n this.#opts.languageHint &&\n this.#opts.languageHint.length > 0 &&\n this.#opts.model !== 'flux-general-multi'\n ) {\n this.#logger.warn(\n { model: this.#opts.model },\n '`languageHint` is only supported by `flux-general-multi` and will be ignored for this model',\n );\n }\n this.#logger.debug('Updated STTv2 options');\n }\n\n override _updateSessionKeyterms(keyterms: string[]): void {\n if (\n keyterms.length === this.#sessionKeyterms.length &&\n keyterms.every((t, i) => t === this.#sessionKeyterms[i])\n ) {\n return;\n }\n this.#sessionKeyterms = [...keyterms];\n const merged = [...new Set([...this.#userKeyterms, ...keyterms])];\n this.#opts.keyterms = merged;\n for (const ref of this.#streams) {\n const stream = ref.deref();\n if (!stream) {\n this.#streams.delete(ref);\n continue;\n }\n if (stream._speaking) {\n // defer the reconnect to the end of the utterance so we don't cut it off\n stream._pendingKeyterm = merged;\n } else {\n stream.updateOptions({ keyterms: merged });\n }\n }\n }\n}\n\n// --- Stream Implementation ---\n\nclass SpeechStreamv2 extends stt.SpeechStream {\n readonly label = 'deepgram.SpeechStreamv2';\n #opts: STTv2Options & { apiKey: string };\n #logger = log();\n #ws: WebSocket | null = null;\n\n #audioDurationCollector: PeriodicCollector<number>;\n #requestId = '';\n #speaking = false;\n\n // Parity: _reconnect_event - using existing Event class from @livekit/agents\n #reconnectEvent = new Event();\n\n // keyterms set while the user is speaking; applied at END_OF_SPEECH (latest wins)\n /** @internal */\n _pendingKeyterm: string[] | null = null;\n\n constructor(\n sttInstance: STTv2,\n opts: STTv2Options & { apiKey: string },\n connOptions?: APIConnectOptions,\n ) {\n super(sttInstance, opts.sampleRate, connOptions);\n this.#opts = opts;\n\n this.#audioDurationCollector = new PeriodicCollector(\n (duration) => this.#onAudioDurationReport(duration),\n { duration: 5.0 },\n );\n }\n\n /** @internal */\n get _speaking(): boolean {\n return this.#speaking;\n }\n\n updateOptions(opts: Partial<STTv2Options>) {\n this.#logger.debug('Stream received option update', opts);\n this.#opts = {\n ...this.#opts,\n ...opts,\n language:\n opts.language !== undefined ? normalizeLanguage(opts.language) : this.#opts.language,\n };\n if (opts.tags) this.#opts.tags = validateTags(opts.tags);\n if (opts.keyterms !== undefined) {\n this._pendingKeyterm = null;\n }\n\n // Trigger reconnection loop\n this.#reconnectEvent.set();\n }\n\n #onEndOfSpeech() {\n if (this._pendingKeyterm !== null) {\n this.updateOptions({ keyterms: this._pendingKeyterm });\n this._pendingKeyterm = null;\n }\n }\n\n protected async run() {\n // Outer Loop: Handles reconnections (Configuration updates)\n while (!this.closed) {\n try {\n this.#reconnectEvent.clear();\n\n const url = this.#getDeepgramUrl();\n this.#logger.debug(`Connecting to Deepgram: ${url}`);\n\n this.#ws = new WebSocket(url, {\n headers: { Authorization: `Token ${this.#opts.apiKey}` },\n });\n\n // 1. Wait for Connection Open\n await new Promise<void>((resolve, reject) => {\n if (!this.#ws) return reject(new Error('WebSocket not initialized'));\n\n const onOpen = () => {\n this.#ws?.off('error', onError);\n resolve();\n };\n const onError = (err: Error) => {\n this.#ws?.off('open', onOpen);\n reject(err);\n };\n\n this.#ws.once('open', onOpen);\n this.#ws.once('error', onError);\n });\n\n // 2. Run Concurrent Tasks (Send & Receive)\n const sendPromise = this.#sendTask();\n const recvPromise = this.#recvTask();\n const reconnectWait = this.#reconnectEvent.wait();\n\n // 3. Race: Normal Completion vs Reconnect Signal\n const result = await Promise.race([\n Promise.all([sendPromise, recvPromise]),\n reconnectWait.then(() => 'RECONNECT'),\n ]);\n\n if (result === 'RECONNECT') {\n this.#logger.debug('Reconnecting stream due to option update...');\n // Close current socket; loop will restart and open a new one\n this.#ws.close();\n } else {\n // Normal finish (Stream ended or Error thrown)\n break;\n }\n } catch (error) {\n this.#logger.error('Deepgram stream error', { error });\n throw error; // Let Base Class handle retry logic\n } finally {\n if (this.#ws?.readyState === WebSocket.OPEN) {\n this.#ws.close();\n }\n }\n }\n this.close();\n }\n\n async #sendTask() {\n if (!this.#ws) return;\n\n // Buffer audio into 50ms chunks (Parity)\n const samples50ms = Math.floor(this.#opts.sampleRate / 20);\n const audioBstream = new AudioByteStream(this.#opts.sampleRate, 1, samples50ms);\n\n let hasEnded = false;\n\n // Manual Iterator to allow racing against Reconnect Signal\n const iterator = this.input[Symbol.asyncIterator]();\n\n while (true) {\n const nextPromise = iterator.next();\n // If reconnect signal fires, abort the wait\n const abortPromise = this.#reconnectEvent.wait().then(() => ({ abort: true }) as const);\n\n const result = await Promise.race([nextPromise, abortPromise]);\n\n // Check if we need to abort (Reconnect) or if stream is done\n if ('abort' in result || result.done) {\n if (!('abort' in result) && result.done) {\n // Normal stream end\n hasEnded = true;\n } else {\n // Reconnect triggered - break loop immediately\n break;\n }\n }\n\n // If we broke above, we don't process data. If not, 'result' is IteratorResult\n if (hasEnded && result.value === undefined) {\n // Process flush below\n } else if ('value' in result) {\n const data = result.value;\n const frames: AudioFrame[] = [];\n\n if (data === SpeechStreamv2.FLUSH_SENTINEL) {\n frames.push(...audioBstream.flush());\n hasEnded = true;\n } else {\n frames.push(...audioBstream.write((data as AudioFrame).data.buffer as ArrayBuffer));\n }\n\n for (const frame of frames) {\n this.#audioDurationCollector.push(calculateAudioDurationSeconds(frame));\n\n if (this.#ws!.readyState === WebSocket.OPEN) {\n this.#ws!.send(frame.data);\n }\n\n if (hasEnded) {\n this.#audioDurationCollector.flush();\n hasEnded = false;\n }\n }\n }\n\n if (hasEnded) break;\n }\n\n // Only send CloseStream if we are exiting normally (not reconnecting)\n if (!this.#reconnectEvent.isSet && this.#ws!.readyState === WebSocket.OPEN) {\n this.#logger.debug('Sending CloseStream message to Deepgram');\n this.#ws!.send(_CLOSE_MSG);\n }\n }\n\n async #recvTask() {\n if (!this.#ws) return;\n\n return new Promise<void>((resolve) => {\n if (!this.#ws) return resolve();\n\n this.#ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n this.#logger.warn('Received unexpected binary message from Deepgram');\n return;\n }\n try {\n const msg = JSON.parse(data.toString());\n this.#processStreamEvent(msg);\n } catch (error) {\n this.#logger.error('Failed to parse Deepgram message', { error });\n }\n });\n\n this.#ws.on('close', (code, reason) => {\n this.#logger.debug(`Deepgram WebSocket closed: ${code} ${reason}`);\n resolve();\n });\n\n // Errors are caught by run() listener, resolve here to clean up task\n this.#ws.on('error', () => resolve());\n });\n }\n\n #processStreamEvent(data: Record<string, unknown>) {\n if (data.request_id) {\n this.#requestId = data.request_id as string;\n }\n\n if (data.type === 'TurnInfo') {\n const eventType = data.event;\n\n if (eventType === 'StartOfTurn') {\n if (this.#speaking) return;\n\n this.#speaking = true;\n this.queue.put({\n type: stt.SpeechEventType.START_OF_SPEECH,\n requestId: this.#requestId,\n });\n\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, data);\n } else if (eventType === 'Update') {\n if (!this.#speaking) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, data);\n } else if (eventType === 'EagerEndOfTurn') {\n if (!this.#speaking) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, data);\n } else if (eventType === 'TurnResumed') {\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, data);\n } else if (eventType === 'EndOfTurn') {\n if (!this.#speaking) return;\n\n this.#speaking = false;\n this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, data);\n\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#onEndOfSpeech();\n }\n } else if (data.type === 'Error') {\n this.#logger.warn('deepgram sent an error', { data });\n const desc = (data.description as string) || 'unknown error from deepgram';\n throw new Error(`Deepgram API Error: ${desc}`);\n }\n }\n\n #sendTranscriptEvent(eventType: stt.SpeechEventType, data: Record<string, unknown>) {\n const alts = parseTranscription(this.#opts.language || 'en', data, this.startTimeOffset);\n\n if (alts.length > 0) {\n this.queue.put({\n type: eventType,\n requestId: this.#requestId,\n alternatives: [alts[0]!, ...alts.slice(1)],\n });\n }\n }\n\n #onAudioDurationReport(duration: number) {\n const usageEvent: stt.SpeechEvent = {\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: {\n audioDuration: duration,\n },\n };\n this.queue.put(usageEvent);\n }\n\n #getDeepgramUrl(): string {\n const params: Record<string, string | string[]> = {\n model: this.#opts.model,\n sample_rate: this.#opts.sampleRate.toString(),\n encoding: 'linear16',\n mip_opt_out: String(this.#opts.mipOptOut),\n };\n\n // Note: v2 API does NOT include 'language' parameter\n if (this.#opts.eagerEotThreshold)\n params.eager_eot_threshold = this.#opts.eagerEotThreshold.toString();\n if (this.#opts.eotThreshold) params.eot_threshold = this.#opts.eotThreshold.toString();\n if (this.#opts.eotTimeoutMs) params.eot_timeout_ms = this.#opts.eotTimeoutMs.toString();\n\n if (this.#opts.keyterms.length > 0) params.keyterm = this.#opts.keyterms;\n if (this.#opts.tags && this.#opts.tags.length > 0) params.tag = this.#opts.tags;\n\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 480-481 lines\n if (this.#opts.languageHint && this.#opts.languageHint.length > 0) {\n params.language_hint = this.#opts.languageHint;\n }\n\n const baseUrl = this.#opts.endpointUrl.replace(/^http/, 'ws');\n const qs = queryString.stringify(params);\n return `${baseUrl}?${qs}`;\n }\n\n override close() {\n super.close();\n this.#ws?.close();\n }\n}\n\n// --- Helpers ---\n\nfunction parseTranscription(\n language: string,\n data: Record<string, unknown>,\n startTimeOffset: number,\n): stt.SpeechData[] {\n const transcript = data.transcript as string | undefined;\n const wordsData = (data.words as Array<Record<string, unknown>>) || [];\n\n if (!wordsData || wordsData.length === 0) {\n return [];\n }\n\n let confidence = 0;\n if (wordsData.length > 0) {\n const sum = wordsData.reduce((acc: number, w) => acc + ((w.confidence as number) || 0), 0);\n confidence = sum / wordsData.length;\n }\n\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 587-591 lines\n const detectedLanguagesRaw = Array.isArray(data.languages) ? (data.languages as string[]) : [];\n const detectedLanguages = detectedLanguagesRaw.map((lang) => normalizeLanguage(lang));\n const primaryLanguage =\n detectedLanguages.length > 0 ? detectedLanguages[0]! : normalizeLanguage(language);\n\n const sd: stt.SpeechData = {\n language: primaryLanguage,\n startTime: ((data.audio_window_start as number) || 0) + startTimeOffset,\n endTime: ((data.audio_window_end as number) || 0) + startTimeOffset,\n confidence: confidence,\n text: transcript || '',\n // Ref: python livekit-plugins/livekit-plugins-deepgram/livekit/plugins/deepgram/stt_v2.py - 598 line\n sourceLanguages: detectedLanguages.length > 0 ? detectedLanguages : undefined,\n // Note: Deepgram V2 (Flux) API does not provide word-level timing (start/end).\n // Words only contain 'word' and 'confidence' fields, so startTime/endTime will be 0.\n // See: https://developers.deepgram.com/docs/flux/nova-3-migration\n words: wordsData.map((word) =>\n createTimedString({\n text: (word.word as string) ?? '',\n startTime: ((word.start as number) ?? 0) + startTimeOffset,\n endTime: ((word.end as number) ?? 0) + startTimeOffset,\n confidence: (word.confidence as number) ?? 0.0,\n startTimeOffset,\n }),\n ),\n };\n\n return [sd];\n}\n"],"mappings":"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","names":["_a"]}
|
package/dist/tts.cjs
CHANGED
package/dist/tts.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIError,\n APIStatusError,\n APITimeoutError,\n AudioByteStream,\n log,\n shortuuid,\n tokenize,\n tts,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { request } from 'node:https';\nimport { type RawData, WebSocket } from 'ws';\nimport type { TTSEncoding, TTSModels } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'Authorization';\nconst NUM_CHANNELS = 1;\nconst MIN_SENTENCE_LENGTH = 8;\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n bitRate?: number | null;\n speed?: number;\n mipOptOut: boolean;\n apiKey?: string;\n baseUrl?: string;\n sentenceTokenizer: tokenize.SentenceTokenizer;\n capabilities: tts.TTSCapabilities;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'aura-2-andromeda-en',\n encoding: 'linear16',\n sampleRate: 24000,\n bitRate: null,\n mipOptOut: false,\n apiKey: process.env.DEEPGRAM_API_KEY,\n baseUrl: 'https://api.deepgram.com',\n capabilities: {\n streaming: true,\n },\n sentenceTokenizer: new tokenize.basic.SentenceTokenizer({\n minSentenceLength: MIN_SENTENCE_LENGTH,\n }),\n};\n\nexport class TTS extends tts.TTS {\n private opts: TTSOptions;\n label = 'deepgram.TTS';\n\n get model(): string {\n return this.opts.model;\n }\n\n get provider(): string {\n return 'Deepgram';\n }\n\n override get sampleRate(): number {\n return this.opts.sampleRate;\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: opts.capabilities?.streaming ?? defaultTTSOptions.capabilities.streaming,\n });\n\n this.opts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n if (this.opts.apiKey === undefined) {\n throw new Error(\n 'Deepgram API key is required, whether as an argument or as $DEEPGRAM_API_KEY',\n );\n }\n\n if (this.opts.speed !== undefined && (this.opts.speed < 0.7 || this.opts.speed > 1.5)) {\n throw new Error(`Deepgram TTS speed must be between 0.7 and 1.5, got ${this.opts.speed}`);\n }\n }\n\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): tts.ChunkedStream {\n return new ChunkedStream(this, text, this.opts, connOptions, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): tts.SynthesizeStream {\n return new SynthesizeStream(this, this.opts, options?.connOptions);\n }\n\n updateOptions(\n opts: Partial<\n Pick<TTSOptions, 'model' | 'encoding' | 'sampleRate' | 'bitRate' | 'speed' | 'mipOptOut'>\n >,\n ) {\n this.opts = {\n ...this.opts,\n ...opts,\n };\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'deepgram.ChunkedStream';\n #logger = log();\n private opts: TTSOptions;\n private text: string;\n\n constructor(\n tts: TTS,\n text: string,\n opts: TTSOptions,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, tts, connOptions, abortSignal);\n this.text = text;\n this.opts = opts;\n }\n\n protected async run() {\n const requestId = shortuuid();\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const json = { text: this.text };\n const url = new URL(`${this.opts.baseUrl!}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('container', 'none');\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n fn();\n }\n };\n\n const req = request(\n {\n hostname: url.hostname,\n port: 443,\n path: url.pathname + url.search,\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n 'Content-Type': 'application/json',\n },\n signal: this.abortSignal,\n },\n (res) => {\n if (res.statusCode !== 200) {\n settle(() =>\n reject(\n new Error(\n `Deepgram TTS HTTP request failed: ${res.statusCode} ${res.statusMessage}`,\n ),\n ),\n );\n return;\n }\n\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n });\n\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Deepgram TTS response error');\n settle(() => reject(err));\n });\n\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n if (!this.queue.closed) {\n this.queue.close();\n }\n settle(() => resolve());\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Deepgram TTS request error');\n settle(() => reject(err));\n });\n\n req.on('close', () => settle(() => resolve()));\n req.write(JSON.stringify(json));\n req.end();\n });\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n private opts: TTSOptions;\n private tokenizer: tokenize.SentenceStream;\n #logger = log();\n label = 'deepgram.SynthesizeStream';\n\n private static readonly FLUSH_MSG = JSON.stringify({ type: 'Flush' });\n private static readonly CLOSE_MSG = JSON.stringify({ type: 'Close' });\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tokenizer = opts.sentenceTokenizer.stream();\n }\n\n private async closeWebSocket(ws: WebSocket): Promise<void> {\n try {\n // Send Flush and Close messages to ensure Deepgram processes all remaining audio\n // and properly terminates the session, preventing lingering TTS sessions\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n ws.send(SynthesizeStream.CLOSE_MSG);\n\n // Wait for server acknowledgment to prevent race conditions and ensure\n // proper cleanup, avoiding 429 Too Many Requests errors from lingering sessions\n try {\n await new Promise<void>((resolve, _reject) => {\n const timeout = setTimeout(() => {\n resolve();\n }, 1000);\n\n ws.once('message', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('error', () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n } catch (e) {\n // Ignore timeout or other errors during close sequence\n }\n }\n } catch (e) {\n console.warn(`Error during WebSocket close sequence: ${e}`);\n } finally {\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n }\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n const segmentId = shortuuid();\n\n const wsUrl = this.opts.baseUrl!.replace(/^http/, 'ws');\n const url = new URL(`${wsUrl}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n },\n });\n\n await new Promise((resolve, reject) => {\n ws.on('open', resolve);\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(`WebSocket returned ${code}`));\n });\n\n const inputTask = async () => {\n for await (const data of this.input) {\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n this.tokenizer.flush();\n continue;\n }\n this.tokenizer.pushText(data);\n }\n this.tokenizer.endInput();\n this.tokenizer.close();\n };\n\n let markInputSent: () => void = () => {};\n const inputSent = new Promise<void>((resolve) => {\n markInputSent = resolve;\n });\n\n const sendTask = async () => {\n try {\n for await (const event of this.tokenizer) {\n if (this.abortController.signal.aborted) break;\n\n let text = event.token;\n if (!text.endsWith(' ')) {\n text += ' ';\n }\n\n const message = JSON.stringify({\n type: 'Speak',\n text: text,\n });\n\n ws.send(message);\n markInputSent();\n }\n\n if (!this.abortController.signal.aborted) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n markInputSent();\n }\n } finally {\n markInputSent();\n }\n };\n\n const recvTask = async () => {\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n let finalReceived = false;\n let timeout: NodeJS.Timeout | null = null;\n let lastFrame: AudioFrame | undefined;\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const clearMessageTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n const resetMessageTimeout = (reject: (reason?: unknown) => void) => {\n clearMessageTimeout();\n timeout = setTimeout(() => {\n reject(new APITimeoutError({ message: 'Deepgram TTS recv idle timeout' }));\n }, this.connOptions.timeoutMs);\n };\n\n await inputSent;\n if (this.abortController.signal.aborted) return;\n\n return new Promise<void>((resolve, reject) => {\n resetMessageTimeout(reject);\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n clearMessageTimeout();\n\n if (!isBinary) {\n const message = JSON.parse(data.toString());\n if (message.type === 'Flushed') {\n finalReceived = true;\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n resolve();\n return;\n } else if (message.type === 'Warning') {\n this.#logger.warn(`Deepgram warning: ${message.warn_msg}`);\n } else if (message.type === 'Error' || message.type === 'error') {\n reject(new APIError('Deepgram TTS returned error', { body: message }));\n return;\n } else if (message.type !== 'Metadata') {\n this.#logger.warn({ message }, 'Unknown Deepgram message type');\n }\n\n resetMessageTimeout(reject);\n return;\n }\n\n const buffer =\n data instanceof Buffer\n ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)\n : (data as ArrayBuffer);\n for (const frame of bstream.write(buffer as ArrayBuffer)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n resetMessageTimeout(reject);\n });\n\n ws.on('close', (code, reason) => {\n clearMessageTimeout();\n if (!finalReceived) {\n reject(\n new APIStatusError({\n message: 'Deepgram websocket connection closed unexpectedly',\n options: {\n statusCode: code || -1,\n body: { reason: reason.toString() },\n },\n }),\n );\n return;\n }\n resolve();\n });\n\n ws.on('error', (error) => {\n clearMessageTimeout();\n reject(error);\n });\n });\n };\n\n try {\n await Promise.all([inputTask(), sendTask(), recvTask()]);\n } catch (e) {\n if (this.abortController.signal.aborted) return;\n if (e instanceof APIError) throw e;\n throw new APIConnectionError({\n message: `Deepgram TTS WebSocket failed: ${(e as Error).message ?? 'unknown error'}`,\n });\n } finally {\n await this.closeWebSocket(ws);\n }\n }\n}\n"],"mappings":"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","names":["tts","segmentId"]}
|
|
1
|
+
{"version":3,"sources":["../src/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIError,\n APIStatusError,\n APITimeoutError,\n AudioByteStream,\n log,\n shortuuid,\n tokenize,\n tts,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { request } from 'node:https';\nimport { type RawData, WebSocket } from 'ws';\nimport type { TTSEncoding, TTSModels } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'Authorization';\nconst NUM_CHANNELS = 1;\nconst MIN_SENTENCE_LENGTH = 8;\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n bitRate?: number | null;\n speed?: number;\n mipOptOut: boolean;\n apiKey?: string;\n baseUrl?: string;\n sentenceTokenizer: tokenize.SentenceTokenizer;\n capabilities: tts.TTSCapabilities;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'aura-2-andromeda-en',\n encoding: 'linear16',\n sampleRate: 24000,\n bitRate: null,\n mipOptOut: false,\n apiKey: process.env.DEEPGRAM_API_KEY,\n baseUrl: 'https://api.deepgram.com',\n capabilities: {\n streaming: true,\n },\n sentenceTokenizer: new tokenize.basic.SentenceTokenizer({\n minSentenceLength: MIN_SENTENCE_LENGTH,\n }),\n};\n\nexport class TTS extends tts.TTS {\n private opts: TTSOptions;\n label = 'deepgram.TTS';\n\n get model(): string {\n return this.opts.model;\n }\n\n get provider(): string {\n return 'Deepgram';\n }\n\n override get sampleRate(): number {\n return this.opts.sampleRate;\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: opts.capabilities?.streaming ?? defaultTTSOptions.capabilities.streaming,\n });\n\n this.opts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n if (this.opts.apiKey === undefined) {\n throw new Error(\n 'Deepgram API key is required, whether as an argument or as $DEEPGRAM_API_KEY',\n );\n }\n\n if (this.opts.speed !== undefined && (this.opts.speed < 0.7 || this.opts.speed > 1.5)) {\n throw new Error(`Deepgram TTS speed must be between 0.7 and 1.5, got ${this.opts.speed}`);\n }\n }\n\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): tts.ChunkedStream {\n return new ChunkedStream(this, text, this.opts, connOptions, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): tts.SynthesizeStream {\n return new SynthesizeStream(this, this.opts, options?.connOptions);\n }\n\n updateOptions(\n opts: Partial<\n Pick<TTSOptions, 'model' | 'encoding' | 'sampleRate' | 'bitRate' | 'speed' | 'mipOptOut'>\n >,\n ) {\n this.opts = {\n ...this.opts,\n ...opts,\n };\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'deepgram.ChunkedStream';\n #logger = log();\n private opts: TTSOptions;\n private text: string;\n\n constructor(\n tts: TTS,\n text: string,\n opts: TTSOptions,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, tts, connOptions, abortSignal);\n this.text = text;\n this.opts = opts;\n }\n\n protected async run() {\n const requestId = shortuuid();\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const json = { text: this.text };\n const url = new URL(`${this.opts.baseUrl!}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('container', 'none');\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n fn();\n }\n };\n\n const req = request(\n {\n hostname: url.hostname,\n port: 443,\n path: url.pathname + url.search,\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n 'Content-Type': 'application/json',\n },\n signal: this.abortSignal,\n },\n (res) => {\n if (res.statusCode !== 200) {\n settle(() =>\n reject(\n new Error(\n `Deepgram TTS HTTP request failed: ${res.statusCode} ${res.statusMessage}`,\n ),\n ),\n );\n return;\n }\n\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n });\n\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Deepgram TTS response error');\n settle(() => reject(err));\n });\n\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n if (!this.queue.closed) {\n this.queue.close();\n }\n settle(() => resolve());\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Deepgram TTS request error');\n settle(() => reject(err));\n });\n\n req.on('close', () => settle(() => resolve()));\n req.write(JSON.stringify(json));\n req.end();\n });\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n private opts: TTSOptions;\n private tokenizer: tokenize.SentenceStream;\n #logger = log();\n label = 'deepgram.SynthesizeStream';\n\n private static readonly FLUSH_MSG = JSON.stringify({ type: 'Flush' });\n private static readonly CLOSE_MSG = JSON.stringify({ type: 'Close' });\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tokenizer = opts.sentenceTokenizer.stream();\n }\n\n private async closeWebSocket(ws: WebSocket): Promise<void> {\n try {\n // Send Flush and Close messages to ensure Deepgram processes all remaining audio\n // and properly terminates the session, preventing lingering TTS sessions\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n ws.send(SynthesizeStream.CLOSE_MSG);\n\n // Wait for server acknowledgment to prevent race conditions and ensure\n // proper cleanup, avoiding 429 Too Many Requests errors from lingering sessions\n try {\n await new Promise<void>((resolve, _reject) => {\n const timeout = setTimeout(() => {\n resolve();\n }, 1000);\n\n ws.once('message', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('error', () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n } catch (e) {\n // Ignore timeout or other errors during close sequence\n }\n }\n } catch (e) {\n console.warn(`Error during WebSocket close sequence: ${e}`);\n } finally {\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n }\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n const segmentId = shortuuid();\n\n const wsUrl = this.opts.baseUrl!.replace(/^http/, 'ws');\n const url = new URL(`${wsUrl}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n },\n });\n\n await new Promise((resolve, reject) => {\n ws.on('open', resolve);\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(`WebSocket returned ${code}`));\n });\n\n const inputTask = async () => {\n for await (const data of this.input) {\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n this.tokenizer.flush();\n continue;\n }\n this.tokenizer.pushText(data);\n }\n this.tokenizer.endInput();\n this.tokenizer.close();\n };\n\n let markInputSent: () => void = () => {};\n const inputSent = new Promise<void>((resolve) => {\n markInputSent = resolve;\n });\n\n const sendTask = async () => {\n try {\n for await (const event of this.tokenizer) {\n if (this.abortController.signal.aborted) break;\n\n let text = event.token;\n if (!text.endsWith(' ')) {\n text += ' ';\n }\n\n const message = JSON.stringify({\n type: 'Speak',\n text: text,\n });\n\n this.markStarted();\n ws.send(message);\n markInputSent();\n }\n\n if (!this.abortController.signal.aborted) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n markInputSent();\n }\n } finally {\n markInputSent();\n }\n };\n\n const recvTask = async () => {\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n let finalReceived = false;\n let timeout: NodeJS.Timeout | null = null;\n let lastFrame: AudioFrame | undefined;\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const clearMessageTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n const resetMessageTimeout = (reject: (reason?: unknown) => void) => {\n clearMessageTimeout();\n timeout = setTimeout(() => {\n reject(new APITimeoutError({ message: 'Deepgram TTS recv idle timeout' }));\n }, this.connOptions.timeoutMs);\n };\n\n await inputSent;\n if (this.abortController.signal.aborted) return;\n\n return new Promise<void>((resolve, reject) => {\n resetMessageTimeout(reject);\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n clearMessageTimeout();\n\n if (!isBinary) {\n const message = JSON.parse(data.toString());\n if (message.type === 'Flushed') {\n finalReceived = true;\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n resolve();\n return;\n } else if (message.type === 'Warning') {\n this.#logger.warn(`Deepgram warning: ${message.warn_msg}`);\n } else if (message.type === 'Error' || message.type === 'error') {\n reject(new APIError('Deepgram TTS returned error', { body: message }));\n return;\n } else if (message.type !== 'Metadata') {\n this.#logger.warn({ message }, 'Unknown Deepgram message type');\n }\n\n resetMessageTimeout(reject);\n return;\n }\n\n const buffer =\n data instanceof Buffer\n ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)\n : (data as ArrayBuffer);\n for (const frame of bstream.write(buffer as ArrayBuffer)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n resetMessageTimeout(reject);\n });\n\n ws.on('close', (code, reason) => {\n clearMessageTimeout();\n if (!finalReceived) {\n reject(\n new APIStatusError({\n message: 'Deepgram websocket connection closed unexpectedly',\n options: {\n statusCode: code || -1,\n body: { reason: reason.toString() },\n },\n }),\n );\n return;\n }\n resolve();\n });\n\n ws.on('error', (error) => {\n clearMessageTimeout();\n reject(error);\n });\n });\n };\n\n try {\n await Promise.all([inputTask(), sendTask(), recvTask()]);\n } catch (e) {\n if (this.abortController.signal.aborted) return;\n if (e instanceof APIError) throw e;\n throw new APIConnectionError({\n message: `Deepgram TTS WebSocket failed: ${(e as Error).message ?? 'unknown error'}`,\n });\n } finally {\n await this.closeWebSocket(ws);\n }\n }\n}\n"],"mappings":"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","names":["tts","segmentId"]}
|
package/dist/tts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../src/tts.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAQtB,QAAQ,EACR,GAAG,EACJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM1D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB,CAAC;IAC9C,YAAY,EAAE,GAAG,CAAC,eAAe,CAAC;CACnC;AAkBD,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;IAC9B,OAAO,CAAC,IAAI,CAAa;IACzB,KAAK,SAAkB;IAEvB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAa,UAAU,IAAI,MAAM,CAEhC;gBAEW,IAAI,GAAE,OAAO,CAAC,UAAU,CAAM;IAqB1C,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW,GACxB,GAAG,CAAC,aAAa;IAIpB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,GAAG,CAAC,gBAAgB;IAI3E,aAAa,CACX,IAAI,EAAE,OAAO,CACX,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC,CAC1F;CAOJ;AAED,qBAAa,aAAc,SAAQ,GAAG,CAAC,aAAa;;IAClD,KAAK,SAA4B;IAEjC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,IAAI,CAAS;gBAGnB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW;cAOX,GAAG;CAmGpB;AAED,qBAAa,gBAAiB,SAAQ,GAAG,CAAC,gBAAgB;;IACxD,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,SAAS,CAA0B;IAE3C,KAAK,SAA+B;IAEpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAqC;IACtE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAqC;gBAE1D,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,iBAAiB;YAMzD,cAAc;cA4CZ,GAAG;
|
|
1
|
+
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../src/tts.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAQtB,QAAQ,EACR,GAAG,EACJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM1D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB,CAAC;IAC9C,YAAY,EAAE,GAAG,CAAC,eAAe,CAAC;CACnC;AAkBD,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;IAC9B,OAAO,CAAC,IAAI,CAAa;IACzB,KAAK,SAAkB;IAEvB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAa,UAAU,IAAI,MAAM,CAEhC;gBAEW,IAAI,GAAE,OAAO,CAAC,UAAU,CAAM;IAqB1C,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW,GACxB,GAAG,CAAC,aAAa;IAIpB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,GAAG,CAAC,gBAAgB;IAI3E,aAAa,CACX,IAAI,EAAE,OAAO,CACX,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC,CAC1F;CAOJ;AAED,qBAAa,aAAc,SAAQ,GAAG,CAAC,aAAa;;IAClD,KAAK,SAA4B;IAEjC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,IAAI,CAAS;gBAGnB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,WAAW,CAAC,EAAE,WAAW;cAOX,GAAG;CAmGpB;AAED,qBAAa,gBAAiB,SAAQ,GAAG,CAAC,gBAAgB;;IACxD,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,SAAS,CAA0B;IAE3C,KAAK,SAA+B;IAEpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAqC;IACtE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAqC;gBAE1D,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,iBAAiB;YAMzD,cAAc;cA4CZ,GAAG;CA0LpB"}
|
package/dist/tts.js
CHANGED
package/dist/tts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIError,\n APIStatusError,\n APITimeoutError,\n AudioByteStream,\n log,\n shortuuid,\n tokenize,\n tts,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { request } from 'node:https';\nimport { type RawData, WebSocket } from 'ws';\nimport type { TTSEncoding, TTSModels } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'Authorization';\nconst NUM_CHANNELS = 1;\nconst MIN_SENTENCE_LENGTH = 8;\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n bitRate?: number | null;\n speed?: number;\n mipOptOut: boolean;\n apiKey?: string;\n baseUrl?: string;\n sentenceTokenizer: tokenize.SentenceTokenizer;\n capabilities: tts.TTSCapabilities;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'aura-2-andromeda-en',\n encoding: 'linear16',\n sampleRate: 24000,\n bitRate: null,\n mipOptOut: false,\n apiKey: process.env.DEEPGRAM_API_KEY,\n baseUrl: 'https://api.deepgram.com',\n capabilities: {\n streaming: true,\n },\n sentenceTokenizer: new tokenize.basic.SentenceTokenizer({\n minSentenceLength: MIN_SENTENCE_LENGTH,\n }),\n};\n\nexport class TTS extends tts.TTS {\n private opts: TTSOptions;\n label = 'deepgram.TTS';\n\n get model(): string {\n return this.opts.model;\n }\n\n get provider(): string {\n return 'Deepgram';\n }\n\n override get sampleRate(): number {\n return this.opts.sampleRate;\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: opts.capabilities?.streaming ?? defaultTTSOptions.capabilities.streaming,\n });\n\n this.opts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n if (this.opts.apiKey === undefined) {\n throw new Error(\n 'Deepgram API key is required, whether as an argument or as $DEEPGRAM_API_KEY',\n );\n }\n\n if (this.opts.speed !== undefined && (this.opts.speed < 0.7 || this.opts.speed > 1.5)) {\n throw new Error(`Deepgram TTS speed must be between 0.7 and 1.5, got ${this.opts.speed}`);\n }\n }\n\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): tts.ChunkedStream {\n return new ChunkedStream(this, text, this.opts, connOptions, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): tts.SynthesizeStream {\n return new SynthesizeStream(this, this.opts, options?.connOptions);\n }\n\n updateOptions(\n opts: Partial<\n Pick<TTSOptions, 'model' | 'encoding' | 'sampleRate' | 'bitRate' | 'speed' | 'mipOptOut'>\n >,\n ) {\n this.opts = {\n ...this.opts,\n ...opts,\n };\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'deepgram.ChunkedStream';\n #logger = log();\n private opts: TTSOptions;\n private text: string;\n\n constructor(\n tts: TTS,\n text: string,\n opts: TTSOptions,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, tts, connOptions, abortSignal);\n this.text = text;\n this.opts = opts;\n }\n\n protected async run() {\n const requestId = shortuuid();\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const json = { text: this.text };\n const url = new URL(`${this.opts.baseUrl!}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('container', 'none');\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n fn();\n }\n };\n\n const req = request(\n {\n hostname: url.hostname,\n port: 443,\n path: url.pathname + url.search,\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n 'Content-Type': 'application/json',\n },\n signal: this.abortSignal,\n },\n (res) => {\n if (res.statusCode !== 200) {\n settle(() =>\n reject(\n new Error(\n `Deepgram TTS HTTP request failed: ${res.statusCode} ${res.statusMessage}`,\n ),\n ),\n );\n return;\n }\n\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n });\n\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Deepgram TTS response error');\n settle(() => reject(err));\n });\n\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n if (!this.queue.closed) {\n this.queue.close();\n }\n settle(() => resolve());\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Deepgram TTS request error');\n settle(() => reject(err));\n });\n\n req.on('close', () => settle(() => resolve()));\n req.write(JSON.stringify(json));\n req.end();\n });\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n private opts: TTSOptions;\n private tokenizer: tokenize.SentenceStream;\n #logger = log();\n label = 'deepgram.SynthesizeStream';\n\n private static readonly FLUSH_MSG = JSON.stringify({ type: 'Flush' });\n private static readonly CLOSE_MSG = JSON.stringify({ type: 'Close' });\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tokenizer = opts.sentenceTokenizer.stream();\n }\n\n private async closeWebSocket(ws: WebSocket): Promise<void> {\n try {\n // Send Flush and Close messages to ensure Deepgram processes all remaining audio\n // and properly terminates the session, preventing lingering TTS sessions\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n ws.send(SynthesizeStream.CLOSE_MSG);\n\n // Wait for server acknowledgment to prevent race conditions and ensure\n // proper cleanup, avoiding 429 Too Many Requests errors from lingering sessions\n try {\n await new Promise<void>((resolve, _reject) => {\n const timeout = setTimeout(() => {\n resolve();\n }, 1000);\n\n ws.once('message', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('error', () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n } catch (e) {\n // Ignore timeout or other errors during close sequence\n }\n }\n } catch (e) {\n console.warn(`Error during WebSocket close sequence: ${e}`);\n } finally {\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n }\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n const segmentId = shortuuid();\n\n const wsUrl = this.opts.baseUrl!.replace(/^http/, 'ws');\n const url = new URL(`${wsUrl}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n },\n });\n\n await new Promise((resolve, reject) => {\n ws.on('open', resolve);\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(`WebSocket returned ${code}`));\n });\n\n const inputTask = async () => {\n for await (const data of this.input) {\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n this.tokenizer.flush();\n continue;\n }\n this.tokenizer.pushText(data);\n }\n this.tokenizer.endInput();\n this.tokenizer.close();\n };\n\n let markInputSent: () => void = () => {};\n const inputSent = new Promise<void>((resolve) => {\n markInputSent = resolve;\n });\n\n const sendTask = async () => {\n try {\n for await (const event of this.tokenizer) {\n if (this.abortController.signal.aborted) break;\n\n let text = event.token;\n if (!text.endsWith(' ')) {\n text += ' ';\n }\n\n const message = JSON.stringify({\n type: 'Speak',\n text: text,\n });\n\n ws.send(message);\n markInputSent();\n }\n\n if (!this.abortController.signal.aborted) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n markInputSent();\n }\n } finally {\n markInputSent();\n }\n };\n\n const recvTask = async () => {\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n let finalReceived = false;\n let timeout: NodeJS.Timeout | null = null;\n let lastFrame: AudioFrame | undefined;\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const clearMessageTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n const resetMessageTimeout = (reject: (reason?: unknown) => void) => {\n clearMessageTimeout();\n timeout = setTimeout(() => {\n reject(new APITimeoutError({ message: 'Deepgram TTS recv idle timeout' }));\n }, this.connOptions.timeoutMs);\n };\n\n await inputSent;\n if (this.abortController.signal.aborted) return;\n\n return new Promise<void>((resolve, reject) => {\n resetMessageTimeout(reject);\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n clearMessageTimeout();\n\n if (!isBinary) {\n const message = JSON.parse(data.toString());\n if (message.type === 'Flushed') {\n finalReceived = true;\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n resolve();\n return;\n } else if (message.type === 'Warning') {\n this.#logger.warn(`Deepgram warning: ${message.warn_msg}`);\n } else if (message.type === 'Error' || message.type === 'error') {\n reject(new APIError('Deepgram TTS returned error', { body: message }));\n return;\n } else if (message.type !== 'Metadata') {\n this.#logger.warn({ message }, 'Unknown Deepgram message type');\n }\n\n resetMessageTimeout(reject);\n return;\n }\n\n const buffer =\n data instanceof Buffer\n ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)\n : (data as ArrayBuffer);\n for (const frame of bstream.write(buffer as ArrayBuffer)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n resetMessageTimeout(reject);\n });\n\n ws.on('close', (code, reason) => {\n clearMessageTimeout();\n if (!finalReceived) {\n reject(\n new APIStatusError({\n message: 'Deepgram websocket connection closed unexpectedly',\n options: {\n statusCode: code || -1,\n body: { reason: reason.toString() },\n },\n }),\n );\n return;\n }\n resolve();\n });\n\n ws.on('error', (error) => {\n clearMessageTimeout();\n reject(error);\n });\n });\n };\n\n try {\n await Promise.all([inputTask(), sendTask(), recvTask()]);\n } catch (e) {\n if (this.abortController.signal.aborted) return;\n if (e instanceof APIError) throw e;\n throw new APIConnectionError({\n message: `Deepgram TTS WebSocket failed: ${(e as Error).message ?? 'unknown error'}`,\n });\n } finally {\n await this.closeWebSocket(ws);\n }\n }\n}\n"],"mappings":"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","names":["tts","segmentId"]}
|
|
1
|
+
{"version":3,"sources":["../src/tts.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIError,\n APIStatusError,\n APITimeoutError,\n AudioByteStream,\n log,\n shortuuid,\n tokenize,\n tts,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { request } from 'node:https';\nimport { type RawData, WebSocket } from 'ws';\nimport type { TTSEncoding, TTSModels } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'Authorization';\nconst NUM_CHANNELS = 1;\nconst MIN_SENTENCE_LENGTH = 8;\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n bitRate?: number | null;\n speed?: number;\n mipOptOut: boolean;\n apiKey?: string;\n baseUrl?: string;\n sentenceTokenizer: tokenize.SentenceTokenizer;\n capabilities: tts.TTSCapabilities;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'aura-2-andromeda-en',\n encoding: 'linear16',\n sampleRate: 24000,\n bitRate: null,\n mipOptOut: false,\n apiKey: process.env.DEEPGRAM_API_KEY,\n baseUrl: 'https://api.deepgram.com',\n capabilities: {\n streaming: true,\n },\n sentenceTokenizer: new tokenize.basic.SentenceTokenizer({\n minSentenceLength: MIN_SENTENCE_LENGTH,\n }),\n};\n\nexport class TTS extends tts.TTS {\n private opts: TTSOptions;\n label = 'deepgram.TTS';\n\n get model(): string {\n return this.opts.model;\n }\n\n get provider(): string {\n return 'Deepgram';\n }\n\n override get sampleRate(): number {\n return this.opts.sampleRate;\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: opts.capabilities?.streaming ?? defaultTTSOptions.capabilities.streaming,\n });\n\n this.opts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n if (this.opts.apiKey === undefined) {\n throw new Error(\n 'Deepgram API key is required, whether as an argument or as $DEEPGRAM_API_KEY',\n );\n }\n\n if (this.opts.speed !== undefined && (this.opts.speed < 0.7 || this.opts.speed > 1.5)) {\n throw new Error(`Deepgram TTS speed must be between 0.7 and 1.5, got ${this.opts.speed}`);\n }\n }\n\n synthesize(\n text: string,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ): tts.ChunkedStream {\n return new ChunkedStream(this, text, this.opts, connOptions, abortSignal);\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): tts.SynthesizeStream {\n return new SynthesizeStream(this, this.opts, options?.connOptions);\n }\n\n updateOptions(\n opts: Partial<\n Pick<TTSOptions, 'model' | 'encoding' | 'sampleRate' | 'bitRate' | 'speed' | 'mipOptOut'>\n >,\n ) {\n this.opts = {\n ...this.opts,\n ...opts,\n };\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'deepgram.ChunkedStream';\n #logger = log();\n private opts: TTSOptions;\n private text: string;\n\n constructor(\n tts: TTS,\n text: string,\n opts: TTSOptions,\n connOptions?: APIConnectOptions,\n abortSignal?: AbortSignal,\n ) {\n super(text, tts, connOptions, abortSignal);\n this.text = text;\n this.opts = opts;\n }\n\n protected async run() {\n const requestId = shortuuid();\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n const json = { text: this.text };\n const url = new URL(`${this.opts.baseUrl!}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('container', 'none');\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n fn();\n }\n };\n\n const req = request(\n {\n hostname: url.hostname,\n port: 443,\n path: url.pathname + url.search,\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n 'Content-Type': 'application/json',\n },\n signal: this.abortSignal,\n },\n (res) => {\n if (res.statusCode !== 200) {\n settle(() =>\n reject(\n new Error(\n `Deepgram TTS HTTP request failed: ${res.statusCode} ${res.statusMessage}`,\n ),\n ),\n );\n return;\n }\n\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n });\n\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Deepgram TTS response error');\n settle(() => reject(err));\n });\n\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n if (!this.queue.closed) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n }\n if (!this.queue.closed) {\n this.queue.close();\n }\n settle(() => resolve());\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Deepgram TTS request error');\n settle(() => reject(err));\n });\n\n req.on('close', () => settle(() => resolve()));\n req.write(JSON.stringify(json));\n req.end();\n });\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n private opts: TTSOptions;\n private tokenizer: tokenize.SentenceStream;\n #logger = log();\n label = 'deepgram.SynthesizeStream';\n\n private static readonly FLUSH_MSG = JSON.stringify({ type: 'Flush' });\n private static readonly CLOSE_MSG = JSON.stringify({ type: 'Close' });\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n this.opts = opts;\n this.tokenizer = opts.sentenceTokenizer.stream();\n }\n\n private async closeWebSocket(ws: WebSocket): Promise<void> {\n try {\n // Send Flush and Close messages to ensure Deepgram processes all remaining audio\n // and properly terminates the session, preventing lingering TTS sessions\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n ws.send(SynthesizeStream.CLOSE_MSG);\n\n // Wait for server acknowledgment to prevent race conditions and ensure\n // proper cleanup, avoiding 429 Too Many Requests errors from lingering sessions\n try {\n await new Promise<void>((resolve, _reject) => {\n const timeout = setTimeout(() => {\n resolve();\n }, 1000);\n\n ws.once('message', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('close', () => {\n clearTimeout(timeout);\n resolve();\n });\n\n ws.once('error', () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n } catch (e) {\n // Ignore timeout or other errors during close sequence\n }\n }\n } catch (e) {\n console.warn(`Error during WebSocket close sequence: ${e}`);\n } finally {\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n }\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n const segmentId = shortuuid();\n\n const wsUrl = this.opts.baseUrl!.replace(/^http/, 'ws');\n const url = new URL(`${wsUrl}/v1/speak`);\n url.searchParams.append('sample_rate', this.opts.sampleRate.toString());\n url.searchParams.append('model', this.opts.model);\n url.searchParams.append('encoding', this.opts.encoding);\n url.searchParams.append('mip_opt_out', String(this.opts.mipOptOut));\n if (this.opts.bitRate !== undefined && this.opts.bitRate !== null) {\n url.searchParams.append('bit_rate', this.opts.bitRate.toString());\n }\n if (this.opts.speed !== undefined) {\n url.searchParams.append('speed', this.opts.speed.toString());\n }\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: `Token ${this.opts.apiKey!}`,\n },\n });\n\n await new Promise((resolve, reject) => {\n ws.on('open', resolve);\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(`WebSocket returned ${code}`));\n });\n\n const inputTask = async () => {\n for await (const data of this.input) {\n if (data === SynthesizeStream.FLUSH_SENTINEL) {\n this.tokenizer.flush();\n continue;\n }\n this.tokenizer.pushText(data);\n }\n this.tokenizer.endInput();\n this.tokenizer.close();\n };\n\n let markInputSent: () => void = () => {};\n const inputSent = new Promise<void>((resolve) => {\n markInputSent = resolve;\n });\n\n const sendTask = async () => {\n try {\n for await (const event of this.tokenizer) {\n if (this.abortController.signal.aborted) break;\n\n let text = event.token;\n if (!text.endsWith(' ')) {\n text += ' ';\n }\n\n const message = JSON.stringify({\n type: 'Speak',\n text: text,\n });\n\n this.markStarted();\n ws.send(message);\n markInputSent();\n }\n\n if (!this.abortController.signal.aborted) {\n ws.send(SynthesizeStream.FLUSH_MSG);\n markInputSent();\n }\n } finally {\n markInputSent();\n }\n };\n\n const recvTask = async () => {\n const bstream = new AudioByteStream(this.opts.sampleRate, NUM_CHANNELS);\n let finalReceived = false;\n let timeout: NodeJS.Timeout | null = null;\n let lastFrame: AudioFrame | undefined;\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n this.queue.put({ requestId, segmentId, frame: lastFrame, final });\n lastFrame = undefined;\n }\n };\n\n const clearMessageTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n const resetMessageTimeout = (reject: (reason?: unknown) => void) => {\n clearMessageTimeout();\n timeout = setTimeout(() => {\n reject(new APITimeoutError({ message: 'Deepgram TTS recv idle timeout' }));\n }, this.connOptions.timeoutMs);\n };\n\n await inputSent;\n if (this.abortController.signal.aborted) return;\n\n return new Promise<void>((resolve, reject) => {\n resetMessageTimeout(reject);\n\n ws.on('message', (data: RawData, isBinary: boolean) => {\n clearMessageTimeout();\n\n if (!isBinary) {\n const message = JSON.parse(data.toString());\n if (message.type === 'Flushed') {\n finalReceived = true;\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n resolve();\n return;\n } else if (message.type === 'Warning') {\n this.#logger.warn(`Deepgram warning: ${message.warn_msg}`);\n } else if (message.type === 'Error' || message.type === 'error') {\n reject(new APIError('Deepgram TTS returned error', { body: message }));\n return;\n } else if (message.type !== 'Metadata') {\n this.#logger.warn({ message }, 'Unknown Deepgram message type');\n }\n\n resetMessageTimeout(reject);\n return;\n }\n\n const buffer =\n data instanceof Buffer\n ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)\n : (data as ArrayBuffer);\n for (const frame of bstream.write(buffer as ArrayBuffer)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n resetMessageTimeout(reject);\n });\n\n ws.on('close', (code, reason) => {\n clearMessageTimeout();\n if (!finalReceived) {\n reject(\n new APIStatusError({\n message: 'Deepgram websocket connection closed unexpectedly',\n options: {\n statusCode: code || -1,\n body: { reason: reason.toString() },\n },\n }),\n );\n return;\n }\n resolve();\n });\n\n ws.on('error', (error) => {\n clearMessageTimeout();\n reject(error);\n });\n });\n };\n\n try {\n await Promise.all([inputTask(), sendTask(), recvTask()]);\n } catch (e) {\n if (this.abortController.signal.aborted) return;\n if (e instanceof APIError) throw e;\n throw new APIConnectionError({\n message: `Deepgram TTS WebSocket failed: ${(e as Error).message ?? 'unknown error'}`,\n });\n } finally {\n await this.closeWebSocket(ws);\n }\n }\n}\n"],"mappings":"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","names":["tts","segmentId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/agents-plugin-deepgram",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Deepgram plugin for LiveKit Agents for Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"require": "dist/index.cjs",
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"README.md"
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@livekit/rtc-node": "^0.13.
|
|
28
|
+
"@livekit/rtc-node": "^0.13.31",
|
|
29
29
|
"@microsoft/api-extractor": "^7.35.0",
|
|
30
30
|
"@types/ws": "^8.5.10",
|
|
31
31
|
"tsup": "^8.3.5",
|
|
32
32
|
"typescript": "^5.0.0",
|
|
33
|
-
"@livekit/agents": "1.5.
|
|
34
|
-
"@livekit/agents-plugin-openai": "^1.5.
|
|
35
|
-
"@livekit/agents-plugin-silero": "1.5.
|
|
36
|
-
"@livekit/agents-plugins-test": "1.5.
|
|
33
|
+
"@livekit/agents": "1.5.1",
|
|
34
|
+
"@livekit/agents-plugin-openai": "^1.5.1",
|
|
35
|
+
"@livekit/agents-plugin-silero": "1.5.1",
|
|
36
|
+
"@livekit/agents-plugins-test": "1.5.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"ws": "^8.18.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@livekit/rtc-node": "^0.13.
|
|
43
|
-
"@livekit/agents": "1.5.
|
|
42
|
+
"@livekit/rtc-node": "^0.13.31",
|
|
43
|
+
"@livekit/agents": "1.5.1"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup --onSuccess \"pnpm build:types\"",
|
package/src/stt.ts
CHANGED
|
@@ -83,6 +83,10 @@ const defaultSTTOptions: STTOptions = {
|
|
|
83
83
|
export class STT extends stt.STT {
|
|
84
84
|
#opts: STTOptions;
|
|
85
85
|
#logger = log();
|
|
86
|
+
// session keyterm propagation)
|
|
87
|
+
#streams = new Set<WeakRef<SpeechStream>>();
|
|
88
|
+
#userKeyterm: string[];
|
|
89
|
+
#sessionKeyterms: string[] = [];
|
|
86
90
|
label = 'deepgram.STT';
|
|
87
91
|
private abortController = new AbortController();
|
|
88
92
|
|
|
@@ -99,6 +103,7 @@ export class STT extends stt.STT {
|
|
|
99
103
|
streaming: true,
|
|
100
104
|
interimResults: opts.interimResults ?? defaultSTTOptions.interimResults,
|
|
101
105
|
alignedTranscript: 'word',
|
|
106
|
+
keyterms: true,
|
|
102
107
|
});
|
|
103
108
|
if (opts.apiKey === undefined && defaultSTTOptions.apiKey === undefined) {
|
|
104
109
|
throw new Error(
|
|
@@ -117,6 +122,8 @@ export class STT extends stt.STT {
|
|
|
117
122
|
} else {
|
|
118
123
|
this.#opts.model = this.#validateModel(this.#opts.model, this.#opts.language);
|
|
119
124
|
}
|
|
125
|
+
|
|
126
|
+
this.#userKeyterm = [...this.#opts.keyterm];
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
async _recognize(buffer: AudioBuffer, abortSignal?: AbortSignal): Promise<stt.SpeechEvent> {
|
|
@@ -170,14 +177,45 @@ export class STT extends stt.STT {
|
|
|
170
177
|
? this.#validateModel(opts.model ?? this.#opts.model, language)
|
|
171
178
|
: this.#opts.model;
|
|
172
179
|
|
|
180
|
+
const nextOpts = { ...opts };
|
|
181
|
+
if (nextOpts.keyterm !== undefined) {
|
|
182
|
+
this.#userKeyterm = [...nextOpts.keyterm];
|
|
183
|
+
nextOpts.keyterm = [...new Set([...this.#userKeyterm, ...this.#sessionKeyterms])];
|
|
184
|
+
}
|
|
185
|
+
|
|
173
186
|
this.#opts = {
|
|
174
187
|
...this.#opts,
|
|
175
|
-
...
|
|
188
|
+
...nextOpts,
|
|
176
189
|
language,
|
|
177
190
|
model,
|
|
178
191
|
};
|
|
179
192
|
}
|
|
180
193
|
|
|
194
|
+
override _updateSessionKeyterms(keyterms: string[]): void {
|
|
195
|
+
if (
|
|
196
|
+
keyterms.length === this.#sessionKeyterms.length &&
|
|
197
|
+
keyterms.every((t, i) => t === this.#sessionKeyterms[i])
|
|
198
|
+
) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.#sessionKeyterms = [...keyterms];
|
|
202
|
+
const merged = [...new Set([...this.#userKeyterm, ...keyterms])];
|
|
203
|
+
this.#opts.keyterm = merged;
|
|
204
|
+
for (const ref of this.#streams) {
|
|
205
|
+
const stream = ref.deref();
|
|
206
|
+
if (!stream) {
|
|
207
|
+
this.#streams.delete(ref);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (stream._speaking) {
|
|
211
|
+
// defer the reconnect to the end of the utterance so we don't cut it off
|
|
212
|
+
stream._pendingKeyterm = merged;
|
|
213
|
+
} else {
|
|
214
|
+
stream.updateOptions({ keyterm: merged });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
#validateModel(model: STTModels | string, language?: string) {
|
|
182
220
|
if (
|
|
183
221
|
language &&
|
|
@@ -211,7 +249,9 @@ export class STT extends stt.STT {
|
|
|
211
249
|
'language detection is not supported in streaming mode, please disable it and specify a language',
|
|
212
250
|
);
|
|
213
251
|
}
|
|
214
|
-
|
|
252
|
+
const stream = new SpeechStream(this, this.#opts, options?.connOptions);
|
|
253
|
+
this.#streams.add(new WeakRef(stream));
|
|
254
|
+
return stream;
|
|
215
255
|
}
|
|
216
256
|
|
|
217
257
|
async close() {
|
|
@@ -226,6 +266,9 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
226
266
|
#speaking = false;
|
|
227
267
|
#resetWS = new Future();
|
|
228
268
|
#requestId = '';
|
|
269
|
+
// keyterms set while the user is speaking; applied at END_OF_SPEECH (latest wins)
|
|
270
|
+
/** @internal */
|
|
271
|
+
_pendingKeyterm: string[] | null = null;
|
|
229
272
|
#audioDurationCollector: PeriodicCollector<number>;
|
|
230
273
|
label = 'deepgram.SpeechStream';
|
|
231
274
|
|
|
@@ -310,11 +353,26 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
310
353
|
this.closed = true;
|
|
311
354
|
}
|
|
312
355
|
|
|
356
|
+
/** @internal */
|
|
357
|
+
get _speaking(): boolean {
|
|
358
|
+
return this.#speaking;
|
|
359
|
+
}
|
|
360
|
+
|
|
313
361
|
updateOptions(opts: Partial<STTOptions>) {
|
|
314
362
|
this.#opts = { ...this.#opts, ...opts };
|
|
363
|
+
if (opts.keyterm !== undefined) {
|
|
364
|
+
this._pendingKeyterm = null;
|
|
365
|
+
}
|
|
315
366
|
this.#resetWS.resolve();
|
|
316
367
|
}
|
|
317
368
|
|
|
369
|
+
#onEndOfSpeech() {
|
|
370
|
+
if (this._pendingKeyterm !== null) {
|
|
371
|
+
this.updateOptions({ keyterm: this._pendingKeyterm });
|
|
372
|
+
this._pendingKeyterm = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
318
376
|
async #runWS(ws: WebSocket) {
|
|
319
377
|
this.#resetWS = new Future();
|
|
320
378
|
let closing = false;
|
|
@@ -465,6 +523,7 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
465
523
|
if (isEndpoint && this.#speaking) {
|
|
466
524
|
this.#speaking = false;
|
|
467
525
|
putMessage({ type: stt.SpeechEventType.END_OF_SPEECH });
|
|
526
|
+
this.#onEndOfSpeech();
|
|
468
527
|
}
|
|
469
528
|
|
|
470
529
|
break;
|
|
@@ -476,6 +535,7 @@ export class SpeechStream extends stt.SpeechStream {
|
|
|
476
535
|
if (this.#speaking) {
|
|
477
536
|
this.#speaking = false;
|
|
478
537
|
putMessage({ type: stt.SpeechEventType.END_OF_SPEECH });
|
|
538
|
+
this.#onEndOfSpeech();
|
|
479
539
|
}
|
|
480
540
|
break;
|
|
481
541
|
}
|