decibri 3.4.2 → 4.0.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/CHANGELOG.md +386 -0
- package/MIGRATION.md +151 -0
- package/README.md +81 -39
- package/examples/wav-capture.js +2 -2
- package/examples/websocket-stream.js +2 -2
- package/index.d.ts +1 -1
- package/index.js +52 -52
- package/package.json +9 -7
- package/src/browser/decibri-browser.js +37 -11
- package/src/browser/index.d.ts +28 -15
- package/src/browser/index.js +2 -2
- package/src/decibri-output.js +24 -29
- package/src/decibri.d.ts +81 -47
- package/src/decibri.js +106 -41
- package/src/errors.js +126 -23
package/src/decibri.js
CHANGED
|
@@ -4,7 +4,18 @@ const { Readable } = require('stream');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const { DecibriBridge } = require('../index.js');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
wrapNativeError,
|
|
9
|
+
DecibriError,
|
|
10
|
+
DeviceError,
|
|
11
|
+
OrtError,
|
|
12
|
+
OrtPathError,
|
|
13
|
+
} = require('./errors');
|
|
14
|
+
|
|
15
|
+
// The npm package version, reported as `binding` by version(). Read from
|
|
16
|
+
// package.json so it tracks the published package and cannot drift from a
|
|
17
|
+
// hardcoded string.
|
|
18
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
8
19
|
|
|
9
20
|
// ─── Bundled ONNX Runtime path resolution ────────────────────────────────────
|
|
10
21
|
|
|
@@ -63,9 +74,9 @@ function resolveBundledOrtPath() {
|
|
|
63
74
|
|
|
64
75
|
// ─── RMS helper ──────────────────────────────────────────────────────────────
|
|
65
76
|
|
|
66
|
-
function computeRMS(chunk,
|
|
77
|
+
function computeRMS(chunk, dtype) {
|
|
67
78
|
let sum = 0, n;
|
|
68
|
-
if (
|
|
79
|
+
if (dtype === 'float32') {
|
|
69
80
|
const samples = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4);
|
|
70
81
|
n = samples.length;
|
|
71
82
|
for (let i = 0; i < n; i++) sum += samples[i] * samples[i];
|
|
@@ -80,11 +91,11 @@ function computeRMS(chunk, format) {
|
|
|
80
91
|
return n > 0 ? Math.sqrt(sum / n) : 0;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
// ───
|
|
94
|
+
// ─── Microphone (Readable) ──────────────────────────────────────────────────
|
|
84
95
|
|
|
85
|
-
class
|
|
96
|
+
class Microphone extends Readable {
|
|
86
97
|
/**
|
|
87
|
-
* @param {import('./decibri').
|
|
98
|
+
* @param {import('./decibri').MicrophoneOptions} [options]
|
|
88
99
|
*/
|
|
89
100
|
constructor(options = {}) {
|
|
90
101
|
super({ highWaterMark: options.highWaterMark, objectMode: false });
|
|
@@ -106,33 +117,23 @@ class Decibri extends Readable {
|
|
|
106
117
|
throw new RangeError('frames per buffer must be between 64 and 65536');
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
throw new TypeError("
|
|
120
|
+
const dtype = options.dtype ?? 'int16';
|
|
121
|
+
if (dtype !== 'int16' && dtype !== 'float32') {
|
|
122
|
+
throw new TypeError("dtype must be 'int16' or 'float32'");
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
// ── Resolve device ──────────────────────────────────────────────────────
|
|
115
126
|
|
|
127
|
+
// Name and multi-match resolution are delegated to the core, which owns
|
|
128
|
+
// the renamed-vocabulary errors (MicrophoneNotFound / MultipleDevicesMatch).
|
|
129
|
+
// A string name and an { id } object are passed straight through to the
|
|
130
|
+
// native addon. Only the numeric index keeps a client-side bounds check,
|
|
131
|
+
// for a clean Node-side RangeError without a round-trip.
|
|
116
132
|
let resolvedDevice = options.device;
|
|
117
|
-
if (typeof options.device === '
|
|
118
|
-
const lower = options.device.toLowerCase();
|
|
119
|
-
const matches = DecibriBridge.devices().filter(d =>
|
|
120
|
-
d.name.toLowerCase().includes(lower)
|
|
121
|
-
);
|
|
122
|
-
if (matches.length === 0) {
|
|
123
|
-
throw new TypeError(`No audio input device found matching "${options.device}"`);
|
|
124
|
-
}
|
|
125
|
-
if (matches.length > 1) {
|
|
126
|
-
const names = matches.map(d => ` [${d.index}] ${d.name}`).join('\n');
|
|
127
|
-
throw new TypeError(
|
|
128
|
-
`Multiple devices match "${options.device}":\n${names}\nUse a more specific name or pass the device index directly.`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
resolvedDevice = matches[0].index;
|
|
132
|
-
} else if (typeof options.device === 'number') {
|
|
133
|
+
if (typeof options.device === 'number') {
|
|
133
134
|
const devices = DecibriBridge.devices();
|
|
134
135
|
if (options.device < 0 || options.device >= devices.length) {
|
|
135
|
-
throw new RangeError('device index out of range. Call
|
|
136
|
+
throw new RangeError('device index out of range. Call Microphone.devices() to list available devices');
|
|
136
137
|
}
|
|
137
138
|
resolvedDevice = options.device;
|
|
138
139
|
} else if (
|
|
@@ -151,14 +152,32 @@ class Decibri extends Readable {
|
|
|
151
152
|
|
|
152
153
|
// ── Validate VAD options ─────────────────────────────────────────────────
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Single vad union: false (disabled, default), 'silero', or 'energy'. The
|
|
156
|
+
// legacy two-flag form (vad: true plus vadMode) is rejected with a
|
|
157
|
+
// migration error. Energy and Silero are both computed in this wrapper;
|
|
158
|
+
// the union only selects which.
|
|
159
|
+
const vad = options.vad ?? false;
|
|
160
|
+
let vadEnabled;
|
|
161
|
+
let vadMode;
|
|
162
|
+
if (vad === false) {
|
|
163
|
+
vadEnabled = false;
|
|
164
|
+
vadMode = 'energy'; // inert placeholder; ignored while disabled
|
|
165
|
+
} else if (vad === true) {
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
"vad: true is no longer supported. Specify the mode explicitly: vad: 'silero' or vad: 'energy'."
|
|
168
|
+
);
|
|
169
|
+
} else if (vad === 'silero' || vad === 'energy') {
|
|
170
|
+
vadEnabled = true;
|
|
171
|
+
vadMode = vad;
|
|
172
|
+
} else {
|
|
173
|
+
throw new TypeError(
|
|
174
|
+
`Invalid vad value: ${JSON.stringify(vad)}. Expected false, 'silero', or 'energy'.`
|
|
175
|
+
);
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
let modelPath = undefined;
|
|
160
179
|
let ortLibraryPath = undefined;
|
|
161
|
-
if (vadMode === 'silero'
|
|
180
|
+
if (vadEnabled && vadMode === 'silero') {
|
|
162
181
|
modelPath = options.modelPath || path.join(__dirname, '..', 'models', 'silero_vad.onnx');
|
|
163
182
|
if (!fs.existsSync(modelPath)) {
|
|
164
183
|
throw new Error(`Silero VAD model not found at ${modelPath}. Ensure the models/ directory is included in your installation.`);
|
|
@@ -172,11 +191,12 @@ class Decibri extends Readable {
|
|
|
172
191
|
|
|
173
192
|
// ── Store config ───────────────────────────────────────────────────────
|
|
174
193
|
|
|
175
|
-
this.
|
|
176
|
-
this._vad =
|
|
194
|
+
this._dtype = dtype;
|
|
195
|
+
this._vad = vadEnabled;
|
|
177
196
|
this._vadMode = vadMode;
|
|
178
197
|
this._vadThreshold = options.vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01);
|
|
179
198
|
this._vadHoldoff = options.vadHoldoff ?? 300;
|
|
199
|
+
this._vadScore = 0;
|
|
180
200
|
this._isSpeaking = false;
|
|
181
201
|
this._silenceTimer = null;
|
|
182
202
|
this._started = false;
|
|
@@ -188,9 +208,9 @@ class Decibri extends Readable {
|
|
|
188
208
|
sampleRate,
|
|
189
209
|
channels,
|
|
190
210
|
framesPerBuffer,
|
|
191
|
-
format,
|
|
211
|
+
format: dtype,
|
|
192
212
|
device: resolvedDevice,
|
|
193
|
-
vadMode
|
|
213
|
+
vadMode,
|
|
194
214
|
modelPath,
|
|
195
215
|
ortLibraryPath,
|
|
196
216
|
});
|
|
@@ -230,7 +250,7 @@ class Decibri extends Readable {
|
|
|
230
250
|
|
|
231
251
|
/** @internal Energy-based VAD (RMS threshold) */
|
|
232
252
|
_processVadEnergy(chunk) {
|
|
233
|
-
const rms = computeRMS(chunk, this.
|
|
253
|
+
const rms = computeRMS(chunk, this._dtype);
|
|
234
254
|
this._processVadValue(rms);
|
|
235
255
|
}
|
|
236
256
|
|
|
@@ -241,6 +261,7 @@ class Decibri extends Readable {
|
|
|
241
261
|
|
|
242
262
|
/** @internal Common speech/silence state machine */
|
|
243
263
|
_processVadValue(value) {
|
|
264
|
+
this._vadScore = value;
|
|
244
265
|
if (value >= this._vadThreshold) {
|
|
245
266
|
clearTimeout(this._silenceTimer);
|
|
246
267
|
this._silenceTimer = null;
|
|
@@ -277,6 +298,16 @@ class Decibri extends Readable {
|
|
|
277
298
|
return this._native.isOpen;
|
|
278
299
|
}
|
|
279
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Most recent VAD score for the active mode: the Silero speech probability
|
|
303
|
+
* in 'silero' mode, the normalized RMS of the last chunk in 'energy' mode.
|
|
304
|
+
* 0 when VAD is disabled or before the first chunk is processed.
|
|
305
|
+
* @returns {number}
|
|
306
|
+
*/
|
|
307
|
+
get vadScore() {
|
|
308
|
+
return this._vadScore;
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
/**
|
|
281
312
|
* List all available input devices on the system.
|
|
282
313
|
* @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
@@ -286,15 +317,49 @@ class Decibri extends Readable {
|
|
|
286
317
|
}
|
|
287
318
|
|
|
288
319
|
/**
|
|
289
|
-
* Version information for decibri
|
|
290
|
-
* @returns {{ decibri: string,
|
|
320
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
321
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
291
322
|
*/
|
|
292
323
|
static version() {
|
|
293
|
-
|
|
324
|
+
const v = DecibriBridge.version();
|
|
325
|
+
return { decibri: v.decibri, audioBackend: v.audioBackend, binding: PACKAGE_VERSION };
|
|
294
326
|
}
|
|
295
327
|
}
|
|
296
328
|
|
|
297
|
-
const
|
|
298
|
-
|
|
329
|
+
const Speaker = require('./decibri-output.js');
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* List all available audio input devices.
|
|
333
|
+
* @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
334
|
+
*/
|
|
335
|
+
function inputDevices() {
|
|
336
|
+
return Microphone.devices();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* List all available audio output devices.
|
|
341
|
+
* @returns {Array<{index: number, name: string, maxOutputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
342
|
+
*/
|
|
343
|
+
function outputDevices() {
|
|
344
|
+
return Speaker.devices();
|
|
345
|
+
}
|
|
299
346
|
|
|
300
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
349
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
350
|
+
*/
|
|
351
|
+
function version() {
|
|
352
|
+
return Microphone.version();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = {
|
|
356
|
+
Microphone,
|
|
357
|
+
Speaker,
|
|
358
|
+
inputDevices,
|
|
359
|
+
outputDevices,
|
|
360
|
+
version,
|
|
361
|
+
DecibriError,
|
|
362
|
+
DeviceError,
|
|
363
|
+
OrtError,
|
|
364
|
+
OrtPathError,
|
|
365
|
+
};
|
package/src/errors.js
CHANGED
|
@@ -1,40 +1,143 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* decibri error classes.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* allowed range). Non-InvalidArg errors are passed through unchanged.
|
|
6
|
+
* A shallow set of catch-root classes that mirror the catch-roots the Python
|
|
7
|
+
* binding exposes (see bindings/python/python/decibri/exceptions.py), so
|
|
8
|
+
* `instanceof DecibriError` / `instanceof DeviceError` works the same way
|
|
9
|
+
* across bindings. Node idiom is shallow classes plus a stable `code` string,
|
|
10
|
+
* not a deep subclass tree: branch on `err.code` or on `instanceof`.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* DecibriError (base; extends Error)
|
|
13
|
+
* +- DeviceError (device enumeration / selection failures)
|
|
14
|
+
* +- OrtError (ONNX Runtime setup / inference failures)
|
|
15
|
+
* +- OrtPathError (a specific ORT library path could not be loaded)
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Argument validation (bad sample rate, channels, frames, dtype, vad) keeps
|
|
18
|
+
* Node's built-in RangeError / TypeError, matching Node core; those are not
|
|
19
|
+
* decibri error classes by design.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
class DecibriError extends Error {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} message
|
|
25
|
+
* @param {string} [code] Stable string code for `err.code` branching.
|
|
26
|
+
*/
|
|
27
|
+
constructor(message, code) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'DecibriError';
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class DeviceError extends DecibriError {
|
|
35
|
+
constructor(message, code) {
|
|
36
|
+
super(message, code);
|
|
37
|
+
this.name = 'DeviceError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class OrtError extends DecibriError {
|
|
42
|
+
constructor(message, code) {
|
|
43
|
+
super(message, code);
|
|
44
|
+
this.name = 'OrtError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class OrtPathError extends OrtError {
|
|
49
|
+
constructor(message, code) {
|
|
50
|
+
super(message, code);
|
|
51
|
+
this.name = 'OrtPathError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Native error classification ─────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
// The napi layer flattens every core DecibriError variant to a Status plus the
|
|
58
|
+
// variant's Display string (see bindings/node/src/lib.rs to_napi_error), so the
|
|
59
|
+
// message text is the only thing left to classify on. The prefixes below match
|
|
60
|
+
// the frozen core messages in crates/decibri/src/error.rs.
|
|
61
|
+
|
|
62
|
+
const DEVICE_CODES = [
|
|
63
|
+
['No microphone found matching', 'MICROPHONE_NOT_FOUND'],
|
|
64
|
+
['No speaker found matching', 'SPEAKER_NOT_FOUND'],
|
|
65
|
+
['Multiple devices match', 'MULTIPLE_DEVICES_MATCH'],
|
|
66
|
+
['No microphone found.', 'NO_MICROPHONE_FOUND'],
|
|
67
|
+
['No speaker found.', 'NO_SPEAKER_FOUND'],
|
|
68
|
+
['Selected device is not a valid microphone', 'NOT_AN_INPUT_DEVICE'],
|
|
69
|
+
['Failed to enumerate devices', 'DEVICE_ENUMERATION_FAILED'],
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const ORT_CODES = [
|
|
73
|
+
['decibri: failed to initialize ONNX Runtime', 'ORT_INIT_FAILED'],
|
|
74
|
+
['Failed to load Silero VAD model from', 'VAD_MODEL_LOAD_FAILED'],
|
|
75
|
+
['Failed to create ort session builder', 'ORT_SESSION_BUILD_FAILED'],
|
|
76
|
+
['Failed to set ort threads', 'ORT_THREADS_CONFIG_FAILED'],
|
|
77
|
+
['Silero VAD inference failed', 'ORT_INFERENCE_FAILED'],
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wrap an error thrown from a native DecibriBridge / DecibriOutputBridge call
|
|
82
|
+
* into the appropriate JS error: a built-in for argument validation, or a
|
|
83
|
+
* decibri catch-root class (with a `code`) for device and ONNX Runtime
|
|
84
|
+
* failures. The message is preserved verbatim.
|
|
85
|
+
*
|
|
86
|
+
* @param {Error} err Error thrown from a native constructor.
|
|
87
|
+
* @returns {Error} A RangeError, TypeError, or DecibriError subclass.
|
|
21
88
|
*/
|
|
22
89
|
function wrapNativeError(err) {
|
|
23
|
-
|
|
90
|
+
const msg = (err && err.message) || String(err);
|
|
24
91
|
|
|
25
|
-
|
|
26
|
-
|
|
92
|
+
// Argument validation stays as Node built-ins. `device index out of range`
|
|
93
|
+
// is kept here (RangeError) so the index error type is the same whether it
|
|
94
|
+
// is raised by the client-side bounds check or by the core.
|
|
95
|
+
if (
|
|
27
96
|
msg.startsWith('sample rate must be between') ||
|
|
28
97
|
msg.startsWith('channels must be between') ||
|
|
29
98
|
msg.startsWith('frames per buffer must be between') ||
|
|
30
99
|
msg.startsWith('Silero VAD only supports') ||
|
|
31
100
|
msg.startsWith('VAD threshold must be between') ||
|
|
32
|
-
msg.startsWith('device index out of range')
|
|
101
|
+
msg.startsWith('device index out of range')
|
|
102
|
+
) {
|
|
103
|
+
return new RangeError(msg);
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
msg.startsWith("dtype must be 'int16' or 'float32'") ||
|
|
107
|
+
msg.startsWith("format must be 'int16' or 'float32'")
|
|
108
|
+
) {
|
|
109
|
+
return new TypeError(msg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Device enumeration / selection failures.
|
|
113
|
+
for (const [prefix, code] of DEVICE_CODES) {
|
|
114
|
+
if (msg.startsWith(prefix)) return new DeviceError(msg, code);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ONNX Runtime: a bad library path is an OrtPathError (subclass of OrtError);
|
|
118
|
+
// OrtLoadFailed and OrtPathInvalid share this message prefix and the same
|
|
119
|
+
// user-facing meaning. Other ORT failures are OrtError.
|
|
120
|
+
if (msg.startsWith('decibri: failed to load ONNX Runtime from')) {
|
|
121
|
+
return new OrtPathError(msg, 'ORT_LOAD_FAILED');
|
|
122
|
+
}
|
|
123
|
+
for (const [prefix, code] of ORT_CODES) {
|
|
124
|
+
if (msg.startsWith(prefix)) return new OrtError(msg, code);
|
|
125
|
+
}
|
|
126
|
+
if (msg.startsWith('Failed to create') && msg.includes('tensor')) {
|
|
127
|
+
return new OrtError(msg, 'ORT_TENSOR_CREATE_FAILED');
|
|
128
|
+
}
|
|
129
|
+
if (msg.startsWith('Failed to extract') && msg.includes('tensor')) {
|
|
130
|
+
return new OrtError(msg, 'ORT_TENSOR_EXTRACT_FAILED');
|
|
131
|
+
}
|
|
33
132
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
wrapped.code = err.code;
|
|
37
|
-
return wrapped;
|
|
133
|
+
// Any other error from the native constructor is still a decibri error.
|
|
134
|
+
return new DecibriError(msg, 'DECIBRI_ERROR');
|
|
38
135
|
}
|
|
39
136
|
|
|
40
|
-
module.exports = {
|
|
137
|
+
module.exports = {
|
|
138
|
+
DecibriError,
|
|
139
|
+
DeviceError,
|
|
140
|
+
OrtError,
|
|
141
|
+
OrtPathError,
|
|
142
|
+
wrapNativeError,
|
|
143
|
+
};
|