@livekit/agents-plugin-cartesia 1.4.8 → 1.4.11

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 CHANGED
@@ -22,7 +22,7 @@ class CartesiaPlugin extends import_agents.Plugin {
22
22
  constructor() {
23
23
  super({
24
24
  title: "cartesia",
25
- version: "1.4.8",
25
+ version: "1.4.11",
26
26
  package: "@livekit/agents-plugin-cartesia"
27
27
  });
28
28
  }
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ class CartesiaPlugin extends Plugin {
5
5
  constructor() {
6
6
  super({
7
7
  title: "cartesia",
8
- version: "1.4.8",
8
+ version: "1.4.11",
9
9
  package: "@livekit/agents-plugin-cartesia"
10
10
  });
11
11
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/models.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * LiveKit uses this encoding for all audio\n */\nexport const AUDIO_ENCODING = 'pcm_s16le';\n\n// ============================================================================\n// TTS\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSModels =\n | 'sonic'\n | 'sonic-2'\n | 'sonic-3'\n | 'sonic-lite'\n | 'sonic-preview'\n | 'sonic-turbo';\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSLanguages = 'en' | 'es' | 'fr' | 'de' | 'pt' | 'zh' | 'ja';\n\nexport const TTSDefaultVoiceId = 'f786b574-daa5-4673-aa0c-cbe3e8534c02';\n\nexport const isSonic3 = (model: string): boolean => model.startsWith('sonic-3');\n\nexport type TTSVoiceSpeed = 'fastest' | 'fast' | 'normal' | 'slow' | 'slowest';\n\nexport type TTSVoiceEmotion =\n | 'anger:lowest'\n | 'anger:low'\n | 'anger'\n | 'anger:high'\n | 'anger:highest'\n | 'positivity:lowest'\n | 'positivity:low'\n | 'positivity'\n | 'positivity:high'\n | 'positivity:highest'\n | 'surprise:lowest'\n | 'surprise:low'\n | 'surprise'\n | 'surprise:high'\n | 'surprise:highest'\n | 'sadness:lowest'\n | 'sadness:low'\n | 'sadness'\n | 'sadness:high'\n | 'sadness:highest'\n | 'curiosity:lowest'\n | 'curiosity:low'\n | 'curiosity'\n | 'curiosity:high'\n | 'curiosity:highest';\n\n/**\n * @deprecated Encoding should not be parameterized. Only `pcm_s16le`is allowed. Prefer using {@link AUDIO_ENCODING}.\n */\nexport type TTSEncoding = 'pcm_s16le';\n\n// ============================================================================\n// STT\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.\n */\nexport type STTModel = 'ink-2';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,MAAM,iBAAiB;AAsBvB,MAAM,oBAAoB;AAE1B,MAAM,WAAW,CAAC,UAA2B,MAAM,WAAW,SAAS;","names":[]}
1
+ {"version":3,"sources":["../src/models.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * LiveKit uses this encoding for all audio\n */\nexport const AUDIO_ENCODING = 'pcm_s16le';\n\n// ============================================================================\n// TTS\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSModels =\n | 'sonic'\n | 'sonic-2'\n | 'sonic-3'\n | 'sonic-lite'\n | 'sonic-preview'\n | 'sonic-turbo';\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSLanguages = 'en' | 'es' | 'fr' | 'de' | 'pt' | 'zh' | 'ja';\n\nexport const TTSDefaultVoiceId = 'f786b574-daa5-4673-aa0c-cbe3e8534c02';\n\nexport const isSonic3 = (model: string): boolean => model.startsWith('sonic-3');\n\nexport type TTSVoiceSpeed = 'fastest' | 'fast' | 'normal' | 'slow' | 'slowest';\n\nexport type TTSVoiceEmotion =\n | 'anger:lowest'\n | 'anger:low'\n | 'anger'\n | 'anger:high'\n | 'anger:highest'\n | 'positivity:lowest'\n | 'positivity:low'\n | 'positivity'\n | 'positivity:high'\n | 'positivity:highest'\n | 'surprise:lowest'\n | 'surprise:low'\n | 'surprise'\n | 'surprise:high'\n | 'surprise:highest'\n | 'sadness:lowest'\n | 'sadness:low'\n | 'sadness'\n | 'sadness:high'\n | 'sadness:highest'\n | 'curiosity:lowest'\n | 'curiosity:low'\n | 'curiosity'\n | 'curiosity:high'\n | 'curiosity:highest';\n\n/**\n * @deprecated Encoding should not be parameterized. Only `pcm_s16le`is allowed. Prefer using {@link AUDIO_ENCODING}.\n */\nexport type TTSEncoding = 'pcm_s16le';\n\n// ============================================================================\n// STT\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.\n *\n * `ink-2` currently supports English only; `ink-whisper` is multilingual.\n */\nexport type STTModel = 'ink-2' | 'ink-whisper';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,MAAM,iBAAiB;AAsBvB,MAAM,oBAAoB;AAE1B,MAAM,WAAW,CAAC,UAA2B,MAAM,WAAW,SAAS;","names":[]}
package/dist/models.d.cts CHANGED
@@ -20,6 +20,8 @@ export type TTSVoiceEmotion = 'anger:lowest' | 'anger:low' | 'anger' | 'anger:hi
20
20
  export type TTSEncoding = 'pcm_s16le';
21
21
  /**
22
22
  * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.
23
+ *
24
+ * `ink-2` currently supports English only; `ink-whisper` is multilingual.
23
25
  */
24
- export type STTModel = 'ink-2';
26
+ export type STTModel = 'ink-2' | 'ink-whisper';
25
27
  //# sourceMappingURL=models.d.ts.map
package/dist/models.d.ts CHANGED
@@ -20,6 +20,8 @@ export type TTSVoiceEmotion = 'anger:lowest' | 'anger:low' | 'anger' | 'anger:hi
20
20
  export type TTSEncoding = 'pcm_s16le';
21
21
  /**
22
22
  * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.
23
+ *
24
+ * `ink-2` currently supports English only; `ink-whisper` is multilingual.
23
25
  */
24
- export type STTModel = 'ink-2';
26
+ export type STTModel = 'ink-2' | 'ink-whisper';
25
27
  //# sourceMappingURL=models.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,eAAO,MAAM,cAAc,cAAc,CAAC;AAM1C;;GAEG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,SAAS,GACT,SAAS,GACT,YAAY,GACZ,eAAe,GACf,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1E,eAAO,MAAM,iBAAiB,yCAAyC,CAAC;AAExE,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,KAAG,OAAsC,CAAC;AAEhF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE/E,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,WAAW,GACX,OAAO,GACP,YAAY,GACZ,eAAe,GACf,mBAAmB,GACnB,gBAAgB,GAChB,YAAY,GACZ,iBAAiB,GACjB,oBAAoB,GACpB,iBAAiB,GACjB,cAAc,GACd,UAAU,GACV,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,aAAa,GACb,SAAS,GACT,cAAc,GACd,iBAAiB,GACjB,kBAAkB,GAClB,eAAe,GACf,WAAW,GACX,gBAAgB,GAChB,mBAAmB,CAAC;AAExB;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,CAAC;AAMtC;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,eAAO,MAAM,cAAc,cAAc,CAAC;AAM1C;;GAEG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,SAAS,GACT,SAAS,GACT,YAAY,GACZ,eAAe,GACf,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1E,eAAO,MAAM,iBAAiB,yCAAyC,CAAC;AAExE,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,KAAG,OAAsC,CAAC;AAEhF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE/E,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,WAAW,GACX,OAAO,GACP,YAAY,GACZ,eAAe,GACf,mBAAmB,GACnB,gBAAgB,GAChB,YAAY,GACZ,iBAAiB,GACjB,oBAAoB,GACpB,iBAAiB,GACjB,cAAc,GACd,UAAU,GACV,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,aAAa,GACb,SAAS,GACT,cAAc,GACd,iBAAiB,GACjB,kBAAkB,GAClB,eAAe,GACf,WAAW,GACX,gBAAgB,GAChB,mBAAmB,CAAC;AAExB;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,CAAC;AAMtC;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/models.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * LiveKit uses this encoding for all audio\n */\nexport const AUDIO_ENCODING = 'pcm_s16le';\n\n// ============================================================================\n// TTS\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSModels =\n | 'sonic'\n | 'sonic-2'\n | 'sonic-3'\n | 'sonic-lite'\n | 'sonic-preview'\n | 'sonic-turbo';\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSLanguages = 'en' | 'es' | 'fr' | 'de' | 'pt' | 'zh' | 'ja';\n\nexport const TTSDefaultVoiceId = 'f786b574-daa5-4673-aa0c-cbe3e8534c02';\n\nexport const isSonic3 = (model: string): boolean => model.startsWith('sonic-3');\n\nexport type TTSVoiceSpeed = 'fastest' | 'fast' | 'normal' | 'slow' | 'slowest';\n\nexport type TTSVoiceEmotion =\n | 'anger:lowest'\n | 'anger:low'\n | 'anger'\n | 'anger:high'\n | 'anger:highest'\n | 'positivity:lowest'\n | 'positivity:low'\n | 'positivity'\n | 'positivity:high'\n | 'positivity:highest'\n | 'surprise:lowest'\n | 'surprise:low'\n | 'surprise'\n | 'surprise:high'\n | 'surprise:highest'\n | 'sadness:lowest'\n | 'sadness:low'\n | 'sadness'\n | 'sadness:high'\n | 'sadness:highest'\n | 'curiosity:lowest'\n | 'curiosity:low'\n | 'curiosity'\n | 'curiosity:high'\n | 'curiosity:highest';\n\n/**\n * @deprecated Encoding should not be parameterized. Only `pcm_s16le`is allowed. Prefer using {@link AUDIO_ENCODING}.\n */\nexport type TTSEncoding = 'pcm_s16le';\n\n// ============================================================================\n// STT\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.\n */\nexport type STTModel = 'ink-2';\n"],"mappings":"AAOO,MAAM,iBAAiB;AAsBvB,MAAM,oBAAoB;AAE1B,MAAM,WAAW,CAAC,UAA2B,MAAM,WAAW,SAAS;","names":[]}
1
+ {"version":3,"sources":["../src/models.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * LiveKit uses this encoding for all audio\n */\nexport const AUDIO_ENCODING = 'pcm_s16le';\n\n// ============================================================================\n// TTS\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSModels =\n | 'sonic'\n | 'sonic-2'\n | 'sonic-3'\n | 'sonic-lite'\n | 'sonic-preview'\n | 'sonic-turbo';\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/tts-models/latest) for all options.\n */\nexport type TTSLanguages = 'en' | 'es' | 'fr' | 'de' | 'pt' | 'zh' | 'ja';\n\nexport const TTSDefaultVoiceId = 'f786b574-daa5-4673-aa0c-cbe3e8534c02';\n\nexport const isSonic3 = (model: string): boolean => model.startsWith('sonic-3');\n\nexport type TTSVoiceSpeed = 'fastest' | 'fast' | 'normal' | 'slow' | 'slowest';\n\nexport type TTSVoiceEmotion =\n | 'anger:lowest'\n | 'anger:low'\n | 'anger'\n | 'anger:high'\n | 'anger:highest'\n | 'positivity:lowest'\n | 'positivity:low'\n | 'positivity'\n | 'positivity:high'\n | 'positivity:highest'\n | 'surprise:lowest'\n | 'surprise:low'\n | 'surprise'\n | 'surprise:high'\n | 'surprise:highest'\n | 'sadness:lowest'\n | 'sadness:low'\n | 'sadness'\n | 'sadness:high'\n | 'sadness:highest'\n | 'curiosity:lowest'\n | 'curiosity:low'\n | 'curiosity'\n | 'curiosity:high'\n | 'curiosity:highest';\n\n/**\n * @deprecated Encoding should not be parameterized. Only `pcm_s16le`is allowed. Prefer using {@link AUDIO_ENCODING}.\n */\nexport type TTSEncoding = 'pcm_s16le';\n\n// ============================================================================\n// STT\n// ============================================================================\n\n/**\n * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.\n *\n * `ink-2` currently supports English only; `ink-whisper` is multilingual.\n */\nexport type STTModel = 'ink-2' | 'ink-whisper';\n"],"mappings":"AAOO,MAAM,iBAAiB;AAsBvB,MAAM,oBAAoB;AAE1B,MAAM,WAAW,CAAC,UAA2B,MAAM,WAAW,SAAS;","names":[]}
package/dist/stt.cjs CHANGED
@@ -37,7 +37,8 @@ const defaultSTTOptions = {
37
37
  sampleRate: 16e3,
38
38
  /** recommended default */
39
39
  audioChunkDurationMS: 160,
40
- baseUrl: "https://api.cartesia.ai"
40
+ baseUrl: "https://api.cartesia.ai",
41
+ language: "en"
41
42
  };
42
43
  function mergeSTTOptions(base, override) {
43
44
  return {
@@ -45,9 +46,13 @@ function mergeSTTOptions(base, override) {
45
46
  baseUrl: override.baseUrl ?? base.baseUrl,
46
47
  model: override.model ?? base.model,
47
48
  sampleRate: override.sampleRate ?? base.sampleRate,
48
- audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS
49
+ audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,
50
+ language: override.language !== void 0 ? (0, import_agents.normalizeLanguage)(override.language) : base.language
49
51
  };
50
52
  }
53
+ function resolveSTTModel(language) {
54
+ return (0, import_agents.getBaseLanguage)(language) === "en" ? "ink-2" : "ink-whisper";
55
+ }
51
56
  class STT extends import_agents.stt.STT {
52
57
  #opts;
53
58
  constructor(opts = {}) {
@@ -64,6 +69,9 @@ class STT extends import_agents.stt.STT {
64
69
  );
65
70
  }
66
71
  this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);
72
+ if (opts.model === void 0) {
73
+ this.#opts.model = resolveSTTModel(this.#opts.language);
74
+ }
67
75
  }
68
76
  get label() {
69
77
  return "cartesia.STT";
@@ -80,6 +88,12 @@ class STT extends import_agents.stt.STT {
80
88
  stream(options) {
81
89
  return new SpeechStream(this, this.#opts, options == null ? void 0 : options.connOptions);
82
90
  }
91
+ updateOptions(opts) {
92
+ this.#opts = mergeSTTOptions(this.#opts, opts);
93
+ if (opts.language !== void 0 && opts.model === void 0) {
94
+ this.#opts.model = resolveSTTModel(this.#opts.language);
95
+ }
96
+ }
83
97
  }
84
98
  class SpeechStream extends import_agents.stt.SpeechStream {
85
99
  #opts;
@@ -333,7 +347,7 @@ class SpeechStream extends import_agents.stt.SpeechStream {
333
347
  }
334
348
  case "turn.eager_end": {
335
349
  if (!this.#speaking) return;
336
- const transcript = data.transcript;
350
+ const transcript = data.transcript || this.#currentTranscript;
337
351
  if (!transcript) return;
338
352
  this.#currentTranscript = transcript;
339
353
  this.#sendTranscriptEvent(import_agents.stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);
@@ -345,7 +359,7 @@ class SpeechStream extends import_agents.stt.SpeechStream {
345
359
  return;
346
360
  case "turn.end": {
347
361
  if (!this.#speaking) return;
348
- const transcript = data.transcript;
362
+ const transcript = data.transcript || this.#currentTranscript;
349
363
  this.#sendTranscriptEvent(import_agents.stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);
350
364
  this.#speaking = false;
351
365
  if (!this.queue.closed)
@@ -376,8 +390,7 @@ class SpeechStream extends import_agents.stt.SpeechStream {
376
390
  requestId: this.#requestId,
377
391
  alternatives: [
378
392
  {
379
- // Cartesia STT only supports English at this time.
380
- language: (0, import_agents.asLanguageCode)("en"),
393
+ language: (0, import_agents.asLanguageCode)(this.#opts.language),
381
394
  text: transcript,
382
395
  startTime: 0,
383
396
  endTime: 0,
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 log,\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};\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};\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 };\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\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\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;\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;\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 // Cartesia STT only supports English at this time.\n language: asLanguageCode('en'),\n text: transcript,\n startTime: 0,\n endTime: 0,\n confidence: 0,\n },\n ],\n });\n }\n\n #getCartesiaUrl(): string {\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 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":[]}
package/dist/stt.d.cts CHANGED
@@ -6,6 +6,7 @@ export type STTOptions = {
6
6
  sampleRate: number;
7
7
  baseUrl: string;
8
8
  audioChunkDurationMS: number;
9
+ language: string;
9
10
  };
10
11
  /**
11
12
  * Cartesia speech to text.
@@ -40,6 +41,7 @@ export declare class STT extends stt.STT {
40
41
  stream(options?: {
41
42
  connOptions?: APIConnectOptions;
42
43
  }): SpeechStream;
44
+ updateOptions(opts: Partial<STTOptions>): void;
43
45
  }
44
46
  export declare class SpeechStream extends stt.SpeechStream {
45
47
  #private;
package/dist/stt.d.ts CHANGED
@@ -6,6 +6,7 @@ export type STTOptions = {
6
6
  sampleRate: number;
7
7
  baseUrl: string;
8
8
  audioChunkDurationMS: number;
9
+ language: string;
9
10
  };
10
11
  /**
11
12
  * Cartesia speech to text.
@@ -40,6 +41,7 @@ export declare class STT extends stt.STT {
40
41
  stream(options?: {
41
42
  connOptions?: APIConnectOptions;
42
43
  }): SpeechStream;
44
+ updateOptions(opts: Partial<STTOptions>): void;
43
45
  }
44
46
  export declare class SpeechStream extends stt.SpeechStream {
45
47
  #private;
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,EAMtB,GAAG,EAEJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAuH5D,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;CAC9B,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,GAAI,SAAQ,GAAG,CAAC,GAAG;;gBAGlB,IAAI,GAAE,OAAO,CAAC,UAAU,CAAM;IAkB1C,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;CAG7E;AAED,qBAAa,YAAa,SAAQ,GAAG,CAAC,YAAY;;gBAUpC,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,iBAAiB;IAK/E,IAAa,KAAK,IAAI,MAAM,CAE3B;cAEwB,GAAG;IA8WnB,KAAK;CAIf"}
1
+ {"version":3,"file":"stt.d.ts","sourceRoot":"","sources":["../src/stt.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,iBAAiB,EAQtB,GAAG,EAEJ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAuH5D,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;;gBAUpC,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,iBAAiB;IAK/E,IAAa,KAAK,IAAI,MAAM,CAE3B;cAEwB,GAAG;IAiXnB,KAAK;CAIf"}
package/dist/stt.js CHANGED
@@ -3,7 +3,9 @@ import {
3
3
  AudioByteStream,
4
4
  asLanguageCode,
5
5
  calculateAudioDurationSeconds,
6
+ getBaseLanguage,
6
7
  log,
8
+ normalizeLanguage,
7
9
  stt,
8
10
  waitForAbort
9
11
  } from "@livekit/agents";
@@ -21,7 +23,8 @@ const defaultSTTOptions = {
21
23
  sampleRate: 16e3,
22
24
  /** recommended default */
23
25
  audioChunkDurationMS: 160,
24
- baseUrl: "https://api.cartesia.ai"
26
+ baseUrl: "https://api.cartesia.ai",
27
+ language: "en"
25
28
  };
26
29
  function mergeSTTOptions(base, override) {
27
30
  return {
@@ -29,9 +32,13 @@ function mergeSTTOptions(base, override) {
29
32
  baseUrl: override.baseUrl ?? base.baseUrl,
30
33
  model: override.model ?? base.model,
31
34
  sampleRate: override.sampleRate ?? base.sampleRate,
32
- audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS
35
+ audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,
36
+ language: override.language !== void 0 ? normalizeLanguage(override.language) : base.language
33
37
  };
34
38
  }
39
+ function resolveSTTModel(language) {
40
+ return getBaseLanguage(language) === "en" ? "ink-2" : "ink-whisper";
41
+ }
35
42
  class STT extends stt.STT {
36
43
  #opts;
37
44
  constructor(opts = {}) {
@@ -48,6 +55,9 @@ class STT extends stt.STT {
48
55
  );
49
56
  }
50
57
  this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);
58
+ if (opts.model === void 0) {
59
+ this.#opts.model = resolveSTTModel(this.#opts.language);
60
+ }
51
61
  }
52
62
  get label() {
53
63
  return "cartesia.STT";
@@ -64,6 +74,12 @@ class STT extends stt.STT {
64
74
  stream(options) {
65
75
  return new SpeechStream(this, this.#opts, options == null ? void 0 : options.connOptions);
66
76
  }
77
+ updateOptions(opts) {
78
+ this.#opts = mergeSTTOptions(this.#opts, opts);
79
+ if (opts.language !== void 0 && opts.model === void 0) {
80
+ this.#opts.model = resolveSTTModel(this.#opts.language);
81
+ }
82
+ }
67
83
  }
68
84
  class SpeechStream extends stt.SpeechStream {
69
85
  #opts;
@@ -317,7 +333,7 @@ class SpeechStream extends stt.SpeechStream {
317
333
  }
318
334
  case "turn.eager_end": {
319
335
  if (!this.#speaking) return;
320
- const transcript = data.transcript;
336
+ const transcript = data.transcript || this.#currentTranscript;
321
337
  if (!transcript) return;
322
338
  this.#currentTranscript = transcript;
323
339
  this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);
@@ -329,7 +345,7 @@ class SpeechStream extends stt.SpeechStream {
329
345
  return;
330
346
  case "turn.end": {
331
347
  if (!this.#speaking) return;
332
- const transcript = data.transcript;
348
+ const transcript = data.transcript || this.#currentTranscript;
333
349
  this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);
334
350
  this.#speaking = false;
335
351
  if (!this.queue.closed)
@@ -360,8 +376,7 @@ class SpeechStream extends stt.SpeechStream {
360
376
  requestId: this.#requestId,
361
377
  alternatives: [
362
378
  {
363
- // Cartesia STT only supports English at this time.
364
- language: asLanguageCode("en"),
379
+ language: asLanguageCode(this.#opts.language),
365
380
  text: transcript,
366
381
  startTime: 0,
367
382
  endTime: 0,
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 log,\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};\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};\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 };\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\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\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;\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;\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 // Cartesia STT only supports English at this time.\n language: asLanguageCode('en'),\n text: transcript,\n startTime: 0,\n endTime: 0,\n confidence: 0,\n },\n ],\n });\n }\n\n #getCartesiaUrl(): string {\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 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":"AAGA;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAC1B,SAAS,sBAAqC;AAE9C,MAAM,uBAAuB;AAC7B,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,cAAc;AACpB,MAAM,mBAAmB;AACzB,MAAM,wBAAwB;AA0H9B,MAAM,oBAAoB;AAAA,EACxB,OAAO;AAAA;AAAA,EAEP,YAAY;AAAA;AAAA,EAEZ,sBAAsB;AAAA,EACtB,SAAS;AAAA,EACT,UAAU;AACZ;AAEA,SAAS,gBAAgB,MAAkB,UAA2C;AACpF,SAAO;AAAA,IACL,QAAQ,SAAS,UAAU,KAAK;AAAA,IAChC,SAAS,SAAS,WAAW,KAAK;AAAA,IAClC,OAAO,SAAS,SAAS,KAAK;AAAA,IAC9B,YAAY,SAAS,cAAc,KAAK;AAAA,IACxC,sBAAsB,SAAS,wBAAwB,KAAK;AAAA,IAC5D,UACE,SAAS,aAAa,SAAY,kBAAkB,SAAS,QAAQ,IAAI,KAAK;AAAA,EAClF;AACF;AAOA,SAAS,gBAAgB,UAA4B;AACnD,SAAO,gBAAgB,QAAQ,MAAM,OAAO,UAAU;AACxD;AAyBO,MAAM,YAAY,IAAI,IAAI;AAAA,EAC/B;AAAA,EAEA,YAAY,OAA4B,CAAC,GAAG;AAC1C,UAAM;AAAA,MACJ,WAAW;AAAA,MACX,gBAAgB;AAAA,MAChB,mBAAmB;AAAA,MACnB,aAAa;AAAA,IACf,CAAC;AAED,UAAM,SAAS,KAAK,UAAU,QAAQ,IAAI;AAC1C,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,SAAK,QAAQ,gBAAgB,EAAE,GAAG,mBAAmB,OAAO,GAAG,IAAI;AAInE,QAAI,KAAK,UAAU,QAAW;AAC5B,WAAK,MAAM,QAAQ,gBAAgB,KAAK,MAAM,QAAQ;AAAA,IACxD;AAAA,EACF;AAAA,EAEA,IAAa,QAAgB;AAC3B,WAAO;AAAA,EACT;AAAA,EAEA,IAAa,QAAgB;AAC3B,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,IAAa,WAAmB;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAyB,aAAuC;AAC9D,UAAM,IAAI,MAAM,uEAAuE;AAAA,EACzF;AAAA,EAES,OAAO,SAA6D;AAC3E,WAAO,IAAI,aAAa,MAAM,KAAK,OAAO,mCAAS,WAAW;AAAA,EAChE;AAAA,EAEA,cAAc,MAA2B;AACvC,SAAK,QAAQ,gBAAgB,KAAK,OAAO,IAAI;AAK7C,QAAI,KAAK,aAAa,UAAa,KAAK,UAAU,QAAW;AAC3D,WAAK,MAAM,QAAQ,gBAAgB,KAAK,MAAM,QAAQ;AAAA,IACxD;AAAA,EACF;AACF;AAEO,MAAM,qBAAqB,IAAI,aAAa;AAAA,EACjD;AAAA,EACA,UAAU,IAAI;AAAA,EACd,MAAwB;AAAA,EACxB;AAAA,EACA,YAAY;AAAA,EACZ,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EAEb,YAAY,aAAkB,MAAkB,aAAiC;AAC/E,UAAM,aAAa,KAAK,YAAY,WAAW;AAC/C,SAAK,QAAQ,EAAE,GAAG,KAAK;AAAA,EACzB;AAAA,EAEA,IAAa,QAAgB;AAC3B,WAAO;AAAA,EACT;AAAA,EAEA,MAAyB,MAAM;AAvRjC;AAwRI,QAAI,KAAK,QAAQ;AACf,WAAK,MAAM;AACX;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,qBAAqB;AAC1B,SAAK,kBAAkB;AACvB,SAAK,aAAa;AAElB,QAAI;AACJ,UAAM,kBAAkB,IAAI,gBAAgB;AAE5C,QAAI;AACF,YAAM,MAAM,KAAK,gBAAgB;AACjC,WAAK,QAAQ,MAAM,+BAA+B,GAAG,EAAE;AAEvD,YAAM,KAAK,IAAI,UAAU,KAAK;AAAA,QAC5B,SAAS;AAAA,UACP,CAAC,oBAAoB,GAAG,KAAK,MAAM;AAAA,UACnC,CAAC,cAAc,GAAG;AAAA,QACpB;AAAA,MACF,CAAC;AACD,WAAK,MAAM;AAKX,SAAG,KAAK,WAAW,CAAC,aAA8B;AAChD,cAAM,cAAc,SAAS,QAAQ,iBAAiB;AACtD,cAAM,YAAY,MAAM,QAAQ,WAAW,IAAI,YAAY,CAAC,IAAI;AAChE,YAAI,WAAW;AACb,eAAK,aAAa;AAClB,eAAK,QAAQ,MAAM,EAAE,mBAAmB,UAAU,GAAG,kCAAkC;AAAA,QACzF;AAAA,MACF,CAAC;AAED,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,cAAM,SAAS,MAAM;AACnB,aAAG,IAAI,SAAS,OAAO;AACvB,kBAAQ;AAAA,QACV;AACA,cAAM,UAAU,CAAC,QAAe;AAC9B,aAAG,IAAI,QAAQ,MAAM;AACrB,iBAAO,IAAI,mBAAmB,EAAE,SAAS,IAAI,SAAS,SAAS,EAAE,WAAW,KAAK,EAAE,CAAC,CAAC;AAAA,QACvF;AAEA,WAAG,KAAK,QAAQ,MAAM;AACtB,WAAG,KAAK,SAAS,OAAO;AAAA,MAC1B,CAAC;AAOD,UAAI;AACJ,YAAM,WAAW,CAAC,QAAiB;AACjC,YAAI,eAAe,OAAW,cAAa;AAC3C,wBAAgB,MAAM;AACtB,YAAI,GAAG,eAAe,UAAU,KAAM,IAAG,MAAM;AAAA,MACjD;AACA,YAAM,cAAc,KAAK,UAAU,IAAI,gBAAgB,MAAM,EAAE,MAAM,QAAQ;AAC7E,YAAM,cAAc,KAAK,UAAU,EAAE,EAAE,MAAM,QAAQ;AAErD,0BAAoB,YAAY,MAAM;AACpC,YAAI;AACF,cAAI,GAAG,eAAe,UAAU,MAAM;AACpC,eAAG,KAAK;AAAA,UACV;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF,GAAG,qBAAqB;AACxB,YAAM,QAAQ,IAAI,CAAC,aAAa,WAAW,CAAC;AAC5C,UAAI,eAAe,OAAW,OAAM;AAAA,IACtC,SAAS,OAAO;AACd,WAAK,QAAQ,MAAM,6BAA6B,EAAE,MAAM,CAAC;AACzD,YAAM;AAAA,IACR,UAAE;AACA,sBAAgB,MAAM;AACtB,UAAI,kBAAmB,eAAc,iBAAiB;AACtD,YAAI,UAAK,QAAL,mBAAU,gBAAe,UAAU,MAAM;AAC3C,aAAK,IAAI,MAAM;AAAA,MACjB;AAAA,IACF;AAEA,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,MAAM,UAAU,IAAe,aAA0B;AACvD,UAAM,kBAAkB,KAAK;AAAA,MAC1B,KAAK,MAAM,aAAa,KAAK,MAAM,uBAAwB;AAAA,IAC9D;AACA,UAAM,eAAe,IAAI,gBAAgB,KAAK,MAAM,YAAY,GAAG,eAAe;AAElF,QAAI,WAAW;AACf,UAAM,WAAW,KAAK,MAAM,OAAO,aAAa,EAAE;AAClD,UAAM,eAAe,aAAa,WAAW;AAE7C,WAAO,MAAM;AACX,YAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,SAAS,KAAK,GAAG,YAAY,CAAC;AAEjE,UAAI,WAAW,OAAW;AAE1B,UAAI,OAAO,MAAM;AACf,mBAAW;AAAA,MACb,OAAO;AACL,cAAM,OAAO,OAAO;AACpB,cAAM,SAAuB,CAAC;AAE9B,YAAI,SAAS,aAAa,gBAAgB;AACxC,iBAAO,KAAK,GAAG,aAAa,MAAM,CAAC;AAAA,QACrC,OAAO;AAIL,iBAAO,KAAK,GAAG,aAAa,MAAM,KAAK,IAAI,CAAC;AAAA,QAC9C;AAEA,mBAAW,SAAS,QAAQ;AAC1B,eAAK,mBAAmB,8BAA8B,KAAK;AAC3D,cAAI,GAAG,eAAe,UAAU,MAAM;AACpC,eAAG,KAAK,MAAM,IAAI;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAEA,UAAI,SAAU;AAAA,IAChB;AAEA,QAAI,GAAG,eAAe,UAAU,MAAM;AACpC,WAAK,aAAa;AAClB,WAAK,QAAQ,MAAM,uCAAuC;AAC1D,SAAG,KAAK,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC,CAAC;AAKzC,YAAM,aAAa,WAAW,MAAM;AAClC,aAAK,QAAQ;AAAA,UACX,qCAAqC,gBAAgB;AAAA,QACvD;AACA,YAAI,GAAG,eAAe,UAAU,QAAQ,GAAG,eAAe,UAAU,SAAS;AAC3E,aAAG,UAAU;AAAA,QACf;AAAA,MACF,GAAG,gBAAgB;AACnB,SAAG,KAAK,SAAS,MAAM,aAAa,UAAU,CAAC;AAAA,IACjD;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,IAAe;AAC7B,QAAI,eAAkC;AAEtC,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,SAAS,CAAC,QAAgB;AAC9B,YAAI,IAAK,QAAO,GAAG;AAAA,YACd,SAAQ;AAAA,MACf;AAEA,SAAG,GAAG,WAAW,CAAC,MAAc,aAAsB;AACpD,YAAI,UAAU;AACZ,eAAK,QAAQ,KAAK,sDAAsD;AACxE;AAAA,QACF;AACA,YAAI;AACJ,YAAI;AACF,gBAAM,KAAK,MAAM,KAAK,SAAS,CAAC;AAAA,QAClC,SAAS,OAAO;AACd,eAAK,QAAQ,MAAM,wCAAwC,EAAE,MAAM,CAAC;AACpE;AAAA,QACF;AACA,YAAI;AACF,cAAI,IAAI,SAAS,SAAS;AACxB,iBAAK,QAAQ,MAAM,0BAA0B,GAAG;AAGhD,gBAAI,IAAI,gBAAgB,UAAa,IAAI,eAAe,KAAK;AAI3D,6BAAe,IAAI,mBAAmB;AAAA,gBACpC,SAAS,IAAI,WAAW,IAAI;AAAA,gBAC5B,SAAS;AAAA,kBACP,WAAW;AAAA,gBACb;AAAA,cACF,CAAC;AACD,iBAAG,MAAM;AACT;AAAA,YACF;AAAA,UACF,OAAO;AACL,iBAAK,oBAAoB,GAAG;AAAA,UAC9B;AAAA,QACF,SAAS,OAAO;AACd,eAAK,QAAQ,MAAM,0CAA0C,EAAE,MAAM,CAAC;AAAA,QACxE;AAAA,MACF,CAAC;AAED,SAAG,GAAG,SAAS,CAAC,MAAM,WAAW;AAI/B,YAAI,KAAK,WAAW;AAClB,cAAI,KAAK,oBAAoB;AAC3B,iBAAK;AAAA,cACH,IAAI,gBAAgB;AAAA,cACpB,KAAK;AAAA,YACP;AAAA,UACF;AACA,cAAI,CAAC,KAAK,MAAM;AACd,iBAAK,MAAM,IAAI;AAAA,cACb,MAAM,IAAI,gBAAgB;AAAA,cAC1B,WAAW,KAAK;AAAA,YAClB,CAAC;AACH,eAAK,QAAQ,MAAM,4BAA4B;AAC/C,cAAI,KAAK,kBAAkB,KAAK,CAAC,KAAK,MAAM,QAAQ;AAClD,iBAAK,MAAM,IAAI;AAAA,cACb,MAAM,IAAI,gBAAgB;AAAA,cAC1B,WAAW,KAAK;AAAA,cAChB,kBAAkB,EAAE,eAAe,KAAK,gBAAgB;AAAA,YAC1D,CAAC;AACD,iBAAK,kBAAkB;AAAA,UACzB;AACA,eAAK,YAAY;AACjB,eAAK,qBAAqB;AAAA,QAC5B;AAEA,YAAI,cAAc;AAChB,iBAAO,YAAY;AACnB;AAAA,QACF;AACA,YAAI,KAAK,cAAc,KAAK,QAAQ;AAClC,iBAAO;AACP;AAAA,QACF;AACA,aAAK,QAAQ,KAAK,kCAAkC,IAAI,IAAI,OAAO,SAAS,CAAC,EAAE;AAC/E;AAAA,UACE,IAAI,mBAAmB;AAAA,YACrB,SAAS,qDAAqD,IAAI;AAAA,YAClE,SAAS,EAAE,WAAW,KAAK;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAED,SAAG,GAAG,SAAS,CAAC,QAAQ;AACtB,YAAI,KAAK,QAAQ;AACf,iBAAO;AACP;AAAA,QACF;AACA,eAAO,IAAI,mBAAmB,EAAE,SAAS,IAAI,SAAS,SAAS,EAAE,WAAW,KAAK,EAAE,CAAC,CAAC;AAAA,MACvF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,oBAAoB,MAAmD;AACrE,QAAI,KAAK,YAAY;AACnB,WAAK,aAAa,KAAK;AAAA,IACzB;AAEA,YAAQ,KAAK,MAAM;AAAA,MACjB,KAAK;AACH;AAAA,MAEF,KAAK,cAAc;AACjB,YAAI,KAAK,UAAW;AACpB,aAAK,YAAY;AACjB,aAAK,qBAAqB;AAC1B,YAAI,CAAC,KAAK,MAAM;AACd,eAAK,MAAM,IAAI;AAAA,YACb,MAAM,IAAI,gBAAgB;AAAA,YAC1B,WAAW,KAAK;AAAA,UAClB,CAAC;AACH,aAAK,QAAQ,MAAM,8BAA8B;AACjD;AAAA,MACF;AAAA,MAEA,KAAK,eAAe;AAClB,YAAI,CAAC,KAAK,UAAW;AACrB,cAAM,aAAa,KAAK;AACxB,YAAI,CAAC,WAAY;AAGjB,YAAI,KAAK,uBAAuB,WAAY;AAC5C,aAAK,qBAAqB;AAC1B,aAAK,qBAAqB,IAAI,gBAAgB,oBAAoB,UAAU;AAC5E;AAAA,MACF;AAAA,MAEA,KAAK,kBAAkB;AACrB,YAAI,CAAC,KAAK,UAAW;AACrB,cAAM,aAAa,KAAK,cAAc,KAAK;AAC3C,YAAI,CAAC,WAAY;AACjB,aAAK,qBAAqB;AAC1B,aAAK,qBAAqB,IAAI,gBAAgB,sBAAsB,UAAU;AAC9E;AAAA,MACF;AAAA,MAEA,KAAK;AAGH,YAAI,CAAC,KAAK,aAAa,CAAC,KAAK,mBAAoB;AACjD,aAAK,qBAAqB,IAAI,gBAAgB,oBAAoB,KAAK,kBAAkB;AACzF;AAAA,MAEF,KAAK,YAAY;AACf,YAAI,CAAC,KAAK,UAAW;AACrB,cAAM,aAAa,KAAK,cAAc,KAAK;AAE3C,aAAK,qBAAqB,IAAI,gBAAgB,kBAAkB,UAAU;AAE1E,aAAK,YAAY;AACjB,YAAI,CAAC,KAAK,MAAM;AACd,eAAK,MAAM,IAAI;AAAA,YACb,MAAM,IAAI,gBAAgB;AAAA,YAC1B,WAAW,KAAK;AAAA,UAClB,CAAC;AACH,aAAK,QAAQ,MAAM,4BAA4B;AAC/C,aAAK,qBAAqB;AAE1B,YAAI,KAAK,kBAAkB,KAAK,CAAC,KAAK,MAAM,QAAQ;AAClD,eAAK,MAAM,IAAI;AAAA,YACb,MAAM,IAAI,gBAAgB;AAAA,YAC1B,WAAW,KAAK;AAAA,YAChB,kBAAkB,EAAE,eAAe,KAAK,gBAAgB;AAAA,UAC1D,CAAC;AACD,eAAK,kBAAkB;AAAA,QACzB;AAEA;AAAA,MACF;AAAA,MAEA;AACE,aAAK,QAAQ,KAAK,iDAAiD,EAAE,KAAK,CAAC;AAAA,IAC/E;AAAA,EACF;AAAA,EAEA,qBAAqB,WAAgC,YAAoB;AACvE,QAAI,KAAK,MAAM,OAAQ;AACvB,SAAK,MAAM,IAAI;AAAA,MACb,MAAM;AAAA,MACN,WAAW,KAAK;AAAA,MAChB,cAAc;AAAA,QACZ;AAAA,UACE,UAAU,eAAe,KAAK,MAAM,QAAQ;AAAA,UAC5C,MAAM;AAAA,UACN,WAAW;AAAA,UACX,SAAS;AAAA,UACT,YAAY;AAAA,QACd;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,kBAA0B;AAKxB,UAAM,SAAS,IAAI,gBAAgB;AAAA,MACjC,OAAO,KAAK,MAAM;AAAA,MAClB,aAAa,KAAK,MAAM,WAAW,SAAS;AAAA,MAC5C,UAAU;AAAA,IACZ,CAAC;AAED,UAAM,SAAS,KAAK,MAAM,QAAQ,QAAQ,SAAS,IAAI;AACvD,WAAO,GAAG,MAAM,wBAAwB,OAAO,SAAS,CAAC;AAAA,EAC3D;AAAA,EAES,QAAQ;AAxoBnB;AAyoBI,UAAM,MAAM;AACZ,eAAK,QAAL,mBAAU;AAAA,EACZ;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livekit/agents-plugin-cartesia",
3
- "version": "1.4.8",
3
+ "version": "1.4.11",
4
4
  "description": "Cartesia plugin for LiveKit Node Agents",
5
5
  "main": "dist/index.js",
6
6
  "require": "dist/index.cjs",
@@ -30,10 +30,10 @@
30
30
  "@types/ws": "^8.5.10",
31
31
  "tsup": "^8.3.5",
32
32
  "typescript": "^5.0.0",
33
- "@livekit/agents": "1.4.8",
34
- "@livekit/agents-plugin-openai": "1.4.8",
35
- "@livekit/agents-plugin-silero": "1.4.8",
36
- "@livekit/agents-plugins-test": "1.4.8"
33
+ "@livekit/agents": "1.4.11",
34
+ "@livekit/agents-plugin-openai": "1.4.11",
35
+ "@livekit/agents-plugin-silero": "1.4.11",
36
+ "@livekit/agents-plugins-test": "1.4.11"
37
37
  },
38
38
  "dependencies": {
39
39
  "ws": "^8.18.0"
@@ -41,7 +41,7 @@
41
41
  "peerDependencies": {
42
42
  "@livekit/rtc-node": "^0.13.29",
43
43
  "zod": "^3.25.76 || ^4.1.8",
44
- "@livekit/agents": "1.4.8"
44
+ "@livekit/agents": "1.4.11"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup --onSuccess \"pnpm build:types\"",
package/src/models.ts CHANGED
@@ -71,5 +71,7 @@ export type TTSEncoding = 'pcm_s16le';
71
71
 
72
72
  /**
73
73
  * See [the docs](https://docs.cartesia.ai/build-with-cartesia/stt-models/latest) for all options.
74
+ *
75
+ * `ink-2` currently supports English only; `ink-whisper` is multilingual.
74
76
  */
75
- export type STTModel = 'ink-2';
77
+ export type STTModel = 'ink-2' | 'ink-whisper';
package/src/stt.ts CHANGED
@@ -7,7 +7,9 @@ import {
7
7
  AudioByteStream,
8
8
  asLanguageCode,
9
9
  calculateAudioDurationSeconds,
10
+ getBaseLanguage,
10
11
  log,
12
+ normalizeLanguage,
11
13
  stt,
12
14
  waitForAbort,
13
15
  } from '@livekit/agents';
@@ -140,6 +142,7 @@ export type STTOptions = {
140
142
  sampleRate: number;
141
143
  baseUrl: string;
142
144
  audioChunkDurationMS: number;
145
+ language: string;
143
146
  };
144
147
 
145
148
  const defaultSTTOptions = {
@@ -149,6 +152,7 @@ const defaultSTTOptions = {
149
152
  /** recommended default */
150
153
  audioChunkDurationMS: 160,
151
154
  baseUrl: 'https://api.cartesia.ai',
155
+ language: 'en',
152
156
  };
153
157
 
154
158
  function mergeSTTOptions(base: STTOptions, override: Partial<STTOptions>): STTOptions {
@@ -158,9 +162,20 @@ function mergeSTTOptions(base: STTOptions, override: Partial<STTOptions>): STTOp
158
162
  model: override.model ?? base.model,
159
163
  sampleRate: override.sampleRate ?? base.sampleRate,
160
164
  audioChunkDurationMS: override.audioChunkDurationMS ?? base.audioChunkDurationMS,
165
+ language:
166
+ override.language !== undefined ? normalizeLanguage(override.language) : base.language,
161
167
  };
162
168
  }
163
169
 
170
+ /**
171
+ * Resolve the STT model from the configured language when the caller did not
172
+ * pass one explicitly. Mirrors the Python plugin: `ink-2` only supports
173
+ * English, so non-English languages route to the multilingual `ink-whisper`.
174
+ */
175
+ function resolveSTTModel(language: string): STTModel {
176
+ return getBaseLanguage(language) === 'en' ? 'ink-2' : 'ink-whisper';
177
+ }
178
+
164
179
  /**
165
180
  * Cartesia speech to text.
166
181
  *
@@ -203,6 +218,12 @@ export class STT extends stt.STT {
203
218
  }
204
219
 
205
220
  this.#opts = mergeSTTOptions({ ...defaultSTTOptions, apiKey }, opts);
221
+
222
+ // Route to the multilingual model for non-English languages unless the
223
+ // caller pinned a model. Matches the Python plugin's language→model logic.
224
+ if (opts.model === undefined) {
225
+ this.#opts.model = resolveSTTModel(this.#opts.language);
226
+ }
206
227
  }
207
228
 
208
229
  override get label(): string {
@@ -224,6 +245,17 @@ export class STT extends stt.STT {
224
245
  override stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {
225
246
  return new SpeechStream(this, this.#opts, options?.connOptions);
226
247
  }
248
+
249
+ updateOptions(opts: Partial<STTOptions>) {
250
+ this.#opts = mergeSTTOptions(this.#opts, opts);
251
+
252
+ // Keep the model in sync with a newly set language (e.g. switching to a
253
+ // non-English language must move off the English-only ink-2), unless the
254
+ // caller pinned a model in the same call. Mirrors the constructor.
255
+ if (opts.language !== undefined && opts.model === undefined) {
256
+ this.#opts.model = resolveSTTModel(this.#opts.language);
257
+ }
258
+ }
227
259
  }
228
260
 
229
261
  export class SpeechStream extends stt.SpeechStream {
@@ -536,7 +568,7 @@ export class SpeechStream extends stt.SpeechStream {
536
568
 
537
569
  case 'turn.eager_end': {
538
570
  if (!this.#speaking) return;
539
- const transcript = data.transcript;
571
+ const transcript = data.transcript || this.#currentTranscript;
540
572
  if (!transcript) return;
541
573
  this.#currentTranscript = transcript;
542
574
  this.#sendTranscriptEvent(stt.SpeechEventType.PREFLIGHT_TRANSCRIPT, transcript);
@@ -552,7 +584,7 @@ export class SpeechStream extends stt.SpeechStream {
552
584
 
553
585
  case 'turn.end': {
554
586
  if (!this.#speaking) return;
555
- const transcript = data.transcript;
587
+ const transcript = data.transcript || this.#currentTranscript;
556
588
 
557
589
  this.#sendTranscriptEvent(stt.SpeechEventType.FINAL_TRANSCRIPT, transcript);
558
590
 
@@ -589,8 +621,7 @@ export class SpeechStream extends stt.SpeechStream {
589
621
  requestId: this.#requestId,
590
622
  alternatives: [
591
623
  {
592
- // Cartesia STT only supports English at this time.
593
- language: asLanguageCode('en'),
624
+ language: asLanguageCode(this.#opts.language),
594
625
  text: transcript,
595
626
  startTime: 0,
596
627
  endTime: 0,
@@ -601,6 +632,10 @@ export class SpeechStream extends stt.SpeechStream {
601
632
  }
602
633
 
603
634
  #getCartesiaUrl(): string {
635
+ // The Cartesia /stt/turns/websocket endpoint only accepts model, sample_rate
636
+ // and encoding — there is no `language` query param. Language selection is
637
+ // expressed through the model (ink-2 for English, ink-whisper otherwise),
638
+ // so #opts.language is used only to tag emitted transcripts.
604
639
  const params = new URLSearchParams({
605
640
  model: this.#opts.model,
606
641
  sample_rate: this.#opts.sampleRate.toString(),