@livekit/agents-plugin-assemblyai 0.0.0 → 1.2.7

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <!--
2
- SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ SPDX-FileCopyrightText: 2026 LiveKit, Inc.
3
3
 
4
4
  SPDX-License-Identifier: Apache-2.0
5
5
  -->
@@ -9,8 +9,10 @@ The Agents Framework is designed for building realtime, programmable
9
9
  participants that run on servers. Use it to create conversational, multi-modal
10
10
  voice agents that can see, hear, and understand.
11
11
 
12
- This package contains the AssemblyAI plugin, which allows for speech recognition.
13
- Refer to the [documentation](https://docs.livekit.io/agents/overview/) for
14
- information on how to use it.
12
+ This package contains the AssemblyAI plugin, which allows for speech recognition
13
+ via AssemblyAI's Universal-Streaming (v3) API. Refer to the
14
+ [documentation](https://docs.livekit.io/agents/overview/) for information on how
15
+ to use it, or browse the [API
16
+ reference](https://docs.livekit.io/agents-js/modules/plugins_agents_plugin_assemblyai.html).
15
17
  See the [repository](https://github.com/livekit/agents-js) for more information
16
18
  about the framework as a whole.
package/dist/index.cjs CHANGED
@@ -13,11 +13,24 @@ var __copyProps = (to, from, except, desc) => {
13
13
  };
14
14
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
15
15
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
16
- var src_exports = {};
17
- module.exports = __toCommonJS(src_exports);
18
- __reExport(src_exports, require("./stt.cjs"), module.exports);
16
+ var index_exports = {};
17
+ module.exports = __toCommonJS(index_exports);
18
+ var import_agents = require("@livekit/agents");
19
+ __reExport(index_exports, require("./models.cjs"), module.exports);
20
+ __reExport(index_exports, require("./stt.cjs"), module.exports);
21
+ class AssemblyAIPlugin extends import_agents.Plugin {
22
+ constructor() {
23
+ super({
24
+ title: "assemblyai",
25
+ version: "1.2.7",
26
+ package: "@livekit/agents-plugin-assemblyai"
27
+ });
28
+ }
29
+ }
30
+ import_agents.Plugin.registerPlugin(new AssemblyAIPlugin());
19
31
  // Annotate the CommonJS export names for ESM import in node:
20
32
  0 && (module.exports = {
33
+ ...require("./models.cjs"),
21
34
  ...require("./stt.cjs")
22
35
  });
23
36
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\nexport * from './stt.js';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAIA,wBAAc,qBAJd;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { Plugin } from '@livekit/agents';\n\nexport * from './models.js';\nexport * from './stt.js';\n\nclass AssemblyAIPlugin extends Plugin {\n constructor() {\n super({\n title: 'assemblyai',\n version: __PACKAGE_VERSION__,\n package: __PACKAGE_NAME__,\n });\n }\n}\n\nPlugin.registerPlugin(new AssemblyAIPlugin());\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAGA,oBAAuB;AAEvB,0BAAc,wBALd;AAMA,0BAAc,qBANd;AAQA,MAAM,yBAAyB,qBAAO;AAAA,EACpC,cAAc;AACZ,UAAM;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AACF;AAEA,qBAAO,eAAe,IAAI,iBAAiB,CAAC;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './models.js';
1
2
  export * from './stt.js';
2
3
  //# sourceMappingURL=index.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './models.js';
1
2
  export * from './stt.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,14 @@
1
+ import { Plugin } from "@livekit/agents";
2
+ export * from "./models.js";
1
3
  export * from "./stt.js";
4
+ class AssemblyAIPlugin extends Plugin {
5
+ constructor() {
6
+ super({
7
+ title: "assemblyai",
8
+ version: "1.2.7",
9
+ package: "@livekit/agents-plugin-assemblyai"
10
+ });
11
+ }
12
+ }
13
+ Plugin.registerPlugin(new AssemblyAIPlugin());
2
14
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\nexport * from './stt.js';\n"],"mappings":"AAIA,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { Plugin } from '@livekit/agents';\n\nexport * from './models.js';\nexport * from './stt.js';\n\nclass AssemblyAIPlugin extends Plugin {\n constructor() {\n super({\n title: 'assemblyai',\n version: __PACKAGE_VERSION__,\n package: __PACKAGE_NAME__,\n });\n }\n}\n\nPlugin.registerPlugin(new AssemblyAIPlugin());\n"],"mappings":"AAGA,SAAS,cAAc;AAEvB,cAAc;AACd,cAAc;AAEd,MAAM,yBAAyB,OAAO;AAAA,EACpC,cAAc;AACZ,UAAM;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AACF;AAEA,OAAO,eAAe,IAAI,iBAAiB,CAAC;","names":[]}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+ var models_exports = {};
16
+ module.exports = __toCommonJS(models_exports);
17
+ //# sourceMappingURL=models.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/models.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n\nexport type STTModels =\n | 'universal-streaming-english'\n | 'universal-streaming-multilingual'\n | 'u3-rt-pro'\n // Deprecated alias — AssemblyAI maps this to `u3-rt-pro` server-side, but the\n // Python plugin emits a warning and rewrites it. Kept here so TS users don't\n // break if they already pass it.\n | 'u3-pro';\n\nexport type STTEncoding = 'pcm_s16le' | 'pcm_mulaw';\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -0,0 +1,3 @@
1
+ export type STTModels = 'universal-streaming-english' | 'universal-streaming-multilingual' | 'u3-rt-pro' | 'u3-pro';
2
+ export type STTEncoding = 'pcm_s16le' | 'pcm_mulaw';
3
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1,3 @@
1
+ export type STTModels = 'universal-streaming-english' | 'universal-streaming-multilingual' | 'u3-rt-pro' | 'u3-pro';
2
+ export type STTEncoding = 'pcm_s16le' | 'pcm_mulaw';
3
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GACjB,6BAA6B,GAC7B,kCAAkC,GAClC,WAAW,GAIX,QAAQ,CAAC;AAEb,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC"}
package/dist/models.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/stt.cjs CHANGED
@@ -23,144 +23,423 @@ __export(stt_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(stt_exports);
25
25
  var import_agents = require("@livekit/agents");
26
- var import_assemblyai = require("assemblyai");
26
+ var import_ws = require("ws");
27
27
  const defaultSTTOptions = {
28
- apiKey: process.env.ASSEMBLY_AI_KEY,
29
- interimResults: true,
28
+ apiKey: process.env.ASSEMBLYAI_API_KEY,
30
29
  sampleRate: 16e3,
31
- keywords: [],
32
- // NOTE:
33
- // The default is 700ms from AssemblyAI.
34
- // We use a low default of 300ms here because we also use
35
- // the new end-of-utterance model from LiveKit to handle
36
- // turn detection in my agent. Which means that even though
37
- // this will quickly return a final transcript EVEN THOUGH
38
- // USER IS NOT DONE SPEAKING, the EOU model from LiveKit
39
- // DOES properly differentiate and doesn't interrupt (magically!)
40
- // Ref: https://blog.livekit.io/using-a-transformer-to-improve-end-of-turn-detection/
41
- endUtteranceSilenceThreshold: 200
30
+ bufferSizeMs: 50,
31
+ encoding: "pcm_s16le",
32
+ speechModel: "universal-streaming-english",
33
+ baseUrl: "wss://streaming.assemblyai.com"
42
34
  };
43
35
  class STT extends import_agents.stt.STT {
44
36
  #opts;
45
- #logger = (0, import_agents.log)();
37
+ #streams = /* @__PURE__ */ new Set();
46
38
  label = "assemblyai.STT";
47
- constructor(opts = defaultSTTOptions) {
39
+ get model() {
40
+ return this.#opts.speechModel;
41
+ }
42
+ get provider() {
43
+ return "AssemblyAI";
44
+ }
45
+ constructor(opts = {}) {
48
46
  super({
49
47
  streaming: true,
50
- interimResults: opts.interimResults ?? defaultSTTOptions.interimResults
48
+ interimResults: true,
49
+ alignedTranscript: "word"
51
50
  });
52
- if (opts.apiKey === void 0 && defaultSTTOptions.apiKey === void 0) {
51
+ if (opts.speechModel === "u3-pro") {
52
+ (0, import_agents.log)().warn("'u3-pro' is deprecated, use 'u3-rt-pro' instead.");
53
+ opts.speechModel = "u3-rt-pro";
54
+ }
55
+ if (opts.prompt !== void 0 && opts.speechModel !== "u3-rt-pro") {
56
+ throw new Error("The 'prompt' parameter is only supported with the 'u3-rt-pro' model.");
57
+ }
58
+ const apiKey = opts.apiKey ?? defaultSTTOptions.apiKey;
59
+ if (!apiKey) {
53
60
  throw new Error(
54
- "AssemblyAI API key is required, whether as an argument or as $ASSEMBLY_AI_KEY"
61
+ "AssemblyAI API key is required. Pass one in via the `apiKey` parameter, or set it as the `ASSEMBLYAI_API_KEY` environment variable"
55
62
  );
56
63
  }
57
- this.#opts = { ...defaultSTTOptions, ...opts };
64
+ const minTurnSilence = opts.minTurnSilence ?? 100;
65
+ this.#opts = {
66
+ ...defaultSTTOptions,
67
+ ...opts,
68
+ apiKey,
69
+ minTurnSilence
70
+ };
58
71
  }
59
72
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
60
73
  async _recognize(_) {
61
- throw new Error("Recognize is not supported on AssemblyAI STT");
74
+ throw new Error("Non-streaming recognize is not supported on AssemblyAI STT");
62
75
  }
63
- stream() {
64
- return new SpeechStream(this, this.#opts);
76
+ updateOptions(opts) {
77
+ this.#opts = { ...this.#opts, ...opts };
78
+ for (const ref of this.#streams) {
79
+ const stream = ref.deref();
80
+ if (stream) {
81
+ stream.updateOptions(opts);
82
+ } else {
83
+ this.#streams.delete(ref);
84
+ }
85
+ }
86
+ }
87
+ stream(options) {
88
+ const stream = new SpeechStream(this, this.#opts, options == null ? void 0 : options.connOptions);
89
+ this.#streams.add(new WeakRef(stream));
90
+ return stream;
65
91
  }
66
92
  }
67
93
  class SpeechStream extends import_agents.stt.SpeechStream {
94
+ static CLOSE_MSG = JSON.stringify({ type: "Terminate" });
68
95
  #opts;
69
96
  #logger = (0, import_agents.log)();
70
- #speaking = false;
71
- #client;
72
- #transcriber;
97
+ #speechDurationInS = 0;
98
+ #lastPreflightStartTime = 0;
99
+ #pendingConfigMessages = [];
100
+ #configMessagePending = new import_agents.Future();
101
+ #sessionId = null;
102
+ #expiresAt = null;
73
103
  label = "assemblyai.SpeechStream";
74
- constructor(stt2, opts) {
75
- super(stt2);
104
+ constructor(stt2, opts, connOptions) {
105
+ super(stt2, opts.sampleRate, connOptions);
76
106
  this.#opts = opts;
77
107
  this.closed = false;
78
- this.#client = new import_assemblyai.AssemblyAI({
79
- apiKey: this.#opts.apiKey || ""
80
- });
81
- this.#run();
82
108
  }
83
- async #run() {
84
- try {
85
- this.#transcriber = this.#client.realtime.transcriber({
86
- sampleRate: this.#opts.sampleRate,
87
- wordBoost: this.#opts.keywords.map((k) => k[0]),
88
- endUtteranceSilenceThreshold: this.#opts.endUtteranceSilenceThreshold
89
- });
90
- this.#transcriber.on("open", (data) => {
91
- this.#logger.child({ sessionId: data.sessionId, expiresAt: data.expiresAt }).debug(`AssemblyAI session opened`);
92
- });
93
- this.#transcriber.on("close", (code, reason) => {
94
- this.#logger.child({ code, reason }).debug(`AssemblyAI session closed`);
95
- if (!this.closed) {
96
- this.#run();
97
- }
98
- });
99
- this.#transcriber.on("error", (error) => {
100
- this.#logger.child({ error: error.message }).error(`AssemblyAI error`);
101
- });
102
- this.#transcriber.on("transcript", (transcript) => {
103
- if (this.closed) return;
104
- if (!transcript.text || transcript.text.trim() === "") {
105
- return;
106
- }
107
- if (!this.#speaking) {
108
- this.#speaking = true;
109
- this.queue.put({ type: import_agents.stt.SpeechEventType.START_OF_SPEECH });
110
- }
111
- if (transcript.message_type === "PartialTranscript") {
112
- this.queue.put({
113
- type: import_agents.stt.SpeechEventType.INTERIM_TRANSCRIPT,
114
- alternatives: [assemblyTranscriptToSpeechData(transcript)]
115
- });
116
- } else if (transcript.message_type === "FinalTranscript") {
117
- this.queue.put({
118
- type: import_agents.stt.SpeechEventType.FINAL_TRANSCRIPT,
119
- alternatives: [assemblyTranscriptToSpeechData(transcript)]
120
- });
109
+ /**
110
+ * The AssemblyAI session ID. Set when the WebSocket connection is established
111
+ * (before any speech events). Null until the connection completes.
112
+ * Share this with the AssemblyAI team when reporting issues.
113
+ */
114
+ get sessionId() {
115
+ return this.#sessionId;
116
+ }
117
+ /**
118
+ * Unix timestamp when the AssemblyAI session expires. Set alongside
119
+ * {@link sessionId} when the WebSocket connection is established.
120
+ */
121
+ get expiresAt() {
122
+ return this.#expiresAt;
123
+ }
124
+ updateOptions(opts) {
125
+ this.#opts = { ...this.#opts, ...opts };
126
+ const configMsg = { type: "UpdateConfiguration" };
127
+ if (opts.prompt !== void 0) configMsg.prompt = opts.prompt;
128
+ if (opts.keytermsPrompt !== void 0) configMsg.keyterms_prompt = opts.keytermsPrompt;
129
+ if (opts.maxTurnSilence !== void 0) configMsg.max_turn_silence = opts.maxTurnSilence;
130
+ if (opts.minTurnSilence !== void 0) configMsg.min_turn_silence = opts.minTurnSilence;
131
+ if (opts.endOfTurnConfidenceThreshold !== void 0) {
132
+ configMsg.end_of_turn_confidence_threshold = opts.endOfTurnConfidenceThreshold;
133
+ }
134
+ if (opts.vadThreshold !== void 0) configMsg.vad_threshold = opts.vadThreshold;
135
+ if (Object.keys(configMsg).length > 1) {
136
+ this.#pendingConfigMessages.push(configMsg);
137
+ if (!this.#configMessagePending.done) this.#configMessagePending.resolve();
138
+ }
139
+ }
140
+ /**
141
+ * Force-finalize the current turn immediately.
142
+ */
143
+ forceEndpoint() {
144
+ this.#pendingConfigMessages.push({ type: "ForceEndpoint" });
145
+ if (!this.#configMessagePending.done) this.#configMessagePending.resolve();
146
+ }
147
+ // Deepgram-style reconnect loop around a single websocket lifetime.
148
+ async run() {
149
+ const maxRetry = 32;
150
+ let retries = 0;
151
+ while (!this.input.closed && !this.closed) {
152
+ try {
153
+ const ws = await this.#connectWS();
154
+ await this.#runWS(ws);
155
+ retries = 0;
156
+ } catch (e) {
157
+ if (!this.closed && !this.input.closed) {
158
+ if (retries >= maxRetry) {
159
+ throw new Error(`failed to connect to AssemblyAI after ${retries} attempts: ${e}`);
160
+ }
161
+ const retryDelaySeconds = Math.min(retries * 5, 10);
162
+ retries++;
163
+ this.#logger.warn(
164
+ `failed to connect to AssemblyAI, retrying in ${retryDelaySeconds} seconds: ${e} (${retries}/${maxRetry})`
165
+ );
166
+ await (0, import_agents.delay)(retryDelaySeconds * 1e3);
167
+ } else {
168
+ this.#logger.warn(
169
+ `AssemblyAI disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`
170
+ );
121
171
  }
172
+ }
173
+ }
174
+ this.closed = true;
175
+ }
176
+ async #connectWS() {
177
+ let minSilence = this.#opts.minTurnSilence;
178
+ let maxSilence = this.#opts.maxTurnSilence;
179
+ if (this.#opts.speechModel === "u3-rt-pro") {
180
+ if (minSilence === void 0) minSilence = 100;
181
+ if (maxSilence === void 0) maxSilence = minSilence;
182
+ }
183
+ const defaultLanguageDetection = this.#opts.speechModel.includes("multilingual") || this.#opts.speechModel === "u3-rt-pro";
184
+ const languageDetection = this.#opts.languageDetection ?? defaultLanguageDetection;
185
+ const liveConfig = {
186
+ sample_rate: this.#opts.sampleRate,
187
+ encoding: this.#opts.encoding,
188
+ speech_model: this.#opts.speechModel,
189
+ format_turns: this.#opts.formatTurns,
190
+ end_of_turn_confidence_threshold: this.#opts.endOfTurnConfidenceThreshold,
191
+ min_turn_silence: minSilence,
192
+ max_turn_silence: maxSilence,
193
+ keyterms_prompt: this.#opts.keytermsPrompt !== void 0 ? JSON.stringify(this.#opts.keytermsPrompt) : void 0,
194
+ language_detection: languageDetection,
195
+ prompt: this.#opts.prompt,
196
+ vad_threshold: this.#opts.vadThreshold,
197
+ speaker_labels: this.#opts.speakerLabels,
198
+ max_speakers: this.#opts.maxSpeakers,
199
+ domain: this.#opts.domain
200
+ };
201
+ const url = new URL(`${this.#opts.baseUrl}/v3/ws`);
202
+ for (const [key, value] of Object.entries(liveConfig)) {
203
+ if (value === void 0 || value === null) continue;
204
+ if (typeof value === "boolean") {
205
+ url.searchParams.append(key, value ? "true" : "false");
206
+ } else {
207
+ url.searchParams.append(key, String(value));
208
+ }
209
+ }
210
+ const ws = new import_ws.WebSocket(url, {
211
+ headers: {
212
+ Authorization: this.#opts.apiKey,
213
+ "Content-Type": "application/json",
214
+ "User-Agent": "AssemblyAI/1.0 (integration=Livekit)"
215
+ }
216
+ });
217
+ await new Promise((resolve, reject) => {
218
+ ws.on("open", () => resolve());
219
+ ws.on("error", (error) => reject(error));
220
+ ws.on("close", (code) => reject(new Error(`WebSocket returned ${code}`)));
221
+ });
222
+ return ws;
223
+ }
224
+ async #runWS(ws) {
225
+ let closing = false;
226
+ const sessionController = new AbortController();
227
+ const wsMonitor = import_agents.Task.from(async (controller) => {
228
+ const closed = new Promise((_, reject) => {
229
+ ws.once("close", (code, reason) => {
230
+ if (!closing) {
231
+ this.#logger.error(`WebSocket closed with code ${code}: ${reason}`);
232
+ reject(new Error("WebSocket closed"));
233
+ }
234
+ });
122
235
  });
123
- await this.#transcriber.connect();
124
- const sendTask = async () => {
125
- var _a;
126
- const samples100Ms = Math.floor(this.#opts.sampleRate / 10);
127
- const stream = new import_agents.AudioByteStream(this.#opts.sampleRate, 1, samples100Ms);
128
- for await (const data of this.input) {
129
- if (this.closed) break;
236
+ await Promise.race([closed, (0, import_agents.waitForAbort)(controller.signal)]);
237
+ });
238
+ const sendTask = async () => {
239
+ const samplesPerBuffer = Math.floor(this.#opts.sampleRate * this.#opts.bufferSizeMs / 1e3);
240
+ const audioStream = new import_agents.AudioByteStream(this.#opts.sampleRate, 1, samplesPerBuffer);
241
+ const abortPromise = (0, import_agents.waitForAbort)(this.abortSignal);
242
+ const sessionAbort = (0, import_agents.waitForAbort)(sessionController.signal);
243
+ try {
244
+ while (!this.closed) {
245
+ const result = await Promise.race([this.input.next(), abortPromise, sessionAbort]);
246
+ if (result === void 0) return;
247
+ if (result.done) break;
248
+ const data = result.value;
130
249
  let frames;
131
250
  if (data === SpeechStream.FLUSH_SENTINEL) {
132
- frames = stream.flush();
133
- } else if (data.sampleRate === this.#opts.sampleRate) {
134
- frames = stream.write(data.data.buffer);
251
+ frames = audioStream.flush();
252
+ } else if (data.sampleRate === this.#opts.sampleRate && data.channels === 1) {
253
+ frames = audioStream.write(data.data.buffer);
135
254
  } else {
136
- throw new Error(`Sample rate or channel count of frame does not match`);
255
+ throw new Error("sample rate or channel count of frame does not match");
137
256
  }
138
- for await (const frame of frames) {
139
- (_a = this.#transcriber) == null ? void 0 : _a.sendAudio(new Uint8Array(frame.data.buffer));
257
+ for (const frame of frames) {
258
+ this.#speechDurationInS += frame.samplesPerChannel / frame.sampleRate;
259
+ ws.send(frame.data.buffer);
140
260
  }
141
261
  }
142
- if (this.#transcriber) {
143
- await this.#transcriber.close();
262
+ } finally {
263
+ closing = true;
264
+ try {
265
+ ws.send(SpeechStream.CLOSE_MSG);
266
+ } catch {
267
+ }
268
+ wsMonitor.cancel();
269
+ }
270
+ };
271
+ let messageHandler = null;
272
+ const listenTask = import_agents.Task.from(async (controller) => {
273
+ const listenMessage = new Promise((resolve, reject) => {
274
+ messageHandler = (msg, isBinary) => {
275
+ if (isBinary) {
276
+ this.#logger.error("unexpected binary message from AssemblyAI");
277
+ return;
278
+ }
279
+ try {
280
+ const json = JSON.parse(msg.toString());
281
+ this.#processStreamEvent(json);
282
+ if (this.closed || closing) {
283
+ resolve();
284
+ }
285
+ } catch (err) {
286
+ this.#logger.error(`AssemblyAI: error processing message: ${msg}`);
287
+ reject(err);
288
+ }
289
+ };
290
+ ws.on("message", messageHandler);
291
+ });
292
+ await Promise.race([listenMessage, (0, import_agents.waitForAbort)(controller.signal)]);
293
+ });
294
+ const configTask = import_agents.Task.from(async (controller) => {
295
+ while (this.#pendingConfigMessages.length > 0) {
296
+ const msg = this.#pendingConfigMessages.shift();
297
+ ws.send(JSON.stringify(msg));
298
+ }
299
+ while (!controller.signal.aborted) {
300
+ await Promise.race([this.#configMessagePending.await, (0, import_agents.waitForAbort)(controller.signal)]);
301
+ if (controller.signal.aborted) return;
302
+ this.#configMessagePending = new import_agents.Future();
303
+ while (this.#pendingConfigMessages.length > 0) {
304
+ const msg = this.#pendingConfigMessages.shift();
305
+ ws.send(JSON.stringify(msg));
144
306
  }
145
- };
146
- await sendTask();
147
- } catch (error) {
148
- this.#logger.child({ error }).error(`Error in AssemblyAI STT`);
149
- if (!this.closed) {
150
- setTimeout(() => this.#run(), 5e3);
307
+ }
308
+ });
309
+ try {
310
+ await Promise.all([sendTask(), listenTask.result, wsMonitor.result]);
311
+ } finally {
312
+ closing = true;
313
+ sessionController.abort();
314
+ listenTask.cancel();
315
+ configTask.cancel();
316
+ if (messageHandler) ws.off("message", messageHandler);
317
+ try {
318
+ ws.close();
319
+ } catch {
320
+ }
321
+ }
322
+ }
323
+ #averageConfidence(words) {
324
+ if (words.length === 0) return 0;
325
+ return words.reduce((sum, w) => sum + (w.confidence ?? 0), 0) / words.length;
326
+ }
327
+ #processStreamEvent(data) {
328
+ const messageType = data.type;
329
+ if (messageType === "Begin") {
330
+ this.#sessionId = data.id ?? null;
331
+ this.#expiresAt = data.expires_at ?? null;
332
+ this.#logger.info(
333
+ `AssemblyAI session started id=${this.#sessionId} expires_at=${this.#expiresAt}`
334
+ );
335
+ return;
336
+ }
337
+ if (messageType === "SpeechStarted") {
338
+ this.queue.put({ type: import_agents.stt.SpeechEventType.START_OF_SPEECH });
339
+ return;
340
+ }
341
+ if (messageType === "Termination") {
342
+ this.#logger.debug(
343
+ `AssemblyAI session terminated audio_duration=${data.audio_duration_seconds}s session_duration=${data.session_duration_seconds}s`
344
+ );
345
+ return;
346
+ }
347
+ if (messageType !== "Turn") {
348
+ return;
349
+ }
350
+ const words = data.words ?? [];
351
+ const endOfTurn = Boolean(data.end_of_turn);
352
+ const turnIsFormatted = Boolean(data.turn_is_formatted);
353
+ const utterance = data.utterance ?? "";
354
+ const transcript = data.transcript ?? "";
355
+ const language = (0, import_agents.normalizeLanguage)(data.language_code ?? "en");
356
+ const timedWords = words.map(
357
+ (word) => (0, import_agents.createTimedString)({
358
+ text: word.text ?? "",
359
+ startTime: (word.start ?? 0) / 1e3 + this.startTimeOffset,
360
+ endTime: (word.end ?? 0) / 1e3 + this.startTimeOffset,
361
+ confidence: word.confidence ?? 0,
362
+ startTimeOffset: this.startTimeOffset
363
+ })
364
+ );
365
+ let startTime = 0;
366
+ let endTime = 0;
367
+ let confidence = 0;
368
+ if (timedWords.length > 0) {
369
+ const interimText = timedWords.map((w) => w.text).join(" ");
370
+ startTime = timedWords[0].startTime ?? 0;
371
+ endTime = timedWords[timedWords.length - 1].endTime ?? 0;
372
+ confidence = this.#averageConfidence(timedWords);
373
+ this.queue.put({
374
+ type: import_agents.stt.SpeechEventType.INTERIM_TRANSCRIPT,
375
+ alternatives: [
376
+ {
377
+ language,
378
+ text: interimText,
379
+ startTime,
380
+ endTime,
381
+ confidence,
382
+ words: timedWords
383
+ }
384
+ ]
385
+ });
386
+ }
387
+ if (utterance) {
388
+ if (this.#lastPreflightStartTime === 0) {
389
+ this.#lastPreflightStartTime = startTime;
390
+ }
391
+ const utteranceWords = timedWords.filter(
392
+ (w) => w.startTime !== void 0 && w.startTime >= this.#lastPreflightStartTime
393
+ );
394
+ const utteranceConfidence = this.#averageConfidence(utteranceWords);
395
+ this.queue.put({
396
+ type: import_agents.stt.SpeechEventType.PREFLIGHT_TRANSCRIPT,
397
+ alternatives: [
398
+ {
399
+ language,
400
+ text: utterance,
401
+ startTime: this.#lastPreflightStartTime,
402
+ endTime,
403
+ confidence: utteranceConfidence,
404
+ words: utteranceWords
405
+ }
406
+ ]
407
+ });
408
+ this.#lastPreflightStartTime = endTime;
409
+ }
410
+ const waitingForFormatted = this.#opts.formatTurns === true && !turnIsFormatted;
411
+ if (endOfTurn && !waitingForFormatted) {
412
+ this.queue.put({
413
+ type: import_agents.stt.SpeechEventType.FINAL_TRANSCRIPT,
414
+ alternatives: [
415
+ {
416
+ language,
417
+ text: transcript,
418
+ startTime,
419
+ endTime,
420
+ confidence,
421
+ words: timedWords
422
+ }
423
+ ]
424
+ });
425
+ this.queue.put({ type: import_agents.stt.SpeechEventType.END_OF_SPEECH });
426
+ if (this.#speechDurationInS > 0) {
427
+ this.queue.put({
428
+ type: import_agents.stt.SpeechEventType.RECOGNITION_USAGE,
429
+ // Propagate the AssemblyAI session id as the request id so metrics
430
+ // can be correlated back to a specific connection, mirroring how
431
+ // Deepgram surfaces its `request_id`.
432
+ requestId: this.#sessionId ?? void 0,
433
+ recognitionUsage: {
434
+ audioDuration: this.#speechDurationInS
435
+ }
436
+ });
437
+ this.#speechDurationInS = 0;
438
+ this.#lastPreflightStartTime = 0;
151
439
  }
152
440
  }
153
441
  }
154
442
  }
155
- const assemblyTranscriptToSpeechData = (transcript) => {
156
- return {
157
- language: "en-US",
158
- startTime: transcript.audio_start || 0,
159
- endTime: transcript.audio_end || 0,
160
- confidence: transcript.confidence || 1,
161
- text: transcript.text || ""
162
- };
163
- };
164
443
  // Annotate the CommonJS export names for ESM import in node:
165
444
  0 && (module.exports = {
166
445
  STT,