@mieweb/ui 0.6.1-dev.165 → 0.6.1-dev.166

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.
Files changed (43) hide show
  1. package/dist/brands/index.cjs +7 -7
  2. package/dist/brands/index.js +2 -2
  3. package/dist/{chunk-NBD236EH.js → chunk-5O23GUS3.js} +15 -2
  4. package/dist/chunk-5O23GUS3.js.map +1 -0
  5. package/dist/chunk-6I7IDZ4A.js +51 -0
  6. package/dist/chunk-6I7IDZ4A.js.map +1 -0
  7. package/dist/{chunk-Z6NRP4Z5.cjs → chunk-JWTCEWQ4.cjs} +2 -2
  8. package/dist/{chunk-Z6NRP4Z5.cjs.map → chunk-JWTCEWQ4.cjs.map} +1 -1
  9. package/dist/{chunk-Y65SK5Y2.cjs → chunk-MJ7YITLN.cjs} +2 -2
  10. package/dist/{chunk-Y65SK5Y2.cjs.map → chunk-MJ7YITLN.cjs.map} +1 -1
  11. package/dist/{chunk-R6PBBPU3.js → chunk-TXRQQMG5.js} +2 -2
  12. package/dist/{chunk-R6PBBPU3.js.map → chunk-TXRQQMG5.js.map} +1 -1
  13. package/dist/chunk-UVSODK6V.cjs +53 -0
  14. package/dist/chunk-UVSODK6V.cjs.map +1 -0
  15. package/dist/{chunk-YYDW3ZZS.cjs → chunk-W5B3VQUQ.cjs} +15 -2
  16. package/dist/chunk-W5B3VQUQ.cjs.map +1 -0
  17. package/dist/{chunk-NSLR3B7K.js → chunk-XVF472GT.js} +2 -2
  18. package/dist/{chunk-NSLR3B7K.js.map → chunk-XVF472GT.js.map} +1 -1
  19. package/dist/components/Markdown/index.cjs +10 -10
  20. package/dist/components/Markdown/index.js +2 -2
  21. package/dist/components/Skeleton/index.d.cts +1 -1
  22. package/dist/components/Skeleton/index.d.ts +1 -1
  23. package/dist/hey-buddy-CLUVAY2X.cjs +1133 -0
  24. package/dist/hey-buddy-CLUVAY2X.cjs.map +1 -0
  25. package/dist/hey-buddy-NMSWZ4TN.js +1131 -0
  26. package/dist/hey-buddy-NMSWZ4TN.js.map +1 -0
  27. package/dist/index.cjs +4345 -542
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +786 -1
  30. package/dist/index.d.ts +786 -1
  31. package/dist/index.js +4321 -569
  32. package/dist/index.js.map +1 -1
  33. package/dist/speaker-verify-5GTWAN5Y.cjs +357 -0
  34. package/dist/speaker-verify-5GTWAN5Y.cjs.map +1 -0
  35. package/dist/speaker-verify-R67P433H.js +355 -0
  36. package/dist/speaker-verify-R67P433H.js.map +1 -0
  37. package/dist/styles/init.css +5 -0
  38. package/dist/styles.css +1 -1
  39. package/dist/tailwind-preset.cjs +4 -4
  40. package/dist/tailwind-preset.js +1 -1
  41. package/package.json +2 -1
  42. package/dist/chunk-NBD236EH.js.map +0 -1
  43. package/dist/chunk-YYDW3ZZS.cjs.map +0 -1
