@livekit/agents-plugin-cartesia 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/stt.cjs +53 -11
- package/dist/stt.cjs.map +1 -1
- package/dist/stt.d.ts.map +1 -1
- package/dist/stt.js +56 -11
- package/dist/stt.js.map +1 -1
- package/dist/stt.test.cjs +35 -0
- package/dist/stt.test.cjs.map +1 -1
- package/dist/stt.test.js +38 -3
- package/dist/stt.test.js.map +1 -1
- package/dist/tts.cjs +38 -5
- package/dist/tts.cjs.map +1 -1
- package/dist/tts.js +38 -5
- package/dist/tts.js.map +1 -1
- package/dist/tts.test.cjs +49 -0
- package/dist/tts.test.cjs.map +1 -1
- package/dist/tts.test.js +55 -1
- package/dist/tts.test.js.map +1 -1
- package/package.json +9 -9
- package/src/stt.test.ts +42 -3
- package/src/stt.ts +58 -11
- package/src/tts.test.ts +62 -1
- package/src/tts.ts +43 -5
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/stt.cjs
CHANGED
|
@@ -31,6 +31,13 @@ const REQUEST_ID_HEADER = "cartesia-request-id";
|
|
|
31
31
|
const API_VERSION = "2026-03-01";
|
|
32
32
|
const DRAIN_TIMEOUT_MS = 5e3;
|
|
33
33
|
const KEEPALIVE_INTERVAL_MS = 3e4;
|
|
34
|
+
const sanitizedErrorName = (error) => {
|
|
35
|
+
if (error instanceof SyntaxError) return "SyntaxError";
|
|
36
|
+
if (error instanceof TypeError) return "TypeError";
|
|
37
|
+
if (error instanceof RangeError) return "RangeError";
|
|
38
|
+
if (error instanceof AggregateError) return "AggregateError";
|
|
39
|
+
return "Error";
|
|
40
|
+
};
|
|
34
41
|
const defaultSTTOptions = {
|
|
35
42
|
model: "ink-2",
|
|
36
43
|
/** recommended default */
|
|
@@ -104,9 +111,11 @@ class SpeechStream extends import_agents.stt.SpeechStream {
|
|
|
104
111
|
#currentTranscript = "";
|
|
105
112
|
#speechDuration = 0;
|
|
106
113
|
#closingWs = false;
|
|
114
|
+
#connectTimeout;
|
|
107
115
|
constructor(sttInstance, opts, connOptions) {
|
|
108
116
|
super(sttInstance, opts.sampleRate, connOptions);
|
|
109
117
|
this.#opts = { ...opts };
|
|
118
|
+
this.#connectTimeout = (connOptions == null ? void 0 : connOptions.timeoutMs) ?? import_agents.DEFAULT_API_CONNECT_OPTIONS.timeoutMs;
|
|
110
119
|
}
|
|
111
120
|
get label() {
|
|
112
121
|
return "cartesia.SpeechStream";
|
|
@@ -126,12 +135,21 @@ class SpeechStream extends import_agents.stt.SpeechStream {
|
|
|
126
135
|
try {
|
|
127
136
|
const url = this.#getCartesiaUrl();
|
|
128
137
|
this.#logger.debug(`Connecting to Cartesia STT: ${url}`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
let ws;
|
|
139
|
+
try {
|
|
140
|
+
ws = new import_ws.WebSocket(url, {
|
|
141
|
+
handshakeTimeout: this.#connectTimeout,
|
|
142
|
+
headers: {
|
|
143
|
+
[AUTHORIZATION_HEADER]: this.#opts.apiKey,
|
|
144
|
+
[VERSION_HEADER]: API_VERSION
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new import_agents.APIConnectionError({
|
|
149
|
+
message: sanitizedErrorName((0, import_agents.asError)(error)),
|
|
150
|
+
options: { retryable: true }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
135
153
|
this.#ws = ws;
|
|
136
154
|
ws.once("upgrade", (response) => {
|
|
137
155
|
const headerValue = response.headers[REQUEST_ID_HEADER];
|
|
@@ -142,15 +160,39 @@ class SpeechStream extends import_agents.stt.SpeechStream {
|
|
|
142
160
|
}
|
|
143
161
|
});
|
|
144
162
|
await new Promise((resolve, reject) => {
|
|
145
|
-
const
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
ws.off("open", onOpen);
|
|
165
|
+
ws.off("unexpected-response", onUnexpectedResponse);
|
|
146
166
|
ws.off("error", onError);
|
|
167
|
+
};
|
|
168
|
+
const onOpen = () => {
|
|
169
|
+
cleanup();
|
|
147
170
|
resolve();
|
|
148
171
|
};
|
|
172
|
+
const onUnexpectedResponse = (_request, response) => {
|
|
173
|
+
cleanup();
|
|
174
|
+
ws.on("error", () => {
|
|
175
|
+
});
|
|
176
|
+
ws.close();
|
|
177
|
+
const statusCode = response.statusCode ?? -1;
|
|
178
|
+
reject(
|
|
179
|
+
new import_agents.APIStatusError({
|
|
180
|
+
message: `Cartesia WebSocket connection rejected with status ${statusCode}`,
|
|
181
|
+
options: { statusCode }
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
};
|
|
149
185
|
const onError = (err) => {
|
|
150
|
-
|
|
151
|
-
reject(
|
|
186
|
+
cleanup();
|
|
187
|
+
reject(
|
|
188
|
+
new import_agents.APIConnectionError({
|
|
189
|
+
message: sanitizedErrorName(err),
|
|
190
|
+
options: { retryable: true }
|
|
191
|
+
})
|
|
192
|
+
);
|
|
152
193
|
};
|
|
153
194
|
ws.once("open", onOpen);
|
|
195
|
+
ws.once("unexpected-response", onUnexpectedResponse);
|
|
154
196
|
ws.once("error", onError);
|
|
155
197
|
});
|
|
156
198
|
let firstError;
|
|
@@ -249,7 +291,7 @@ class SpeechStream extends import_agents.stt.SpeechStream {
|
|
|
249
291
|
}
|
|
250
292
|
try {
|
|
251
293
|
if (msg.type === "error") {
|
|
252
|
-
this.#logger.error("Cartesia sent an error"
|
|
294
|
+
this.#logger.error({ "lk.pii.message": msg }, "Cartesia sent an error");
|
|
253
295
|
if (msg.status_code === void 0 || msg.status_code >= 500) {
|
|
254
296
|
pendingError = new import_agents.APIConnectionError({
|
|
255
297
|
message: msg.message || msg.title,
|
|
@@ -380,7 +422,7 @@ class SpeechStream extends import_agents.stt.SpeechStream {
|
|
|
380
422
|
return;
|
|
381
423
|
}
|
|
382
424
|
default:
|
|
383
|
-
this.#logger.warn("received unexpected message from Cartesia STT"
|
|
425
|
+
this.#logger.warn({ "lk.pii.data": data }, "received unexpected message from Cartesia STT");
|
|
384
426
|
}
|
|
385
427
|
}
|
|
386
428
|
#sendTranscriptEvent(eventType, transcript) {
|
package/dist/stt.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n AudioByteStream,\n asLanguageCode,\n calculateAudioDurationSeconds,\n getBaseLanguage,\n log,\n normalizeLanguage,\n stt,\n waitForAbort,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { IncomingMessage } from 'node:http';\nimport { WebSocket } from 'ws';\nimport { AUDIO_ENCODING, type STTModel } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'X-API-Key';\nconst VERSION_HEADER = 'Cartesia-Version';\nconst REQUEST_ID_HEADER = 'cartesia-request-id';\nconst API_VERSION = '2026-03-01';\nconst DRAIN_TIMEOUT_MS = 5000;\nconst KEEPALIVE_INTERVAL_MS = 30000;\n\n/**\n * Fires once when the WebSocket connection is established.\n *\n * You do not need to wait for this event before sending audio.\n */\ntype STTConnectedEvent = {\n type: 'connected';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Model predicts {@link stt.SpeechEventType.START_OF_SPEECH}.\n */\ntype STTTurnStartEvent = {\n type: 'turn.start';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires repeatedly as the model transcribes the current user turn.\n *\n * Can be used for {@link stt.SpeechEventType.INTERIM_TRANSCRIPT}.\n */\ntype STTTurnUpdateEvent = {\n type: 'turn.update';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires when the model predicts that the user might be done speaking.\n *\n * Can be used for {@link stt.SpeechEventType.PREFLIGHT_TRANSCRIPT}.\n */\ntype STTTurnEagerEndEvent = {\n type: 'turn.eager_end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires after {@link STTTurnEagerEndEvent} if the user turn has not actually ended.\n */\ntype STTTurnResumeEvent = {\n type: 'turn.resume';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Marks the end of a user turn.\n *\n * This is used for {@link stt.SpeechEventType.END_OF_SPEECH} and {@link stt.SpeechEventType.FINAL_TRANSCRIPT}.\n */\ntype STTTurnEndEvent = {\n type: 'turn.end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\ntype STTErrorEvent = {\n type: 'error';\n error_code?: string;\n status_code?: number;\n title?: string;\n message?: string;\n doc_url?: string;\n request_id?: string;\n};\n\n/**\n * Server-sent event on the `/stt/turns/websocket` endpoint.\n *\n * See https://docs.cartesia.ai/api-reference/stt/turns/websocket.\n */\ntype STTEventMessage =\n | STTConnectedEvent\n | STTTurnStartEvent\n | STTTurnUpdateEvent\n | STTTurnEagerEndEvent\n | STTTurnResumeEvent\n | STTTurnEndEvent\n | STTErrorEvent;\n\nexport type STTOptions = {\n apiKey: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n model: STTModel | (string & {});\n sampleRate: number;\n baseUrl: string;\n audioChunkDurationMS: number;\n language: string;\n};\n\nconst defaultSTTOptions = {\n model: 'ink-2' satisfies STTModel,\n /** recommended default */\n sampleRate: 16_000,\n /** recommended default */\n audioChunkDurationMS: 160,\n baseUrl: 'https://api.cartesia.ai',\n language: 'en',\n};\n\nfunction mergeSTTOptions(base: STTOptions, override: Partial<STTOptions>): STTOptions {\n return {\n apiKey: override.apiKey ?? base.apiKey,\n baseUrl: override.baseUrl ?? base.baseUrl,\n model: override.model ?? base.model,\n sampleRate: override.sampleRate ?? base.sampleRate,\n audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,\n language:\n override.language !== undefined ? normalizeLanguage(override.language) : base.language,\n };\n}\n\n/**\n * Resolve the STT model from the configured language when the caller did not\n * pass one explicitly. Mirrors the Python plugin: `ink-2` only supports\n * English, so non-English languages route to the multilingual `ink-whisper`.\n */\nfunction resolveSTTModel(language: string): STTModel {\n return getBaseLanguage(language) === 'en' ? 'ink-2' : 'ink-whisper';\n}\n\n/**\n * Cartesia speech to text.\n *\n * Supports:\n * - Streaming\n * - Turn detection\n * - Interim results\n *\n * See https://docs.cartesia.ai/build-with-cartesia/stt-models/latest\n *\n * @example\n * ```typescript\n * import { voice } from '@livekit/agents';\n * import { STT, TTS } from '@livekit/agents-plugin-cartesia';\n *\n * const session = new voice.AgentSession({\n * stt: new STT(),\n * llm: new LLM(), // choose your favorite LLM\n * tts: new TTS(),\n * turnHandling: { turnDetection: 'stt' },\n * });\n * ```\n */\nexport class STT extends stt.STT {\n #opts: STTOptions;\n\n constructor(opts: Partial<STTOptions> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: false,\n diarization: false,\n });\n\n const apiKey = opts.apiKey ?? process.env.CARTESIA_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Cartesia API key is required, whether as an argument or as $CARTESIA_API_KEY',\n );\n }\n\n this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);\n\n // Route to the multilingual model for non-English languages unless the\n // caller pinned a model. Matches the Python plugin's language→model logic.\n if (opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n\n override get label(): string {\n return 'cartesia.STT';\n }\n\n override get model(): string {\n return this.#opts.model;\n }\n\n override get provider(): string {\n return 'Cartesia';\n }\n\n protected override async _recognize(): Promise<stt.SpeechEvent> {\n throw new Error('Cartesia STT does not support batch recognition, use stream() instead');\n }\n\n override stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {\n return new SpeechStream(this, this.#opts, options?.connOptions);\n }\n\n updateOptions(opts: Partial<STTOptions>) {\n this.#opts = mergeSTTOptions(this.#opts, opts);\n\n // Keep the model in sync with a newly set language (e.g. switching to a\n // non-English language must move off the English-only ink-2), unless the\n // caller pinned a model in the same call. Mirrors the constructor.\n if (opts.language !== undefined && opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n #opts: STTOptions;\n #logger = log();\n #ws: WebSocket | null = null;\n #requestId: string | undefined;\n #speaking = false;\n #currentTranscript = '';\n #speechDuration = 0;\n #closingWs = false;\n\n constructor(sttInstance: STT, opts: STTOptions, connOptions?: APIConnectOptions) {\n super(sttInstance, opts.sampleRate, connOptions);\n this.#opts = { ...opts };\n }\n\n override get label(): string {\n return 'cartesia.SpeechStream';\n }\n\n protected override async run() {\n if (this.closed) {\n this.close();\n return;\n }\n\n this.#speaking = false;\n this.#currentTranscript = '';\n this.#speechDuration = 0;\n this.#closingWs = false;\n\n let keepaliveInterval: NodeJS.Timeout | undefined;\n const abortController = new AbortController();\n\n try {\n const url = this.#getCartesiaUrl();\n this.#logger.debug(`Connecting to Cartesia STT: ${url}`);\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: this.#opts.apiKey,\n [VERSION_HEADER]: API_VERSION,\n },\n });\n this.#ws = ws;\n\n // Cartesia returns the request id on the WS upgrade response, before any\n // turn events are sent. Capture it so logs and metrics emitted on the\n // connection (and any pre-`turn.start` errors) are attributable.\n ws.once('upgrade', (response: IncomingMessage) => {\n const headerValue = response.headers[REQUEST_ID_HEADER];\n const requestId = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n if (requestId) {\n this.#requestId = requestId;\n this.#logger.debug({ cartesiaRequestId: requestId }, 'Cartesia STT WebSocket connected');\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n ws.off('error', onError);\n resolve();\n };\n const onError = (err: Error) => {\n ws.off('open', onOpen);\n reject(new APIConnectionError({ message: err.message, options: { retryable: true } }));\n };\n\n ws.once('open', onOpen);\n ws.once('error', onError);\n });\n\n // If one task fails, abort its peer and close the WS so the peer can\n // exit before we re-throw. Otherwise a dangling task would survive\n // into the next retry attempt (sendTask blocked on input.next() would\n // steal frames; recvTask's close handler could still push events to\n // the queue and surface an unhandled rejection).\n let firstError: unknown;\n const stopPeer = (err: unknown) => {\n if (firstError === undefined) firstError = err;\n abortController.abort();\n if (ws.readyState === WebSocket.OPEN) ws.close();\n };\n const sendPromise = this.#sendTask(ws, abortController.signal).catch(stopPeer);\n const recvPromise = this.#recvTask(ws).catch(stopPeer);\n\n keepaliveInterval = setInterval(() => {\n try {\n if (ws.readyState === WebSocket.OPEN) {\n ws.ping();\n }\n } catch {\n // ignore\n }\n }, KEEPALIVE_INTERVAL_MS);\n await Promise.all([sendPromise, recvPromise]);\n if (firstError !== undefined) throw firstError;\n } catch (error) {\n this.#logger.error('Cartesia STT stream error', { error });\n throw error;\n } finally {\n abortController.abort();\n if (keepaliveInterval) clearInterval(keepaliveInterval);\n if (this.#ws?.readyState === WebSocket.OPEN) {\n this.#ws.close();\n }\n }\n\n this.close();\n }\n\n async #sendTask(ws: WebSocket, abortSignal: AbortSignal) {\n const samplesPerChunk = Math.floor(\n (this.#opts.sampleRate * this.#opts.audioChunkDurationMS) / 1000,\n );\n const audioBstream = new AudioByteStream(this.#opts.sampleRate, 1, samplesPerChunk);\n\n let hasEnded = false;\n const iterator = this.input[Symbol.asyncIterator]();\n const abortPromise = waitForAbort(abortSignal);\n\n while (true) {\n const result = await Promise.race([iterator.next(), abortPromise]);\n\n if (result === undefined) return; // aborted\n\n if (result.done) {\n hasEnded = true;\n } else {\n const data = result.value;\n const frames: AudioFrame[] = [];\n\n if (data === SpeechStream.FLUSH_SENTINEL) {\n frames.push(...audioBstream.flush());\n } else {\n // Pass the typed array directly so AudioByteStream uses byteOffset/\n // byteLength — `.buffer` would include foreign bytes when rtc-node\n // hands us a view over a pooled allocator.\n frames.push(...audioBstream.write(data.data));\n }\n\n for (const frame of frames) {\n this.#speechDuration += calculateAudioDurationSeconds(frame);\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(frame.data);\n }\n }\n }\n\n if (hasEnded) break;\n }\n\n if (ws.readyState === WebSocket.OPEN) {\n this.#closingWs = true;\n this.#logger.debug('Sending close message to Cartesia STT');\n ws.send(JSON.stringify({ type: 'close' }));\n\n // After `close`, the server should flush any pending turn events and\n // close the socket. If it hangs, force the issue rather than letting\n // the stream wait forever.\n const drainTimer = setTimeout(() => {\n this.#logger.warn(\n `Cartesia STT did not close within ${DRAIN_TIMEOUT_MS}ms after done; forcing close`,\n );\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {\n ws.terminate();\n }\n }, DRAIN_TIMEOUT_MS);\n ws.once('close', () => clearTimeout(drainTimer));\n }\n }\n\n async #recvTask(ws: WebSocket) {\n let pendingError: Error | undefined = undefined;\n\n return new Promise<void>((resolve, reject) => {\n const settle = (err?: Error) => {\n if (err) reject(err);\n else resolve();\n };\n\n ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n this.#logger.warn('Received unexpected binary message from Cartesia STT');\n return;\n }\n let msg: STTEventMessage;\n try {\n msg = JSON.parse(data.toString());\n } catch (error) {\n this.#logger.error('Failed to parse Cartesia STT message', { error });\n return;\n }\n try {\n if (msg.type === 'error') {\n this.#logger.error('Cartesia sent an error', msg);\n\n // do not close the websocket on bad requests since that may be caused by invalid messages\n if (msg.status_code === undefined || msg.status_code >= 500) {\n // Defer until the WS is fully closed so we don't race the `close`\n // handler with a rejected `recvTask`. Close the socket explicitly\n // in case the server doesn't, otherwise recv would hang.\n pendingError = new APIConnectionError({\n message: msg.message || msg.title,\n options: {\n retryable: true,\n },\n });\n ws.close();\n return;\n }\n } else {\n this.#processStreamEvent(msg);\n }\n } catch (error) {\n this.#logger.error('Failed to process Cartesia STT message', { error });\n }\n });\n\n ws.on('close', (code, reason) => {\n // If a turn was in progress, close it out so consumers see a balanced\n // START_OF_SPEECH/END_OF_SPEECH pair and don't carry stale turn state\n // into the next reconnect.\n if (this.#speaking) {\n if (this.#currentTranscript) {\n this.#sendTranscriptEvent(\n stt.SpeechEventType.FINAL_TRANSCRIPT,\n this.#currentTranscript,\n );\n }\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n this.#speaking = false;\n this.#currentTranscript = '';\n }\n\n if (pendingError) {\n settle(pendingError);\n return;\n }\n if (this.#closingWs || this.closed) {\n settle();\n return;\n }\n this.#logger.warn(`Cartesia STT WebSocket closed: ${code} ${reason.toString()}`);\n settle(\n new APIConnectionError({\n message: `Cartesia STT connection closed unexpectedly (code=${code})`,\n options: { retryable: true },\n }),\n );\n });\n\n ws.on('error', (err) => {\n if (this.closed) {\n settle();\n return;\n }\n settle(new APIConnectionError({ message: err.message, options: { retryable: true } }));\n });\n });\n }\n\n #processStreamEvent(data: Exclude<STTEventMessage, { type: 'error' }>) {\n if (data.request_id) {\n this.#requestId = data.request_id;\n }\n\n switch (data.type) {\n case 'connected':\n return;\n\n case 'turn.start': {\n if (this.#speaking) return;\n this.#speaking = true;\n this.#currentTranscript = '';\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.START_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT START_OF_SPEECH');\n return;\n }\n\n case 'turn.update': {\n if (!this.#speaking) return;\n const transcript = data.transcript;\n if (!transcript) return;\n // Only emit interim updates when the cumulative transcript actually\n // changed; this avoids canceling preflight generation needlessly.\n if (this.#currentTranscript === transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.eager_end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n if (!transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.resume':\n // turn.resume has no transcript; re-emit the latest cumulative transcript\n // as an interim event so the pipeline cancels the pending preflight.\n if (!this.#speaking || !this.#currentTranscript) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, this.#currentTranscript);\n return;\n\n case 'turn.end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n\n this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);\n\n this.#speaking = false;\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n this.#currentTranscript = '';\n\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n\n return;\n }\n\n default:\n this.#logger.warn('received unexpected message from Cartesia STT', { data });\n }\n }\n\n #sendTranscriptEvent(eventType: stt.SpeechEventType, transcript: string) {\n if (this.queue.closed) return;\n this.queue.put({\n type: eventType,\n requestId: this.#requestId,\n alternatives: [\n {\n language: asLanguageCode(this.#opts.language),\n text: transcript,\n startTime: 0,\n endTime: 0,\n confidence: 0,\n },\n ],\n });\n }\n\n #getCartesiaUrl(): string {\n // The Cartesia /stt/turns/websocket endpoint only accepts model, sample_rate\n // and encoding — there is no `language` query param. Language selection is\n // expressed through the model (ink-2 for English, ink-whisper otherwise),\n // so #opts.language is used only to tag emitted transcripts.\n const params = new URLSearchParams({\n model: this.#opts.model,\n sample_rate: this.#opts.sampleRate.toString(),\n encoding: AUDIO_ENCODING,\n });\n\n const wsBase = this.#opts.baseUrl.replace(/^http/, 'ws');\n return `${wsBase}/stt/turns/websocket?${params.toString()}`;\n }\n\n override close() {\n super.close();\n this.#ws?.close();\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n AudioByteStream,\n DEFAULT_API_CONNECT_OPTIONS,\n asError,\n asLanguageCode,\n calculateAudioDurationSeconds,\n getBaseLanguage,\n log,\n normalizeLanguage,\n stt,\n waitForAbort,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { IncomingMessage } from 'node:http';\nimport { WebSocket } from 'ws';\nimport { AUDIO_ENCODING, type STTModel } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'X-API-Key';\nconst VERSION_HEADER = 'Cartesia-Version';\nconst REQUEST_ID_HEADER = 'cartesia-request-id';\nconst API_VERSION = '2026-03-01';\nconst DRAIN_TIMEOUT_MS = 5000;\nconst KEEPALIVE_INTERVAL_MS = 30000;\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\n/**\n * Fires once when the WebSocket connection is established.\n *\n * You do not need to wait for this event before sending audio.\n */\ntype STTConnectedEvent = {\n type: 'connected';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Model predicts {@link stt.SpeechEventType.START_OF_SPEECH}.\n */\ntype STTTurnStartEvent = {\n type: 'turn.start';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires repeatedly as the model transcribes the current user turn.\n *\n * Can be used for {@link stt.SpeechEventType.INTERIM_TRANSCRIPT}.\n */\ntype STTTurnUpdateEvent = {\n type: 'turn.update';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires when the model predicts that the user might be done speaking.\n *\n * Can be used for {@link stt.SpeechEventType.PREFLIGHT_TRANSCRIPT}.\n */\ntype STTTurnEagerEndEvent = {\n type: 'turn.eager_end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires after {@link STTTurnEagerEndEvent} if the user turn has not actually ended.\n */\ntype STTTurnResumeEvent = {\n type: 'turn.resume';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Marks the end of a user turn.\n *\n * This is used for {@link stt.SpeechEventType.END_OF_SPEECH} and {@link stt.SpeechEventType.FINAL_TRANSCRIPT}.\n */\ntype STTTurnEndEvent = {\n type: 'turn.end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\ntype STTErrorEvent = {\n type: 'error';\n error_code?: string;\n status_code?: number;\n title?: string;\n message?: string;\n doc_url?: string;\n request_id?: string;\n};\n\n/**\n * Server-sent event on the `/stt/turns/websocket` endpoint.\n *\n * See https://docs.cartesia.ai/api-reference/stt/turns/websocket.\n */\ntype STTEventMessage =\n | STTConnectedEvent\n | STTTurnStartEvent\n | STTTurnUpdateEvent\n | STTTurnEagerEndEvent\n | STTTurnResumeEvent\n | STTTurnEndEvent\n | STTErrorEvent;\n\nexport type STTOptions = {\n apiKey: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n model: STTModel | (string & {});\n sampleRate: number;\n baseUrl: string;\n audioChunkDurationMS: number;\n language: string;\n};\n\nconst defaultSTTOptions = {\n model: 'ink-2' satisfies STTModel,\n /** recommended default */\n sampleRate: 16_000,\n /** recommended default */\n audioChunkDurationMS: 160,\n baseUrl: 'https://api.cartesia.ai',\n language: 'en',\n};\n\nfunction mergeSTTOptions(base: STTOptions, override: Partial<STTOptions>): STTOptions {\n return {\n apiKey: override.apiKey ?? base.apiKey,\n baseUrl: override.baseUrl ?? base.baseUrl,\n model: override.model ?? base.model,\n sampleRate: override.sampleRate ?? base.sampleRate,\n audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,\n language:\n override.language !== undefined ? normalizeLanguage(override.language) : base.language,\n };\n}\n\n/**\n * Resolve the STT model from the configured language when the caller did not\n * pass one explicitly. Mirrors the Python plugin: `ink-2` only supports\n * English, so non-English languages route to the multilingual `ink-whisper`.\n */\nfunction resolveSTTModel(language: string): STTModel {\n return getBaseLanguage(language) === 'en' ? 'ink-2' : 'ink-whisper';\n}\n\n/**\n * Cartesia speech to text.\n *\n * Supports:\n * - Streaming\n * - Turn detection\n * - Interim results\n *\n * See https://docs.cartesia.ai/build-with-cartesia/stt-models/latest\n *\n * @example\n * ```typescript\n * import { voice } from '@livekit/agents';\n * import { STT, TTS } from '@livekit/agents-plugin-cartesia';\n *\n * const session = new voice.AgentSession({\n * stt: new STT(),\n * llm: new LLM(), // choose your favorite LLM\n * tts: new TTS(),\n * turnHandling: { turnDetection: 'stt' },\n * });\n * ```\n */\nexport class STT extends stt.STT {\n #opts: STTOptions;\n\n constructor(opts: Partial<STTOptions> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: false,\n diarization: false,\n });\n\n const apiKey = opts.apiKey ?? process.env.CARTESIA_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Cartesia API key is required, whether as an argument or as $CARTESIA_API_KEY',\n );\n }\n\n this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);\n\n // Route to the multilingual model for non-English languages unless the\n // caller pinned a model. Matches the Python plugin's language→model logic.\n if (opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n\n override get label(): string {\n return 'cartesia.STT';\n }\n\n override get model(): string {\n return this.#opts.model;\n }\n\n override get provider(): string {\n return 'Cartesia';\n }\n\n protected override async _recognize(): Promise<stt.SpeechEvent> {\n throw new Error('Cartesia STT does not support batch recognition, use stream() instead');\n }\n\n override stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {\n return new SpeechStream(this, this.#opts, options?.connOptions);\n }\n\n updateOptions(opts: Partial<STTOptions>) {\n this.#opts = mergeSTTOptions(this.#opts, opts);\n\n // Keep the model in sync with a newly set language (e.g. switching to a\n // non-English language must move off the English-only ink-2), unless the\n // caller pinned a model in the same call. Mirrors the constructor.\n if (opts.language !== undefined && opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n #opts: STTOptions;\n #logger = log();\n #ws: WebSocket | null = null;\n #requestId: string | undefined;\n #speaking = false;\n #currentTranscript = '';\n #speechDuration = 0;\n #closingWs = false;\n #connectTimeout: number;\n\n constructor(sttInstance: STT, opts: STTOptions, connOptions?: APIConnectOptions) {\n super(sttInstance, opts.sampleRate, connOptions);\n this.#opts = { ...opts };\n this.#connectTimeout = connOptions?.timeoutMs ?? DEFAULT_API_CONNECT_OPTIONS.timeoutMs;\n }\n\n override get label(): string {\n return 'cartesia.SpeechStream';\n }\n\n protected override async run() {\n if (this.closed) {\n this.close();\n return;\n }\n\n this.#speaking = false;\n this.#currentTranscript = '';\n this.#speechDuration = 0;\n this.#closingWs = false;\n\n let keepaliveInterval: NodeJS.Timeout | undefined;\n const abortController = new AbortController();\n\n try {\n const url = this.#getCartesiaUrl();\n this.#logger.debug(`Connecting to Cartesia STT: ${url}`);\n\n let ws: WebSocket;\n try {\n ws = new WebSocket(url, {\n handshakeTimeout: this.#connectTimeout,\n headers: {\n [AUTHORIZATION_HEADER]: this.#opts.apiKey,\n [VERSION_HEADER]: API_VERSION,\n },\n });\n } catch (error) {\n throw new APIConnectionError({\n message: sanitizedErrorName(asError(error)),\n options: { retryable: true },\n });\n }\n this.#ws = ws;\n\n // Cartesia returns the request id on the WS upgrade response, before any\n // turn events are sent. Capture it so logs and metrics emitted on the\n // connection (and any pre-`turn.start` errors) are attributable.\n ws.once('upgrade', (response: IncomingMessage) => {\n const headerValue = response.headers[REQUEST_ID_HEADER];\n const requestId = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n if (requestId) {\n this.#requestId = requestId;\n this.#logger.debug({ cartesiaRequestId: requestId }, 'Cartesia STT WebSocket connected');\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n ws.off('open', onOpen);\n ws.off('unexpected-response', onUnexpectedResponse);\n ws.off('error', onError);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onUnexpectedResponse = (_request: unknown, response: { statusCode?: number }) => {\n cleanup();\n ws.on('error', () => {});\n ws.close();\n // Authentication headers can appear in WebSocket handshake errors.\n const statusCode = response.statusCode ?? -1;\n reject(\n new APIStatusError({\n message: `Cartesia WebSocket connection rejected with status ${statusCode}`,\n options: { statusCode },\n }),\n );\n };\n const onError = (err: Error) => {\n cleanup();\n // Transport errors can contain credentials in URLs.\n reject(\n new APIConnectionError({\n message: sanitizedErrorName(err),\n options: { retryable: true },\n }),\n );\n };\n\n ws.once('open', onOpen);\n ws.once('unexpected-response', onUnexpectedResponse);\n ws.once('error', onError);\n });\n\n // If one task fails, abort its peer and close the WS so the peer can\n // exit before we re-throw. Otherwise a dangling task would survive\n // into the next retry attempt (sendTask blocked on input.next() would\n // steal frames; recvTask's close handler could still push events to\n // the queue and surface an unhandled rejection).\n let firstError: unknown;\n const stopPeer = (err: unknown) => {\n if (firstError === undefined) firstError = err;\n abortController.abort();\n if (ws.readyState === WebSocket.OPEN) ws.close();\n };\n const sendPromise = this.#sendTask(ws, abortController.signal).catch(stopPeer);\n const recvPromise = this.#recvTask(ws).catch(stopPeer);\n\n keepaliveInterval = setInterval(() => {\n try {\n if (ws.readyState === WebSocket.OPEN) {\n ws.ping();\n }\n } catch {\n // ignore\n }\n }, KEEPALIVE_INTERVAL_MS);\n await Promise.all([sendPromise, recvPromise]);\n if (firstError !== undefined) throw firstError;\n } catch (error) {\n this.#logger.error('Cartesia STT stream error', { error });\n throw error;\n } finally {\n abortController.abort();\n if (keepaliveInterval) clearInterval(keepaliveInterval);\n if (this.#ws?.readyState === WebSocket.OPEN) {\n this.#ws.close();\n }\n }\n\n this.close();\n }\n\n async #sendTask(ws: WebSocket, abortSignal: AbortSignal) {\n const samplesPerChunk = Math.floor(\n (this.#opts.sampleRate * this.#opts.audioChunkDurationMS) / 1000,\n );\n const audioBstream = new AudioByteStream(this.#opts.sampleRate, 1, samplesPerChunk);\n\n let hasEnded = false;\n const iterator = this.input[Symbol.asyncIterator]();\n const abortPromise = waitForAbort(abortSignal);\n\n while (true) {\n const result = await Promise.race([iterator.next(), abortPromise]);\n\n if (result === undefined) return; // aborted\n\n if (result.done) {\n hasEnded = true;\n } else {\n const data = result.value;\n const frames: AudioFrame[] = [];\n\n if (data === SpeechStream.FLUSH_SENTINEL) {\n frames.push(...audioBstream.flush());\n } else {\n // Pass the typed array directly so AudioByteStream uses byteOffset/\n // byteLength — `.buffer` would include foreign bytes when rtc-node\n // hands us a view over a pooled allocator.\n frames.push(...audioBstream.write(data.data));\n }\n\n for (const frame of frames) {\n this.#speechDuration += calculateAudioDurationSeconds(frame);\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(frame.data);\n }\n }\n }\n\n if (hasEnded) break;\n }\n\n if (ws.readyState === WebSocket.OPEN) {\n this.#closingWs = true;\n this.#logger.debug('Sending close message to Cartesia STT');\n ws.send(JSON.stringify({ type: 'close' }));\n\n // After `close`, the server should flush any pending turn events and\n // close the socket. If it hangs, force the issue rather than letting\n // the stream wait forever.\n const drainTimer = setTimeout(() => {\n this.#logger.warn(\n `Cartesia STT did not close within ${DRAIN_TIMEOUT_MS}ms after done; forcing close`,\n );\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {\n ws.terminate();\n }\n }, DRAIN_TIMEOUT_MS);\n ws.once('close', () => clearTimeout(drainTimer));\n }\n }\n\n async #recvTask(ws: WebSocket) {\n let pendingError: Error | undefined = undefined;\n\n return new Promise<void>((resolve, reject) => {\n const settle = (err?: Error) => {\n if (err) reject(err);\n else resolve();\n };\n\n ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n this.#logger.warn('Received unexpected binary message from Cartesia STT');\n return;\n }\n let msg: STTEventMessage;\n try {\n msg = JSON.parse(data.toString());\n } catch (error) {\n this.#logger.error('Failed to parse Cartesia STT message', { error });\n return;\n }\n try {\n if (msg.type === 'error') {\n this.#logger.error({ 'lk.pii.message': msg }, 'Cartesia sent an error');\n\n // do not close the websocket on bad requests since that may be caused by invalid messages\n if (msg.status_code === undefined || msg.status_code >= 500) {\n // Defer until the WS is fully closed so we don't race the `close`\n // handler with a rejected `recvTask`. Close the socket explicitly\n // in case the server doesn't, otherwise recv would hang.\n pendingError = new APIConnectionError({\n message: msg.message || msg.title,\n options: {\n retryable: true,\n },\n });\n ws.close();\n return;\n }\n } else {\n this.#processStreamEvent(msg);\n }\n } catch (error) {\n this.#logger.error('Failed to process Cartesia STT message', { error });\n }\n });\n\n ws.on('close', (code, reason) => {\n // If a turn was in progress, close it out so consumers see a balanced\n // START_OF_SPEECH/END_OF_SPEECH pair and don't carry stale turn state\n // into the next reconnect.\n if (this.#speaking) {\n if (this.#currentTranscript) {\n this.#sendTranscriptEvent(\n stt.SpeechEventType.FINAL_TRANSCRIPT,\n this.#currentTranscript,\n );\n }\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n this.#speaking = false;\n this.#currentTranscript = '';\n }\n\n if (pendingError) {\n settle(pendingError);\n return;\n }\n if (this.#closingWs || this.closed) {\n settle();\n return;\n }\n this.#logger.warn(`Cartesia STT WebSocket closed: ${code} ${reason.toString()}`);\n settle(\n new APIConnectionError({\n message: `Cartesia STT connection closed unexpectedly (code=${code})`,\n options: { retryable: true },\n }),\n );\n });\n\n ws.on('error', (err) => {\n if (this.closed) {\n settle();\n return;\n }\n settle(new APIConnectionError({ message: err.message, options: { retryable: true } }));\n });\n });\n }\n\n #processStreamEvent(data: Exclude<STTEventMessage, { type: 'error' }>) {\n if (data.request_id) {\n this.#requestId = data.request_id;\n }\n\n switch (data.type) {\n case 'connected':\n return;\n\n case 'turn.start': {\n if (this.#speaking) return;\n this.#speaking = true;\n this.#currentTranscript = '';\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.START_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT START_OF_SPEECH');\n return;\n }\n\n case 'turn.update': {\n if (!this.#speaking) return;\n const transcript = data.transcript;\n if (!transcript) return;\n // Only emit interim updates when the cumulative transcript actually\n // changed; this avoids canceling preflight generation needlessly.\n if (this.#currentTranscript === transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.eager_end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n if (!transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.resume':\n // turn.resume has no transcript; re-emit the latest cumulative transcript\n // as an interim event so the pipeline cancels the pending preflight.\n if (!this.#speaking || !this.#currentTranscript) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, this.#currentTranscript);\n return;\n\n case 'turn.end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n\n this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);\n\n this.#speaking = false;\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n this.#currentTranscript = '';\n\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n\n return;\n }\n\n default:\n this.#logger.warn({ 'lk.pii.data': data }, 'received unexpected message from Cartesia STT');\n }\n }\n\n #sendTranscriptEvent(eventType: stt.SpeechEventType, transcript: string) {\n if (this.queue.closed) return;\n this.queue.put({\n type: eventType,\n requestId: this.#requestId,\n alternatives: [\n {\n language: asLanguageCode(this.#opts.language),\n text: transcript,\n startTime: 0,\n endTime: 0,\n confidence: 0,\n },\n ],\n });\n }\n\n #getCartesiaUrl(): string {\n // The Cartesia /stt/turns/websocket endpoint only accepts model, sample_rate\n // and encoding — there is no `language` query param. Language selection is\n // expressed through the model (ink-2 for English, ink-whisper otherwise),\n // so #opts.language is used only to tag emitted transcripts.\n const params = new URLSearchParams({\n model: this.#opts.model,\n sample_rate: this.#opts.sampleRate.toString(),\n encoding: AUDIO_ENCODING,\n });\n\n const wsBase = this.#opts.baseUrl.replace(/^http/, 'ws');\n return `${wsBase}/stt/turns/websocket?${params.toString()}`;\n }\n\n override close() {\n super.close();\n this.#ws?.close();\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/stt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stt.d.ts","sourceRoot":"","sources":["../src/stt.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,
|
|
1
|
+
{"version":3,"file":"stt.d.ts","sourceRoot":"","sources":["../src/stt.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAWtB,GAAG,EAEJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AA+H5D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IAEf,KAAK,EAAE,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAiCF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;;gBAGlB,IAAI,GAAE,OAAO,CAAC,UAAU,CAAM;IAwB1C,IAAa,KAAK,IAAI,MAAM,CAE3B;IAED,IAAa,KAAK,IAAI,MAAM,CAE3B;IAED,IAAa,QAAQ,IAAI,MAAM,CAE9B;cAEwB,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IAItD,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,YAAY;IAI5E,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC;CAUxC;AAED,qBAAa,YAAa,SAAQ,GAAG,CAAC,YAAY;;gBAWpC,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,iBAAiB;IAM/E,IAAa,KAAK,IAAI,MAAM,CAE3B;cAEwB,GAAG;IAmZnB,KAAK;CAIf"}
|
package/dist/stt.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
APIConnectionError,
|
|
3
|
+
APIStatusError,
|
|
3
4
|
AudioByteStream,
|
|
5
|
+
DEFAULT_API_CONNECT_OPTIONS,
|
|
6
|
+
asError,
|
|
4
7
|
asLanguageCode,
|
|
5
8
|
calculateAudioDurationSeconds,
|
|
6
9
|
getBaseLanguage,
|
|
@@ -17,6 +20,13 @@ const REQUEST_ID_HEADER = "cartesia-request-id";
|
|
|
17
20
|
const API_VERSION = "2026-03-01";
|
|
18
21
|
const DRAIN_TIMEOUT_MS = 5e3;
|
|
19
22
|
const KEEPALIVE_INTERVAL_MS = 3e4;
|
|
23
|
+
const sanitizedErrorName = (error) => {
|
|
24
|
+
if (error instanceof SyntaxError) return "SyntaxError";
|
|
25
|
+
if (error instanceof TypeError) return "TypeError";
|
|
26
|
+
if (error instanceof RangeError) return "RangeError";
|
|
27
|
+
if (error instanceof AggregateError) return "AggregateError";
|
|
28
|
+
return "Error";
|
|
29
|
+
};
|
|
20
30
|
const defaultSTTOptions = {
|
|
21
31
|
model: "ink-2",
|
|
22
32
|
/** recommended default */
|
|
@@ -90,9 +100,11 @@ class SpeechStream extends stt.SpeechStream {
|
|
|
90
100
|
#currentTranscript = "";
|
|
91
101
|
#speechDuration = 0;
|
|
92
102
|
#closingWs = false;
|
|
103
|
+
#connectTimeout;
|
|
93
104
|
constructor(sttInstance, opts, connOptions) {
|
|
94
105
|
super(sttInstance, opts.sampleRate, connOptions);
|
|
95
106
|
this.#opts = { ...opts };
|
|
107
|
+
this.#connectTimeout = (connOptions == null ? void 0 : connOptions.timeoutMs) ?? DEFAULT_API_CONNECT_OPTIONS.timeoutMs;
|
|
96
108
|
}
|
|
97
109
|
get label() {
|
|
98
110
|
return "cartesia.SpeechStream";
|
|
@@ -112,12 +124,21 @@ class SpeechStream extends stt.SpeechStream {
|
|
|
112
124
|
try {
|
|
113
125
|
const url = this.#getCartesiaUrl();
|
|
114
126
|
this.#logger.debug(`Connecting to Cartesia STT: ${url}`);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
let ws;
|
|
128
|
+
try {
|
|
129
|
+
ws = new WebSocket(url, {
|
|
130
|
+
handshakeTimeout: this.#connectTimeout,
|
|
131
|
+
headers: {
|
|
132
|
+
[AUTHORIZATION_HEADER]: this.#opts.apiKey,
|
|
133
|
+
[VERSION_HEADER]: API_VERSION
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new APIConnectionError({
|
|
138
|
+
message: sanitizedErrorName(asError(error)),
|
|
139
|
+
options: { retryable: true }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
121
142
|
this.#ws = ws;
|
|
122
143
|
ws.once("upgrade", (response) => {
|
|
123
144
|
const headerValue = response.headers[REQUEST_ID_HEADER];
|
|
@@ -128,15 +149,39 @@ class SpeechStream extends stt.SpeechStream {
|
|
|
128
149
|
}
|
|
129
150
|
});
|
|
130
151
|
await new Promise((resolve, reject) => {
|
|
131
|
-
const
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
ws.off("open", onOpen);
|
|
154
|
+
ws.off("unexpected-response", onUnexpectedResponse);
|
|
132
155
|
ws.off("error", onError);
|
|
156
|
+
};
|
|
157
|
+
const onOpen = () => {
|
|
158
|
+
cleanup();
|
|
133
159
|
resolve();
|
|
134
160
|
};
|
|
161
|
+
const onUnexpectedResponse = (_request, response) => {
|
|
162
|
+
cleanup();
|
|
163
|
+
ws.on("error", () => {
|
|
164
|
+
});
|
|
165
|
+
ws.close();
|
|
166
|
+
const statusCode = response.statusCode ?? -1;
|
|
167
|
+
reject(
|
|
168
|
+
new APIStatusError({
|
|
169
|
+
message: `Cartesia WebSocket connection rejected with status ${statusCode}`,
|
|
170
|
+
options: { statusCode }
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
};
|
|
135
174
|
const onError = (err) => {
|
|
136
|
-
|
|
137
|
-
reject(
|
|
175
|
+
cleanup();
|
|
176
|
+
reject(
|
|
177
|
+
new APIConnectionError({
|
|
178
|
+
message: sanitizedErrorName(err),
|
|
179
|
+
options: { retryable: true }
|
|
180
|
+
})
|
|
181
|
+
);
|
|
138
182
|
};
|
|
139
183
|
ws.once("open", onOpen);
|
|
184
|
+
ws.once("unexpected-response", onUnexpectedResponse);
|
|
140
185
|
ws.once("error", onError);
|
|
141
186
|
});
|
|
142
187
|
let firstError;
|
|
@@ -235,7 +280,7 @@ class SpeechStream extends stt.SpeechStream {
|
|
|
235
280
|
}
|
|
236
281
|
try {
|
|
237
282
|
if (msg.type === "error") {
|
|
238
|
-
this.#logger.error("Cartesia sent an error"
|
|
283
|
+
this.#logger.error({ "lk.pii.message": msg }, "Cartesia sent an error");
|
|
239
284
|
if (msg.status_code === void 0 || msg.status_code >= 500) {
|
|
240
285
|
pendingError = new APIConnectionError({
|
|
241
286
|
message: msg.message || msg.title,
|
|
@@ -366,7 +411,7 @@ class SpeechStream extends stt.SpeechStream {
|
|
|
366
411
|
return;
|
|
367
412
|
}
|
|
368
413
|
default:
|
|
369
|
-
this.#logger.warn("received unexpected message from Cartesia STT"
|
|
414
|
+
this.#logger.warn({ "lk.pii.data": data }, "received unexpected message from Cartesia STT");
|
|
370
415
|
}
|
|
371
416
|
}
|
|
372
417
|
#sendTranscriptEvent(eventType, transcript) {
|
package/dist/stt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n AudioByteStream,\n asLanguageCode,\n calculateAudioDurationSeconds,\n getBaseLanguage,\n log,\n normalizeLanguage,\n stt,\n waitForAbort,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { IncomingMessage } from 'node:http';\nimport { WebSocket } from 'ws';\nimport { AUDIO_ENCODING, type STTModel } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'X-API-Key';\nconst VERSION_HEADER = 'Cartesia-Version';\nconst REQUEST_ID_HEADER = 'cartesia-request-id';\nconst API_VERSION = '2026-03-01';\nconst DRAIN_TIMEOUT_MS = 5000;\nconst KEEPALIVE_INTERVAL_MS = 30000;\n\n/**\n * Fires once when the WebSocket connection is established.\n *\n * You do not need to wait for this event before sending audio.\n */\ntype STTConnectedEvent = {\n type: 'connected';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Model predicts {@link stt.SpeechEventType.START_OF_SPEECH}.\n */\ntype STTTurnStartEvent = {\n type: 'turn.start';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires repeatedly as the model transcribes the current user turn.\n *\n * Can be used for {@link stt.SpeechEventType.INTERIM_TRANSCRIPT}.\n */\ntype STTTurnUpdateEvent = {\n type: 'turn.update';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires when the model predicts that the user might be done speaking.\n *\n * Can be used for {@link stt.SpeechEventType.PREFLIGHT_TRANSCRIPT}.\n */\ntype STTTurnEagerEndEvent = {\n type: 'turn.eager_end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires after {@link STTTurnEagerEndEvent} if the user turn has not actually ended.\n */\ntype STTTurnResumeEvent = {\n type: 'turn.resume';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Marks the end of a user turn.\n *\n * This is used for {@link stt.SpeechEventType.END_OF_SPEECH} and {@link stt.SpeechEventType.FINAL_TRANSCRIPT}.\n */\ntype STTTurnEndEvent = {\n type: 'turn.end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\ntype STTErrorEvent = {\n type: 'error';\n error_code?: string;\n status_code?: number;\n title?: string;\n message?: string;\n doc_url?: string;\n request_id?: string;\n};\n\n/**\n * Server-sent event on the `/stt/turns/websocket` endpoint.\n *\n * See https://docs.cartesia.ai/api-reference/stt/turns/websocket.\n */\ntype STTEventMessage =\n | STTConnectedEvent\n | STTTurnStartEvent\n | STTTurnUpdateEvent\n | STTTurnEagerEndEvent\n | STTTurnResumeEvent\n | STTTurnEndEvent\n | STTErrorEvent;\n\nexport type STTOptions = {\n apiKey: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n model: STTModel | (string & {});\n sampleRate: number;\n baseUrl: string;\n audioChunkDurationMS: number;\n language: string;\n};\n\nconst defaultSTTOptions = {\n model: 'ink-2' satisfies STTModel,\n /** recommended default */\n sampleRate: 16_000,\n /** recommended default */\n audioChunkDurationMS: 160,\n baseUrl: 'https://api.cartesia.ai',\n language: 'en',\n};\n\nfunction mergeSTTOptions(base: STTOptions, override: Partial<STTOptions>): STTOptions {\n return {\n apiKey: override.apiKey ?? base.apiKey,\n baseUrl: override.baseUrl ?? base.baseUrl,\n model: override.model ?? base.model,\n sampleRate: override.sampleRate ?? base.sampleRate,\n audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,\n language:\n override.language !== undefined ? normalizeLanguage(override.language) : base.language,\n };\n}\n\n/**\n * Resolve the STT model from the configured language when the caller did not\n * pass one explicitly. Mirrors the Python plugin: `ink-2` only supports\n * English, so non-English languages route to the multilingual `ink-whisper`.\n */\nfunction resolveSTTModel(language: string): STTModel {\n return getBaseLanguage(language) === 'en' ? 'ink-2' : 'ink-whisper';\n}\n\n/**\n * Cartesia speech to text.\n *\n * Supports:\n * - Streaming\n * - Turn detection\n * - Interim results\n *\n * See https://docs.cartesia.ai/build-with-cartesia/stt-models/latest\n *\n * @example\n * ```typescript\n * import { voice } from '@livekit/agents';\n * import { STT, TTS } from '@livekit/agents-plugin-cartesia';\n *\n * const session = new voice.AgentSession({\n * stt: new STT(),\n * llm: new LLM(), // choose your favorite LLM\n * tts: new TTS(),\n * turnHandling: { turnDetection: 'stt' },\n * });\n * ```\n */\nexport class STT extends stt.STT {\n #opts: STTOptions;\n\n constructor(opts: Partial<STTOptions> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: false,\n diarization: false,\n });\n\n const apiKey = opts.apiKey ?? process.env.CARTESIA_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Cartesia API key is required, whether as an argument or as $CARTESIA_API_KEY',\n );\n }\n\n this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);\n\n // Route to the multilingual model for non-English languages unless the\n // caller pinned a model. Matches the Python plugin's language→model logic.\n if (opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n\n override get label(): string {\n return 'cartesia.STT';\n }\n\n override get model(): string {\n return this.#opts.model;\n }\n\n override get provider(): string {\n return 'Cartesia';\n }\n\n protected override async _recognize(): Promise<stt.SpeechEvent> {\n throw new Error('Cartesia STT does not support batch recognition, use stream() instead');\n }\n\n override stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {\n return new SpeechStream(this, this.#opts, options?.connOptions);\n }\n\n updateOptions(opts: Partial<STTOptions>) {\n this.#opts = mergeSTTOptions(this.#opts, opts);\n\n // Keep the model in sync with a newly set language (e.g. switching to a\n // non-English language must move off the English-only ink-2), unless the\n // caller pinned a model in the same call. Mirrors the constructor.\n if (opts.language !== undefined && opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n #opts: STTOptions;\n #logger = log();\n #ws: WebSocket | null = null;\n #requestId: string | undefined;\n #speaking = false;\n #currentTranscript = '';\n #speechDuration = 0;\n #closingWs = false;\n\n constructor(sttInstance: STT, opts: STTOptions, connOptions?: APIConnectOptions) {\n super(sttInstance, opts.sampleRate, connOptions);\n this.#opts = { ...opts };\n }\n\n override get label(): string {\n return 'cartesia.SpeechStream';\n }\n\n protected override async run() {\n if (this.closed) {\n this.close();\n return;\n }\n\n this.#speaking = false;\n this.#currentTranscript = '';\n this.#speechDuration = 0;\n this.#closingWs = false;\n\n let keepaliveInterval: NodeJS.Timeout | undefined;\n const abortController = new AbortController();\n\n try {\n const url = this.#getCartesiaUrl();\n this.#logger.debug(`Connecting to Cartesia STT: ${url}`);\n\n const ws = new WebSocket(url, {\n headers: {\n [AUTHORIZATION_HEADER]: this.#opts.apiKey,\n [VERSION_HEADER]: API_VERSION,\n },\n });\n this.#ws = ws;\n\n // Cartesia returns the request id on the WS upgrade response, before any\n // turn events are sent. Capture it so logs and metrics emitted on the\n // connection (and any pre-`turn.start` errors) are attributable.\n ws.once('upgrade', (response: IncomingMessage) => {\n const headerValue = response.headers[REQUEST_ID_HEADER];\n const requestId = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n if (requestId) {\n this.#requestId = requestId;\n this.#logger.debug({ cartesiaRequestId: requestId }, 'Cartesia STT WebSocket connected');\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const onOpen = () => {\n ws.off('error', onError);\n resolve();\n };\n const onError = (err: Error) => {\n ws.off('open', onOpen);\n reject(new APIConnectionError({ message: err.message, options: { retryable: true } }));\n };\n\n ws.once('open', onOpen);\n ws.once('error', onError);\n });\n\n // If one task fails, abort its peer and close the WS so the peer can\n // exit before we re-throw. Otherwise a dangling task would survive\n // into the next retry attempt (sendTask blocked on input.next() would\n // steal frames; recvTask's close handler could still push events to\n // the queue and surface an unhandled rejection).\n let firstError: unknown;\n const stopPeer = (err: unknown) => {\n if (firstError === undefined) firstError = err;\n abortController.abort();\n if (ws.readyState === WebSocket.OPEN) ws.close();\n };\n const sendPromise = this.#sendTask(ws, abortController.signal).catch(stopPeer);\n const recvPromise = this.#recvTask(ws).catch(stopPeer);\n\n keepaliveInterval = setInterval(() => {\n try {\n if (ws.readyState === WebSocket.OPEN) {\n ws.ping();\n }\n } catch {\n // ignore\n }\n }, KEEPALIVE_INTERVAL_MS);\n await Promise.all([sendPromise, recvPromise]);\n if (firstError !== undefined) throw firstError;\n } catch (error) {\n this.#logger.error('Cartesia STT stream error', { error });\n throw error;\n } finally {\n abortController.abort();\n if (keepaliveInterval) clearInterval(keepaliveInterval);\n if (this.#ws?.readyState === WebSocket.OPEN) {\n this.#ws.close();\n }\n }\n\n this.close();\n }\n\n async #sendTask(ws: WebSocket, abortSignal: AbortSignal) {\n const samplesPerChunk = Math.floor(\n (this.#opts.sampleRate * this.#opts.audioChunkDurationMS) / 1000,\n );\n const audioBstream = new AudioByteStream(this.#opts.sampleRate, 1, samplesPerChunk);\n\n let hasEnded = false;\n const iterator = this.input[Symbol.asyncIterator]();\n const abortPromise = waitForAbort(abortSignal);\n\n while (true) {\n const result = await Promise.race([iterator.next(), abortPromise]);\n\n if (result === undefined) return; // aborted\n\n if (result.done) {\n hasEnded = true;\n } else {\n const data = result.value;\n const frames: AudioFrame[] = [];\n\n if (data === SpeechStream.FLUSH_SENTINEL) {\n frames.push(...audioBstream.flush());\n } else {\n // Pass the typed array directly so AudioByteStream uses byteOffset/\n // byteLength — `.buffer` would include foreign bytes when rtc-node\n // hands us a view over a pooled allocator.\n frames.push(...audioBstream.write(data.data));\n }\n\n for (const frame of frames) {\n this.#speechDuration += calculateAudioDurationSeconds(frame);\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(frame.data);\n }\n }\n }\n\n if (hasEnded) break;\n }\n\n if (ws.readyState === WebSocket.OPEN) {\n this.#closingWs = true;\n this.#logger.debug('Sending close message to Cartesia STT');\n ws.send(JSON.stringify({ type: 'close' }));\n\n // After `close`, the server should flush any pending turn events and\n // close the socket. If it hangs, force the issue rather than letting\n // the stream wait forever.\n const drainTimer = setTimeout(() => {\n this.#logger.warn(\n `Cartesia STT did not close within ${DRAIN_TIMEOUT_MS}ms after done; forcing close`,\n );\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {\n ws.terminate();\n }\n }, DRAIN_TIMEOUT_MS);\n ws.once('close', () => clearTimeout(drainTimer));\n }\n }\n\n async #recvTask(ws: WebSocket) {\n let pendingError: Error | undefined = undefined;\n\n return new Promise<void>((resolve, reject) => {\n const settle = (err?: Error) => {\n if (err) reject(err);\n else resolve();\n };\n\n ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n this.#logger.warn('Received unexpected binary message from Cartesia STT');\n return;\n }\n let msg: STTEventMessage;\n try {\n msg = JSON.parse(data.toString());\n } catch (error) {\n this.#logger.error('Failed to parse Cartesia STT message', { error });\n return;\n }\n try {\n if (msg.type === 'error') {\n this.#logger.error('Cartesia sent an error', msg);\n\n // do not close the websocket on bad requests since that may be caused by invalid messages\n if (msg.status_code === undefined || msg.status_code >= 500) {\n // Defer until the WS is fully closed so we don't race the `close`\n // handler with a rejected `recvTask`. Close the socket explicitly\n // in case the server doesn't, otherwise recv would hang.\n pendingError = new APIConnectionError({\n message: msg.message || msg.title,\n options: {\n retryable: true,\n },\n });\n ws.close();\n return;\n }\n } else {\n this.#processStreamEvent(msg);\n }\n } catch (error) {\n this.#logger.error('Failed to process Cartesia STT message', { error });\n }\n });\n\n ws.on('close', (code, reason) => {\n // If a turn was in progress, close it out so consumers see a balanced\n // START_OF_SPEECH/END_OF_SPEECH pair and don't carry stale turn state\n // into the next reconnect.\n if (this.#speaking) {\n if (this.#currentTranscript) {\n this.#sendTranscriptEvent(\n stt.SpeechEventType.FINAL_TRANSCRIPT,\n this.#currentTranscript,\n );\n }\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n this.#speaking = false;\n this.#currentTranscript = '';\n }\n\n if (pendingError) {\n settle(pendingError);\n return;\n }\n if (this.#closingWs || this.closed) {\n settle();\n return;\n }\n this.#logger.warn(`Cartesia STT WebSocket closed: ${code} ${reason.toString()}`);\n settle(\n new APIConnectionError({\n message: `Cartesia STT connection closed unexpectedly (code=${code})`,\n options: { retryable: true },\n }),\n );\n });\n\n ws.on('error', (err) => {\n if (this.closed) {\n settle();\n return;\n }\n settle(new APIConnectionError({ message: err.message, options: { retryable: true } }));\n });\n });\n }\n\n #processStreamEvent(data: Exclude<STTEventMessage, { type: 'error' }>) {\n if (data.request_id) {\n this.#requestId = data.request_id;\n }\n\n switch (data.type) {\n case 'connected':\n return;\n\n case 'turn.start': {\n if (this.#speaking) return;\n this.#speaking = true;\n this.#currentTranscript = '';\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.START_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT START_OF_SPEECH');\n return;\n }\n\n case 'turn.update': {\n if (!this.#speaking) return;\n const transcript = data.transcript;\n if (!transcript) return;\n // Only emit interim updates when the cumulative transcript actually\n // changed; this avoids canceling preflight generation needlessly.\n if (this.#currentTranscript === transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.eager_end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n if (!transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.resume':\n // turn.resume has no transcript; re-emit the latest cumulative transcript\n // as an interim event so the pipeline cancels the pending preflight.\n if (!this.#speaking || !this.#currentTranscript) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, this.#currentTranscript);\n return;\n\n case 'turn.end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n\n this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);\n\n this.#speaking = false;\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n this.#currentTranscript = '';\n\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n\n return;\n }\n\n default:\n this.#logger.warn('received unexpected message from Cartesia STT', { data });\n }\n }\n\n #sendTranscriptEvent(eventType: stt.SpeechEventType, transcript: string) {\n if (this.queue.closed) return;\n this.queue.put({\n type: eventType,\n requestId: this.#requestId,\n alternatives: [\n {\n language: asLanguageCode(this.#opts.language),\n text: transcript,\n startTime: 0,\n endTime: 0,\n confidence: 0,\n },\n ],\n });\n }\n\n #getCartesiaUrl(): string {\n // The Cartesia /stt/turns/websocket endpoint only accepts model, sample_rate\n // and encoding — there is no `language` query param. Language selection is\n // expressed through the model (ink-2 for English, ink-whisper otherwise),\n // so #opts.language is used only to tag emitted transcripts.\n const params = new URLSearchParams({\n model: this.#opts.model,\n sample_rate: this.#opts.sampleRate.toString(),\n encoding: AUDIO_ENCODING,\n });\n\n const wsBase = this.#opts.baseUrl.replace(/^http/, 'ws');\n return `${wsBase}/stt/turns/websocket?${params.toString()}`;\n }\n\n override close() {\n super.close();\n this.#ws?.close();\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport {\n type APIConnectOptions,\n APIConnectionError,\n APIStatusError,\n AudioByteStream,\n DEFAULT_API_CONNECT_OPTIONS,\n asError,\n asLanguageCode,\n calculateAudioDurationSeconds,\n getBaseLanguage,\n log,\n normalizeLanguage,\n stt,\n waitForAbort,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { IncomingMessage } from 'node:http';\nimport { WebSocket } from 'ws';\nimport { AUDIO_ENCODING, type STTModel } from './models.js';\n\nconst AUTHORIZATION_HEADER = 'X-API-Key';\nconst VERSION_HEADER = 'Cartesia-Version';\nconst REQUEST_ID_HEADER = 'cartesia-request-id';\nconst API_VERSION = '2026-03-01';\nconst DRAIN_TIMEOUT_MS = 5000;\nconst KEEPALIVE_INTERVAL_MS = 30000;\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\n/**\n * Fires once when the WebSocket connection is established.\n *\n * You do not need to wait for this event before sending audio.\n */\ntype STTConnectedEvent = {\n type: 'connected';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Model predicts {@link stt.SpeechEventType.START_OF_SPEECH}.\n */\ntype STTTurnStartEvent = {\n type: 'turn.start';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires repeatedly as the model transcribes the current user turn.\n *\n * Can be used for {@link stt.SpeechEventType.INTERIM_TRANSCRIPT}.\n */\ntype STTTurnUpdateEvent = {\n type: 'turn.update';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires when the model predicts that the user might be done speaking.\n *\n * Can be used for {@link stt.SpeechEventType.PREFLIGHT_TRANSCRIPT}.\n */\ntype STTTurnEagerEndEvent = {\n type: 'turn.eager_end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Fires after {@link STTTurnEagerEndEvent} if the user turn has not actually ended.\n */\ntype STTTurnResumeEvent = {\n type: 'turn.resume';\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\n/**\n * Marks the end of a user turn.\n *\n * This is used for {@link stt.SpeechEventType.END_OF_SPEECH} and {@link stt.SpeechEventType.FINAL_TRANSCRIPT}.\n */\ntype STTTurnEndEvent = {\n type: 'turn.end';\n /**\n * Cumulative text for the current turn, i.e. the full text transcribed so far in this turn, not a delta.\n */\n transcript: string;\n /**\n * Unique identifier for this connection. Does not change between turns.\n */\n request_id: string;\n};\n\ntype STTErrorEvent = {\n type: 'error';\n error_code?: string;\n status_code?: number;\n title?: string;\n message?: string;\n doc_url?: string;\n request_id?: string;\n};\n\n/**\n * Server-sent event on the `/stt/turns/websocket` endpoint.\n *\n * See https://docs.cartesia.ai/api-reference/stt/turns/websocket.\n */\ntype STTEventMessage =\n | STTConnectedEvent\n | STTTurnStartEvent\n | STTTurnUpdateEvent\n | STTTurnEagerEndEvent\n | STTTurnResumeEvent\n | STTTurnEndEvent\n | STTErrorEvent;\n\nexport type STTOptions = {\n apiKey: string;\n // eslint-disable-next-line @typescript-eslint/ban-types\n model: STTModel | (string & {});\n sampleRate: number;\n baseUrl: string;\n audioChunkDurationMS: number;\n language: string;\n};\n\nconst defaultSTTOptions = {\n model: 'ink-2' satisfies STTModel,\n /** recommended default */\n sampleRate: 16_000,\n /** recommended default */\n audioChunkDurationMS: 160,\n baseUrl: 'https://api.cartesia.ai',\n language: 'en',\n};\n\nfunction mergeSTTOptions(base: STTOptions, override: Partial<STTOptions>): STTOptions {\n return {\n apiKey: override.apiKey ?? base.apiKey,\n baseUrl: override.baseUrl ?? base.baseUrl,\n model: override.model ?? base.model,\n sampleRate: override.sampleRate ?? base.sampleRate,\n audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,\n language:\n override.language !== undefined ? normalizeLanguage(override.language) : base.language,\n };\n}\n\n/**\n * Resolve the STT model from the configured language when the caller did not\n * pass one explicitly. Mirrors the Python plugin: `ink-2` only supports\n * English, so non-English languages route to the multilingual `ink-whisper`.\n */\nfunction resolveSTTModel(language: string): STTModel {\n return getBaseLanguage(language) === 'en' ? 'ink-2' : 'ink-whisper';\n}\n\n/**\n * Cartesia speech to text.\n *\n * Supports:\n * - Streaming\n * - Turn detection\n * - Interim results\n *\n * See https://docs.cartesia.ai/build-with-cartesia/stt-models/latest\n *\n * @example\n * ```typescript\n * import { voice } from '@livekit/agents';\n * import { STT, TTS } from '@livekit/agents-plugin-cartesia';\n *\n * const session = new voice.AgentSession({\n * stt: new STT(),\n * llm: new LLM(), // choose your favorite LLM\n * tts: new TTS(),\n * turnHandling: { turnDetection: 'stt' },\n * });\n * ```\n */\nexport class STT extends stt.STT {\n #opts: STTOptions;\n\n constructor(opts: Partial<STTOptions> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: false,\n diarization: false,\n });\n\n const apiKey = opts.apiKey ?? process.env.CARTESIA_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'Cartesia API key is required, whether as an argument or as $CARTESIA_API_KEY',\n );\n }\n\n this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);\n\n // Route to the multilingual model for non-English languages unless the\n // caller pinned a model. Matches the Python plugin's language→model logic.\n if (opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n\n override get label(): string {\n return 'cartesia.STT';\n }\n\n override get model(): string {\n return this.#opts.model;\n }\n\n override get provider(): string {\n return 'Cartesia';\n }\n\n protected override async _recognize(): Promise<stt.SpeechEvent> {\n throw new Error('Cartesia STT does not support batch recognition, use stream() instead');\n }\n\n override stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {\n return new SpeechStream(this, this.#opts, options?.connOptions);\n }\n\n updateOptions(opts: Partial<STTOptions>) {\n this.#opts = mergeSTTOptions(this.#opts, opts);\n\n // Keep the model in sync with a newly set language (e.g. switching to a\n // non-English language must move off the English-only ink-2), unless the\n // caller pinned a model in the same call. Mirrors the constructor.\n if (opts.language !== undefined && opts.model === undefined) {\n this.#opts.model = resolveSTTModel(this.#opts.language);\n }\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n #opts: STTOptions;\n #logger = log();\n #ws: WebSocket | null = null;\n #requestId: string | undefined;\n #speaking = false;\n #currentTranscript = '';\n #speechDuration = 0;\n #closingWs = false;\n #connectTimeout: number;\n\n constructor(sttInstance: STT, opts: STTOptions, connOptions?: APIConnectOptions) {\n super(sttInstance, opts.sampleRate, connOptions);\n this.#opts = { ...opts };\n this.#connectTimeout = connOptions?.timeoutMs ?? DEFAULT_API_CONNECT_OPTIONS.timeoutMs;\n }\n\n override get label(): string {\n return 'cartesia.SpeechStream';\n }\n\n protected override async run() {\n if (this.closed) {\n this.close();\n return;\n }\n\n this.#speaking = false;\n this.#currentTranscript = '';\n this.#speechDuration = 0;\n this.#closingWs = false;\n\n let keepaliveInterval: NodeJS.Timeout | undefined;\n const abortController = new AbortController();\n\n try {\n const url = this.#getCartesiaUrl();\n this.#logger.debug(`Connecting to Cartesia STT: ${url}`);\n\n let ws: WebSocket;\n try {\n ws = new WebSocket(url, {\n handshakeTimeout: this.#connectTimeout,\n headers: {\n [AUTHORIZATION_HEADER]: this.#opts.apiKey,\n [VERSION_HEADER]: API_VERSION,\n },\n });\n } catch (error) {\n throw new APIConnectionError({\n message: sanitizedErrorName(asError(error)),\n options: { retryable: true },\n });\n }\n this.#ws = ws;\n\n // Cartesia returns the request id on the WS upgrade response, before any\n // turn events are sent. Capture it so logs and metrics emitted on the\n // connection (and any pre-`turn.start` errors) are attributable.\n ws.once('upgrade', (response: IncomingMessage) => {\n const headerValue = response.headers[REQUEST_ID_HEADER];\n const requestId = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n if (requestId) {\n this.#requestId = requestId;\n this.#logger.debug({ cartesiaRequestId: requestId }, 'Cartesia STT WebSocket connected');\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n ws.off('open', onOpen);\n ws.off('unexpected-response', onUnexpectedResponse);\n ws.off('error', onError);\n };\n const onOpen = () => {\n cleanup();\n resolve();\n };\n const onUnexpectedResponse = (_request: unknown, response: { statusCode?: number }) => {\n cleanup();\n ws.on('error', () => {});\n ws.close();\n // Authentication headers can appear in WebSocket handshake errors.\n const statusCode = response.statusCode ?? -1;\n reject(\n new APIStatusError({\n message: `Cartesia WebSocket connection rejected with status ${statusCode}`,\n options: { statusCode },\n }),\n );\n };\n const onError = (err: Error) => {\n cleanup();\n // Transport errors can contain credentials in URLs.\n reject(\n new APIConnectionError({\n message: sanitizedErrorName(err),\n options: { retryable: true },\n }),\n );\n };\n\n ws.once('open', onOpen);\n ws.once('unexpected-response', onUnexpectedResponse);\n ws.once('error', onError);\n });\n\n // If one task fails, abort its peer and close the WS so the peer can\n // exit before we re-throw. Otherwise a dangling task would survive\n // into the next retry attempt (sendTask blocked on input.next() would\n // steal frames; recvTask's close handler could still push events to\n // the queue and surface an unhandled rejection).\n let firstError: unknown;\n const stopPeer = (err: unknown) => {\n if (firstError === undefined) firstError = err;\n abortController.abort();\n if (ws.readyState === WebSocket.OPEN) ws.close();\n };\n const sendPromise = this.#sendTask(ws, abortController.signal).catch(stopPeer);\n const recvPromise = this.#recvTask(ws).catch(stopPeer);\n\n keepaliveInterval = setInterval(() => {\n try {\n if (ws.readyState === WebSocket.OPEN) {\n ws.ping();\n }\n } catch {\n // ignore\n }\n }, KEEPALIVE_INTERVAL_MS);\n await Promise.all([sendPromise, recvPromise]);\n if (firstError !== undefined) throw firstError;\n } catch (error) {\n this.#logger.error('Cartesia STT stream error', { error });\n throw error;\n } finally {\n abortController.abort();\n if (keepaliveInterval) clearInterval(keepaliveInterval);\n if (this.#ws?.readyState === WebSocket.OPEN) {\n this.#ws.close();\n }\n }\n\n this.close();\n }\n\n async #sendTask(ws: WebSocket, abortSignal: AbortSignal) {\n const samplesPerChunk = Math.floor(\n (this.#opts.sampleRate * this.#opts.audioChunkDurationMS) / 1000,\n );\n const audioBstream = new AudioByteStream(this.#opts.sampleRate, 1, samplesPerChunk);\n\n let hasEnded = false;\n const iterator = this.input[Symbol.asyncIterator]();\n const abortPromise = waitForAbort(abortSignal);\n\n while (true) {\n const result = await Promise.race([iterator.next(), abortPromise]);\n\n if (result === undefined) return; // aborted\n\n if (result.done) {\n hasEnded = true;\n } else {\n const data = result.value;\n const frames: AudioFrame[] = [];\n\n if (data === SpeechStream.FLUSH_SENTINEL) {\n frames.push(...audioBstream.flush());\n } else {\n // Pass the typed array directly so AudioByteStream uses byteOffset/\n // byteLength — `.buffer` would include foreign bytes when rtc-node\n // hands us a view over a pooled allocator.\n frames.push(...audioBstream.write(data.data));\n }\n\n for (const frame of frames) {\n this.#speechDuration += calculateAudioDurationSeconds(frame);\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(frame.data);\n }\n }\n }\n\n if (hasEnded) break;\n }\n\n if (ws.readyState === WebSocket.OPEN) {\n this.#closingWs = true;\n this.#logger.debug('Sending close message to Cartesia STT');\n ws.send(JSON.stringify({ type: 'close' }));\n\n // After `close`, the server should flush any pending turn events and\n // close the socket. If it hangs, force the issue rather than letting\n // the stream wait forever.\n const drainTimer = setTimeout(() => {\n this.#logger.warn(\n `Cartesia STT did not close within ${DRAIN_TIMEOUT_MS}ms after done; forcing close`,\n );\n if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {\n ws.terminate();\n }\n }, DRAIN_TIMEOUT_MS);\n ws.once('close', () => clearTimeout(drainTimer));\n }\n }\n\n async #recvTask(ws: WebSocket) {\n let pendingError: Error | undefined = undefined;\n\n return new Promise<void>((resolve, reject) => {\n const settle = (err?: Error) => {\n if (err) reject(err);\n else resolve();\n };\n\n ws.on('message', (data: Buffer, isBinary: boolean) => {\n if (isBinary) {\n this.#logger.warn('Received unexpected binary message from Cartesia STT');\n return;\n }\n let msg: STTEventMessage;\n try {\n msg = JSON.parse(data.toString());\n } catch (error) {\n this.#logger.error('Failed to parse Cartesia STT message', { error });\n return;\n }\n try {\n if (msg.type === 'error') {\n this.#logger.error({ 'lk.pii.message': msg }, 'Cartesia sent an error');\n\n // do not close the websocket on bad requests since that may be caused by invalid messages\n if (msg.status_code === undefined || msg.status_code >= 500) {\n // Defer until the WS is fully closed so we don't race the `close`\n // handler with a rejected `recvTask`. Close the socket explicitly\n // in case the server doesn't, otherwise recv would hang.\n pendingError = new APIConnectionError({\n message: msg.message || msg.title,\n options: {\n retryable: true,\n },\n });\n ws.close();\n return;\n }\n } else {\n this.#processStreamEvent(msg);\n }\n } catch (error) {\n this.#logger.error('Failed to process Cartesia STT message', { error });\n }\n });\n\n ws.on('close', (code, reason) => {\n // If a turn was in progress, close it out so consumers see a balanced\n // START_OF_SPEECH/END_OF_SPEECH pair and don't carry stale turn state\n // into the next reconnect.\n if (this.#speaking) {\n if (this.#currentTranscript) {\n this.#sendTranscriptEvent(\n stt.SpeechEventType.FINAL_TRANSCRIPT,\n this.#currentTranscript,\n );\n }\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n this.#speaking = false;\n this.#currentTranscript = '';\n }\n\n if (pendingError) {\n settle(pendingError);\n return;\n }\n if (this.#closingWs || this.closed) {\n settle();\n return;\n }\n this.#logger.warn(`Cartesia STT WebSocket closed: ${code} ${reason.toString()}`);\n settle(\n new APIConnectionError({\n message: `Cartesia STT connection closed unexpectedly (code=${code})`,\n options: { retryable: true },\n }),\n );\n });\n\n ws.on('error', (err) => {\n if (this.closed) {\n settle();\n return;\n }\n settle(new APIConnectionError({ message: err.message, options: { retryable: true } }));\n });\n });\n }\n\n #processStreamEvent(data: Exclude<STTEventMessage, { type: 'error' }>) {\n if (data.request_id) {\n this.#requestId = data.request_id;\n }\n\n switch (data.type) {\n case 'connected':\n return;\n\n case 'turn.start': {\n if (this.#speaking) return;\n this.#speaking = true;\n this.#currentTranscript = '';\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.START_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT START_OF_SPEECH');\n return;\n }\n\n case 'turn.update': {\n if (!this.#speaking) return;\n const transcript = data.transcript;\n if (!transcript) return;\n // Only emit interim updates when the cumulative transcript actually\n // changed; this avoids canceling preflight generation needlessly.\n if (this.#currentTranscript === transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.eager_end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n if (!transcript) return;\n this.#currentTranscript = transcript;\n this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);\n return;\n }\n\n case 'turn.resume':\n // turn.resume has no transcript; re-emit the latest cumulative transcript\n // as an interim event so the pipeline cancels the pending preflight.\n if (!this.#speaking || !this.#currentTranscript) return;\n this.#sendTranscriptEvent(stt.SpeechEventType.INTERIM_TRANSCRIPT, this.#currentTranscript);\n return;\n\n case 'turn.end': {\n if (!this.#speaking) return;\n const transcript = data.transcript || this.#currentTranscript;\n\n this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);\n\n this.#speaking = false;\n if (!this.queue.closed)\n this.queue.put({\n type: stt.SpeechEventType.END_OF_SPEECH,\n requestId: this.#requestId,\n });\n this.#logger.debug('Cartesia STT END_OF_SPEECH');\n this.#currentTranscript = '';\n\n if (this.#speechDuration > 0 && !this.queue.closed) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n requestId: this.#requestId,\n recognitionUsage: { audioDuration: this.#speechDuration },\n });\n this.#speechDuration = 0;\n }\n\n return;\n }\n\n default:\n this.#logger.warn({ 'lk.pii.data': data }, 'received unexpected message from Cartesia STT');\n }\n }\n\n #sendTranscriptEvent(eventType: stt.SpeechEventType, transcript: string) {\n if (this.queue.closed) return;\n this.queue.put({\n type: eventType,\n requestId: this.#requestId,\n alternatives: [\n {\n language: asLanguageCode(this.#opts.language),\n text: transcript,\n startTime: 0,\n endTime: 0,\n confidence: 0,\n },\n ],\n });\n }\n\n #getCartesiaUrl(): string {\n // The Cartesia /stt/turns/websocket endpoint only accepts model, sample_rate\n // and encoding — there is no `language` query param. Language selection is\n // expressed through the model (ink-2 for English, ink-whisper otherwise),\n // so #opts.language is used only to tag emitted transcripts.\n const params = new URLSearchParams({\n model: this.#opts.model,\n sample_rate: this.#opts.sampleRate.toString(),\n encoding: AUDIO_ENCODING,\n });\n\n const wsBase = this.#opts.baseUrl.replace(/^http/, 'ws');\n return `${wsBase}/stt/turns/websocket?${params.toString()}`;\n }\n\n override close() {\n super.close();\n this.#ws?.close();\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/stt.test.cjs
CHANGED
|
@@ -1,15 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var import_agents = require("@livekit/agents");
|
|
2
3
|
var import_agents_plugin_silero = require("@livekit/agents-plugin-silero");
|
|
3
4
|
var import_agents_plugins_test = require("@livekit/agents-plugins-test");
|
|
5
|
+
var import_node_events = require("node:events");
|
|
4
6
|
var import_vitest = require("vitest");
|
|
5
7
|
var import_stt = require("./stt.cjs");
|
|
6
8
|
const hasCartesiaApiKey = Boolean(process.env.CARTESIA_API_KEY);
|
|
9
|
+
const swallowExpectedRejection = (reason) => {
|
|
10
|
+
if (reason instanceof import_agents.APIError) return;
|
|
11
|
+
throw reason;
|
|
12
|
+
};
|
|
13
|
+
(0, import_vitest.beforeAll)(() => process.on("unhandledRejection", swallowExpectedRejection));
|
|
14
|
+
(0, import_vitest.afterAll)(() => void process.off("unhandledRejection", swallowExpectedRejection));
|
|
7
15
|
(0, import_vitest.describe)("Cartesia STT capabilities", () => {
|
|
8
16
|
(0, import_vitest.it)("reports no aligned transcript for Ink-2", () => {
|
|
9
17
|
const instance = new import_stt.STT({ apiKey: "test-key", model: "ink-2" });
|
|
10
18
|
(0, import_vitest.expect)(instance.capabilities.alignedTranscript).toBe(false);
|
|
11
19
|
});
|
|
12
20
|
});
|
|
21
|
+
(0, import_vitest.describe)("Cartesia STT connection errors", () => {
|
|
22
|
+
(0, import_vitest.it)("does not retain synchronous WebSocket connection errors", async () => {
|
|
23
|
+
const secret = "cartesia-secret-api-key-do-not-log";
|
|
24
|
+
const cartesia = new import_stt.STT({ apiKey: "test-key", baseUrl: `http://[${secret}` });
|
|
25
|
+
const errorEvent = (0, import_node_events.once)(cartesia, "error");
|
|
26
|
+
const stream = cartesia.stream({
|
|
27
|
+
connOptions: { ...import_agents.DEFAULT_API_CONNECT_OPTIONS, maxRetry: 0 }
|
|
28
|
+
});
|
|
29
|
+
const drain = (async () => {
|
|
30
|
+
for await (const _ of stream) {
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
try {
|
|
34
|
+
const [{ error }] = await errorEvent;
|
|
35
|
+
(0, import_vitest.expect)(error).toBeInstanceOf(import_agents.APIConnectionError);
|
|
36
|
+
(0, import_vitest.expect)(error.message).toBe("SyntaxError");
|
|
37
|
+
(0, import_vitest.expect)(error.message).not.toContain(secret);
|
|
38
|
+
(0, import_vitest.expect)(error.toString()).not.toContain(secret);
|
|
39
|
+
(0, import_vitest.expect)(error.cause).toBeUndefined();
|
|
40
|
+
} finally {
|
|
41
|
+
stream.close();
|
|
42
|
+
await drain.catch(() => {
|
|
43
|
+
});
|
|
44
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
13
48
|
if (hasCartesiaApiKey) {
|
|
14
49
|
(0, import_vitest.describe)("Cartesia STT", async () => {
|
|
15
50
|
await (0, import_agents_plugins_test.stt)(new import_stt.STT(), await import_agents_plugin_silero.VAD.load(), { nonStreaming: false });
|