@kuralle-syrinx/silero-vad 4.2.0 → 4.3.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/package.json +3 -3
- package/src/index.test.ts +0 -329
- package/src/workers.test.ts +0 -124
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/silero-vad",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Silero VAD plugin for Syrinx — ONNX voice activity detection for Node and Workers",
|
|
6
6
|
"keywords": [
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"onnxruntime-node": "1.24.3",
|
|
34
34
|
"onnxruntime-web": "1.24.3",
|
|
35
|
-
"@kuralle-syrinx/core": "4.
|
|
35
|
+
"@kuralle-syrinx/core": "4.3.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.7.0",
|
|
39
|
-
"vitest": "^2.
|
|
39
|
+
"vitest": "^3.2.6"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"typecheck": "tsc --noEmit",
|
package/src/index.test.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import type { PipelineBus } from "@kuralle-syrinx/core";
|
|
5
|
-
|
|
6
|
-
import { SileroVADPlugin } from "./index.js";
|
|
7
|
-
|
|
8
|
-
// Shared mutable state that the vi.mock factory can close over via vi.hoisted.
|
|
9
|
-
const mockControl = vi.hoisted(() => ({
|
|
10
|
-
confidence: 0.9,
|
|
11
|
-
stateCallArgs: [] as Float32Array[],
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock("onnxruntime-node", () => {
|
|
15
|
-
// Minimal Tensor stand-in — stores data so session.run() can read it back.
|
|
16
|
-
function MockTensor(
|
|
17
|
-
this: { data: unknown },
|
|
18
|
-
_type: string,
|
|
19
|
-
data: unknown,
|
|
20
|
-
_shape: unknown,
|
|
21
|
-
) {
|
|
22
|
-
this.data = data;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const run = vi.fn(async (inputs: Record<string, { data: unknown }>) => {
|
|
26
|
-
const state = inputs["state"]?.data;
|
|
27
|
-
if (state instanceof Float32Array) {
|
|
28
|
-
mockControl.stateCallArgs.push(new Float32Array(state));
|
|
29
|
-
}
|
|
30
|
-
// Return a distinctive non-zero state so a subsequent reset is detectable.
|
|
31
|
-
return {
|
|
32
|
-
output: { data: [mockControl.confidence] },
|
|
33
|
-
stateN: { data: new Float32Array(2 * 1 * 128).fill(0.5) },
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
InferenceSession: { create: vi.fn(async () => ({ run })) },
|
|
39
|
-
Tensor: MockTensor,
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ── helpers ────────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
function makeBus(): { bus: PipelineBus; emitted: Array<{ kind: string }> } {
|
|
46
|
-
const emitted: Array<{ kind: string }> = [];
|
|
47
|
-
const bus = {
|
|
48
|
-
push(_route: unknown, pkt: unknown) {
|
|
49
|
-
emitted.push(pkt as { kind: string });
|
|
50
|
-
},
|
|
51
|
-
on(_event: string, _handler: unknown) {
|
|
52
|
-
return () => {};
|
|
53
|
-
},
|
|
54
|
-
} as unknown as PipelineBus;
|
|
55
|
-
return { bus, emitted };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 512-sample (32 ms) silent PCM frame — value irrelevant since model is mocked. */
|
|
59
|
-
function makePcmFrame(): Uint8Array {
|
|
60
|
-
return new Uint8Array(512 * 2);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function initPlugin(
|
|
64
|
-
bus: PipelineBus,
|
|
65
|
-
config: Record<string, unknown> = {},
|
|
66
|
-
): Promise<SileroVADPlugin> {
|
|
67
|
-
const plugin = new SileroVADPlugin();
|
|
68
|
-
await plugin.initialize(bus, { model_path: "/dev/null", ...config });
|
|
69
|
-
return plugin;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function countKind(emitted: Array<{ kind: string }>, kind: string): number {
|
|
73
|
-
return emitted.filter((p) => p.kind === kind).length;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function metricValue(
|
|
77
|
-
emitted: Array<{ kind: string; name?: string; value?: string }>,
|
|
78
|
-
name: string,
|
|
79
|
-
): string | undefined {
|
|
80
|
-
return emitted.find((p) => p.kind === "metric.conversation" && p.name === name)?.value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── tests ──────────────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe("SileroVADPlugin — G11: periodic state reset gating", () => {
|
|
86
|
-
beforeEach(() => {
|
|
87
|
-
vi.useFakeTimers();
|
|
88
|
-
vi.setSystemTime(0);
|
|
89
|
-
mockControl.confidence = 0.9;
|
|
90
|
-
mockControl.stateCallArgs.length = 0;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
afterEach(() => {
|
|
94
|
-
vi.useRealTimers();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("does not reset model state mid-speech when the 5 s timer elapses", async () => {
|
|
98
|
-
const { bus, emitted } = makeBus();
|
|
99
|
-
const plugin = await initPlugin(bus);
|
|
100
|
-
|
|
101
|
-
// Frame 1 (T=0): confidence=0.9 → speech_started, vadState=speaking.
|
|
102
|
-
// stateCallArgs[0] = all-zeros (initial state).
|
|
103
|
-
await plugin.processAudio(makePcmFrame(), "ctx-1");
|
|
104
|
-
expect(emitted.some((p) => p.kind === "vad.speech_started")).toBe(true);
|
|
105
|
-
|
|
106
|
-
// Advance past the 5 s reset boundary while still speaking.
|
|
107
|
-
vi.setSystemTime(6000);
|
|
108
|
-
|
|
109
|
-
// Frame 2 (T=6000): fix must skip the reset because vadState=speaking.
|
|
110
|
-
// stateCallArgs[1] = [0.5...] (state updated by frame 1 output).
|
|
111
|
-
// After this call, state stays [0.5...] (no reset).
|
|
112
|
-
await plugin.processAudio(makePcmFrame(), "ctx-1");
|
|
113
|
-
|
|
114
|
-
// Frame 3 (T=6000): if a reset had fired after frame 2, this call would
|
|
115
|
-
// receive all-zero state. With the fix it receives the non-zero value.
|
|
116
|
-
await plugin.processAudio(makePcmFrame(), "ctx-1");
|
|
117
|
-
|
|
118
|
-
expect(emitted.some((p) => p.kind === "vad.speech_ended")).toBe(false);
|
|
119
|
-
|
|
120
|
-
// State entering frame 3 must still carry the non-zero value from the mock
|
|
121
|
-
// (0.5), proving no mid-speech reset occurred.
|
|
122
|
-
const stateAtFrame3 = mockControl.stateCallArgs[2];
|
|
123
|
-
expect(stateAtFrame3).toBeDefined();
|
|
124
|
-
expect(stateAtFrame3!.some((v) => v !== 0)).toBe(true);
|
|
125
|
-
|
|
126
|
-
await plugin.close();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("resets model state at the 5 s boundary once speech has ended", async () => {
|
|
130
|
-
const { bus, emitted } = makeBus();
|
|
131
|
-
const plugin = await initPlugin(bus);
|
|
132
|
-
|
|
133
|
-
// Frame 1 (T=0): speech starts.
|
|
134
|
-
mockControl.confidence = 0.9;
|
|
135
|
-
await plugin.processAudio(makePcmFrame(), "ctx-2");
|
|
136
|
-
expect(emitted.some((p) => p.kind === "vad.speech_started")).toBe(true);
|
|
137
|
-
|
|
138
|
-
// Switch to silence. silenceFrameTarget = ceil(200/32) = 7; speech ends when
|
|
139
|
-
// silenceFrames*32 >= minSilenceDurationMs(200) + speechPadMs(80) = 280 ms,
|
|
140
|
-
// i.e. after 9 consecutive silence frames (9*32=288 >= 280).
|
|
141
|
-
mockControl.confidence = 0.1;
|
|
142
|
-
for (let i = 0; i < 9; i++) {
|
|
143
|
-
await plugin.processAudio(makePcmFrame(), "ctx-2");
|
|
144
|
-
}
|
|
145
|
-
expect(emitted.some((p) => p.kind === "vad.speech_ended")).toBe(true);
|
|
146
|
-
|
|
147
|
-
// Time has not advanced yet; no reset should have fired (0 - 0 < 5000).
|
|
148
|
-
// Now advance past the reset window.
|
|
149
|
-
vi.setSystemTime(6000);
|
|
150
|
-
|
|
151
|
-
// Frame at T=6000 (not speaking): reset fires after this run call.
|
|
152
|
-
await plugin.processAudio(makePcmFrame(), "ctx-2");
|
|
153
|
-
|
|
154
|
-
// One more frame: should receive all-zero state because the reset zeroed it.
|
|
155
|
-
await plugin.processAudio(makePcmFrame(), "ctx-2");
|
|
156
|
-
|
|
157
|
-
const lastStateArg = mockControl.stateCallArgs.at(-1);
|
|
158
|
-
expect(lastStateArg).toBeDefined();
|
|
159
|
-
expect(lastStateArg!.every((v) => v === 0)).toBe(true);
|
|
160
|
-
|
|
161
|
-
await plugin.close();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("SileroVADPlugin — four-state VAD (VE-02)", () => {
|
|
166
|
-
beforeEach(() => {
|
|
167
|
-
mockControl.confidence = 0.9;
|
|
168
|
-
mockControl.stateCallArgs.length = 0;
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("vad_flap_rejected: sub-threshold speech burst does not emit speech_started", async () => {
|
|
172
|
-
const { bus, emitted } = makeBus();
|
|
173
|
-
const plugin = await initPlugin(bus, { speech_start_duration_ms: 96 });
|
|
174
|
-
|
|
175
|
-
mockControl.confidence = 0.9;
|
|
176
|
-
await plugin.processAudio(makePcmFrame(), "ctx-flap");
|
|
177
|
-
|
|
178
|
-
mockControl.confidence = 0.1;
|
|
179
|
-
await plugin.processAudio(makePcmFrame(), "ctx-flap");
|
|
180
|
-
|
|
181
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(0);
|
|
182
|
-
expect(metricValue(emitted, "vad.start_delay_ms")).toBeUndefined();
|
|
183
|
-
|
|
184
|
-
await plugin.close();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("vad_full_cycle: sustained speech then silence emits one start and one end", async () => {
|
|
188
|
-
const { bus, emitted } = makeBus();
|
|
189
|
-
const plugin = await initPlugin(bus);
|
|
190
|
-
|
|
191
|
-
mockControl.confidence = 0.9;
|
|
192
|
-
for (let i = 0; i < 4; i++) {
|
|
193
|
-
await plugin.processAudio(makePcmFrame(), "ctx-cycle");
|
|
194
|
-
}
|
|
195
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(1);
|
|
196
|
-
expect(metricValue(emitted, "vad.start_delay_ms")).toBe("32");
|
|
197
|
-
|
|
198
|
-
mockControl.confidence = 0.1;
|
|
199
|
-
for (let i = 0; i < 9; i++) {
|
|
200
|
-
await plugin.processAudio(makePcmFrame(), "ctx-cycle");
|
|
201
|
-
}
|
|
202
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(1);
|
|
203
|
-
expect(metricValue(emitted, "vad.stop_hangover_ms")).toBe("288");
|
|
204
|
-
|
|
205
|
-
await plugin.close();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("vad_resume_no_end: brief silence dip during speech does not emit speech_ended", async () => {
|
|
209
|
-
const { bus, emitted } = makeBus();
|
|
210
|
-
const plugin = await initPlugin(bus);
|
|
211
|
-
|
|
212
|
-
mockControl.confidence = 0.9;
|
|
213
|
-
await plugin.processAudio(makePcmFrame(), "ctx-resume");
|
|
214
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(1);
|
|
215
|
-
|
|
216
|
-
mockControl.confidence = 0.1;
|
|
217
|
-
for (let i = 0; i < 3; i++) {
|
|
218
|
-
await plugin.processAudio(makePcmFrame(), "ctx-resume");
|
|
219
|
-
}
|
|
220
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(0);
|
|
221
|
-
|
|
222
|
-
mockControl.confidence = 0.9;
|
|
223
|
-
await plugin.processAudio(makePcmFrame(), "ctx-resume");
|
|
224
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(0);
|
|
225
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(1);
|
|
226
|
-
|
|
227
|
-
await plugin.close();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("default speech_start_duration_ms emits speech_started on the first speech frame", async () => {
|
|
231
|
-
const { bus, emitted } = makeBus();
|
|
232
|
-
const plugin = await initPlugin(bus);
|
|
233
|
-
|
|
234
|
-
mockControl.confidence = 0.9;
|
|
235
|
-
await plugin.processAudio(makePcmFrame(), "ctx-default");
|
|
236
|
-
|
|
237
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(1);
|
|
238
|
-
expect(metricValue(emitted, "vad.start_delay_ms")).toBe("32");
|
|
239
|
-
|
|
240
|
-
await plugin.close();
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe("SileroVADPlugin — odd byteOffset PCM (browser buffer alignment)", () => {
|
|
245
|
-
beforeEach(() => {
|
|
246
|
-
mockControl.confidence = 0.9;
|
|
247
|
-
mockControl.stateCallArgs.length = 0;
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("processes a PCM frame whose byteOffset is odd without throwing", async () => {
|
|
251
|
-
const { bus, emitted } = makeBus();
|
|
252
|
-
const plugin = await initPlugin(bus);
|
|
253
|
-
|
|
254
|
-
// Inbound browser PCM is frequently a Uint8Array view into a pooled Node
|
|
255
|
-
// Buffer at an ODD byteOffset. `new Int16Array(buffer, oddOffset, …)` throws
|
|
256
|
-
// "start offset of Int16Array should be a multiple of 2" — this reproduces it.
|
|
257
|
-
const backing = new Uint8Array(512 * 2 + 1);
|
|
258
|
-
const oddOffsetFrame = backing.subarray(1); // byteOffset 1 (odd), even length
|
|
259
|
-
expect(oddOffsetFrame.byteOffset % 2).toBe(1);
|
|
260
|
-
|
|
261
|
-
await expect(plugin.processAudio(oddOffsetFrame, "ctx-odd")).resolves.toBeUndefined();
|
|
262
|
-
expect(emitted.some((p) => p.kind === "vad.error")).toBe(false);
|
|
263
|
-
// Reached the model and emitted a VAD verdict — proof the frame was processed.
|
|
264
|
-
expect(emitted.some((p) => p.kind === "vad.speech_started")).toBe(true);
|
|
265
|
-
|
|
266
|
-
await plugin.close();
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe("SileroVADPlugin — telephony saturation hardening", () => {
|
|
271
|
-
beforeEach(() => {
|
|
272
|
-
vi.useFakeTimers();
|
|
273
|
-
vi.setSystemTime(0);
|
|
274
|
-
mockControl.confidence = 0.9;
|
|
275
|
-
mockControl.stateCallArgs.length = 0;
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
afterEach(() => {
|
|
279
|
-
vi.useRealTimers();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
async function frame(plugin: SileroVADPlugin, confidence: number): Promise<void> {
|
|
283
|
-
mockControl.confidence = confidence;
|
|
284
|
-
await plugin.processAudio(makePcmFrame(), "ctx-flap");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
it("absorbs single-frame confidence spikes during stopping so speech still ends", async () => {
|
|
288
|
-
const { bus, emitted } = makeBus();
|
|
289
|
-
const plugin = await initPlugin(bus);
|
|
290
|
-
|
|
291
|
-
await frame(plugin, 0.9); // speech_started → speaking
|
|
292
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(1);
|
|
293
|
-
|
|
294
|
-
// Saturation flap: real silence with an isolated spike in the middle.
|
|
295
|
-
for (const confidence of [0.2, 0.2, 0.2, 0.2, 0.9, 0.2, 0.2, 0.2, 0.2, 0.2]) {
|
|
296
|
-
await frame(plugin, confidence);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(1);
|
|
300
|
-
expect(metricValue(emitted, "vad.stop_hangover_ms")).toBeDefined();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("two consecutive speech frames during stopping resume speaking (real speech is unaffected)", async () => {
|
|
304
|
-
const { bus, emitted } = makeBus();
|
|
305
|
-
const plugin = await initPlugin(bus);
|
|
306
|
-
|
|
307
|
-
await frame(plugin, 0.9); // speaking
|
|
308
|
-
for (const confidence of [0.2, 0.2, 0.2]) await frame(plugin, confidence); // stopping
|
|
309
|
-
for (const confidence of [0.9, 0.9]) await frame(plugin, confidence); // sustained speech → resume
|
|
310
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(0);
|
|
311
|
-
|
|
312
|
-
for (let i = 0; i < 9; i += 1) await frame(plugin, 0.2); // genuine end
|
|
313
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(1);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it("resets model state during prolonged continuous speech (saturation guard)", async () => {
|
|
317
|
-
const { bus, emitted } = makeBus();
|
|
318
|
-
const plugin = await initPlugin(bus);
|
|
319
|
-
|
|
320
|
-
await frame(plugin, 0.9); // speaking, state seeded
|
|
321
|
-
vi.setSystemTime(13_000); // past speaking_state_reset_interval_ms (12 s)
|
|
322
|
-
await frame(plugin, 0.9); // reset fires after this frame's inference
|
|
323
|
-
await frame(plugin, 0.9); // this inference must receive the zeroed state
|
|
324
|
-
|
|
325
|
-
expect(metricValue(emitted, "vad.state_reset_in_speech")).toBe("1");
|
|
326
|
-
const stateForThirdFrame = mockControl.stateCallArgs[2]!;
|
|
327
|
-
expect(stateForThirdFrame.every((value) => value === 0)).toBe(true);
|
|
328
|
-
});
|
|
329
|
-
});
|
package/src/workers.test.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import type { PipelineBus } from "@kuralle-syrinx/core";
|
|
5
|
-
|
|
6
|
-
import { SileroVADPlugin } from "./workers.js";
|
|
7
|
-
|
|
8
|
-
const mockControl = vi.hoisted(() => ({
|
|
9
|
-
confidence: 0.9,
|
|
10
|
-
stateCallArgs: [] as Float32Array[],
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock("onnxruntime-web", () => {
|
|
14
|
-
function MockTensor(this: { data: unknown }, _type: string, data: unknown, _shape: unknown) {
|
|
15
|
-
this.data = data;
|
|
16
|
-
}
|
|
17
|
-
const run = vi.fn(async (inputs: Record<string, { data: unknown }>) => {
|
|
18
|
-
const state = inputs["state"]?.data;
|
|
19
|
-
if (state instanceof Float32Array) {
|
|
20
|
-
mockControl.stateCallArgs.push(new Float32Array(state));
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
output: { data: [mockControl.confidence] },
|
|
24
|
-
stateN: { data: new Float32Array(2 * 1 * 128).fill(0.5) },
|
|
25
|
-
};
|
|
26
|
-
});
|
|
27
|
-
return {
|
|
28
|
-
InferenceSession: { create: vi.fn(async () => ({ run })) },
|
|
29
|
-
Tensor: MockTensor,
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
function makeBus(): { bus: PipelineBus; emitted: Array<{ kind: string }> } {
|
|
34
|
-
const emitted: Array<{ kind: string }> = [];
|
|
35
|
-
const bus = {
|
|
36
|
-
push(_route: unknown, pkt: unknown) {
|
|
37
|
-
emitted.push(pkt as { kind: string });
|
|
38
|
-
},
|
|
39
|
-
on(_event: string, _handler: unknown) {
|
|
40
|
-
return () => {};
|
|
41
|
-
},
|
|
42
|
-
} as unknown as PipelineBus;
|
|
43
|
-
return { bus, emitted };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makePcmFrame(): Uint8Array {
|
|
47
|
-
return new Uint8Array(512 * 2);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function initPlugin(bus: PipelineBus, config: Record<string, unknown> = {}): Promise<SileroVADPlugin> {
|
|
51
|
-
const plugin = new SileroVADPlugin();
|
|
52
|
-
await plugin.initialize(bus, { model_bytes: new Uint8Array(8), ...config });
|
|
53
|
-
return plugin;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function countKind(emitted: Array<{ kind: string }>, kind: string): number {
|
|
57
|
-
return emitted.filter((p) => p.kind === kind).length;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function metricValue(
|
|
61
|
-
emitted: Array<{ kind: string; name?: string; value?: string }>,
|
|
62
|
-
name: string,
|
|
63
|
-
): string | undefined {
|
|
64
|
-
return emitted.find((p) => p.kind === "metric.conversation" && p.name === name)?.value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
describe("SileroVADPlugin (Workers) — shared state machine parity", () => {
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
vi.useFakeTimers();
|
|
70
|
-
vi.setSystemTime(0);
|
|
71
|
-
mockControl.confidence = 0.9;
|
|
72
|
-
mockControl.stateCallArgs.length = 0;
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
afterEach(() => {
|
|
76
|
-
vi.useRealTimers();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
async function frame(plugin: SileroVADPlugin, confidence: number): Promise<void> {
|
|
80
|
-
mockControl.confidence = confidence;
|
|
81
|
-
await plugin.processAudio(makePcmFrame(), "ctx-w");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
it("absorbs single-frame confidence spikes during stopping so speech still ends", async () => {
|
|
85
|
-
const { bus, emitted } = makeBus();
|
|
86
|
-
const plugin = await initPlugin(bus);
|
|
87
|
-
|
|
88
|
-
await frame(plugin, 0.9);
|
|
89
|
-
expect(countKind(emitted, "vad.speech_started")).toBe(1);
|
|
90
|
-
|
|
91
|
-
for (const confidence of [0.2, 0.2, 0.2, 0.2, 0.9, 0.2, 0.2, 0.2, 0.2, 0.2]) {
|
|
92
|
-
await frame(plugin, confidence);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(1);
|
|
96
|
-
expect(metricValue(emitted, "vad.stop_hangover_ms")).toBeDefined();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("resets model state during prolonged continuous speech (saturation guard)", async () => {
|
|
100
|
-
const { bus, emitted } = makeBus();
|
|
101
|
-
const plugin = await initPlugin(bus);
|
|
102
|
-
|
|
103
|
-
await frame(plugin, 0.9);
|
|
104
|
-
vi.setSystemTime(13_000);
|
|
105
|
-
await frame(plugin, 0.9);
|
|
106
|
-
await frame(plugin, 0.9);
|
|
107
|
-
|
|
108
|
-
expect(metricValue(emitted, "vad.state_reset_in_speech")).toBe("1");
|
|
109
|
-
expect(mockControl.stateCallArgs[2]!.every((value) => value === 0)).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("two consecutive speech frames during stopping resume speaking", async () => {
|
|
113
|
-
const { bus, emitted } = makeBus();
|
|
114
|
-
const plugin = await initPlugin(bus);
|
|
115
|
-
|
|
116
|
-
await frame(plugin, 0.9);
|
|
117
|
-
for (const confidence of [0.2, 0.2, 0.2]) await frame(plugin, confidence);
|
|
118
|
-
for (const confidence of [0.9, 0.9]) await frame(plugin, confidence);
|
|
119
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(0);
|
|
120
|
-
|
|
121
|
-
for (let i = 0; i < 9; i += 1) await frame(plugin, 0.2);
|
|
122
|
-
expect(countKind(emitted, "vad.speech_ended")).toBe(1);
|
|
123
|
-
});
|
|
124
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"noImplicitReturns": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"outDir": "./dist",
|
|
14
|
-
"rootDir": "./src",
|
|
15
|
-
"esModuleInterop": true,
|
|
16
|
-
"skipLibCheck": true,
|
|
17
|
-
"forceConsistentCasingInFileNames": true
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*.ts"],
|
|
20
|
-
"exclude": ["node_modules", "dist"]
|
|
21
|
-
}
|