decibri 3.4.2 → 4.1.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 +393 -0
- package/MIGRATION.md +168 -0
- package/README.md +115 -39
- package/examples/wav-capture.js +2 -2
- package/examples/websocket-stream.js +2 -2
- package/index.d.ts +32 -1
- package/index.js +66 -56
- package/package.json +10 -8
- 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 +137 -39
- package/src/decibri.d.ts +142 -47
- package/src/decibri.js +187 -56
- 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,15 +91,61 @@ 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]
|
|
99
|
+
* @param {{ prepared: object, native: object }} [_internal] Internal: a
|
|
100
|
+
* pre-resolved options bundle and an already-constructed native bridge,
|
|
101
|
+
* passed by the async `Microphone.open()` factory so the heavy open work
|
|
102
|
+
* (the Silero model load) is not repeated on the event loop. Not part of
|
|
103
|
+
* the public API.
|
|
88
104
|
*/
|
|
89
|
-
constructor(options = {}) {
|
|
105
|
+
constructor(options = {}, _internal = undefined) {
|
|
90
106
|
super({ highWaterMark: options.highWaterMark, objectMode: false });
|
|
91
107
|
|
|
108
|
+
// Validate and resolve options once. The async factory passes its already
|
|
109
|
+
// resolved bundle through `_internal` to avoid recomputing it.
|
|
110
|
+
const prepared = _internal ? _internal.prepared : Microphone._prepareOptions(options);
|
|
111
|
+
|
|
112
|
+
// ── Store config ───────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
this._dtype = prepared.dtype;
|
|
115
|
+
this._vad = prepared.vadEnabled;
|
|
116
|
+
this._vadMode = prepared.vadMode;
|
|
117
|
+
this._vadThreshold = prepared.vadThreshold;
|
|
118
|
+
this._vadHoldoff = prepared.vadHoldoff;
|
|
119
|
+
this._vadScore = 0;
|
|
120
|
+
this._isSpeaking = false;
|
|
121
|
+
this._silenceTimer = null;
|
|
122
|
+
this._started = false;
|
|
123
|
+
|
|
124
|
+
// ── Create or adopt native bridge ───────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
if (_internal) {
|
|
127
|
+
// Built off the event loop by Microphone.open(); already wrapped.
|
|
128
|
+
this._native = _internal.native;
|
|
129
|
+
} else {
|
|
130
|
+
try {
|
|
131
|
+
this._native = new DecibriBridge(prepared.nativeOptions);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw wrapNativeError(err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate the constructor options and resolve them into the native options
|
|
140
|
+
* object plus the wrapper-side state. Throws the same `RangeError` /
|
|
141
|
+
* `TypeError` / `Error` as the constructor on invalid input. Shared by the
|
|
142
|
+
* synchronous constructor and the async `open()` factory so both validate
|
|
143
|
+
* identically. Does no native open work beyond the numeric-device bounds
|
|
144
|
+
* check (a fast device enumeration).
|
|
145
|
+
* @internal
|
|
146
|
+
* @param {import('./decibri').MicrophoneOptions} options
|
|
147
|
+
*/
|
|
148
|
+
static _prepareOptions(options) {
|
|
92
149
|
// ── Validate options ───────────────────────────────────────────────────
|
|
93
150
|
|
|
94
151
|
const sampleRate = options.sampleRate ?? 16000;
|
|
@@ -106,33 +163,23 @@ class Decibri extends Readable {
|
|
|
106
163
|
throw new RangeError('frames per buffer must be between 64 and 65536');
|
|
107
164
|
}
|
|
108
165
|
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
throw new TypeError("
|
|
166
|
+
const dtype = options.dtype ?? 'int16';
|
|
167
|
+
if (dtype !== 'int16' && dtype !== 'float32') {
|
|
168
|
+
throw new TypeError("dtype must be 'int16' or 'float32'");
|
|
112
169
|
}
|
|
113
170
|
|
|
114
171
|
// ── Resolve device ──────────────────────────────────────────────────────
|
|
115
172
|
|
|
173
|
+
// Name and multi-match resolution are delegated to the core, which owns
|
|
174
|
+
// the renamed-vocabulary errors (MicrophoneNotFound / MultipleDevicesMatch).
|
|
175
|
+
// A string name and an { id } object are passed straight through to the
|
|
176
|
+
// native addon. Only the numeric index keeps a client-side bounds check,
|
|
177
|
+
// for a clean Node-side RangeError without a round-trip.
|
|
116
178
|
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') {
|
|
179
|
+
if (typeof options.device === 'number') {
|
|
133
180
|
const devices = DecibriBridge.devices();
|
|
134
181
|
if (options.device < 0 || options.device >= devices.length) {
|
|
135
|
-
throw new RangeError('device index out of range. Call
|
|
182
|
+
throw new RangeError('device index out of range. Call Microphone.devices() to list available devices');
|
|
136
183
|
}
|
|
137
184
|
resolvedDevice = options.device;
|
|
138
185
|
} else if (
|
|
@@ -151,14 +198,32 @@ class Decibri extends Readable {
|
|
|
151
198
|
|
|
152
199
|
// ── Validate VAD options ─────────────────────────────────────────────────
|
|
153
200
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
201
|
+
// Single vad union: false (disabled, default), 'silero', or 'energy'. The
|
|
202
|
+
// legacy two-flag form (vad: true plus vadMode) is rejected with a
|
|
203
|
+
// migration error. Energy and Silero are both computed in this wrapper;
|
|
204
|
+
// the union only selects which.
|
|
205
|
+
const vad = options.vad ?? false;
|
|
206
|
+
let vadEnabled;
|
|
207
|
+
let vadMode;
|
|
208
|
+
if (vad === false) {
|
|
209
|
+
vadEnabled = false;
|
|
210
|
+
vadMode = 'energy'; // inert placeholder; ignored while disabled
|
|
211
|
+
} else if (vad === true) {
|
|
212
|
+
throw new TypeError(
|
|
213
|
+
"vad: true is no longer supported. Specify the mode explicitly: vad: 'silero' or vad: 'energy'."
|
|
214
|
+
);
|
|
215
|
+
} else if (vad === 'silero' || vad === 'energy') {
|
|
216
|
+
vadEnabled = true;
|
|
217
|
+
vadMode = vad;
|
|
218
|
+
} else {
|
|
219
|
+
throw new TypeError(
|
|
220
|
+
`Invalid vad value: ${JSON.stringify(vad)}. Expected false, 'silero', or 'energy'.`
|
|
221
|
+
);
|
|
157
222
|
}
|
|
158
223
|
|
|
159
224
|
let modelPath = undefined;
|
|
160
225
|
let ortLibraryPath = undefined;
|
|
161
|
-
if (vadMode === 'silero'
|
|
226
|
+
if (vadEnabled && vadMode === 'silero') {
|
|
162
227
|
modelPath = options.modelPath || path.join(__dirname, '..', 'models', 'silero_vad.onnx');
|
|
163
228
|
if (!fs.existsSync(modelPath)) {
|
|
164
229
|
throw new Error(`Silero VAD model not found at ${modelPath}. Ensure the models/ directory is included in your installation.`);
|
|
@@ -170,33 +235,54 @@ class Decibri extends Readable {
|
|
|
170
235
|
ortLibraryPath = resolveBundledOrtPath();
|
|
171
236
|
}
|
|
172
237
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this._isSpeaking = false;
|
|
181
|
-
this._silenceTimer = null;
|
|
182
|
-
this._started = false;
|
|
183
|
-
|
|
184
|
-
// ── Create native bridge ───────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
this._native = new DecibriBridge({
|
|
238
|
+
return {
|
|
239
|
+
dtype,
|
|
240
|
+
vadEnabled,
|
|
241
|
+
vadMode,
|
|
242
|
+
vadThreshold: options.vadThreshold ?? (vadMode === 'silero' ? 0.5 : 0.01),
|
|
243
|
+
vadHoldoff: options.vadHoldoff ?? 300,
|
|
244
|
+
nativeOptions: {
|
|
188
245
|
sampleRate,
|
|
189
246
|
channels,
|
|
190
247
|
framesPerBuffer,
|
|
191
|
-
format,
|
|
248
|
+
format: dtype,
|
|
192
249
|
device: resolvedDevice,
|
|
193
|
-
vadMode
|
|
250
|
+
vadMode,
|
|
194
251
|
modelPath,
|
|
195
252
|
ortLibraryPath,
|
|
196
|
-
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Construct a Microphone without blocking the event loop on the open work.
|
|
259
|
+
*
|
|
260
|
+
* The synchronous `new Microphone(...)` constructor loads the Silero VAD
|
|
261
|
+
* model inline when `vad: 'silero'` is set, which blocks the event loop for
|
|
262
|
+
* roughly 100 to 500 ms on a cold cache. This static factory runs that load
|
|
263
|
+
* (and device resolution) on the native thread pool and resolves to a ready
|
|
264
|
+
* instance, so latency-sensitive callers (voice pipelines, websocket
|
|
265
|
+
* handlers) do not stall. The synchronous constructor remains available and
|
|
266
|
+
* unchanged.
|
|
267
|
+
*
|
|
268
|
+
* Mirrors the Python `AsyncMicrophone.open()` factory. Options are identical
|
|
269
|
+
* to the constructor. A failed open (bad model path, unknown device, ORT
|
|
270
|
+
* load failure) rejects the returned Promise with the matching error class
|
|
271
|
+
* (`RangeError` / `TypeError` for invalid options, `DeviceError` / `OrtError`
|
|
272
|
+
* / `OrtPathError` for native failures), rather than throwing synchronously.
|
|
273
|
+
*
|
|
274
|
+
* @param {import('./decibri').MicrophoneOptions} [options]
|
|
275
|
+
* @returns {Promise<Microphone>}
|
|
276
|
+
*/
|
|
277
|
+
static async open(options = {}) {
|
|
278
|
+
const prepared = Microphone._prepareOptions(options);
|
|
279
|
+
let native;
|
|
280
|
+
try {
|
|
281
|
+
native = await DecibriBridge.openAsync(prepared.nativeOptions);
|
|
197
282
|
} catch (err) {
|
|
198
283
|
throw wrapNativeError(err);
|
|
199
284
|
}
|
|
285
|
+
return new Microphone(options, { prepared, native });
|
|
200
286
|
}
|
|
201
287
|
|
|
202
288
|
/** @internal */
|
|
@@ -230,7 +316,7 @@ class Decibri extends Readable {
|
|
|
230
316
|
|
|
231
317
|
/** @internal Energy-based VAD (RMS threshold) */
|
|
232
318
|
_processVadEnergy(chunk) {
|
|
233
|
-
const rms = computeRMS(chunk, this.
|
|
319
|
+
const rms = computeRMS(chunk, this._dtype);
|
|
234
320
|
this._processVadValue(rms);
|
|
235
321
|
}
|
|
236
322
|
|
|
@@ -241,6 +327,7 @@ class Decibri extends Readable {
|
|
|
241
327
|
|
|
242
328
|
/** @internal Common speech/silence state machine */
|
|
243
329
|
_processVadValue(value) {
|
|
330
|
+
this._vadScore = value;
|
|
244
331
|
if (value >= this._vadThreshold) {
|
|
245
332
|
clearTimeout(this._silenceTimer);
|
|
246
333
|
this._silenceTimer = null;
|
|
@@ -277,6 +364,16 @@ class Decibri extends Readable {
|
|
|
277
364
|
return this._native.isOpen;
|
|
278
365
|
}
|
|
279
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Most recent VAD score for the active mode: the Silero speech probability
|
|
369
|
+
* in 'silero' mode, the normalized RMS of the last chunk in 'energy' mode.
|
|
370
|
+
* 0 when VAD is disabled or before the first chunk is processed.
|
|
371
|
+
* @returns {number}
|
|
372
|
+
*/
|
|
373
|
+
get vadScore() {
|
|
374
|
+
return this._vadScore;
|
|
375
|
+
}
|
|
376
|
+
|
|
280
377
|
/**
|
|
281
378
|
* List all available input devices on the system.
|
|
282
379
|
* @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
@@ -286,15 +383,49 @@ class Decibri extends Readable {
|
|
|
286
383
|
}
|
|
287
384
|
|
|
288
385
|
/**
|
|
289
|
-
* Version information for decibri
|
|
290
|
-
* @returns {{ decibri: string,
|
|
386
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
387
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
291
388
|
*/
|
|
292
389
|
static version() {
|
|
293
|
-
|
|
390
|
+
const v = DecibriBridge.version();
|
|
391
|
+
return { decibri: v.decibri, audioBackend: v.audioBackend, binding: PACKAGE_VERSION };
|
|
294
392
|
}
|
|
295
393
|
}
|
|
296
394
|
|
|
297
|
-
const
|
|
298
|
-
|
|
395
|
+
const Speaker = require('./decibri-output.js');
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* List all available audio input devices.
|
|
399
|
+
* @returns {Array<{index: number, name: string, maxInputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
400
|
+
*/
|
|
401
|
+
function inputDevices() {
|
|
402
|
+
return Microphone.devices();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* List all available audio output devices.
|
|
407
|
+
* @returns {Array<{index: number, name: string, maxOutputChannels: number, defaultSampleRate: number, isDefault: boolean}>}
|
|
408
|
+
*/
|
|
409
|
+
function outputDevices() {
|
|
410
|
+
return Speaker.devices();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Version information for decibri, the audio backend, and this binding.
|
|
415
|
+
* @returns {{ decibri: string, audioBackend: string, binding: string }}
|
|
416
|
+
*/
|
|
417
|
+
function version() {
|
|
418
|
+
return Microphone.version();
|
|
419
|
+
}
|
|
299
420
|
|
|
300
|
-
module.exports =
|
|
421
|
+
module.exports = {
|
|
422
|
+
Microphone,
|
|
423
|
+
Speaker,
|
|
424
|
+
inputDevices,
|
|
425
|
+
outputDevices,
|
|
426
|
+
version,
|
|
427
|
+
DecibriError,
|
|
428
|
+
DeviceError,
|
|
429
|
+
OrtError,
|
|
430
|
+
OrtPathError,
|
|
431
|
+
};
|
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
|
+
};
|