@livekit/agents-plugin-azure 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +18 -0
- package/dist/index.cjs +34 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/stt.cjs +465 -0
- package/dist/stt.cjs.map +1 -0
- package/dist/stt.d.cts +106 -0
- package/dist/stt.d.ts +106 -0
- package/dist/stt.d.ts.map +1 -0
- package/dist/stt.js +435 -0
- package/dist/stt.js.map +1 -0
- package/dist/stt.test.cjs +209 -0
- package/dist/stt.test.cjs.map +1 -0
- package/dist/stt.test.d.cts +2 -0
- package/dist/stt.test.d.ts +2 -0
- package/dist/stt.test.d.ts.map +1 -0
- package/dist/stt.test.js +208 -0
- package/dist/stt.test.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +18 -0
- package/src/stt.test.ts +245 -0
- package/src/stt.ts +585 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_agents = require("@livekit/agents");
|
|
3
|
+
var import_rtc_node = require("@livekit/rtc-node");
|
|
4
|
+
var import_vitest = require("vitest");
|
|
5
|
+
var import_stt = require("./stt.cjs");
|
|
6
|
+
const azureHarness = import_vitest.vi.hoisted(() => ({
|
|
7
|
+
activeReaders: 0,
|
|
8
|
+
cancellationErrors: 0,
|
|
9
|
+
deadStreamWrites: 0,
|
|
10
|
+
maxActiveReaders: 0,
|
|
11
|
+
recognizers: [],
|
|
12
|
+
streams: []
|
|
13
|
+
}));
|
|
14
|
+
import_vitest.vi.mock("microsoft-cognitiveservices-speech-sdk", async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal();
|
|
16
|
+
class FakePushStream {
|
|
17
|
+
closed = false;
|
|
18
|
+
frames = [];
|
|
19
|
+
write(buffer) {
|
|
20
|
+
if (this.closed) {
|
|
21
|
+
azureHarness.deadStreamWrites += 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.frames.push(new Int16Array(buffer)[0] ?? 0);
|
|
25
|
+
if (azureHarness.streams[0] === this && this.frames.length === 1) {
|
|
26
|
+
queueMicrotask(() => {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
azureHarness.cancellationErrors += 1;
|
|
29
|
+
(_b = (_a = azureHarness.recognizers[0]) == null ? void 0 : _a.canceled) == null ? void 0 : _b.call(_a, void 0, {
|
|
30
|
+
reason: actual.CancellationReason.Error,
|
|
31
|
+
errorCode: actual.CancellationErrorCode.ServiceTimeout,
|
|
32
|
+
errorDetails: "timeout"
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
close() {
|
|
38
|
+
this.closed = true;
|
|
39
|
+
const index = azureHarness.streams.indexOf(this);
|
|
40
|
+
queueMicrotask(() => {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
(_b = (_a = azureHarness.recognizers[index]) == null ? void 0 : _a.sessionStopped) == null ? void 0 : _b.call(_a, void 0, {});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
class FakeRecognizer {
|
|
47
|
+
recognizing;
|
|
48
|
+
recognized;
|
|
49
|
+
speechStartDetected;
|
|
50
|
+
speechEndDetected;
|
|
51
|
+
sessionStarted;
|
|
52
|
+
sessionStopped;
|
|
53
|
+
canceled;
|
|
54
|
+
constructor() {
|
|
55
|
+
azureHarness.recognizers.push(this);
|
|
56
|
+
}
|
|
57
|
+
startContinuousRecognitionAsync(resolve) {
|
|
58
|
+
resolve();
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
(_a = this.sessionStarted) == null ? void 0 : _a.call(this, void 0, {});
|
|
62
|
+
(_b = this.speechStartDetected) == null ? void 0 : _b.call(this, void 0, {});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
stopContinuousRecognitionAsync(resolve) {
|
|
66
|
+
resolve();
|
|
67
|
+
}
|
|
68
|
+
close() {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...actual,
|
|
73
|
+
AudioConfig: {
|
|
74
|
+
...actual.AudioConfig,
|
|
75
|
+
fromStreamInput: () => ({})
|
|
76
|
+
},
|
|
77
|
+
AudioInputStream: {
|
|
78
|
+
...actual.AudioInputStream,
|
|
79
|
+
createPushStream: () => {
|
|
80
|
+
const stream = new FakePushStream();
|
|
81
|
+
azureHarness.streams.push(stream);
|
|
82
|
+
return stream;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
SpeechRecognizer: FakeRecognizer
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
function canceledEvent(reason, errorCode, errorDetails = "") {
|
|
89
|
+
return { reason, errorCode, errorDetails };
|
|
90
|
+
}
|
|
91
|
+
(0, import_vitest.describe)("Azure STT cancellation handling", () => {
|
|
92
|
+
(0, import_vitest.beforeEach)(() => {
|
|
93
|
+
azureHarness.activeReaders = 0;
|
|
94
|
+
azureHarness.cancellationErrors = 0;
|
|
95
|
+
azureHarness.deadStreamWrites = 0;
|
|
96
|
+
azureHarness.maxActiveReaders = 0;
|
|
97
|
+
azureHarness.recognizers.length = 0;
|
|
98
|
+
azureHarness.streams.length = 0;
|
|
99
|
+
});
|
|
100
|
+
(0, import_vitest.it)("unblocks run on canceled error", () => {
|
|
101
|
+
const stream = import_stt.SpeechStream.prototype;
|
|
102
|
+
const testStream = Object.create(stream);
|
|
103
|
+
testStream._sessionStoppedEvent = {
|
|
104
|
+
isSet: false,
|
|
105
|
+
set() {
|
|
106
|
+
this.isSet = true;
|
|
107
|
+
},
|
|
108
|
+
clear() {
|
|
109
|
+
this.isSet = false;
|
|
110
|
+
},
|
|
111
|
+
wait: () => Promise.resolve()
|
|
112
|
+
};
|
|
113
|
+
testStream._cancellationError = null;
|
|
114
|
+
const event = canceledEvent(
|
|
115
|
+
import_stt.speechsdk.CancellationReason.Error,
|
|
116
|
+
import_stt.speechsdk.CancellationErrorCode.ServiceTimeout,
|
|
117
|
+
"timeout"
|
|
118
|
+
);
|
|
119
|
+
testStream._onCanceled(event);
|
|
120
|
+
(0, import_vitest.expect)(testStream._sessionStoppedEvent.isSet).toBe(true);
|
|
121
|
+
(0, import_vitest.expect)(testStream._cancellationError).toBe(event);
|
|
122
|
+
});
|
|
123
|
+
(0, import_vitest.it)("ignores cancellations without error", () => {
|
|
124
|
+
const stream = import_stt.SpeechStream.prototype;
|
|
125
|
+
const testStream = Object.create(stream);
|
|
126
|
+
testStream._sessionStoppedEvent = {
|
|
127
|
+
isSet: false,
|
|
128
|
+
set() {
|
|
129
|
+
this.isSet = true;
|
|
130
|
+
},
|
|
131
|
+
clear() {
|
|
132
|
+
this.isSet = false;
|
|
133
|
+
},
|
|
134
|
+
wait: () => Promise.resolve()
|
|
135
|
+
};
|
|
136
|
+
testStream._cancellationError = null;
|
|
137
|
+
testStream._onCanceled(canceledEvent(import_stt.speechsdk.CancellationReason.EndOfStream));
|
|
138
|
+
(0, import_vitest.expect)(testStream._sessionStoppedEvent.isSet).toBe(false);
|
|
139
|
+
(0, import_vitest.expect)(testStream._cancellationError).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
(0, import_vitest.it)("replaces a canceled recognizer without leaving its input consumer alive", async () => {
|
|
142
|
+
var _a;
|
|
143
|
+
const stt2 = new import_stt.STT({ speechHost: "wss://azure.test" });
|
|
144
|
+
const stream = stt2.stream({
|
|
145
|
+
connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1e3 }
|
|
146
|
+
});
|
|
147
|
+
const internal = stream;
|
|
148
|
+
const originalNext = internal.input.next.bind(internal.input);
|
|
149
|
+
internal.input.next = async (options = {}) => {
|
|
150
|
+
azureHarness.activeReaders += 1;
|
|
151
|
+
azureHarness.maxActiveReaders = Math.max(
|
|
152
|
+
azureHarness.maxActiveReaders,
|
|
153
|
+
azureHarness.activeReaders
|
|
154
|
+
);
|
|
155
|
+
try {
|
|
156
|
+
return await originalNext(options);
|
|
157
|
+
} finally {
|
|
158
|
+
azureHarness.activeReaders -= 1;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
stream.pushFrame(frame(1));
|
|
162
|
+
await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(azureHarness.recognizers).toHaveLength(2));
|
|
163
|
+
stream.pushFrame(frame(2));
|
|
164
|
+
stream.pushFrame(frame(3));
|
|
165
|
+
await import_vitest.vi.waitFor(() => {
|
|
166
|
+
var _a2;
|
|
167
|
+
return (0, import_vitest.expect)((_a2 = azureHarness.streams[1]) == null ? void 0 : _a2.frames).toEqual([2, 3]);
|
|
168
|
+
});
|
|
169
|
+
(0, import_vitest.expect)(azureHarness.cancellationErrors).toBe(1);
|
|
170
|
+
(0, import_vitest.expect)(azureHarness.maxActiveReaders).toBe(1);
|
|
171
|
+
(0, import_vitest.expect)((_a = azureHarness.streams[0]) == null ? void 0 : _a.closed).toBe(true);
|
|
172
|
+
(0, import_vitest.expect)(azureHarness.deadStreamWrites).toBe(0);
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
174
|
+
(0, import_vitest.expect)(azureHarness.recognizers).toHaveLength(2);
|
|
175
|
+
stream.close();
|
|
176
|
+
});
|
|
177
|
+
(0, import_vitest.it)("emits speech start after replacing a canceled recognizer mid-speech", async () => {
|
|
178
|
+
const azureStt = new import_stt.STT({ speechHost: "wss://azure.test" });
|
|
179
|
+
const stream = azureStt.stream({
|
|
180
|
+
connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1e3 }
|
|
181
|
+
});
|
|
182
|
+
const startEvents = [];
|
|
183
|
+
const collectEvents = (async () => {
|
|
184
|
+
for await (const event of stream) {
|
|
185
|
+
if (event.type === import_agents.stt.SpeechEventType.START_OF_SPEECH) {
|
|
186
|
+
startEvents.push(event.type);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
try {
|
|
191
|
+
await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(startEvents).toEqual([import_agents.stt.SpeechEventType.START_OF_SPEECH]));
|
|
192
|
+
stream.pushFrame(frame(1));
|
|
193
|
+
await import_vitest.vi.waitFor(() => (0, import_vitest.expect)(azureHarness.recognizers).toHaveLength(2));
|
|
194
|
+
await import_vitest.vi.waitFor(
|
|
195
|
+
() => (0, import_vitest.expect)(startEvents).toEqual([
|
|
196
|
+
import_agents.stt.SpeechEventType.START_OF_SPEECH,
|
|
197
|
+
import_agents.stt.SpeechEventType.START_OF_SPEECH
|
|
198
|
+
])
|
|
199
|
+
);
|
|
200
|
+
} finally {
|
|
201
|
+
stream.close();
|
|
202
|
+
await collectEvents;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
function frame(value) {
|
|
207
|
+
return new import_rtc_node.AudioFrame(new Int16Array([value]), 16e3, 1, 1);
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=stt.test.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stt.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { stt } from '@livekit/agents';\nimport { AudioFrame } from '@livekit/rtc-node';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { STT, SpeechStream, speechsdk } from './stt.js';\n\nconst azureHarness = vi.hoisted(() => ({\n activeReaders: 0,\n cancellationErrors: 0,\n deadStreamWrites: 0,\n maxActiveReaders: 0,\n recognizers: [] as Array<{\n canceled?: (_sender: unknown, event: unknown) => void;\n sessionStarted?: (_sender: unknown, event: unknown) => void;\n sessionStopped?: (_sender: unknown, event: unknown) => void;\n speechStartDetected?: (_sender: unknown, event: unknown) => void;\n }>,\n streams: [] as Array<{\n closed: boolean;\n frames: number[];\n }>,\n}));\n\nvi.mock('microsoft-cognitiveservices-speech-sdk', async (importOriginal) => {\n const actual = await importOriginal<typeof import('microsoft-cognitiveservices-speech-sdk')>();\n\n class FakePushStream {\n closed = false;\n frames: number[] = [];\n\n write(buffer: ArrayBuffer): void {\n if (this.closed) {\n azureHarness.deadStreamWrites += 1;\n return;\n }\n this.frames.push(new Int16Array(buffer)[0] ?? 0);\n if (azureHarness.streams[0] === this && this.frames.length === 1) {\n queueMicrotask(() => {\n azureHarness.cancellationErrors += 1;\n azureHarness.recognizers[0]?.canceled?.(undefined, {\n reason: actual.CancellationReason.Error,\n errorCode: actual.CancellationErrorCode.ServiceTimeout,\n errorDetails: 'timeout',\n });\n });\n }\n }\n\n close(): void {\n this.closed = true;\n const index = azureHarness.streams.indexOf(this);\n queueMicrotask(() => {\n azureHarness.recognizers[index]?.sessionStopped?.(undefined, {});\n });\n }\n }\n\n class FakeRecognizer {\n recognizing?: (_sender: unknown, event: unknown) => void;\n recognized?: (_sender: unknown, event: unknown) => void;\n speechStartDetected?: (_sender: unknown, event: unknown) => void;\n speechEndDetected?: (_sender: unknown, event: unknown) => void;\n sessionStarted?: (_sender: unknown, event: unknown) => void;\n sessionStopped?: (_sender: unknown, event: unknown) => void;\n canceled?: (_sender: unknown, event: unknown) => void;\n\n constructor() {\n azureHarness.recognizers.push(this);\n }\n\n startContinuousRecognitionAsync(resolve: () => void): void {\n resolve();\n queueMicrotask(() => {\n this.sessionStarted?.(undefined, {});\n this.speechStartDetected?.(undefined, {});\n });\n }\n\n stopContinuousRecognitionAsync(resolve: () => void): void {\n resolve();\n }\n\n close(): void {}\n }\n\n return {\n ...actual,\n AudioConfig: {\n ...actual.AudioConfig,\n fromStreamInput: () => ({}),\n },\n AudioInputStream: {\n ...actual.AudioInputStream,\n createPushStream: () => {\n const stream = new FakePushStream();\n azureHarness.streams.push(stream);\n return stream;\n },\n },\n SpeechRecognizer: FakeRecognizer,\n };\n});\n\nfunction canceledEvent(\n reason: speechsdk.CancellationReason,\n errorCode?: speechsdk.CancellationErrorCode,\n errorDetails = '',\n) {\n return { reason, errorCode, errorDetails };\n}\n\ndescribe('Azure STT cancellation handling', () => {\n beforeEach(() => {\n azureHarness.activeReaders = 0;\n azureHarness.cancellationErrors = 0;\n azureHarness.deadStreamWrites = 0;\n azureHarness.maxActiveReaders = 0;\n azureHarness.recognizers.length = 0;\n azureHarness.streams.length = 0;\n });\n\n it('unblocks run on canceled error', () => {\n const stream = SpeechStream.prototype as SpeechStream;\n const testStream = Object.create(stream) as SpeechStream;\n testStream._sessionStoppedEvent = {\n isSet: false,\n set() {\n this.isSet = true;\n },\n clear() {\n this.isSet = false;\n },\n wait: () => Promise.resolve(),\n } as SpeechStream['_sessionStoppedEvent'];\n testStream._cancellationError = null;\n\n const event = canceledEvent(\n speechsdk.CancellationReason.Error,\n speechsdk.CancellationErrorCode.ServiceTimeout,\n 'timeout',\n );\n testStream._onCanceled(event);\n\n expect(testStream._sessionStoppedEvent.isSet).toBe(true);\n expect(testStream._cancellationError).toBe(event);\n });\n\n it('ignores cancellations without error', () => {\n const stream = SpeechStream.prototype as SpeechStream;\n const testStream = Object.create(stream) as SpeechStream;\n testStream._sessionStoppedEvent = {\n isSet: false,\n set() {\n this.isSet = true;\n },\n clear() {\n this.isSet = false;\n },\n wait: () => Promise.resolve(),\n } as SpeechStream['_sessionStoppedEvent'];\n testStream._cancellationError = null;\n\n testStream._onCanceled(canceledEvent(speechsdk.CancellationReason.EndOfStream));\n\n expect(testStream._sessionStoppedEvent.isSet).toBe(false);\n expect(testStream._cancellationError).toBeNull();\n });\n\n it('replaces a canceled recognizer without leaving its input consumer alive', async () => {\n const stt = new STT({ speechHost: 'wss://azure.test' });\n const stream = stt.stream({\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const internal = stream as unknown as {\n input: {\n next(options?: { signal?: AbortSignal }): Promise<IteratorResult<AudioFrame | symbol>>;\n };\n };\n const originalNext = internal.input.next.bind(internal.input);\n internal.input.next = async (options = {}) => {\n azureHarness.activeReaders += 1;\n azureHarness.maxActiveReaders = Math.max(\n azureHarness.maxActiveReaders,\n azureHarness.activeReaders,\n );\n try {\n return await originalNext(options);\n } finally {\n azureHarness.activeReaders -= 1;\n }\n };\n\n stream.pushFrame(frame(1));\n await vi.waitFor(() => expect(azureHarness.recognizers).toHaveLength(2));\n stream.pushFrame(frame(2));\n stream.pushFrame(frame(3));\n await vi.waitFor(() => expect(azureHarness.streams[1]?.frames).toEqual([2, 3]));\n\n expect(azureHarness.cancellationErrors).toBe(1);\n expect(azureHarness.maxActiveReaders).toBe(1);\n expect(azureHarness.streams[0]?.closed).toBe(true);\n expect(azureHarness.deadStreamWrites).toBe(0);\n await new Promise((resolve) => setTimeout(resolve, 100));\n expect(azureHarness.recognizers).toHaveLength(2);\n\n stream.close();\n });\n\n it('emits speech start after replacing a canceled recognizer mid-speech', async () => {\n const azureStt = new STT({ speechHost: 'wss://azure.test' });\n const stream = azureStt.stream({\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const startEvents: stt.SpeechEventType[] = [];\n const collectEvents = (async () => {\n for await (const event of stream) {\n if (event.type === stt.SpeechEventType.START_OF_SPEECH) {\n startEvents.push(event.type);\n }\n }\n })();\n\n try {\n await vi.waitFor(() => expect(startEvents).toEqual([stt.SpeechEventType.START_OF_SPEECH]));\n stream.pushFrame(frame(1));\n await vi.waitFor(() => expect(azureHarness.recognizers).toHaveLength(2));\n\n await vi.waitFor(() =>\n expect(startEvents).toEqual([\n stt.SpeechEventType.START_OF_SPEECH,\n stt.SpeechEventType.START_OF_SPEECH,\n ]),\n );\n } finally {\n stream.close();\n await collectEvents;\n }\n });\n});\n\nfunction frame(value: number): AudioFrame {\n return new AudioFrame(new Int16Array([value]), 16000, 1, 1);\n}\n"],"mappings":"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","names":["stt","_a"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stt.test.d.ts","sourceRoot":"","sources":["../src/stt.test.ts"],"names":[],"mappings":""}
|
package/dist/stt.test.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { stt } from "@livekit/agents";
|
|
2
|
+
import { AudioFrame } from "@livekit/rtc-node";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { STT, SpeechStream, speechsdk } from "./stt.js";
|
|
5
|
+
const azureHarness = vi.hoisted(() => ({
|
|
6
|
+
activeReaders: 0,
|
|
7
|
+
cancellationErrors: 0,
|
|
8
|
+
deadStreamWrites: 0,
|
|
9
|
+
maxActiveReaders: 0,
|
|
10
|
+
recognizers: [],
|
|
11
|
+
streams: []
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("microsoft-cognitiveservices-speech-sdk", async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal();
|
|
15
|
+
class FakePushStream {
|
|
16
|
+
closed = false;
|
|
17
|
+
frames = [];
|
|
18
|
+
write(buffer) {
|
|
19
|
+
if (this.closed) {
|
|
20
|
+
azureHarness.deadStreamWrites += 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.frames.push(new Int16Array(buffer)[0] ?? 0);
|
|
24
|
+
if (azureHarness.streams[0] === this && this.frames.length === 1) {
|
|
25
|
+
queueMicrotask(() => {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
azureHarness.cancellationErrors += 1;
|
|
28
|
+
(_b = (_a = azureHarness.recognizers[0]) == null ? void 0 : _a.canceled) == null ? void 0 : _b.call(_a, void 0, {
|
|
29
|
+
reason: actual.CancellationReason.Error,
|
|
30
|
+
errorCode: actual.CancellationErrorCode.ServiceTimeout,
|
|
31
|
+
errorDetails: "timeout"
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
close() {
|
|
37
|
+
this.closed = true;
|
|
38
|
+
const index = azureHarness.streams.indexOf(this);
|
|
39
|
+
queueMicrotask(() => {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
(_b = (_a = azureHarness.recognizers[index]) == null ? void 0 : _a.sessionStopped) == null ? void 0 : _b.call(_a, void 0, {});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
class FakeRecognizer {
|
|
46
|
+
recognizing;
|
|
47
|
+
recognized;
|
|
48
|
+
speechStartDetected;
|
|
49
|
+
speechEndDetected;
|
|
50
|
+
sessionStarted;
|
|
51
|
+
sessionStopped;
|
|
52
|
+
canceled;
|
|
53
|
+
constructor() {
|
|
54
|
+
azureHarness.recognizers.push(this);
|
|
55
|
+
}
|
|
56
|
+
startContinuousRecognitionAsync(resolve) {
|
|
57
|
+
resolve();
|
|
58
|
+
queueMicrotask(() => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
(_a = this.sessionStarted) == null ? void 0 : _a.call(this, void 0, {});
|
|
61
|
+
(_b = this.speechStartDetected) == null ? void 0 : _b.call(this, void 0, {});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
stopContinuousRecognitionAsync(resolve) {
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...actual,
|
|
72
|
+
AudioConfig: {
|
|
73
|
+
...actual.AudioConfig,
|
|
74
|
+
fromStreamInput: () => ({})
|
|
75
|
+
},
|
|
76
|
+
AudioInputStream: {
|
|
77
|
+
...actual.AudioInputStream,
|
|
78
|
+
createPushStream: () => {
|
|
79
|
+
const stream = new FakePushStream();
|
|
80
|
+
azureHarness.streams.push(stream);
|
|
81
|
+
return stream;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
SpeechRecognizer: FakeRecognizer
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
function canceledEvent(reason, errorCode, errorDetails = "") {
|
|
88
|
+
return { reason, errorCode, errorDetails };
|
|
89
|
+
}
|
|
90
|
+
describe("Azure STT cancellation handling", () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
azureHarness.activeReaders = 0;
|
|
93
|
+
azureHarness.cancellationErrors = 0;
|
|
94
|
+
azureHarness.deadStreamWrites = 0;
|
|
95
|
+
azureHarness.maxActiveReaders = 0;
|
|
96
|
+
azureHarness.recognizers.length = 0;
|
|
97
|
+
azureHarness.streams.length = 0;
|
|
98
|
+
});
|
|
99
|
+
it("unblocks run on canceled error", () => {
|
|
100
|
+
const stream = SpeechStream.prototype;
|
|
101
|
+
const testStream = Object.create(stream);
|
|
102
|
+
testStream._sessionStoppedEvent = {
|
|
103
|
+
isSet: false,
|
|
104
|
+
set() {
|
|
105
|
+
this.isSet = true;
|
|
106
|
+
},
|
|
107
|
+
clear() {
|
|
108
|
+
this.isSet = false;
|
|
109
|
+
},
|
|
110
|
+
wait: () => Promise.resolve()
|
|
111
|
+
};
|
|
112
|
+
testStream._cancellationError = null;
|
|
113
|
+
const event = canceledEvent(
|
|
114
|
+
speechsdk.CancellationReason.Error,
|
|
115
|
+
speechsdk.CancellationErrorCode.ServiceTimeout,
|
|
116
|
+
"timeout"
|
|
117
|
+
);
|
|
118
|
+
testStream._onCanceled(event);
|
|
119
|
+
expect(testStream._sessionStoppedEvent.isSet).toBe(true);
|
|
120
|
+
expect(testStream._cancellationError).toBe(event);
|
|
121
|
+
});
|
|
122
|
+
it("ignores cancellations without error", () => {
|
|
123
|
+
const stream = SpeechStream.prototype;
|
|
124
|
+
const testStream = Object.create(stream);
|
|
125
|
+
testStream._sessionStoppedEvent = {
|
|
126
|
+
isSet: false,
|
|
127
|
+
set() {
|
|
128
|
+
this.isSet = true;
|
|
129
|
+
},
|
|
130
|
+
clear() {
|
|
131
|
+
this.isSet = false;
|
|
132
|
+
},
|
|
133
|
+
wait: () => Promise.resolve()
|
|
134
|
+
};
|
|
135
|
+
testStream._cancellationError = null;
|
|
136
|
+
testStream._onCanceled(canceledEvent(speechsdk.CancellationReason.EndOfStream));
|
|
137
|
+
expect(testStream._sessionStoppedEvent.isSet).toBe(false);
|
|
138
|
+
expect(testStream._cancellationError).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
it("replaces a canceled recognizer without leaving its input consumer alive", async () => {
|
|
141
|
+
var _a;
|
|
142
|
+
const stt2 = new STT({ speechHost: "wss://azure.test" });
|
|
143
|
+
const stream = stt2.stream({
|
|
144
|
+
connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1e3 }
|
|
145
|
+
});
|
|
146
|
+
const internal = stream;
|
|
147
|
+
const originalNext = internal.input.next.bind(internal.input);
|
|
148
|
+
internal.input.next = async (options = {}) => {
|
|
149
|
+
azureHarness.activeReaders += 1;
|
|
150
|
+
azureHarness.maxActiveReaders = Math.max(
|
|
151
|
+
azureHarness.maxActiveReaders,
|
|
152
|
+
azureHarness.activeReaders
|
|
153
|
+
);
|
|
154
|
+
try {
|
|
155
|
+
return await originalNext(options);
|
|
156
|
+
} finally {
|
|
157
|
+
azureHarness.activeReaders -= 1;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
stream.pushFrame(frame(1));
|
|
161
|
+
await vi.waitFor(() => expect(azureHarness.recognizers).toHaveLength(2));
|
|
162
|
+
stream.pushFrame(frame(2));
|
|
163
|
+
stream.pushFrame(frame(3));
|
|
164
|
+
await vi.waitFor(() => {
|
|
165
|
+
var _a2;
|
|
166
|
+
return expect((_a2 = azureHarness.streams[1]) == null ? void 0 : _a2.frames).toEqual([2, 3]);
|
|
167
|
+
});
|
|
168
|
+
expect(azureHarness.cancellationErrors).toBe(1);
|
|
169
|
+
expect(azureHarness.maxActiveReaders).toBe(1);
|
|
170
|
+
expect((_a = azureHarness.streams[0]) == null ? void 0 : _a.closed).toBe(true);
|
|
171
|
+
expect(azureHarness.deadStreamWrites).toBe(0);
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
173
|
+
expect(azureHarness.recognizers).toHaveLength(2);
|
|
174
|
+
stream.close();
|
|
175
|
+
});
|
|
176
|
+
it("emits speech start after replacing a canceled recognizer mid-speech", async () => {
|
|
177
|
+
const azureStt = new STT({ speechHost: "wss://azure.test" });
|
|
178
|
+
const stream = azureStt.stream({
|
|
179
|
+
connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1e3 }
|
|
180
|
+
});
|
|
181
|
+
const startEvents = [];
|
|
182
|
+
const collectEvents = (async () => {
|
|
183
|
+
for await (const event of stream) {
|
|
184
|
+
if (event.type === stt.SpeechEventType.START_OF_SPEECH) {
|
|
185
|
+
startEvents.push(event.type);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
try {
|
|
190
|
+
await vi.waitFor(() => expect(startEvents).toEqual([stt.SpeechEventType.START_OF_SPEECH]));
|
|
191
|
+
stream.pushFrame(frame(1));
|
|
192
|
+
await vi.waitFor(() => expect(azureHarness.recognizers).toHaveLength(2));
|
|
193
|
+
await vi.waitFor(
|
|
194
|
+
() => expect(startEvents).toEqual([
|
|
195
|
+
stt.SpeechEventType.START_OF_SPEECH,
|
|
196
|
+
stt.SpeechEventType.START_OF_SPEECH
|
|
197
|
+
])
|
|
198
|
+
);
|
|
199
|
+
} finally {
|
|
200
|
+
stream.close();
|
|
201
|
+
await collectEvents;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
function frame(value) {
|
|
206
|
+
return new AudioFrame(new Int16Array([value]), 16e3, 1, 1);
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=stt.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stt.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { stt } from '@livekit/agents';\nimport { AudioFrame } from '@livekit/rtc-node';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { STT, SpeechStream, speechsdk } from './stt.js';\n\nconst azureHarness = vi.hoisted(() => ({\n activeReaders: 0,\n cancellationErrors: 0,\n deadStreamWrites: 0,\n maxActiveReaders: 0,\n recognizers: [] as Array<{\n canceled?: (_sender: unknown, event: unknown) => void;\n sessionStarted?: (_sender: unknown, event: unknown) => void;\n sessionStopped?: (_sender: unknown, event: unknown) => void;\n speechStartDetected?: (_sender: unknown, event: unknown) => void;\n }>,\n streams: [] as Array<{\n closed: boolean;\n frames: number[];\n }>,\n}));\n\nvi.mock('microsoft-cognitiveservices-speech-sdk', async (importOriginal) => {\n const actual = await importOriginal<typeof import('microsoft-cognitiveservices-speech-sdk')>();\n\n class FakePushStream {\n closed = false;\n frames: number[] = [];\n\n write(buffer: ArrayBuffer): void {\n if (this.closed) {\n azureHarness.deadStreamWrites += 1;\n return;\n }\n this.frames.push(new Int16Array(buffer)[0] ?? 0);\n if (azureHarness.streams[0] === this && this.frames.length === 1) {\n queueMicrotask(() => {\n azureHarness.cancellationErrors += 1;\n azureHarness.recognizers[0]?.canceled?.(undefined, {\n reason: actual.CancellationReason.Error,\n errorCode: actual.CancellationErrorCode.ServiceTimeout,\n errorDetails: 'timeout',\n });\n });\n }\n }\n\n close(): void {\n this.closed = true;\n const index = azureHarness.streams.indexOf(this);\n queueMicrotask(() => {\n azureHarness.recognizers[index]?.sessionStopped?.(undefined, {});\n });\n }\n }\n\n class FakeRecognizer {\n recognizing?: (_sender: unknown, event: unknown) => void;\n recognized?: (_sender: unknown, event: unknown) => void;\n speechStartDetected?: (_sender: unknown, event: unknown) => void;\n speechEndDetected?: (_sender: unknown, event: unknown) => void;\n sessionStarted?: (_sender: unknown, event: unknown) => void;\n sessionStopped?: (_sender: unknown, event: unknown) => void;\n canceled?: (_sender: unknown, event: unknown) => void;\n\n constructor() {\n azureHarness.recognizers.push(this);\n }\n\n startContinuousRecognitionAsync(resolve: () => void): void {\n resolve();\n queueMicrotask(() => {\n this.sessionStarted?.(undefined, {});\n this.speechStartDetected?.(undefined, {});\n });\n }\n\n stopContinuousRecognitionAsync(resolve: () => void): void {\n resolve();\n }\n\n close(): void {}\n }\n\n return {\n ...actual,\n AudioConfig: {\n ...actual.AudioConfig,\n fromStreamInput: () => ({}),\n },\n AudioInputStream: {\n ...actual.AudioInputStream,\n createPushStream: () => {\n const stream = new FakePushStream();\n azureHarness.streams.push(stream);\n return stream;\n },\n },\n SpeechRecognizer: FakeRecognizer,\n };\n});\n\nfunction canceledEvent(\n reason: speechsdk.CancellationReason,\n errorCode?: speechsdk.CancellationErrorCode,\n errorDetails = '',\n) {\n return { reason, errorCode, errorDetails };\n}\n\ndescribe('Azure STT cancellation handling', () => {\n beforeEach(() => {\n azureHarness.activeReaders = 0;\n azureHarness.cancellationErrors = 0;\n azureHarness.deadStreamWrites = 0;\n azureHarness.maxActiveReaders = 0;\n azureHarness.recognizers.length = 0;\n azureHarness.streams.length = 0;\n });\n\n it('unblocks run on canceled error', () => {\n const stream = SpeechStream.prototype as SpeechStream;\n const testStream = Object.create(stream) as SpeechStream;\n testStream._sessionStoppedEvent = {\n isSet: false,\n set() {\n this.isSet = true;\n },\n clear() {\n this.isSet = false;\n },\n wait: () => Promise.resolve(),\n } as SpeechStream['_sessionStoppedEvent'];\n testStream._cancellationError = null;\n\n const event = canceledEvent(\n speechsdk.CancellationReason.Error,\n speechsdk.CancellationErrorCode.ServiceTimeout,\n 'timeout',\n );\n testStream._onCanceled(event);\n\n expect(testStream._sessionStoppedEvent.isSet).toBe(true);\n expect(testStream._cancellationError).toBe(event);\n });\n\n it('ignores cancellations without error', () => {\n const stream = SpeechStream.prototype as SpeechStream;\n const testStream = Object.create(stream) as SpeechStream;\n testStream._sessionStoppedEvent = {\n isSet: false,\n set() {\n this.isSet = true;\n },\n clear() {\n this.isSet = false;\n },\n wait: () => Promise.resolve(),\n } as SpeechStream['_sessionStoppedEvent'];\n testStream._cancellationError = null;\n\n testStream._onCanceled(canceledEvent(speechsdk.CancellationReason.EndOfStream));\n\n expect(testStream._sessionStoppedEvent.isSet).toBe(false);\n expect(testStream._cancellationError).toBeNull();\n });\n\n it('replaces a canceled recognizer without leaving its input consumer alive', async () => {\n const stt = new STT({ speechHost: 'wss://azure.test' });\n const stream = stt.stream({\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const internal = stream as unknown as {\n input: {\n next(options?: { signal?: AbortSignal }): Promise<IteratorResult<AudioFrame | symbol>>;\n };\n };\n const originalNext = internal.input.next.bind(internal.input);\n internal.input.next = async (options = {}) => {\n azureHarness.activeReaders += 1;\n azureHarness.maxActiveReaders = Math.max(\n azureHarness.maxActiveReaders,\n azureHarness.activeReaders,\n );\n try {\n return await originalNext(options);\n } finally {\n azureHarness.activeReaders -= 1;\n }\n };\n\n stream.pushFrame(frame(1));\n await vi.waitFor(() => expect(azureHarness.recognizers).toHaveLength(2));\n stream.pushFrame(frame(2));\n stream.pushFrame(frame(3));\n await vi.waitFor(() => expect(azureHarness.streams[1]?.frames).toEqual([2, 3]));\n\n expect(azureHarness.cancellationErrors).toBe(1);\n expect(azureHarness.maxActiveReaders).toBe(1);\n expect(azureHarness.streams[0]?.closed).toBe(true);\n expect(azureHarness.deadStreamWrites).toBe(0);\n await new Promise((resolve) => setTimeout(resolve, 100));\n expect(azureHarness.recognizers).toHaveLength(2);\n\n stream.close();\n });\n\n it('emits speech start after replacing a canceled recognizer mid-speech', async () => {\n const azureStt = new STT({ speechHost: 'wss://azure.test' });\n const stream = azureStt.stream({\n connOptions: { maxRetry: 1, retryIntervalMs: 0, timeoutMs: 1000 },\n });\n const startEvents: stt.SpeechEventType[] = [];\n const collectEvents = (async () => {\n for await (const event of stream) {\n if (event.type === stt.SpeechEventType.START_OF_SPEECH) {\n startEvents.push(event.type);\n }\n }\n })();\n\n try {\n await vi.waitFor(() => expect(startEvents).toEqual([stt.SpeechEventType.START_OF_SPEECH]));\n stream.pushFrame(frame(1));\n await vi.waitFor(() => expect(azureHarness.recognizers).toHaveLength(2));\n\n await vi.waitFor(() =>\n expect(startEvents).toEqual([\n stt.SpeechEventType.START_OF_SPEECH,\n stt.SpeechEventType.START_OF_SPEECH,\n ]),\n );\n } finally {\n stream.close();\n await collectEvents;\n }\n });\n});\n\nfunction frame(value: number): AudioFrame {\n return new AudioFrame(new Int16Array([value]), 16000, 1, 1);\n}\n"],"mappings":"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","names":["stt","_a"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@livekit/agents-plugin-azure",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Azure plugin for LiveKit Agents for Node.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"require": "dist/index.cjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.d.cts",
|
|
15
|
+
"default": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"author": "LiveKit",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"repository": "git@github.com:livekit/agents-js.git",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@livekit/rtc-node": "^0.13.31",
|
|
29
|
+
"@microsoft/api-extractor": "^7.35.0",
|
|
30
|
+
"tsup": "^8.3.5",
|
|
31
|
+
"typescript": "^5.0.0",
|
|
32
|
+
"@livekit/agents": "1.5.2"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"microsoft-cognitiveservices-speech-sdk": "^1.50.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@livekit/rtc-node": "^0.13.31",
|
|
39
|
+
"@livekit/agents": "1.5.2"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup --onSuccess \"pnpm build:types\"",
|
|
43
|
+
"build:types": "tsc --declaration --emitDeclarationOnly && node ../../scripts/copyDeclarationOutput.js",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"clean:build": "pnpm clean && pnpm build",
|
|
46
|
+
"lint": "eslint -f unix \"src/**/*.{ts,js}\"",
|
|
47
|
+
"api:check": "api-extractor run --typescript-compiler-folder ../../node_modules/typescript",
|
|
48
|
+
"api:update": "api-extractor run --local --typescript-compiler-folder ../../node_modules/typescript --verbose"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { Plugin } from '@livekit/agents';
|
|
5
|
+
|
|
6
|
+
export * from './stt.js';
|
|
7
|
+
|
|
8
|
+
class AzurePlugin extends Plugin {
|
|
9
|
+
constructor() {
|
|
10
|
+
super({
|
|
11
|
+
title: 'azure',
|
|
12
|
+
version: __PACKAGE_VERSION__,
|
|
13
|
+
package: __PACKAGE_NAME__,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Plugin.registerPlugin(new AzurePlugin());
|