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