@livekit/agents-plugin-assemblyai 0.0.0 → 1.2.8

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/stt.js CHANGED
@@ -1,142 +1,431 @@
1
- import { AudioByteStream, log, stt } from "@livekit/agents";
2
- import { AssemblyAI } from "assemblyai";
1
+ import {
2
+ AudioByteStream,
3
+ Future,
4
+ Task,
5
+ createTimedString,
6
+ delay,
7
+ log,
8
+ normalizeLanguage,
9
+ stt,
10
+ waitForAbort
11
+ } from "@livekit/agents";
12
+ import { WebSocket } from "ws";
3
13
  const defaultSTTOptions = {
4
- apiKey: process.env.ASSEMBLY_AI_KEY,
5
- interimResults: true,
14
+ apiKey: process.env.ASSEMBLYAI_API_KEY,
6
15
  sampleRate: 16e3,
7
- keywords: [],
8
- // NOTE:
9
- // The default is 700ms from AssemblyAI.
10
- // We use a low default of 300ms here because we also use
11
- // the new end-of-utterance model from LiveKit to handle
12
- // turn detection in my agent. Which means that even though
13
- // this will quickly return a final transcript EVEN THOUGH
14
- // USER IS NOT DONE SPEAKING, the EOU model from LiveKit
15
- // DOES properly differentiate and doesn't interrupt (magically!)
16
- // Ref: https://blog.livekit.io/using-a-transformer-to-improve-end-of-turn-detection/
17
- endUtteranceSilenceThreshold: 200
16
+ bufferSizeMs: 50,
17
+ encoding: "pcm_s16le",
18
+ speechModel: "universal-streaming-english",
19
+ baseUrl: "wss://streaming.assemblyai.com"
18
20
  };
