@livekit/agents-plugin-cartesia 1.6.3 → 1.6.4

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/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 ConnectionPool,\n Future,\n type TimedString,\n asError,\n createTimedString,\n getBaseLanguage,\n log,\n normalizeLanguage,\n shortuuid,\n stream,\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 {\n TTSDefaultVoiceId,\n type TTSEncoding,\n type TTSModels,\n type TTSVoiceEmotion,\n type TTSVoiceSpeed,\n isSonic3,\n} from './models.js';\nimport {\n type CartesiaServerMessage,\n cartesiaMessageSchema,\n hasWordTimestamps,\n isChunkMessage,\n isDoneMessage,\n isErrorMessage,\n isFlushDoneMessage,\n} from './types.js';\n\nconst AUTHORIZATION_HEADER = 'X-API-Key';\nconst VERSION_HEADER = 'Cartesia-Version';\nconst API_VERSION = '2025-04-16';\nconst API_VERSION_WITH_EXPERIMENTAL_CONTROLS = '2024-11-13';\nconst MODEL_WITH_EXPERIMENTAL_CONTROLS = 'sonic-2-2025-03-07';\nconst NUM_CHANNELS = 1;\nconst BUFFERED_WORDS_COUNT = 8;\n// Cartesia refreshes a pooled socket after this long so a very long call cannot\n// keep one connection open indefinitely. Matches the Python plugin's 300s.\nconst MAX_SESSION_DURATION_MS = 300_000;\n\n// Lets each SynthesizeStream reach the pool owned by the TTS that created it,\n// without widening the constructor signature the base class fixes.\nconst connectionPools = new WeakMap<TTS, ConnectionPool<WebSocket>>();\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n voice: string | number[];\n speed?: TTSVoiceSpeed | number;\n emotion?: (TTSVoiceEmotion | string)[];\n /**\n * Volume of the speech. For sonic-3, the value is valid between 0.5 and 2.0.\n * @see https://docs.cartesia.ai/api-reference/tts/bytes#body-generation-config-volume\n */\n volume?: number;\n apiKey?: string;\n language: string;\n baseUrl: string;\n apiVersion: string;\n\n /**\n * The timeout for the next chunk to be received from the Cartesia API.\n */\n chunkTimeout: number;\n\n /**\n * Whether to add word timestamps to the output. When enabled, the TTS will return\n * timing information for each word in the transcript.\n * @defaultValue true\n */\n wordTimestamps?: boolean;\n\n pronunciationDictId?: string;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'sonic-3',\n encoding: 'pcm_s16le',\n sampleRate: 24000,\n voice: TTSDefaultVoiceId,\n apiKey: process.env.CARTESIA_API_KEY,\n language: 'en',\n baseUrl: 'https://api.cartesia.ai',\n apiVersion: API_VERSION,\n chunkTimeout: 5000,\n wordTimestamps: true,\n};\n\nconst checkGenerationConfig = (opts: TTSOptions) => {\n const logger = log();\n if (isSonic3(opts.model)) {\n if (opts.speed !== undefined && typeof opts.speed === 'number') {\n if (opts.speed < 0.6 || opts.speed > 2.0) {\n logger.warn('speed must be between 0.6 and 2.0 for sonic-3');\n }\n }\n if (opts.volume !== undefined && (opts.volume < 0.5 || opts.volume > 2.0)) {\n logger.warn('volume must be between 0.5 and 2.0 for sonic-3');\n }\n } else if (\n opts.apiVersion !== API_VERSION_WITH_EXPERIMENTAL_CONTROLS ||\n opts.model !== MODEL_WITH_EXPERIMENTAL_CONTROLS\n ) {\n if (opts.speed || opts.emotion) {\n logger.warn(\n { model: opts.model, speed: opts.speed, emotion: opts.emotion },\n `speed and emotion controls are only supported for model '${MODEL_WITH_EXPERIMENTAL_CONTROLS}' ` +\n `or sonic-3 models, see https://docs.cartesia.ai/developer-tools/changelog for details`,\n );\n }\n }\n\n if (opts.pronunciationDictId && !isSonic3(opts.model)) {\n logger.warn(\n { model: opts.model, pronunciationDictId: opts.pronunciationDictId },\n 'pronunciationDictId is only supported for sonic-3 models',\n );\n }\n};\n\nexport class TTS extends tts.TTS {\n #opts: TTSOptions;\n #pool: ConnectionPool<WebSocket>;\n #closed = false;\n label = 'cartesia.TTS';\n\n get model(): string {\n return this.#opts.model;\n }\n\n get provider(): string {\n return 'Cartesia';\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n const resolvedOpts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n super(resolvedOpts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: true,\n alignedTranscript: resolvedOpts.wordTimestamps ?? true,\n });\n\n this.#opts = resolvedOpts;\n this.#opts.language = normalizeLanguage(this.#opts.language);\n\n if (this.#opts.apiKey === undefined) {\n throw new Error(\n 'Cartesia API key is required, whether as an argument or as $CARTESIA_API_KEY',\n );\n }\n\n if (\n this.#opts.speed ||\n this.#opts.emotion ||\n this.#opts.volume ||\n this.#opts.pronunciationDictId\n ) {\n checkGenerationConfig(this.#opts);\n }\n\n // One socket, reused across generations. Cartesia recommends a single\n // preconnected WebSocket for many generations because a fresh connection\n // repays TCP/TLS setup on every turn:\n // https://docs.cartesia.ai/use-the-api/compare-tts-endpoints\n this.#pool = new ConnectionPool<WebSocket>({\n connectCb: (timeoutMs) => this.#connectWebSocket(timeoutMs),\n closeCb: async (ws) => safeCloseWebSocket(ws),\n maxSessionDuration: MAX_SESSION_DURATION_MS,\n markRefreshedOnGet: true,\n });\n connectionPools.set(this, this.#pool);\n }\n\n updateOptions(opts: Partial<TTSOptions>) {\n // Only these three fields reach Cartesia at WebSocket-handshake time (auth\n // header, version header, host). Everything else (model, voice, encoding,\n // sample rate, speed, emotion, volume, language) is sent in-band on each\n // generation, so a pooled socket serves the new value without reconnecting.\n // Reconnect only when one of the handshake inputs actually changes.\n const handshakeChanged =\n (opts.apiKey !== undefined && opts.apiKey !== this.#opts.apiKey) ||\n (opts.apiVersion !== undefined && opts.apiVersion !== this.#opts.apiVersion) ||\n (opts.baseUrl !== undefined && opts.baseUrl !== this.#opts.baseUrl);\n\n this.#opts = { ...this.#opts, ...opts };\n if (opts.language !== undefined) {\n this.#opts.language = normalizeLanguage(opts.language);\n }\n\n if (\n this.#opts.speed ||\n this.#opts.emotion ||\n this.#opts.volume ||\n this.#opts.pronunciationDictId\n ) {\n checkGenerationConfig(this.#opts);\n }\n\n if (handshakeChanged) {\n this.#pool.invalidate();\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 }): SynthesizeStream {\n return new SynthesizeStream(this, { ...this.#opts }, options?.connOptions);\n }\n\n /**\n * Open the pooled WebSocket ahead of the first generation so the first turn\n * does not pay the connect. Safe to call more than once; it is a no-op when a\n * connection already exists.\n */\n prewarm(): void {\n this.#pool.prewarm();\n }\n\n override async close(): Promise<void> {\n this.#closed = true;\n await this.#pool.close();\n await super.close();\n }\n\n async #connectWebSocket(timeoutMs: number): Promise<WebSocket> {\n // Snapshot the handshake inputs. If a concurrent updateOptions() changes one\n // of them while this connect is in flight, reconnect on the new value rather\n // than pooling a socket built on stale credentials (mirrors the fishaudio\n // plugin's model re-check).\n const apiKey = this.#opts.apiKey!;\n const apiVersion = this.#opts.apiVersion;\n const baseUrl = this.#opts.baseUrl;\n const url = `${baseUrl.replace(/^http/, 'ws')}/tts/websocket`;\n const ws = await connectCartesiaWebSocket({\n url,\n headers: {\n [AUTHORIZATION_HEADER]: apiKey,\n [VERSION_HEADER]: apiVersion,\n },\n timeoutMs,\n });\n if (this.#closed) {\n safeCloseWebSocket(ws);\n throw new APIConnectionError({ message: 'Cartesia TTS is closed' });\n }\n if (\n apiKey !== this.#opts.apiKey ||\n apiVersion !== this.#opts.apiVersion ||\n baseUrl !== this.#opts.baseUrl\n ) {\n safeCloseWebSocket(ws);\n return await this.#connectWebSocket(timeoutMs);\n }\n // Drop a socket that closes (or errors) while idle in the pool. Between turns\n // no generation listeners are attached, so without this the pool keeps a dead\n // socket in `available` and the next turn spends a retry to discard it, or\n // fails outright at maxRetry:0. A generation attaches its own listeners on top\n // of these; the no-op error listener also stops an idle 'error' from crashing\n // the process. Remove is a no-op once the socket is no longer pooled, so this\n // is safe during an active generation and during close().\n ws.on('error', () => {});\n ws.on('close', () => this.#pool.remove(ws));\n return ws;\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'cartesia.ChunkedStream';\n #logger = log();\n #opts: TTSOptions;\n #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 = toCartesiaOptions(this.#opts);\n json.transcript = this.#text;\n\n const baseUrl = new URL(this.#opts.baseUrl);\n const doneFut = new Future<void>();\n\n const req = request(\n {\n hostname: baseUrl.hostname,\n port: parseInt(baseUrl.port) || (baseUrl.protocol === 'https:' ? 443 : 80),\n path: '/tts/bytes',\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: this.#opts.apiKey!,\n [VERSION_HEADER]: this.#opts.apiVersion,\n },\n signal: this.abortSignal,\n },\n (res) => {\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n });\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n this.queue.close();\n if (!doneFut.done) doneFut.resolve();\n });\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Cartesia TTS response error');\n if (!doneFut.done) doneFut.reject(err);\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Cartesia TTS request error');\n if (!doneFut.done) doneFut.reject(err);\n });\n req.on('close', () => {\n if (!doneFut.done) doneFut.resolve();\n });\n req.write(JSON.stringify(json));\n req.end();\n\n try {\n await doneFut.await;\n } catch (e) {\n if (this.abortSignal.aborted) return;\n if (!this.queue.closed) this.queue.close();\n throw toRetryableConnectionError(e);\n }\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n #opts: TTSOptions;\n #pool: ConnectionPool<WebSocket>;\n #logger = log();\n #tokenizer = new tokenize.basic.SentenceTokenizer({\n minSentenceLength: BUFFERED_WORDS_COUNT,\n }).stream();\n label = 'cartesia.SynthesizeStream';\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n const pool = connectionPools.get(tts);\n if (!pool) throw new Error('Cartesia connection pool is not initialized');\n this.#pool = pool;\n this.#opts = opts;\n }\n\n updateOptions(opts: Partial<TTSOptions>) {\n this.#opts = { ...this.#opts, ...opts };\n\n if (\n this.#opts.speed ||\n this.#opts.emotion ||\n this.#opts.volume ||\n this.#opts.pronunciationDictId\n ) {\n checkGenerationConfig(this.#opts);\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n // Only finish the generation once both: 1) Cartesia returns done, AND 2) all sentences have been sent\n let sentenceStreamClosed = false;\n\n const sentenceStreamTask = async (ws: WebSocket) => {\n const packet = toCartesiaOptions(this.#opts, true);\n for await (const event of this.#tokenizer) {\n const msg = {\n ...packet,\n context_id: requestId,\n transcript: event.token + ' ',\n continue: true,\n };\n this.markStarted();\n ws.send(JSON.stringify(msg));\n }\n\n const endMsg = {\n ...packet,\n context_id: requestId,\n transcript: ' ',\n continue: false,\n };\n ws.send(JSON.stringify(endMsg));\n // Mark sentence stream as closed\n sentenceStreamClosed = true;\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 // Use event channel and set up listeners ONCE to avoid missing messages during listener re-registration\n const recvTask = async (ws: WebSocket) => {\n const bstream = new AudioByteStream(this.#opts.sampleRate, NUM_CHANNELS);\n\n // Create event channel to buffer incoming messages\n // This prevents message loss between listener re-registrations\n const eventChannel = stream.createStreamChannel<RawData>();\n\n let lastFrame: AudioFrame | undefined;\n let pendingTimedTranscripts: TimedString[] = [];\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n // Include timedTranscripts with the audio frame\n this.queue.put({\n requestId,\n segmentId,\n frame: lastFrame,\n final,\n timedTranscripts:\n pendingTimedTranscripts.length > 0 ? pendingTimedTranscripts : undefined,\n });\n lastFrame = undefined;\n pendingTimedTranscripts = [];\n }\n };\n\n let timeout: NodeJS.Timeout | null = null;\n // Set when the chunk watchdog fires: the socket is discarded, not pooled.\n let timedOut = false;\n // Set once this generation's `done` has been handled. Until then, a socket\n // close or error is a mid-generation drop, not a normal end.\n let completed = false;\n // A socket close/error before completion. Thrown after the loop so the turn\n // fails over (and the dead socket is discarded) instead of ending silently.\n let streamError: Error | undefined;\n\n const clearTTSChunkTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n // Set up WebSocket listeners ONCE (not in a loop)\n const onMessage = (data: RawData) => {\n void eventChannel.write(data).catch((error: unknown) => {\n this.#logger.debug({ error }, 'Failed writing Cartesia event to channel (likely closed)');\n });\n };\n\n const onClose = (code: number, reason: Buffer) => {\n // A close during an active generation is unexpected: the pool owns the\n // socket lifecycle and does not close it between turns. If it happens\n // before `done`, surface it so the turn retries rather than ending mid\n // speech, and so withConnection discards the dead socket.\n this.#logger.debug(`WebSocket closed with code ${code}: ${reason.toString()}`);\n clearTTSChunkTimeout();\n if (!completed && !timedOut && !streamError) {\n streamError = new APIConnectionError({\n message: `Cartesia WebSocket closed mid-generation (code=${code})`,\n });\n }\n void eventChannel.close();\n };\n\n const onError = (err: Error) => {\n this.#logger.error({ err }, 'Cartesia WebSocket error');\n if (!completed && !timedOut && !streamError) {\n streamError = err instanceof APIError ? err : toRetryableConnectionError(err);\n }\n void eventChannel.close();\n };\n\n // Attach listeners ONCE\n ws.on('message', onMessage);\n ws.on('close', onClose);\n ws.on('error', onError);\n\n try {\n // Process messages from the channel\n const reader = eventChannel.stream().getReader();\n\n while (!this.closed && !this.abortController.signal.aborted) {\n const result = await reader.read();\n if (result.done) break;\n\n const rawMsg = result.value;\n\n // Parse message with Zod schema for type safety\n let serverMsg: CartesiaServerMessage;\n try {\n const json = JSON.parse(rawMsg.toString());\n serverMsg = cartesiaMessageSchema.parse(json);\n } catch (parseErr) {\n this.#logger.warn({ parseErr }, 'Failed to parse Cartesia message');\n continue;\n }\n\n const segmentId = serverMsg.context_id;\n\n // Handle error frames first. 4xx (e.g. empty-transcript on\n // function-call turns) is non-fatal — log and fall through so an\n // accompanying done:true still triggers the unified close path\n // below. 5xx bubbles up so the base SynthesizeStream can retry.\n if (isErrorMessage(serverMsg)) {\n if (serverMsg.status_code >= 400 && serverMsg.status_code < 500) {\n this.#logger.debug({ error: serverMsg.error }, 'Cartesia sent a non-fatal error');\n } else {\n this.#logger.error({ error: serverMsg.error }, 'Cartesia returned error');\n throw new APIStatusError({\n message: `Cartesia returned error: ${serverMsg.error}`,\n options: { statusCode: serverMsg.status_code, retryable: true },\n });\n }\n }\n\n if (isChunkMessage(serverMsg)) {\n const audioBuffer = Buffer.from(serverMsg.data, 'base64');\n // Extract ArrayBuffer from Buffer for AudioByteStream compatibility\n const audioData = audioBuffer.buffer.slice(\n audioBuffer.byteOffset,\n audioBuffer.byteOffset + audioBuffer.byteLength,\n );\n for (const frame of bstream.write(audioData)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n\n // IMPORTANT: close WS if TTS chunk stream been stuck too long\n // this allows unblock the current \"broken\" TTS node so that any future TTS nodes\n // can continue to process the stream without been blocked by the stuck node\n clearTTSChunkTimeout();\n timeout = setTimeout(() => {\n // cartesia chunk timeout quite often, so we make it a debug log\n this.#logger.debug(\n `Cartesia WebSocket TTS chunk stream timeout after ${this.#opts.chunkTimeout}ms`,\n );\n // The socket is stuck mid-generation, so it must not return to the\n // pool. Poison it and unblock the reader; the post-loop check turns\n // this into a retryable error so withConnection discards the socket.\n timedOut = true;\n safeCloseWebSocket(ws);\n void eventChannel.close();\n }, this.#opts.chunkTimeout);\n } else if (this.#opts.wordTimestamps !== false && hasWordTimestamps(serverMsg)) {\n const wordTimestamps = serverMsg.word_timestamps;\n for (let i = 0; i < wordTimestamps.words.length; i++) {\n const word = wordTimestamps.words[i];\n const startTime = wordTimestamps.start[i];\n const endTime = wordTimestamps.end[i];\n if (word !== undefined && startTime !== undefined && endTime !== undefined) {\n pendingTimedTranscripts.push(\n createTimedString({\n text: word + ' ', // Add space after word for consistency\n startTime,\n endTime,\n }),\n );\n }\n }\n } else if (isDoneMessage(serverMsg) || (isErrorMessage(serverMsg) && serverMsg.done)) {\n // This ensures all sentences have been sent before closing\n if (sentenceStreamClosed) {\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n\n if (segmentId === requestId) {\n clearTTSChunkTimeout();\n completed = true;\n // Leave the socket open so the pool reuses it on the next turn.\n break; // Exit the loop\n }\n }\n // If sentenceStreamClosed is false, continue receiving - more done messages will come\n } else if (!isFlushDoneMessage(serverMsg) && !isErrorMessage(serverMsg)) {\n // flush_done is an ack with nothing to do; error frames without\n // done:true were already logged above.\n this.#logger.warn({ message: serverMsg }, 'Unknown Cartesia message');\n }\n }\n\n if (timedOut) {\n throw new APITimeoutError({\n message: `Cartesia TTS chunk stream timed out after ${this.#opts.chunkTimeout}ms`,\n });\n }\n if (streamError) {\n throw streamError;\n }\n } catch (err) {\n // Always propagate API errors so the base SynthesizeStream can retry\n // and emit tts_error once retries are exhausted.\n if (err instanceof APIError) throw err;\n // skip log error for normal websocket close\n if (err instanceof Error && !err.message.includes('WebSocket closed')) {\n if (\n err.message.includes('Queue is closed') ||\n err.message.includes('Channel is closed')\n ) {\n this.#logger.warn(\n { err },\n 'Channel closed during transcript processing (expected during disconnect)',\n );\n } else {\n this.#logger.error({ err }, 'Error in recvTask from Cartesia WebSocket');\n }\n }\n } finally {\n // IMPORTANT: Remove listeners so connection can be reused\n ws.off('message', onMessage);\n ws.off('close', onClose);\n ws.off('error', onError);\n clearTTSChunkTimeout();\n }\n };\n\n try {\n // The pool hands back one live socket per call and reclaims it on success\n // (put) or discards it on any thrown error (remove). A generation never\n // closes the socket itself, so the next turn skips the handshake.\n await this.#pool.withConnection(\n async (ws) => {\n if (ws.readyState !== WebSocket.OPEN) {\n throw new APIConnectionError({ message: 'Cartesia pooled websocket is not open' });\n }\n await Promise.all([inputTask(), sentenceStreamTask(ws), recvTask(ws)]);\n },\n { timeout: this.connOptions.timeoutMs, signal: this.abortSignal },\n );\n } catch (e) {\n if (this.abortSignal.aborted) {\n return;\n }\n if (e instanceof APIError) throw e;\n throw toRetryableConnectionError(e);\n }\n }\n}\n\nconst transientNetworkCodes = new Set([\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'EAI_AGAIN',\n 'ENETUNREACH',\n 'ECONNREFUSED',\n 'EHOSTUNREACH',\n]);\n\nconst isRecord = (v: unknown): v is Record<string, unknown> => {\n return v !== null && typeof v === 'object';\n};\n\nconst isAggregateErrorLike = (e: unknown): e is { errors: unknown[]; name?: string } => {\n if (!isRecord(e)) return false;\n return e.name === 'AggregateError' && Array.isArray(e.errors);\n};\n\nconst hasErrorCode = (e: unknown, code: string): boolean => {\n if (isRecord(e) && e.code === code) return true;\n if (isAggregateErrorLike(e)) {\n return e.errors.some((inner) => hasErrorCode(inner, code));\n }\n return false;\n};\n\nconst hasAnyTransientCode = (e: unknown): boolean => {\n if (isRecord(e) && typeof e.code === 'string') {\n return transientNetworkCodes.has(e.code);\n }\n if (isAggregateErrorLike(e)) {\n return e.errors.some((inner) => hasAnyTransientCode(inner));\n }\n return false;\n};\n\nconst toRetryableConnectionError = (e: unknown): APIConnectionError => {\n const err = asError(e);\n const isTimeout =\n hasErrorCode(e, 'ETIMEDOUT') ||\n (typeof err.message === 'string' && err.message.includes('ETIMEDOUT'));\n const message = isTimeout\n ? `Cartesia connection timed out`\n : `Cartesia connection failed: ${err.message || 'unknown error'}`;\n return isTimeout ? new APITimeoutError({ message }) : new APIConnectionError({ message });\n};\n\nconst waitForWsOpen = async ({\n ws,\n timeoutMs,\n abortSignal,\n}: {\n ws: WebSocket;\n timeoutMs: number;\n abortSignal?: AbortSignal;\n}) => {\n if (abortSignal?.aborted) {\n throw new Error('aborted');\n }\n\n const fut = new Future<void>();\n let timeout: NodeJS.Timeout | undefined;\n\n const cleanup = () => {\n if (timeout) clearTimeout(timeout);\n ws.off('open', onOpen);\n ws.off('error', onError);\n ws.off('close', onClose);\n abortSignal?.removeEventListener('abort', onAbort);\n };\n\n const onOpen = () => fut.resolve();\n const onError = (err: Error) => fut.reject(asError(err));\n const onClose = (code: number, reason: Buffer) =>\n fut.reject(\n new Error(`WebSocket closed before open (code=${code}, reason=${reason.toString()})`),\n );\n const onAbort = () => fut.reject(new Error('aborted'));\n\n ws.on('open', onOpen);\n ws.on('error', onError);\n ws.on('close', onClose);\n abortSignal?.addEventListener('abort', onAbort, { once: true });\n\n if (timeoutMs > 0) {\n timeout = setTimeout(() => fut.reject(new Error('connect timeout')), timeoutMs);\n }\n\n try {\n await fut.await;\n } finally {\n cleanup();\n }\n};\n\nconst safeTerminateWebSocket = (ws: WebSocket) => {\n // `ws` can emit an 'error' event during teardown (especially if CONNECTING).\n // If there is no error listener at that moment, Node will treat it as unhandled and crash the process.\n try {\n ws.on('error', () => {});\n } catch {\n // ignore\n }\n\n try {\n // `terminate()` can throw if the socket was never established; `close()` is safer in CONNECTING.\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n } else {\n ws.terminate();\n }\n } catch {\n // ignore\n }\n};\n\n// Graceful close used by the connection pool. A pooled socket is healthy when it\n// is retired (session age, option change, or TTS close), so a clean close frame\n// is preferable to an abrupt terminate; terminate remains the fallback for a\n// socket caught mid-handshake.\nconst safeCloseWebSocket = (ws: WebSocket) => {\n try {\n // `ws` can emit 'error' during teardown; without a listener Node treats it as\n // unhandled and crashes the process.\n ws.on('error', () => {});\n } catch {\n // ignore\n }\n\n try {\n if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) {\n ws.close();\n } else if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {\n ws.terminate();\n }\n } catch {\n // ignore\n }\n};\n\nconst connectCartesiaWebSocket = async ({\n url,\n headers,\n timeoutMs,\n abortSignal,\n}: {\n url: string;\n headers: Record<string, string>;\n timeoutMs: number;\n abortSignal?: AbortSignal;\n}): Promise<WebSocket> => {\n const connectOnce = async (family?: number): Promise<WebSocket> => {\n const ws = new WebSocket(url, { handshakeTimeout: timeoutMs, family, headers });\n try {\n await waitForWsOpen({ ws, timeoutMs, abortSignal });\n return ws;\n } catch (e) {\n safeTerminateWebSocket(ws);\n throw e;\n }\n };\n\n try {\n return await connectOnce();\n } catch (e) {\n // Mitigation for Node.js dual-stack (IPv6/IPv4) connect flakiness (\"happy eyeballs\"):\n // some environments surface `AggregateError` with nested `ETIMEDOUT` during the initial\n // WebSocket open. In that case we do a one-off retry forcing IPv4 (`family: 4`) before\n // letting the outer framework retry loop handle further attempts.\n //\n // If you still see `AggregateError`/`ETIMEDOUT`:\n // - Increase the session TTS connect timeout (`connOptions.ttsConnOptions.timeoutMs`)\n // - Or adjust Node's family autoselection behavior via `NODE_OPTIONS`, e.g.\n // `--network-family-autoselection-attempt-timeout=5000` (or disable it entirely).\n if (hasAnyTransientCode(e) || isAggregateErrorLike(e)) {\n return await connectOnce(4);\n }\n throw e;\n }\n};\n\nconst toCartesiaOptions = (\n opts: TTSOptions,\n streaming: boolean = false,\n): { [id: string]: unknown } => {\n const voice: { [id: string]: unknown } = {};\n if (typeof opts.voice === 'string') {\n voice.mode = 'id';\n voice.id = opts.voice;\n } else {\n voice.mode = 'embedding';\n voice.embedding = opts.voice;\n }\n\n if (opts.apiVersion === API_VERSION_WITH_EXPERIMENTAL_CONTROLS) {\n const voiceControls: { [id: string]: unknown } = {};\n if (opts.speed) {\n voiceControls.speed = opts.speed;\n }\n if (opts.emotion) {\n voiceControls.emotion = opts.emotion;\n }\n if (Object.keys(voiceControls).length) {\n voice.__experimental_controls = voiceControls;\n }\n }\n\n const result: { [id: string]: unknown } = {\n model_id: opts.model,\n voice,\n output_format: {\n container: 'raw',\n encoding: opts.encoding,\n sample_rate: opts.sampleRate,\n },\n language: getBaseLanguage(opts.language),\n max_buffer_delay_ms: 0,\n };\n\n if (opts.pronunciationDictId) {\n result.pronunciation_dict_id = opts.pronunciationDictId;\n }\n\n if (opts.apiVersion > API_VERSION_WITH_EXPERIMENTAL_CONTROLS && isSonic3(opts.model)) {\n const generationConfig: { [id: string]: unknown } = {};\n if (opts.speed) {\n generationConfig.speed = opts.speed;\n }\n if (opts.emotion) {\n generationConfig.emotion = opts.emotion[0];\n }\n if (opts.volume) {\n generationConfig.volume = opts.volume;\n }\n if (Object.keys(generationConfig).length) {\n result.generation_config = generationConfig;\n }\n }\n\n if (streaming && opts.wordTimestamps !== false) {\n result.add_timestamps = true;\n }\n\n return result;\n};\n"],"mappings":"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","names":["tts"]}
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 ConnectionPool,\n Future,\n type TimedString,\n asError,\n createTimedString,\n getBaseLanguage,\n log,\n normalizeLanguage,\n shortuuid,\n stream,\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 {\n TTSDefaultVoiceId,\n type TTSEncoding,\n type TTSModels,\n type TTSVoiceEmotion,\n type TTSVoiceSpeed,\n isSonic3,\n} from './models.js';\nimport {\n type CartesiaServerMessage,\n cartesiaMessageSchema,\n hasWordTimestamps,\n isChunkMessage,\n isDoneMessage,\n isErrorMessage,\n isFlushDoneMessage,\n} from './types.js';\n\nconst AUTHORIZATION_HEADER = 'X-API-Key';\nconst VERSION_HEADER = 'Cartesia-Version';\nconst API_VERSION = '2025-04-16';\nconst API_VERSION_WITH_EXPERIMENTAL_CONTROLS = '2024-11-13';\nconst MODEL_WITH_EXPERIMENTAL_CONTROLS = 'sonic-2-2025-03-07';\nconst NUM_CHANNELS = 1;\nconst BUFFERED_WORDS_COUNT = 8;\n// Cartesia refreshes a pooled socket after this long so a very long call cannot\n// keep one connection open indefinitely. Matches the Python plugin's 300s.\nconst MAX_SESSION_DURATION_MS = 300_000;\n\n// Lets each SynthesizeStream reach the pool owned by the TTS that created it,\n// without widening the constructor signature the base class fixes.\nconst connectionPools = new WeakMap<TTS, ConnectionPool<WebSocket>>();\n\nexport interface TTSOptions {\n model: TTSModels | string;\n encoding: TTSEncoding;\n sampleRate: number;\n voice: string | number[];\n speed?: TTSVoiceSpeed | number;\n emotion?: (TTSVoiceEmotion | string)[];\n /**\n * Volume of the speech. For sonic-3, the value is valid between 0.5 and 2.0.\n * @see https://docs.cartesia.ai/api-reference/tts/bytes#body-generation-config-volume\n */\n volume?: number;\n apiKey?: string;\n language: string;\n baseUrl: string;\n apiVersion: string;\n\n /**\n * The timeout for the next chunk to be received from the Cartesia API.\n */\n chunkTimeout: number;\n\n /**\n * Whether to add word timestamps to the output. When enabled, the TTS will return\n * timing information for each word in the transcript.\n * @defaultValue true\n */\n wordTimestamps?: boolean;\n\n pronunciationDictId?: string;\n}\n\nconst defaultTTSOptions: TTSOptions = {\n model: 'sonic-3',\n encoding: 'pcm_s16le',\n sampleRate: 24000,\n voice: TTSDefaultVoiceId,\n apiKey: process.env.CARTESIA_API_KEY,\n language: 'en',\n baseUrl: 'https://api.cartesia.ai',\n apiVersion: API_VERSION,\n chunkTimeout: 5000,\n wordTimestamps: true,\n};\n\nconst checkGenerationConfig = (opts: TTSOptions) => {\n const logger = log();\n if (isSonic3(opts.model)) {\n if (opts.speed !== undefined && typeof opts.speed === 'number') {\n if (opts.speed < 0.6 || opts.speed > 2.0) {\n logger.warn('speed must be between 0.6 and 2.0 for sonic-3');\n }\n }\n if (opts.volume !== undefined && (opts.volume < 0.5 || opts.volume > 2.0)) {\n logger.warn('volume must be between 0.5 and 2.0 for sonic-3');\n }\n } else if (\n opts.apiVersion !== API_VERSION_WITH_EXPERIMENTAL_CONTROLS ||\n opts.model !== MODEL_WITH_EXPERIMENTAL_CONTROLS\n ) {\n if (opts.speed || opts.emotion) {\n logger.warn(\n { model: opts.model, speed: opts.speed, emotion: opts.emotion },\n `speed and emotion controls are only supported for model '${MODEL_WITH_EXPERIMENTAL_CONTROLS}' ` +\n `or sonic-3 models, see https://docs.cartesia.ai/developer-tools/changelog for details`,\n );\n }\n }\n\n if (opts.pronunciationDictId && !isSonic3(opts.model)) {\n logger.warn(\n { model: opts.model, pronunciationDictId: opts.pronunciationDictId },\n 'pronunciationDictId is only supported for sonic-3 models',\n );\n }\n};\n\nexport class TTS extends tts.TTS {\n #opts: TTSOptions;\n #pool: ConnectionPool<WebSocket>;\n #closed = false;\n label = 'cartesia.TTS';\n\n get model(): string {\n return this.#opts.model;\n }\n\n get provider(): string {\n return 'Cartesia';\n }\n\n constructor(opts: Partial<TTSOptions> = {}) {\n const resolvedOpts = {\n ...defaultTTSOptions,\n ...opts,\n };\n\n super(resolvedOpts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, {\n streaming: true,\n alignedTranscript: resolvedOpts.wordTimestamps ?? true,\n });\n\n this.#opts = resolvedOpts;\n this.#opts.language = normalizeLanguage(this.#opts.language);\n\n if (this.#opts.apiKey === undefined) {\n throw new Error(\n 'Cartesia API key is required, whether as an argument or as $CARTESIA_API_KEY',\n );\n }\n\n if (\n this.#opts.speed ||\n this.#opts.emotion ||\n this.#opts.volume ||\n this.#opts.pronunciationDictId\n ) {\n checkGenerationConfig(this.#opts);\n }\n\n // One socket, reused across generations. Cartesia recommends a single\n // preconnected WebSocket for many generations because a fresh connection\n // repays TCP/TLS setup on every turn:\n // https://docs.cartesia.ai/use-the-api/compare-tts-endpoints\n this.#pool = new ConnectionPool<WebSocket>({\n connectCb: (timeoutMs) => this.#connectWebSocket(timeoutMs),\n closeCb: async (ws) => safeCloseWebSocket(ws),\n maxSessionDuration: MAX_SESSION_DURATION_MS,\n markRefreshedOnGet: true,\n });\n connectionPools.set(this, this.#pool);\n }\n\n updateOptions(opts: Partial<TTSOptions>) {\n // Only these three fields reach Cartesia at WebSocket-handshake time (auth\n // header, version header, host). Everything else (model, voice, encoding,\n // sample rate, speed, emotion, volume, language) is sent in-band on each\n // generation, so a pooled socket serves the new value without reconnecting.\n // Reconnect only when one of the handshake inputs actually changes.\n const handshakeChanged =\n (opts.apiKey !== undefined && opts.apiKey !== this.#opts.apiKey) ||\n (opts.apiVersion !== undefined && opts.apiVersion !== this.#opts.apiVersion) ||\n (opts.baseUrl !== undefined && opts.baseUrl !== this.#opts.baseUrl);\n\n this.#opts = { ...this.#opts, ...opts };\n if (opts.language !== undefined) {\n this.#opts.language = normalizeLanguage(opts.language);\n }\n\n if (\n this.#opts.speed ||\n this.#opts.emotion ||\n this.#opts.volume ||\n this.#opts.pronunciationDictId\n ) {\n checkGenerationConfig(this.#opts);\n }\n\n if (handshakeChanged) {\n this.#pool.invalidate();\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 }): SynthesizeStream {\n return new SynthesizeStream(this, { ...this.#opts }, options?.connOptions);\n }\n\n /**\n * Open the pooled WebSocket ahead of the first generation so the first turn\n * does not pay the connect. Safe to call more than once; it is a no-op when a\n * connection already exists.\n */\n prewarm(): void {\n this.#pool.prewarm();\n }\n\n override async close(): Promise<void> {\n this.#closed = true;\n await this.#pool.close();\n await super.close();\n }\n\n async #connectWebSocket(timeoutMs: number): Promise<WebSocket> {\n // Snapshot the handshake inputs. If a concurrent updateOptions() changes one\n // of them while this connect is in flight, reconnect on the new value rather\n // than pooling a socket built on stale credentials (mirrors the fishaudio\n // plugin's model re-check).\n const apiKey = this.#opts.apiKey!;\n const apiVersion = this.#opts.apiVersion;\n const baseUrl = this.#opts.baseUrl;\n const url = `${baseUrl.replace(/^http/, 'ws')}/tts/websocket`;\n const ws = await connectCartesiaWebSocket({\n url,\n headers: {\n [AUTHORIZATION_HEADER]: apiKey,\n [VERSION_HEADER]: apiVersion,\n },\n timeoutMs,\n });\n if (this.#closed) {\n safeCloseWebSocket(ws);\n throw new APIConnectionError({ message: 'Cartesia TTS is closed' });\n }\n if (\n apiKey !== this.#opts.apiKey ||\n apiVersion !== this.#opts.apiVersion ||\n baseUrl !== this.#opts.baseUrl\n ) {\n safeCloseWebSocket(ws);\n return await this.#connectWebSocket(timeoutMs);\n }\n // Drop a socket that closes (or errors) while idle in the pool. Between turns\n // no generation listeners are attached, so without this the pool keeps a dead\n // socket in `available` and the next turn spends a retry to discard it, or\n // fails outright at maxRetry:0. A generation attaches its own listeners on top\n // of these; the no-op error listener also stops an idle 'error' from crashing\n // the process. Remove is a no-op once the socket is no longer pooled, so this\n // is safe during an active generation and during close().\n ws.on('error', () => {});\n ws.on('close', () => this.#pool.remove(ws));\n return ws;\n }\n}\n\nexport class ChunkedStream extends tts.ChunkedStream {\n label = 'cartesia.ChunkedStream';\n #logger = log();\n #opts: TTSOptions;\n #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 = toCartesiaOptions(this.#opts);\n json.transcript = this.#text;\n\n const baseUrl = new URL(this.#opts.baseUrl);\n const doneFut = new Future<void>();\n\n const req = request(\n {\n hostname: baseUrl.hostname,\n port: parseInt(baseUrl.port) || (baseUrl.protocol === 'https:' ? 443 : 80),\n path: '/tts/bytes',\n method: 'POST',\n headers: {\n [AUTHORIZATION_HEADER]: this.#opts.apiKey!,\n [VERSION_HEADER]: this.#opts.apiVersion,\n },\n signal: this.abortSignal,\n },\n (res) => {\n res.on('data', (chunk) => {\n for (const frame of bstream.write(chunk)) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n });\n res.on('close', () => {\n for (const frame of bstream.flush()) {\n this.queue.put({\n requestId,\n frame,\n final: false,\n segmentId: requestId,\n });\n }\n this.queue.close();\n if (!doneFut.done) doneFut.resolve();\n });\n res.on('error', (err) => {\n if (err.message === 'aborted') return;\n this.#logger.error({ err }, 'Cartesia TTS response error');\n if (!doneFut.done) doneFut.reject(err);\n });\n },\n );\n\n req.on('error', (err) => {\n if (err.name === 'AbortError') return;\n this.#logger.error({ err }, 'Cartesia TTS request error');\n if (!doneFut.done) doneFut.reject(err);\n });\n req.on('close', () => {\n if (!doneFut.done) doneFut.resolve();\n });\n req.write(JSON.stringify(json));\n req.end();\n\n try {\n await doneFut.await;\n } catch (e) {\n if (this.abortSignal.aborted) return;\n if (!this.queue.closed) this.queue.close();\n throw toRetryableConnectionError(e);\n }\n }\n}\n\nexport class SynthesizeStream extends tts.SynthesizeStream {\n #opts: TTSOptions;\n #pool: ConnectionPool<WebSocket>;\n #logger = log();\n #tokenizer = new tokenize.basic.SentenceTokenizer({\n minSentenceLength: BUFFERED_WORDS_COUNT,\n }).stream();\n label = 'cartesia.SynthesizeStream';\n\n constructor(tts: TTS, opts: TTSOptions, connOptions?: APIConnectOptions) {\n super(tts, connOptions);\n const pool = connectionPools.get(tts);\n if (!pool) throw new Error('Cartesia connection pool is not initialized');\n this.#pool = pool;\n this.#opts = opts;\n }\n\n updateOptions(opts: Partial<TTSOptions>) {\n this.#opts = { ...this.#opts, ...opts };\n\n if (\n this.#opts.speed ||\n this.#opts.emotion ||\n this.#opts.volume ||\n this.#opts.pronunciationDictId\n ) {\n checkGenerationConfig(this.#opts);\n }\n }\n\n protected async run() {\n const requestId = shortuuid();\n // Only finish the generation once both: 1) Cartesia returns done, AND 2) all sentences have been sent\n let sentenceStreamClosed = false;\n\n const sentenceStreamTask = async (ws: WebSocket) => {\n const packet = toCartesiaOptions(this.#opts, true);\n for await (const event of this.#tokenizer) {\n const msg = {\n ...packet,\n context_id: requestId,\n transcript: event.token + ' ',\n continue: true,\n };\n this.markStarted();\n ws.send(JSON.stringify(msg));\n }\n\n const endMsg = {\n ...packet,\n context_id: requestId,\n transcript: ' ',\n continue: false,\n };\n ws.send(JSON.stringify(endMsg));\n // Mark sentence stream as closed\n sentenceStreamClosed = true;\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 // Use event channel and set up listeners ONCE to avoid missing messages during listener re-registration\n const recvTask = async (ws: WebSocket) => {\n const bstream = new AudioByteStream(this.#opts.sampleRate, NUM_CHANNELS);\n\n // Create event channel to buffer incoming messages\n // This prevents message loss between listener re-registrations\n const eventChannel = stream.createStreamChannel<RawData>();\n\n let lastFrame: AudioFrame | undefined;\n let pendingTimedTranscripts: TimedString[] = [];\n\n const sendLastFrame = (segmentId: string, final: boolean) => {\n if (lastFrame && !this.queue.closed) {\n // Include timedTranscripts with the audio frame\n this.queue.put({\n requestId,\n segmentId,\n frame: lastFrame,\n final,\n timedTranscripts:\n pendingTimedTranscripts.length > 0 ? pendingTimedTranscripts : undefined,\n });\n lastFrame = undefined;\n pendingTimedTranscripts = [];\n }\n };\n\n let timeout: NodeJS.Timeout | null = null;\n // Set when the chunk watchdog fires: the socket is discarded, not pooled.\n let timedOut = false;\n // Set once this generation's `done` has been handled. Until then, a socket\n // close or error is a mid-generation drop, not a normal end.\n let completed = false;\n // A socket close/error before completion. Thrown after the loop so the turn\n // fails over (and the dead socket is discarded) instead of ending silently.\n let streamError: Error | undefined;\n\n const clearTTSChunkTimeout = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n // Set up WebSocket listeners ONCE (not in a loop)\n const onMessage = (data: RawData) => {\n void eventChannel.write(data).catch((error: unknown) => {\n this.#logger.debug({ error }, 'Failed writing Cartesia event to channel (likely closed)');\n });\n };\n\n const onClose = (code: number, reason: Buffer) => {\n // A close during an active generation is unexpected: the pool owns the\n // socket lifecycle and does not close it between turns. If it happens\n // before `done`, surface it so the turn retries rather than ending mid\n // speech, and so withConnection discards the dead socket.\n this.#logger.debug(`WebSocket closed with code ${code}: ${reason.toString()}`);\n clearTTSChunkTimeout();\n if (!completed && !timedOut && !streamError) {\n streamError = new APIConnectionError({\n message: `Cartesia WebSocket closed mid-generation (code=${code})`,\n });\n }\n void eventChannel.close();\n };\n\n const onError = (err: Error) => {\n this.#logger.error({ err }, 'Cartesia WebSocket error');\n if (!completed && !timedOut && !streamError) {\n streamError = err instanceof APIError ? err : toRetryableConnectionError(err);\n }\n void eventChannel.close();\n };\n\n // Attach listeners ONCE\n ws.on('message', onMessage);\n ws.on('close', onClose);\n ws.on('error', onError);\n\n try {\n // Process messages from the channel\n const reader = eventChannel.stream().getReader();\n\n while (!this.closed && !this.abortController.signal.aborted) {\n const result = await reader.read();\n if (result.done) break;\n\n const rawMsg = result.value;\n\n // Parse message with Zod schema for type safety\n let serverMsg: CartesiaServerMessage;\n try {\n const json = JSON.parse(rawMsg.toString());\n serverMsg = cartesiaMessageSchema.parse(json);\n } catch (parseErr) {\n this.#logger.warn({ parseErr }, 'Failed to parse Cartesia message');\n continue;\n }\n\n const segmentId = serverMsg.context_id;\n\n // Handle error frames first. 4xx (e.g. empty-transcript on\n // function-call turns) is non-fatal — log and fall through so an\n // accompanying done:true still triggers the unified close path\n // below. 5xx bubbles up so the base SynthesizeStream can retry.\n if (isErrorMessage(serverMsg)) {\n if (serverMsg.status_code >= 400 && serverMsg.status_code < 500) {\n this.#logger.debug({ error: serverMsg.error }, 'Cartesia sent a non-fatal error');\n } else {\n this.#logger.error({ error: serverMsg.error }, 'Cartesia returned error');\n throw new APIStatusError({\n message: `Cartesia returned error: ${serverMsg.error}`,\n options: { statusCode: serverMsg.status_code, retryable: true },\n });\n }\n }\n\n if (isChunkMessage(serverMsg)) {\n const audioBuffer = Buffer.from(serverMsg.data, 'base64');\n // Extract ArrayBuffer from Buffer for AudioByteStream compatibility\n const audioData = audioBuffer.buffer.slice(\n audioBuffer.byteOffset,\n audioBuffer.byteOffset + audioBuffer.byteLength,\n );\n for (const frame of bstream.write(audioData)) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n\n // IMPORTANT: close WS if TTS chunk stream been stuck too long\n // this allows unblock the current \"broken\" TTS node so that any future TTS nodes\n // can continue to process the stream without been blocked by the stuck node\n clearTTSChunkTimeout();\n timeout = setTimeout(() => {\n // cartesia chunk timeout quite often, so we make it a debug log\n this.#logger.debug(\n `Cartesia WebSocket TTS chunk stream timeout after ${this.#opts.chunkTimeout}ms`,\n );\n // The socket is stuck mid-generation, so it must not return to the\n // pool. Poison it and unblock the reader; the post-loop check turns\n // this into a retryable error so withConnection discards the socket.\n timedOut = true;\n safeCloseWebSocket(ws);\n void eventChannel.close();\n }, this.#opts.chunkTimeout);\n } else if (this.#opts.wordTimestamps !== false && hasWordTimestamps(serverMsg)) {\n const wordTimestamps = serverMsg.word_timestamps;\n for (let i = 0; i < wordTimestamps.words.length; i++) {\n const word = wordTimestamps.words[i];\n const startTime = wordTimestamps.start[i];\n const endTime = wordTimestamps.end[i];\n if (word !== undefined && startTime !== undefined && endTime !== undefined) {\n pendingTimedTranscripts.push(\n createTimedString({\n text: word + ' ', // Add space after word for consistency\n startTime,\n endTime,\n }),\n );\n }\n }\n } else if (isDoneMessage(serverMsg) || (isErrorMessage(serverMsg) && serverMsg.done)) {\n // This ensures all sentences have been sent before closing\n if (sentenceStreamClosed) {\n for (const frame of bstream.flush()) {\n sendLastFrame(segmentId, false);\n lastFrame = frame;\n }\n sendLastFrame(segmentId, true);\n if (!this.queue.closed) {\n this.queue.put(SynthesizeStream.END_OF_STREAM);\n }\n\n if (segmentId === requestId) {\n clearTTSChunkTimeout();\n completed = true;\n // Leave the socket open so the pool reuses it on the next turn.\n break; // Exit the loop\n }\n }\n // If sentenceStreamClosed is false, continue receiving - more done messages will come\n } else if (!isFlushDoneMessage(serverMsg) && !isErrorMessage(serverMsg)) {\n // flush_done is an ack with nothing to do; error frames without\n // done:true were already logged above.\n this.#logger.warn({ message: serverMsg }, 'Unknown Cartesia message');\n }\n }\n\n if (timedOut) {\n throw new APITimeoutError({\n message: `Cartesia TTS chunk stream timed out after ${this.#opts.chunkTimeout}ms`,\n });\n }\n if (streamError) {\n throw streamError;\n }\n } catch (err) {\n // Always propagate API errors so the base SynthesizeStream can retry\n // and emit tts_error once retries are exhausted.\n if (err instanceof APIError) throw err;\n // skip log error for normal websocket close\n if (err instanceof Error && !err.message.includes('WebSocket closed')) {\n if (\n err.message.includes('Queue is closed') ||\n err.message.includes('Channel is closed')\n ) {\n this.#logger.warn(\n { err },\n 'Channel closed during transcript processing (expected during disconnect)',\n );\n } else {\n this.#logger.error({ err }, 'Error in recvTask from Cartesia WebSocket');\n }\n }\n } finally {\n // IMPORTANT: Remove listeners so connection can be reused\n ws.off('message', onMessage);\n ws.off('close', onClose);\n ws.off('error', onError);\n clearTTSChunkTimeout();\n }\n };\n\n try {\n // The pool hands back one live socket per call and reclaims it on success\n // (put) or discards it on any thrown error (remove). A generation never\n // closes the socket itself, so the next turn skips the handshake.\n await this.#pool.withConnection(\n async (ws) => {\n if (ws.readyState !== WebSocket.OPEN) {\n throw new APIConnectionError({ message: 'Cartesia pooled websocket is not open' });\n }\n await Promise.all([inputTask(), sentenceStreamTask(ws), recvTask(ws)]);\n },\n { timeout: this.connOptions.timeoutMs, signal: this.abortSignal },\n );\n } catch (e) {\n if (this.abortSignal.aborted) {\n return;\n }\n if (e instanceof APIError) throw e;\n throw toRetryableConnectionError(e);\n }\n }\n}\n\nconst transientNetworkCodes = new Set([\n 'ETIMEDOUT',\n 'ECONNRESET',\n 'EAI_AGAIN',\n 'ENETUNREACH',\n 'ECONNREFUSED',\n 'EHOSTUNREACH',\n]);\n\nconst isRecord = (v: unknown): v is Record<string, unknown> => {\n return v !== null && typeof v === 'object';\n};\n\nconst sanitizedErrorName = (error: Error): string => {\n if (error instanceof SyntaxError) return 'SyntaxError';\n if (error instanceof TypeError) return 'TypeError';\n if (error instanceof RangeError) return 'RangeError';\n if (error instanceof AggregateError) return 'AggregateError';\n return 'Error';\n};\n\nconst isAggregateErrorLike = (e: unknown): e is { errors: unknown[]; name?: string } => {\n if (!isRecord(e)) return false;\n return e.name === 'AggregateError' && Array.isArray(e.errors);\n};\n\nconst hasErrorCode = (e: unknown, code: string): boolean => {\n if (isRecord(e) && e.code === code) return true;\n if (isAggregateErrorLike(e)) {\n return e.errors.some((inner) => hasErrorCode(inner, code));\n }\n return false;\n};\n\nconst hasAnyTransientCode = (e: unknown): boolean => {\n if (isRecord(e) && typeof e.code === 'string') {\n return transientNetworkCodes.has(e.code);\n }\n if (isAggregateErrorLike(e)) {\n return e.errors.some((inner) => hasAnyTransientCode(inner));\n }\n return false;\n};\n\nconst toRetryableConnectionError = (e: unknown): APIConnectionError => {\n const err = asError(e);\n const isTimeout =\n hasErrorCode(e, 'ETIMEDOUT') ||\n (typeof err.message === 'string' && err.message.includes('ETIMEDOUT'));\n const message = isTimeout\n ? `Cartesia connection timed out`\n : `Cartesia connection failed: ${err.message || 'unknown error'}`;\n return isTimeout ? new APITimeoutError({ message }) : new APIConnectionError({ message });\n};\n\nconst waitForWsOpen = async ({\n ws,\n timeoutMs,\n abortSignal,\n}: {\n ws: WebSocket;\n timeoutMs: number;\n abortSignal?: AbortSignal;\n}) => {\n if (abortSignal?.aborted) {\n throw new Error('aborted');\n }\n\n const fut = new Future<void>();\n let timeout: NodeJS.Timeout | undefined;\n\n const cleanup = () => {\n if (timeout) clearTimeout(timeout);\n ws.off('open', onOpen);\n ws.off('unexpected-response', onUnexpectedResponse);\n ws.off('error', onError);\n ws.off('close', onClose);\n abortSignal?.removeEventListener('abort', onAbort);\n };\n\n const onOpen = () => fut.resolve();\n const onError = (err: Error) => fut.reject(asError(err));\n const onUnexpectedResponse = (_request: unknown, response: { statusCode?: number }) => {\n // Authentication headers can appear in WebSocket handshake errors.\n const statusCode = response.statusCode ?? -1;\n fut.reject(\n new APIStatusError({\n message: `Cartesia WebSocket connection rejected with status ${statusCode}`,\n options: { statusCode },\n }),\n );\n };\n const onClose = (code: number, reason: Buffer) =>\n fut.reject(\n new Error(`WebSocket closed before open (code=${code}, reason=${reason.toString()})`),\n );\n const onAbort = () => fut.reject(new Error('aborted'));\n\n ws.on('open', onOpen);\n ws.on('unexpected-response', onUnexpectedResponse);\n ws.on('error', onError);\n ws.on('close', onClose);\n abortSignal?.addEventListener('abort', onAbort, { once: true });\n\n if (timeoutMs > 0) {\n timeout = setTimeout(\n () => fut.reject(new APITimeoutError({ message: 'Cartesia WebSocket connection timed out' })),\n timeoutMs,\n );\n }\n\n try {\n await fut.await;\n } finally {\n cleanup();\n }\n};\n\nconst safeTerminateWebSocket = (ws: WebSocket) => {\n // `ws` can emit an 'error' event during teardown (especially if CONNECTING).\n // If there is no error listener at that moment, Node will treat it as unhandled and crash the process.\n try {\n ws.on('error', () => {});\n } catch {\n // ignore\n }\n\n try {\n // `terminate()` can throw if the socket was never established; `close()` is safer in CONNECTING.\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.close();\n } else {\n ws.terminate();\n }\n } catch {\n // ignore\n }\n};\n\n// Graceful close used by the connection pool. A pooled socket is healthy when it\n// is retired (session age, option change, or TTS close), so a clean close frame\n// is preferable to an abrupt terminate; terminate remains the fallback for a\n// socket caught mid-handshake.\nconst safeCloseWebSocket = (ws: WebSocket) => {\n try {\n // `ws` can emit 'error' during teardown; without a listener Node treats it as\n // unhandled and crashes the process.\n ws.on('error', () => {});\n } catch {\n // ignore\n }\n\n try {\n if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) {\n ws.close();\n } else if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {\n ws.terminate();\n }\n } catch {\n // ignore\n }\n};\n\nconst connectCartesiaWebSocket = async ({\n url,\n headers,\n timeoutMs,\n abortSignal,\n}: {\n url: string;\n headers: Record<string, string>;\n timeoutMs: number;\n abortSignal?: AbortSignal;\n}): Promise<WebSocket> => {\n const connectOnce = async (family?: number): Promise<WebSocket> => {\n const ws = new WebSocket(url, { handshakeTimeout: timeoutMs, family, headers });\n try {\n await waitForWsOpen({ ws, timeoutMs, abortSignal });\n return ws;\n } catch (e) {\n safeTerminateWebSocket(ws);\n throw e;\n }\n };\n\n let connectError: unknown;\n try {\n return await connectOnce();\n } catch (e) {\n connectError = e;\n // Mitigation for Node.js dual-stack (IPv6/IPv4) connect flakiness (\"happy eyeballs\"):\n // some environments surface `AggregateError` with nested `ETIMEDOUT` during the initial\n // WebSocket open. In that case we do a one-off retry forcing IPv4 (`family: 4`) before\n // letting the outer framework retry loop handle further attempts.\n //\n // If you still see `AggregateError`/`ETIMEDOUT`:\n // - Increase the session TTS connect timeout (`connOptions.ttsConnOptions.timeoutMs`)\n // - Or adjust Node's family autoselection behavior via `NODE_OPTIONS`, e.g.\n // `--network-family-autoselection-attempt-timeout=5000` (or disable it entirely).\n if (!(e instanceof APIError) && (hasAnyTransientCode(e) || isAggregateErrorLike(e))) {\n try {\n return await connectOnce(4);\n } catch (retryError) {\n connectError = retryError;\n }\n }\n }\n\n if (connectError instanceof APIError) throw connectError;\n const error = asError(connectError);\n const isTimeout =\n hasErrorCode(connectError, 'ETIMEDOUT') || /timed?\\s*out|timeout/i.test(error.message);\n if (isTimeout) {\n throw new APITimeoutError({ message: 'Cartesia WebSocket connection timed out' });\n }\n // Transport errors can contain credentials in URLs.\n throw new APIConnectionError({ message: sanitizedErrorName(error) });\n};\n\nconst toCartesiaOptions = (\n opts: TTSOptions,\n streaming: boolean = false,\n): { [id: string]: unknown } => {\n const voice: { [id: string]: unknown } = {};\n if (typeof opts.voice === 'string') {\n voice.mode = 'id';\n voice.id = opts.voice;\n } else {\n voice.mode = 'embedding';\n voice.embedding = opts.voice;\n }\n\n if (opts.apiVersion === API_VERSION_WITH_EXPERIMENTAL_CONTROLS) {\n const voiceControls: { [id: string]: unknown } = {};\n if (opts.speed) {\n voiceControls.speed = opts.speed;\n }\n if (opts.emotion) {\n voiceControls.emotion = opts.emotion;\n }\n if (Object.keys(voiceControls).length) {\n voice.__experimental_controls = voiceControls;\n }\n }\n\n const result: { [id: string]: unknown } = {\n model_id: opts.model,\n voice,\n output_format: {\n container: 'raw',\n encoding: opts.encoding,\n sample_rate: opts.sampleRate,\n },\n language: getBaseLanguage(opts.language),\n max_buffer_delay_ms: 0,\n };\n\n if (opts.pronunciationDictId) {\n result.pronunciation_dict_id = opts.pronunciationDictId;\n }\n\n if (opts.apiVersion > API_VERSION_WITH_EXPERIMENTAL_CONTROLS && isSonic3(opts.model)) {\n const generationConfig: { [id: string]: unknown } = {};\n if (opts.speed) {\n generationConfig.speed = opts.speed;\n }\n if (opts.emotion) {\n generationConfig.emotion = opts.emotion[0];\n }\n if (opts.volume) {\n generationConfig.volume = opts.volume;\n }\n if (Object.keys(generationConfig).length) {\n result.generation_config = generationConfig;\n }\n }\n\n if (streaming && opts.wordTimestamps !== false) {\n result.add_timestamps = true;\n }\n\n return result;\n};\n"],"mappings":"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","names":["tts"]}
package/dist/tts.test.cjs CHANGED
@@ -85,6 +85,55 @@ async function synthesizeTurn(cartesia, text, connOptions) {
85
85
  }
