@picovoice/eagle-web 1.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,1191 +1,1546 @@
1
- /*
2
- Copyright 2023 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
- aligned_alloc_type,
18
- pv_free_type,
19
- buildWasm,
20
- arrayBufferToStringAtIndex,
21
- isAccessKeyValid,
22
- loadModel,
23
- PvError,
24
- } from '@picovoice/web-utils';
25
-
26
- import { simd } from 'wasm-feature-detect';
27
-
28
- import {
29
- EagleModel,
30
- EagleProfile,
31
- EagleProfilerEnrollResult,
32
- PvStatus
33
- } from './types';
34
-
35
- import * as EagleErrors from './eagle_errors';
36
- import { pvStatusToException } from './eagle_errors';
37
-
38
- /**
39
- * WebAssembly function types
40
- */
41
- type pv_eagle_profiler_init_type = (
42
- accessKey: number,
43
- modelPath: number,
44
- object: number
45
- ) => Promise<number>;
46
- type pv_eagle_profiler_delete_type = (object: number) => Promise<void>;
47
- type pv_eagle_profiler_enroll_type = (
48
- object: number,
49
- pcm: number,
50
- numSamples: number,
51
- feedback: number,
52
- percentage: number
53
- ) => Promise<number>;
54
- type pv_eagle_profiler_enroll_min_audio_length_samples_type = (
55
- object: number,
56
- numSamples: number
57
- ) => Promise<number>;
58
- type pv_eagle_profiler_export_type = (
59
- object: number,
60
- speakerProfile: number
61
- ) => Promise<number>;
62
- type pv_eagle_profiler_export_size_type = (
63
- object: number,
64
- speakerProfileSizeBytes: number
65
- ) => Promise<number>;
66
- type pv_eagle_profiler_reset_type = (object: number) => Promise<number>;
67
- type pv_eagle_init_type = (
68
- accessKey: number,
69
- modelPath: number,
70
- numSpeakers: number,
71
- speakerProfiles: number,
72
- object: number
73
- ) => Promise<number>;
74
- type pv_eagle_delete_type = (object: number) => Promise<void>;
75
- type pv_eagle_process_type = (
76
- object: number,
77
- pcm: number,
78
- scores: number
79
- ) => Promise<number>;
80
- type pv_eagle_reset_type = (object: number) => Promise<number>;
81
- type pv_eagle_frame_length_type = () => Promise<number>;
82
- type pv_eagle_version_type = () => Promise<number>;
83
- type pv_sample_rate_type = () => Promise<number>;
84
- type pv_status_to_string_type = (status: number) => Promise<number>;
85
- type pv_set_sdk_type = (sdk: number) => Promise<void>;
86
- type pv_get_error_stack_type = (
87
- messageStack: number,
88
- messageStackDepth: number
89
- ) => Promise<number>;
90
- type pv_free_error_stack_type = (messageStack: number) => Promise<void>;
91
-
92
- type EagleBaseWasmOutput = {
93
- memory: WebAssembly.Memory;
94
- alignedAlloc: aligned_alloc_type;
95
- pvFree: pv_free_type;
96
- pvError: PvError;
97
-
98
- sampleRate: number;
99
- version: string;
100
-
101
- messageStackAddressAddressAddress: number;
102
- messageStackDepthAddress: number;
103
-
104
- pvStatusToString: pv_status_to_string_type;
105
- pvGetErrorStack: pv_get_error_stack_type;
106
- pvFreeErrorStack: pv_free_error_stack_type;
107
-
108
- exports: any;
109
- };
110
-
111
- type EagleProfilerWasmOutput = EagleBaseWasmOutput & {
112
- minEnrollSamples: number;
113
- profileSize: number;
114
-
115
- objectAddress: number;
116
-
117
- pvEagleProfilerDelete: pv_eagle_profiler_delete_type;
118
- pvEagleProfilerEnroll: pv_eagle_profiler_enroll_type;
119
- pvEagleProfilerExport: pv_eagle_profiler_export_type;
120
- pvEagleProfilerReset: pv_eagle_profiler_reset_type;
121
- };
122
-
123
- type EagleWasmOutput = EagleBaseWasmOutput & {
124
- frameLength: number;
125
- numSpeakers: number;
126
-
127
- objectAddress: number;
128
- scoresAddress: number;
129
-
130
- pvEagleDelete: pv_eagle_delete_type;
131
- pvEagleProcess: pv_eagle_process_type;
132
- pvEagleReset: pv_eagle_profiler_reset_type;
133
- };
134
-
135
- const PV_STATUS_SUCCESS = 10000;
136
- const MAX_PCM_LENGTH_SEC = 60 * 15;
137
-
138
- class EagleBase {
139
- protected readonly _pvStatusToString: pv_status_to_string_type;
140
- protected _wasmMemory: WebAssembly.Memory | undefined;
141
- protected readonly _alignedAlloc: CallableFunction;
142
- protected readonly _pvFree: pv_free_type;
143
- protected readonly _functionMutex: Mutex;
144
-
145
- protected readonly _pvGetErrorStack: pv_get_error_stack_type;
146
- protected readonly _pvFreeErrorStack: pv_free_error_stack_type;
147
-
148
- protected readonly _messageStackAddressAddressAddress: number;
149
- protected readonly _messageStackDepthAddress: number;
150
-
151
- protected static _sampleRate: number;
152
- protected static _version: string;
153
-
154
- protected static _wasm: string;
155
- protected static _wasmSimd: string;
156
- protected static _sdk: string = 'web';
157
-
158
- protected static _eagleMutex = new Mutex();
159
-
160
- protected readonly _pvError: PvError;
161
-
162
- protected constructor(handleWasm: EagleBaseWasmOutput) {
163
- EagleBase._sampleRate = handleWasm.sampleRate;
164
- EagleBase._version = handleWasm.version;
165
-
166
- this._pvStatusToString = handleWasm.pvStatusToString;
167
-
168
- this._wasmMemory = handleWasm.memory;
169
- this._alignedAlloc = handleWasm.alignedAlloc;
170
- this._pvFree = handleWasm.pvFree;
171
- this._pvError = handleWasm.pvError;
172
-
173
- this._pvGetErrorStack = handleWasm.pvGetErrorStack;
174
- this._pvFreeErrorStack = handleWasm.pvFreeErrorStack;
175
-
176
- this._messageStackAddressAddressAddress = handleWasm.messageStackAddressAddressAddress;
177
- this._messageStackDepthAddress = handleWasm.messageStackDepthAddress;
178
-
179
- this._functionMutex = new Mutex();
180
- }
181
-
182
- /**
183
- * Audio sample rate required by Eagle.
184
- */
185
- get sampleRate(): number {
186
- return EagleBase._sampleRate;
187
- }
188
-
189
- /**
190
- * Version of Eagle.
191
- */
192
- get version(): string {
193
- return EagleBase._version;
194
- }
195
-
196
- /**
197
- * Set base64 wasm file.
198
- * @param wasm Base64'd wasm file to use to initialize wasm.
199
- */
200
- public static setWasm(wasm: string): void {
201
- if (this._wasm === undefined) {
202
- this._wasm = wasm;
203
- }
204
- }
205
-
206
- /**
207
- * Set base64 wasm file with SIMD feature.
208
- * @param wasmSimd Base64'd wasm file to use to initialize wasm.
209
- */
210
- public static setWasmSimd(wasmSimd: string): void {
211
- if (this._wasmSimd === undefined) {
212
- this._wasmSimd = wasmSimd;
213
- }
214
- }
215
-
216
- public static setSdk(sdk: string): void {
217
- EagleBase._sdk = sdk;
218
- }
219
-
220
- protected static async _initBaseWasm(
221
- wasmBase64: string,
222
- wasmMemorySize: number
223
- ): Promise<EagleBaseWasmOutput> {
224
- // A WebAssembly page has a constant size of 64KiB. -> 1MiB ~= 16 pages
225
- const memory = new WebAssembly.Memory({ initial: wasmMemorySize });
226
- const memoryBufferUint8 = new Uint8Array(memory.buffer);
227
- const pvError = new PvError();
228
- const exports = await buildWasm(memory, wasmBase64, pvError);
229
-
230
- const aligned_alloc = exports.aligned_alloc as aligned_alloc_type;
231
- const pv_free = exports.pv_free as pv_free_type;
232
- const pv_eagle_version = exports.pv_eagle_version as pv_eagle_version_type;
233
- const pv_sample_rate = exports.pv_sample_rate as pv_sample_rate_type;
234
- const pv_status_to_string =
235
- exports.pv_status_to_string as pv_status_to_string_type;
236
- const pv_set_sdk = exports.pv_set_sdk as pv_set_sdk_type;
237
- const pv_get_error_stack =
238
- exports.pv_get_error_stack as pv_get_error_stack_type;
239
- const pv_free_error_stack =
240
- exports.pv_free_error_stack as pv_free_error_stack_type;
241
-
242
- const sampleRate = await pv_sample_rate();
243
- const versionAddress = await pv_eagle_version();
244
- const version = arrayBufferToStringAtIndex(
245
- memoryBufferUint8,
246
- versionAddress
247
- );
248
-
249
- const sdkEncoded = new TextEncoder().encode(this._sdk);
250
- const sdkAddress = await aligned_alloc(
251
- Uint8Array.BYTES_PER_ELEMENT,
252
- (sdkEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
253
- );
254
- if (!sdkAddress) {
255
- throw new EagleErrors.EagleOutOfMemoryError(
256
- 'malloc failed: Cannot allocate memory'
257
- );
258
- }
259
- memoryBufferUint8.set(sdkEncoded, sdkAddress);
260
- memoryBufferUint8[sdkAddress + sdkEncoded.length] = 0;
261
- await pv_set_sdk(sdkAddress);
262
-
263
- const messageStackDepthAddress = await aligned_alloc(
264
- Int32Array.BYTES_PER_ELEMENT,
265
- Int32Array.BYTES_PER_ELEMENT
266
- );
267
- if (!messageStackDepthAddress) {
268
- throw new EagleErrors.EagleOutOfMemoryError(
269
- 'malloc failed: Cannot allocate memory'
270
- );
271
- }
272
-
273
- const messageStackAddressAddressAddress = await aligned_alloc(
274
- Int32Array.BYTES_PER_ELEMENT,
275
- Int32Array.BYTES_PER_ELEMENT
276
- );
277
- if (!messageStackAddressAddressAddress) {
278
- throw new EagleErrors.EagleOutOfMemoryError(
279
- 'malloc failed: Cannot allocate memory'
280
- );
281
- }
282
-
283
- return {
284
- memory: memory,
285
- alignedAlloc: aligned_alloc,
286
- pvFree: pv_free,
287
- pvError: pvError,
288
-
289
- sampleRate: sampleRate,
290
- version: version,
291
-
292
- messageStackAddressAddressAddress: messageStackAddressAddressAddress,
293
- messageStackDepthAddress: messageStackDepthAddress,
294
-
295
- pvStatusToString: pv_status_to_string,
296
- pvGetErrorStack: pv_get_error_stack,
297
- pvFreeErrorStack: pv_free_error_stack,
298
-
299
- exports: exports,
300
- };
301
- }
302
-
303
- /**
304
- * Releases resources acquired by Eagle
305
- */
306
- public async release(): Promise<void> {
307
- await this._pvFree(this._messageStackAddressAddressAddress);
308
- await this._pvFree(this._messageStackDepthAddress);
309
- }
310
-
311
- protected static async getMessageStack(
312
- pv_get_error_stack: pv_get_error_stack_type,
313
- pv_free_error_stack: pv_free_error_stack_type,
314
- messageStackAddressAddressAddress: number,
315
- messageStackDepthAddress: number,
316
- memoryBufferView: DataView,
317
- memoryBufferUint8: Uint8Array
318
- ): Promise<string[]> {
319
- const status = await pv_get_error_stack(
320
- messageStackAddressAddressAddress,
321
- messageStackDepthAddress
322
- );
323
- if (status !== PvStatus.SUCCESS) {
324
- throw pvStatusToException(status, 'Unable to get Eagle error state');
325
- }
326
-
327
- const messageStackAddressAddress = memoryBufferView.getInt32(
328
- messageStackAddressAddressAddress,
329
- true
330
- );
331
-
332
- const messageStackDepth = memoryBufferView.getInt32(
333
- messageStackDepthAddress,
334
- true
335
- );
336
- const messageStack: string[] = [];
337
- for (let i = 0; i < messageStackDepth; i++) {
338
- const messageStackAddress = memoryBufferView.getInt32(
339
- messageStackAddressAddress + i * Int32Array.BYTES_PER_ELEMENT,
340
- true
341
- );
342
- const message = arrayBufferToStringAtIndex(
343
- memoryBufferUint8,
344
- messageStackAddress
345
- );
346
- messageStack.push(message);
347
- }
348
-
349
- await pv_free_error_stack(messageStackAddressAddress);
350
-
351
- return messageStack;
352
- }
353
- }
354
-
355
- /**
356
- * JavaScript/WebAssembly binding for the profiler of the Eagle Speaker Recognition engine.
357
- * It enrolls a speaker given a set of utterances and then constructs a profile for the enrolled speaker.
358
- */
359
- export class EagleProfiler extends EagleBase {
360
- private readonly _pvEagleProfilerDelete: pv_eagle_profiler_delete_type;
361
- private readonly _pvEagleProfilerEnroll: pv_eagle_profiler_enroll_type;
362
- private readonly _pvEagleProfilerExport: pv_eagle_profiler_export_type;
363
- private readonly _pvEagleProfilerReset: pv_eagle_profiler_reset_type;
364
-
365
- private readonly _objectAddress: number;
366
-
367
- private static _maxEnrollSamples: number;
368
- private static _minEnrollSamples: number;
369
- private static _profileSize: number;
370
-
371
- private constructor(handleWasm: EagleProfilerWasmOutput) {
372
- super(handleWasm);
373
-
374
- EagleProfiler._minEnrollSamples = handleWasm.minEnrollSamples;
375
- EagleProfiler._profileSize = handleWasm.profileSize;
376
- EagleProfiler._maxEnrollSamples =
377
- MAX_PCM_LENGTH_SEC * EagleProfiler._sampleRate;
378
-
379
- this._pvEagleProfilerDelete = handleWasm.pvEagleProfilerDelete;
380
- this._pvEagleProfilerEnroll = handleWasm.pvEagleProfilerEnroll;
381
- this._pvEagleProfilerExport = handleWasm.pvEagleProfilerExport;
382
- this._pvEagleProfilerReset = handleWasm.pvEagleProfilerReset;
383
-
384
- this._objectAddress = handleWasm.objectAddress;
385
- }
386
-
387
- /**
388
- * The minimum length of the input pcm required by `.enroll()`.
389
- */
390
- get minEnrollSamples(): number {
391
- return EagleProfiler._minEnrollSamples;
392
- }
393
-
394
- /**
395
- * Creates an instance of profiler component of the Eagle Speaker Recognition Engine.
396
- *
397
- * @param accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/).
398
- * @param model Eagle model options.
399
- * @param model.base64 The model in base64 string to initialize Eagle.
400
- * @param model.publicPath The model path relative to the public directory.
401
- * @param model.customWritePath Custom path to save the model in storage.
402
- * Set to a different name to use multiple models across `eagle` instances.
403
- * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
404
- * @param model.version Version of the model file. Increment to update the model file in storage.
405
- *
406
- * @return An instance of the Eagle Profiler.
407
- */
408
- public static async create(
409
- accessKey: string,
410
- model: EagleModel
411
- ): Promise<EagleProfiler> {
412
- const customWritePath = model.customWritePath
413
- ? model.customWritePath
414
- : 'eagle_model';
415
- const modelPath = await loadModel({ ...model, customWritePath });
416
-
417
- return EagleProfiler._init(accessKey, modelPath);
418
- }
419
-
420
- public static async _init(
421
- accessKey: string,
422
- modelPath: string
423
- ): Promise<EagleProfiler> {
424
- if (!isAccessKeyValid(accessKey)) {
425
- throw new EagleErrors.EagleInvalidArgumentError('Invalid AccessKey');
426
- }
427
-
428
- return new Promise<EagleProfiler>((resolve, reject) => {
429
- EagleProfiler._eagleMutex
430
- .runExclusive(async () => {
431
- const isSimd = await simd();
432
- const wasmOutput = await EagleProfiler._initProfilerWasm(
433
- accessKey,
434
- modelPath,
435
- isSimd ? this._wasmSimd : this._wasm
436
- );
437
- return new EagleProfiler(wasmOutput);
438
- })
439
- .then((result: EagleProfiler) => {
440
- resolve(result);
441
- })
442
- .catch((error: any) => {
443
- reject(error);
444
- });
445
- });
446
- }
447
-
448
- /**
449
- * Enrolls a speaker. This function should be called multiple times with different utterances of the same speaker
450
- * until `percentage` reaches `100.0`, at which point a speaker voice profile can be exported using `.export()`.
451
- * Any further enrollment can be used to improve the speaker profile. The minimum length of the input pcm to
452
- * `.enroll()` can be obtained by calling `.minEnrollSamples`.
453
- * The audio data used for enrollment should satisfy the following requirements:
454
- * - only one speaker should be present in the audio
455
- * - the speaker should be speaking in a normal voice
456
- * - the audio should contain no speech from other speakers and no other sounds (e.g. music)
457
- * - it should be captured in a quiet environment with no background noise
458
- * @param pcm Audio data for enrollment. The audio needs to have a sample rate equal to `.sampleRate` and be
459
- * 16-bit linearly-encoded. EagleProfiler operates on single-channel audio.
460
- *
461
- * @return The percentage of completeness of the speaker enrollment process along with the feedback code
462
- * corresponding to the last enrollment attempt:
463
- * - `AUDIO_OK`: The audio is good for enrollment.
464
- * - `AUDIO_TOO_SHORT`: Audio length is insufficient for enrollment,
465
- * i.e. it is shorter than`.min_enroll_samples`.
466
- * - `UNKNOWN_SPEAKER`: There is another speaker in the audio that is different from the speaker
467
- * being enrolled. Too much background noise may cause this error as well.
468
- * - `NO_VOICE_FOUND`: The audio does not contain any voice, i.e. it is silent or
469
- * has a low signal-to-noise ratio.
470
- * - `QUALITY_ISSUE`: The audio quality is too low for enrollment due to a bad microphone
471
- * or recording environment.
472
- */
473
- public async enroll(pcm: Int16Array): Promise<EagleProfilerEnrollResult> {
474
- if (!(pcm instanceof Int16Array)) {
475
- throw new EagleErrors.EagleInvalidArgumentError("The argument 'pcm' must be provided as an Int16Array");
476
- }
477
-
478
- if (pcm.length > EagleProfiler._maxEnrollSamples) {
479
- throw new EagleErrors.EagleInvalidArgumentError(
480
- `'pcm' size must be smaller than ${EagleProfiler._maxEnrollSamples}`
481
- );
482
- }
483
-
484
- return new Promise<EagleProfilerEnrollResult>((resolve, reject) => {
485
- this._functionMutex
486
- .runExclusive(async () => {
487
- if (this._wasmMemory === undefined) {
488
- throw new EagleErrors.EagleInvalidStateError('Attempted to call `.enroll()` after release');
489
- }
490
-
491
- const pcmAddress = await this._alignedAlloc(
492
- Int16Array.BYTES_PER_ELEMENT,
493
- pcm.length * Int16Array.BYTES_PER_ELEMENT
494
- );
495
-
496
- const feedbackAddress = await this._alignedAlloc(
497
- Int32Array.BYTES_PER_ELEMENT,
498
- Int32Array.BYTES_PER_ELEMENT
499
- );
500
- if (feedbackAddress === 0) {
501
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
502
- }
503
- const percentageAddress = await this._alignedAlloc(
504
- Int32Array.BYTES_PER_ELEMENT,
505
- Int32Array.BYTES_PER_ELEMENT
506
- );
507
- if (percentageAddress === 0) {
508
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
509
- }
510
-
511
- const memoryBufferInt16 = new Int16Array(this._wasmMemory.buffer);
512
- memoryBufferInt16.set(pcm, pcmAddress / Int16Array.BYTES_PER_ELEMENT);
513
-
514
- const status = await this._pvEagleProfilerEnroll(
515
- this._objectAddress,
516
- pcmAddress,
517
- pcm.length,
518
- feedbackAddress,
519
- percentageAddress
520
- );
521
- await this._pvFree(pcmAddress);
522
- if (status !== PV_STATUS_SUCCESS) {
523
- await this._pvFree(feedbackAddress);
524
- await this._pvFree(percentageAddress);
525
-
526
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
527
- const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer);
528
-
529
- const messageStack = await EagleProfiler.getMessageStack(
530
- this._pvGetErrorStack,
531
- this._pvFreeErrorStack,
532
- this._messageStackAddressAddressAddress,
533
- this._messageStackDepthAddress,
534
- memoryBufferView,
535
- memoryBufferUint8
536
- );
537
-
538
- throw pvStatusToException(status, "EagleProfiler enroll failed", messageStack);
539
- }
540
-
541
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
542
-
543
- const feedback = memoryBufferView.getInt32(
544
- feedbackAddress,
545
- true
546
- );
547
-
548
- const percentage = memoryBufferView.getFloat32(
549
- percentageAddress,
550
- true
551
- );
552
-
553
- await this._pvFree(feedbackAddress);
554
- await this._pvFree(percentageAddress);
555
-
556
- return { feedback, percentage };
557
- })
558
- .then((result: EagleProfilerEnrollResult) => {
559
- resolve(result);
560
- })
561
- .catch((error: any) => {
562
- reject(error);
563
- });
564
- });
565
- }
566
-
567
- /**
568
- * Exports the speaker profile of the current session.
569
- * Will throw error if the profile is not ready.
570
- *
571
- * @return An EagleProfile object.
572
- */
573
- public async export(): Promise<EagleProfile> {
574
- return new Promise<EagleProfile>((resolve, reject) => {
575
- this._functionMutex
576
- .runExclusive(async () => {
577
- if (this._wasmMemory === undefined) {
578
- throw new EagleErrors.EagleInvalidStateError('Attempted to call `.export()` after release');
579
- }
580
-
581
- const profileAddress = await this._alignedAlloc(
582
- Uint8Array.BYTES_PER_ELEMENT,
583
- Uint8Array.BYTES_PER_ELEMENT * EagleProfiler._profileSize
584
- );
585
-
586
- const status = await this._pvEagleProfilerExport(
587
- this._objectAddress,
588
- profileAddress
589
- );
590
- if (status !== PV_STATUS_SUCCESS) {
591
- await this._pvFree(profileAddress);
592
-
593
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
594
- const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer);
595
-
596
- const messageStack = await EagleProfiler.getMessageStack(
597
- this._pvGetErrorStack,
598
- this._pvFreeErrorStack,
599
- this._messageStackAddressAddressAddress,
600
- this._messageStackDepthAddress,
601
- memoryBufferView,
602
- memoryBufferUint8
603
- );
604
-
605
- throw pvStatusToException(status, "EagleProfiler export failed", messageStack);
606
- }
607
-
608
- const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer);
609
-
610
- const profile = memoryBufferUint8.slice(
611
- profileAddress / Uint8Array.BYTES_PER_ELEMENT,
612
- profileAddress / Uint8Array.BYTES_PER_ELEMENT +
613
- EagleProfiler._profileSize
614
- );
615
- await this._pvFree(profileAddress);
616
-
617
- return { bytes: profile };
618
- })
619
- .then((result: EagleProfile) => {
620
- resolve(result);
621
- })
622
- .catch((error: any) => {
623
- reject(error);
624
- });
625
- });
626
- }
627
-
628
- /**
629
- * Resets the internal state of Eagle Profiler.
630
- * It should be called before starting a new enrollment session.
631
- */
632
- public async reset(): Promise<void> {
633
- return new Promise<void>((resolve, reject) => {
634
- this._functionMutex
635
- .runExclusive(async () => {
636
- if (this._wasmMemory === undefined) {
637
- throw new EagleErrors.EagleInvalidStateError('Attempted to call `.reset()` after release');
638
- }
639
-
640
- const status = await this._pvEagleProfilerReset(this._objectAddress);
641
- if (status !== PV_STATUS_SUCCESS) {
642
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
643
- const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer);
644
-
645
- const messageStack = await EagleProfiler.getMessageStack(
646
- this._pvGetErrorStack,
647
- this._pvFreeErrorStack,
648
- this._messageStackAddressAddressAddress,
649
- this._messageStackDepthAddress,
650
- memoryBufferView,
651
- memoryBufferUint8
652
- );
653
-
654
- throw pvStatusToException(status, "EagleProfiler reset failed", messageStack);
655
- }
656
- })
657
- .then(() => {
658
- resolve();
659
- })
660
- .catch((error: any) => {
661
- reject(error);
662
- });
663
- });
664
- }
665
-
666
- /**
667
- * Releases resources acquired by Eagle Profiler
668
- */
669
- public async release(): Promise<void> {
670
- await super.release();
671
- await this._pvEagleProfilerDelete(this._objectAddress);
672
- delete this._wasmMemory;
673
- this._wasmMemory = undefined;
674
- }
675
-
676
- private static async _initProfilerWasm(
677
- accessKey: string,
678
- modelPath: string,
679
- wasmBase64: string
680
- ): Promise<EagleProfilerWasmOutput> {
681
- const baseWasmOutput = await super._initBaseWasm(wasmBase64, 3500);
682
- const memoryBufferUint8 = new Uint8Array(baseWasmOutput.memory.buffer);
683
-
684
- const pv_eagle_profiler_init = baseWasmOutput.exports
685
- .pv_eagle_profiler_init as pv_eagle_profiler_init_type;
686
- const pv_eagle_profiler_delete = baseWasmOutput.exports
687
- .pv_eagle_profiler_delete as pv_eagle_profiler_delete_type;
688
- const pv_eagle_profiler_enroll = baseWasmOutput.exports
689
- .pv_eagle_profiler_enroll as pv_eagle_profiler_enroll_type;
690
- const pv_eagle_profiler_enroll_min_audio_length_samples = baseWasmOutput
691
- .exports
692
- .pv_eagle_profiler_enroll_min_audio_length_samples as pv_eagle_profiler_enroll_min_audio_length_samples_type;
693
- const pv_eagle_profiler_export = baseWasmOutput.exports
694
- .pv_eagle_profiler_export as pv_eagle_profiler_export_type;
695
- const pv_eagle_profiler_export_size = baseWasmOutput.exports
696
- .pv_eagle_profiler_export_size as pv_eagle_profiler_export_size_type;
697
- const pv_eagle_profiler_reset = baseWasmOutput.exports
698
- .pv_eagle_profiler_reset as pv_eagle_profiler_reset_type;
699
-
700
- const objectAddressAddress = await baseWasmOutput.alignedAlloc(
701
- Int32Array.BYTES_PER_ELEMENT,
702
- Int32Array.BYTES_PER_ELEMENT
703
- );
704
- if (objectAddressAddress === 0) {
705
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
706
- }
707
-
708
- const accessKeyAddress = await baseWasmOutput.alignedAlloc(
709
- Uint8Array.BYTES_PER_ELEMENT,
710
- (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT
711
- );
712
- if (accessKeyAddress === 0) {
713
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
714
- }
715
- for (let i = 0; i < accessKey.length; i++) {
716
- memoryBufferUint8[accessKeyAddress + i] = accessKey.charCodeAt(i);
717
- }
718
- memoryBufferUint8[accessKeyAddress + accessKey.length] = 0;
719
-
720
- const modelPathEncoded = new TextEncoder().encode(modelPath);
721
- const modelPathAddress = await baseWasmOutput.alignedAlloc(
722
- Uint8Array.BYTES_PER_ELEMENT,
723
- (modelPathEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
724
- );
725
- if (modelPathAddress === 0) {
726
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
727
- }
728
- memoryBufferUint8.set(modelPathEncoded, modelPathAddress);
729
- memoryBufferUint8[modelPathAddress + modelPathEncoded.length] = 0;
730
-
731
- let status = await pv_eagle_profiler_init(
732
- accessKeyAddress,
733
- modelPathAddress,
734
- objectAddressAddress
735
- );
736
- await baseWasmOutput.pvFree(accessKeyAddress);
737
- await baseWasmOutput.pvFree(modelPathAddress);
738
-
739
- const memoryBufferView = new DataView(baseWasmOutput.memory.buffer);
740
-
741
- if (status !== PV_STATUS_SUCCESS) {
742
- const messageStack = await EagleProfiler.getMessageStack(
743
- baseWasmOutput.pvGetErrorStack,
744
- baseWasmOutput.pvFreeErrorStack,
745
- baseWasmOutput.messageStackAddressAddressAddress,
746
- baseWasmOutput.messageStackDepthAddress,
747
- memoryBufferView,
748
- memoryBufferUint8
749
- );
750
-
751
- throw pvStatusToException(status, "EagleProfiler init failed", messageStack, baseWasmOutput.pvError);
752
- }
753
-
754
- const objectAddress = memoryBufferView.getInt32(objectAddressAddress, true);
755
- await baseWasmOutput.pvFree(objectAddressAddress);
756
-
757
- const minEnrollSamplesAddress = await baseWasmOutput.alignedAlloc(
758
- Int32Array.BYTES_PER_ELEMENT,
759
- Int32Array.BYTES_PER_ELEMENT
760
- );
761
- if (minEnrollSamplesAddress === 0) {
762
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
763
- }
764
-
765
- status = await pv_eagle_profiler_enroll_min_audio_length_samples(
766
- objectAddress,
767
- minEnrollSamplesAddress
768
- );
769
- if (status !== PV_STATUS_SUCCESS) {
770
- const messageStack = await EagleProfiler.getMessageStack(
771
- baseWasmOutput.pvGetErrorStack,
772
- baseWasmOutput.pvFreeErrorStack,
773
- baseWasmOutput.messageStackAddressAddressAddress,
774
- baseWasmOutput.messageStackDepthAddress,
775
- memoryBufferView,
776
- memoryBufferUint8
777
- );
778
-
779
- throw pvStatusToException(status, "EagleProfiler failed to get min enroll audio length", messageStack, baseWasmOutput.pvError);
780
- }
781
-
782
- const minEnrollSamples = memoryBufferView.getInt32(
783
- minEnrollSamplesAddress,
784
- true
785
- );
786
- await baseWasmOutput.pvFree(minEnrollSamplesAddress);
787
-
788
- const profileSizeAddress = await baseWasmOutput.alignedAlloc(
789
- Int32Array.BYTES_PER_ELEMENT,
790
- Int32Array.BYTES_PER_ELEMENT
791
- );
792
- if (profileSizeAddress === 0) {
793
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
794
- }
795
-
796
- status = await pv_eagle_profiler_export_size(
797
- objectAddress,
798
- profileSizeAddress
799
- );
800
- if (status !== PV_STATUS_SUCCESS) {
801
- const messageStack = await EagleProfiler.getMessageStack(
802
- baseWasmOutput.pvGetErrorStack,
803
- baseWasmOutput.pvFreeErrorStack,
804
- baseWasmOutput.messageStackAddressAddressAddress,
805
- baseWasmOutput.messageStackDepthAddress,
806
- memoryBufferView,
807
- memoryBufferUint8
808
- );
809
-
810
- throw pvStatusToException(status, "EagleProfiler failed to get export size", messageStack, baseWasmOutput.pvError);
811
- }
812
-
813
- const profileSize = memoryBufferView.getInt32(profileSizeAddress, true);
814
- await baseWasmOutput.pvFree(profileSizeAddress);
815
-
816
- return {
817
- ...baseWasmOutput,
818
- minEnrollSamples: minEnrollSamples,
819
- profileSize: profileSize,
820
-
821
- objectAddress: objectAddress,
822
-
823
- pvEagleProfilerDelete: pv_eagle_profiler_delete,
824
- pvEagleProfilerEnroll: pv_eagle_profiler_enroll,
825
- pvEagleProfilerExport: pv_eagle_profiler_export,
826
- pvEagleProfilerReset: pv_eagle_profiler_reset,
827
- };
828
- }
829
- }
830
-
831
- /**
832
- * JavaScript/WebAssembly binding for Eagle Speaker Recognition engine.
833
- * It processes incoming audio in consecutive frames and emits a similarity score for each enrolled speaker.
834
- */
835
- export class Eagle extends EagleBase {
836
- private readonly _pvEagleDelete: pv_eagle_delete_type;
837
- private readonly _pvEagleProcess: pv_eagle_process_type;
838
- private readonly _pvEagleReset: pv_eagle_profiler_reset_type;
839
-
840
- private readonly _objectAddress: number;
841
- private readonly _scoresAddress: number;
842
- private readonly _numSpeakers: number;
843
-
844
- private static _frameLength: number;
845
-
846
- private constructor(handleWasm: EagleWasmOutput) {
847
- super(handleWasm);
848
-
849
- Eagle._frameLength = handleWasm.frameLength;
850
-
851
- this._pvEagleDelete = handleWasm.pvEagleDelete;
852
- this._pvEagleProcess = handleWasm.pvEagleProcess;
853
- this._pvEagleReset = handleWasm.pvEagleReset;
854
-
855
- this._objectAddress = handleWasm.objectAddress;
856
- this._scoresAddress = handleWasm.scoresAddress;
857
- this._numSpeakers = handleWasm.numSpeakers;
858
- }
859
-
860
- /**
861
- * Number of audio samples per frame expected by Eagle (i.e. length of the array passed into `.process()`)
862
- */
863
- get frameLength(): number {
864
- return Eagle._frameLength;
865
- }
866
-
867
- /**
868
- * Creates an instance of the Picovoice Eagle Speaker Recognition Engine.
869
- *
870
- * @param accessKey AccessKey obtained from Picovoice Console (https://console.picovoice.ai/)
871
- * @param model Eagle model options.
872
- * @param model.base64 The model in base64 string to initialize Eagle.
873
- * @param model.publicPath The model path relative to the public directory.
874
- * @param model.customWritePath Custom path to save the model in storage.
875
- * Set to a different name to use multiple models across `eagle` instances.
876
- * @param model.forceWrite Flag to overwrite the model in storage even if it exists.
877
- * @param model.version Version of the model file. Increment to update the model file in storage.
878
- * @param speakerProfiles One or more Eagle speaker profiles. These can be constructed using `EagleProfiler`.
879
- *
880
- * @return An instance of the Eagle engine.
881
- */
882
- public static async create(
883
- accessKey: string,
884
- model: EagleModel,
885
- speakerProfiles: EagleProfile[] | EagleProfile
886
- ): Promise<Eagle> {
887
- const customWritePath = model.customWritePath
888
- ? model.customWritePath
889
- : 'eagle_model';
890
- const modelPath = await loadModel({ ...model, customWritePath });
891
-
892
- return Eagle._init(
893
- accessKey,
894
- modelPath,
895
- !Array.isArray(speakerProfiles) ? [speakerProfiles] : speakerProfiles
896
- );
897
- }
898
-
899
- public static async _init(
900
- accessKey: string,
901
- modelPath: string,
902
- speakerProfiles: EagleProfile[]
903
- ): Promise<Eagle> {
904
- if (!isAccessKeyValid(accessKey)) {
905
- throw new EagleErrors.EagleInvalidArgumentError('Invalid AccessKey');
906
- }
907
-
908
- if (!speakerProfiles || speakerProfiles.length === 0) {
909
- throw new EagleErrors.EagleInvalidArgumentError('No speaker profiles provided');
910
- }
911
-
912
- return new Promise<Eagle>((resolve, reject) => {
913
- Eagle._eagleMutex
914
- .runExclusive(async () => {
915
- const isSimd = await simd();
916
- const wasmOutput = await Eagle._initWasm(
917
- accessKey,
918
- modelPath,
919
- speakerProfiles,
920
- isSimd ? this._wasmSimd : this._wasm
921
- );
922
- return new Eagle(wasmOutput);
923
- })
924
- .then((result: Eagle) => {
925
- resolve(result);
926
- })
927
- .catch((error: any) => {
928
- reject(error);
929
- });
930
- });
931
- }
932
-
933
- /**
934
- * Processes a frame of audio and returns a list of similarity scores for each speaker profile.
935
- *
936
- * @param pcm A frame of audio samples. The number of samples per frame can be attained by calling
937
- * `.frameLength`. The incoming audio needs to have a sample rate equal to `.sampleRate` and be 16-bit
938
- * linearly-encoded. Eagle operates on single-channel audio.
939
- *
940
- * @return A list of similarity scores for each speaker profile. A higher score indicates that the voice
941
- * belongs to the corresponding speaker. The range is [0, 1] with 1.0 representing a perfect match.
942
- */
943
- public async process(pcm: Int16Array): Promise<number[]> {
944
- if (!(pcm instanceof Int16Array)) {
945
- throw new EagleErrors.EagleInvalidArgumentError("The argument 'pcm' must be provided as an Int16Array");
946
- }
947
-
948
- if (pcm.length !== Eagle._frameLength) {
949
- throw new EagleErrors.EagleInvalidArgumentError(
950
- `Length of input frame (${pcm.length}) does not match required frame length (${Eagle._frameLength})`
951
- );
952
- }
953
-
954
- return new Promise<number[]>((resolve, reject) => {
955
- this._functionMutex
956
- .runExclusive(async () => {
957
- if (this._wasmMemory === undefined) {
958
- throw new EagleErrors.EagleInvalidStateError('Attempted to call `.process` after release');
959
- }
960
-
961
- const pcmAddress = await this._alignedAlloc(
962
- Int16Array.BYTES_PER_ELEMENT,
963
- pcm.length * Int16Array.BYTES_PER_ELEMENT
964
- );
965
-
966
- const memoryBufferInt16 = new Int16Array(this._wasmMemory.buffer);
967
- memoryBufferInt16.set(pcm, pcmAddress / Int16Array.BYTES_PER_ELEMENT);
968
-
969
- const status = await this._pvEagleProcess(
970
- this._objectAddress,
971
- pcmAddress,
972
- this._scoresAddress
973
- );
974
- await this._pvFree(pcmAddress);
975
- if (status !== PV_STATUS_SUCCESS) {
976
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
977
- const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer);
978
-
979
- const messageStack = await EagleProfiler.getMessageStack(
980
- this._pvGetErrorStack,
981
- this._pvFreeErrorStack,
982
- this._messageStackAddressAddressAddress,
983
- this._messageStackDepthAddress,
984
- memoryBufferView,
985
- memoryBufferUint8
986
- );
987
-
988
- throw pvStatusToException(status, "Eagle process failed", messageStack);
989
- }
990
-
991
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
992
-
993
- const scores: number[] = [];
994
- for (let i = 0; i < this._numSpeakers; i++) {
995
- scores.push(
996
- memoryBufferView.getFloat32(
997
- this._scoresAddress + i * Float32Array.BYTES_PER_ELEMENT,
998
- true
999
- )
1000
- );
1001
- }
1002
-
1003
- return scores;
1004
- })
1005
- .then((result: number[]) => {
1006
- resolve(result);
1007
- })
1008
- .catch((error: any) => {
1009
- reject(error);
1010
- });
1011
- });
1012
- }
1013
-
1014
- /**
1015
- * Resets the internal state of the engine.
1016
- * It is best to call before processing a new sequence of audio (e.g. a new voice interaction).
1017
- * This ensures that the accuracy of the engine is not affected by a change in audio context.
1018
- */
1019
- public async reset(): Promise<void> {
1020
- return new Promise<void>((resolve, reject) => {
1021
- this._functionMutex
1022
- .runExclusive(async () => {
1023
- if (this._wasmMemory === undefined) {
1024
- throw new EagleErrors.EagleInvalidStateError('Attempted to call `.reset` after release');
1025
- }
1026
-
1027
- const status = await this._pvEagleReset(this._objectAddress);
1028
- if (status !== PV_STATUS_SUCCESS) {
1029
- const memoryBufferView = new DataView(this._wasmMemory.buffer);
1030
- const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer);
1031
-
1032
- const messageStack = await EagleProfiler.getMessageStack(
1033
- this._pvGetErrorStack,
1034
- this._pvFreeErrorStack,
1035
- this._messageStackAddressAddressAddress,
1036
- this._messageStackDepthAddress,
1037
- memoryBufferView,
1038
- memoryBufferUint8
1039
- );
1040
-
1041
- throw pvStatusToException(status, "Eagle reset failed", messageStack);
1042
- }
1043
- })
1044
- .then(() => {
1045
- resolve();
1046
- })
1047
- .catch((error: any) => {
1048
- reject(error);
1049
- });
1050
- });
1051
- }
1052
-
1053
- /**
1054
- * Releases resources acquired by Eagle
1055
- */
1056
- public async release(): Promise<void> {
1057
- await super.release();
1058
- await this._pvFree(this._scoresAddress);
1059
- await this._pvEagleDelete(this._objectAddress);
1060
- delete this._wasmMemory;
1061
- this._wasmMemory = undefined;
1062
- }
1063
-
1064
- private static async _initWasm(
1065
- accessKey: string,
1066
- modelPath: string,
1067
- speakerProfiles: EagleProfile[],
1068
- wasmBase64: string
1069
- ): Promise<EagleWasmOutput> {
1070
- const baseWasmOutput = await super._initBaseWasm(wasmBase64, 3150);
1071
- const memoryBufferUint8 = new Uint8Array(baseWasmOutput.memory.buffer);
1072
- const memoryBufferInt32 = new Int32Array(baseWasmOutput.memory.buffer);
1073
-
1074
- const pv_eagle_init = baseWasmOutput.exports
1075
- .pv_eagle_init as pv_eagle_init_type;
1076
- const pv_eagle_delete = baseWasmOutput.exports
1077
- .pv_eagle_delete as pv_eagle_delete_type;
1078
- const pv_eagle_process = baseWasmOutput.exports
1079
- .pv_eagle_process as pv_eagle_process_type;
1080
- const pv_eagle_frame_length = baseWasmOutput.exports
1081
- .pv_eagle_frame_length as pv_eagle_frame_length_type;
1082
- const pv_eagle_reset = baseWasmOutput.exports
1083
- .pv_eagle_reset as pv_eagle_reset_type;
1084
-
1085
- const objectAddressAddress = await baseWasmOutput.alignedAlloc(
1086
- Int32Array.BYTES_PER_ELEMENT,
1087
- Int32Array.BYTES_PER_ELEMENT
1088
- );
1089
- if (objectAddressAddress === 0) {
1090
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
1091
- }
1092
-
1093
- const accessKeyAddress = await baseWasmOutput.alignedAlloc(
1094
- Uint8Array.BYTES_PER_ELEMENT,
1095
- (accessKey.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1096
- );
1097
- if (accessKeyAddress === 0) {
1098
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
1099
- }
1100
- for (let i = 0; i < accessKey.length; i++) {
1101
- memoryBufferUint8[accessKeyAddress + i] = accessKey.charCodeAt(i);
1102
- }
1103
- memoryBufferUint8[accessKeyAddress + accessKey.length] = 0;
1104
-
1105
- const modelPathEncoded = new TextEncoder().encode(modelPath);
1106
- const modelPathAddress = await baseWasmOutput.alignedAlloc(
1107
- Uint8Array.BYTES_PER_ELEMENT,
1108
- (modelPathEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
1109
- );
1110
- if (modelPathAddress === 0) {
1111
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
1112
- }
1113
- memoryBufferUint8.set(modelPathEncoded, modelPathAddress);
1114
- memoryBufferUint8[modelPathAddress + modelPathEncoded.length] = 0;
1115
-
1116
- const numSpeakers = speakerProfiles.length;
1117
- const profilesAddressAddress = await baseWasmOutput.alignedAlloc(
1118
- Int32Array.BYTES_PER_ELEMENT,
1119
- numSpeakers * Int32Array.BYTES_PER_ELEMENT
1120
- );
1121
- if (profilesAddressAddress === 0) {
1122
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
1123
- }
1124
- const profilesAddressList: number[] = [];
1125
- for (const profile of speakerProfiles) {
1126
- const profileAddress = await baseWasmOutput.alignedAlloc(
1127
- Uint8Array.BYTES_PER_ELEMENT,
1128
- profile.bytes.length * Uint8Array.BYTES_PER_ELEMENT
1129
- );
1130
- if (profileAddress === 0) {
1131
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
1132
- }
1133
- memoryBufferUint8.set(profile.bytes, profileAddress);
1134
- profilesAddressList.push(profileAddress);
1135
- }
1136
- memoryBufferInt32.set(
1137
- new Int32Array(profilesAddressList),
1138
- profilesAddressAddress / Int32Array.BYTES_PER_ELEMENT
1139
- );
1140
- const status = await pv_eagle_init(
1141
- accessKeyAddress,
1142
- modelPathAddress,
1143
- numSpeakers,
1144
- profilesAddressAddress,
1145
- objectAddressAddress
1146
- );
1147
- await baseWasmOutput.pvFree(accessKeyAddress);
1148
- await baseWasmOutput.pvFree(modelPathAddress);
1149
- await baseWasmOutput.pvFree(profilesAddressAddress);
1150
-
1151
- const memoryBufferView = new DataView(baseWasmOutput.memory.buffer);
1152
-
1153
- if (status !== PV_STATUS_SUCCESS) {
1154
- const messageStack = await EagleProfiler.getMessageStack(
1155
- baseWasmOutput.pvGetErrorStack,
1156
- baseWasmOutput.pvFreeErrorStack,
1157
- baseWasmOutput.messageStackAddressAddressAddress,
1158
- baseWasmOutput.messageStackDepthAddress,
1159
- memoryBufferView,
1160
- memoryBufferUint8
1161
- );
1162
-
1163
- throw pvStatusToException(status, "Eagle init failed", messageStack, baseWasmOutput.pvError);
1164
- }
1165
-
1166
- const objectAddress = memoryBufferView.getInt32(objectAddressAddress, true);
1167
- await baseWasmOutput.pvFree(objectAddressAddress);
1168
-
1169
- const scoresAddress = await baseWasmOutput.alignedAlloc(
1170
- Float32Array.BYTES_PER_ELEMENT,
1171
- numSpeakers * Float32Array.BYTES_PER_ELEMENT
1172
- );
1173
- if (scoresAddress === 0) {
1174
- throw new EagleErrors.EagleOutOfMemoryError('malloc failed: Cannot allocate memory');
1175
- }
1176
-
1177
- const frameLength = await pv_eagle_frame_length();
1178
-
1179
- return {
1180
- ...baseWasmOutput,
1181
- frameLength: frameLength,
1182
- numSpeakers: numSpeakers,
1183
- objectAddress: objectAddress,
1184
- scoresAddress: scoresAddress,
1185
-
1186
- pvEagleDelete: pv_eagle_delete,
1187
- pvEagleProcess: pv_eagle_process,
1188
- pvEagleReset: pv_eagle_reset,
1189
- };
1190
- }
1191
- }
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
+ }