@picovoice/eagle-web 2.0.0 → 3.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/src/eagle.ts CHANGED
@@ -1,1508 +1,1546 @@
1
- /*
2
- Copyright 2023-2025 Picovoice Inc.
3
-
4
- You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
5
- file accompanying this source.
6
-
7
- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
- an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
- specific language governing permissions and limitations under the License.
10
- */
11
-
12
- /* eslint camelcase: 0 */
13
-
14
- import { Mutex } from 'async-mutex';
15
-
16
- import {
17
- arrayBufferToStringAtIndex,
18
- base64ToUint8Array,
19
- isAccessKeyValid,
20
- loadModel,
21
- } from '@picovoice/web-utils';
22
-
23
- import createModuleSimd from "./lib/pv_eagle_simd";
24
- import createModulePThread from "./lib/pv_eagle_pthread";
25
-
26
- import { simd } from 'wasm-feature-detect';
27
-
28
- import {
29
- EagleModel,
30
- EagleOptions,
31
- EagleProfile,
32
- EagleProfilerEnrollResult,
33
- EagleProfilerOptions,
34
- PvStatus
35
- } from './types';
36
-
37
- import * as EagleErrors from './eagle_errors';
38
- import { pvStatusToException } from './eagle_errors';
39
-
40
- /**
41
- * WebAssembly function types
42
- */
43
- type pv_eagle_profiler_init_type = (
44
- accessKey: number,
45
- modelPath: number,
46
- device: number,
47
- object: number
48
- ) => Promise<number>;
49
- type pv_eagle_profiler_delete_type = (object: number) => Promise<void>;
50
- type pv_eagle_profiler_enroll_type = (
51
- object: number,
52
- pcm: number,
53
- numSamples: number,
54
- feedback: number,
55
- percentage: number
56
- ) => Promise<number>;
57
- type pv_eagle_profiler_enroll_min_audio_length_samples_type = (
58
- object: number,
59
- numSamples: number
60
- ) => number;
61
- type pv_eagle_profiler_export_type = (
62
- object: number,
63
- speakerProfile: number
64
- ) => number;
65
- type pv_eagle_profiler_export_size_type = (
66
- object: number,
67
- speakerProfileSizeBytes: number
68
- ) => number;
69
- type pv_eagle_profiler_reset_type = (object: number) => Promise<number>;
70
- type pv_eagle_init_type = (
71
- accessKey: number,
72
- modelPath: number,
73
- device: number,
74
- numSpeakers: number,
75
- speakerProfiles: number,
76
- object: number
77
- ) => Promise<number>;
78
- type pv_eagle_delete_type = (object: number) => Promise<void>;
79
- type pv_eagle_process_type = (
80
- object: number,
81
- pcm: number,
82
- scores: number
83
- ) => Promise<number>;
84
- type pv_eagle_reset_type = (object: number) => Promise<number>;
85
- type pv_eagle_frame_length_type = () => number;
86
- type pv_eagle_version_type = () => number;
87
- type pv_eagle_list_hardware_devices_type = (
88
- hardwareDevices: number,
89
- numHardwareDevices: number
90
- ) => number;
91
- type pv_eagle_free_hardware_devices_type = (
92
- hardwareDevices: number,
93
- numHardwareDevices: number
94
- ) => number;
95
- type pv_sample_rate_type = () => number;
96
- type pv_set_sdk_type = (sdk: number) => void;
97
- type pv_get_error_stack_type = (
98
- messageStack: number,
99
- messageStackDepth: number
100
- ) => number;
101
- type pv_free_error_stack_type = (messageStack: number) => void;
102
-
103
- type EagleModule = EmscriptenModule & {
104
- _pv_free: (address: number) => void;
105
-
106
- _pv_eagle_profiler_enroll_min_audio_length_samples: pv_eagle_profiler_enroll_min_audio_length_samples_type
107
- _pv_eagle_profiler_export: pv_eagle_profiler_export_type
108
- _pv_eagle_profiler_export_size: pv_eagle_profiler_export_size_type
109
- _pv_eagle_frame_length: pv_eagle_frame_length_type
110
- _pv_eagle_version: pv_eagle_version_type
111
- _pv_eagle_list_hardware_devices: pv_eagle_list_hardware_devices_type;
112
- _pv_eagle_free_hardware_devices: pv_eagle_free_hardware_devices_type;
113
- _pv_sample_rate: pv_sample_rate_type
114
-
115
- _pv_set_sdk: pv_set_sdk_type;
116
- _pv_get_error_stack: pv_get_error_stack_type;
117
- _pv_free_error_stack: pv_free_error_stack_type;
118
-
119
- // em default functions
120
- addFunction: typeof addFunction;
121
- ccall: typeof ccall;
122
- cwrap: typeof cwrap;
123
- }
124
-
125
- type EagleBaseWasmOutput = {
126
- module: EagleModule;
127
-
128
- sampleRate: number;
129
- version: string;
130
-
131
- messageStackAddressAddressAddress: number;
132
- messageStackDepthAddress: number;
133
- };
134
-
135
- type EagleProfilerWasmOutput = EagleBaseWasmOutput & {
136
- minEnrollSamples: number;
137
- profileSize: number;
138
-
139
- objectAddress: number;
140
- feedbackAddress: number;
141
- percentageAddress: number;
142
- profileAddress: number;
143
-
144
- pv_eagle_profiler_enroll: pv_eagle_profiler_enroll_type;
145
- pv_eagle_profiler_reset: pv_eagle_profiler_reset_type;
146
- pv_eagle_profiler_delete: pv_eagle_profiler_delete_type;
147
- };
148
-
149
- type EagleWasmOutput = EagleBaseWasmOutput & {
150
- frameLength: number;
151
- numSpeakers: number;
152
-
153
- objectAddress: number;
154
- scoresAddress: number;
155
-
156
- pv_eagle_process: pv_eagle_process_type;
157
- pv_eagle_reset: pv_eagle_reset_type;
158
- pv_eagle_delete: pv_eagle_delete_type;
159
- };
160
-
161
- const PV_STATUS_SUCCESS = 10000;
162
- const MAX_PCM_LENGTH_SEC = 60 * 15;
163
-
164
- class EagleBase {
165
- protected _module?: EagleModule;
166
-
167
- protected readonly _functionMutex: Mutex;
168
-
169
- protected readonly _messageStackAddressAddressAddress: number;
170
- protected readonly _messageStackDepthAddress: number;
171
-
172
- protected readonly _sampleRate: number;
173
- protected readonly _version: string;
174
-
175
- protected static _wasmSimd: string;
176
- protected static _wasmSimdLib: string;
177
- protected static _wasmPThread: string;
178
- protected static _wasmPThreadLib: string;
179
-
180
- protected static _sdk: string = 'web';
181
-
182
- protected static _eagleMutex = new Mutex();
183
-
184
- protected constructor(handleWasm: EagleBaseWasmOutput) {
185
- this._module = handleWasm.module;
186
-
187
- this._sampleRate = handleWasm.sampleRate;
188
- this._version = handleWasm.version;
189
-
190
- this._messageStackAddressAddressAddress = handleWasm.messageStackAddressAddressAddress;
191
- this._messageStackDepthAddress = handleWasm.messageStackDepthAddress;
192
-
193
- this._functionMutex = new Mutex();
194
- }
195
-
196
- /**
197
- * Audio sample rate required by Eagle.
198
- */
199
- get sampleRate(): number {
200
- return this._sampleRate;
201
- }
202
-
203
- /**
204
- * Version of Eagle.
205
- */
206
- get version(): string {
207
- return this._version;
208
- }
209
-
210
- /**
211
- * Set base64 wasm file with SIMD feature.
212
- * @param wasmSimd Base64'd wasm file to use to initialize wasm.
213
- */
214
- public static setWasmSimd(wasmSimd: string): void {
215
- if (this._wasmSimd === undefined) {
216
- this._wasmSimd = wasmSimd;
217
- }
218
- }
219
-
220
- /**
221
- * Set base64 SIMD wasm file in text format.
222
- * @param wasmSimdLib Base64'd wasm file in text format.
223
- */
224
- public static setWasmSimdLib(wasmSimdLib: string): void {
225
- if (this._wasmSimdLib === undefined) {
226
- this._wasmSimdLib = wasmSimdLib;
227
- }
228
- }
229
-
230
- /**
231
- * Set base64 wasm file with SIMD and pthread feature.
232
- * @param wasmPThread Base64'd wasm file to use to initialize wasm.
233
- */
234
- public static setWasmPThread(wasmPThread: string): void {
235
- if (this._wasmPThread === undefined) {
236
- this._wasmPThread = wasmPThread;
237
- }
238
- }
239
-
240
- /**
241
- * Set base64 SIMD and thread wasm file in text format.
242
- * @param wasmPThreadLib Base64'd wasm file in text format.
243
- */
244
- public static setWasmPThreadLib(wasmPThreadLib: string): void {
245
- if (this._wasmPThreadLib === undefined) {
246
- this._wasmPThreadLib = wasmPThreadLib;
247
- }
248
- }
249
-
250
- public static setSdk(sdk: string): void {
251
- EagleBase._sdk = sdk;
252
- }
253
-
254
- protected static async _initBaseWasm(
255
- wasmBase64: string,
256
- wasmLibBase64: string,
257
- createModuleFunc: any,
258
- ): Promise<EagleBaseWasmOutput> {
259
- const blob = new Blob(
260
- [base64ToUint8Array(wasmLibBase64)],
261
- { type: 'application/javascript' }
262
- );
263
- const module: EagleModule = await createModuleFunc({
264
- mainScriptUrlOrBlob: blob,
265
- wasmBinary: base64ToUint8Array(wasmBase64),
266
- });
267
-
268
- const sampleRate = module._pv_sample_rate();
269
- const versionAddress = module._pv_eagle_version();
270
- const version = arrayBufferToStringAtIndex(
271
- module.HEAPU8,
272
- versionAddress,
273
- );
274
-
275
- const sdkEncoded = new TextEncoder().encode(this._sdk);
276
- const sdkAddress = module._malloc((sdkEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT);
277
- if (!sdkAddress) {
278
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
279
- }
280
- module.HEAPU8.set(sdkEncoded, sdkAddress);
281
- module.HEAPU8[sdkAddress + sdkEncoded.length] = 0;
282
- module._pv_set_sdk(sdkAddress);
283
- module._pv_free(sdkAddress);
284
-
285
- const messageStackDepthAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT);
286
- if (!messageStackDepthAddress) {
287
- throw new EagleErrors.EagleOutOfMemoryError(
288
- 'malloc failed: Cannot allocate memory'
289
- );
290
- }
291
-
292
- const messageStackAddressAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT);
293
- if (!messageStackAddressAddressAddress) {
294
- throw new EagleErrors.EagleOutOfMemoryError(
295
- 'malloc failed: Cannot allocate memory'
296
- );
297
- }
298
-
299
- return {
300
- module: module,
301
-
302
- sampleRate: sampleRate,
303
- version: version,
304
-
305
- messageStackAddressAddressAddress: messageStackAddressAddressAddress,
306
- messageStackDepthAddress: messageStackDepthAddress,
307
- };
308
- }
309
-
310
- /**
311
- * Releases resources acquired by Eagle
312
- */
313
- public async release(): Promise<void> {
314
- if (!this._module) {
315
- return;
316
- }
317
- this._module._pv_free(this._messageStackAddressAddressAddress);
318
- this._module._pv_free(this._messageStackDepthAddress);
319
- }
320
-
321
- protected static async getMessageStack(
322
- pv_get_error_stack: pv_get_error_stack_type,
323
- pv_free_error_stack: pv_free_error_stack_type,
324
- messageStackAddressAddressAddress: number,
325
- messageStackDepthAddress: number,
326
- memoryBufferInt32: Int32Array,
327
- memoryBufferUint8: Uint8Array
328
- ): Promise<string[]> {
329
- const status = pv_get_error_stack(messageStackAddressAddressAddress, messageStackDepthAddress);
330
- if (status !== PvStatus.SUCCESS) {
331
- throw pvStatusToException(status, 'Unable to get Eagle error state');
332
- }
333
-
334
- const messageStackAddressAddress = memoryBufferInt32[messageStackAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT];
335
-
336
- const messageStackDepth = memoryBufferInt32[messageStackDepthAddress / Int32Array.BYTES_PER_ELEMENT];
337
- const messageStack: string[] = [];
338
- for (let i = 0; i < messageStackDepth; i++) {
339
- const messageStackAddress = memoryBufferInt32[
340
- (messageStackAddressAddress / Int32Array.BYTES_PER_ELEMENT) + i
341
- ];
342
- const message = arrayBufferToStringAtIndex(memoryBufferUint8, messageStackAddress);
343
- messageStack.push(message);
344
- }
345
-
346
- pv_free_error_stack(messageStackAddressAddress);
347
-
348
- return messageStack;
349
- }
350
-
351
- protected static wrapAsyncFunction(module: EagleModule, functionName: string, numArgs: number): (...args: any[]) => any {
352
- // @ts-ignore
353
- return module.cwrap(
354
- functionName,
355
- "number",
356
- Array(numArgs).fill("number"),
357
- { async: true }
358
- );
359
- }
360
- }
361
-
362
- /**
363
- * JavaScript/WebAssembly binding for the profiler of the Eagle Speaker Recognition engine.
364
- * It enrolls a speaker given a set of utterances and then constructs a profile for the enrolled speaker.
365
- */
366
- export class EagleProfiler extends EagleBase {
367
- private readonly _pv_eagle_profiler_enroll: pv_eagle_profiler_enroll_type;
368
- private readonly _pv_eagle_profiler_reset: pv_eagle_profiler_reset_type;
369
- private readonly _pv_eagle_profiler_delete: pv_eagle_profiler_delete_type;
370
-
371
- private readonly _objectAddress: number;
372
- private readonly _feedbackAddress: number;
373
- private readonly _percentageAddress: number;
374
-
375
- private readonly _maxEnrollSamples: number;
376
- private readonly _minEnrollSamples: number;
377
- private readonly _profileSize: number;
378
-
379
- private constructor(handleWasm: EagleProfilerWasmOutput) {
380
- super(handleWasm);
381
-
382
- this._minEnrollSamples = handleWasm.minEnrollSamples;
383
- this._profileSize = handleWasm.profileSize;
384
- this._maxEnrollSamples = MAX_PCM_LENGTH_SEC * this._sampleRate;
385
-
386
- this._pv_eagle_profiler_enroll = handleWasm.pv_eagle_profiler_enroll;
387
- this._pv_eagle_profiler_reset = handleWasm.pv_eagle_profiler_reset;
388
- this._pv_eagle_profiler_delete = handleWasm.pv_eagle_profiler_delete;
389
-
390
- this._objectAddress = handleWasm.objectAddress;
391
- this._feedbackAddress = handleWasm.feedbackAddress;
392
- this._percentageAddress = handleWasm.percentageAddress;
393
- }
394
-
395
- /**
396
- * The minimum length of the input pcm required by `.enroll()`.
397
- */
398
- get minEnrollSamples(): number {
399
- return this._minEnrollSamples;
400
- }
401
-
402
- /**
403
- * Creates an instance of profiler component of the Eagle Speaker Recognition Engine.
404
- *
405
- * @param accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/).
406
- * @param model Eagle model options.
407
- * @param model.base64 The model in base64 string to initialize Eagle.
408
- * @param model.publicPath The model path relative to the public directory.
409
- * @param model.customWritePath Custom path to save the model in storage.
410
- * Set to a different name to use multiple models across `eagle` instances.
411
- * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
412
- * @param model.version Version of the model file. Increment to update the model file in storage.
413
- * @param options Optional configuration arguments.
414
- * @param options.device String representation of the device (e.g., CPU or GPU) to use. If set to `best`, the most
415
- * suitable device is selected automatically. If set to `gpu`, the engine uses the first available GPU device. To select a specific
416
- * GPU device, set this argument to `gpu:${GPU_INDEX}`, where `${GPU_INDEX}` is the index of the target GPU. If set to
417
- * `cpu`, the engine will run on the CPU with the default number of threads. To specify the number of threads, set this
418
- * argument to `cpu:${NUM_THREADS}`, where `${NUM_THREADS}` is the desired number of threads.
419
- *
420
- * @return An instance of the Eagle Profiler.
421
- */
422
- public static async create(
423
- accessKey: string,
424
- model: EagleModel,
425
- options: EagleProfilerOptions = {}
426
- ): Promise<EagleProfiler> {
427
- const customWritePath = model.customWritePath
428
- ? model.customWritePath
429
- : 'eagle_model';
430
- const modelPath = await loadModel({ ...model, customWritePath });
431
-
432
- return EagleProfiler._init(accessKey, modelPath, options);
433
- }
434
-
435
- public static async _init(
436
- accessKey: string,
437
- modelPath: string,
438
- options: EagleProfilerOptions = {}
439
- ): Promise<EagleProfiler> {
440
- if (!isAccessKeyValid(accessKey)) {
441
- throw new EagleErrors.EagleInvalidArgumentError('Invalid AccessKey');
442
- }
443
-
444
- let { device = "best" } = options;
445
-
446
- const isSimd = await simd();
447
- if (!isSimd) {
448
- throw new EagleErrors.EagleRuntimeError('Browser not supported.');
449
- }
450
-
451
- const isWorkerScope =
452
- typeof WorkerGlobalScope !== 'undefined' &&
453
- self instanceof WorkerGlobalScope;
454
- if (
455
- !isWorkerScope &&
456
- (device === 'best' || (device.startsWith('cpu') && device !== 'cpu:1'))
457
- ) {
458
- // eslint-disable-next-line no-console
459
- console.warn('Multi-threading is not supported on main thread.');
460
- device = 'cpu:1';
461
- }
462
-
463
- const sabDefined = typeof SharedArrayBuffer !== 'undefined'
464
- && (device !== "cpu:1");
465
-
466
- return new Promise<EagleProfiler>((resolve, reject) => {
467
- EagleProfiler._eagleMutex
468
- .runExclusive(async () => {
469
- const wasmOutput = await EagleProfiler._initProfilerWasm(
470
- accessKey.trim(),
471
- modelPath.trim(),
472
- device,
473
- sabDefined ? this._wasmPThread : this._wasmSimd,
474
- sabDefined ? this._wasmPThreadLib : this._wasmSimdLib,
475
- sabDefined ? createModulePThread : createModuleSimd
476
- );
477
- return new EagleProfiler(wasmOutput);
478
- })
479
- .then((result: EagleProfiler) => {
480
- resolve(result);
481
- })
482
- .catch((error: any) => {
483
- reject(error);
484
- });
485
- });
486
- }
487
-
488
- /**
489
- * Enrolls a speaker. This function should be called multiple times with different utterances of the same speaker
490
- * until `percentage` reaches `100.0`, at which point a speaker voice profile can be exported using `.export()`.
491
- * Any further enrollment can be used to improve the speaker profile. The minimum length of the input pcm to
492
- * `.enroll()` can be obtained by calling `.minEnrollSamples`.
493
- * The audio data used for enrollment should satisfy the following requirements:
494
- * - only one speaker should be present in the audio
495
- * - the speaker should be speaking in a normal voice
496
- * - the audio should contain no speech from other speakers and no other sounds (e.g. music)
497
- * - it should be captured in a quiet environment with no background noise
498
- * @param pcm Audio data for enrollment. The audio needs to have a sample rate equal to `.sampleRate` and be
499
- * 16-bit linearly-encoded. EagleProfiler operates on single-channel audio.
500
- *
501
- * @return The percentage of completeness of the speaker enrollment process along with the feedback code
502
- * corresponding to the last enrollment attempt:
503
- * - `AUDIO_OK`: The audio is good for enrollment.
504
- * - `AUDIO_TOO_SHORT`: Audio length is insufficient for enrollment,
505
- * i.e. it is shorter than`.min_enroll_samples`.
506
- * - `UNKNOWN_SPEAKER`: There is another speaker in the audio that is different from the speaker
507
- * being enrolled. Too much background noise may cause this error as well.
508
- * - `NO_VOICE_FOUND`: The audio does not contain any voice, i.e. it is silent or
509
- * has a low signal-to-noise ratio.
510
- * - `QUALITY_ISSUE`: The audio quality is too low for enrollment due to a bad microphone
511
- * or recording environment.
512
- */
513
- public async enroll(pcm: Int16Array): Promise<EagleProfilerEnrollResult> {
514
- if (!(pcm instanceof Int16Array)) {
515
- throw new EagleErrors.EagleInvalidArgumentError(
516
- "The argument 'pcm' must be provided as an Int16Array"
517
- );
518
- }
519
-
520
- if (pcm.length > this._maxEnrollSamples) {
521
- throw new EagleErrors.EagleInvalidArgumentError(
522
- `'pcm' size must be smaller than ${this._maxEnrollSamples}`
523
- );
524
- }
525
-
526
- return new Promise<EagleProfilerEnrollResult>((resolve, reject) => {
527
- this._functionMutex
528
- .runExclusive(async () => {
529
- if (this._module === undefined) {
530
- throw new EagleErrors.EagleInvalidStateError(
531
- 'Attempted to call `.enroll()` after release'
532
- );
533
- }
534
-
535
- const pcmAddress = this._module._malloc(
536
- pcm.length * Int16Array.BYTES_PER_ELEMENT
537
- );
538
-
539
- this._module.HEAP16.set(
540
- pcm,
541
- pcmAddress / Int16Array.BYTES_PER_ELEMENT
542
- );
543
- const status = await this._pv_eagle_profiler_enroll(
544
- this._objectAddress,
545
- pcmAddress,
546
- pcm.length,
547
- this._feedbackAddress,
548
- this._percentageAddress
549
- );
550
- this._module._pv_free(pcmAddress);
551
-
552
- if (status !== PV_STATUS_SUCCESS) {
553
- const messageStack = await EagleProfiler.getMessageStack(
554
- this._module._pv_get_error_stack,
555
- this._module._pv_free_error_stack,
556
- this._messageStackAddressAddressAddress,
557
- this._messageStackDepthAddress,
558
- this._module.HEAP32,
559
- this._module.HEAPU8
560
- );
561
-
562
- throw pvStatusToException(
563
- status,
564
- 'EagleProfiler enroll failed',
565
- messageStack
566
- );
567
- }
568
-
569
- const feedback =
570
- this._module.HEAP32[
571
- this._feedbackAddress / Int32Array.BYTES_PER_ELEMENT
572
- ];
573
- const percentage =
574
- this._module.HEAPF32[
575
- this._percentageAddress / Float32Array.BYTES_PER_ELEMENT
576
- ];
577
-
578
- return { feedback, percentage };
579
- })
580
- .then((result: EagleProfilerEnrollResult) => {
581
- resolve(result);
582
- })
583
- .catch((error: any) => {
584
- reject(error);
585
- });
586
- });
587
- }
588
-
589
- /**
590
- * Exports the speaker profile of the current session.
591
- * Will throw error if the profile is not ready.
592
- *
593
- * @return An EagleProfile object.
594
- */
595
- public async export(): Promise<EagleProfile> {
596
- return new Promise<EagleProfile>((resolve, reject) => {
597
- this._functionMutex
598
- .runExclusive(async () => {
599
- if (this._module === undefined) {
600
- throw new EagleErrors.EagleInvalidStateError(
601
- 'Attempted to call `.export()` after release'
602
- );
603
- }
604
-
605
- const profileAddress = this._module._malloc(
606
- Uint8Array.BYTES_PER_ELEMENT * this._profileSize
607
- );
608
-
609
- const status = this._module._pv_eagle_profiler_export(
610
- this._objectAddress,
611
- profileAddress
612
- );
613
- if (status !== PV_STATUS_SUCCESS) {
614
- this._module._pv_free(profileAddress);
615
-
616
- const messageStack = await EagleProfiler.getMessageStack(
617
- this._module._pv_get_error_stack,
618
- this._module._pv_free_error_stack,
619
- this._messageStackAddressAddressAddress,
620
- this._messageStackDepthAddress,
621
- this._module.HEAP32,
622
- this._module.HEAPU8
623
- );
624
-
625
- throw pvStatusToException(
626
- status,
627
- 'EagleProfiler export failed',
628
- messageStack
629
- );
630
- }
631
-
632
- const profile = this._module.HEAPU8.slice(
633
- profileAddress,
634
- profileAddress + Uint8Array.BYTES_PER_ELEMENT * this._profileSize
635
- );
636
- this._module._pv_free(profileAddress);
637
-
638
- return { bytes: profile };
639
- })
640
- .then((result: EagleProfile) => {
641
- resolve(result);
642
- })
643
- .catch((error: any) => {
644
- reject(error);
645
- });
646
- });
647
- }
648
-
649
- /**
650
- * Resets the internal state of Eagle Profiler.
651
- * It should be called before starting a new enrollment session.
652
- */
653
- public async reset(): Promise<void> {
654
- return new Promise<void>((resolve, reject) => {
655
- this._functionMutex
656
- .runExclusive(async () => {
657
- if (this._module === undefined) {
658
- throw new EagleErrors.EagleInvalidStateError(
659
- 'Attempted to call `.reset()` after release'
660
- );
661
- }
662
-
663
- const status = await this._pv_eagle_profiler_reset(
664
- this._objectAddress
665
- );
666
- if (status !== PV_STATUS_SUCCESS) {
667
- const messageStack = await EagleProfiler.getMessageStack(
668
- this._module._pv_get_error_stack,
669
- this._module._pv_free_error_stack,
670
- this._messageStackAddressAddressAddress,
671
- this._messageStackDepthAddress,
672
- this._module.HEAP32,
673
- this._module.HEAPU8
674
- );
675
-
676
- throw pvStatusToException(
677
- status,
678
- 'EagleProfiler reset failed',
679
- messageStack
680
- );
681
- }
682
- })
683
- .then(() => {
684
- resolve();
685
- })
686
- .catch((error: any) => {
687
- reject(error);
688
- });
689
- });
690
- }
691
-
692
- /**
693
- * Releases resources acquired by Eagle Profiler
694
- */
695
- public async release(): Promise<void> {
696
- if (!this._module) {
697
- return;
698
- }
699
-
700
- await super.release();
701
- await this._pv_eagle_profiler_delete(this._objectAddress);
702
- this._module = undefined;
703
- }
704
-
705
- private static async _initProfilerWasm(
706
- accessKey: string,
707
- modelPath: string,
708
- device: string,
709
- wasmBase64: string,
710
- wasmLibBase64: string,
711
- createModuleFunc: any
712
- ): Promise<EagleProfilerWasmOutput> {
713
- const baseWasmOutput = await super._initBaseWasm(
714
- wasmBase64,
715
- wasmLibBase64,
716
- createModuleFunc
717
- );
718
-
719
- const pv_eagle_profiler_init: pv_eagle_profiler_init_type =
720
- this.wrapAsyncFunction(
721
- baseWasmOutput.module,
722
- 'pv_eagle_profiler_init',
723
- 4
724
- );
725
- const pv_eagle_profiler_enroll: pv_eagle_profiler_enroll_type =
726
- this.wrapAsyncFunction(
727
- baseWasmOutput.module,
728
- 'pv_eagle_profiler_enroll',
729
- 5
730
- );
731
-
732
- const pv_eagle_profiler_reset: pv_eagle_profiler_reset_type =
733
- this.wrapAsyncFunction(
734
- baseWasmOutput.module,
735
- 'pv_eagle_profiler_reset',
736
- 1
737
- );
738
-
739
- const pv_eagle_profiler_delete: pv_eagle_profiler_delete_type =
740
- this.wrapAsyncFunction(
741
- baseWasmOutput.module,
742
- 'pv_eagle_profiler_delete',
743
- 1
744
- );
745
-
746
- const objectAddressAddress = baseWasmOutput.module._malloc(
747
- Int32Array.BYTES_PER_ELEMENT
748
- );
749
- if (objectAddressAddress === 0) {
750
- throw new EagleErrors.EagleOutOfMemoryError(
751
- 'malloc failed: Cannot allocate memory'
752
- );
753
- }
754
-
755
- const accessKeyAddress = baseWasmOutput.module._malloc(
756
- (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT
757
- );
758
- if (accessKeyAddress === 0) {
759
- throw new EagleErrors.EagleOutOfMemoryError(
760
- 'malloc failed: Cannot allocate memory'
761
- );
762
- }
763
- for (let i = 0; i < accessKey.length; i++) {
764
- baseWasmOutput.module.HEAPU8[accessKeyAddress + i] =
765
- accessKey.charCodeAt(i);
766
- }
767
- baseWasmOutput.module.HEAPU8[accessKeyAddress + accessKey.length] = 0;
768
-
769
- const modelPathEncoded = new TextEncoder().encode(modelPath);
770
- const modelPathAddress = baseWasmOutput.module._malloc(
771
- (modelPathEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
772
- );
773
- if (modelPathAddress === 0) {
774
- throw new EagleErrors.EagleOutOfMemoryError(
775
- 'malloc failed: Cannot allocate memory'
776
- );
777
- }
778
- baseWasmOutput.module.HEAPU8.set(modelPathEncoded, modelPathAddress);
779
- baseWasmOutput.module.HEAPU8[
780
- modelPathAddress + modelPathEncoded.length
781
- ] = 0;
782
-
783
- const deviceAddress = baseWasmOutput.module._malloc(
784
- (device.length + 1) * Uint8Array.BYTES_PER_ELEMENT
785
- );
786
- if (deviceAddress === 0) {
787
- throw new EagleErrors.EagleOutOfMemoryError(
788
- 'malloc failed: Cannot allocate memory'
789
- );
790
- }
791
- for (let i = 0; i < device.length; i++) {
792
- baseWasmOutput.module.HEAPU8[deviceAddress + i] = device.charCodeAt(i);
793
- }
794
- baseWasmOutput.module.HEAPU8[deviceAddress + device.length] = 0;
795
-
796
- let status = await pv_eagle_profiler_init(
797
- accessKeyAddress,
798
- modelPathAddress,
799
- deviceAddress,
800
- objectAddressAddress
801
- );
802
- baseWasmOutput.module._pv_free(accessKeyAddress);
803
- baseWasmOutput.module._pv_free(modelPathAddress);
804
- baseWasmOutput.module._pv_free(deviceAddress);
805
- if (status !== PvStatus.SUCCESS) {
806
- const messageStack = await EagleProfiler.getMessageStack(
807
- baseWasmOutput.module._pv_get_error_stack,
808
- baseWasmOutput.module._pv_free_error_stack,
809
- baseWasmOutput.messageStackAddressAddressAddress,
810
- baseWasmOutput.messageStackDepthAddress,
811
- baseWasmOutput.module.HEAP32,
812
- baseWasmOutput.module.HEAPU8
813
- );
814
-
815
- throw pvStatusToException(status, 'Initialization failed', messageStack);
816
- }
817
-
818
- const objectAddress =
819
- baseWasmOutput.module.HEAP32[
820
- objectAddressAddress / Int32Array.BYTES_PER_ELEMENT
821
- ];
822
- baseWasmOutput.module._pv_free(objectAddressAddress);
823
-
824
- const minEnrollSamplesAddress = baseWasmOutput.module._malloc(
825
- Int32Array.BYTES_PER_ELEMENT
826
- );
827
- if (minEnrollSamplesAddress === 0) {
828
- throw new EagleErrors.EagleOutOfMemoryError(
829
- 'malloc failed: Cannot allocate memory'
830
- );
831
- }
832
-
833
- status =
834
- baseWasmOutput.module._pv_eagle_profiler_enroll_min_audio_length_samples(
835
- objectAddress,
836
- minEnrollSamplesAddress
837
- );
838
- if (status !== PV_STATUS_SUCCESS) {
839
- const messageStack = await EagleProfiler.getMessageStack(
840
- baseWasmOutput.module._pv_get_error_stack,
841
- baseWasmOutput.module._pv_free_error_stack,
842
- baseWasmOutput.messageStackAddressAddressAddress,
843
- baseWasmOutput.messageStackDepthAddress,
844
- baseWasmOutput.module.HEAP32,
845
- baseWasmOutput.module.HEAPU8
846
- );
847
-
848
- throw pvStatusToException(
849
- status,
850
- 'EagleProfiler failed to get min enroll audio length',
851
- messageStack
852
- );
853
- }
854
-
855
- const minEnrollSamples =
856
- baseWasmOutput.module.HEAP32[
857
- minEnrollSamplesAddress / Int32Array.BYTES_PER_ELEMENT
858
- ];
859
- baseWasmOutput.module._pv_free(minEnrollSamplesAddress);
860
-
861
- const profileSizeAddress = baseWasmOutput.module._malloc(
862
- Int32Array.BYTES_PER_ELEMENT
863
- );
864
- if (profileSizeAddress === 0) {
865
- throw new EagleErrors.EagleOutOfMemoryError(
866
- 'malloc failed: Cannot allocate memory'
867
- );
868
- }
869
-
870
- status = baseWasmOutput.module._pv_eagle_profiler_export_size(
871
- objectAddress,
872
- profileSizeAddress
873
- );
874
- if (status !== PV_STATUS_SUCCESS) {
875
- const messageStack = await EagleProfiler.getMessageStack(
876
- baseWasmOutput.module._pv_get_error_stack,
877
- baseWasmOutput.module._pv_free_error_stack,
878
- baseWasmOutput.messageStackAddressAddressAddress,
879
- baseWasmOutput.messageStackDepthAddress,
880
- baseWasmOutput.module.HEAP32,
881
- baseWasmOutput.module.HEAPU8
882
- );
883
-
884
- throw pvStatusToException(
885
- status,
886
- 'EagleProfiler failed to get export size',
887
- messageStack
888
- );
889
- }
890
-
891
- const profileSize =
892
- baseWasmOutput.module.HEAP32[
893
- profileSizeAddress / Int32Array.BYTES_PER_ELEMENT
894
- ];
895
- baseWasmOutput.module._pv_free(profileSizeAddress);
896
-
897
- const feedbackAddress = baseWasmOutput.module._malloc(
898
- Int32Array.BYTES_PER_ELEMENT
899
- );
900
- if (feedbackAddress === 0) {
901
- throw new EagleErrors.EagleOutOfMemoryError(
902
- 'malloc failed: Cannot allocate memory'
903
- );
904
- }
905
-
906
- const percentageAddress = baseWasmOutput.module._malloc(
907
- Int32Array.BYTES_PER_ELEMENT
908
- );
909
- if (percentageAddress === 0) {
910
- throw new EagleErrors.EagleOutOfMemoryError(
911
- 'malloc failed: Cannot allocate memory'
912
- );
913
- }
914
-
915
- const profileAddress = baseWasmOutput.module._malloc(
916
- Uint8Array.BYTES_PER_ELEMENT * profileSize
917
- );
918
- if (profileAddress === 0) {
919
- throw new EagleErrors.EagleOutOfMemoryError(
920
- 'malloc failed: Cannot allocate memory'
921
- );
922
- }
923
-
924
- return {
925
- ...baseWasmOutput,
926
- minEnrollSamples: minEnrollSamples,
927
- profileSize: profileSize,
928
-
929
- objectAddress: objectAddress,
930
- feedbackAddress: feedbackAddress,
931
- percentageAddress: percentageAddress,
932
- profileAddress: profileAddress,
933
-
934
- pv_eagle_profiler_enroll: pv_eagle_profiler_enroll,
935
- pv_eagle_profiler_reset: pv_eagle_profiler_reset,
936
- pv_eagle_profiler_delete: pv_eagle_profiler_delete,
937
- };
938
- }
939
- }
940
-
941
- /**
942
- * JavaScript/WebAssembly binding for Eagle Speaker Recognition engine.
943
- * It processes incoming audio in consecutive frames and emits a similarity score for each enrolled speaker.
944
- */
945
- export class Eagle extends EagleBase {
946
- private readonly _pv_eagle_process: pv_eagle_process_type;
947
- private readonly _pv_eagle_reset: pv_eagle_reset_type;
948
- private readonly _pv_eagle_delete: pv_eagle_delete_type;
949
-
950
- private readonly _objectAddress: number;
951
- private readonly _scoresAddress: number;
952
-
953
- private readonly _frameLength: number;
954
- private readonly _numSpeakers: number;
955
-
956
- private constructor(handleWasm: EagleWasmOutput) {
957
- super(handleWasm);
958
-
959
- this._frameLength = handleWasm.frameLength;
960
- this._numSpeakers = handleWasm.numSpeakers;
961
-
962
- this._pv_eagle_process = handleWasm.pv_eagle_process;
963
- this._pv_eagle_reset = handleWasm.pv_eagle_reset;
964
- this._pv_eagle_delete = handleWasm.pv_eagle_delete;
965
-
966
- this._objectAddress = handleWasm.objectAddress;
967
- this._scoresAddress = handleWasm.scoresAddress;
968
- }
969
-
970
- /**
971
- * Number of audio samples per frame expected by Eagle (i.e. length of the array passed into `.process()`)
972
- */
973
- get frameLength(): number {
974
- return this._frameLength;
975
- }
976
-
977
- /**
978
- * Creates an instance of the Picovoice Eagle Speaker Recognition Engine.
979
- *
980
- * @param accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/)
981
- * @param model Eagle model options.
982
- * @param model.base64 The model in base64 string to initialize Eagle.
983
- * @param model.publicPath The model path relative to the public directory.
984
- * @param model.customWritePath Custom path to save the model in storage.
985
- * Set to a different name to use multiple models across `eagle` instances.
986
- * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
987
- * @param model.version Version of the model file. Increment to update the model file in storage.
988
- * @param speakerProfiles One or more Eagle speaker profiles. These can be constructed using `EagleProfiler`.
989
- * @param options Optional configuration arguments.
990
- * @param options.device String representation of the device (e.g., CPU or GPU) to use. If set to `best`, the most
991
- * suitable device is selected automatically. If set to `gpu`, the engine uses the first available GPU device. To select a specific
992
- * GPU device, set this argument to `gpu:${GPU_INDEX}`, where `${GPU_INDEX}` is the index of the target GPU. If set to
993
- * `cpu`, the engine will run on the CPU with the default number of threads. To specify the number of threads, set this
994
- * argument to `cpu:${NUM_THREADS}`, where `${NUM_THREADS}` is the desired number of threads.
995
- *
996
- * @return An instance of the Eagle engine.
997
- */
998
- public static async create(
999
- accessKey: string,
1000
- model: EagleModel,
1001
- speakerProfiles: EagleProfile[] | EagleProfile,
1002
- options: EagleOptions = {}
1003
- ): Promise<Eagle> {
1004
- const customWritePath = model.customWritePath
1005
- ? model.customWritePath
1006
- : 'eagle_model';
1007
- const modelPath = await loadModel({ ...model, customWritePath });
1008
-
1009
- return Eagle._init(
1010
- accessKey,
1011
- modelPath,
1012
- !Array.isArray(speakerProfiles) ? [speakerProfiles] : speakerProfiles,
1013
- options
1014
- );
1015
- }
1016
-
1017
- public static async _init(
1018
- accessKey: string,
1019
- modelPath: string,
1020
- speakerProfiles: EagleProfile[],
1021
- options: EagleOptions = {}
1022
- ): Promise<Eagle> {
1023
- if (!isAccessKeyValid(accessKey)) {
1024
- throw new EagleErrors.EagleInvalidArgumentError('Invalid AccessKey');
1025
- }
1026
-
1027
- if (!speakerProfiles || speakerProfiles.length === 0) {
1028
- throw new EagleErrors.EagleInvalidArgumentError(
1029
- 'No speaker profiles provided'
1030
- );
1031
- }
1032
-
1033
- let { device = "best" } = options;
1034
-
1035
- const isSimd = await simd();
1036
- if (!isSimd) {
1037
- throw new EagleErrors.EagleRuntimeError('Browser not supported.');
1038
- }
1039
-
1040
- const isWorkerScope =
1041
- typeof WorkerGlobalScope !== 'undefined' &&
1042
- self instanceof WorkerGlobalScope;
1043
- if (
1044
- !isWorkerScope &&
1045
- (device === 'best' || (device.startsWith('cpu') && device !== 'cpu:1'))
1046
- ) {
1047
- // eslint-disable-next-line no-console
1048
- console.warn('Multi-threading is not supported on main thread.');
1049
- device = 'cpu:1';
1050
- }
1051
-
1052
- const sabDefined = typeof SharedArrayBuffer !== 'undefined'
1053
- && (device !== "cpu:1");
1054
-
1055
- return new Promise<Eagle>((resolve, reject) => {
1056
- Eagle._eagleMutex
1057
- .runExclusive(async () => {
1058
- const wasmOutput = await Eagle._initWasm(
1059
- accessKey.trim(),
1060
- modelPath.trim(),
1061
- device,
1062
- speakerProfiles,
1063
- sabDefined ? this._wasmPThread : this._wasmSimd,
1064
- sabDefined ? this._wasmPThreadLib : this._wasmSimdLib,
1065
- sabDefined ? createModulePThread : createModuleSimd
1066
- );
1067
- return new Eagle(wasmOutput);
1068
- })
1069
- .then((result: Eagle) => {
1070
- resolve(result);
1071
- })
1072
- .catch((error: any) => {
1073
- reject(error);
1074
- });
1075
- });
1076
- }
1077
-
1078
- /**
1079
- * Processes a frame of audio and returns a list of similarity scores for each speaker profile.
1080
- *
1081
- * @param pcm A frame of audio samples. The number of samples per frame can be attained by calling
1082
- * `.frameLength`. The incoming audio needs to have a sample rate equal to `.sampleRate` and be 16-bit
1083
- * linearly-encoded. Eagle operates on single-channel audio.
1084
- *
1085
- * @return A list of similarity scores for each speaker profile. A higher score indicates that the voice
1086
- * belongs to the corresponding speaker. The range is [0, 1] with 1.0 representing a perfect match.
1087
- */
1088
- public async process(pcm: Int16Array): Promise<number[]> {
1089
- if (!(pcm instanceof Int16Array)) {
1090
- throw new EagleErrors.EagleInvalidArgumentError(
1091
- "The argument 'pcm' must be provided as an Int16Array"
1092
- );
1093
- }
1094
-
1095
- if (pcm.length !== this._frameLength) {
1096
- throw new EagleErrors.EagleInvalidArgumentError(
1097
- `Length of input frame (${pcm.length}) does not match required frame length (${this._frameLength})`
1098
- );
1099
- }
1100
-
1101
- return new Promise<number[]>((resolve, reject) => {
1102
- this._functionMutex
1103
- .runExclusive(async () => {
1104
- if (this._module === undefined) {
1105
- throw new EagleErrors.EagleInvalidStateError(
1106
- 'Attempted to call `.process` after release'
1107
- );
1108
- }
1109
-
1110
- const pcmAddress = this._module._malloc(
1111
- pcm.length * Int16Array.BYTES_PER_ELEMENT
1112
- );
1113
-
1114
- this._module.HEAP16.set(
1115
- pcm,
1116
- pcmAddress / Int16Array.BYTES_PER_ELEMENT
1117
- );
1118
-
1119
- const status = await this._pv_eagle_process(
1120
- this._objectAddress,
1121
- pcmAddress,
1122
- this._scoresAddress
1123
- );
1124
- this._module._pv_free(pcmAddress);
1125
-
1126
- if (status !== PV_STATUS_SUCCESS) {
1127
- const messageStack = await Eagle.getMessageStack(
1128
- this._module._pv_get_error_stack,
1129
- this._module._pv_free_error_stack,
1130
- this._messageStackAddressAddressAddress,
1131
- this._messageStackDepthAddress,
1132
- this._module.HEAP32,
1133
- this._module.HEAPU8
1134
- );
1135
-
1136
- throw pvStatusToException(
1137
- status,
1138
- 'Eagle process failed',
1139
- messageStack
1140
- );
1141
- }
1142
-
1143
- const scores: number[] = [];
1144
- for (let i = 0; i < this._numSpeakers; i++) {
1145
- scores.push(
1146
- this._module.HEAPF32[
1147
- this._scoresAddress / Float32Array.BYTES_PER_ELEMENT + i
1148
- ]
1149
- );
1150
- }
1151
-
1152
- return scores;
1153
- })
1154
- .then((result: number[]) => {
1155
- resolve(result);
1156
- })
1157
- .catch((error: any) => {
1158
- reject(error);
1159
- });
1160
- });
1161
- }
1162
-
1163
- /**
1164
- * Resets the internal state of the engine.
1165
- * It is best to call before processing a new sequence of audio (e.g. a new voice interaction).
1166
- * This ensures that the accuracy of the engine is not affected by a change in audio context.
1167
- */
1168
- public async reset(): Promise<void> {
1169
- return new Promise<void>((resolve, reject) => {
1170
- this._functionMutex
1171
- .runExclusive(async () => {
1172
- if (this._module === undefined) {
1173
- throw new EagleErrors.EagleInvalidStateError(
1174
- 'Attempted to call `.reset` after release'
1175
- );
1176
- }
1177
-
1178
- const status = await this._pv_eagle_reset(this._objectAddress);
1179
- if (status !== PV_STATUS_SUCCESS) {
1180
- const messageStack = await Eagle.getMessageStack(
1181
- this._module._pv_get_error_stack,
1182
- this._module._pv_free_error_stack,
1183
- this._messageStackAddressAddressAddress,
1184
- this._messageStackDepthAddress,
1185
- this._module.HEAP32,
1186
- this._module.HEAPU8
1187
- );
1188
-
1189
- throw pvStatusToException(
1190
- status,
1191
- 'Eagle reset failed',
1192
- messageStack
1193
- );
1194
- }
1195
- })
1196
- .then(() => {
1197
- resolve();
1198
- })
1199
- .catch((error: any) => {
1200
- reject(error);
1201
- });
1202
- });
1203
- }
1204
-
1205
- /**
1206
- * Releases resources acquired by Eagle
1207
- */
1208
- public async release(): Promise<void> {
1209
- if (!this._module) {
1210
- return;
1211
- }
1212
-
1213
- await super.release();
1214
- await this._pv_eagle_delete(this._objectAddress);
1215
- this._module = undefined;
1216
- }
1217
-
1218
- /**
1219
- * Lists all available devices that Eagle can use for inference.
1220
- * Each entry in the list can be the used as the `device` argument for the `.create` method.
1221
- *
1222
- * @returns List of all available devices that Eagle can use for inference.
1223
- */
1224
- public static async listAvailableDevices(): Promise<string[]> {
1225
- return new Promise<string[]>((resolve, reject) => {
1226
- Eagle._eagleMutex
1227
- .runExclusive(async () => {
1228
- const isSimd = await simd();
1229
- if (!isSimd) {
1230
- throw new EagleErrors.EagleRuntimeError('Unsupported Browser');
1231
- }
1232
-
1233
- const blob = new Blob([base64ToUint8Array(this._wasmSimdLib)], {
1234
- type: 'application/javascript',
1235
- });
1236
- const module: EagleModule = await createModuleSimd({
1237
- mainScriptUrlOrBlob: blob,
1238
- wasmBinary: base64ToUint8Array(this._wasmSimd),
1239
- });
1240
-
1241
- const hardwareDevicesAddressAddress = module._malloc(
1242
- Int32Array.BYTES_PER_ELEMENT
1243
- );
1244
- if (hardwareDevicesAddressAddress === 0) {
1245
- throw new EagleErrors.EagleOutOfMemoryError(
1246
- 'malloc failed: Cannot allocate memory for hardwareDevices'
1247
- );
1248
- }
1249
-
1250
- const numHardwareDevicesAddress = module._malloc(
1251
- Int32Array.BYTES_PER_ELEMENT
1252
- );
1253
- if (numHardwareDevicesAddress === 0) {
1254
- throw new EagleErrors.EagleOutOfMemoryError(
1255
- 'malloc failed: Cannot allocate memory for numHardwareDevices'
1256
- );
1257
- }
1258
-
1259
- const status: PvStatus = module._pv_eagle_list_hardware_devices(
1260
- hardwareDevicesAddressAddress,
1261
- numHardwareDevicesAddress
1262
- );
1263
-
1264
- const messageStackDepthAddress = module._malloc(
1265
- Int32Array.BYTES_PER_ELEMENT
1266
- );
1267
- if (!messageStackDepthAddress) {
1268
- throw new EagleErrors.EagleOutOfMemoryError(
1269
- 'malloc failed: Cannot allocate memory for messageStackDepth'
1270
- );
1271
- }
1272
-
1273
- const messageStackAddressAddressAddress = module._malloc(
1274
- Int32Array.BYTES_PER_ELEMENT
1275
- );
1276
- if (!messageStackAddressAddressAddress) {
1277
- throw new EagleErrors.EagleOutOfMemoryError(
1278
- 'malloc failed: Cannot allocate memory messageStack'
1279
- );
1280
- }
1281
-
1282
- if (status !== PvStatus.SUCCESS) {
1283
- const messageStack = await Eagle.getMessageStack(
1284
- module._pv_get_error_stack,
1285
- module._pv_free_error_stack,
1286
- messageStackAddressAddressAddress,
1287
- messageStackDepthAddress,
1288
- module.HEAP32,
1289
- module.HEAPU8
1290
- );
1291
- module._pv_free(messageStackAddressAddressAddress);
1292
- module._pv_free(messageStackDepthAddress);
1293
-
1294
- throw pvStatusToException(
1295
- status,
1296
- 'List devices failed',
1297
- messageStack
1298
- );
1299
- }
1300
- module._pv_free(messageStackAddressAddressAddress);
1301
- module._pv_free(messageStackDepthAddress);
1302
-
1303
- const numHardwareDevices: number =
1304
- module.HEAP32[
1305
- numHardwareDevicesAddress / Int32Array.BYTES_PER_ELEMENT
1306
- ];
1307
- module._pv_free(numHardwareDevicesAddress);
1308
-
1309
- const hardwareDevicesAddress =
1310
- module.HEAP32[
1311
- hardwareDevicesAddressAddress / Int32Array.BYTES_PER_ELEMENT
1312
- ];
1313
-
1314
- const hardwareDevices: string[] = [];
1315
- for (let i = 0; i < numHardwareDevices; i++) {
1316
- const deviceAddress =
1317
- module.HEAP32[
1318
- hardwareDevicesAddress / Int32Array.BYTES_PER_ELEMENT + i
1319
- ];
1320
- hardwareDevices.push(
1321
- arrayBufferToStringAtIndex(module.HEAPU8, deviceAddress)
1322
- );
1323
- }
1324
- module._pv_eagle_free_hardware_devices(
1325
- hardwareDevicesAddress,
1326
- numHardwareDevices
1327
- );
1328
- module._pv_free(hardwareDevicesAddressAddress);
1329
-
1330
- return hardwareDevices;
1331
- })
1332
- .then((result: string[]) => {
1333
- resolve(result);
1334
- })
1335
- .catch((error: any) => {
1336
- reject(error);
1337
- });
1338
- });
1339
- }
1340
-
1341
- private static async _initWasm(
1342
- accessKey: string,
1343
- modelPath: string,
1344
- device: string,
1345
- speakerProfiles: EagleProfile[],
1346
- wasmBase64: string,
1347
- wasmLibBase64: string,
1348
- createModuleFunc: any
1349
- ): Promise<EagleWasmOutput> {
1350
- const baseWasmOutput = await super._initBaseWasm(
1351
- wasmBase64,
1352
- wasmLibBase64,
1353
- createModuleFunc
1354
- );
1355
-
1356
- const pv_eagle_init: pv_eagle_init_type = this.wrapAsyncFunction(
1357
- baseWasmOutput.module,
1358
- 'pv_eagle_init',
1359
- 6
1360
- );
1361
- const pv_eagle_process: pv_eagle_process_type = this.wrapAsyncFunction(
1362
- baseWasmOutput.module,
1363
- 'pv_eagle_process',
1364
- 3
1365
- );
1366
- const pv_eagle_reset: pv_eagle_reset_type = this.wrapAsyncFunction(
1367
- baseWasmOutput.module,
1368
- 'pv_eagle_reset',
1369
- 1
1370
- );
1371
- const pv_eagle_delete: pv_eagle_delete_type = this.wrapAsyncFunction(
1372
- baseWasmOutput.module,
1373
- 'pv_eagle_delete',
1374
- 1
1375
- );
1376
-
1377
- const objectAddressAddress = baseWasmOutput.module._malloc(
1378
- Int32Array.BYTES_PER_ELEMENT
1379
- );
1380
- if (objectAddressAddress === 0) {
1381
- throw new EagleErrors.EagleOutOfMemoryError(
1382
- 'malloc failed: Cannot allocate memory'
1383
- );
1384
- }
1385
-
1386
- const accessKeyAddress = baseWasmOutput.module._malloc(
1387
- (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1388
- );
1389
- if (accessKeyAddress === 0) {
1390
- throw new EagleErrors.EagleOutOfMemoryError(
1391
- 'malloc failed: Cannot allocate memory'
1392
- );
1393
- }
1394
- for (let i = 0; i < accessKey.length; i++) {
1395
- baseWasmOutput.module.HEAPU8[accessKeyAddress + i] =
1396
- accessKey.charCodeAt(i);
1397
- }
1398
- baseWasmOutput.module.HEAPU8[accessKeyAddress + accessKey.length] = 0;
1399
-
1400
- const modelPathEncoded = new TextEncoder().encode(modelPath);
1401
- const modelPathAddress = baseWasmOutput.module._malloc(
1402
- (modelPathEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1403
- );
1404
- if (modelPathAddress === 0) {
1405
- throw new EagleErrors.EagleOutOfMemoryError(
1406
- 'malloc failed: Cannot allocate memory'
1407
- );
1408
- }
1409
- baseWasmOutput.module.HEAPU8.set(modelPathEncoded, modelPathAddress);
1410
- baseWasmOutput.module.HEAPU8[
1411
- modelPathAddress + modelPathEncoded.length
1412
- ] = 0;
1413
-
1414
- const deviceAddress = baseWasmOutput.module._malloc(
1415
- (device.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1416
- );
1417
- if (deviceAddress === 0) {
1418
- throw new EagleErrors.EagleOutOfMemoryError(
1419
- 'malloc failed: Cannot allocate memory'
1420
- );
1421
- }
1422
- for (let i = 0; i < device.length; i++) {
1423
- baseWasmOutput.module.HEAPU8[deviceAddress + i] = device.charCodeAt(i);
1424
- }
1425
- baseWasmOutput.module.HEAPU8[deviceAddress + device.length] = 0;
1426
-
1427
- const numSpeakers = speakerProfiles.length;
1428
- const profilesAddressAddress = baseWasmOutput.module._malloc(
1429
- numSpeakers * Int32Array.BYTES_PER_ELEMENT
1430
- );
1431
- if (profilesAddressAddress === 0) {
1432
- throw new EagleErrors.EagleOutOfMemoryError(
1433
- 'malloc failed: Cannot allocate memory'
1434
- );
1435
- }
1436
- const profilesAddressList: number[] = [];
1437
- for (const profile of speakerProfiles) {
1438
- const profileAddress = baseWasmOutput.module._malloc(
1439
- profile.bytes.length * Uint8Array.BYTES_PER_ELEMENT
1440
- );
1441
- if (profileAddress === 0) {
1442
- throw new EagleErrors.EagleOutOfMemoryError(
1443
- 'malloc failed: Cannot allocate memory'
1444
- );
1445
- }
1446
- baseWasmOutput.module.HEAPU8.set(profile.bytes, profileAddress);
1447
- profilesAddressList.push(profileAddress);
1448
- }
1449
- baseWasmOutput.module.HEAP32.set(
1450
- new Int32Array(profilesAddressList),
1451
- profilesAddressAddress / Int32Array.BYTES_PER_ELEMENT
1452
- );
1453
- const status = await pv_eagle_init(
1454
- accessKeyAddress,
1455
- modelPathAddress,
1456
- deviceAddress,
1457
- numSpeakers,
1458
- profilesAddressAddress,
1459
- objectAddressAddress
1460
- );
1461
- baseWasmOutput.module._pv_free(accessKeyAddress);
1462
- baseWasmOutput.module._pv_free(modelPathAddress);
1463
- baseWasmOutput.module._pv_free(deviceAddress);
1464
- baseWasmOutput.module._pv_free(profilesAddressAddress);
1465
-
1466
- if (status !== PV_STATUS_SUCCESS) {
1467
- const messageStack = await Eagle.getMessageStack(
1468
- baseWasmOutput.module._pv_get_error_stack,
1469
- baseWasmOutput.module._pv_free_error_stack,
1470
- baseWasmOutput.messageStackAddressAddressAddress,
1471
- baseWasmOutput.messageStackDepthAddress,
1472
- baseWasmOutput.module.HEAP32,
1473
- baseWasmOutput.module.HEAPU8
1474
- );
1475
-
1476
- throw pvStatusToException(status, 'Eagle init failed', messageStack);
1477
- }
1478
-
1479
- const objectAddress =
1480
- baseWasmOutput.module.HEAP32[
1481
- objectAddressAddress / Int32Array.BYTES_PER_ELEMENT
1482
- ];
1483
- baseWasmOutput.module._pv_free(objectAddressAddress);
1484
-
1485
- const scoresAddress = baseWasmOutput.module._malloc(
1486
- Float32Array.BYTES_PER_ELEMENT * numSpeakers
1487
- );
1488
- if (scoresAddress === 0) {
1489
- throw new EagleErrors.EagleOutOfMemoryError(
1490
- 'malloc failed: Cannot allocate memory'
1491
- );
1492
- }
1493
-
1494
- const frameLength = baseWasmOutput.module._pv_eagle_frame_length();
1495
-
1496
- return {
1497
- ...baseWasmOutput,
1498
- frameLength: frameLength,
1499
- numSpeakers: numSpeakers,
1500
- objectAddress: objectAddress,
1501
- scoresAddress: scoresAddress,
1502
-
1503
- pv_eagle_process: pv_eagle_process,
1504
- pv_eagle_reset: pv_eagle_reset,
1505
- pv_eagle_delete: pv_eagle_delete,
1506
- };
1507
- }
1508
- }
1
+ /*
2
+ Copyright 2023-2026 Picovoice Inc.
3
+
4
+ You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE"
5
+ file accompanying this source.
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ */
11
+
12
+ /* eslint camelcase: 0 */
13
+
14
+ import { Mutex } from 'async-mutex';
15
+
16
+ import {
17
+ arrayBufferToStringAtIndex,
18
+ base64ToUint8Array,
19
+ isAccessKeyValid,
20
+ loadModel,
21
+ } from '@picovoice/web-utils';
22
+
23
+ import createModuleSimd from "./lib/pv_eagle_simd";
24
+ import createModulePThread from "./lib/pv_eagle_pthread";
25
+
26
+ import { simd } from 'wasm-feature-detect';
27
+
28
+ import {
29
+ EagleModel,
30
+ EagleOptions,
31
+ EagleProfile,
32
+ EagleProfilerOptions,
33
+ PvStatus
34
+ } from './types';
35
+
36
+ import * as EagleErrors from './eagle_errors';
37
+ import { pvStatusToException } from './eagle_errors';
38
+
39
+ /**
40
+ * WebAssembly function types
41
+ */
42
+ type pv_eagle_profiler_init_type = (
43
+ accessKey: number,
44
+ modelPath: number,
45
+ device: number,
46
+ min_enrollment_chunks,
47
+ voice_threshold: number,
48
+ object: number
49
+ ) => Promise<number>;
50
+ type pv_eagle_profiler_delete_type = (object: number) => Promise<void>;
51
+ type pv_eagle_profiler_enroll_type = (
52
+ object: number,
53
+ pcm: number,
54
+ percentage: number
55
+ ) => Promise<number>;
56
+ type pv_eagle_profiler_flush_type = (
57
+ object: number,
58
+ percentage: number
59
+ ) => Promise<number>;
60
+ type pv_eagle_profiler_frame_length_type = () => number;
61
+ type pv_eagle_profiler_export_type = (
62
+ object: number,
63
+ speakerProfile: number
64
+ ) => number;
65
+ type pv_eagle_profiler_export_size_type = (
66
+ object: number,
67
+ speakerProfileSizeBytes: number
68
+ ) => number;
69
+ type pv_eagle_profiler_reset_type = (object: number) => Promise<number>;
70
+ type pv_eagle_init_type = (
71
+ accessKey: number,
72
+ modelPath: number,
73
+ device: number,
74
+ voiceThreshold: number,
75
+ object: number
76
+ ) => Promise<number>;
77
+ type pv_eagle_delete_type = (object: number) => Promise<void>;
78
+ type pv_eagle_process_type = (
79
+ object: number,
80
+ pcm: number,
81
+ pcmLength: number,
82
+ speakerProfiles: number,
83
+ numSpeakers: number,
84
+ scores: number
85
+ ) => Promise<number>;
86
+ type pv_eagle_scores_delete_type = (
87
+ scores: number
88
+ ) => Promise<void>;
89
+ type pv_eagle_process_min_audio_length_samples_type = (
90
+ object: number,
91
+ numSamples: number
92
+ ) => number;
93
+ type pv_eagle_version_type = () => number;
94
+ type pv_eagle_list_hardware_devices_type = (
95
+ hardwareDevices: number,
96
+ numHardwareDevices: number
97
+ ) => number;
98
+ type pv_eagle_free_hardware_devices_type = (
99
+ hardwareDevices: number,
100
+ numHardwareDevices: number
101
+ ) => number;
102
+ type pv_sample_rate_type = () => number;
103
+ type pv_set_sdk_type = (sdk: number) => void;
104
+ type pv_get_error_stack_type = (
105
+ messageStack: number,
106
+ messageStackDepth: number
107
+ ) => number;
108
+ type pv_free_error_stack_type = (messageStack: number) => void;
109
+
110
+ type EagleModule = EmscriptenModule & {
111
+ _pv_free: (address: number) => void;
112
+
113
+ _pv_eagle_profiler_export: pv_eagle_profiler_export_type
114
+ _pv_eagle_profiler_export_size: pv_eagle_profiler_export_size_type
115
+ _pv_eagle_profiler_frame_length: pv_eagle_profiler_frame_length_type
116
+ _pv_eagle_process_min_audio_length_samples: pv_eagle_process_min_audio_length_samples_type
117
+ _pv_eagle_version: pv_eagle_version_type
118
+ _pv_eagle_list_hardware_devices: pv_eagle_list_hardware_devices_type;
119
+ _pv_eagle_free_hardware_devices: pv_eagle_free_hardware_devices_type;
120
+ _pv_sample_rate: pv_sample_rate_type
121
+
122
+ _pv_set_sdk: pv_set_sdk_type;
123
+ _pv_get_error_stack: pv_get_error_stack_type;
124
+ _pv_free_error_stack: pv_free_error_stack_type;
125
+
126
+ // em default functions
127
+ addFunction: typeof addFunction;
128
+ ccall: typeof ccall;
129
+ cwrap: typeof cwrap;
130
+ }
131
+
132
+ type EagleBaseWasmOutput = {
133
+ module: EagleModule;
134
+
135
+ sampleRate: number;
136
+ version: string;
137
+
138
+ messageStackAddressAddressAddress: number;
139
+ messageStackDepthAddress: number;
140
+ };
141
+
142
+ type EagleProfilerWasmOutput = EagleBaseWasmOutput & {
143
+ frameLength: number;
144
+ profileSize: number;
145
+
146
+ objectAddress: number;
147
+ percentageAddress: number;
148
+ profileAddress: number;
149
+
150
+ pv_eagle_profiler_enroll: pv_eagle_profiler_enroll_type;
151
+ pv_eagle_profiler_flush: pv_eagle_profiler_flush_type;
152
+ pv_eagle_profiler_reset: pv_eagle_profiler_reset_type;
153
+ pv_eagle_profiler_delete: pv_eagle_profiler_delete_type;
154
+ };
155
+
156
+ type EagleWasmOutput = EagleBaseWasmOutput & {
157
+ minProcessSamples: number;
158
+
159
+ objectAddress: number;
160
+ scoresAddressAddress: number;
161
+
162
+ pv_eagle_process: pv_eagle_process_type;
163
+ pv_eagle_scores_delete: pv_eagle_scores_delete_type;
164
+ pv_eagle_delete: pv_eagle_delete_type;
165
+ };
166
+
167
+ const PV_STATUS_SUCCESS = 10000;
168
+ const MAX_PCM_LENGTH_SEC = 60 * 15;
169
+
170
+ class EagleBase {
171
+ protected _module?: EagleModule;
172
+
173
+ protected readonly _functionMutex: Mutex;
174
+
175
+ protected readonly _messageStackAddressAddressAddress: number;
176
+ protected readonly _messageStackDepthAddress: number;
177
+
178
+ protected readonly _sampleRate: number;
179
+ protected readonly _version: string;
180
+
181
+ protected static _wasmSimd: string;
182
+ protected static _wasmSimdLib: string;
183
+ protected static _wasmPThread: string;
184
+ protected static _wasmPThreadLib: string;
185
+
186
+ protected static _sdk: string = 'web';
187
+
188
+ protected static _eagleMutex = new Mutex();
189
+
190
+ protected constructor(handleWasm: EagleBaseWasmOutput) {
191
+ this._module = handleWasm.module;
192
+
193
+ this._sampleRate = handleWasm.sampleRate;
194
+ this._version = handleWasm.version;
195
+
196
+ this._messageStackAddressAddressAddress = handleWasm.messageStackAddressAddressAddress;
197
+ this._messageStackDepthAddress = handleWasm.messageStackDepthAddress;
198
+
199
+ this._functionMutex = new Mutex();
200
+ }
201
+
202
+ /**
203
+ * Audio sample rate required by Eagle.
204
+ */
205
+ get sampleRate(): number {
206
+ return this._sampleRate;
207
+ }
208
+
209
+ /**
210
+ * Version of Eagle.
211
+ */
212
+ get version(): string {
213
+ return this._version;
214
+ }
215
+
216
+ /**
217
+ * Set base64 wasm file with SIMD feature.
218
+ * @param wasmSimd Base64'd wasm file to use to initialize wasm.
219
+ */
220
+ public static setWasmSimd(wasmSimd: string): void {
221
+ if (this._wasmSimd === undefined) {
222
+ this._wasmSimd = wasmSimd;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Set base64 SIMD wasm file in text format.
228
+ * @param wasmSimdLib Base64'd wasm file in text format.
229
+ */
230
+ public static setWasmSimdLib(wasmSimdLib: string): void {
231
+ if (this._wasmSimdLib === undefined) {
232
+ this._wasmSimdLib = wasmSimdLib;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Set base64 wasm file with SIMD and pthread feature.
238
+ * @param wasmPThread Base64'd wasm file to use to initialize wasm.
239
+ */
240
+ public static setWasmPThread(wasmPThread: string): void {
241
+ if (this._wasmPThread === undefined) {
242
+ this._wasmPThread = wasmPThread;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Set base64 SIMD and thread wasm file in text format.
248
+ * @param wasmPThreadLib Base64'd wasm file in text format.
249
+ */
250
+ public static setWasmPThreadLib(wasmPThreadLib: string): void {
251
+ if (this._wasmPThreadLib === undefined) {
252
+ this._wasmPThreadLib = wasmPThreadLib;
253
+ }
254
+ }
255
+
256
+ public static setSdk(sdk: string): void {
257
+ EagleBase._sdk = sdk;
258
+ }
259
+
260
+ protected static async _initBaseWasm(
261
+ wasmBase64: string,
262
+ wasmLibBase64: string,
263
+ createModuleFunc: any,
264
+ ): Promise<EagleBaseWasmOutput> {
265
+ const blob = new Blob(
266
+ [base64ToUint8Array(wasmLibBase64)],
267
+ { type: 'application/javascript' }
268
+ );
269
+ const module: EagleModule = await createModuleFunc({
270
+ mainScriptUrlOrBlob: blob,
271
+ wasmBinary: base64ToUint8Array(wasmBase64),
272
+ });
273
+
274
+ const sampleRate = module._pv_sample_rate();
275
+ const versionAddress = module._pv_eagle_version();
276
+ const version = arrayBufferToStringAtIndex(
277
+ module.HEAPU8,
278
+ versionAddress,
279
+ );
280
+
281
+ const sdkEncoded = new TextEncoder().encode(this._sdk);
282
+ const sdkAddress = module._malloc((sdkEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT);
283
+ if (!sdkAddress) {
284
+ throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
285
+ }
286
+ module.HEAPU8.set(sdkEncoded, sdkAddress);
287
+ module.HEAPU8[sdkAddress + sdkEncoded.length] = 0;
288
+ module._pv_set_sdk(sdkAddress);
289
+ module._pv_free(sdkAddress);
290
+
291
+ const messageStackDepthAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT);
292
+ if (!messageStackDepthAddress) {
293
+ throw new EagleErrors.EagleOutOfMemoryError(
294
+ 'malloc failed: Cannot allocate memory'
295
+ );
296
+ }
297
+
298
+ const messageStackAddressAddressAddress = module._malloc(Int32Array.BYTES_PER_ELEMENT);
299
+ if (!messageStackAddressAddressAddress) {
300
+ throw new EagleErrors.EagleOutOfMemoryError(
301
+ 'malloc failed: Cannot allocate memory'
302
+ );
303
+ }
304
+
305
+ return {
306
+ module: module,
307
+
308
+ sampleRate: sampleRate,
309
+ version: version,
310
+
311
+ messageStackAddressAddressAddress: messageStackAddressAddressAddress,
312
+ messageStackDepthAddress: messageStackDepthAddress,
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Releases resources acquired by Eagle
318
+ */
319
+ public async release(): Promise<void> {
320
+ if (!this._module) {
321
+ return;
322
+ }
323
+ this._module._pv_free(this._messageStackAddressAddressAddress);
324
+ this._module._pv_free(this._messageStackDepthAddress);
325
+ }
326
+
327
+ protected static async getMessageStack(
328
+ pv_get_error_stack: pv_get_error_stack_type,
329
+ pv_free_error_stack: pv_free_error_stack_type,
330
+ messageStackAddressAddressAddress: number,
331
+ messageStackDepthAddress: number,
332
+ memoryBufferInt32: Int32Array,
333
+ memoryBufferUint8: Uint8Array
334
+ ): Promise<string[]> {
335
+ const status = pv_get_error_stack(messageStackAddressAddressAddress, messageStackDepthAddress);
336
+ if (status !== PvStatus.SUCCESS) {
337
+ throw pvStatusToException(status, 'Unable to get Eagle error state');
338
+ }
339
+
340
+ const messageStackAddressAddress = memoryBufferInt32[messageStackAddressAddressAddress / Int32Array.BYTES_PER_ELEMENT];
341
+
342
+ const messageStackDepth = memoryBufferInt32[messageStackDepthAddress / Int32Array.BYTES_PER_ELEMENT];
343
+ const messageStack: string[] = [];
344
+ for (let i = 0; i < messageStackDepth; i++) {
345
+ const messageStackAddress = memoryBufferInt32[
346
+ (messageStackAddressAddress / Int32Array.BYTES_PER_ELEMENT) + i
347
+ ];
348
+ const message = arrayBufferToStringAtIndex(memoryBufferUint8, messageStackAddress);
349
+ messageStack.push(message);
350
+ }
351
+
352
+ pv_free_error_stack(messageStackAddressAddress);
353
+
354
+ return messageStack;
355
+ }
356
+
357
+ protected static wrapAsyncFunction(module: EagleModule, functionName: string, numArgs: number): (...args: any[]) => any {
358
+ // @ts-ignore
359
+ return module.cwrap(
360
+ functionName,
361
+ "number",
362
+ Array(numArgs).fill("number"),
363
+ { async: true }
364
+ );
365
+ }
366
+ }
367
+
368
+ /**
369
+ * JavaScript/WebAssembly binding for the profiler of the Eagle Speaker Recognition engine.
370
+ * It enrolls a speaker given a set of utterances and then constructs a profile for the enrolled speaker.
371
+ */
372
+ export class EagleProfiler extends EagleBase {
373
+ private readonly _pv_eagle_profiler_enroll: pv_eagle_profiler_enroll_type;
374
+ private readonly _pv_eagle_profiler_flush: pv_eagle_profiler_flush_type;
375
+ private readonly _pv_eagle_profiler_reset: pv_eagle_profiler_reset_type;
376
+ private readonly _pv_eagle_profiler_delete: pv_eagle_profiler_delete_type;
377
+
378
+ private readonly _objectAddress: number;
379
+ private readonly _percentageAddress: number;
380
+
381
+ private readonly _frameLength: number;
382
+ private readonly _profileSize: number;
383
+
384
+ private constructor(handleWasm: EagleProfilerWasmOutput) {
385
+ super(handleWasm);
386
+
387
+ this._frameLength = handleWasm.frameLength;
388
+ this._profileSize = handleWasm.profileSize;
389
+
390
+ this._pv_eagle_profiler_enroll = handleWasm.pv_eagle_profiler_enroll;
391
+ this._pv_eagle_profiler_flush = handleWasm.pv_eagle_profiler_flush;
392
+ this._pv_eagle_profiler_reset = handleWasm.pv_eagle_profiler_reset;
393
+ this._pv_eagle_profiler_delete = handleWasm.pv_eagle_profiler_delete;
394
+
395
+ this._objectAddress = handleWasm.objectAddress;
396
+ this._percentageAddress = handleWasm.percentageAddress;
397
+ }
398
+
399
+ /**
400
+ * The length of the input pcm required by `.enroll()`.
401
+ */
402
+ get frameLength(): number {
403
+ return this._frameLength;
404
+ }
405
+
406
+ /**
407
+ * Creates an instance of profiler component of the Eagle Speaker Recognition Engine.
408
+ *
409
+ * @param accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/).
410
+ * @param model Eagle model options.
411
+ * @param model.base64 The model in base64 string to initialize Eagle.
412
+ * @param model.publicPath The model path relative to the public directory.
413
+ * @param model.customWritePath Custom path to save the model in storage.
414
+ * Set to a different name to use multiple models across `eagle` instances.
415
+ * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
416
+ * @param model.version Version of the model file. Increment to update the model file in storage.
417
+ * @param options Optional configuration arguments.
418
+ * @param options.device String representation of the device (e.g., CPU or GPU) to use. If set to `best`, the most
419
+ * suitable device is selected automatically. If set to `gpu`, the engine uses the first available GPU device. To select a specific
420
+ * GPU device, set this argument to `gpu:${GPU_INDEX}`, where `${GPU_INDEX}` is the index of the target GPU. If set to
421
+ * `cpu`, the engine will run on the CPU with the default number of threads. To specify the number of threads, set this
422
+ * argument to `cpu:${NUM_THREADS}`, where `${NUM_THREADS}` is the desired number of threads.
423
+ * @param options.minEnrollmentChunks Minimum number of chunks to be processed before enroll returns 100%
424
+ * @param options.voiceThreshold Sensitivity threshold for detecting voice.
425
+ *
426
+ * @return An instance of the Eagle Profiler.
427
+ */
428
+ public static async create(
429
+ accessKey: string,
430
+ model: EagleModel,
431
+ options: EagleProfilerOptions = {}
432
+ ): Promise<EagleProfiler> {
433
+ const customWritePath = model.customWritePath
434
+ ? model.customWritePath
435
+ : 'eagle_model';
436
+ const modelPath = await loadModel({ ...model, customWritePath });
437
+
438
+ return EagleProfiler._init(accessKey, modelPath, options);
439
+ }
440
+
441
+ public static async _init(
442
+ accessKey: string,
443
+ modelPath: string,
444
+ options: EagleProfilerOptions = {}
445
+ ): Promise<EagleProfiler> {
446
+ if (!isAccessKeyValid(accessKey)) {
447
+ throw new EagleErrors.EagleInvalidArgumentError('Invalid AccessKey');
448
+ }
449
+
450
+ let {
451
+ device = "best",
452
+ minEnrollmentChunks = 1,
453
+ voiceThreshold = 0.3,
454
+ } = options;
455
+
456
+ const isSimd = await simd();
457
+ if (!isSimd) {
458
+ throw new EagleErrors.EagleRuntimeError('Browser not supported.');
459
+ }
460
+
461
+ const isWorkerScope =
462
+ typeof WorkerGlobalScope !== 'undefined' &&
463
+ self instanceof WorkerGlobalScope;
464
+ if (
465
+ !isWorkerScope &&
466
+ (device === 'best' || (device.startsWith('cpu') && device !== 'cpu:1'))
467
+ ) {
468
+ // eslint-disable-next-line no-console
469
+ console.warn('Multi-threading is not supported on main thread.');
470
+ device = 'cpu:1';
471
+ }
472
+
473
+ const sabDefined = typeof SharedArrayBuffer !== 'undefined'
474
+ && (device !== "cpu:1");
475
+
476
+ return new Promise<EagleProfiler>((resolve, reject) => {
477
+ EagleProfiler._eagleMutex
478
+ .runExclusive(async () => {
479
+ const wasmOutput = await EagleProfiler._initProfilerWasm(
480
+ accessKey.trim(),
481
+ modelPath.trim(),
482
+ device,
483
+ minEnrollmentChunks,
484
+ voiceThreshold,
485
+ sabDefined ? this._wasmPThread : this._wasmSimd,
486
+ sabDefined ? this._wasmPThreadLib : this._wasmSimdLib,
487
+ sabDefined ? createModulePThread : createModuleSimd
488
+ );
489
+ return new EagleProfiler(wasmOutput);
490
+ })
491
+ .then((result: EagleProfiler) => {
492
+ resolve(result);
493
+ })
494
+ .catch((error: any) => {
495
+ reject(error);
496
+ });
497
+ });
498
+ }
499
+
500
+ /**
501
+ * Enrolls a speaker. This function should be called multiple times with different utterances of the same speaker
502
+ * until `percentage` reaches `100.0`, at which point a speaker voice profile can be exported using `.export()`.
503
+ * Any further enrollment can be used to improve the speaker profile. The minimum length of the input pcm to
504
+ * `.enroll()` can be obtained by calling `.minEnrollSamples`.
505
+ * The audio data used for enrollment should satisfy the following requirements:
506
+ * - only one speaker should be present in the audio
507
+ * - the speaker should be speaking in a normal voice
508
+ * - the audio should contain no speech from other speakers and no other sounds (e.g. music)
509
+ * - it should be captured in a quiet environment with no background noise
510
+ * @param pcm Audio data for enrollment. The audio needs to have a sample rate equal to `.sampleRate` and be
511
+ * 16-bit linearly-encoded. EagleProfiler operates on single-channel audio.
512
+ *
513
+ * @return The percentage of completeness of the speaker enrollment process.
514
+ */
515
+ public async enroll(pcm: Int16Array): Promise<number> {
516
+ if (!(pcm instanceof Int16Array)) {
517
+ throw new EagleErrors.EagleInvalidArgumentError(
518
+ "The argument 'pcm' must be provided as an Int16Array"
519
+ );
520
+ }
521
+
522
+ if (pcm.length !== this.frameLength) {
523
+ throw new EagleErrors.EagleInvalidArgumentError(
524
+ `'pcm' size must be equal to ${this.frameLength}`
525
+ );
526
+ }
527
+
528
+ return new Promise<number>((resolve, reject) => {
529
+ this._functionMutex
530
+ .runExclusive(async () => {
531
+ if (this._module === undefined) {
532
+ throw new EagleErrors.EagleInvalidStateError(
533
+ 'Attempted to call `.enroll()` after release'
534
+ );
535
+ }
536
+
537
+ const pcmAddress = this._module._malloc(
538
+ pcm.length * Int16Array.BYTES_PER_ELEMENT
539
+ );
540
+
541
+ this._module.HEAP16.set(
542
+ pcm,
543
+ pcmAddress / Int16Array.BYTES_PER_ELEMENT
544
+ );
545
+ const status = await this._pv_eagle_profiler_enroll(
546
+ this._objectAddress,
547
+ pcmAddress,
548
+ this._percentageAddress
549
+ );
550
+ this._module._pv_free(pcmAddress);
551
+
552
+ if (status !== PV_STATUS_SUCCESS) {
553
+ const messageStack = await EagleProfiler.getMessageStack(
554
+ this._module._pv_get_error_stack,
555
+ this._module._pv_free_error_stack,
556
+ this._messageStackAddressAddressAddress,
557
+ this._messageStackDepthAddress,
558
+ this._module.HEAP32,
559
+ this._module.HEAPU8
560
+ );
561
+
562
+ throw pvStatusToException(
563
+ status,
564
+ 'EagleProfiler enroll failed',
565
+ messageStack
566
+ );
567
+ }
568
+
569
+ const percentage =
570
+ this._module.HEAPF32[
571
+ this._percentageAddress / Float32Array.BYTES_PER_ELEMENT
572
+ ];
573
+
574
+ return percentage;
575
+ })
576
+ .then((result: number) => {
577
+ resolve(result);
578
+ })
579
+ .catch((error: any) => {
580
+ reject(error);
581
+ });
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Marks the end of the audio stream, flushes internal state of the object, and returns the percentage of enrollment
587
+ * completed.
588
+ *
589
+ * @return The percentage of completeness of the speaker enrollment process.
590
+ */
591
+ public async flush(): Promise<number> {
592
+ return new Promise<number>((resolve, reject) => {
593
+ this._functionMutex
594
+ .runExclusive(async () => {
595
+ if (this._module === undefined) {
596
+ throw new EagleErrors.EagleInvalidStateError(
597
+ 'Attempted to call `.flush()` after release'
598
+ );
599
+ }
600
+
601
+ const status = await this._pv_eagle_profiler_flush(
602
+ this._objectAddress,
603
+ this._percentageAddress
604
+ );
605
+
606
+ if (status !== PV_STATUS_SUCCESS) {
607
+ const messageStack = await EagleProfiler.getMessageStack(
608
+ this._module._pv_get_error_stack,
609
+ this._module._pv_free_error_stack,
610
+ this._messageStackAddressAddressAddress,
611
+ this._messageStackDepthAddress,
612
+ this._module.HEAP32,
613
+ this._module.HEAPU8
614
+ );
615
+
616
+ throw pvStatusToException(
617
+ status,
618
+ 'EagleProfiler flush failed',
619
+ messageStack
620
+ );
621
+ }
622
+
623
+ const percentage =
624
+ this._module.HEAPF32[
625
+ this._percentageAddress / Float32Array.BYTES_PER_ELEMENT
626
+ ];
627
+
628
+ return percentage;
629
+ })
630
+ .then((result: number) => {
631
+ resolve(result);
632
+ })
633
+ .catch((error: any) => {
634
+ reject(error);
635
+ });
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Exports the speaker profile of the current session.
641
+ * Will throw error if the profile is not ready.
642
+ *
643
+ * @return An EagleProfile object.
644
+ */
645
+ public async export(): Promise<EagleProfile> {
646
+ return new Promise<EagleProfile>((resolve, reject) => {
647
+ this._functionMutex
648
+ .runExclusive(async () => {
649
+ if (this._module === undefined) {
650
+ throw new EagleErrors.EagleInvalidStateError(
651
+ 'Attempted to call `.export()` after release'
652
+ );
653
+ }
654
+
655
+ const profileAddress = this._module._malloc(
656
+ Uint8Array.BYTES_PER_ELEMENT * this._profileSize
657
+ );
658
+
659
+ const status = this._module._pv_eagle_profiler_export(
660
+ this._objectAddress,
661
+ profileAddress
662
+ );
663
+ if (status !== PV_STATUS_SUCCESS) {
664
+ this._module._pv_free(profileAddress);
665
+
666
+ const messageStack = await EagleProfiler.getMessageStack(
667
+ this._module._pv_get_error_stack,
668
+ this._module._pv_free_error_stack,
669
+ this._messageStackAddressAddressAddress,
670
+ this._messageStackDepthAddress,
671
+ this._module.HEAP32,
672
+ this._module.HEAPU8
673
+ );
674
+
675
+ throw pvStatusToException(
676
+ status,
677
+ 'EagleProfiler export failed',
678
+ messageStack
679
+ );
680
+ }
681
+
682
+ const profile = this._module.HEAPU8.slice(
683
+ profileAddress,
684
+ profileAddress + Uint8Array.BYTES_PER_ELEMENT * this._profileSize
685
+ );
686
+ this._module._pv_free(profileAddress);
687
+
688
+ return { bytes: profile };
689
+ })
690
+ .then((result: EagleProfile) => {
691
+ resolve(result);
692
+ })
693
+ .catch((error: any) => {
694
+ reject(error);
695
+ });
696
+ });
697
+ }
698
+
699
+ /**
700
+ * Resets the internal state of Eagle Profiler.
701
+ * It should be called before starting a new enrollment session.
702
+ */
703
+ public async reset(): Promise<void> {
704
+ return new Promise<void>((resolve, reject) => {
705
+ this._functionMutex
706
+ .runExclusive(async () => {
707
+ if (this._module === undefined) {
708
+ throw new EagleErrors.EagleInvalidStateError(
709
+ 'Attempted to call `.reset()` after release'
710
+ );
711
+ }
712
+
713
+ const status = await this._pv_eagle_profiler_reset(
714
+ this._objectAddress
715
+ );
716
+ if (status !== PV_STATUS_SUCCESS) {
717
+ const messageStack = await EagleProfiler.getMessageStack(
718
+ this._module._pv_get_error_stack,
719
+ this._module._pv_free_error_stack,
720
+ this._messageStackAddressAddressAddress,
721
+ this._messageStackDepthAddress,
722
+ this._module.HEAP32,
723
+ this._module.HEAPU8
724
+ );
725
+
726
+ throw pvStatusToException(
727
+ status,
728
+ 'EagleProfiler reset failed',
729
+ messageStack
730
+ );
731
+ }
732
+ })
733
+ .then(() => {
734
+ resolve();
735
+ })
736
+ .catch((error: any) => {
737
+ reject(error);
738
+ });
739
+ });
740
+ }
741
+
742
+ /**
743
+ * Releases resources acquired by Eagle Profiler
744
+ */
745
+ public async release(): Promise<void> {
746
+ if (!this._module) {
747
+ return;
748
+ }
749
+
750
+ await super.release();
751
+ await this._pv_eagle_profiler_delete(this._objectAddress);
752
+ this._module = undefined;
753
+ }
754
+
755
+ private static async _initProfilerWasm(
756
+ accessKey: string,
757
+ modelPath: string,
758
+ device: string,
759
+ minEnrollmentChunks: number,
760
+ voiceThreshold: number,
761
+ wasmBase64: string,
762
+ wasmLibBase64: string,
763
+ createModuleFunc: any
764
+ ): Promise<EagleProfilerWasmOutput> {
765
+ const baseWasmOutput = await super._initBaseWasm(
766
+ wasmBase64,
767
+ wasmLibBase64,
768
+ createModuleFunc
769
+ );
770
+
771
+ const pv_eagle_profiler_init: pv_eagle_profiler_init_type =
772
+ this.wrapAsyncFunction(
773
+ baseWasmOutput.module,
774
+ 'pv_eagle_profiler_init',
775
+ 6
776
+ );
777
+ const pv_eagle_profiler_enroll: pv_eagle_profiler_enroll_type =
778
+ this.wrapAsyncFunction(
779
+ baseWasmOutput.module,
780
+ 'pv_eagle_profiler_enroll',
781
+ 3
782
+ );
783
+ const pv_eagle_profiler_flush: pv_eagle_profiler_flush_type =
784
+ this.wrapAsyncFunction(
785
+ baseWasmOutput.module,
786
+ 'pv_eagle_profiler_flush',
787
+ 2
788
+ );
789
+
790
+ const pv_eagle_profiler_reset: pv_eagle_profiler_reset_type =
791
+ this.wrapAsyncFunction(
792
+ baseWasmOutput.module,
793
+ 'pv_eagle_profiler_reset',
794
+ 1
795
+ );
796
+
797
+ const pv_eagle_profiler_delete: pv_eagle_profiler_delete_type =
798
+ this.wrapAsyncFunction(
799
+ baseWasmOutput.module,
800
+ 'pv_eagle_profiler_delete',
801
+ 1
802
+ );
803
+
804
+ const objectAddressAddress = baseWasmOutput.module._malloc(
805
+ Int32Array.BYTES_PER_ELEMENT
806
+ );
807
+ if (objectAddressAddress === 0) {
808
+ throw new EagleErrors.EagleOutOfMemoryError(
809
+ 'malloc failed: Cannot allocate memory'
810
+ );
811
+ }
812
+
813
+ const accessKeyAddress = baseWasmOutput.module._malloc(
814
+ (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT
815
+ );
816
+ if (accessKeyAddress === 0) {
817
+ throw new EagleErrors.EagleOutOfMemoryError(
818
+ 'malloc failed: Cannot allocate memory'
819
+ );
820
+ }
821
+ for (let i = 0; i < accessKey.length; i++) {
822
+ baseWasmOutput.module.HEAPU8[accessKeyAddress + i] =
823
+ accessKey.charCodeAt(i);
824
+ }
825
+ baseWasmOutput.module.HEAPU8[accessKeyAddress + accessKey.length] = 0;
826
+
827
+ const modelPathEncoded = new TextEncoder().encode(modelPath);
828
+ const modelPathAddress = baseWasmOutput.module._malloc(
829
+ (modelPathEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
830
+ );
831
+ if (modelPathAddress === 0) {
832
+ throw new EagleErrors.EagleOutOfMemoryError(
833
+ 'malloc failed: Cannot allocate memory'
834
+ );
835
+ }
836
+ baseWasmOutput.module.HEAPU8.set(modelPathEncoded, modelPathAddress);
837
+ baseWasmOutput.module.HEAPU8[
838
+ modelPathAddress + modelPathEncoded.length
839
+ ] = 0;
840
+
841
+ const deviceAddress = baseWasmOutput.module._malloc(
842
+ (device.length + 1) * Uint8Array.BYTES_PER_ELEMENT
843
+ );
844
+ if (deviceAddress === 0) {
845
+ throw new EagleErrors.EagleOutOfMemoryError(
846
+ 'malloc failed: Cannot allocate memory'
847
+ );
848
+ }
849
+ for (let i = 0; i < device.length; i++) {
850
+ baseWasmOutput.module.HEAPU8[deviceAddress + i] = device.charCodeAt(i);
851
+ }
852
+ baseWasmOutput.module.HEAPU8[deviceAddress + device.length] = 0;
853
+
854
+ let status = await pv_eagle_profiler_init(
855
+ accessKeyAddress,
856
+ modelPathAddress,
857
+ deviceAddress,
858
+ minEnrollmentChunks,
859
+ voiceThreshold,
860
+ objectAddressAddress,
861
+ );
862
+ baseWasmOutput.module._pv_free(accessKeyAddress);
863
+ baseWasmOutput.module._pv_free(modelPathAddress);
864
+ baseWasmOutput.module._pv_free(deviceAddress);
865
+ if (status !== PvStatus.SUCCESS) {
866
+ const messageStack = await EagleProfiler.getMessageStack(
867
+ baseWasmOutput.module._pv_get_error_stack,
868
+ baseWasmOutput.module._pv_free_error_stack,
869
+ baseWasmOutput.messageStackAddressAddressAddress,
870
+ baseWasmOutput.messageStackDepthAddress,
871
+ baseWasmOutput.module.HEAP32,
872
+ baseWasmOutput.module.HEAPU8
873
+ );
874
+
875
+ throw pvStatusToException(status, 'Initialization failed', messageStack);
876
+ }
877
+
878
+ const objectAddress =
879
+ baseWasmOutput.module.HEAP32[
880
+ objectAddressAddress / Int32Array.BYTES_PER_ELEMENT
881
+ ];
882
+ baseWasmOutput.module._pv_free(objectAddressAddress);
883
+
884
+ const frameLength =
885
+ baseWasmOutput.module._pv_eagle_profiler_frame_length();
886
+ if (status !== PV_STATUS_SUCCESS) {
887
+ const messageStack = await EagleProfiler.getMessageStack(
888
+ baseWasmOutput.module._pv_get_error_stack,
889
+ baseWasmOutput.module._pv_free_error_stack,
890
+ baseWasmOutput.messageStackAddressAddressAddress,
891
+ baseWasmOutput.messageStackDepthAddress,
892
+ baseWasmOutput.module.HEAP32,
893
+ baseWasmOutput.module.HEAPU8
894
+ );
895
+
896
+ throw pvStatusToException(
897
+ status,
898
+ 'EagleProfiler failed to get min enroll audio length',
899
+ messageStack
900
+ );
901
+ }
902
+
903
+ const profileSizeAddress = baseWasmOutput.module._malloc(
904
+ Int32Array.BYTES_PER_ELEMENT
905
+ );
906
+ if (profileSizeAddress === 0) {
907
+ throw new EagleErrors.EagleOutOfMemoryError(
908
+ 'malloc failed: Cannot allocate memory'
909
+ );
910
+ }
911
+
912
+ status = baseWasmOutput.module._pv_eagle_profiler_export_size(
913
+ objectAddress,
914
+ profileSizeAddress
915
+ );
916
+ if (status !== PV_STATUS_SUCCESS) {
917
+ const messageStack = await EagleProfiler.getMessageStack(
918
+ baseWasmOutput.module._pv_get_error_stack,
919
+ baseWasmOutput.module._pv_free_error_stack,
920
+ baseWasmOutput.messageStackAddressAddressAddress,
921
+ baseWasmOutput.messageStackDepthAddress,
922
+ baseWasmOutput.module.HEAP32,
923
+ baseWasmOutput.module.HEAPU8
924
+ );
925
+
926
+ throw pvStatusToException(
927
+ status,
928
+ 'EagleProfiler failed to get export size',
929
+ messageStack
930
+ );
931
+ }
932
+
933
+ const profileSize =
934
+ baseWasmOutput.module.HEAP32[
935
+ profileSizeAddress / Int32Array.BYTES_PER_ELEMENT
936
+ ];
937
+ baseWasmOutput.module._pv_free(profileSizeAddress);
938
+
939
+ const percentageAddress = baseWasmOutput.module._malloc(
940
+ Int32Array.BYTES_PER_ELEMENT
941
+ );
942
+ if (percentageAddress === 0) {
943
+ throw new EagleErrors.EagleOutOfMemoryError(
944
+ 'malloc failed: Cannot allocate memory'
945
+ );
946
+ }
947
+
948
+ const profileAddress = baseWasmOutput.module._malloc(
949
+ Uint8Array.BYTES_PER_ELEMENT * profileSize
950
+ );
951
+ if (profileAddress === 0) {
952
+ throw new EagleErrors.EagleOutOfMemoryError(
953
+ 'malloc failed: Cannot allocate memory'
954
+ );
955
+ }
956
+
957
+ return {
958
+ ...baseWasmOutput,
959
+ frameLength: frameLength,
960
+ profileSize: profileSize,
961
+
962
+ objectAddress: objectAddress,
963
+ percentageAddress: percentageAddress,
964
+ profileAddress: profileAddress,
965
+
966
+ pv_eagle_profiler_enroll: pv_eagle_profiler_enroll,
967
+ pv_eagle_profiler_flush: pv_eagle_profiler_flush,
968
+ pv_eagle_profiler_reset: pv_eagle_profiler_reset,
969
+ pv_eagle_profiler_delete: pv_eagle_profiler_delete,
970
+ };
971
+ }
972
+ }
973
+
974
+ /**
975
+ * JavaScript/WebAssembly binding for Eagle Speaker Recognition engine.
976
+ * It processes incoming audio in consecutive frames and emits a similarity score for each enrolled speaker.
977
+ */
978
+ export class Eagle extends EagleBase {
979
+ private readonly _pv_eagle_process: pv_eagle_process_type;
980
+ private readonly _pv_eagle_scores_delete: pv_eagle_scores_delete_type;
981
+ private readonly _pv_eagle_delete: pv_eagle_delete_type;
982
+
983
+ private readonly _objectAddress: number;
984
+ private readonly _scoresAddressAddress: number;
985
+
986
+ private readonly _minProcessSamples: number;
987
+
988
+ private constructor(handleWasm: EagleWasmOutput) {
989
+ super(handleWasm);
990
+
991
+ this._minProcessSamples = handleWasm.minProcessSamples;
992
+
993
+ this._pv_eagle_process = handleWasm.pv_eagle_process;
994
+ this._pv_eagle_scores_delete = handleWasm.pv_eagle_scores_delete;
995
+ this._pv_eagle_delete = handleWasm.pv_eagle_delete;
996
+
997
+ this._objectAddress = handleWasm.objectAddress;
998
+ this._scoresAddressAddress = handleWasm.scoresAddressAddress;
999
+ }
1000
+
1001
+ /**
1002
+ * Number of audio samples per frame expected by Eagle (i.e. length of the array passed into `.process()`)
1003
+ */
1004
+ get minProcessSamples(): number {
1005
+ return this._minProcessSamples;
1006
+ }
1007
+
1008
+ /**
1009
+ * Creates an instance of the Picovoice Eagle Speaker Recognition Engine.
1010
+ *
1011
+ * @param accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/)
1012
+ * @param model Eagle model options.
1013
+ * @param model.base64 The model in base64 string to initialize Eagle.
1014
+ * @param model.publicPath The model path relative to the public directory.
1015
+ * @param model.customWritePath Custom path to save the model in storage.
1016
+ * Set to a different name to use multiple models across `eagle` instances.
1017
+ * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
1018
+ * @param model.version Version of the model file. Increment to update the model file in storage.
1019
+ * @param options Optional configuration arguments.
1020
+ * @param options.device String representation of the device (e.g., CPU or GPU) to use. If set to `best`, the most
1021
+ * suitable device is selected automatically. If set to `gpu`, the engine uses the first available GPU device. To select a specific
1022
+ * GPU device, set this argument to `gpu:${GPU_INDEX}`, where `${GPU_INDEX}` is the index of the target GPU. If set to
1023
+ * `cpu`, the engine will run on the CPU with the default number of threads. To specify the number of threads, set this
1024
+ * argument to `cpu:${NUM_THREADS}`, where `${NUM_THREADS}` is the desired number of threads.
1025
+ * @param options.voiceThreshold Sensitivity threshold for detecting voice.
1026
+ *
1027
+ * @return An instance of the Eagle engine.
1028
+ */
1029
+ public static async create(
1030
+ accessKey: string,
1031
+ model: EagleModel,
1032
+ options: EagleOptions = {}
1033
+ ): Promise<Eagle> {
1034
+ const customWritePath = model.customWritePath
1035
+ ? model.customWritePath
1036
+ : 'eagle_model';
1037
+ const modelPath = await loadModel({ ...model, customWritePath });
1038
+
1039
+ return Eagle._init(
1040
+ accessKey,
1041
+ modelPath,
1042
+ options
1043
+ );
1044
+ }
1045
+
1046
+ public static async _init(
1047
+ accessKey: string,
1048
+ modelPath: string,
1049
+ options: EagleOptions = {}
1050
+ ): Promise<Eagle> {
1051
+ if (!isAccessKeyValid(accessKey)) {
1052
+ throw new EagleErrors.EagleInvalidArgumentError('Invalid AccessKey');
1053
+ }
1054
+
1055
+ let {
1056
+ device = "best",
1057
+ voiceThreshold = 0.3
1058
+ } = options;
1059
+
1060
+ const isSimd = await simd();
1061
+ if (!isSimd) {
1062
+ throw new EagleErrors.EagleRuntimeError('Browser not supported.');
1063
+ }
1064
+
1065
+ const isWorkerScope =
1066
+ typeof WorkerGlobalScope !== 'undefined' &&
1067
+ self instanceof WorkerGlobalScope;
1068
+ if (
1069
+ !isWorkerScope &&
1070
+ (device === 'best' || (device.startsWith('cpu') && device !== 'cpu:1'))
1071
+ ) {
1072
+ // eslint-disable-next-line no-console
1073
+ console.warn('Multi-threading is not supported on main thread.');
1074
+ device = 'cpu:1';
1075
+ }
1076
+
1077
+ const sabDefined = typeof SharedArrayBuffer !== 'undefined'
1078
+ && (device !== "cpu:1");
1079
+
1080
+ return new Promise<Eagle>((resolve, reject) => {
1081
+ Eagle._eagleMutex
1082
+ .runExclusive(async () => {
1083
+ const wasmOutput = await Eagle._initWasm(
1084
+ accessKey.trim(),
1085
+ modelPath.trim(),
1086
+ device,
1087
+ voiceThreshold,
1088
+ sabDefined ? this._wasmPThread : this._wasmSimd,
1089
+ sabDefined ? this._wasmPThreadLib : this._wasmSimdLib,
1090
+ sabDefined ? createModulePThread : createModuleSimd
1091
+ );
1092
+ return new Eagle(wasmOutput);
1093
+ })
1094
+ .then((result: Eagle) => {
1095
+ resolve(result);
1096
+ })
1097
+ .catch((error: any) => {
1098
+ reject(error);
1099
+ });
1100
+ });
1101
+ }
1102
+
1103
+ /**
1104
+ * Processes audio and returns a list of similarity scores for each speaker profile.
1105
+ *
1106
+ * @param pcm Array of audio samples. The minimum number of samples per frame can be attained by calling
1107
+ * `.minProcessSamples`. The incoming audio needs to have a sample rate equal to `.sampleRate` and be 16-bit
1108
+ * linearly-encoded. Eagle operates on single-channel audio.
1109
+ * @param speakerProfiles One or more Eagle speaker profiles. These can be constructed using `EagleProfiler`.
1110
+ *
1111
+ * @return A list of similarity scores for each speaker profile. A higher score indicates that the voice
1112
+ * belongs to the corresponding speaker. The range is [0, 1] with 1.0 representing a perfect match.
1113
+ */
1114
+ public async process(
1115
+ pcm: Int16Array,
1116
+ speakerProfiles: EagleProfile[] | EagleProfile,
1117
+ ): Promise<number[] | null> {
1118
+ if (!(pcm instanceof Int16Array)) {
1119
+ throw new EagleErrors.EagleInvalidArgumentError(
1120
+ "The argument 'pcm' must be provided as an Int16Array"
1121
+ );
1122
+ }
1123
+
1124
+ if (pcm.length < this._minProcessSamples) {
1125
+ throw new EagleErrors.EagleInvalidArgumentError(
1126
+ `Length of input sample (${pcm.length}) is not greater than minimum sample length (${this._minProcessSamples})`
1127
+ );
1128
+ }
1129
+
1130
+ const profiles = !Array.isArray(speakerProfiles) ? [speakerProfiles] : speakerProfiles;
1131
+
1132
+ if (!profiles || profiles.length === 0) {
1133
+ throw new EagleErrors.EagleInvalidArgumentError(
1134
+ 'No speaker profiles provided'
1135
+ );
1136
+ }
1137
+
1138
+ return new Promise<number[] | null>((resolve, reject) => {
1139
+ this._functionMutex
1140
+ .runExclusive(async () => {
1141
+ if (this._module === undefined) {
1142
+ throw new EagleErrors.EagleInvalidStateError(
1143
+ 'Attempted to call `.process` after release'
1144
+ );
1145
+ }
1146
+
1147
+ const pcmAddress = this._module._malloc(
1148
+ pcm.length * Int16Array.BYTES_PER_ELEMENT
1149
+ );
1150
+
1151
+ this._module.HEAP16.set(
1152
+ pcm,
1153
+ pcmAddress / Int16Array.BYTES_PER_ELEMENT
1154
+ );
1155
+
1156
+ const numSpeakers = profiles.length;
1157
+ const profilesAddressAddress = this._module._malloc(
1158
+ numSpeakers * Int32Array.BYTES_PER_ELEMENT
1159
+ );
1160
+ if (profilesAddressAddress === 0) {
1161
+ throw new EagleErrors.EagleOutOfMemoryError(
1162
+ 'malloc failed: Cannot allocate memory'
1163
+ );
1164
+ }
1165
+ const profilesAddressList: number[] = [];
1166
+ for (const profile of profiles) {
1167
+ const profileAddress = this._module._malloc(
1168
+ profile.bytes.length * Uint8Array.BYTES_PER_ELEMENT
1169
+ );
1170
+ if (profileAddress === 0) {
1171
+ throw new EagleErrors.EagleOutOfMemoryError(
1172
+ 'malloc failed: Cannot allocate memory'
1173
+ );
1174
+ }
1175
+ this._module.HEAPU8.set(profile.bytes, profileAddress);
1176
+ profilesAddressList.push(profileAddress);
1177
+ }
1178
+ this._module.HEAP32.set(
1179
+ new Int32Array(profilesAddressList),
1180
+ profilesAddressAddress / Int32Array.BYTES_PER_ELEMENT
1181
+ );
1182
+
1183
+ const status = await this._pv_eagle_process(
1184
+ this._objectAddress,
1185
+ pcmAddress,
1186
+ pcm.length,
1187
+ profilesAddressAddress,
1188
+ profiles.length,
1189
+ this._scoresAddressAddress
1190
+ );
1191
+
1192
+ for (let i = 0; i < profiles.length; i++) {
1193
+ this._module._pv_free(profilesAddressList[i]);
1194
+ }
1195
+ this._module._pv_free(profilesAddressAddress);
1196
+ this._module._pv_free(pcmAddress);
1197
+
1198
+ if (status !== PV_STATUS_SUCCESS) {
1199
+ const messageStack = await Eagle.getMessageStack(
1200
+ this._module._pv_get_error_stack,
1201
+ this._module._pv_free_error_stack,
1202
+ this._messageStackAddressAddressAddress,
1203
+ this._messageStackDepthAddress,
1204
+ this._module.HEAP32,
1205
+ this._module.HEAPU8
1206
+ );
1207
+
1208
+ throw pvStatusToException(
1209
+ status,
1210
+ 'Eagle process failed',
1211
+ messageStack
1212
+ );
1213
+ }
1214
+
1215
+ const scoresAddress = this._module.HEAP32[this._scoresAddressAddress / Int32Array.BYTES_PER_ELEMENT];
1216
+
1217
+ if (scoresAddress) {
1218
+ const scores: number[] = [];
1219
+ for (let i = 0; i < profiles.length; i++) {
1220
+ scores[i] = this._module.HEAPF32[(scoresAddress / Float32Array.BYTES_PER_ELEMENT) + i];
1221
+ }
1222
+ this._pv_eagle_scores_delete(scoresAddress);
1223
+ this._module.HEAP32[this._scoresAddressAddress / Int32Array.BYTES_PER_ELEMENT] = 0;
1224
+
1225
+ return scores;
1226
+ } else {
1227
+ return null;
1228
+ }
1229
+ })
1230
+ .then((result: number[] | null) => {
1231
+ resolve(result);
1232
+ })
1233
+ .catch((error: any) => {
1234
+ reject(error);
1235
+ });
1236
+ });
1237
+ }
1238
+
1239
+ /**
1240
+ * Releases resources acquired by Eagle
1241
+ */
1242
+ public async release(): Promise<void> {
1243
+ if (!this._module) {
1244
+ return;
1245
+ }
1246
+
1247
+ await super.release();
1248
+ await this._pv_eagle_delete(this._objectAddress);
1249
+ this._module = undefined;
1250
+ }
1251
+
1252
+ /**
1253
+ * Lists all available devices that Eagle can use for inference.
1254
+ * Each entry in the list can be the used as the `device` argument for the `.create` method.
1255
+ *
1256
+ * @returns List of all available devices that Eagle can use for inference.
1257
+ */
1258
+ public static async listAvailableDevices(): Promise<string[]> {
1259
+ return new Promise<string[]>((resolve, reject) => {
1260
+ Eagle._eagleMutex
1261
+ .runExclusive(async () => {
1262
+ const isSimd = await simd();
1263
+ if (!isSimd) {
1264
+ throw new EagleErrors.EagleRuntimeError('Unsupported Browser');
1265
+ }
1266
+
1267
+ const blob = new Blob([base64ToUint8Array(this._wasmSimdLib)], {
1268
+ type: 'application/javascript',
1269
+ });
1270
+ const module: EagleModule = await createModuleSimd({
1271
+ mainScriptUrlOrBlob: blob,
1272
+ wasmBinary: base64ToUint8Array(this._wasmSimd),
1273
+ });
1274
+
1275
+ const hardwareDevicesAddressAddress = module._malloc(
1276
+ Int32Array.BYTES_PER_ELEMENT
1277
+ );
1278
+ if (hardwareDevicesAddressAddress === 0) {
1279
+ throw new EagleErrors.EagleOutOfMemoryError(
1280
+ 'malloc failed: Cannot allocate memory for hardwareDevices'
1281
+ );
1282
+ }
1283
+
1284
+ const numHardwareDevicesAddress = module._malloc(
1285
+ Int32Array.BYTES_PER_ELEMENT
1286
+ );
1287
+ if (numHardwareDevicesAddress === 0) {
1288
+ throw new EagleErrors.EagleOutOfMemoryError(
1289
+ 'malloc failed: Cannot allocate memory for numHardwareDevices'
1290
+ );
1291
+ }
1292
+
1293
+ const status: PvStatus = module._pv_eagle_list_hardware_devices(
1294
+ hardwareDevicesAddressAddress,
1295
+ numHardwareDevicesAddress
1296
+ );
1297
+
1298
+ const messageStackDepthAddress = module._malloc(
1299
+ Int32Array.BYTES_PER_ELEMENT
1300
+ );
1301
+ if (!messageStackDepthAddress) {
1302
+ throw new EagleErrors.EagleOutOfMemoryError(
1303
+ 'malloc failed: Cannot allocate memory for messageStackDepth'
1304
+ );
1305
+ }
1306
+
1307
+ const messageStackAddressAddressAddress = module._malloc(
1308
+ Int32Array.BYTES_PER_ELEMENT
1309
+ );
1310
+ if (!messageStackAddressAddressAddress) {
1311
+ throw new EagleErrors.EagleOutOfMemoryError(
1312
+ 'malloc failed: Cannot allocate memory messageStack'
1313
+ );
1314
+ }
1315
+
1316
+ if (status !== PvStatus.SUCCESS) {
1317
+ const messageStack = await Eagle.getMessageStack(
1318
+ module._pv_get_error_stack,
1319
+ module._pv_free_error_stack,
1320
+ messageStackAddressAddressAddress,
1321
+ messageStackDepthAddress,
1322
+ module.HEAP32,
1323
+ module.HEAPU8
1324
+ );
1325
+ module._pv_free(messageStackAddressAddressAddress);
1326
+ module._pv_free(messageStackDepthAddress);
1327
+
1328
+ throw pvStatusToException(
1329
+ status,
1330
+ 'List devices failed',
1331
+ messageStack
1332
+ );
1333
+ }
1334
+ module._pv_free(messageStackAddressAddressAddress);
1335
+ module._pv_free(messageStackDepthAddress);
1336
+
1337
+ const numHardwareDevices: number =
1338
+ module.HEAP32[
1339
+ numHardwareDevicesAddress / Int32Array.BYTES_PER_ELEMENT
1340
+ ];
1341
+ module._pv_free(numHardwareDevicesAddress);
1342
+
1343
+ const hardwareDevicesAddress =
1344
+ module.HEAP32[
1345
+ hardwareDevicesAddressAddress / Int32Array.BYTES_PER_ELEMENT
1346
+ ];
1347
+
1348
+ const hardwareDevices: string[] = [];
1349
+ for (let i = 0; i < numHardwareDevices; i++) {
1350
+ const deviceAddress =
1351
+ module.HEAP32[
1352
+ hardwareDevicesAddress / Int32Array.BYTES_PER_ELEMENT + i
1353
+ ];
1354
+ hardwareDevices.push(
1355
+ arrayBufferToStringAtIndex(module.HEAPU8, deviceAddress)
1356
+ );
1357
+ }
1358
+ module._pv_eagle_free_hardware_devices(
1359
+ hardwareDevicesAddress,
1360
+ numHardwareDevices
1361
+ );
1362
+ module._pv_free(hardwareDevicesAddressAddress);
1363
+
1364
+ return hardwareDevices;
1365
+ })
1366
+ .then((result: string[]) => {
1367
+ resolve(result);
1368
+ })
1369
+ .catch((error: any) => {
1370
+ reject(error);
1371
+ });
1372
+ });
1373
+ }
1374
+
1375
+ private static async _initWasm(
1376
+ accessKey: string,
1377
+ modelPath: string,
1378
+ device: string,
1379
+ voiceThreshold: number,
1380
+ wasmBase64: string,
1381
+ wasmLibBase64: string,
1382
+ createModuleFunc: any
1383
+ ): Promise<EagleWasmOutput> {
1384
+ const baseWasmOutput = await super._initBaseWasm(
1385
+ wasmBase64,
1386
+ wasmLibBase64,
1387
+ createModuleFunc
1388
+ );
1389
+
1390
+ const pv_eagle_init: pv_eagle_init_type = this.wrapAsyncFunction(
1391
+ baseWasmOutput.module,
1392
+ 'pv_eagle_init',
1393
+ 5
1394
+ );
1395
+ const pv_eagle_process: pv_eagle_process_type = this.wrapAsyncFunction(
1396
+ baseWasmOutput.module,
1397
+ 'pv_eagle_process',
1398
+ 5
1399
+ );
1400
+ const pv_eagle_scores_delete: pv_eagle_scores_delete_type = this.wrapAsyncFunction(
1401
+ baseWasmOutput.module,
1402
+ 'pv_eagle_scores_delete',
1403
+ 1
1404
+ );
1405
+ const pv_eagle_delete: pv_eagle_delete_type = this.wrapAsyncFunction(
1406
+ baseWasmOutput.module,
1407
+ 'pv_eagle_delete',
1408
+ 1
1409
+ );
1410
+
1411
+ const objectAddressAddress = baseWasmOutput.module._malloc(
1412
+ Int32Array.BYTES_PER_ELEMENT
1413
+ );
1414
+ if (objectAddressAddress === 0) {
1415
+ throw new EagleErrors.EagleOutOfMemoryError(
1416
+ 'malloc failed: Cannot allocate memory'
1417
+ );
1418
+ }
1419
+
1420
+ const accessKeyAddress = baseWasmOutput.module._malloc(
1421
+ (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1422
+ );
1423
+ if (accessKeyAddress === 0) {
1424
+ throw new EagleErrors.EagleOutOfMemoryError(
1425
+ 'malloc failed: Cannot allocate memory'
1426
+ );
1427
+ }
1428
+ for (let i = 0; i < accessKey.length; i++) {
1429
+ baseWasmOutput.module.HEAPU8[accessKeyAddress + i] =
1430
+ accessKey.charCodeAt(i);
1431
+ }
1432
+ baseWasmOutput.module.HEAPU8[accessKeyAddress + accessKey.length] = 0;
1433
+
1434
+ const modelPathEncoded = new TextEncoder().encode(modelPath);
1435
+ const modelPathAddress = baseWasmOutput.module._malloc(
1436
+ (modelPathEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1437
+ );
1438
+ if (modelPathAddress === 0) {
1439
+ throw new EagleErrors.EagleOutOfMemoryError(
1440
+ 'malloc failed: Cannot allocate memory'
1441
+ );
1442
+ }
1443
+ baseWasmOutput.module.HEAPU8.set(modelPathEncoded, modelPathAddress);
1444
+ baseWasmOutput.module.HEAPU8[
1445
+ modelPathAddress + modelPathEncoded.length
1446
+ ] = 0;
1447
+
1448
+ const deviceAddress = baseWasmOutput.module._malloc(
1449
+ (device.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1450
+ );
1451
+ if (deviceAddress === 0) {
1452
+ throw new EagleErrors.EagleOutOfMemoryError(
1453
+ 'malloc failed: Cannot allocate memory'
1454
+ );
1455
+ }
1456
+ for (let i = 0; i < device.length; i++) {
1457
+ baseWasmOutput.module.HEAPU8[deviceAddress + i] = device.charCodeAt(i);
1458
+ }
1459
+ baseWasmOutput.module.HEAPU8[deviceAddress + device.length] = 0;
1460
+
1461
+ let status = await pv_eagle_init(
1462
+ accessKeyAddress,
1463
+ modelPathAddress,
1464
+ deviceAddress,
1465
+ voiceThreshold,
1466
+ objectAddressAddress
1467
+ );
1468
+ baseWasmOutput.module._pv_free(accessKeyAddress);
1469
+ baseWasmOutput.module._pv_free(modelPathAddress);
1470
+ baseWasmOutput.module._pv_free(deviceAddress);
1471
+
1472
+ if (status !== PV_STATUS_SUCCESS) {
1473
+ const messageStack = await Eagle.getMessageStack(
1474
+ baseWasmOutput.module._pv_get_error_stack,
1475
+ baseWasmOutput.module._pv_free_error_stack,
1476
+ baseWasmOutput.messageStackAddressAddressAddress,
1477
+ baseWasmOutput.messageStackDepthAddress,
1478
+ baseWasmOutput.module.HEAP32,
1479
+ baseWasmOutput.module.HEAPU8
1480
+ );
1481
+
1482
+ throw pvStatusToException(status, 'Eagle init failed', messageStack);
1483
+ }
1484
+
1485
+ const objectAddress =
1486
+ baseWasmOutput.module.HEAP32[
1487
+ objectAddressAddress / Int32Array.BYTES_PER_ELEMENT
1488
+ ];
1489
+ baseWasmOutput.module._pv_free(objectAddressAddress);
1490
+
1491
+ const scoresAddressAddress = baseWasmOutput.module._malloc(Int32Array.BYTES_PER_ELEMENT);
1492
+ if (scoresAddressAddress === 0) {
1493
+ throw new EagleErrors.EagleOutOfMemoryError(
1494
+ 'malloc failed: Cannot allocate memory'
1495
+ );
1496
+ }
1497
+
1498
+ const minProcessSamplesAddress = baseWasmOutput.module._malloc(
1499
+ Int32Array.BYTES_PER_ELEMENT
1500
+ );
1501
+ if (minProcessSamplesAddress === 0) {
1502
+ throw new EagleErrors.EagleOutOfMemoryError(
1503
+ 'malloc failed: Cannot allocate memory'
1504
+ );
1505
+ }
1506
+
1507
+ status =
1508
+ baseWasmOutput.module._pv_eagle_process_min_audio_length_samples(
1509
+ objectAddress,
1510
+ minProcessSamplesAddress
1511
+ );
1512
+ if (status !== PV_STATUS_SUCCESS) {
1513
+ const messageStack = await EagleProfiler.getMessageStack(
1514
+ baseWasmOutput.module._pv_get_error_stack,
1515
+ baseWasmOutput.module._pv_free_error_stack,
1516
+ baseWasmOutput.messageStackAddressAddressAddress,
1517
+ baseWasmOutput.messageStackDepthAddress,
1518
+ baseWasmOutput.module.HEAP32,
1519
+ baseWasmOutput.module.HEAPU8
1520
+ );
1521
+
1522
+ throw pvStatusToException(
1523
+ status,
1524
+ 'EagleProfiler failed to get min process audio length',
1525
+ messageStack
1526
+ );
1527
+ }
1528
+
1529
+ const minProcessSamples =
1530
+ baseWasmOutput.module.HEAP32[
1531
+ minProcessSamplesAddress / Int32Array.BYTES_PER_ELEMENT
1532
+ ];
1533
+ baseWasmOutput.module._pv_free(minProcessSamplesAddress);
1534
+
1535
+ return {
1536
+ ...baseWasmOutput,
1537
+ minProcessSamples: minProcessSamples,
1538
+ objectAddress: objectAddress,
1539
+ scoresAddressAddress: scoresAddressAddress,
1540
+
1541
+ pv_eagle_process: pv_eagle_process,
1542
+ pv_eagle_scores_delete: pv_eagle_scores_delete,
1543
+ pv_eagle_delete: pv_eagle_delete,
1544
+ };
1545
+ }
1546
+ }