86
86
  }
87
87
  (0, import_vitest.describe)("Cartesia streaming pool", () => {
88
+ (0, import_vitest.it)("redacts API keys from WebSocket handshake errors", async () => {
89
+ const secret = "cartesia-secret-api-key-do-not-log";
90
+ const wss = new import_ws.WebSocketServer({
91
+ host: "127.0.0.1",
92
+ port: 0,
93
+ verifyClient: (_info, done) => done(false, 401, "Unauthorized")
94
+ });
95
+ await (0, import_node_events.once)(wss, "listening");
96
+ const address = wss.address();
97
+ const cartesia = new import_tts.TTS({ apiKey: secret, baseUrl: `http://127.0.0.1:${address.port}` });
98
+ const errorEvent = (0, import_node_events.once)(cartesia, "error");
99
+ try {
100
+ const stream = cartesia.stream({
101
+ connOptions: { ...import_agents.DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }
102
+ });
103
+ stream.pushText("test");
104
+ stream.endInput();
105
+ const [{ error }] = await errorEvent;
106
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_agents.APIStatusError);
107
+ (0, import_vitest.expect)(error.statusCode).toBe(401);
108
+ (0, import_vitest.expect)(error.message).not.toContain(secret);
109
+ (0, import_vitest.expect)(error.toString()).not.toContain(secret);
110
+ stream.close();
111
+ } finally {
112
+ await cartesia.close();
113
+ await closeWebSocketServer(wss);
114
+ }
115
+ });
116
+ (0, import_vitest.it)("does not retain generic WebSocket connection errors", async () => {
117
+ const secret = "cartesia-secret-api-key-do-not-log";
118
+ const cartesia = new import_tts.TTS({ apiKey: secret, baseUrl: `http://[${secret}` });
119
+ const errorEvent = (0, import_node_events.once)(cartesia, "error");
120
+ try {
121
+ const stream = cartesia.stream({
122
+ connOptions: { ...import_agents.DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }
123
+ });
124
+ stream.pushText("test");
125
+ stream.endInput();
126
+ const [{ error }] = await errorEvent;
127
+ (0, import_vitest.expect)(error).toBeInstanceOf(import_agents.APIConnectionError);
128
+ (0, import_vitest.expect)(error.message).toBe("SyntaxError");
129
+ (0, import_vitest.expect)(error.message).not.toContain(secret);
130
+ (0, import_vitest.expect)(error.toString()).not.toContain(secret);
131
+ (0, import_vitest.expect)(error.cause).toBeUndefined();
132
+ stream.close();
133
+ } finally {
134
+ await cartesia.close();
135
+ }
136
+ });
88
137
  (0, import_vitest.it)("reuses one websocket across sequential turns", async () => {
89
138
  const { wss, baseURL } = await startWebSocketServer();
90
139
  const server = serveCartesia(wss);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, tts } from '@livekit/agents';\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts as testTts } from '@livekit/agents-plugins-test';\nimport { once } from 'node:events';\nimport type { AddressInfo } from 'node:net';\nimport { describe, expect, it } from 'vitest';\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await testTts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n\n// A single 24 kHz mono s16le frame's worth of silence, base64-encoded the way\n// Cartesia sends audio chunks.\nconst CHUNK_BASE64 = Buffer.alloc(4800).toString('base64');\n\nasync function startWebSocketServer() {\n const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n return { wss, baseURL: `http://127.0.0.1:${address.port}` };\n}\n\nasync function closeWebSocketServer(wss: WebSocketServer): Promise<void> {\n for (const client of wss.clients) {\n client.close();\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()));\n}\n\nasync function waitFor<T>(promise: Promise<T>, timeoutMs = 1000): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('timed out waiting for promise')), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\n// A minimal Cartesia TTS WebSocket server: for every generation it replies with\n// one audio chunk and a done message, echoing the caller's context_id. `onStop`\n// lets a test override the reply (e.g. to simulate a provider failure); return\n// false to suppress the normal chunk/done reply.\nfunction serveCartesia(\n wss: WebSocketServer,\n onStop?: (ws: WebSocket, contextId: string, connectionNumber: number) => boolean,\n): { connectionCount: () => number } {\n let connectionCount = 0;\n wss.on('connection', (ws) => {\n connectionCount++;\n const connectionNumber = connectionCount;\n ws.on('message', (raw) => {\n const message = JSON.parse(raw.toString()) as { context_id: string; continue?: boolean };\n if (message.continue !== false) return; // only reply once the turn is closed\n const contextId = message.context_id;\n if (onStop && !onStop(ws, contextId, connectionNumber)) return;\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n ws.send(\n JSON.stringify({ type: 'done', done: true, status_code: 200, context_id: contextId }),\n );\n });\n });\n return { connectionCount: () => connectionCount };\n}\n\nasync function synthesizeTurn(\n cartesia: TTS,\n text: string,\n connOptions?: APIConnectOptions,\n): Promise<tts.SynthesizedAudio[]> {\n const stream = cartesia.stream({ connOptions });\n stream.pushText(text);\n stream.endInput();\n\n try {\n const events: tts.SynthesizedAudio[] = [];\n for await (const event of stream) {\n if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);\n }\n return events;\n } finally {\n stream.close();\n }\n}\n\ndescribe('Cartesia streaming pool', () => {\n it('reuses one websocket across sequential turns', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'second turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('prewarms and reuses the ready websocket', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n const connected = once(wss, 'connection');\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n cartesia.prewarm();\n await waitFor(connected);\n expect(await synthesizeTurn(cartesia, 'prewarmed turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('discards a poisoned websocket after a failure', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // The first connection drops the turn; the second serves it normally.\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.close(1011, 'provider failure');\n return false;\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(\n await synthesizeTurn(cartesia, 'failing turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('fails over when the socket drops mid-generation instead of ending silently', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // Connection 1 emits one audio chunk, then drops WITHOUT a done message,\n // i.e. mid-speech. Connection 2 serves the recovery turn normally.\n const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n setTimeout(() => ws.close(1011, 'mid-speech drop'), 5);\n return false; // suppress the normal chunk/done reply\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n // The dropped turn does not complete successfully (it fails over rather\n // than silently ending); at maxRetry: 0 that surfaces as no audio.\n expect(\n await synthesizeTurn(cartesia, 'dropping turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n // The dead socket is discarded, so the next turn opens a fresh one.\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('replaces a websocket that closed while idle', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n let firstConnectionClosed: (() => void) | undefined;\n const firstClosed = new Promise<void>((resolve) => {\n firstConnectionClosed = resolve;\n });\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.on('close', () => firstConnectionClosed?.());\n // Serve the turn, then drop the idle socket so the next turn reconnects.\n setTimeout(() => ws.close(), 10);\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n await waitFor(firstClosed);\n // Let the client observe the close so the idle handler removes the socket\n // before the next checkout, making the maxRetry: 0 assertion deterministic.\n await new Promise((resolve) => setTimeout(resolve, 100));\n // maxRetry: 0 proves the idle-closed socket was dropped from the pool, not\n // handed back to burn the turn's only attempt.\n expect(\n await waitFor(\n synthesizeTurn(cartesia, 'second turn.', { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }),\n ),\n ).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('closes the pooled websocket when the TTS closes', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n await synthesizeTurn(cartesia, 'closing turn.');\n await cartesia.close();\n // close() drains the pooled socket; give the close frame a beat to land.\n await waitFor(\n (async () => {\n while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));\n })(),\n );\n expect(wss.clients.size).toBe(0);\n } finally {\n await closeWebSocketServer(wss);\n }\n });\n});\n"],"mappings":"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","names":["testTts"]}
1
+ {"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n tts,\n} from '@livekit/agents';\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts as testTts } from '@livekit/agents-plugins-test';\nimport { once } from 'node:events';\nimport type { AddressInfo } from 'node:net';\nimport { describe, expect, it } from 'vitest';\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await testTts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n\n// A single 24 kHz mono s16le frame's worth of silence, base64-encoded the way\n// Cartesia sends audio chunks.\nconst CHUNK_BASE64 = Buffer.alloc(4800).toString('base64');\n\nasync function startWebSocketServer() {\n const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n return { wss, baseURL: `http://127.0.0.1:${address.port}` };\n}\n\nasync function closeWebSocketServer(wss: WebSocketServer): Promise<void> {\n for (const client of wss.clients) {\n client.close();\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()));\n}\n\nasync function waitFor<T>(promise: Promise<T>, timeoutMs = 1000): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('timed out waiting for promise')), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\n// A minimal Cartesia TTS WebSocket server: for every generation it replies with\n// one audio chunk and a done message, echoing the caller's context_id. `onStop`\n// lets a test override the reply (e.g. to simulate a provider failure); return\n// false to suppress the normal chunk/done reply.\nfunction serveCartesia(\n wss: WebSocketServer,\n onStop?: (ws: WebSocket, contextId: string, connectionNumber: number) => boolean,\n): { connectionCount: () => number } {\n let connectionCount = 0;\n wss.on('connection', (ws) => {\n connectionCount++;\n const connectionNumber = connectionCount;\n ws.on('message', (raw) => {\n const message = JSON.parse(raw.toString()) as { context_id: string; continue?: boolean };\n if (message.continue !== false) return; // only reply once the turn is closed\n const contextId = message.context_id;\n if (onStop && !onStop(ws, contextId, connectionNumber)) return;\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n ws.send(\n JSON.stringify({ type: 'done', done: true, status_code: 200, context_id: contextId }),\n );\n });\n });\n return { connectionCount: () => connectionCount };\n}\n\nasync function synthesizeTurn(\n cartesia: TTS,\n text: string,\n connOptions?: APIConnectOptions,\n): Promise<tts.SynthesizedAudio[]> {\n const stream = cartesia.stream({ connOptions });\n stream.pushText(text);\n stream.endInput();\n\n try {\n const events: tts.SynthesizedAudio[] = [];\n for await (const event of stream) {\n if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);\n }\n return events;\n } finally {\n stream.close();\n }\n}\n\ndescribe('Cartesia streaming pool', () => {\n it('redacts API keys from WebSocket handshake errors', async () => {\n const secret = 'cartesia-secret-api-key-do-not-log';\n const wss = new WebSocketServer({\n host: '127.0.0.1',\n port: 0,\n verifyClient: (_info, done) => done(false, 401, 'Unauthorized'),\n });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n const cartesia = new TTS({ apiKey: secret, baseUrl: `http://127.0.0.1:${address.port}` });\n const errorEvent = once(cartesia, 'error') as Promise<Parameters<tts.TTSCallbacks['error']>>;\n\n try {\n const stream = cartesia.stream({\n connOptions: { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 },\n });\n stream.pushText('test');\n stream.endInput();\n\n const [{ error }] = await errorEvent;\n expect(error).toBeInstanceOf(APIStatusError);\n expect((error as APIStatusError).statusCode).toBe(401);\n expect(error.message).not.toContain(secret);\n expect(error.toString()).not.toContain(secret);\n stream.close();\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('does not retain generic WebSocket connection errors', async () => {\n const secret = 'cartesia-secret-api-key-do-not-log';\n const cartesia = new TTS({ apiKey: secret, baseUrl: `http://[${secret}` });\n const errorEvent = once(cartesia, 'error') as Promise<Parameters<tts.TTSCallbacks['error']>>;\n\n try {\n const stream = cartesia.stream({\n connOptions: { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 },\n });\n stream.pushText('test');\n stream.endInput();\n\n const [{ error }] = await errorEvent;\n expect(error).toBeInstanceOf(APIConnectionError);\n expect(error.message).toBe('SyntaxError');\n expect(error.message).not.toContain(secret);\n expect(error.toString()).not.toContain(secret);\n expect((error as Error & { cause?: unknown }).cause).toBeUndefined();\n stream.close();\n } finally {\n await cartesia.close();\n }\n });\n\n it('reuses one websocket across sequential turns', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'second turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('prewarms and reuses the ready websocket', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n const connected = once(wss, 'connection');\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n cartesia.prewarm();\n await waitFor(connected);\n expect(await synthesizeTurn(cartesia, 'prewarmed turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('discards a poisoned websocket after a failure', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // The first connection drops the turn; the second serves it normally.\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.close(1011, 'provider failure');\n return false;\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(\n await synthesizeTurn(cartesia, 'failing turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('fails over when the socket drops mid-generation instead of ending silently', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // Connection 1 emits one audio chunk, then drops WITHOUT a done message,\n // i.e. mid-speech. Connection 2 serves the recovery turn normally.\n const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n setTimeout(() => ws.close(1011, 'mid-speech drop'), 5);\n return false; // suppress the normal chunk/done reply\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n // The dropped turn does not complete successfully (it fails over rather\n // than silently ending); at maxRetry: 0 that surfaces as no audio.\n expect(\n await synthesizeTurn(cartesia, 'dropping turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n // The dead socket is discarded, so the next turn opens a fresh one.\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('replaces a websocket that closed while idle', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n let firstConnectionClosed: (() => void) | undefined;\n const firstClosed = new Promise<void>((resolve) => {\n firstConnectionClosed = resolve;\n });\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.on('close', () => firstConnectionClosed?.());\n // Serve the turn, then drop the idle socket so the next turn reconnects.\n setTimeout(() => ws.close(), 10);\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n await waitFor(firstClosed);\n // Let the client observe the close so the idle handler removes the socket\n // before the next checkout, making the maxRetry: 0 assertion deterministic.\n await new Promise((resolve) => setTimeout(resolve, 100));\n // maxRetry: 0 proves the idle-closed socket was dropped from the pool, not\n // handed back to burn the turn's only attempt.\n expect(\n await waitFor(\n synthesizeTurn(cartesia, 'second turn.', { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }),\n ),\n ).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('closes the pooled websocket when the TTS closes', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n await synthesizeTurn(cartesia, 'closing turn.');\n await cartesia.close();\n // close() drains the pooled socket; give the close frame a beat to land.\n await waitFor(\n (async () => {\n while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));\n })(),\n );\n expect(wss.clients.size).toBe(0);\n } finally {\n await closeWebSocketServer(wss);\n }\n });\n});\n"],"mappings":";AAGA,oBAMO;AACP,kCAAoB;AACpB,iCAA+B;AAC/B,yBAAqB;AAErB,oBAAqC;AACrC,gBAAgD;AAChD,iBAAoB;AAEpB,MAAM,oBAAoB,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,cAAc;AAE5F,IAAI,mBAAmB;AACrB,8BAAS,YAAY,YAAY;AAC/B,cAAM,2BAAAA,KAAQ,IAAI,eAAI,GAAG,IAAI,gCAAI,CAAC;AAAA,EACpC,CAAC;AACH,OAAO;AACL,8BAAS,YAAY,MAAM;AACzB,qBAAG,KAAK,gDAAgD,MAAM;AAAA,IAAC,CAAC;AAAA,EAClE,CAAC;AACH;AAIA,MAAM,eAAe,OAAO,MAAM,IAAI,EAAE,SAAS,QAAQ;AAEzD,eAAe,uBAAuB;AACpC,QAAM,MAAM,IAAI,0BAAgB,EAAE,MAAM,aAAa,MAAM,EAAE,CAAC;AAC9D,YAAM,yBAAK,KAAK,WAAW;AAC3B,QAAM,UAAU,IAAI,QAAQ;AAC5B,SAAO,EAAE,KAAK,SAAS,oBAAoB,QAAQ,IAAI,GAAG;AAC5D;AAEA,eAAe,qBAAqB,KAAqC;AACvE,aAAW,UAAU,IAAI,SAAS;AAChC,WAAO,MAAM;AAAA,EACf;AACA,QAAM,IAAI,QAAc,CAAC,YAAY,IAAI,MAAM,MAAM,QAAQ,CAAC,CAAC;AACjE;AAEA,eAAe,QAAW,SAAqB,YAAY,KAAkB;AAC3E,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK;AAAA,MACxB;AAAA,MACA,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,kBAAU,WAAW,MAAM,OAAO,IAAI,MAAM,+BAA+B,CAAC,GAAG,SAAS;AAAA,MAC1F,CAAC;AAAA,IACH,CAAC;AAAA,EACH,UAAE;AACA,QAAI,QAAS,cAAa,OAAO;AAAA,EACnC;AACF;AAMA,SAAS,cACP,KACA,QACmC;AACnC,MAAI,kBAAkB;AACtB,MAAI,GAAG,cAAc,CAAC,OAAO;AAC3B;AACA,UAAM,mBAAmB;AACzB,OAAG,GAAG,WAAW,CAAC,QAAQ;AACxB,YAAM,UAAU,KAAK,MAAM,IAAI,SAAS,CAAC;AACzC,UAAI,QAAQ,aAAa,MAAO;AAChC,YAAM,YAAY,QAAQ;AAC1B,UAAI,UAAU,CAAC,OAAO,IAAI,WAAW,gBAAgB,EAAG;AACxD,SAAG;AAAA,QACD,KAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,MAAM;AAAA,UACN,MAAM;AAAA,UACN,aAAa;AAAA,UACb,WAAW;AAAA,UACX,YAAY;AAAA,QACd,CAAC;AAAA,MACH;AACA,SAAG;AAAA,QACD,KAAK,UAAU,EAAE,MAAM,QAAQ,MAAM,MAAM,aAAa,KAAK,YAAY,UAAU,CAAC;AAAA,MACtF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,SAAO,EAAE,iBAAiB,MAAM,gBAAgB;AAClD;AAEA,eAAe,eACb,UACA,MACA,aACiC;AACjC,QAAM,SAAS,SAAS,OAAO,EAAE,YAAY,CAAC;AAC9C,SAAO,SAAS,IAAI;AACpB,SAAO,SAAS;AAEhB,MAAI;AACF,UAAM,SAAiC,CAAC;AACxC,qBAAiB,SAAS,QAAQ;AAChC,UAAI,UAAU,kBAAI,iBAAiB,cAAe,QAAO,KAAK,KAAK;AAAA,IACrE;AACA,WAAO;AAAA,EACT,UAAE;AACA,WAAO,MAAM;AAAA,EACf;AACF;AAAA,IAEA,wBAAS,2BAA2B,MAAM;AACxC,wBAAG,oDAAoD,YAAY;AACjE,UAAM,SAAS;AACf,UAAM,MAAM,IAAI,0BAAgB;AAAA,MAC9B,MAAM;AAAA,MACN,MAAM;AAAA,MACN,cAAc,CAAC,OAAO,SAAS,KAAK,OAAO,KAAK,cAAc;AAAA,IAChE,CAAC;AACD,cAAM,yBAAK,KAAK,WAAW;AAC3B,UAAM,UAAU,IAAI,QAAQ;AAC5B,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,QAAQ,SAAS,oBAAoB,QAAQ,IAAI,GAAG,CAAC;AACxF,UAAM,iBAAa,yBAAK,UAAU,OAAO;AAEzC,QAAI;AACF,YAAM,SAAS,SAAS,OAAO;AAAA,QAC7B,aAAa,EAAE,GAAG,2CAA6B,UAAU,EAAE;AAAA,MAC7D,CAAC;AACD,aAAO,SAAS,MAAM;AACtB,aAAO,SAAS;AAEhB,YAAM,CAAC,EAAE,MAAM,CAAC,IAAI,MAAM;AAC1B,gCAAO,KAAK,EAAE,eAAe,4BAAc;AAC3C,gCAAQ,MAAyB,UAAU,EAAE,KAAK,GAAG;AACrD,gCAAO,MAAM,OAAO,EAAE,IAAI,UAAU,MAAM;AAC1C,gCAAO,MAAM,SAAS,CAAC,EAAE,IAAI,UAAU,MAAM;AAC7C,aAAO,MAAM;AAAA,IACf,UAAE;AACA,YAAM,SAAS,MAAM;AACrB,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AAED,wBAAG,uDAAuD,YAAY;AACpE,UAAM,SAAS;AACf,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,QAAQ,SAAS,WAAW,MAAM,GAAG,CAAC;AACzE,UAAM,iBAAa,yBAAK,UAAU,OAAO;AAEzC,QAAI;AACF,YAAM,SAAS,SAAS,OAAO;AAAA,QAC7B,aAAa,EAAE,GAAG,2CAA6B,UAAU,EAAE;AAAA,MAC7D,CAAC;AACD,aAAO,SAAS,MAAM;AACtB,aAAO,SAAS;AAEhB,YAAM,CAAC,EAAE,MAAM,CAAC,IAAI,MAAM;AAC1B,gCAAO,KAAK,EAAE,eAAe,gCAAkB;AAC/C,gCAAO,MAAM,OAAO,EAAE,KAAK,aAAa;AACxC,gCAAO,MAAM,OAAO,EAAE,IAAI,UAAU,MAAM;AAC1C,gCAAO,MAAM,SAAS,CAAC,EAAE,IAAI,UAAU,MAAM;AAC7C,gCAAQ,MAAsC,KAAK,EAAE,cAAc;AACnE,aAAO,MAAM;AAAA,IACf,UAAE;AACA,YAAM,SAAS,MAAM;AAAA,IACvB;AAAA,EACF,CAAC;AAED,wBAAG,gDAAgD,YAAY;AAC7D,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,qBAAqB;AACpD,UAAM,SAAS,cAAc,GAAG;AAEhC,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,YAAY,SAAS,QAAQ,CAAC;AACjE,QAAI;AACF,gCAAO,MAAM,eAAe,UAAU,aAAa,CAAC,EAAE,IAAI,aAAa,CAAC;AACxE,gCAAO,MAAM,eAAe,UAAU,cAAc,CAAC,EAAE,IAAI,aAAa,CAAC;AACzE,gCAAO,OAAO,gBAAgB,CAAC,EAAE,KAAK,CAAC;AAAA,IACzC,UAAE;AACA,YAAM,SAAS,MAAM;AACrB,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AAED,wBAAG,2CAA2C,YAAY;AACxD,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,qBAAqB;AACpD,UAAM,SAAS,cAAc,GAAG;AAChC,UAAM,gBAAY,yBAAK,KAAK,YAAY;AAExC,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,YAAY,SAAS,QAAQ,CAAC;AACjE,QAAI;AACF,eAAS,QAAQ;AACjB,YAAM,QAAQ,SAAS;AACvB,gCAAO,MAAM,eAAe,UAAU,iBAAiB,CAAC,EAAE,IAAI,aAAa,CAAC;AAC5E,gCAAO,OAAO,gBAAgB,CAAC,EAAE,KAAK,CAAC;AAAA,IACzC,UAAE;AACA,YAAM,SAAS,MAAM;AACrB,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AAED,wBAAG,iDAAiD,YAAY;AAC9D,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,qBAAqB;AAEpD,UAAM,SAAS,cAAc,KAAK,CAAC,IAAI,YAAY,qBAAqB;AACtE,UAAI,qBAAqB,GAAG;AAC1B,WAAG,MAAM,MAAM,kBAAkB;AACjC,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,YAAY,SAAS,QAAQ,CAAC;AACjE,QAAI;AACF;AAAA,QACE,MAAM,eAAe,UAAU,iBAAiB;AAAA,UAC9C,GAAG;AAAA,UACH,UAAU;AAAA,QACZ,CAAC;AAAA,MACH,EAAE,aAAa,CAAC;AAChB,gCAAO,MAAM,eAAe,UAAU,gBAAgB,CAAC,EAAE,IAAI,aAAa,CAAC;AAC3E,gCAAO,OAAO,gBAAgB,CAAC,EAAE,KAAK,CAAC;AAAA,IACzC,UAAE;AACA,YAAM,SAAS,MAAM;AACrB,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AAED,wBAAG,8EAA8E,YAAY;AAC3F,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,qBAAqB;AAGpD,UAAM,SAAS,cAAc,KAAK,CAAC,IAAI,WAAW,qBAAqB;AACrE,UAAI,qBAAqB,GAAG;AAC1B,WAAG;AAAA,UACD,KAAK,UAAU;AAAA,YACb,MAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM;AAAA,YACN,aAAa;AAAA,YACb,WAAW;AAAA,YACX,YAAY;AAAA,UACd,CAAC;AAAA,QACH;AACA,mBAAW,MAAM,GAAG,MAAM,MAAM,iBAAiB,GAAG,CAAC;AACrD,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,YAAY,SAAS,QAAQ,CAAC;AACjE,QAAI;AAGF;AAAA,QACE,MAAM,eAAe,UAAU,kBAAkB;AAAA,UAC/C,GAAG;AAAA,UACH,UAAU;AAAA,QACZ,CAAC;AAAA,MACH,EAAE,aAAa,CAAC;AAEhB,gCAAO,MAAM,eAAe,UAAU,gBAAgB,CAAC,EAAE,IAAI,aAAa,CAAC;AAC3E,gCAAO,OAAO,gBAAgB,CAAC,EAAE,KAAK,CAAC;AAAA,IACzC,UAAE;AACA,YAAM,SAAS,MAAM;AACrB,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AAED,wBAAG,+CAA+C,YAAY;AAC5D,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,qBAAqB;AACpD,QAAI;AACJ,UAAM,cAAc,IAAI,QAAc,CAAC,YAAY;AACjD,8BAAwB;AAAA,IAC1B,CAAC;AACD,UAAM,SAAS,cAAc,KAAK,CAAC,IAAI,YAAY,qBAAqB;AACtE,UAAI,qBAAqB,GAAG;AAC1B,WAAG,GAAG,SAAS,MAAM,gEAAyB;AAE9C,mBAAW,MAAM,GAAG,MAAM,GAAG,EAAE;AAAA,MACjC;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,YAAY,SAAS,QAAQ,CAAC;AACjE,QAAI;AACF,gCAAO,MAAM,eAAe,UAAU,aAAa,CAAC,EAAE,IAAI,aAAa,CAAC;AACxE,YAAM,QAAQ,WAAW;AAGzB,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAG,CAAC;AAGvD;AAAA,QACE,MAAM;AAAA,UACJ,eAAe,UAAU,gBAAgB,EAAE,GAAG,2CAA6B,UAAU,EAAE,CAAC;AAAA,QAC1F;AAAA,MACF,EAAE,IAAI,aAAa,CAAC;AACpB,gCAAO,OAAO,gBAAgB,CAAC,EAAE,KAAK,CAAC;AAAA,IACzC,UAAE;AACA,YAAM,SAAS,MAAM;AACrB,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AAED,wBAAG,mDAAmD,YAAY;AAChE,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,qBAAqB;AACpD,kBAAc,GAAG;AAEjB,UAAM,WAAW,IAAI,eAAI,EAAE,QAAQ,YAAY,SAAS,QAAQ,CAAC;AACjE,QAAI;AACF,YAAM,eAAe,UAAU,eAAe;AAC9C,YAAM,SAAS,MAAM;AAErB,YAAM;AAAA,SACH,YAAY;AACX,iBAAO,IAAI,QAAQ,OAAO,EAAG,OAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAAA,QACxE,GAAG;AAAA,MACL;AACA,gCAAO,IAAI,QAAQ,IAAI,EAAE,KAAK,CAAC;AAAA,IACjC,UAAE;AACA,YAAM,qBAAqB,GAAG;AAAA,IAChC;AAAA,EACF,CAAC;AACH,CAAC;","names":["testTts"]}
package/dist/tts.test.js CHANGED
@@ -1,4 +1,9 @@
1
- import { DEFAULT_API_CONNECT_OPTIONS, tts } from "@livekit/agents";
1
+ import {
2
+ APIConnectionError,
3
+ APIStatusError,
4
+ DEFAULT_API_CONNECT_OPTIONS,
5
+ tts
6
+ } from "@livekit/agents";
2
7
  import { STT } from "@livekit/agents-plugin-openai";
