@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/README.md +6 -4
- package/dist/index.cjs +16 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/models.cjs +17 -0
- package/dist/models.cjs.map +1 -0
- package/dist/models.d.cts +3 -0
- package/dist/models.d.ts +3 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +1 -0
- package/dist/models.js.map +1 -0
- package/dist/stt.cjs +379 -100
- package/dist/stt.cjs.map +1 -1
- package/dist/stt.d.cts +58 -6
- package/dist/stt.d.ts +58 -6
- package/dist/stt.d.ts.map +1 -1
- package/dist/stt.js +390 -101
- package/dist/stt.js.map +1 -1
- package/dist/stt.test.cjs +11 -5
- package/dist/stt.test.cjs.map +1 -1
- package/dist/stt.test.js +12 -6
- package/dist/stt.test.js.map +1 -1
- package/package.json +10 -9
- package/src/index.ts +15 -1
- package/src/models.ts +14 -0
- package/src/stt.test.ts +13 -7
- package/src/stt.ts +517 -108
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText:
|
|
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
|
-
|
|
14
|
-
information on how
|
|
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
|
|
17
|
-
module.exports = __toCommonJS(
|
|
18
|
-
|
|
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.8",
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText:
|
|
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
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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.8",
|
|
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:
|
|
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":[]}
|
package/dist/models.cjs
ADDED
|
@@ -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":[]}
|
package/dist/models.d.ts
ADDED
|
@@ -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
|
|
26
|
+
var import_ws = require("ws");
|
|
27
27
|
const defaultSTTOptions = {
|
|
28
|
-
apiKey: process.env.
|
|
29
|
-
interimResults: true,
|
|
28
|
+
apiKey: process.env.ASSEMBLYAI_API_KEY,
|
|
30
29
|
sampleRate: 16e3,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
#
|
|
37
|
+
#streams = /* @__PURE__ */ new Set();
|
|
46
38
|
label = "assemblyai.STT";
|
|
47
|
-
|
|
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:
|
|
48
|
+
interimResults: true,
|
|
49
|
+
alignedTranscript: "word"
|
|
51
50
|
});
|
|
52
|
-
if (opts.
|
|
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
|
|
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
|
-
|
|
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("
|
|
74
|
+
throw new Error("Non-streaming recognize is not supported on AssemblyAI STT");
|
|
62
75
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 =
|
|
133
|
-
} else if (data.sampleRate === this.#opts.sampleRate) {
|
|
134
|
-
frames =
|
|
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(
|
|
255
|
+
throw new Error("sample rate or channel count of frame does not match");
|
|
137
256
|
}
|
|
138
|
-
for
|
|
139
|
-
|
|
257
|
+
for (const frame of frames) {
|
|
258
|
+
this.#speechDurationInS += frame.samplesPerChannel / frame.sampleRate;
|
|
259
|
+
ws.send(frame.data.buffer);
|
|
140
260
|
}
|
|
141
261
|
}
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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,
|