@guidekit/vad 0.1.0-beta.1
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 +56 -0
- package/dist/index.cjs +527 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +488 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @guidekit/vad
|
|
2
|
+
|
|
3
|
+
Voice Activity Detection package for the GuideKit SDK. Wraps the Silero VAD ONNX model to detect when a user is speaking, enabling half-duplex voice interactions with barge-in detection.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @guidekit/vad
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This package is an optional peer dependency of `@guidekit/core`. It is automatically used when voice mode is enabled.
|
|
12
|
+
|
|
13
|
+
## How It Works
|
|
14
|
+
|
|
15
|
+
`@guidekit/vad` loads the Silero VAD model via ONNX Runtime Web and runs inference on audio frames from the microphone. It emits `speech_start` and `speech_end` events that the core engine uses to trigger STT (Deepgram) and manage barge-in.
|
|
16
|
+
|
|
17
|
+
## Usage with GuideKit
|
|
18
|
+
|
|
19
|
+
When using `@guidekit/react` or `@guidekit/vanilla`, simply install this package and enable voice mode:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<GuideKitProvider
|
|
23
|
+
tokenEndpoint="/api/guidekit/token"
|
|
24
|
+
agent={{ name: 'Guide' }}
|
|
25
|
+
options={{ mode: 'voice' }}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</GuideKitProvider>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The SDK detects the package automatically. If it is not installed and voice mode is requested, the SDK emits a `VAD_PACKAGE_MISSING` error and falls back to text-only mode.
|
|
32
|
+
|
|
33
|
+
## Standalone Usage
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { createVAD } from '@guidekit/vad';
|
|
37
|
+
|
|
38
|
+
const vad = await createVAD({
|
|
39
|
+
onSpeechStart: () => console.log('Speaking...'),
|
|
40
|
+
onSpeechEnd: (audio) => console.log('Done, audio length:', audio.length),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Start processing audio from a MediaStream
|
|
44
|
+
await vad.start(mediaStream);
|
|
45
|
+
|
|
46
|
+
// Stop
|
|
47
|
+
vad.destroy();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
Full documentation: [guidekit.dev/docs/voice](https://guidekit.dev/docs/voice)
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
[MIT](../../LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
FRAME_SIZE: () => FRAME_SIZE,
|
|
34
|
+
SileroVAD: () => SileroVAD,
|
|
35
|
+
TARGET_SAMPLE_RATE: () => TARGET_SAMPLE_RATE,
|
|
36
|
+
VAD_VERSION: () => VAD_VERSION,
|
|
37
|
+
createVAD: () => createVAD
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
var ort = __toESM(require("onnxruntime-web"), 1);
|
|
41
|
+
var VAD_VERSION = "0.1.0";
|
|
42
|
+
var LOG_PREFIX = "[GuideKit:VAD]";
|
|
43
|
+
var DEFAULT_MODEL_URL = "https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.20/dist/silero_vad_v5.onnx";
|
|
44
|
+
var CACHE_NAME = `guidekit-vad-v${VAD_VERSION}`;
|
|
45
|
+
var CACHE_MODEL_KEY = "model.onnx";
|
|
46
|
+
var FRAME_SIZE = 512;
|
|
47
|
+
var TARGET_SAMPLE_RATE = 16e3;
|
|
48
|
+
var CALIBRATION_DURATION_MS = 500;
|
|
49
|
+
var STATE_SIZE = 128;
|
|
50
|
+
async function loadModelFromCache() {
|
|
51
|
+
if (typeof caches === "undefined") return null;
|
|
52
|
+
try {
|
|
53
|
+
const cache = await caches.open(CACHE_NAME);
|
|
54
|
+
const response = await cache.match(CACHE_MODEL_KEY);
|
|
55
|
+
return response ? response.arrayBuffer() : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function saveModelToCache(data) {
|
|
61
|
+
if (typeof caches === "undefined") return;
|
|
62
|
+
try {
|
|
63
|
+
const cache = await caches.open(CACHE_NAME);
|
|
64
|
+
await cache.put(CACHE_MODEL_KEY, new Response(data));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function resample(input, inputRate, outputRate) {
|
|
69
|
+
if (inputRate === outputRate) return input;
|
|
70
|
+
const ratio = inputRate / outputRate;
|
|
71
|
+
const outputLength = Math.round(input.length / ratio);
|
|
72
|
+
const output = new Float32Array(outputLength);
|
|
73
|
+
for (let i = 0; i < outputLength; i++) {
|
|
74
|
+
const srcIndex = i * ratio;
|
|
75
|
+
const srcFloor = Math.floor(srcIndex);
|
|
76
|
+
const srcCeil = Math.min(srcFloor + 1, input.length - 1);
|
|
77
|
+
const frac = srcIndex - srcFloor;
|
|
78
|
+
output[i] = input[srcFloor] * (1 - frac) + input[srcCeil] * frac;
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
var SileroVAD = class {
|
|
83
|
+
// Options (resolved with defaults)
|
|
84
|
+
_threshold;
|
|
85
|
+
_minSpeechDurationMs;
|
|
86
|
+
_silenceDurationMs;
|
|
87
|
+
_sampleRate;
|
|
88
|
+
_debug;
|
|
89
|
+
_modelUrl;
|
|
90
|
+
// ONNX Runtime state
|
|
91
|
+
_session = null;
|
|
92
|
+
_h = null;
|
|
93
|
+
_c = null;
|
|
94
|
+
// Audio pipeline
|
|
95
|
+
_audioContext = null;
|
|
96
|
+
_ownsAudioContext = false;
|
|
97
|
+
_sourceNode = null;
|
|
98
|
+
_workletNode = null;
|
|
99
|
+
_stream = null;
|
|
100
|
+
// Frame buffer for accumulating resampled samples into FRAME_SIZE chunks
|
|
101
|
+
_frameBuffer = new Float32Array(0);
|
|
102
|
+
_frameBufferOffset = 0;
|
|
103
|
+
// State tracking
|
|
104
|
+
_isReady = false;
|
|
105
|
+
_isSpeaking = false;
|
|
106
|
+
_isStarted = false;
|
|
107
|
+
_isDestroyed = false;
|
|
108
|
+
// Duration tracking (in frames)
|
|
109
|
+
_consecutiveSpeechFrames = 0;
|
|
110
|
+
_consecutiveSilenceFrames = 0;
|
|
111
|
+
_frameDurationMs;
|
|
112
|
+
_minSpeechFrames;
|
|
113
|
+
_silenceFrames;
|
|
114
|
+
// Noise floor calibration
|
|
115
|
+
_isCalibrating = false;
|
|
116
|
+
_calibrationSamples = [];
|
|
117
|
+
_calibrationFramesNeeded = 0;
|
|
118
|
+
_calibratedThreshold;
|
|
119
|
+
// Event listeners
|
|
120
|
+
_listeners = /* @__PURE__ */ new Map();
|
|
121
|
+
// Processing lock to serialise frame inference
|
|
122
|
+
_processingPromise = Promise.resolve();
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this._threshold = options?.threshold ?? 0.5;
|
|
125
|
+
this._minSpeechDurationMs = options?.minSpeechDurationMs ?? 300;
|
|
126
|
+
this._silenceDurationMs = options?.silenceDurationMs ?? 500;
|
|
127
|
+
this._sampleRate = options?.sampleRate ?? TARGET_SAMPLE_RATE;
|
|
128
|
+
this._debug = options?.debug ?? false;
|
|
129
|
+
this._modelUrl = options?.modelUrl ?? DEFAULT_MODEL_URL;
|
|
130
|
+
this._calibratedThreshold = this._threshold;
|
|
131
|
+
this._frameDurationMs = FRAME_SIZE / this._sampleRate * 1e3;
|
|
132
|
+
this._minSpeechFrames = Math.ceil(this._minSpeechDurationMs / this._frameDurationMs);
|
|
133
|
+
this._silenceFrames = Math.ceil(this._silenceDurationMs / this._frameDurationMs);
|
|
134
|
+
this._log("Created with options", {
|
|
135
|
+
threshold: this._threshold,
|
|
136
|
+
minSpeechDurationMs: this._minSpeechDurationMs,
|
|
137
|
+
silenceDurationMs: this._silenceDurationMs,
|
|
138
|
+
sampleRate: this._sampleRate,
|
|
139
|
+
modelUrl: this._modelUrl
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
// Public API
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
/** Load the ONNX model. Uses Cache API for persistence across sessions. */
|
|
146
|
+
async init() {
|
|
147
|
+
if (this._isDestroyed) {
|
|
148
|
+
throw new Error(`${LOG_PREFIX} Cannot init after destroy`);
|
|
149
|
+
}
|
|
150
|
+
if (this._isReady) {
|
|
151
|
+
this._log("Already initialised \u2014 skipping");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this._log("Initialising...");
|
|
155
|
+
let modelBuffer = await loadModelFromCache();
|
|
156
|
+
if (modelBuffer) {
|
|
157
|
+
this._log("Loaded model from Cache API");
|
|
158
|
+
} else {
|
|
159
|
+
this._log("Fetching model from", this._modelUrl);
|
|
160
|
+
const response = await fetch(this._modelUrl);
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${LOG_PREFIX} Failed to fetch model: ${response.status} ${response.statusText}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
modelBuffer = await response.arrayBuffer();
|
|
167
|
+
this._log("Model fetched, size:", modelBuffer.byteLength, "bytes");
|
|
168
|
+
await saveModelToCache(modelBuffer);
|
|
169
|
+
this._log("Model saved to Cache API");
|
|
170
|
+
}
|
|
171
|
+
this._session = await ort.InferenceSession.create(modelBuffer, {
|
|
172
|
+
executionProviders: ["wasm"],
|
|
173
|
+
graphOptimizationLevel: "all"
|
|
174
|
+
});
|
|
175
|
+
this._resetStates();
|
|
176
|
+
this._isReady = true;
|
|
177
|
+
this._log("Model loaded and ready");
|
|
178
|
+
this._emit({
|
|
179
|
+
type: "vad-ready",
|
|
180
|
+
timestamp: Date.now()
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Process a single audio frame (512 samples at 16 kHz).
|
|
185
|
+
* Returns the speech probability (0-1).
|
|
186
|
+
*/
|
|
187
|
+
async processFrame(audioData) {
|
|
188
|
+
if (!this._session) {
|
|
189
|
+
throw new Error(`${LOG_PREFIX} Model not loaded. Call init() first.`);
|
|
190
|
+
}
|
|
191
|
+
if (audioData.length !== FRAME_SIZE) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`${LOG_PREFIX} Expected ${FRAME_SIZE} samples, got ${audioData.length}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const inputTensor = new ort.Tensor("float32", audioData, [1, FRAME_SIZE]);
|
|
197
|
+
const srTensor = new ort.Tensor("int64", BigInt64Array.from([BigInt(this._sampleRate)]), [1]);
|
|
198
|
+
const feeds = {
|
|
199
|
+
input: inputTensor,
|
|
200
|
+
sr: srTensor,
|
|
201
|
+
h: this._h,
|
|
202
|
+
c: this._c
|
|
203
|
+
};
|
|
204
|
+
const results = await this._session.run(feeds);
|
|
205
|
+
this._h = results["hn"];
|
|
206
|
+
this._c = results["cn"];
|
|
207
|
+
const probability = results["output"].data[0];
|
|
208
|
+
return probability;
|
|
209
|
+
}
|
|
210
|
+
/** Start VAD processing on a MediaStream (typically from getUserMedia). */
|
|
211
|
+
start(stream) {
|
|
212
|
+
if (this._isDestroyed) {
|
|
213
|
+
throw new Error(`${LOG_PREFIX} Cannot start after destroy`);
|
|
214
|
+
}
|
|
215
|
+
if (!this._isReady) {
|
|
216
|
+
throw new Error(`${LOG_PREFIX} Model not loaded. Call init() first.`);
|
|
217
|
+
}
|
|
218
|
+
if (this._isStarted) {
|
|
219
|
+
this._log("Already started \u2014 stopping previous session first");
|
|
220
|
+
this.stop();
|
|
221
|
+
}
|
|
222
|
+
this._log("Starting VAD on MediaStream");
|
|
223
|
+
this._stream = stream;
|
|
224
|
+
this._isStarted = true;
|
|
225
|
+
this._isSpeaking = false;
|
|
226
|
+
this._consecutiveSpeechFrames = 0;
|
|
227
|
+
this._consecutiveSilenceFrames = 0;
|
|
228
|
+
this._frameBuffer = new Float32Array(FRAME_SIZE);
|
|
229
|
+
this._frameBufferOffset = 0;
|
|
230
|
+
this._resetStates();
|
|
231
|
+
this._isCalibrating = true;
|
|
232
|
+
this._calibrationSamples = [];
|
|
233
|
+
this._calibrationFramesNeeded = Math.ceil(
|
|
234
|
+
CALIBRATION_DURATION_MS / 1e3 * this._sampleRate / FRAME_SIZE
|
|
235
|
+
);
|
|
236
|
+
this._log("Calibrating noise floor for", this._calibrationFramesNeeded, "frames");
|
|
237
|
+
this._setupAudioPipeline(stream);
|
|
238
|
+
}
|
|
239
|
+
/** Stop VAD processing and release audio resources (but keep the model). */
|
|
240
|
+
stop() {
|
|
241
|
+
if (!this._isStarted) return;
|
|
242
|
+
this._log("Stopping VAD");
|
|
243
|
+
this._teardownAudioPipeline();
|
|
244
|
+
if (this._isSpeaking) {
|
|
245
|
+
this._isSpeaking = false;
|
|
246
|
+
this._emit({
|
|
247
|
+
type: "speech-end",
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
probability: 0
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
this._isStarted = false;
|
|
253
|
+
this._isSpeaking = false;
|
|
254
|
+
this._consecutiveSpeechFrames = 0;
|
|
255
|
+
this._consecutiveSilenceFrames = 0;
|
|
256
|
+
this._frameBufferOffset = 0;
|
|
257
|
+
this._isCalibrating = false;
|
|
258
|
+
this._calibrationSamples = [];
|
|
259
|
+
this._processingPromise = Promise.resolve();
|
|
260
|
+
this._log("VAD stopped");
|
|
261
|
+
}
|
|
262
|
+
/** Register a callback for speech-start events. Returns an unsubscribe function. */
|
|
263
|
+
onSpeechStart(callback) {
|
|
264
|
+
return this._on("speech-start", callback);
|
|
265
|
+
}
|
|
266
|
+
/** Register a callback for speech-end events. Returns an unsubscribe function. */
|
|
267
|
+
onSpeechEnd(callback) {
|
|
268
|
+
return this._on("speech-end", callback);
|
|
269
|
+
}
|
|
270
|
+
/** Register a callback for vad-ready events. Returns an unsubscribe function. */
|
|
271
|
+
onReady(callback) {
|
|
272
|
+
return this._on("vad-ready", callback);
|
|
273
|
+
}
|
|
274
|
+
/** Whether the ONNX model is loaded and ready. */
|
|
275
|
+
get isReady() {
|
|
276
|
+
return this._isReady;
|
|
277
|
+
}
|
|
278
|
+
/** Whether speech is currently detected. */
|
|
279
|
+
get isSpeaking() {
|
|
280
|
+
return this._isSpeaking;
|
|
281
|
+
}
|
|
282
|
+
/** The MediaStream currently being processed, or null. */
|
|
283
|
+
get stream() {
|
|
284
|
+
return this._stream;
|
|
285
|
+
}
|
|
286
|
+
/** Release ONNX model session and all audio resources. */
|
|
287
|
+
async destroy() {
|
|
288
|
+
if (this._isDestroyed) return;
|
|
289
|
+
this._log("Destroying...");
|
|
290
|
+
this.stop();
|
|
291
|
+
if (this._session) {
|
|
292
|
+
await this._session.release();
|
|
293
|
+
this._session = null;
|
|
294
|
+
}
|
|
295
|
+
this._h?.dispose();
|
|
296
|
+
this._c?.dispose();
|
|
297
|
+
this._h = null;
|
|
298
|
+
this._c = null;
|
|
299
|
+
this._isReady = false;
|
|
300
|
+
this._isDestroyed = true;
|
|
301
|
+
this._listeners.clear();
|
|
302
|
+
this._log("Destroyed");
|
|
303
|
+
}
|
|
304
|
+
// -------------------------------------------------------------------------
|
|
305
|
+
// Private: Event system
|
|
306
|
+
// -------------------------------------------------------------------------
|
|
307
|
+
_on(type, callback) {
|
|
308
|
+
let set = this._listeners.get(type);
|
|
309
|
+
if (!set) {
|
|
310
|
+
set = /* @__PURE__ */ new Set();
|
|
311
|
+
this._listeners.set(type, set);
|
|
312
|
+
}
|
|
313
|
+
set.add(callback);
|
|
314
|
+
return () => {
|
|
315
|
+
set.delete(callback);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
_emit(event) {
|
|
319
|
+
const set = this._listeners.get(event.type);
|
|
320
|
+
if (!set) return;
|
|
321
|
+
for (const cb of set) {
|
|
322
|
+
try {
|
|
323
|
+
cb(event);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.error(`${LOG_PREFIX} Error in ${event.type} callback:`, err);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// -------------------------------------------------------------------------
|
|
330
|
+
// Private: Audio pipeline
|
|
331
|
+
// -------------------------------------------------------------------------
|
|
332
|
+
_setupAudioPipeline(stream) {
|
|
333
|
+
const tracks = stream.getAudioTracks();
|
|
334
|
+
const trackSettings = tracks[0]?.getSettings();
|
|
335
|
+
const inputSampleRate = trackSettings?.sampleRate ?? 48e3;
|
|
336
|
+
this._log("Input sample rate:", inputSampleRate);
|
|
337
|
+
if (typeof AudioContext === "undefined" && typeof webkitAudioContext === "undefined") {
|
|
338
|
+
throw new Error(`${LOG_PREFIX} AudioContext is not available in this environment`);
|
|
339
|
+
}
|
|
340
|
+
const AudioContextClass = typeof AudioContext !== "undefined" ? AudioContext : (
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
342
|
+
globalThis.webkitAudioContext
|
|
343
|
+
);
|
|
344
|
+
this._audioContext = new AudioContextClass({ sampleRate: inputSampleRate });
|
|
345
|
+
this._ownsAudioContext = true;
|
|
346
|
+
this._sourceNode = this._audioContext.createMediaStreamSource(stream);
|
|
347
|
+
this._setupScriptProcessor(inputSampleRate);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* ScriptProcessorNode fallback (works everywhere, including Safari).
|
|
351
|
+
* We use a buffer size of 4096 which gives ~85 ms of audio at 48 kHz.
|
|
352
|
+
*/
|
|
353
|
+
_setupScriptProcessor(inputSampleRate) {
|
|
354
|
+
if (!this._audioContext || !this._sourceNode) return;
|
|
355
|
+
const bufferSize = 4096;
|
|
356
|
+
const processor = this._audioContext.createScriptProcessor(bufferSize, 1, 1);
|
|
357
|
+
processor.onaudioprocess = (event) => {
|
|
358
|
+
if (!this._isStarted) return;
|
|
359
|
+
const inputData = event.inputBuffer.getChannelData(0);
|
|
360
|
+
const resampled = inputSampleRate !== this._sampleRate ? resample(inputData, inputSampleRate, this._sampleRate) : new Float32Array(inputData);
|
|
361
|
+
this._feedAudio(resampled);
|
|
362
|
+
};
|
|
363
|
+
this._sourceNode.connect(processor);
|
|
364
|
+
processor.connect(this._audioContext.destination);
|
|
365
|
+
this._workletNode = processor;
|
|
366
|
+
this._log("Audio pipeline set up (ScriptProcessorNode)");
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Accumulate resampled audio into FRAME_SIZE chunks and process each full frame.
|
|
370
|
+
*/
|
|
371
|
+
_feedAudio(samples) {
|
|
372
|
+
let offset = 0;
|
|
373
|
+
while (offset < samples.length) {
|
|
374
|
+
const remaining = FRAME_SIZE - this._frameBufferOffset;
|
|
375
|
+
const available = samples.length - offset;
|
|
376
|
+
const toCopy = Math.min(remaining, available);
|
|
377
|
+
this._frameBuffer.set(
|
|
378
|
+
samples.subarray(offset, offset + toCopy),
|
|
379
|
+
this._frameBufferOffset
|
|
380
|
+
);
|
|
381
|
+
this._frameBufferOffset += toCopy;
|
|
382
|
+
offset += toCopy;
|
|
383
|
+
if (this._frameBufferOffset === FRAME_SIZE) {
|
|
384
|
+
const frame = new Float32Array(this._frameBuffer);
|
|
385
|
+
this._frameBufferOffset = 0;
|
|
386
|
+
this._processingPromise = this._processingPromise.then(
|
|
387
|
+
() => this._handleFrame(frame)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Process a single FRAME_SIZE frame: run inference and update speech state.
|
|
394
|
+
*/
|
|
395
|
+
async _handleFrame(frame) {
|
|
396
|
+
if (!this._isStarted || !this._session) return;
|
|
397
|
+
let probability;
|
|
398
|
+
try {
|
|
399
|
+
probability = await this.processFrame(frame);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (this._debug) {
|
|
402
|
+
console.error(`${LOG_PREFIX} Inference error:`, err);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this._isCalibrating) {
|
|
407
|
+
this._calibrationSamples.push(probability);
|
|
408
|
+
if (this._calibrationSamples.length >= this._calibrationFramesNeeded) {
|
|
409
|
+
this._finishCalibration();
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const isSpeechFrame = probability >= this._calibratedThreshold;
|
|
414
|
+
if (isSpeechFrame) {
|
|
415
|
+
this._consecutiveSpeechFrames++;
|
|
416
|
+
this._consecutiveSilenceFrames = 0;
|
|
417
|
+
if (!this._isSpeaking && this._consecutiveSpeechFrames >= this._minSpeechFrames) {
|
|
418
|
+
this._isSpeaking = true;
|
|
419
|
+
this._log("Speech started, probability:", probability.toFixed(3));
|
|
420
|
+
this._emit({
|
|
421
|
+
type: "speech-start",
|
|
422
|
+
timestamp: Date.now(),
|
|
423
|
+
probability
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
this._consecutiveSilenceFrames++;
|
|
428
|
+
if (this._isSpeaking && this._consecutiveSilenceFrames >= this._silenceFrames) {
|
|
429
|
+
this._isSpeaking = false;
|
|
430
|
+
this._consecutiveSpeechFrames = 0;
|
|
431
|
+
this._log("Speech ended, probability:", probability.toFixed(3));
|
|
432
|
+
this._emit({
|
|
433
|
+
type: "speech-end",
|
|
434
|
+
timestamp: Date.now(),
|
|
435
|
+
probability
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
_finishCalibration() {
|
|
441
|
+
if (this._calibrationSamples.length === 0) {
|
|
442
|
+
this._isCalibrating = false;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const sum = this._calibrationSamples.reduce((a, b) => a + b, 0);
|
|
446
|
+
const avgNoise = sum / this._calibrationSamples.length;
|
|
447
|
+
const NOISE_MARGIN = 0.15;
|
|
448
|
+
if (avgNoise + NOISE_MARGIN > this._threshold) {
|
|
449
|
+
this._calibratedThreshold = Math.min(avgNoise + NOISE_MARGIN, 0.95);
|
|
450
|
+
this._log(
|
|
451
|
+
"Noise floor is high. Adjusted threshold from",
|
|
452
|
+
this._threshold.toFixed(3),
|
|
453
|
+
"to",
|
|
454
|
+
this._calibratedThreshold.toFixed(3),
|
|
455
|
+
"(avg noise:",
|
|
456
|
+
avgNoise.toFixed(3) + ")"
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
this._calibratedThreshold = this._threshold;
|
|
460
|
+
this._log("Noise floor OK, avg:", avgNoise.toFixed(3), "\u2014 keeping threshold at", this._threshold.toFixed(3));
|
|
461
|
+
}
|
|
462
|
+
this._minSpeechFrames = Math.ceil(this._minSpeechDurationMs / this._frameDurationMs);
|
|
463
|
+
this._silenceFrames = Math.ceil(this._silenceDurationMs / this._frameDurationMs);
|
|
464
|
+
this._isCalibrating = false;
|
|
465
|
+
this._calibrationSamples = [];
|
|
466
|
+
}
|
|
467
|
+
_teardownAudioPipeline() {
|
|
468
|
+
if (this._workletNode) {
|
|
469
|
+
try {
|
|
470
|
+
this._workletNode.disconnect();
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
if ("onaudioprocess" in this._workletNode) {
|
|
474
|
+
this._workletNode.onaudioprocess = null;
|
|
475
|
+
}
|
|
476
|
+
this._workletNode = null;
|
|
477
|
+
}
|
|
478
|
+
if (this._sourceNode) {
|
|
479
|
+
try {
|
|
480
|
+
this._sourceNode.disconnect();
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
this._sourceNode = null;
|
|
484
|
+
}
|
|
485
|
+
if (this._audioContext && this._ownsAudioContext) {
|
|
486
|
+
try {
|
|
487
|
+
void this._audioContext.close();
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
this._audioContext = null;
|
|
491
|
+
this._ownsAudioContext = false;
|
|
492
|
+
}
|
|
493
|
+
this._stream = null;
|
|
494
|
+
}
|
|
495
|
+
// -------------------------------------------------------------------------
|
|
496
|
+
// Private: ONNX state helpers
|
|
497
|
+
// -------------------------------------------------------------------------
|
|
498
|
+
/** Reset the LSTM hidden and cell states to zeros. */
|
|
499
|
+
_resetStates() {
|
|
500
|
+
this._h?.dispose();
|
|
501
|
+
this._c?.dispose();
|
|
502
|
+
const zeros = new Float32Array(2 * STATE_SIZE).fill(0);
|
|
503
|
+
this._h = new ort.Tensor("float32", zeros.slice(0, STATE_SIZE), [2, 1, 64]);
|
|
504
|
+
this._c = new ort.Tensor("float32", zeros.slice(STATE_SIZE), [2, 1, 64]);
|
|
505
|
+
}
|
|
506
|
+
// -------------------------------------------------------------------------
|
|
507
|
+
// Private: Logging
|
|
508
|
+
// -------------------------------------------------------------------------
|
|
509
|
+
_log(...args) {
|
|
510
|
+
if (!this._debug) return;
|
|
511
|
+
console.log(LOG_PREFIX, ...args);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
async function createVAD(options) {
|
|
515
|
+
const vad = new SileroVAD(options);
|
|
516
|
+
await vad.init();
|
|
517
|
+
return vad;
|
|
518
|
+
}
|
|
519
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
520
|
+
0 && (module.exports = {
|
|
521
|
+
FRAME_SIZE,
|
|
522
|
+
SileroVAD,
|
|
523
|
+
TARGET_SAMPLE_RATE,
|
|
524
|
+
VAD_VERSION,
|
|
525
|
+
createVAD
|
|
526
|
+
});
|
|
527
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @guidekit/vad — Silero VAD ONNX model wrapper for voice activity detection\nimport * as ort from 'onnxruntime-web';\n\nexport const VAD_VERSION = '0.1.0';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst LOG_PREFIX = '[GuideKit:VAD]';\n\n/** Default CDN URL for the Silero VAD ONNX model (v5). */\nconst DEFAULT_MODEL_URL =\n 'https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.20/dist/silero_vad_v5.onnx';\n\n/** Cache API key used for persisting the downloaded ONNX model. */\nconst CACHE_NAME = `guidekit-vad-v${VAD_VERSION}`;\nconst CACHE_MODEL_KEY = 'model.onnx';\n\n/** Silero VAD frame size: 512 samples at 16 kHz = 32 ms per frame. */\nconst FRAME_SIZE = 512;\n\n/** Target sample rate for VAD processing. */\nconst TARGET_SAMPLE_RATE = 16000;\n\n/** Duration (in ms) of audio collected for noise floor calibration. */\nconst CALIBRATION_DURATION_MS = 500;\n\n/** Hidden/cell state size for Silero VAD v5 LSTM. */\nconst STATE_SIZE = 128;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface VADOptions {\n /** Speech probability threshold (0-1). Default: 0.5 */\n threshold?: number;\n /** Minimum speech duration in ms to trigger start. Default: 300 */\n minSpeechDurationMs?: number;\n /** Silence duration in ms after speech to trigger end. Default: 500 */\n silenceDurationMs?: number;\n /** Sample rate. Default: 16000 */\n sampleRate?: number;\n /** Enable debug logging. Default: false */\n debug?: boolean;\n /** Custom URL for the Silero VAD ONNX model file. */\n modelUrl?: string;\n}\n\nexport interface VADEvent {\n type: 'speech-start' | 'speech-end' | 'vad-ready';\n timestamp: number;\n /** Speech probability (0-1) at the moment of the event. */\n probability?: number;\n}\n\ntype VADEventType = VADEvent['type'];\ntype VADCallback = (event: VADEvent) => void;\n\n// ---------------------------------------------------------------------------\n// Utility: Cache API helpers\n// ---------------------------------------------------------------------------\n\nasync function loadModelFromCache(): Promise<ArrayBuffer | null> {\n if (typeof caches === 'undefined') return null;\n try {\n const cache = await caches.open(CACHE_NAME);\n const response = await cache.match(CACHE_MODEL_KEY);\n return response ? response.arrayBuffer() : null;\n } catch {\n // Cache API may be unavailable in certain contexts (e.g. opaque origins).\n return null;\n }\n}\n\nasync function saveModelToCache(data: ArrayBuffer): Promise<void> {\n if (typeof caches === 'undefined') return;\n try {\n const cache = await caches.open(CACHE_NAME);\n await cache.put(CACHE_MODEL_KEY, new Response(data));\n } catch {\n // Silently ignore cache write failures.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Utility: Resampler\n// ---------------------------------------------------------------------------\n\n/**\n * Simple linear-interpolation resampler from `inputRate` to `outputRate`.\n * Adequate for VAD where perceptual audio quality is irrelevant.\n */\nfunction resample(\n input: Float32Array,\n inputRate: number,\n outputRate: number,\n): Float32Array {\n if (inputRate === outputRate) return input;\n const ratio = inputRate / outputRate;\n const outputLength = Math.round(input.length / ratio);\n const output = new Float32Array(outputLength);\n for (let i = 0; i < outputLength; i++) {\n const srcIndex = i * ratio;\n const srcFloor = Math.floor(srcIndex);\n const srcCeil = Math.min(srcFloor + 1, input.length - 1);\n const frac = srcIndex - srcFloor;\n output[i] = (input[srcFloor] as number) * (1 - frac) + (input[srcCeil] as number) * frac;\n }\n return output;\n}\n\n// ---------------------------------------------------------------------------\n// SileroVAD\n// ---------------------------------------------------------------------------\n\nexport class SileroVAD {\n // Options (resolved with defaults)\n private readonly _threshold: number;\n private readonly _minSpeechDurationMs: number;\n private readonly _silenceDurationMs: number;\n private readonly _sampleRate: number;\n private readonly _debug: boolean;\n private readonly _modelUrl: string;\n\n // ONNX Runtime state\n private _session: ort.InferenceSession | null = null;\n private _h: ort.Tensor | null = null;\n private _c: ort.Tensor | null = null;\n\n // Audio pipeline\n private _audioContext: AudioContext | null = null;\n private _ownsAudioContext = false;\n private _sourceNode: MediaStreamAudioSourceNode | null = null;\n private _workletNode: AudioWorkletNode | ScriptProcessorNode | null = null;\n private _stream: MediaStream | null = null;\n\n // Frame buffer for accumulating resampled samples into FRAME_SIZE chunks\n private _frameBuffer: Float32Array = new Float32Array(0);\n private _frameBufferOffset = 0;\n\n // State tracking\n private _isReady = false;\n private _isSpeaking = false;\n private _isStarted = false;\n private _isDestroyed = false;\n\n // Duration tracking (in frames)\n private _consecutiveSpeechFrames = 0;\n private _consecutiveSilenceFrames = 0;\n private _frameDurationMs: number;\n private _minSpeechFrames: number;\n private _silenceFrames: number;\n\n // Noise floor calibration\n private _isCalibrating = false;\n private _calibrationSamples: number[] = [];\n private _calibrationFramesNeeded = 0;\n private _calibratedThreshold: number;\n\n // Event listeners\n private _listeners: Map<VADEventType, Set<VADCallback>> = new Map();\n\n // Processing lock to serialise frame inference\n private _processingPromise: Promise<void> = Promise.resolve();\n\n constructor(options?: VADOptions) {\n this._threshold = options?.threshold ?? 0.5;\n this._minSpeechDurationMs = options?.minSpeechDurationMs ?? 300;\n this._silenceDurationMs = options?.silenceDurationMs ?? 500;\n this._sampleRate = options?.sampleRate ?? TARGET_SAMPLE_RATE;\n this._debug = options?.debug ?? false;\n this._modelUrl = options?.modelUrl ?? DEFAULT_MODEL_URL;\n this._calibratedThreshold = this._threshold;\n\n // Pre-compute frame-duration-based counters\n this._frameDurationMs = (FRAME_SIZE / this._sampleRate) * 1000;\n this._minSpeechFrames = Math.ceil(this._minSpeechDurationMs / this._frameDurationMs);\n this._silenceFrames = Math.ceil(this._silenceDurationMs / this._frameDurationMs);\n\n this._log('Created with options', {\n threshold: this._threshold,\n minSpeechDurationMs: this._minSpeechDurationMs,\n silenceDurationMs: this._silenceDurationMs,\n sampleRate: this._sampleRate,\n modelUrl: this._modelUrl,\n });\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n /** Load the ONNX model. Uses Cache API for persistence across sessions. */\n async init(): Promise<void> {\n if (this._isDestroyed) {\n throw new Error(`${LOG_PREFIX} Cannot init after destroy`);\n }\n if (this._isReady) {\n this._log('Already initialised — skipping');\n return;\n }\n\n this._log('Initialising...');\n\n // 1. Attempt to load model bytes from cache, falling back to network.\n let modelBuffer = await loadModelFromCache();\n if (modelBuffer) {\n this._log('Loaded model from Cache API');\n } else {\n this._log('Fetching model from', this._modelUrl);\n const response = await fetch(this._modelUrl);\n if (!response.ok) {\n throw new Error(\n `${LOG_PREFIX} Failed to fetch model: ${response.status} ${response.statusText}`,\n );\n }\n modelBuffer = await response.arrayBuffer();\n this._log('Model fetched, size:', modelBuffer.byteLength, 'bytes');\n\n // Persist to Cache API for next time.\n await saveModelToCache(modelBuffer);\n this._log('Model saved to Cache API');\n }\n\n // 2. Create ONNX InferenceSession.\n this._session = await ort.InferenceSession.create(modelBuffer, {\n executionProviders: ['wasm'],\n graphOptimizationLevel: 'all',\n });\n\n // 3. Initialise LSTM hidden/cell state tensors (zeros).\n this._resetStates();\n\n this._isReady = true;\n this._log('Model loaded and ready');\n\n this._emit({\n type: 'vad-ready',\n timestamp: Date.now(),\n });\n }\n\n /**\n * Process a single audio frame (512 samples at 16 kHz).\n * Returns the speech probability (0-1).\n */\n async processFrame(audioData: Float32Array): Promise<number> {\n if (!this._session) {\n throw new Error(`${LOG_PREFIX} Model not loaded. Call init() first.`);\n }\n if (audioData.length !== FRAME_SIZE) {\n throw new Error(\n `${LOG_PREFIX} Expected ${FRAME_SIZE} samples, got ${audioData.length}`,\n );\n }\n\n const inputTensor = new ort.Tensor('float32', audioData, [1, FRAME_SIZE]);\n const srTensor = new ort.Tensor('int64', BigInt64Array.from([BigInt(this._sampleRate)]), [1]);\n\n const feeds: Record<string, ort.Tensor> = {\n input: inputTensor,\n sr: srTensor,\n h: this._h!,\n c: this._c!,\n };\n\n const results = await this._session.run(feeds);\n\n // Update LSTM hidden/cell states for the next frame.\n this._h = results['hn'] as ort.Tensor;\n this._c = results['cn'] as ort.Tensor;\n\n const probability = (results['output'] as ort.Tensor).data[0] as number;\n return probability;\n }\n\n /** Start VAD processing on a MediaStream (typically from getUserMedia). */\n start(stream: MediaStream): void {\n if (this._isDestroyed) {\n throw new Error(`${LOG_PREFIX} Cannot start after destroy`);\n }\n if (!this._isReady) {\n throw new Error(`${LOG_PREFIX} Model not loaded. Call init() first.`);\n }\n if (this._isStarted) {\n this._log('Already started — stopping previous session first');\n this.stop();\n }\n\n this._log('Starting VAD on MediaStream');\n this._stream = stream;\n this._isStarted = true;\n\n // Reset speech tracking state.\n this._isSpeaking = false;\n this._consecutiveSpeechFrames = 0;\n this._consecutiveSilenceFrames = 0;\n this._frameBuffer = new Float32Array(FRAME_SIZE);\n this._frameBufferOffset = 0;\n\n // Reset LSTM states for a fresh stream.\n this._resetStates();\n\n // Begin noise floor calibration.\n this._isCalibrating = true;\n this._calibrationSamples = [];\n this._calibrationFramesNeeded = Math.ceil(\n (CALIBRATION_DURATION_MS / 1000) * this._sampleRate / FRAME_SIZE,\n );\n this._log('Calibrating noise floor for', this._calibrationFramesNeeded, 'frames');\n\n // Build the audio processing pipeline.\n this._setupAudioPipeline(stream);\n }\n\n /** Stop VAD processing and release audio resources (but keep the model). */\n stop(): void {\n if (!this._isStarted) return;\n\n this._log('Stopping VAD');\n\n // Tear down audio nodes.\n this._teardownAudioPipeline();\n\n // If we were speaking, emit speech-end.\n if (this._isSpeaking) {\n this._isSpeaking = false;\n this._emit({\n type: 'speech-end',\n timestamp: Date.now(),\n probability: 0,\n });\n }\n\n // Reset state.\n this._isStarted = false;\n this._isSpeaking = false;\n this._consecutiveSpeechFrames = 0;\n this._consecutiveSilenceFrames = 0;\n this._frameBufferOffset = 0;\n this._isCalibrating = false;\n this._calibrationSamples = [];\n this._processingPromise = Promise.resolve();\n\n this._log('VAD stopped');\n }\n\n /** Register a callback for speech-start events. Returns an unsubscribe function. */\n onSpeechStart(callback: VADCallback): () => void {\n return this._on('speech-start', callback);\n }\n\n /** Register a callback for speech-end events. Returns an unsubscribe function. */\n onSpeechEnd(callback: VADCallback): () => void {\n return this._on('speech-end', callback);\n }\n\n /** Register a callback for vad-ready events. Returns an unsubscribe function. */\n onReady(callback: VADCallback): () => void {\n return this._on('vad-ready', callback);\n }\n\n /** Whether the ONNX model is loaded and ready. */\n get isReady(): boolean {\n return this._isReady;\n }\n\n /** Whether speech is currently detected. */\n get isSpeaking(): boolean {\n return this._isSpeaking;\n }\n\n /** The MediaStream currently being processed, or null. */\n get stream(): MediaStream | null {\n return this._stream;\n }\n\n /** Release ONNX model session and all audio resources. */\n async destroy(): Promise<void> {\n if (this._isDestroyed) return;\n this._log('Destroying...');\n\n this.stop();\n\n if (this._session) {\n await this._session.release();\n this._session = null;\n }\n\n // Dispose tensors.\n this._h?.dispose();\n this._c?.dispose();\n this._h = null;\n this._c = null;\n\n this._isReady = false;\n this._isDestroyed = true;\n this._listeners.clear();\n\n this._log('Destroyed');\n }\n\n // -------------------------------------------------------------------------\n // Private: Event system\n // -------------------------------------------------------------------------\n\n private _on(type: VADEventType, callback: VADCallback): () => void {\n let set = this._listeners.get(type);\n if (!set) {\n set = new Set();\n this._listeners.set(type, set);\n }\n set.add(callback);\n return () => {\n set!.delete(callback);\n };\n }\n\n private _emit(event: VADEvent): void {\n const set = this._listeners.get(event.type);\n if (!set) return;\n for (const cb of set) {\n try {\n cb(event);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error(`${LOG_PREFIX} Error in ${event.type} callback:`, err);\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Private: Audio pipeline\n // -------------------------------------------------------------------------\n\n private _setupAudioPipeline(stream: MediaStream): void {\n // Determine the incoming sample rate.\n const tracks = stream.getAudioTracks();\n const trackSettings = tracks[0]?.getSettings();\n const inputSampleRate = trackSettings?.sampleRate ?? 48000;\n\n this._log('Input sample rate:', inputSampleRate);\n\n // Create AudioContext at the input sample rate so we don't double-resample.\n // SSR guard: AudioContext may not exist.\n if (typeof AudioContext === 'undefined' && typeof webkitAudioContext === 'undefined') {\n throw new Error(`${LOG_PREFIX} AudioContext is not available in this environment`);\n }\n\n const AudioContextClass =\n typeof AudioContext !== 'undefined'\n ? AudioContext\n : // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (globalThis as any).webkitAudioContext as typeof AudioContext;\n\n this._audioContext = new AudioContextClass({ sampleRate: inputSampleRate });\n this._ownsAudioContext = true;\n\n this._sourceNode = this._audioContext.createMediaStreamSource(stream);\n\n // Try AudioWorklet first, fall back to ScriptProcessorNode.\n this._setupScriptProcessor(inputSampleRate);\n }\n\n /**\n * ScriptProcessorNode fallback (works everywhere, including Safari).\n * We use a buffer size of 4096 which gives ~85 ms of audio at 48 kHz.\n */\n private _setupScriptProcessor(inputSampleRate: number): void {\n if (!this._audioContext || !this._sourceNode) return;\n\n // Buffer size must be a power of 2: 256, 512, 1024, 2048, 4096, 8192, 16384.\n const bufferSize = 4096;\n const processor = this._audioContext.createScriptProcessor(bufferSize, 1, 1);\n\n processor.onaudioprocess = (event: AudioProcessingEvent) => {\n if (!this._isStarted) return;\n\n const inputData = event.inputBuffer.getChannelData(0);\n\n // Resample to target rate if needed.\n const resampled =\n inputSampleRate !== this._sampleRate\n ? resample(inputData, inputSampleRate, this._sampleRate)\n : new Float32Array(inputData);\n\n // Feed resampled audio into frame-sized chunks.\n this._feedAudio(resampled);\n };\n\n this._sourceNode.connect(processor);\n processor.connect(this._audioContext.destination);\n this._workletNode = processor;\n\n this._log('Audio pipeline set up (ScriptProcessorNode)');\n }\n\n /**\n * Accumulate resampled audio into FRAME_SIZE chunks and process each full frame.\n */\n private _feedAudio(samples: Float32Array): void {\n let offset = 0;\n\n while (offset < samples.length) {\n const remaining = FRAME_SIZE - this._frameBufferOffset;\n const available = samples.length - offset;\n const toCopy = Math.min(remaining, available);\n\n this._frameBuffer.set(\n samples.subarray(offset, offset + toCopy),\n this._frameBufferOffset,\n );\n this._frameBufferOffset += toCopy;\n offset += toCopy;\n\n if (this._frameBufferOffset === FRAME_SIZE) {\n const frame = new Float32Array(this._frameBuffer);\n this._frameBufferOffset = 0;\n\n // Serialise inference calls to avoid overlapping ONNX sessions.\n this._processingPromise = this._processingPromise.then(() =>\n this._handleFrame(frame),\n );\n }\n }\n }\n\n /**\n * Process a single FRAME_SIZE frame: run inference and update speech state.\n */\n private async _handleFrame(frame: Float32Array): Promise<void> {\n if (!this._isStarted || !this._session) return;\n\n let probability: number;\n try {\n probability = await this.processFrame(frame);\n } catch (err) {\n if (this._debug) {\n // eslint-disable-next-line no-console\n console.error(`${LOG_PREFIX} Inference error:`, err);\n }\n return;\n }\n\n // Noise floor calibration phase.\n if (this._isCalibrating) {\n this._calibrationSamples.push(probability);\n\n if (this._calibrationSamples.length >= this._calibrationFramesNeeded) {\n this._finishCalibration();\n }\n return;\n }\n\n // Speech state machine.\n const isSpeechFrame = probability >= this._calibratedThreshold;\n\n if (isSpeechFrame) {\n this._consecutiveSpeechFrames++;\n this._consecutiveSilenceFrames = 0;\n\n if (!this._isSpeaking && this._consecutiveSpeechFrames >= this._minSpeechFrames) {\n this._isSpeaking = true;\n this._log('Speech started, probability:', probability.toFixed(3));\n this._emit({\n type: 'speech-start',\n timestamp: Date.now(),\n probability,\n });\n }\n } else {\n this._consecutiveSilenceFrames++;\n // Do NOT reset _consecutiveSpeechFrames here — only reset when speech-end fires.\n\n if (this._isSpeaking && this._consecutiveSilenceFrames >= this._silenceFrames) {\n this._isSpeaking = false;\n this._consecutiveSpeechFrames = 0;\n this._log('Speech ended, probability:', probability.toFixed(3));\n this._emit({\n type: 'speech-end',\n timestamp: Date.now(),\n probability,\n });\n }\n }\n }\n\n private _finishCalibration(): void {\n if (this._calibrationSamples.length === 0) {\n this._isCalibrating = false;\n return;\n }\n\n // Compute average noise floor probability.\n const sum = this._calibrationSamples.reduce((a, b) => a + b, 0);\n const avgNoise = sum / this._calibrationSamples.length;\n\n // If the ambient noise floor is high, nudge the threshold above it.\n // We add a margin so we don't constantly trigger on background noise.\n const NOISE_MARGIN = 0.15;\n if (avgNoise + NOISE_MARGIN > this._threshold) {\n this._calibratedThreshold = Math.min(avgNoise + NOISE_MARGIN, 0.95);\n this._log(\n 'Noise floor is high. Adjusted threshold from',\n this._threshold.toFixed(3),\n 'to',\n this._calibratedThreshold.toFixed(3),\n '(avg noise:',\n avgNoise.toFixed(3) + ')',\n );\n } else {\n this._calibratedThreshold = this._threshold;\n this._log('Noise floor OK, avg:', avgNoise.toFixed(3), '— keeping threshold at', this._threshold.toFixed(3));\n }\n\n // Recompute frame counters in case threshold changed min speech behaviour.\n this._minSpeechFrames = Math.ceil(this._minSpeechDurationMs / this._frameDurationMs);\n this._silenceFrames = Math.ceil(this._silenceDurationMs / this._frameDurationMs);\n\n this._isCalibrating = false;\n this._calibrationSamples = [];\n }\n\n private _teardownAudioPipeline(): void {\n if (this._workletNode) {\n try {\n this._workletNode.disconnect();\n } catch {\n // Ignore disconnect errors.\n }\n if ('onaudioprocess' in this._workletNode) {\n (this._workletNode as ScriptProcessorNode).onaudioprocess = null;\n }\n this._workletNode = null;\n }\n\n if (this._sourceNode) {\n try {\n this._sourceNode.disconnect();\n } catch {\n // Ignore.\n }\n this._sourceNode = null;\n }\n\n if (this._audioContext && this._ownsAudioContext) {\n try {\n void this._audioContext.close();\n } catch {\n // Ignore.\n }\n this._audioContext = null;\n this._ownsAudioContext = false;\n }\n\n this._stream = null;\n }\n\n // -------------------------------------------------------------------------\n // Private: ONNX state helpers\n // -------------------------------------------------------------------------\n\n /** Reset the LSTM hidden and cell states to zeros. */\n private _resetStates(): void {\n // Dispose any existing tensors to free memory.\n this._h?.dispose();\n this._c?.dispose();\n\n const zeros = new Float32Array(2 * STATE_SIZE).fill(0);\n this._h = new ort.Tensor('float32', zeros.slice(0, STATE_SIZE), [2, 1, 64]);\n this._c = new ort.Tensor('float32', zeros.slice(STATE_SIZE), [2, 1, 64]);\n }\n\n // -------------------------------------------------------------------------\n // Private: Logging\n // -------------------------------------------------------------------------\n\n private _log(...args: unknown[]): void {\n if (!this._debug) return;\n // eslint-disable-next-line no-console\n console.log(LOG_PREFIX, ...args);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Convenience factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create and initialise a SileroVAD instance in one call.\n *\n * ```ts\n * const vad = await createVAD({ debug: true });\n * vad.onSpeechStart(() => console.log('speaking'));\n * vad.start(stream);\n * ```\n */\nexport async function createVAD(options?: VADOptions): Promise<SileroVAD> {\n const vad = new SileroVAD(options);\n await vad.init();\n return vad;\n}\n\n// Re-export the frame size constant so consumers can align their buffers.\nexport { FRAME_SIZE, TARGET_SAMPLE_RATE };\n\n// Type-only declaration for environments that provide webkitAudioContext.\ndeclare global {\n // eslint-disable-next-line no-var\n var webkitAudioContext: typeof AudioContext | undefined;\n}\n"],"mappings":"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","names":[]}
|