@@ -0,0 +1,1133 @@
1
+ 'use strict';
2
+
3
+ var chunkUVSODK6V_cjs = require('./chunk-UVSODK6V.cjs');
4
+
5
+ // src/components/AI/HeyOzwell/WakeWord/lib/helpers.js
6
+ var sleep = (ms) => {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ };
9
+
10
+ // src/components/AI/HeyOzwell/WakeWord/lib/onnx.js
11
+ var initialized = false;
12
+ var initError = null;
13
+ var Tensor;
14
+ var InferenceSession;
15
+ if (typeof ort !== "undefined") {
16
+ initialized = true;
17
+ Tensor = ort.Tensor;
18
+ InferenceSession = ort.InferenceSession;
19
+ } else {
20
+ import('onnxruntime-web').then((module) => {
21
+ try {
22
+ if (!module.env.wasm.wasmPaths) {
23
+ const v = module.env && module.env.versions && module.env.versions.web || "";
24
+ module.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web" + (v ? "@" + v : "") + "/dist/";
25
+ }
26
+ module.env.wasm.numThreads = 1;
27
+ } catch (e) {
28
+ }
29
+ initialized = true;
30
+ Tensor = module.Tensor;
31
+ InferenceSession = module.InferenceSession;
32
+ }).catch((e) => {
33
+ initError = e instanceof Error ? e : new Error(String(e));
34
+ });
35
+ }
36
+ var ONNX = class _ONNX {
37
+ /**
38
+ * Wait for the ONNX Runtime Web API to be initialized.
39
+ * @returns {Promise<void>} A promise that resolves when the ONNX Runtime Web API is initialized.
40
+ */
41
+ static async waitForInitialization() {
42
+ while (!initialized) {
43
+ if (initError) throw new Error("ONNX Runtime Web failed to load: " + initError.message);
44
+ await sleep(10);
45
+ }
46
+ }
47
+ /**
48
+ * Create a new tensor.
49
+ * @param {string} dtype The data type of the tensor.
50
+ * @param {Array<number>} data The data of the tensor.
51
+ * @param {Array<number>} dims The dimensions of the tensor.
52
+ * @returns {Promise<Tensor>} A promise that resolves to a new tensor.
53
+ */
54
+ static async createTensor(dtype, data, dims) {
55
+ await _ONNX.waitForInitialization();
56
+ return new Tensor(dtype, data, dims);
57
+ }
58
+ /**
59
+ * Create a new inference session.
60
+ * @param {ArrayBuffer} model The model to load.
61
+ * @param {Object} [options] The options for the inference session.
62
+ * @returns {Promise<InferenceSession>} A promise that resolves to a new inference session.
63
+ */
64
+ static async createInferenceSession(model, options = {}) {
65
+ await _ONNX.waitForInitialization();
66
+ return await InferenceSession.create(model, options);
67
+ }
68
+ };
69
+ ONNX.waitForInitialization().then(() => {
70
+ ONNX.createTensor = (dtype, data, dims) => new Tensor(dtype, data, dims);
71
+ ONNX.createInferenceSession = (model, options = {}) => InferenceSession.create(model, options);
72
+ }).catch(() => {
73
+ });
74
+
75
+ // src/components/AI/HeyOzwell/WakeWord/lib/audio.js
76
+ var workletName = "hey-buddy";
77
+ var workletBlob = new Blob([`(()=>{class t extends AudioWorkletProcessor{constructor(t){super(t),this.targetSampleRate=t.processorOptions.targetSampleRate,this.inputBuffer=new Float32Array(this.inputFrameSize),this.inputBufferSize=0,this.outputBuffer=new Float32Array(this.targetFrameSize)}get inputFrameSize(){return Math.round(sampleRate/50)}get targetFrameSize(){return Math.round(this.targetSampleRate/50)}async flush(){const t=sampleRate/this.targetSampleRate;this.outputBuffer.fill(0);for(let e=0;e<this.targetFrameSize;e++){const i=e*t,r=Math.floor(i),s=Math.min(r+1,this.inputFrameSize-1),u=i-r;this.outputBuffer[e]=this.inputBuffer[r]*(1-u)+this.inputBuffer[s]*u}await this.port.postMessage(this.outputBuffer)}pushAudio(t){const e=t.length,i=this.inputFrameSize-this.inputBufferSize;if(e<i)return this.inputBuffer.set(t,this.inputBufferSize),void(this.inputBufferSize+=e);this.inputBuffer.set(t.subarray(0,i),this.inputBufferSize),this.flush(),this.inputBufferSize=0,this.pushAudio(t.subarray(i))}process(t,e,i){return this.pushAudio(t[0][0]),!0}}registerProcessor("${workletName}",t)})();`], { type: "application/javascript" });
78
+ var workletUrl = URL.createObjectURL(workletBlob);
79
+ var AudioBatcher = class {
80
+ /**
81
+ * @param {number} batchSeconds - The number of seconds to batch.
82
+ * @param {number} batchIntervalSeconds - The number of seconds to wait before calling the callback.
83
+ * @param {number} targetSampleRate - The target sample rate of the worklet.
84
+ */
85
+ constructor(batchSeconds = 2, batchIntervalSeconds = 0.05, targetSampleRate = 16e3) {
86
+ this.initialized = false;
87
+ this.initError = null;
88
+ this.callbacks = [];
89
+ this.batchSeconds = batchSeconds;
90
+ this.batchIntervalSeconds = batchIntervalSeconds;
91
+ this.batchIntervalCount = 0;
92
+ this.targetSampleRate = targetSampleRate;
93
+ this.buffer = new Float32Array(this.batchSamples);
94
+ this.buffer.fill(0);
95
+ this.initialize().catch((e) => {
96
+ this.initError = e;
97
+ console.error("[AudioBatcher] initialization failed", e);
98
+ });
99
+ }
100
+ /**
101
+ * The number of samples in a batch.
102
+ * @type {number}
103
+ */
104
+ get batchSamples() {
105
+ return Math.floor(this.batchSeconds * this.targetSampleRate);
106
+ }
107
+ /**
108
+ * The number of samples in a batch interval.
109
+ * @type {number}
110
+ */
111
+ get batchIntervalSamples() {
112
+ return Math.floor(this.batchIntervalSeconds * this.targetSampleRate);
113
+ }
114
+ /**
115
+ * Clears the buffer.
116
+ */
117
+ clearBuffer() {
118
+ this.buffer.fill(0);
119
+ }
120
+ /**
121
+ * Pushes new audio samples into the buffer.
122
+ * @param {Float32Array} data - The new audio samples.
123
+ */
124
+ push(data) {
125
+ const dataLength = data.length;
126
+ if (dataLength >= this.buffer.length) {
127
+ this.buffer.set(data.subarray(dataLength - this.buffer.length));
128
+ } else {
129
+ this.buffer.set(this.buffer.subarray(dataLength));
130
+ this.buffer.set(data, this.buffer.length - dataLength);
131
+ }
132
+ this.batchIntervalCount += dataLength;
133
+ if (this.batchIntervalCount >= this.batchIntervalSamples) {
134
+ this.callbacks.forEach((callback) => callback(this.buffer));
135
+ this.batchIntervalCount = 0;
136
+ }
137
+ }
138
+ /**
139
+ * Adds a callback to be called with each batch.
140
+ * @param {Function} callback - The callback to add.
141
+ */
142
+ onBatch(callback) {
143
+ this.callbacks.push(callback);
144
+ }
145
+ /**
146
+ * Removes a callback from the list of callbacks.
147
+ * @param {Function} callback - The callback to remove.
148
+ */
149
+ offBatch(callback) {
150
+ this.callbacks = this.callbacks.filter((c) => c !== callback);
151
+ }
152
+ /**
153
+ * Initializes the audio batcher.
154
+ */
155
+ async initialize() {
156
+ if (this.initialized) {
157
+ return;
158
+ }
159
+ this.stream = await navigator.mediaDevices.getUserMedia({
160
+ audio: {
161
+ channelCount: 1,
162
+ echoCancellation: true,
163
+ autoGainControl: true,
164
+ noiseSuppression: true
165
+ }
166
+ });
167
+ this.audioContext = new AudioContext();
168
+ this.audioContext.resume().catch(() => {
169
+ });
170
+ this.sourceNode = new MediaStreamAudioSourceNode(
171
+ this.audioContext,
172
+ { mediaStream: this.stream }
173
+ );
174
+ this.workerNode = await AudioNode.create(
175
+ this.audioContext,
176
+ this.targetSampleRate
177
+ );
178
+ this.sourceNode.connect(this.workerNode.worker);
179
+ this.workerNode.worker.port.onmessage = (event) => {
180
+ this.push(event.data);
181
+ };
182
+ this.clearBuffer();
183
+ this.initialized = true;
184
+ }
185
+ };
186
+ var AudioNode = class _AudioNode {
187
+ /**
188
+ * @param {AudioContext} context - The audio context.
189
+ * @param {AudioWorkletNode} worker - The audio worklet node.
190
+ */
191
+ constructor(context, worker) {
192
+ this.context = context;
193
+ this.worker = worker;
194
+ }
195
+ /**
196
+ * Creates an AudioNode.
197
+ * @param {AudioContext} context - The audio context.
198
+ * @param {number} targetSampleRate - The target sample rate of the worklet.
199
+ * @returns {Promise<AudioNode>} The created AudioNode.
200
+ */
201
+ static async create(context, targetSampleRate) {
202
+ await context.audioWorklet.addModule(workletUrl);
203
+ const workletOptions = {
204
+ processorOptions: {
205
+ targetSampleRate
206
+ }
207
+ };
208
+ const worker = new AudioWorkletNode(context, workletName, workletOptions);
209
+ return new _AudioNode(context, worker);
210
+ }
211
+ };
212
+
213
+ // src/components/AI/HeyOzwell/WakeWord/lib/models/base.js
214
+ var ONNXModel = class {
215
+ /**
216
+ * Constructor
217
+ * @param {string} modelPath - Path to the ONNX model
218
+ * @param {Object} options - Options
219
+ */
220
+ constructor(modelPath, power = 0, webnn = 1, webgpu = 2, webgl = 3, wasm = 4) {
221
+ this.modelPath = modelPath;
222
+ this.session = null;
223
+ this.loadError = null;
224
+ this.duration = 0;
225
+ this.ema = 0.1;
226
+ this.lastTime = 0;
227
+ this.webnn = webnn;
228
+ this.webgpu = webgpu;
229
+ this.webgl = webgl;
230
+ this.wasm = wasm;
231
+ this.power = power;
232
+ this.loadPromise = this.load().catch((e) => {
233
+ this.loadError = e;
234
+ console.warn("[ONNXModel] load failed:", e);
235
+ });
236
+ }
237
+ /**
238
+ * Get the power preference
239
+ * @returns {string} - Power preference
240
+ */
241
+ get powerPreference() {
242
+ switch (this.power) {
243
+ case -1:
244
+ return "low-power";
245
+ case 1:
246
+ return "high-performance";
247
+ default:
248
+ return "default";
249
+ }
250
+ }
251
+ /**
252
+ * Get the execution providers
253
+ * @returns {Array} - Execution providers
254
+ */
255
+ get executionProviders() {
256
+ const providerIndexes = [];
257
+ if (Number.isInteger(this.webnn)) {
258
+ providerIndexes.push([{
259
+ name: "webnn",
260
+ device: "gpu",
261
+ powerPreference: this.powerPreference
262
+ }, this.webnn]);
263
+ }
264
+ if (Number.isInteger(this.webgpu)) {
265
+ providerIndexes.push(["webgpu", this.webgpu]);
266
+ }
267
+ if (Number.isInteger(this.webgl)) {
268
+ providerIndexes.push(["webgl", this.webgl]);
269
+ }
270
+ if (Number.isInteger(this.wasm)) {
271
+ providerIndexes.push(["wasm", this.wasm]);
272
+ }
273
+ providerIndexes.sort((a, b) => a[1] - b[1]);
274
+ return providerIndexes.map((providerIndex) => providerIndex[0]);
275
+ }
276
+ /**
277
+ * Get the session options
278
+ * @returns {Object} - Session options
279
+ * @see https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html#session-options
280
+ */
281
+ get sessionOptions() {
282
+ return {
283
+ executionProviders: ["wasm"]
284
+ };
285
+ }
286
+ /**
287
+ * Initialize the model
288
+ */
289
+ async load() {
290
+ const bytes = await chunkUVSODK6V_cjs.getModelBytes(this.modelPath);
291
+ this.session = await ONNX.createInferenceSession(bytes, this.sessionOptions);
292
+ }
293
+ /**
294
+ * Waits until the model is loaded
295
+ */
296
+ async waitUntilLoaded() {
297
+ await this.loadPromise;
298
+ if (this.loadError) throw this.loadError;
299
+ }
300
+ /**
301
+ * Execute the model
302
+ * @param {Mixed} input - Input data
303
+ * @returns {Promise} - Promise that resolves with the output of the model
304
+ * @throws {Error} - If the method is not implemented
305
+ */
306
+ async execute(input) {
307
+ throw new Error("Not Implemented");
308
+ }
309
+ /**
310
+ * Run the model
311
+ * @param {Mixed} input - Input data
312
+ * @returns {Promise} - Promise that resolves with the output of the model
313
+ */
314
+ async run(input) {
315
+ await this.waitUntilLoaded();
316
+ const currentTime = (/* @__PURE__ */ new Date()).getTime();
317
+ const result = await this.execute(input);
318
+ const executionDuration = (/* @__PURE__ */ new Date()).getTime() - currentTime;
319
+ if (this.duration === 0) {
320
+ this.duration = executionDuration;
321
+ } else {
322
+ this.duration = (1 - this.ema) * this.duration + this.ema * executionDuration;
323
+ }
324
+ this.lastTime = currentTime;
325
+ return result;
326
+ }
327
+ };
328
+
329
+ // src/components/AI/HeyOzwell/WakeWord/lib/models/vad.js
330
+ var SileroVAD = class extends ONNXModel {
331
+ /**
332
+ * Constructor
333
+ * @param {string} modelPath - Path to the ONNX model
334
+ * @param {number} sampleRate - Sample rate of the input audio
335
+ * @param {number} speechVadThreshold - Threshold for speech detection (default: 0.65)
336
+ * @param {number} silenceVadThreshold - Threshold for silence detection (default: 0.4)
337
+ * @param {number} silentFramesCount - Number of silent frames to consider speech ended (default: 10)
338
+ */
339
+ constructor(modelPath = "/pretrained/silero-vad.onnx", sampleRate = 16e3, speechVadThreshold = 0.65, silenceVadThreshold = 0.4, silentFramesCount = 10, power = 0, webnn = 1, webgpu = 2, webgl = 3, wasm = 4) {
340
+ super(
341
+ modelPath,
342
+ power,
343
+ webnn,
344
+ webgpu,
345
+ webgl,
346
+ wasm
347
+ );
348
+ this.sampleRate = sampleRate || 16e3;
349
+ this.speechVadThreshold = speechVadThreshold;
350
+ this.silenceVadThreshold = silenceVadThreshold;
351
+ this.silentFramesCount = silentFramesCount;
352
+ this.silentFrames = 0;
353
+ this.isSpeaking = false;
354
+ }
355
+ /**
356
+ * Test the model
357
+ * @param {boolean} debug - If true, log the result to the console
358
+ * @throws {Error} - If the model fails the test
359
+ */
360
+ async test(debug = false) {
361
+ let result = await this.run(new Float32Array(16e3).fill(0));
362
+ if (!isNaN(result) && 0 <= result && result <= 1) {
363
+ if (debug) {
364
+ console.log(`VAD model OK, executed in ${this.duration} ms`);
365
+ }
366
+ } else {
367
+ throw new Error(`VAD model failed - got ${result}`);
368
+ }
369
+ }
370
+ /**
371
+ * Execute the model
372
+ * @param {Float32Array} input - Input data
373
+ * @returns {Promise} - Promise that resolves with the output of the model, which is a single float
374
+ * @throws {Error} - If the input data is not a Float32Array
375
+ */
376
+ async execute(input) {
377
+ if (this.h === void 0 || this.c === void 0 || this.sr === void 0) {
378
+ this.sr = await ONNX.createTensor("int64", [this.sampleRate], [1]);
379
+ this.h = await ONNX.createTensor("float32", new Array(128).fill(0), [2, 1, 64]);
380
+ this.c = await ONNX.createTensor("float32", new Array(128).fill(0), [2, 1, 64]);
381
+ }
382
+ const inputTensor = await ONNX.createTensor("float32", input, [1, input.length]);
383
+ const output = await this.session.run({
384
+ input: inputTensor,
385
+ h: this.h,
386
+ c: this.c,
387
+ sr: this.sr
388
+ });
389
+ this.c = output.cn;
390
+ this.h = output.hn;
391
+ return output.output.data[0];
392
+ }
393
+ /**
394
+ * Determines if speech is present in the audio, with debouncing logic so a brief pause between
395
+ * words doesn't split one utterance into two: if speech is followed by a short silence and then
396
+ * more speech, the whole span (including the gap) is still treated as speech.
397
+ *
398
+ * @param {Float32Array} audio - Audio data to check for speech
399
+ * @returns {Promise<Object>} - Promise that resolves with an object containing:
400
+ * - isSpeaking: boolean - true if speech is detected, false otherwise
401
+ * - probability: number - the raw VAD probability score (0-1)
402
+ */
403
+ async hasSpeechAudio(audio) {
404
+ const speechProbability = await this.run(audio);
405
+ const hasSpeech = speechProbability > this.speechVadThreshold;
406
+ const hasSilence = speechProbability < this.silenceVadThreshold;
407
+ let justStoppedSpeaking = false;
408
+ let justStartedSpeaking = false;
409
+ if (!hasSpeech) {
410
+ if (hasSilence) {
411
+ this.silentFrames += 1;
412
+ if (this.isSpeaking && this.silentFrames > this.silentFramesCount) {
413
+ this.isSpeaking = false;
414
+ justStoppedSpeaking = true;
415
+ }
416
+ }
417
+ } else {
418
+ this.silentFrames = 0;
419
+ if (!this.isSpeaking) {
420
+ this.isSpeaking = true;
421
+ justStartedSpeaking = true;
422
+ }
423
+ }
424
+ return {
425
+ isSpeaking: this.isSpeaking,
426
+ speechProbability,
427
+ justStoppedSpeaking,
428
+ justStartedSpeaking
429
+ };
430
+ }
431
+ };
432
+
433
+ // src/components/AI/HeyOzwell/WakeWord/lib/models/mel-spectrogram.js
434
+ var MelSpectrogram = class extends ONNXModel {
435
+ /**
436
+ * Constructor
437
+ * @param {string} modelPath - Path to the ONNX model
438
+ */
439
+ constructor(modelPath = "/pretrained/mel-spectrogram.onnx", power = 0, webnn = 1, webgpu = 2, webgl = 3, wasm = 4) {
440
+ super(
441
+ modelPath,
442
+ power,
443
+ webnn,
444
+ webgpu,
445
+ webgl,
446
+ wasm
447
+ );
448
+ }
449
+ /**
450
+ * Test the model
451
+ * @param {boolean} debug - If true, print debug information
452
+ * @throws {Error} - If the model fails the test
453
+ */
454
+ async test(debug = false) {
455
+ let result = await this.run(new Float32Array(12640).fill(1));
456
+ if (result.dims.length === 4 && result.dims[2] === 76 && result.dims[3] === 32) {
457
+ if (debug) {
458
+ console.log(`Mel spectrogram model OK, executed in ${this.duration} ms`);
459
+ }
460
+ } else {
461
+ throw new Error("Mel spectrogram model failed");
462
+ }
463
+ }
464
+ /**
465
+ * Execute the model
466
+ * @param {Float32Array} input - Input data
467
+ * @returns {Promise} - Promise that resolves with the output of the model, which is a 2D array
468
+ * @throws {Error} - If the input data is not a Float32Array
469
+ */
470
+ async execute(input) {
471
+ let peak = 0;
472
+ for (let i = 0; i < input.length; i++) {
473
+ const a = Math.abs(input[i]);
474
+ if (a > peak) peak = a;
475
+ }
476
+ if (peak > 1e-5) {
477
+ const normed = new Float32Array(input.length);
478
+ for (let i = 0; i < input.length; i++) normed[i] = input[i] / peak;
479
+ input = normed;
480
+ }
481
+ const inputTensor = await ONNX.createTensor(
482
+ "float32",
483
+ input,
484
+ [1, input.length]
485
+ );
486
+ const output = await this.session.run({ input: inputTensor });
487
+ const data = output.output.data;
488
+ for (let i = 0; i < data.length; i++) data[i] = data[i] / 10 + 2;
489
+ return await ONNX.createTensor("float32", data, output.output.dims);
490
+ }
491
+ };
492
+
493
+ // src/components/AI/HeyOzwell/WakeWord/lib/models/speech-embedding.js
494
+ var SpeechEmbedding = class extends ONNXModel {
495
+ /**
496
+ * Constructor
497
+ * @param {string} modelPath - Path to the ONNX model
498
+ * @param {MelSpectrogram} spectrogramModel - Mel spectrogram model
499
+ * @param {number} spectrogramMelBins - Number of Mel bins for the Mel spectrogram model
500
+ * @param {number} embeddingDim - Dimension of the embeddings
501
+ * @param {number} windowSize - Size of the window
502
+ * @param {number} windowStride - Stride of the window
503
+ */
504
+ constructor(modelPath, embeddingDim = 96, windowSize = 76, windowStride = 8, power = 0, webnn = 1, webgpu = 2, webgl = 3, wasm = 4) {
505
+ super(
506
+ modelPath,
507
+ power,
508
+ webnn,
509
+ webgpu,
510
+ webgl,
511
+ wasm
512
+ );
513
+ this.embeddingDim = embeddingDim;
514
+ this.windowSize = windowSize;
515
+ this.windowStride = windowStride;
516
+ }
517
+ /**
518
+ * Test the model
519
+ * @param {boolean} debug - Debug mode
520
+ * @throws {Error} - If the model fails the test
521
+ */
522
+ async test(debug = false) {
523
+ const melTensor = await ONNX.createTensor(
524
+ "float32",
525
+ new Float32Array(100 * 32),
526
+ // already zero-initialized
527
+ [100, 32]
528
+ );
529
+ let result = await this.run(melTensor);
530
+ if (result.dims.length === 2 && result.dims[0] === 4 && result.dims[1] === 96) {
531
+ if (debug) {
532
+ console.log(`Speech embedding model OK, executed in ${this.duration} ms`);
533
+ }
534
+ } else {
535
+ console.error("Unexpected speech embedding result", result);
536
+ throw new Error("Speech embedding model failed");
537
+ }
538
+ }
539
+ /**
540
+ * Extracts speech embeddings from a mel spectrogram output
541
+ *
542
+ * This function takes the output from a mel spectrogram model, creates an ONNX tensor
543
+ * with the appropriate dimensions, and runs it through the speech embedding model to
544
+ * generate embeddings that can be used for wake word detection.
545
+ *
546
+ * @param {Object} melSpectogramOutput - The output tensor from a mel spectrogram model
547
+ * @param {Float32Array} melSpectogramOutput.data - The raw data from the mel spectrogram
548
+ * @param {Array<number>} melSpectogramOutput.dims - The dimensions of the mel spectrogram output
549
+ * @returns {Promise<Object>} - A promise that resolves to an ONNX tensor containing the speech embeddings
550
+ */
551
+ async getEmbeddingFromMelSpectrogramOutput(melSpectogramOutput) {
552
+ const spectogramBuffer = await ONNX.createTensor(
553
+ "float32",
554
+ melSpectogramOutput.data,
555
+ melSpectogramOutput.dims.slice(2)
556
+ );
557
+ return this.run(spectogramBuffer);
558
+ }
559
+ /**
560
+ * Execute the model
561
+ * @param {Float32Array} input - Input data
562
+ * @returns {Promise} - Promise that resolves with the output of the model, which is a 2D array
563
+ * @throws {Error} - If the input data is not a Float32Array
564
+ */
565
+ async execute(spectrograms) {
566
+ const [numFrames, melBins] = spectrograms.dims;
567
+ if (numFrames < this.windowSize) {
568
+ throw new Error(`Audio is too short to process - require ${this.windowSize} samples, got ${numFrames}`);
569
+ }
570
+ const numTruncatedFrames = numFrames - (numFrames - this.windowSize) % this.windowStride;
571
+ const numBatches = (numTruncatedFrames - this.windowSize) / this.windowStride + 1;
572
+ const embeddings = await ONNX.createTensor(
573
+ "float32",
574
+ new Array(numBatches * this.embeddingDim).fill(0),
575
+ [numBatches, this.embeddingDim]
576
+ );
577
+ const windowBatches = [];
578
+ for (let windowStart = 0; windowStart < numTruncatedFrames - this.windowSize + this.windowStride; windowStart += this.windowStride) {
579
+ const windowEnd = windowStart + this.windowSize;
580
+ const windowTensor = await ONNX.createTensor(
581
+ "float32",
582
+ spectrograms.data.slice(windowStart * melBins, windowEnd * melBins),
583
+ [this.windowSize, melBins, 1]
584
+ );
585
+ windowBatches.push([windowStart, windowEnd, windowTensor]);
586
+ }
587
+ const stackedWindowTensor = await ONNX.createTensor(
588
+ "float32",
589
+ new Float32Array(numBatches * this.windowSize * melBins),
590
+ [numBatches, this.windowSize, melBins, 1]
591
+ );
592
+ for (let i = 0; i < numBatches; i++) {
593
+ stackedWindowTensor.data.set(windowBatches[i][2].data, i * this.windowSize * melBins);
594
+ }
595
+ const output = await this.session.run({ input_1: stackedWindowTensor });
596
+ for (let i = 0; i < numBatches; i++) {
597
+ embeddings.data.set(
598
+ output.conv2d_19.data.slice(
599
+ i * this.embeddingDim,
600
+ (i + 1) * this.embeddingDim
601
+ ),
602
+ i * this.embeddingDim
603
+ );
604
+ }
605
+ return embeddings;
606
+ }
607
+ };
608
+
609
+ // src/components/AI/HeyOzwell/WakeWord/lib/models/wake-word.js
610
+ var WakeWord = class extends ONNXModel {
611
+ /**
612
+ * Constructor
613
+ * @param {string} modelPath - Path to the ONNX model
614
+ * @param {number} threshold - Threshold for wake word detection (default: 0.5)
615
+ */
616
+ constructor(modelPath, threshold, power = 0, webnn = 1, webgpu = 2, webgl = 3, wasm = 4) {
617
+ super(modelPath, power, webnn, webgpu, webgl, wasm);
618
+ this.threshold = threshold;
619
+ }
620
+ /**
621
+ * Test the model
622
+ * @param {boolean} debug - Whether to log debug messages
623
+ * @throws {Error} - If the model test fails
624
+ */
625
+ async test(debug = false) {
626
+ const embeddings = await ONNX.createTensor(
627
+ "float32",
628
+ new Float32Array(16 * 96).fill(0),
629
+ [1, 16, 96]
630
+ );
631
+ const output = await this.run(embeddings);
632
+ if (0 <= output && output <= 1) {
633
+ if (debug) {
634
+ console.log(`Wake Word model OK, executed in ${this.duration} ms`);
635
+ }
636
+ } else {
637
+ throw new Error(`Wake Word model test failed - expected 0 <= x <= 1, got ${output}`);
638
+ }
639
+ }
640
+ /**
641
+ * Execute the model
642
+ * @param {Float32Array} embeddings - Input embeddings
643
+ * @returns {Promise} - Promise that resolves with the output of the model, which is a single float
644
+ * @throws {Error} - If the input data is not a Float32Array
645
+ */
646
+ async execute(embeddings) {
647
+ const input = {};
648
+ if (embeddings.dims.length === 3) {
649
+ input.input = embeddings;
650
+ } else {
651
+ input.input = await ONNX.createTensor(
652
+ "float32",
653
+ embeddings.data,
654
+ [1, embeddings.dims[0], embeddings.dims[1]]
655
+ );
656
+ }
657
+ const output = await this.session.run(input);
658
+ return output.output.data[0] * 1;
659
+ }
660
+ /**
661
+ * Check if the wake word is detected based on the threshold
662
+ * @param {Float32Array} embeddings - Input embeddings
663
+ * @returns {Promise<Object>} - Promise that resolves with an object containing probability and detected status
664
+ */
665
+ async checkWakeWordCalled(embeddings) {
666
+ const probability = await this.run(embeddings);
667
+ let thr = this.threshold;
668
+ const ov = typeof window !== "undefined" ? window.__baseThr : void 0;
669
+ if (typeof ov === "number") thr = ov;
670
+ else if (ov && this.name && typeof ov[this.name] === "number") thr = ov[this.name];
671
+ return {
672
+ probability,
673
+ detected: probability >= thr
674
+ };
675
+ }
676
+ /**
677
+ * Run wake word detection on audio.
678
+ * @param {Float32Array} embeddings - Input embeddings
679
+ * @returns {Promise} - Promise that resolves when wake word detection is complete.
680
+ */
681
+ async checkWakeWordPresent(embeddings) {
682
+ return await this.execute(embeddings);
683
+ }
684
+ };
685
+
686
+ // src/components/AI/HeyOzwell/WakeWord/lib/hey-buddy.js
687
+ async function embeddingBufferArrayToEmbedding(embeddingBufferArray, numFramesPerEmbedding, embeddingDim) {
688
+ const combinedEmptyData = new Float32Array(numFramesPerEmbedding * embeddingBufferArray.length * embeddingDim);
689
+ const embeddingBuffer = await ONNX.createTensor(
690
+ "float32",
691
+ combinedEmptyData,
692
+ [numFramesPerEmbedding * embeddingBufferArray.length, embeddingDim]
693
+ );
694
+ for (let i = 0; i < embeddingBufferArray.length; i++) {
695
+ const embedding = embeddingBufferArray[i];
696
+ embeddingBuffer.data.set(embedding.data, i * numFramesPerEmbedding * embeddingDim);
697
+ }
698
+ return embeddingBuffer;
699
+ }
700
+ var HeyBuddy = class {
701
+ /**
702
+ * Create a HeyBuddy instance.
703
+ * @param {Object} [options] - Options object.
704
+ * @param {number} [options.positiveVadThreshold=0.5] - VAD threshold for speech.
705
+ * @param {number} [options.negativeVadThreshold=0.25] - VAD threshold for silence.
706
+ * @param {number} [options.negativeVadCount=8] - Number of negative VADs to trigger silence.
707
+ * @param {number} [options.wakeWordThreads=4] - Number of threads for wake word detection.
708
+ * @param {number} [options.wakeWordThreshold=0.5] - Wake word detection threshold.
709
+ * @param {string|string[]} [options.modelPath="/models/hey-buddy.onnx"] - Path to wake word model.
710
+ * @param {string} [options.vadModelPath="/pretrained/silero-vad.onnx"] - Path to VAD model.
711
+ * @param {string} [options.embeddingModelPath="/pretrained/speech-embedding.onnx"] - Path to speech embedding model.
712
+ * @param {string} [options.spectrogramModelPath="/pretrained/mel-spectrogram.onnx"] - Path to mel spectrogram model.
713
+ * @param {number} [options.batchSeconds=1.08] - Number of seconds per batch.
714
+ * @param {number} [options.batchIntervalSeconds=0.12] - Number of seconds between batches.
715
+ * @param {number} [options.targetSampleRate=16000] - Target sample rate for audio.
716
+ * @param {number} [options.spectrogramMelBins=32] - Number of mel bins for spectrogram.
717
+ * @param {number} [options.embeddingDim=96] - Dimension of speech embedding.
718
+ * @param {number} [options.embeddingWindowSize=76] - Window size for speech embedding.
719
+ * @param {number} [options.embeddingWindowStride=8] - Window stride for speech embedding.
720
+ */
721
+ constructor(options) {
722
+ options = options || {};
723
+ this.debug = options.debug || false;
724
+ options.positiveVadThreshold = options.positiveVadThreshold || 0.65;
725
+ options.negativeVadThreshold = options.negativeVadThreshold || 0.4;
726
+ options.negativeVadCount = options.negativeVadCount || 8;
727
+ this.wakeWordThreads = options.wakeWordThreads || 4;
728
+ this.wakeWordThreshold = options.wakeWordThreshold || 0.5;
729
+ this.wakeWordThresholds = options.wakeWordThresholds || {};
730
+ this.wakeWordInterval = options.wakeWordInterval || 2;
731
+ const modelPath = options.modelPath || "/models/hey-buddy.onnx";
732
+ const modelArray = Array.isArray(modelPath) ? modelPath : [modelPath];
733
+ const vadModelPath = options.vadModelPath || "/pretrained/silero-vad.onnx";
734
+ const embeddingModelPath = options.embeddingModelPath || "/pretrained/speech-embedding.onnx";
735
+ const spectrogramModelPath = options.spectrogramModelPath || "/pretrained/mel-spectrogram.onnx";
736
+ const batchSeconds = options.batchSeconds || 1.08;
737
+ const batchIntervalSeconds = options.batchIntervalSeconds || 0.12;
738
+ const targetSampleRate = options.targetSampleRate || 16e3;
739
+ const spectrogramMelBins = options.spectrogramMelBins || 32;
740
+ const embeddingDim = options.embeddingDim || 96;
741
+ const embeddingWindowSize = options.embeddingWindowSize || 76;
742
+ const embeddingWindowStride = options.embeddingWindowStride || 8;
743
+ const wakeWordEmbeddingFrames = options.wakeWordEmbeddingFrames || 16;
744
+ this.vad = new SileroVAD(vadModelPath, targetSampleRate, options.positiveVadThreshold, options.negativeVadThreshold, options.negativeVadCount);
745
+ this.vad.test(this.debug).catch((e) => console.warn("[HeyBuddy] vad.test failed", e));
746
+ this.spectrogram = new MelSpectrogram(spectrogramModelPath);
747
+ this.spectrogram.test(this.debug).catch((e) => console.warn("[HeyBuddy] spectrogram.test failed", e));
748
+ this.spectrogramMelBins = spectrogramMelBins;
749
+ this.embedding = new SpeechEmbedding(
750
+ embeddingModelPath,
751
+ embeddingDim,
752
+ embeddingWindowSize,
753
+ embeddingWindowStride
754
+ );
755
+ this.embedding.test(this.debug).catch((e) => console.warn("[HeyBuddy] embedding.test failed", e));
756
+ this.embeddingDim = embeddingDim;
757
+ this.embeddingWindowSize = embeddingWindowSize;
758
+ this.embeddingWindowStride = embeddingWindowStride;
759
+ this.embeddingBuffer = null;
760
+ this.embeddingBufferArray = [];
761
+ this.voiceprints = {};
762
+ this.voiceprintThreshold = options.voiceprintThreshold ?? 0.85;
763
+ this.voiceprintThresholds = options.voiceprintThresholds || {};
764
+ this.voiceprintGate = options.voiceprintGate ?? 0.3;
765
+ this.embeddingMean = null;
766
+ this.voiceprintRecall = options.voiceprintRecall ?? true;
767
+ this.debounceFrames = options.debounceFrames ?? 1;
768
+ this._consec = {};
769
+ this.lastWakeProb = 0;
770
+ this._peakProb = {};
771
+ this._peakEmb = {};
772
+ this.lastWakeEmbedding = null;
773
+ this.wakeWords = {};
774
+ this.wakeWordTimes = {};
775
+ this.wakeWordEmbeddingFrames = wakeWordEmbeddingFrames;
776
+ for (let model of modelArray) {
777
+ let modelName = model.split("/").pop().split(".")[0];
778
+ let modelThreshold = this.wakeWordThresholds[modelName] ?? this.wakeWordThreshold;
779
+ this.wakeWords[modelName] = new WakeWord(model, modelThreshold);
780
+ this.wakeWords[modelName].name = modelName;
781
+ this.wakeWords[modelName].test(this.debug).catch((e) => console.warn(`[HeyBuddy] ${modelName}.test failed`, e));
782
+ }
783
+ this.recording = false;
784
+ this.audioBuffer = null;
785
+ this.frameIntervalEma = 0;
786
+ this.frameIntervalEmaWeight = 0.1;
787
+ this.frameTimeEma = 0;
788
+ this.frameTimeEmaWeight = 0.1;
789
+ this.speechStartCallbacks = [];
790
+ this.speechEndCallbacks = [];
791
+ this.recordingCallbacks = [];
792
+ this.processedCallbacks = [];
793
+ this.detectedCallbacks = [];
794
+ this.batcher = new AudioBatcher(
795
+ batchSeconds,
796
+ batchIntervalSeconds,
797
+ targetSampleRate
798
+ );
799
+ this.batcher.onBatch((batch) => this.process(batch));
800
+ }
801
+ /**
802
+ * Set a user's enrolled voiceprint for a wake word.
803
+ * @param {string} name - Wake-word name (e.g. "hey-ozwell").
804
+ * @param {Float32Array[]} vectors - Flattened embedding windows captured at enrollment.
805
+ */
806
+ setVoiceprint(name, vectors) {
807
+ this.voiceprints[name] = vectors || [];
808
+ }
809
+ clearVoiceprint(name) {
810
+ delete this.voiceprints[name];
811
+ }
812
+ hasVoiceprint(name) {
813
+ return Array.isArray(this.voiceprints[name]) && this.voiceprints[name].length > 0;
814
+ }
815
+ /**
816
+ * Max cosine similarity between a live embedding window and the stored voiceprint set.
817
+ * Cosine compares DIRECTION not magnitude, so it ignores loudness and keys on what the
818
+ * sound actually is. Returns 0..1 (1 = near-identical).
819
+ */
820
+ voiceprintSimilarity(name, liveVec) {
821
+ const set = this.voiceprints[name];
822
+ if (!Array.isArray(set) || set.length === 0 || !liveVec) return 0;
823
+ const mean = this.embeddingMean;
824
+ let best = -1;
825
+ for (const ref of set) {
826
+ let dot = 0, na = 0, nb = 0;
827
+ const n = Math.min(ref.length, liveVec.length);
828
+ for (let i = 0; i < n; i++) {
829
+ const a = mean ? ref[i] - mean[i] : ref[i];
830
+ const b = mean ? liveVec[i] - mean[i] : liveVec[i];
831
+ dot += a * b;
832
+ na += a * a;
833
+ nb += b * b;
834
+ }
835
+ if (na > 0 && nb > 0) {
836
+ const s = dot / (Math.sqrt(na) * Math.sqrt(nb));
837
+ if (s > best) best = s;
838
+ }
839
+ }
840
+ return best;
841
+ }
842
+ /**
843
+ * Gets the names of wake words, chunked for threaded wake word detection.
844
+ * @returns {string[][]} - Names of wake words.
845
+ */
846
+ get chunkedWakeWords() {
847
+ return Object.keys(this.wakeWords).reduce((carry, name, i) => {
848
+ const chunkIndex = Math.floor(i / this.wakeWordThreads);
849
+ if (!carry[chunkIndex]) {
850
+ carry[chunkIndex] = [];
851
+ }
852
+ carry[chunkIndex].push(name);
853
+ return carry;
854
+ }, []);
855
+ }
856
+ /**
857
+ * Add a callback for when a wake word is detected.
858
+ * @param {string|string[]} names - Name of wake word.
859
+ * @param {Function} callback - Callback function.
860
+ */
861
+ onDetected(names, callback) {
862
+ this.detectedCallbacks.push({ names, callback });
863
+ }
864
+ /**
865
+ * Add a callback for processed data.
866
+ * @param {Function} callback - Callback function.
867
+ */
868
+ onProcessed(callback) {
869
+ this.processedCallbacks.push(callback);
870
+ }
871
+ /**
872
+ * Add a callback for speech start.
873
+ * @param {Function} callback - Callback function.
874
+ */
875
+ onSpeechStart(callback) {
876
+ this.speechStartCallbacks.push(callback);
877
+ }
878
+ /**
879
+ * Add a callback for speech end.
880
+ * @param {Function} callback - Callback function.
881
+ */
882
+ onSpeechEnd(callback) {
883
+ this.speechEndCallbacks.push(callback);
884
+ }
885
+ /**
886
+ * Add a callback for recording.
887
+ * @param {Function} callback - Callback function.
888
+ */
889
+ onRecording(callback) {
890
+ this.recordingCallbacks.push(callback);
891
+ }
892
+ /**
893
+ * Trigger speech start event.
894
+ */
895
+ speechStart() {
896
+ if (this.debug) {
897
+ console.log("Speech start");
898
+ }
899
+ for (let callback of this.speechStartCallbacks) {
900
+ callback();
901
+ }
902
+ }
903
+ /**
904
+ * Trigger speech end event.
905
+ */
906
+ speechEnd() {
907
+ if (this.debug) {
908
+ console.log("Speech end");
909
+ }
910
+ for (let callback of this.speechEndCallbacks) {
911
+ callback();
912
+ }
913
+ if (this.recording) {
914
+ this.dispatchRecording();
915
+ this.recording = false;
916
+ }
917
+ }
918
+ /**
919
+ * Dispatch recording to all recording callbacks.
920
+ */
921
+ dispatchRecording() {
922
+ if (this.audioBuffer === null) {
923
+ console.error("No recording to dispatch");
924
+ return;
925
+ }
926
+ if (this.debug) {
927
+ const recordingLength = this.audioBuffer.length;
928
+ const recordedDuration = recordingLength / this.batcher.targetSampleRate;
929
+ console.log(`Dispatching recording with ${recordingLength} frames (${recordedDuration} s)`);
930
+ }
931
+ for (let callback of this.recordingCallbacks) {
932
+ callback(this.audioBuffer);
933
+ }
934
+ this.audioBuffer = null;
935
+ }
936
+ /**
937
+ * Trigger wake word detection event.
938
+ * @param {string} name - Name of wake word.
939
+ */
940
+ wakeWordDetected(name) {
941
+ const now = Date.now();
942
+ if (this.wakeWordTimes[name] && now - this.wakeWordTimes[name] < this.wakeWordInterval * 1e3) {
943
+ return;
944
+ }
945
+ if (this.debug) {
946
+ console.log("Wake word detected:", name);
947
+ }
948
+ this.recording = true;
949
+ this.wakeWordTimes[name] = now;
950
+ const frameSec = this.batcher && this.batcher.batchIntervalSamples && this.batcher.targetSampleRate ? this.batcher.batchIntervalSamples / this.batcher.targetSampleRate : 0.12;
951
+ this.lastWakeDurationSec = (this._consec[name] || 0) * frameSec;
952
+ this.lastWakeEmbedding = this._peakEmb && this._peakEmb[name] ? this._peakEmb[name] : this.embeddingBuffer && this.embeddingBuffer.data ? Float32Array.from(this.embeddingBuffer.data) : null;
953
+ this.lastWakeProb = this._peakProb && this._peakProb[name] ? this._peakProb[name] : 0;
954
+ for (let { names, callback } of this.detectedCallbacks) {
955
+ if (Array.isArray(names) && names.includes(name) || names === name) {
956
+ callback();
957
+ }
958
+ }
959
+ }
960
+ /**
961
+ * Trigger processed event.
962
+ * @param {Object} data - Processed data.
963
+ */
964
+ processed(data) {
965
+ for (let callback of this.processedCallbacks) {
966
+ callback(data);
967
+ }
968
+ }
969
+ /**
970
+ * Runs wake word detection on a subset of wake words.
971
+ * @param {string[]} wakeWordNames - Names of wake words to check.
972
+ * @returns {Promise} - Promise that resolves when wake word detection is complete.
973
+ */
974
+ async checkWakeWordSubset(wakeWordNames) {
975
+ return await Promise.all(
976
+ wakeWordNames.map((name) => this.wakeWords[name].checkWakeWordCalled(this.embeddingBuffer))
977
+ );
978
+ }
979
+ /**
980
+ * Run wake word detection on audio.
981
+ * @returns {Promise} - Promise that resolves when wake word detection is complete.
982
+ */
983
+ async checkWakeWords() {
984
+ const returnMap = {};
985
+ for (let nameChunk of this.chunkedWakeWords) {
986
+ const wakeWordsCalled = await this.checkWakeWordSubset(nameChunk);
987
+ for (let i = 0; i < nameChunk.length; i++) {
988
+ const name = nameChunk[i];
989
+ const wordCalled = wakeWordsCalled[i];
990
+ returnMap[name] = wordCalled;
991
+ }
992
+ }
993
+ const liveVec = this.embeddingBuffer ? this.embeddingBuffer.data : null;
994
+ for (let name in returnMap) {
995
+ returnMap[name].voiceprintSim = this.hasVoiceprint(name) ? this.voiceprintSimilarity(name, liveVec) : 0;
996
+ }
997
+ for (let name in returnMap) {
998
+ if (returnMap[name].detected) {
999
+ this._consec[name] = (this._consec[name] || 0) + 1;
1000
+ if (returnMap[name].probability > (this._peakProb[name] || 0) && this.embeddingBuffer && this.embeddingBuffer.data) {
1001
+ this._peakProb[name] = returnMap[name].probability;
1002
+ this._peakEmb[name] = Float32Array.from(this.embeddingBuffer.data);
1003
+ }
1004
+ } else {
1005
+ this._consec[name] = 0;
1006
+ this._peakProb[name] = 0;
1007
+ this._peakEmb[name] = null;
1008
+ }
1009
+ }
1010
+ const minRunFor = (name) => {
1011
+ if (typeof window.__debounceFrames === "number") return window.__debounceFrames;
1012
+ if (this.debounceFrames && typeof this.debounceFrames === "object") return this.debounceFrames[name] ?? 1;
1013
+ return this.debounceFrames || 1;
1014
+ };
1015
+ let best = null;
1016
+ for (let name in returnMap) {
1017
+ if (returnMap[name].detected && this._consec[name] >= minRunFor(name)) {
1018
+ const prob = returnMap[name].probability;
1019
+ if (best === null || prob > best.prob) {
1020
+ best = { name, prob };
1021
+ }
1022
+ }
1023
+ }
1024
+ if (best !== null) {
1025
+ this.wakeWordDetected(best.name);
1026
+ } else if (this.voiceprintRecall) {
1027
+ let vbest = null;
1028
+ for (let name in returnMap) {
1029
+ const sim = returnMap[name].voiceprintSim;
1030
+ const vt = this.voiceprintThresholds[name] ?? this.voiceprintThreshold;
1031
+ const modelLit = returnMap[name].probability >= this.voiceprintGate;
1032
+ if (sim >= vt && modelLit && (vbest === null || sim > vbest.sim)) vbest = { name, sim };
1033
+ }
1034
+ if (vbest !== null) {
1035
+ this.wakeWordDetected(vbest.name);
1036
+ }
1037
+ }
1038
+ return returnMap;
1039
+ }
1040
+ /**
1041
+ * Process audio batch.
1042
+ * @param {Float32Array} audio - Audio samples.
1043
+ */
1044
+ async process(audio) {
1045
+ this.frameStart = (/* @__PURE__ */ new Date()).getTime();
1046
+ if (this.frameEnd !== void 0 && this.frameEnd !== null) {
1047
+ this.frameInterval = this.frameStart - this.frameEnd;
1048
+ } else {
1049
+ this.frameInterval = 0;
1050
+ }
1051
+ if (this.frameIntervalEma === 0) {
1052
+ this.frameIntervalEma = this.frameInterval;
1053
+ } else {
1054
+ this.frameIntervalEma = this.frameIntervalEma * (1 - this.frameIntervalEmaWeight) + this.frameInterval * this.frameIntervalEmaWeight;
1055
+ }
1056
+ const lastBatch = audio.subarray(audio.length - this.batcher.batchIntervalSamples);
1057
+ const spectrograms = await this.spectrogram.run(audio);
1058
+ const embedding = await this.embedding.getEmbeddingFromMelSpectrogramOutput(spectrograms);
1059
+ const numFramesPerEmbedding = embedding.dims[0];
1060
+ const maxEmbeddings = this.wakeWordEmbeddingFrames / numFramesPerEmbedding;
1061
+ this.embeddingBufferArray.push(embedding);
1062
+ if (this.embeddingBufferArray.length > maxEmbeddings) this.embeddingBufferArray.shift();
1063
+ this.embeddingBuffer = await embeddingBufferArrayToEmbedding(this.embeddingBufferArray, numFramesPerEmbedding, this.embeddingDim);
1064
+ const { isSpeaking, speechProbability, justStoppedSpeaking, justStartedSpeaking } = await this.vad.hasSpeechAudio(lastBatch);
1065
+ if (!isSpeaking && this.embeddingBuffer && this.embeddingBuffer.data) {
1066
+ const d = this.embeddingBuffer.data;
1067
+ if (!this.embeddingMean || this.embeddingMean.length !== d.length) this.embeddingMean = Float32Array.from(d);
1068
+ else for (let i = 0; i < d.length; i++) this.embeddingMean[i] += 0.02 * (d[i] - this.embeddingMean[i]);
1069
+ }
1070
+ if (justStartedSpeaking) this.speechStart();
1071
+ if (justStoppedSpeaking) this.speechEnd();
1072
+ if (isSpeaking && this.embeddingBuffer.dims[0] === this.wakeWordEmbeddingFrames && !this._wakeBusy) {
1073
+ this._wakeBusy = true;
1074
+ let wakeWordsCalled;
1075
+ try {
1076
+ wakeWordsCalled = await this.checkWakeWords();
1077
+ } finally {
1078
+ this._wakeBusy = false;
1079
+ }
1080
+ this.processed({
1081
+ listening: true,
1082
+ recording: this.recording,
1083
+ speech: { probability: speechProbability, active: isSpeaking },
1084
+ wakeWords: wakeWordsCalled,
1085
+ // Live [16x96] embedding window (flattened) — what enrollment captures and
1086
+ // what the voiceprint layer matches against. Present only while listening.
1087
+ embedding: this.embeddingBuffer ? this.embeddingBuffer.data : null
1088
+ });
1089
+ } else {
1090
+ this.processed({
1091
+ listening: false,
1092
+ recording: this.recording,
1093
+ speech: { probability: speechProbability, active: isSpeaking },
1094
+ wakeWords: Object.entries(this.wakeWords).reduce(
1095
+ (carry, [name, model]) => {
1096
+ carry[name] = {
1097
+ probability: 0,
1098
+ active: false
1099
+ };
1100
+ return carry;
1101
+ },
1102
+ {}
1103
+ ),
1104
+ embedding: this.embeddingBuffer ? this.embeddingBuffer.data : null
1105
+ });
1106
+ }
1107
+ if (this.recording) {
1108
+ if (this.audioBuffer === null) {
1109
+ this.audioBuffer = new Float32Array(audio.length);
1110
+ this.audioBuffer.set(audio);
1111
+ } else {
1112
+ const concatenated = new Float32Array(this.audioBuffer.length + lastBatch.length);
1113
+ concatenated.set(this.audioBuffer);
1114
+ concatenated.set(lastBatch, this.audioBuffer.length);
1115
+ this.audioBuffer = concatenated;
1116
+ }
1117
+ }
1118
+ this.frameEnd = (/* @__PURE__ */ new Date()).getTime();
1119
+ this.frameTime = this.frameEnd - this.frameStart;
1120
+ if (this.frameTimeEma === 0) {
1121
+ this.frameTimeEma = this.frameTime;
1122
+ } else {
1123
+ this.frameTimeEma = this.frameTimeEma * (1 - this.frameTimeEmaWeight) + this.frameTime * this.frameTimeEmaWeight;
1124
+ }
1125
+ }
1126
+ };
1127
+ if (typeof window !== "undefined") {
1128
+ window.HeyBuddy = HeyBuddy;
1129
+ }
1130
+
1131
+ exports.HeyBuddy = HeyBuddy;
1132
+ //# sourceMappingURL=hey-buddy-CLUVAY2X.cjs.map
1133
+ //# sourceMappingURL=hey-buddy-CLUVAY2X.cjs.map