19
21
  class STT extends stt.STT {
20
22
  #opts;
21
- #logger = log();
23
+ #streams = /* @__PURE__ */ new Set();
22
24
  label = "assemblyai.STT";
23
- constructor(opts = defaultSTTOptions) {
25
+ get model() {
26
+ return this.#opts.speechModel;
27
+ }
28
+ get provider() {
29
+ return "AssemblyAI";
30
+ }
31
+ constructor(opts = {}) {
24
32
  super({
25
33
  streaming: true,
26
- interimResults: opts.interimResults ?? defaultSTTOptions.interimResults
34
+ interimResults: true,
35
+ alignedTranscript: "word"
27
36
  });
28
- if (opts.apiKey === void 0 && defaultSTTOptions.apiKey === void 0) {
37
+ if (opts.speechModel === "u3-pro") {
38
+ log().warn("'u3-pro' is deprecated, use 'u3-rt-pro' instead.");
39
+ opts.speechModel = "u3-rt-pro";
40
+ }
41
+ if (opts.prompt !== void 0 && opts.speechModel !== "u3-rt-pro") {
42
+ throw new Error("The 'prompt' parameter is only supported with the 'u3-rt-pro' model.");
43
+ }
44
+ const apiKey = opts.apiKey ?? defaultSTTOptions.apiKey;
45
+ if (!apiKey) {
29
46
  throw new Error(
30
- "AssemblyAI API key is required, whether as an argument or as $ASSEMBLY_AI_KEY"
47
+ "AssemblyAI API key is required. Pass one in via the `apiKey` parameter, or set it as the `ASSEMBLYAI_API_KEY` environment variable"
31
48
  );
32
49
  }
33
- this.#opts = { ...defaultSTTOptions, ...opts };
50
+ const minTurnSilence = opts.minTurnSilence ?? 100;
51
+ this.#opts = {
52
+ ...defaultSTTOptions,
53
+ ...opts,
54
+ apiKey,
55
+ minTurnSilence
56
+ };
34
57
  }
35
58
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
36
59
  async _recognize(_) {
37
- throw new Error("Recognize is not supported on AssemblyAI STT");
60
+ throw new Error("Non-streaming recognize is not supported on AssemblyAI STT");
38
61
  }
39
- stream() {
40
- return new SpeechStream(this, this.#opts);
62
+ updateOptions(opts) {
63
+ this.#opts = { ...this.#opts, ...opts };
64
+ for (const ref of this.#streams) {
65
+ const stream = ref.deref();
66
+ if (stream) {
67
+ stream.updateOptions(opts);
68
+ } else {
69
+ this.#streams.delete(ref);
70
+ }
71
+ }
72
+ }
73
+ stream(options) {
74
+ const stream = new SpeechStream(this, this.#opts, options == null ? void 0 : options.connOptions);
75
+ this.#streams.add(new WeakRef(stream));
76
+ return stream;
41
77
  }
42
78
  }
43
79
  class SpeechStream extends stt.SpeechStream {
80
+ static CLOSE_MSG = JSON.stringify({ type: "Terminate" });
44
81
  #opts;
45
82
  #logger = log();
46
- #speaking = false;
47
- #client;
48
- #transcriber;
83
+ #speechDurationInS = 0;
84
+ #lastPreflightStartTime = 0;
85
+ #pendingConfigMessages = [];
86
+ #configMessagePending = new Future();
87
+ #sessionId = null;
88
+ #expiresAt = null;
49
89
  label = "assemblyai.SpeechStream";
50
- constructor(stt2, opts) {
51
- super(stt2);
90
+ constructor(stt2, opts, connOptions) {
91
+ super(stt2, opts.sampleRate, connOptions);
52
92
  this.#opts = opts;
53
93
  this.closed = false;
54
- this.#client = new AssemblyAI({
55
- apiKey: this.#opts.apiKey || ""
56
- });
57
- this.#run();
58
94
  }
59
- async #run() {
60
- try {
61
- this.#transcriber = this.#client.realtime.transcriber({
62
- sampleRate: this.#opts.sampleRate,
63
- wordBoost: this.#opts.keywords.map((k) => k[0]),
64
- endUtteranceSilenceThreshold: this.#opts.endUtteranceSilenceThreshold
65
- });
66
- this.#transcriber.on("open", (data) => {
67
- this.#logger.child({ sessionId: data.sessionId, expiresAt: data.expiresAt }).debug(`AssemblyAI session opened`);
68
- });
69
- this.#transcriber.on("close", (code, reason) => {
70
- this.#logger.child({ code, reason }).debug(`AssemblyAI session closed`);
71
- if (!this.closed) {
72
- this.#run();
73
- }
74
- });
75
- this.#transcriber.on("error", (error) => {
76
- this.#logger.child({ error: error.message }).error(`AssemblyAI error`);
77
- });
78
- this.#transcriber.on("transcript", (transcript) => {
79
- if (this.closed) return;
80
- if (!transcript.text || transcript.text.trim() === "") {
81
- return;
82
- }
83
- if (!this.#speaking) {
84
- this.#speaking = true;
85
- this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });
86
- }
87
- if (transcript.message_type === "PartialTranscript") {
88
- this.queue.put({
89
- type: stt.SpeechEventType.INTERIM_TRANSCRIPT,
90
- alternatives: [assemblyTranscriptToSpeechData(transcript)]
91
- });
92
- } else if (transcript.message_type === "FinalTranscript") {
93
- this.queue.put({
94
- type: stt.SpeechEventType.FINAL_TRANSCRIPT,
95
- alternatives: [assemblyTranscriptToSpeechData(transcript)]
96
- });
95
+ /**
96
+ * The AssemblyAI session ID. Set when the WebSocket connection is established
97
+ * (before any speech events). Null until the connection completes.
98
+ * Share this with the AssemblyAI team when reporting issues.
99
+ */
100
+ get sessionId() {
101
+ return this.#sessionId;
102
+ }
103
+ /**
104
+ * Unix timestamp when the AssemblyAI session expires. Set alongside
105
+ * {@link sessionId} when the WebSocket connection is established.
106
+ */
107
+ get expiresAt() {
108
+ return this.#expiresAt;
109
+ }
110
+ updateOptions(opts) {
111
+ this.#opts = { ...this.#opts, ...opts };
112
+ const configMsg = { type: "UpdateConfiguration" };
113
+ if (opts.prompt !== void 0) configMsg.prompt = opts.prompt;
114
+ if (opts.keytermsPrompt !== void 0) configMsg.keyterms_prompt = opts.keytermsPrompt;
115
+ if (opts.maxTurnSilence !== void 0) configMsg.max_turn_silence = opts.maxTurnSilence;
116
+ if (opts.minTurnSilence !== void 0) configMsg.min_turn_silence = opts.minTurnSilence;
117
+ if (opts.endOfTurnConfidenceThreshold !== void 0) {
118
+ configMsg.end_of_turn_confidence_threshold = opts.endOfTurnConfidenceThreshold;
119
+ }
120
+ if (opts.vadThreshold !== void 0) configMsg.vad_threshold = opts.vadThreshold;
121
+ if (Object.keys(configMsg).length > 1) {
122
+ this.#pendingConfigMessages.push(configMsg);
123
+ if (!this.#configMessagePending.done) this.#configMessagePending.resolve();
124
+ }
125
+ }
126
+ /**
127
+ * Force-finalize the current turn immediately.
128
+ */
129
+ forceEndpoint() {
130
+ this.#pendingConfigMessages.push({ type: "ForceEndpoint" });
131
+ if (!this.#configMessagePending.done) this.#configMessagePending.resolve();
132
+ }
133
+ // Deepgram-style reconnect loop around a single websocket lifetime.
134
+ async run() {
135
+ const maxRetry = 32;
136
+ let retries = 0;
137
+ while (!this.input.closed && !this.closed) {
138
+ try {
139
+ const ws = await this.#connectWS();
140
+ await this.#runWS(ws);
141
+ retries = 0;
142
+ } catch (e) {
143
+ if (!this.closed && !this.input.closed) {
144
+ if (retries >= maxRetry) {
145
+ throw new Error(`failed to connect to AssemblyAI after ${retries} attempts: ${e}`);
146
+ }
147
+ const retryDelaySeconds = Math.min(retries * 5, 10);
148
+ retries++;
149
+ this.#logger.warn(
150
+ `failed to connect to AssemblyAI, retrying in ${retryDelaySeconds} seconds: ${e} (${retries}/${maxRetry})`
151
+ );
152
+ await delay(retryDelaySeconds * 1e3);
153
+ } else {
154
+ this.#logger.warn(
155
+ `AssemblyAI disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`
156
+ );
97
157
  }
158
+ }
159
+ }
160
+ this.closed = true;
161
+ }
162
+ async #connectWS() {
163
+ let minSilence = this.#opts.minTurnSilence;
164
+ let maxSilence = this.#opts.maxTurnSilence;
165
+ if (this.#opts.speechModel === "u3-rt-pro") {
166
+ if (minSilence === void 0) minSilence = 100;
167
+ if (maxSilence === void 0) maxSilence = minSilence;
168
+ }
169
+ const defaultLanguageDetection = this.#opts.speechModel.includes("multilingual") || this.#opts.speechModel === "u3-rt-pro";
170
+ const languageDetection = this.#opts.languageDetection ?? defaultLanguageDetection;
171
+ const liveConfig = {
172
+ sample_rate: this.#opts.sampleRate,
173
+ encoding: this.#opts.encoding,
174
+ speech_model: this.#opts.speechModel,
175
+ format_turns: this.#opts.formatTurns,
176
+ end_of_turn_confidence_threshold: this.#opts.endOfTurnConfidenceThreshold,
177
+ min_turn_silence: minSilence,
178
+ max_turn_silence: maxSilence,
179
+ keyterms_prompt: this.#opts.keytermsPrompt !== void 0 ? JSON.stringify(this.#opts.keytermsPrompt) : void 0,
180
+ language_detection: languageDetection,
181
+ prompt: this.#opts.prompt,
182
+ vad_threshold: this.#opts.vadThreshold,
183
+ speaker_labels: this.#opts.speakerLabels,
184
+ max_speakers: this.#opts.maxSpeakers,
185
+ domain: this.#opts.domain
186
+ };
187
+ const url = new URL(`${this.#opts.baseUrl}/v3/ws`);
188
+ for (const [key, value] of Object.entries(liveConfig)) {
189
+ if (value === void 0 || value === null) continue;
190
+ if (typeof value === "boolean") {
191
+ url.searchParams.append(key, value ? "true" : "false");
192
+ } else {
193
+ url.searchParams.append(key, String(value));
194
+ }
195
+ }
196
+ const ws = new WebSocket(url, {
197
+ headers: {
198
+ Authorization: this.#opts.apiKey,
199
+ "Content-Type": "application/json",
200
+ "User-Agent": "AssemblyAI/1.0 (integration=Livekit)"
201
+ }
202
+ });
203
+ await new Promise((resolve, reject) => {
204
+ ws.on("open", () => resolve());
205
+ ws.on("error", (error) => reject(error));
206
+ ws.on("close", (code) => reject(new Error(`WebSocket returned ${code}`)));
207
+ });
208
+ return ws;
209
+ }
210
+ async #runWS(ws) {
211
+ let closing = false;
212
+ const sessionController = new AbortController();
213
+ const wsMonitor = Task.from(async (controller) => {
214
+ const closed = new Promise((_, reject) => {
215
+ ws.once("close", (code, reason) => {
216
+ if (!closing) {
217
+ this.#logger.error(`WebSocket closed with code ${code}: ${reason}`);
218
+ reject(new Error("WebSocket closed"));
219
+ }
220
+ });
98
221
  });
99
- await this.#transcriber.connect();
100
- const sendTask = async () => {
101
- var _a;
102
- const samples100Ms = Math.floor(this.#opts.sampleRate / 10);
103
- const stream = new AudioByteStream(this.#opts.sampleRate, 1, samples100Ms);
104
- for await (const data of this.input) {
105
- if (this.closed) break;
222
+ await Promise.race([closed, waitForAbort(controller.signal)]);
223
+ });
224
+ const sendTask = async () => {
225
+ const samplesPerBuffer = Math.floor(this.#opts.sampleRate * this.#opts.bufferSizeMs / 1e3);
226
+ const audioStream = new AudioByteStream(this.#opts.sampleRate, 1, samplesPerBuffer);
227
+ const abortPromise = waitForAbort(this.abortSignal);
228
+ const sessionAbort = waitForAbort(sessionController.signal);
229
+ try {
230
+ while (!this.closed) {
231
+ const result = await Promise.race([this.input.next(), abortPromise, sessionAbort]);
232
+ if (result === void 0) return;
233
+ if (result.done) break;
234
+ const data = result.value;
106
235
  let frames;
107
236
  if (data === SpeechStream.FLUSH_SENTINEL) {
108
- frames = stream.flush();
109
- } else if (data.sampleRate === this.#opts.sampleRate) {
110
- frames = stream.write(data.data.buffer);
237
+ frames = audioStream.flush();
238
+ } else if (data.sampleRate === this.#opts.sampleRate && data.channels === 1) {
239
+ frames = audioStream.write(data.data.buffer);
111
240
  } else {
112
- throw new Error(`Sample rate or channel count of frame does not match`);
241
+ throw new Error("sample rate or channel count of frame does not match");
113
242
  }
114
- for await (const frame of frames) {
115
- (_a = this.#transcriber) == null ? void 0 : _a.sendAudio(new Uint8Array(frame.data.buffer));
243
+ for (const frame of frames) {
244
+ this.#speechDurationInS += frame.samplesPerChannel / frame.sampleRate;
245
+ ws.send(frame.data.buffer);
116
246
  }
117
247
  }
118
- if (this.#transcriber) {
119
- await this.#transcriber.close();
248
+ } finally {
249
+ closing = true;
250
+ try {
251
+ ws.send(SpeechStream.CLOSE_MSG);
252
+ } catch {
253
+ }
254
+ wsMonitor.cancel();
255
+ }
256
+ };
257
+ let messageHandler = null;
258
+ const listenTask = Task.from(async (controller) => {
259
+ const listenMessage = new Promise((resolve, reject) => {
260
+ messageHandler = (msg, isBinary) => {
261
+ if (isBinary) {
262
+ this.#logger.error("unexpected binary message from AssemblyAI");
263
+ return;
264
+ }
265
+ try {
266
+ const json = JSON.parse(msg.toString());
267
+ this.#processStreamEvent(json);
268
+ if (this.closed || closing) {
269
+ resolve();
270
+ }
271
+ } catch (err) {
272
+ this.#logger.error(`AssemblyAI: error processing message: ${msg}`);
273
+ reject(err);
274
+ }
275
+ };
276
+ ws.on("message", messageHandler);
277
+ });
278
+ await Promise.race([listenMessage, waitForAbort(controller.signal)]);
279
+ });
280
+ const configTask = Task.from(async (controller) => {
281
+ while (this.#pendingConfigMessages.length > 0) {
282
+ const msg = this.#pendingConfigMessages.shift();
283
+ ws.send(JSON.stringify(msg));
284
+ }
285
+ while (!controller.signal.aborted) {
286
+ await Promise.race([this.#configMessagePending.await, waitForAbort(controller.signal)]);
287
+ if (controller.signal.aborted) return;
288
+ this.#configMessagePending = new Future();
289
+ while (this.#pendingConfigMessages.length > 0) {
290
+ const msg = this.#pendingConfigMessages.shift();
291
+ ws.send(JSON.stringify(msg));
120
292
  }
121
- };
122
- await sendTask();
123
- } catch (error) {
124
- this.#logger.child({ error }).error(`Error in AssemblyAI STT`);
125
- if (!this.closed) {
126
- setTimeout(() => this.#run(), 5e3);
293
+ }
294
+ });
295
+ try {
296
+ await Promise.all([sendTask(), listenTask.result, wsMonitor.result]);
297
+ } finally {
298
+ closing = true;
299
+ sessionController.abort();
300
+ listenTask.cancel();
301
+ configTask.cancel();
302
+ if (messageHandler) ws.off("message", messageHandler);
303
+ try {
304
+ ws.close();
305
+ } catch {
306
+ }
307
+ }
308
+ }
309
+ #averageConfidence(words) {
310
+ if (words.length === 0) return 0;
311
+ return words.reduce((sum, w) => sum + (w.confidence ?? 0), 0) / words.length;
312
+ }
313
+ #processStreamEvent(data) {
314
+ const messageType = data.type;
315
+ if (messageType === "Begin") {
316
+ this.#sessionId = data.id ?? null;
317
+ this.#expiresAt = data.expires_at ?? null;
318
+ this.#logger.info(
319
+ `AssemblyAI session started id=${this.#sessionId} expires_at=${this.#expiresAt}`
320
+ );
321
+ return;
322
+ }
323
+ if (messageType === "SpeechStarted") {
324
+ this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });
325
+ return;
326
+ }
327
+ if (messageType === "Termination") {
328
+ this.#logger.debug(
329
+ `AssemblyAI session terminated audio_duration=${data.audio_duration_seconds}s session_duration=${data.session_duration_seconds}s`
330
+ );
331
+ return;
332
+ }
333
+ if (messageType !== "Turn") {
334
+ return;
335
+ }
336
+ const words = data.words ?? [];
337
+ const endOfTurn = Boolean(data.end_of_turn);
338
+ const turnIsFormatted = Boolean(data.turn_is_formatted);
339
+ const utterance = data.utterance ?? "";
340
+ const transcript = data.transcript ?? "";
341
+ const language = normalizeLanguage(data.language_code ?? "en");
342
+ const timedWords = words.map(
343
+ (word) => createTimedString({
344
+ text: word.text ?? "",
345
+ startTime: (word.start ?? 0) / 1e3 + this.startTimeOffset,
346
+ endTime: (word.end ?? 0) / 1e3 + this.startTimeOffset,
347
+ confidence: word.confidence ?? 0,
348
+ startTimeOffset: this.startTimeOffset
349
+ })
350
+ );
351
+ let startTime = 0;
352
+ let endTime = 0;
353
+ let confidence = 0;
354
+ if (timedWords.length > 0) {
355
+ const interimText = timedWords.map((w) => w.text).join(" ");
356
+ startTime = timedWords[0].startTime ?? 0;
357
+ endTime = timedWords[timedWords.length - 1].endTime ?? 0;
358
+ confidence = this.#averageConfidence(timedWords);
359
+ this.queue.put({
360
+ type: stt.SpeechEventType.INTERIM_TRANSCRIPT,
361
+ alternatives: [
362
+ {
363
+ language,
364
+ text: interimText,
365
+ startTime,
366
+ endTime,
367
+ confidence,
368
+ words: timedWords
369
+ }
370
+ ]
371
+ });
372
+ }
373
+ if (utterance) {
374
+ if (this.#lastPreflightStartTime === 0) {
375
+ this.#lastPreflightStartTime = startTime;
376
+ }
377
+ const utteranceWords = timedWords.filter(
378
+ (w) => w.startTime !== void 0 && w.startTime >= this.#lastPreflightStartTime
379
+ );
380
+ const utteranceConfidence = this.#averageConfidence(utteranceWords);
381
+ this.queue.put({
382
+ type: stt.SpeechEventType.PREFLIGHT_TRANSCRIPT,
383
+ alternatives: [
384
+ {
385
+ language,
386
+ text: utterance,
387
+ startTime: this.#lastPreflightStartTime,
388
+ endTime,
389
+ confidence: utteranceConfidence,
390
+ words: utteranceWords
391
+ }
392
+ ]
393
+ });
394
+ this.#lastPreflightStartTime = endTime;
395
+ }
396
+ const waitingForFormatted = this.#opts.formatTurns === true && !turnIsFormatted;
397
+ if (endOfTurn && !waitingForFormatted) {
398
+ this.queue.put({
399
+ type: stt.SpeechEventType.FINAL_TRANSCRIPT,
400
+ alternatives: [
401
+ {
402
+ language,
403
+ text: transcript,
404
+ startTime,
405
+ endTime,
406
+ confidence,
407
+ words: timedWords
408
+ }
409
+ ]
410
+ });
411
+ this.queue.put({ type: stt.SpeechEventType.END_OF_SPEECH });
412
+ if (this.#speechDurationInS > 0) {
413
+ this.queue.put({
414
+ type: stt.SpeechEventType.RECOGNITION_USAGE,
415
+ // Propagate the AssemblyAI session id as the request id so metrics
416
+ // can be correlated back to a specific connection, mirroring how
417
+ // Deepgram surfaces its `request_id`.
418
+ requestId: this.#sessionId ?? void 0,
419
+ recognitionUsage: {
420
+ audioDuration: this.#speechDurationInS
421
+ }
422
+ });
423
+ this.#speechDurationInS = 0;
424
+ this.#lastPreflightStartTime = 0;
127
425
  }
128
426
  }
129
427
  }
130
428
  }
131
- const assemblyTranscriptToSpeechData = (transcript) => {
132
- return {
133
- language: "en-US",
134
- startTime: transcript.audio_start || 0,
135
- endTime: transcript.audio_end || 0,
136
- confidence: transcript.confidence || 1,
137
- text: transcript.text || ""
138
- };
139
- };
140
429
  export {
141
430
  STT,
142
431
  SpeechStream
package/dist/stt.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { type AudioBuffer, AudioByteStream, log, stt } from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport { AssemblyAI } from 'assemblyai';\nimport type { RealtimeTranscriber, RealtimeTranscript } from 'assemblyai';\n\nexport interface STTOptions {\n apiKey?: string;\n interimResults: boolean;\n sampleRate: number;\n keywords: [string, number][];\n endUtteranceSilenceThreshold?: number;\n}\n\nconst defaultSTTOptions: STTOptions = {\n apiKey: process.env.ASSEMBLY_AI_KEY,\n interimResults: true,\n sampleRate: 16000,\n keywords: [],\n // NOTE:\n // The default is 700ms from AssemblyAI.\n // We use a low default of 300ms here because we also use\n // the new end-of-utterance model from LiveKit to handle\n // turn detection in my agent. Which means that even though\n // this will quickly return a final transcript EVEN THOUGH\n // USER IS NOT DONE SPEAKING, the EOU model from LiveKit\n // DOES properly differentiate and doesn't interrupt (magically!)\n // Ref: https://blog.livekit.io/using-a-transformer-to-improve-end-of-turn-detection/\n endUtteranceSilenceThreshold: 200,\n};\n\nexport class STT extends stt.STT {\n #opts: STTOptions;\n #logger = log();\n label = 'assemblyai.STT';\n\n constructor(opts: Partial<STTOptions> = defaultSTTOptions) {\n super({\n streaming: true,\n interimResults: opts.interimResults ?? defaultSTTOptions.interimResults,\n });\n if (opts.apiKey === undefined && defaultSTTOptions.apiKey === undefined) {\n throw new Error(\n 'AssemblyAI API key is required, whether as an argument or as $ASSEMBLY_AI_KEY',\n );\n }\n\n this.#opts = { ...defaultSTTOptions, ...opts };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async _recognize(_: AudioBuffer): Promise<stt.SpeechEvent> {\n throw new Error('Recognize is not supported on AssemblyAI STT');\n }\n\n stream(): stt.SpeechStream {\n return new SpeechStream(this, this.#opts);\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n #opts: STTOptions;\n #logger = log();\n #speaking = false;\n #client: AssemblyAI;\n #transcriber?: RealtimeTranscriber;\n label = 'assemblyai.SpeechStream';\n\n constructor(stt: STT, opts: STTOptions) {\n super(stt);\n this.#opts = opts;\n this.closed = false;\n this.#client = new AssemblyAI({\n apiKey: this.#opts.apiKey || '',\n });\n\n this.#run();\n }\n\n async #run() {\n try {\n this.#transcriber = this.#client.realtime.transcriber({\n sampleRate: this.#opts.sampleRate,\n wordBoost: this.#opts.keywords.map((k) => k[0]),\n endUtteranceSilenceThreshold: this.#opts.endUtteranceSilenceThreshold,\n });\n\n this.#transcriber.on('open', (data) => {\n this.#logger\n .child({ sessionId: data.sessionId, expiresAt: data.expiresAt })\n .debug(`AssemblyAI session opened`);\n });\n\n this.#transcriber.on('close', (code, reason) => {\n this.#logger.child({ code, reason }).debug(`AssemblyAI session closed`);\n if (!this.closed) {\n this.#run();\n }\n });\n\n this.#transcriber.on('error', (error) => {\n this.#logger.child({ error: error.message }).error(`AssemblyAI error`);\n });\n\n this.#transcriber.on('transcript', (transcript) => {\n if (this.closed) return;\n\n if (!transcript.text || transcript.text.trim() === '') {\n return;\n }\n\n if (!this.#speaking) {\n this.#speaking = true;\n this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });\n }\n\n if (transcript.message_type === 'PartialTranscript') {\n this.queue.put({\n type: stt.SpeechEventType.INTERIM_TRANSCRIPT,\n alternatives: [assemblyTranscriptToSpeechData(transcript)],\n });\n } else if (transcript.message_type === 'FinalTranscript') {\n this.queue.put({\n type: stt.SpeechEventType.FINAL_TRANSCRIPT,\n alternatives: [assemblyTranscriptToSpeechData(transcript)],\n });\n }\n });\n\n await this.#transcriber.connect();\n\n const sendTask = async () => {\n const samples100Ms = Math.floor(this.#opts.sampleRate / 10);\n const stream = new AudioByteStream(this.#opts.sampleRate, 1, samples100Ms);\n\n for await (const data of this.input) {\n if (this.closed) break;\n\n let frames: AudioFrame[];\n if (data === SpeechStream.FLUSH_SENTINEL) {\n frames = stream.flush();\n } else if (data.sampleRate === this.#opts.sampleRate) {\n frames = stream.write(data.data.buffer);\n } else {\n throw new Error(`Sample rate or channel count of frame does not match`);\n }\n\n for await (const frame of frames) {\n this.#transcriber?.sendAudio(new Uint8Array(frame.data.buffer));\n }\n }\n\n if (this.#transcriber) {\n await this.#transcriber.close();\n }\n };\n\n await sendTask();\n } catch (error: unknown) {\n this.#logger.child({ error }).error(`Error in AssemblyAI STT`);\n\n if (!this.closed) {\n setTimeout(() => this.#run(), 5000);\n }\n }\n }\n}\n\nconst assemblyTranscriptToSpeechData = (transcript: RealtimeTranscript): stt.SpeechData => {\n return {\n language: 'en-US',\n startTime: transcript.audio_start || 0,\n endTime: transcript.audio_end || 0,\n confidence: transcript.confidence || 1.0,\n text: transcript.text || '',\n };\n};\n"],"mappings":"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","names":["stt"]}
1
+ {"version":3,"sources":["../src/stt.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\n//\nimport {\n type APIConnectOptions,\n type AudioBuffer,\n AudioByteStream,\n Future,\n Task,\n createTimedString,\n delay,\n log,\n normalizeLanguage,\n stt,\n waitForAbort,\n} from '@livekit/agents';\nimport type { AudioFrame } from '@livekit/rtc-node';\nimport type { RawData } from 'ws';\nimport { WebSocket } from 'ws';\nimport type { STTEncoding, STTModels } from './models.js';\n\n// AssemblyAI Universal-Streaming (v3) message envelope. All fields are optional\n// since we narrow on `type` before reading anything else.\ninterface StreamEventMessage {\n type?: 'Begin' | 'SpeechStarted' | 'Turn' | 'Termination' | string;\n // Begin\n id?: string;\n expires_at?: number;\n // Turn\n transcript?: string;\n utterance?: string;\n end_of_turn?: boolean;\n end_of_turn_confidence?: number;\n turn_is_formatted?: boolean;\n language_code?: string;\n speaker_label?: string;\n words?: Array<{\n text?: string;\n start?: number;\n end?: number;\n confidence?: number;\n speaker?: string;\n }>;\n // Termination\n audio_duration_seconds?: number;\n session_duration_seconds?: number;\n}\n\nexport interface STTOptions {\n apiKey?: string;\n sampleRate: number;\n /**\n * How large each chunk of audio is before being sent to AssemblyAI, in\n * milliseconds. Corresponds to Python's `buffer_size_seconds` (seconds there,\n * ms here per this repo's time-unit convention).\n */\n bufferSizeMs: number;\n encoding: STTEncoding;\n speechModel: STTModels;\n languageDetection?: boolean;\n endOfTurnConfidenceThreshold?: number;\n /** Minimum silence (ms) before a confident end-of-turn is finalized. */\n minTurnSilence?: number;\n /** Maximum silence (ms) before end-of-turn is forced regardless of confidence. */\n maxTurnSilence?: number;\n formatTurns?: boolean;\n keytermsPrompt?: string[];\n /** Only supported with the `u3-rt-pro` model. */\n prompt?: string;\n vadThreshold?: number;\n /**\n * Enable speaker diarization. Note: AssemblyAI will return per-word speaker\n * labels, but the JS framework's `stt.SpeechData` type does not yet expose\n * a `speakerId` field (unlike the Python framework), so the labels are not\n * currently surfaced on emitted events. Setting this to `true` still has\n * effect server-side. Once the base `SpeechData` interface gains speaker\n * support, `#processStreamEvent` should forward `data.words[].speaker` too.\n */\n speakerLabels?: boolean;\n maxSpeakers?: number;\n domain?: string;\n baseUrl: string;\n}\n\nconst defaultSTTOptions: STTOptions = {\n apiKey: process.env.ASSEMBLYAI_API_KEY,\n sampleRate: 16000,\n bufferSizeMs: 50,\n encoding: 'pcm_s16le',\n speechModel: 'universal-streaming-english',\n baseUrl: 'wss://streaming.assemblyai.com',\n};\n\nexport class STT extends stt.STT {\n #opts: STTOptions;\n #streams = new Set<WeakRef<SpeechStream>>();\n label = 'assemblyai.STT';\n\n get model(): string {\n return this.#opts.speechModel;\n }\n\n get provider(): string {\n return 'AssemblyAI';\n }\n\n constructor(opts: Partial<STTOptions> = {}) {\n super({\n streaming: true,\n interimResults: true,\n alignedTranscript: 'word',\n });\n\n if (opts.speechModel === 'u3-pro') {\n log().warn(\"'u3-pro' is deprecated, use 'u3-rt-pro' instead.\");\n opts.speechModel = 'u3-rt-pro';\n }\n\n if (opts.prompt !== undefined && opts.speechModel !== 'u3-rt-pro') {\n throw new Error(\"The 'prompt' parameter is only supported with the 'u3-rt-pro' model.\");\n }\n\n const apiKey = opts.apiKey ?? defaultSTTOptions.apiKey;\n if (!apiKey) {\n throw new Error(\n 'AssemblyAI API key is required. Pass one in via the `apiKey` parameter, or set it as the `ASSEMBLYAI_API_KEY` environment variable',\n );\n }\n\n // Minimize latency; matches LK's end-of-turn detector well.\n const minTurnSilence = opts.minTurnSilence ?? 100;\n\n this.#opts = {\n ...defaultSTTOptions,\n ...opts,\n apiKey,\n minTurnSilence,\n };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async _recognize(_: AudioBuffer): Promise<stt.SpeechEvent> {\n throw new Error('Non-streaming recognize is not supported on AssemblyAI STT');\n }\n\n updateOptions(opts: Partial<STTOptions>) {\n this.#opts = { ...this.#opts, ...opts };\n for (const ref of this.#streams) {\n const stream = ref.deref();\n if (stream) {\n stream.updateOptions(opts);\n } else {\n this.#streams.delete(ref);\n }\n }\n }\n\n stream(options?: { connOptions?: APIConnectOptions }): SpeechStream {\n const stream = new SpeechStream(this, this.#opts, options?.connOptions);\n this.#streams.add(new WeakRef(stream));\n return stream;\n }\n}\n\nexport class SpeechStream extends stt.SpeechStream {\n static readonly CLOSE_MSG = JSON.stringify({ type: 'Terminate' });\n\n #opts: STTOptions;\n #logger = log();\n #speechDurationInS = 0;\n #lastPreflightStartTime = 0;\n #pendingConfigMessages: Record<string, unknown>[] = [];\n #configMessagePending = new Future();\n #sessionId: string | null = null;\n #expiresAt: number | null = null;\n label = 'assemblyai.SpeechStream';\n\n constructor(stt: STT, opts: STTOptions, connOptions?: APIConnectOptions) {\n super(stt, opts.sampleRate, connOptions);\n this.#opts = opts;\n this.closed = false;\n }\n\n /**\n * The AssemblyAI session ID. Set when the WebSocket connection is established\n * (before any speech events). Null until the connection completes.\n * Share this with the AssemblyAI team when reporting issues.\n */\n get sessionId(): string | null {\n return this.#sessionId;\n }\n\n /**\n * Unix timestamp when the AssemblyAI session expires. Set alongside\n * {@link sessionId} when the WebSocket connection is established.\n */\n get expiresAt(): number | null {\n return this.#expiresAt;\n }\n\n updateOptions(opts: Partial<STTOptions>) {\n this.#opts = { ...this.#opts, ...opts };\n\n const configMsg: Record<string, unknown> = { type: 'UpdateConfiguration' };\n if (opts.prompt !== undefined) configMsg.prompt = opts.prompt;\n if (opts.keytermsPrompt !== undefined) configMsg.keyterms_prompt = opts.keytermsPrompt;\n if (opts.maxTurnSilence !== undefined) configMsg.max_turn_silence = opts.maxTurnSilence;\n if (opts.minTurnSilence !== undefined) configMsg.min_turn_silence = opts.minTurnSilence;\n if (opts.endOfTurnConfidenceThreshold !== undefined) {\n configMsg.end_of_turn_confidence_threshold = opts.endOfTurnConfidenceThreshold;\n }\n if (opts.vadThreshold !== undefined) configMsg.vad_threshold = opts.vadThreshold;\n\n // Only send if any actual fields (besides `type`) were specified.\n if (Object.keys(configMsg).length > 1) {\n this.#pendingConfigMessages.push(configMsg);\n if (!this.#configMessagePending.done) this.#configMessagePending.resolve();\n }\n }\n\n /**\n * Force-finalize the current turn immediately.\n */\n forceEndpoint() {\n this.#pendingConfigMessages.push({ type: 'ForceEndpoint' });\n if (!this.#configMessagePending.done) this.#configMessagePending.resolve();\n }\n\n // Deepgram-style reconnect loop around a single websocket lifetime.\n protected async run() {\n const maxRetry = 32;\n let retries = 0;\n\n while (!this.input.closed && !this.closed) {\n try {\n const ws = await this.#connectWS();\n await this.#runWS(ws);\n retries = 0;\n } catch (e) {\n if (!this.closed && !this.input.closed) {\n if (retries >= maxRetry) {\n throw new Error(`failed to connect to AssemblyAI after ${retries} attempts: ${e}`);\n }\n\n const retryDelaySeconds = Math.min(retries * 5, 10);\n retries++;\n\n this.#logger.warn(\n `failed to connect to AssemblyAI, retrying in ${retryDelaySeconds} seconds: ${e} (${retries}/${maxRetry})`,\n );\n await delay(retryDelaySeconds * 1000);\n } else {\n this.#logger.warn(\n `AssemblyAI disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`,\n );\n }\n }\n }\n\n this.closed = true;\n }\n\n async #connectWS(): Promise<WebSocket> {\n // u3-rt-pro has different silence defaults — if unset, both min and max default to 100ms.\n let minSilence = this.#opts.minTurnSilence;\n let maxSilence = this.#opts.maxTurnSilence;\n if (this.#opts.speechModel === 'u3-rt-pro') {\n if (minSilence === undefined) minSilence = 100;\n if (maxSilence === undefined) maxSilence = minSilence;\n }\n\n // Default language_detection to true for multilingual / u3-rt-pro models, false otherwise.\n const defaultLanguageDetection =\n this.#opts.speechModel.includes('multilingual') || this.#opts.speechModel === 'u3-rt-pro';\n const languageDetection = this.#opts.languageDetection ?? defaultLanguageDetection;\n\n const liveConfig: Record<string, unknown> = {\n sample_rate: this.#opts.sampleRate,\n encoding: this.#opts.encoding,\n speech_model: this.#opts.speechModel,\n format_turns: this.#opts.formatTurns,\n end_of_turn_confidence_threshold: this.#opts.endOfTurnConfidenceThreshold,\n min_turn_silence: minSilence,\n max_turn_silence: maxSilence,\n keyterms_prompt:\n this.#opts.keytermsPrompt !== undefined\n ? JSON.stringify(this.#opts.keytermsPrompt)\n : undefined,\n language_detection: languageDetection,\n prompt: this.#opts.prompt,\n vad_threshold: this.#opts.vadThreshold,\n speaker_labels: this.#opts.speakerLabels,\n max_speakers: this.#opts.maxSpeakers,\n domain: this.#opts.domain,\n };\n\n const url = new URL(`${this.#opts.baseUrl}/v3/ws`);\n // Python serializes booleans as the strings \"true\"/\"false\", so we mirror that.\n for (const [key, value] of Object.entries(liveConfig)) {\n if (value === undefined || value === null) continue;\n if (typeof value === 'boolean') {\n url.searchParams.append(key, value ? 'true' : 'false');\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n\n const ws = new WebSocket(url, {\n headers: {\n Authorization: this.#opts.apiKey!,\n 'Content-Type': 'application/json',\n 'User-Agent': 'AssemblyAI/1.0 (integration=Livekit)',\n },\n });\n\n await new Promise<void>((resolve, reject) => {\n ws.on('open', () => resolve());\n ws.on('error', (error) => reject(error));\n ws.on('close', (code) => reject(new Error(`WebSocket returned ${code}`)));\n });\n\n return ws;\n }\n\n async #runWS(ws: WebSocket) {\n let closing = false;\n const sessionController = new AbortController();\n\n // gets cancelled also when sendTask is complete\n const wsMonitor = Task.from(async (controller) => {\n const closed = new Promise<void>((_, reject) => {\n ws.once('close', (code, reason) => {\n if (!closing) {\n this.#logger.error(`WebSocket closed with code ${code}: ${reason}`);\n reject(new Error('WebSocket closed'));\n }\n });\n });\n\n await Promise.race([closed, waitForAbort(controller.signal)]);\n });\n\n const sendTask = async () => {\n const samplesPerBuffer = Math.floor((this.#opts.sampleRate * this.#opts.bufferSizeMs) / 1000);\n const audioStream = new AudioByteStream(this.#opts.sampleRate, 1, samplesPerBuffer);\n\n const abortPromise = waitForAbort(this.abortSignal);\n const sessionAbort = waitForAbort(sessionController.signal);\n\n try {\n while (!this.closed) {\n const result = await Promise.race([this.input.next(), abortPromise, sessionAbort]);\n\n if (result === undefined) return; // aborted\n if (result.done) break;\n\n const data = result.value;\n\n let frames: AudioFrame[];\n if (data === SpeechStream.FLUSH_SENTINEL) {\n frames = audioStream.flush();\n } else if (data.sampleRate === this.#opts.sampleRate && data.channels === 1) {\n // AssemblyAI expects mono PCM. The base SpeechStream only resamples\n // sample rate, so reject any frame that is not already downmixed.\n frames = audioStream.write(data.data.buffer as ArrayBuffer);\n } else {\n throw new Error('sample rate or channel count of frame does not match');\n }\n\n for (const frame of frames) {\n this.#speechDurationInS += frame.samplesPerChannel / frame.sampleRate;\n ws.send(frame.data.buffer);\n }\n }\n } finally {\n closing = true;\n try {\n ws.send(SpeechStream.CLOSE_MSG);\n } catch {\n // ignore — socket may already be closing\n }\n wsMonitor.cancel();\n }\n };\n\n let messageHandler: ((msg: RawData, isBinary: boolean) => void) | null = null;\n const listenTask = Task.from(async (controller) => {\n const listenMessage = new Promise<void>((resolve, reject) => {\n messageHandler = (msg, isBinary) => {\n if (isBinary) {\n this.#logger.error('unexpected binary message from AssemblyAI');\n return;\n }\n try {\n const json = JSON.parse(msg.toString()) as StreamEventMessage;\n this.#processStreamEvent(json);\n if (this.closed || closing) {\n resolve();\n }\n } catch (err) {\n this.#logger.error(`AssemblyAI: error processing message: ${msg}`);\n reject(err);\n }\n };\n ws.on('message', messageHandler);\n });\n\n await Promise.race([listenMessage, waitForAbort(controller.signal)]);\n });\n\n const configTask = Task.from(async (controller) => {\n // Drain any messages queued while the socket was reconnecting.\n while (this.#pendingConfigMessages.length > 0) {\n const msg = this.#pendingConfigMessages.shift()!;\n ws.send(JSON.stringify(msg));\n }\n\n while (!controller.signal.aborted) {\n await Promise.race([this.#configMessagePending.await, waitForAbort(controller.signal)]);\n if (controller.signal.aborted) return;\n\n this.#configMessagePending = new Future();\n while (this.#pendingConfigMessages.length > 0) {\n const msg = this.#pendingConfigMessages.shift()!;\n ws.send(JSON.stringify(msg));\n }\n }\n });\n\n try {\n await Promise.all([sendTask(), listenTask.result, wsMonitor.result]);\n } finally {\n closing = true;\n sessionController.abort();\n listenTask.cancel();\n configTask.cancel();\n if (messageHandler) ws.off('message', messageHandler);\n try {\n ws.close();\n } catch {\n // ignore\n }\n }\n }\n\n #averageConfidence(words: Array<{ confidence?: number }>): number {\n if (words.length === 0) return 0;\n return words.reduce((sum, w) => sum + (w.confidence ?? 0), 0) / words.length;\n }\n\n #processStreamEvent(data: StreamEventMessage) {\n const messageType = data.type;\n\n if (messageType === 'Begin') {\n this.#sessionId = data.id ?? null;\n this.#expiresAt = data.expires_at ?? null;\n this.#logger.info(\n `AssemblyAI session started id=${this.#sessionId} expires_at=${this.#expiresAt}`,\n );\n return;\n }\n\n if (messageType === 'SpeechStarted') {\n this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH });\n return;\n }\n\n if (messageType === 'Termination') {\n this.#logger.debug(\n `AssemblyAI session terminated audio_duration=${data.audio_duration_seconds}s session_duration=${data.session_duration_seconds}s`,\n );\n return;\n }\n\n if (messageType !== 'Turn') {\n return;\n }\n\n const words = data.words ?? [];\n const endOfTurn = Boolean(data.end_of_turn);\n const turnIsFormatted = Boolean(data.turn_is_formatted);\n const utterance = data.utterance ?? '';\n const transcript = data.transcript ?? '';\n const language = normalizeLanguage(data.language_code ?? 'en');\n\n // Word timestamps are in milliseconds:\n // https://www.assemblyai.com/docs/api-reference/streaming-api/streaming-api#receive.receiveTurn.words\n const timedWords = words.map((word) =>\n createTimedString({\n text: word.text ?? '',\n startTime: (word.start ?? 0) / 1000 + this.startTimeOffset,\n endTime: (word.end ?? 0) / 1000 + this.startTimeOffset,\n confidence: word.confidence ?? 0,\n startTimeOffset: this.startTimeOffset,\n }),\n );\n\n let startTime = 0;\n let endTime = 0;\n let confidence = 0;\n\n // `words` are cumulative for the turn — emit as an interim transcript.\n if (timedWords.length > 0) {\n const interimText = timedWords.map((w) => w.text).join(' ');\n startTime = timedWords[0]!.startTime ?? 0;\n endTime = timedWords[timedWords.length - 1]!.endTime ?? 0;\n confidence = this.#averageConfidence(timedWords);\n\n this.queue.put({\n type: stt.SpeechEventType.INTERIM_TRANSCRIPT,\n alternatives: [\n {\n language,\n text: interimText,\n startTime,\n endTime,\n confidence,\n words: timedWords,\n },\n ],\n });\n }\n\n // `utterance` is chunk-based (not cumulative) — emit as a preflight transcript\n // covering only the words since the last preflight.\n if (utterance) {\n if (this.#lastPreflightStartTime === 0) {\n this.#lastPreflightStartTime = startTime;\n }\n\n const utteranceWords = timedWords.filter(\n (w) => w.startTime !== undefined && w.startTime >= this.#lastPreflightStartTime,\n );\n const utteranceConfidence = this.#averageConfidence(utteranceWords);\n\n this.queue.put({\n type: stt.SpeechEventType.PREFLIGHT_TRANSCRIPT,\n alternatives: [\n {\n language,\n text: utterance,\n startTime: this.#lastPreflightStartTime,\n endTime,\n confidence: utteranceConfidence,\n words: utteranceWords,\n },\n ],\n });\n this.#lastPreflightStartTime = endTime;\n }\n\n // End-of-turn: emit FINAL_TRANSCRIPT + END_OF_SPEECH.\n // If the user asked for formatted turns, wait for a formatted final.\n const waitingForFormatted = this.#opts.formatTurns === true && !turnIsFormatted;\n if (endOfTurn && !waitingForFormatted) {\n this.queue.put({\n type: stt.SpeechEventType.FINAL_TRANSCRIPT,\n alternatives: [\n {\n language,\n text: transcript,\n startTime,\n endTime,\n confidence,\n words: timedWords,\n },\n ],\n });\n\n this.queue.put({ type: stt.SpeechEventType.END_OF_SPEECH });\n if (this.#speechDurationInS > 0) {\n this.queue.put({\n type: stt.SpeechEventType.RECOGNITION_USAGE,\n // Propagate the AssemblyAI session id as the request id so metrics\n // can be correlated back to a specific connection, mirroring how\n // Deepgram surfaces its `request_id`.\n requestId: this.#sessionId ?? undefined,\n recognitionUsage: {\n audioDuration: this.#speechDurationInS,\n },\n });\n this.#speechDurationInS = 0;\n this.#lastPreflightStartTime = 0;\n }\n }\n }\n}\n"],"mappings":"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","names":["stt"]}
package/dist/stt.test.cjs CHANGED
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
- var import_agents = require("@livekit/agents");
3
2
  var import_agents_plugin_silero = require("@livekit/agents-plugin-silero");
4
3
  var import_agents_plugins_test = require("@livekit/agents-plugins-test");
5
4
  var import_vitest = require("vitest");
6
5
  var import_stt = require("./stt.cjs");
7
- (0, import_vitest.describe)("AssemblyAI", async () => {
8
- (0, import_agents.initializeLogger)({ pretty: false });
9
- await (0, import_agents_plugins_test.stt)(new import_stt.STT(), await import_agents_plugin_silero.VAD.load(), { nonStreaming: false });
10
- });
6
+ const hasAssemblyAIApiKey = Boolean(process.env.ASSEMBLYAI_API_KEY);
7
+ if (hasAssemblyAIApiKey) {
8
+ (0, import_vitest.describe)("AssemblyAI", async () => {
9
+ await (0, import_agents_plugins_test.stt)(new import_stt.STT(), await import_agents_plugin_silero.VAD.load(), { nonStreaming: false });
10
+ });
11
+ } else {
12
+ (0, import_vitest.describe)("AssemblyAI", () => {
13
+ import_vitest.it.skip("requires ASSEMBLYAI_API_KEY", () => {
14
+ });
15
+ });
16
+ }
11
17
  //# sourceMappingURL=stt.test.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stt.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { initializeLogger } from '@livekit/agents';\nimport { VAD } from '@livekit/agents-plugin-silero';\nimport { stt } from '@livekit/agents-plugins-test';\nimport { describe } from 'vitest';\nimport { STT } from './stt.js';\n\ndescribe('AssemblyAI', async () => {\n initializeLogger({ pretty: false });\n await stt(new STT(), await VAD.load(), { nonStreaming: false });\n});\n"],"mappings":";AAGA,oBAAiC;AACjC,kCAAoB;AACpB,iCAAoB;AACpB,oBAAyB;AACzB,iBAAoB;AAAA,IAEpB,wBAAS,cAAc,YAAY;AACjC,sCAAiB,EAAE,QAAQ,MAAM,CAAC;AAClC,YAAM,gCAAI,IAAI,eAAI,GAAG,MAAM,gCAAI,KAAK,GAAG,EAAE,cAAc,MAAM,CAAC;AAChE,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/stt.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { VAD } from '@livekit/agents-plugin-silero';\nimport { stt } from '@livekit/agents-plugins-test';\nimport { describe, it } from 'vitest';\nimport { STT } from './stt.js';\n\nconst hasAssemblyAIApiKey = Boolean(process.env.ASSEMBLYAI_API_KEY);\n\nif (hasAssemblyAIApiKey) {\n describe('AssemblyAI', async () => {\n await stt(new STT(), await VAD.load(), { nonStreaming: false });\n });\n} else {\n describe('AssemblyAI', () => {\n it.skip('requires ASSEMBLYAI_API_KEY', () => {});\n });\n}\n"],"mappings":";AAGA,kCAAoB;AACpB,iCAAoB;AACpB,oBAA6B;AAC7B,iBAAoB;AAEpB,MAAM,sBAAsB,QAAQ,QAAQ,IAAI,kBAAkB;AAElE,IAAI,qBAAqB;AACvB,8BAAS,cAAc,YAAY;AACjC,cAAM,gCAAI,IAAI,eAAI,GAAG,MAAM,gCAAI,KAAK,GAAG,EAAE,cAAc,MAAM,CAAC;AAAA,EAChE,CAAC;AACH,OAAO;AACL,8BAAS,cAAc,MAAM;AAC3B,qBAAG,KAAK,+BAA+B,MAAM;AAAA,IAAC,CAAC;AAAA,EACjD,CAAC;AACH;","names":[]}