3
8
  import { tts as testTts } from "@livekit/agents-plugins-test";
4
9
  import { once } from "node:events";
@@ -84,6 +89,55 @@ async function synthesizeTurn(cartesia, text, connOptions) {
84
89
  }
85
90
  }
86
91
  describe("Cartesia streaming pool", () => {
92
+ it("redacts API keys from WebSocket handshake errors", async () => {
93
+ const secret = "cartesia-secret-api-key-do-not-log";
94
+ const wss = new WebSocketServer({
95
+ host: "127.0.0.1",
96
+ port: 0,
97
+ verifyClient: (_info, done) => done(false, 401, "Unauthorized")
98
+ });
99
+ await once(wss, "listening");
100
+ const address = wss.address();
101
+ const cartesia = new TTS({ apiKey: secret, baseUrl: `http://127.0.0.1:${address.port}` });
102
+ const errorEvent = once(cartesia, "error");
103
+ try {
104
+ const stream = cartesia.stream({
105
+ connOptions: { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }
106
+ });
107
+ stream.pushText("test");
108
+ stream.endInput();
109
+ const [{ error }] = await errorEvent;
110
+ expect(error).toBeInstanceOf(APIStatusError);
111
+ expect(error.statusCode).toBe(401);
112
+ expect(error.message).not.toContain(secret);
113
+ expect(error.toString()).not.toContain(secret);
114
+ stream.close();
115
+ } finally {
116
+ await cartesia.close();
117
+ await closeWebSocketServer(wss);
118
+ }
119
+ });
120
+ it("does not retain generic WebSocket connection errors", async () => {
121
+ const secret = "cartesia-secret-api-key-do-not-log";
122
+ const cartesia = new TTS({ apiKey: secret, baseUrl: `http://[${secret}` });
123
+ const errorEvent = once(cartesia, "error");
124
+ try {
125
+ const stream = cartesia.stream({
126
+ connOptions: { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }
127
+ });
128
+ stream.pushText("test");
129
+ stream.endInput();
130
+ const [{ error }] = await errorEvent;
131
+ expect(error).toBeInstanceOf(APIConnectionError);
132
+ expect(error.message).toBe("SyntaxError");
133
+ expect(error.message).not.toContain(secret);
134
+ expect(error.toString()).not.toContain(secret);
135
+ expect(error.cause).toBeUndefined();
136
+ stream.close();
137
+ } finally {
138
+ await cartesia.close();
139
+ }
140
+ });
87
141
  it("reuses one websocket across sequential turns", async () => {
88
142
  const { wss, baseURL } = await startWebSocketServer();
89
143
  const server = serveCartesia(wss);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type APIConnectOptions, DEFAULT_API_CONNECT_OPTIONS, tts } from '@livekit/agents';\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts as testTts } from '@livekit/agents-plugins-test';\nimport { once } from 'node:events';\nimport type { AddressInfo } from 'node:net';\nimport { describe, expect, it } from 'vitest';\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await testTts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n\n// A single 24 kHz mono s16le frame's worth of silence, base64-encoded the way\n// Cartesia sends audio chunks.\nconst CHUNK_BASE64 = Buffer.alloc(4800).toString('base64');\n\nasync function startWebSocketServer() {\n const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n return { wss, baseURL: `http://127.0.0.1:${address.port}` };\n}\n\nasync function closeWebSocketServer(wss: WebSocketServer): Promise<void> {\n for (const client of wss.clients) {\n client.close();\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()));\n}\n\nasync function waitFor<T>(promise: Promise<T>, timeoutMs = 1000): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('timed out waiting for promise')), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\n// A minimal Cartesia TTS WebSocket server: for every generation it replies with\n// one audio chunk and a done message, echoing the caller's context_id. `onStop`\n// lets a test override the reply (e.g. to simulate a provider failure); return\n// false to suppress the normal chunk/done reply.\nfunction serveCartesia(\n wss: WebSocketServer,\n onStop?: (ws: WebSocket, contextId: string, connectionNumber: number) => boolean,\n): { connectionCount: () => number } {\n let connectionCount = 0;\n wss.on('connection', (ws) => {\n connectionCount++;\n const connectionNumber = connectionCount;\n ws.on('message', (raw) => {\n const message = JSON.parse(raw.toString()) as { context_id: string; continue?: boolean };\n if (message.continue !== false) return; // only reply once the turn is closed\n const contextId = message.context_id;\n if (onStop && !onStop(ws, contextId, connectionNumber)) return;\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n ws.send(\n JSON.stringify({ type: 'done', done: true, status_code: 200, context_id: contextId }),\n );\n });\n });\n return { connectionCount: () => connectionCount };\n}\n\nasync function synthesizeTurn(\n cartesia: TTS,\n text: string,\n connOptions?: APIConnectOptions,\n): Promise<tts.SynthesizedAudio[]> {\n const stream = cartesia.stream({ connOptions });\n stream.pushText(text);\n stream.endInput();\n\n try {\n const events: tts.SynthesizedAudio[] = [];\n for await (const event of stream) {\n if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);\n }\n return events;\n } finally {\n stream.close();\n }\n}\n\ndescribe('Cartesia streaming pool', () => {\n it('reuses one websocket across sequential turns', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'second turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('prewarms and reuses the ready websocket', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n const connected = once(wss, 'connection');\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n cartesia.prewarm();\n await waitFor(connected);\n expect(await synthesizeTurn(cartesia, 'prewarmed turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('discards a poisoned websocket after a failure', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // The first connection drops the turn; the second serves it normally.\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.close(1011, 'provider failure');\n return false;\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(\n await synthesizeTurn(cartesia, 'failing turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('fails over when the socket drops mid-generation instead of ending silently', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // Connection 1 emits one audio chunk, then drops WITHOUT a done message,\n // i.e. mid-speech. Connection 2 serves the recovery turn normally.\n const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n setTimeout(() => ws.close(1011, 'mid-speech drop'), 5);\n return false; // suppress the normal chunk/done reply\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n // The dropped turn does not complete successfully (it fails over rather\n // than silently ending); at maxRetry: 0 that surfaces as no audio.\n expect(\n await synthesizeTurn(cartesia, 'dropping turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n // The dead socket is discarded, so the next turn opens a fresh one.\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('replaces a websocket that closed while idle', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n let firstConnectionClosed: (() => void) | undefined;\n const firstClosed = new Promise<void>((resolve) => {\n firstConnectionClosed = resolve;\n });\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.on('close', () => firstConnectionClosed?.());\n // Serve the turn, then drop the idle socket so the next turn reconnects.\n setTimeout(() => ws.close(), 10);\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n await waitFor(firstClosed);\n // Let the client observe the close so the idle handler removes the socket\n // before the next checkout, making the maxRetry: 0 assertion deterministic.\n await new Promise((resolve) => setTimeout(resolve, 100));\n // maxRetry: 0 proves the idle-closed socket was dropped from the pool, not\n // handed back to burn the turn's only attempt.\n expect(\n await waitFor(\n synthesizeTurn(cartesia, 'second turn.', { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }),\n ),\n ).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('closes the pooled websocket when the TTS closes', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n await synthesizeTurn(cartesia, 'closing turn.');\n await cartesia.close();\n // close() drains the pooled socket; give the close frame a beat to land.\n await waitFor(\n (async () => {\n while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));\n })(),\n );\n expect(wss.clients.size).toBe(0);\n } finally {\n await closeWebSocketServer(wss);\n }\n });\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/tts.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n DEFAULT_API_CONNECT_OPTIONS,\n tts,\n} from '@livekit/agents';\nimport { STT } from '@livekit/agents-plugin-openai';\nimport { tts as testTts } from '@livekit/agents-plugins-test';\nimport { once } from 'node:events';\nimport type { AddressInfo } from 'node:net';\nimport { describe, expect, it } from 'vitest';\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport { TTS } from './tts.js';\n\nconst hasCartesiaConfig = Boolean(process.env.CARTESIA_API_KEY && process.env.OPENAI_API_KEY);\n\nif (hasCartesiaConfig) {\n describe('Cartesia', async () => {\n await testTts(new TTS(), new STT());\n });\n} else {\n describe('Cartesia', () => {\n it.skip('requires CARTESIA_API_KEY and OPENAI_API_KEY', () => {});\n });\n}\n\n// A single 24 kHz mono s16le frame's worth of silence, base64-encoded the way\n// Cartesia sends audio chunks.\nconst CHUNK_BASE64 = Buffer.alloc(4800).toString('base64');\n\nasync function startWebSocketServer() {\n const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n return { wss, baseURL: `http://127.0.0.1:${address.port}` };\n}\n\nasync function closeWebSocketServer(wss: WebSocketServer): Promise<void> {\n for (const client of wss.clients) {\n client.close();\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()));\n}\n\nasync function waitFor<T>(promise: Promise<T>, timeoutMs = 1000): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('timed out waiting for promise')), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\n// A minimal Cartesia TTS WebSocket server: for every generation it replies with\n// one audio chunk and a done message, echoing the caller's context_id. `onStop`\n// lets a test override the reply (e.g. to simulate a provider failure); return\n// false to suppress the normal chunk/done reply.\nfunction serveCartesia(\n wss: WebSocketServer,\n onStop?: (ws: WebSocket, contextId: string, connectionNumber: number) => boolean,\n): { connectionCount: () => number } {\n let connectionCount = 0;\n wss.on('connection', (ws) => {\n connectionCount++;\n const connectionNumber = connectionCount;\n ws.on('message', (raw) => {\n const message = JSON.parse(raw.toString()) as { context_id: string; continue?: boolean };\n if (message.continue !== false) return; // only reply once the turn is closed\n const contextId = message.context_id;\n if (onStop && !onStop(ws, contextId, connectionNumber)) return;\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n ws.send(\n JSON.stringify({ type: 'done', done: true, status_code: 200, context_id: contextId }),\n );\n });\n });\n return { connectionCount: () => connectionCount };\n}\n\nasync function synthesizeTurn(\n cartesia: TTS,\n text: string,\n connOptions?: APIConnectOptions,\n): Promise<tts.SynthesizedAudio[]> {\n const stream = cartesia.stream({ connOptions });\n stream.pushText(text);\n stream.endInput();\n\n try {\n const events: tts.SynthesizedAudio[] = [];\n for await (const event of stream) {\n if (event !== tts.SynthesizeStream.END_OF_STREAM) events.push(event);\n }\n return events;\n } finally {\n stream.close();\n }\n}\n\ndescribe('Cartesia streaming pool', () => {\n it('redacts API keys from WebSocket handshake errors', async () => {\n const secret = 'cartesia-secret-api-key-do-not-log';\n const wss = new WebSocketServer({\n host: '127.0.0.1',\n port: 0,\n verifyClient: (_info, done) => done(false, 401, 'Unauthorized'),\n });\n await once(wss, 'listening');\n const address = wss.address() as AddressInfo;\n const cartesia = new TTS({ apiKey: secret, baseUrl: `http://127.0.0.1:${address.port}` });\n const errorEvent = once(cartesia, 'error') as Promise<Parameters<tts.TTSCallbacks['error']>>;\n\n try {\n const stream = cartesia.stream({\n connOptions: { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 },\n });\n stream.pushText('test');\n stream.endInput();\n\n const [{ error }] = await errorEvent;\n expect(error).toBeInstanceOf(APIStatusError);\n expect((error as APIStatusError).statusCode).toBe(401);\n expect(error.message).not.toContain(secret);\n expect(error.toString()).not.toContain(secret);\n stream.close();\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('does not retain generic WebSocket connection errors', async () => {\n const secret = 'cartesia-secret-api-key-do-not-log';\n const cartesia = new TTS({ apiKey: secret, baseUrl: `http://[${secret}` });\n const errorEvent = once(cartesia, 'error') as Promise<Parameters<tts.TTSCallbacks['error']>>;\n\n try {\n const stream = cartesia.stream({\n connOptions: { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 },\n });\n stream.pushText('test');\n stream.endInput();\n\n const [{ error }] = await errorEvent;\n expect(error).toBeInstanceOf(APIConnectionError);\n expect(error.message).toBe('SyntaxError');\n expect(error.message).not.toContain(secret);\n expect(error.toString()).not.toContain(secret);\n expect((error as Error & { cause?: unknown }).cause).toBeUndefined();\n stream.close();\n } finally {\n await cartesia.close();\n }\n });\n\n it('reuses one websocket across sequential turns', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'second turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('prewarms and reuses the ready websocket', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n const server = serveCartesia(wss);\n const connected = once(wss, 'connection');\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n cartesia.prewarm();\n await waitFor(connected);\n expect(await synthesizeTurn(cartesia, 'prewarmed turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(1);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('discards a poisoned websocket after a failure', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // The first connection drops the turn; the second serves it normally.\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.close(1011, 'provider failure');\n return false;\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(\n await synthesizeTurn(cartesia, 'failing turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('fails over when the socket drops mid-generation instead of ending silently', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n // Connection 1 emits one audio chunk, then drops WITHOUT a done message,\n // i.e. mid-speech. Connection 2 serves the recovery turn normally.\n const server = serveCartesia(wss, (ws, contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.send(\n JSON.stringify({\n type: 'chunk',\n data: CHUNK_BASE64,\n done: false,\n status_code: 200,\n step_time: 0,\n context_id: contextId,\n }),\n );\n setTimeout(() => ws.close(1011, 'mid-speech drop'), 5);\n return false; // suppress the normal chunk/done reply\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n // The dropped turn does not complete successfully (it fails over rather\n // than silently ending); at maxRetry: 0 that surfaces as no audio.\n expect(\n await synthesizeTurn(cartesia, 'dropping turn.', {\n ...DEFAULT_API_CONNECT_OPTIONS,\n maxRetry: 0,\n }),\n ).toHaveLength(0);\n // The dead socket is discarded, so the next turn opens a fresh one.\n expect(await synthesizeTurn(cartesia, 'recovery turn.')).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('replaces a websocket that closed while idle', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n let firstConnectionClosed: (() => void) | undefined;\n const firstClosed = new Promise<void>((resolve) => {\n firstConnectionClosed = resolve;\n });\n const server = serveCartesia(wss, (ws, _contextId, connectionNumber) => {\n if (connectionNumber === 1) {\n ws.on('close', () => firstConnectionClosed?.());\n // Serve the turn, then drop the idle socket so the next turn reconnects.\n setTimeout(() => ws.close(), 10);\n }\n return true;\n });\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n expect(await synthesizeTurn(cartesia, 'first turn.')).not.toHaveLength(0);\n await waitFor(firstClosed);\n // Let the client observe the close so the idle handler removes the socket\n // before the next checkout, making the maxRetry: 0 assertion deterministic.\n await new Promise((resolve) => setTimeout(resolve, 100));\n // maxRetry: 0 proves the idle-closed socket was dropped from the pool, not\n // handed back to burn the turn's only attempt.\n expect(\n await waitFor(\n synthesizeTurn(cartesia, 'second turn.', { ...DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }),\n ),\n ).not.toHaveLength(0);\n expect(server.connectionCount()).toBe(2);\n } finally {\n await cartesia.close();\n await closeWebSocketServer(wss);\n }\n });\n\n it('closes the pooled websocket when the TTS closes', async () => {\n const { wss, baseURL } = await startWebSocketServer();\n serveCartesia(wss);\n\n const cartesia = new TTS({ apiKey: 'test-key', baseUrl: baseURL });\n try {\n await synthesizeTurn(cartesia, 'closing turn.');\n await cartesia.close();\n // close() drains the pooled socket; give the close frame a beat to land.\n await waitFor(\n (async () => {\n while (wss.clients.size > 0) await new Promise((r) => setTimeout(r, 5));\n })(),\n );\n expect(wss.clients.size).toBe(0);\n } finally {\n await closeWebSocketServer(wss);\n }\n });\n});\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livekit/agents-plugin-cartesia",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "Cartesia plugin for LiveKit Node Agents",
5
5
  "main": "dist/index.js",
6
6
  "require": "dist/index.cjs",
@@ -33,10 +33,10 @@
33
33
  "@types/ws": "^8.5.10",
34
34
  "tsup": "^8.3.5",
35
35
  "typescript": "^5.0.0",
36
- "@livekit/agents": "1.6.3",
37
- "@livekit/agents-plugin-openai": "1.6.3",
38
- "@livekit/agents-plugin-silero": "1.6.3",
39
- "@livekit/agents-plugins-test": "1.6.3"
36
+ "@livekit/agents": "1.6.4",
37
+ "@livekit/agents-plugin-openai": "1.6.4",
38
+ "@livekit/agents-plugin-silero": "1.6.4",
39
+ "@livekit/agents-plugins-test": "1.6.4"
40
40
  },
41
41
  "dependencies": {
42
42
  "ws": "^8.21.0"
@@ -44,7 +44,7 @@
44
44
  "peerDependencies": {
45
45
  "@livekit/rtc-node": "^0.13.33",
46
46
  "zod": "^3.25.76 || ^4.1.8",
47
- "@livekit/agents": "1.6.3"
47
+ "@livekit/agents": "1.6.4"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsup --onSuccess \"pnpm build:types\"",