openwakeword-js 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +79 -20
- package/models/hello_deepa.onnx +0 -0
- package/package.json +1 -1
- package/scripts/download_models.js +20 -17
package/index.html
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta charset="utf-8" />
|
|
6
6
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
|
7
7
|
<title>AI Wake Word Detector | OpenWakeWord JS</title>
|
|
8
|
+
|
|
8
9
|
<!-- Fonts -->
|
|
9
10
|
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
10
11
|
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
|
|
@@ -286,7 +287,7 @@
|
|
|
286
287
|
class="h-44 overflow-y-auto rounded-xl bg-[#030406] p-4 font-mono text-[10px] text-slate-600 border border-white/5 custom-scrollbar leading-relaxed">
|
|
287
288
|
<div
|
|
288
289
|
class="opacity-40 animate-pulse font-bold text-blue-900 border border-blue-900/40 p-1 inline-block rounded mb-2">
|
|
289
|
-
OPENWAKEWORD-JS V0.1.
|
|
290
|
+
OPENWAKEWORD-JS V0.1.19 READY</div>
|
|
290
291
|
</div>
|
|
291
292
|
</div>
|
|
292
293
|
</details>
|
|
@@ -305,9 +306,12 @@
|
|
|
305
306
|
model: null,
|
|
306
307
|
audioContext: null,
|
|
307
308
|
processor: null,
|
|
308
|
-
startTime: Date.now()
|
|
309
|
+
startTime: Date.now(),
|
|
310
|
+
lastActivationTime: 0,
|
|
311
|
+
cooldownSeconds: 2.0
|
|
309
312
|
};
|
|
310
313
|
|
|
314
|
+
|
|
311
315
|
const ui = {
|
|
312
316
|
btn: document.getElementById('toggleBtn'),
|
|
313
317
|
btnText: document.getElementById('btnText'),
|
|
@@ -345,12 +349,13 @@
|
|
|
345
349
|
log('Neural buffers allocating locally...', 'SYSTEM');
|
|
346
350
|
try {
|
|
347
351
|
state.model = new Model({
|
|
348
|
-
wakewordModels: ['./models/hello_deepa.onnx', './models/namaste_deepa.onnx'],
|
|
352
|
+
wakewordModels: ['./models/hello_deepa.onnx', './models/namaste_deepa.onnx', './models/hello_deepa_old.onnx'],
|
|
349
353
|
melspectrogramModelPath: './models/melspectrogram.onnx',
|
|
350
354
|
embeddingModelPath: './models/embedding_model.onnx',
|
|
351
|
-
vadModelPath: './models/silero_vad.onnx',
|
|
352
355
|
inferenceFramework: 'onnx',
|
|
353
356
|
wasmPaths: './models/'
|
|
357
|
+
|
|
358
|
+
|
|
354
359
|
});
|
|
355
360
|
await state.model.init();
|
|
356
361
|
log('Neural intelligence hub connected.', 'SYSTEM');
|
|
@@ -363,16 +368,33 @@
|
|
|
363
368
|
|
|
364
369
|
function pushDetection(name, score) {
|
|
365
370
|
if (ui.empty) ui.empty.style.display = 'none';
|
|
371
|
+
const isHello = name.includes('hello_deepa');
|
|
372
|
+
const isNamaste = name.includes('namaste');
|
|
373
|
+
|
|
374
|
+
let displayText = name.replace(/_/g, ' ');
|
|
375
|
+
let langLabel = "Neural Activation Triggered";
|
|
376
|
+
let langColor = "text-slate-500";
|
|
377
|
+
|
|
378
|
+
if (isHello) {
|
|
379
|
+
displayText = "Hello Deepa Activated..";
|
|
380
|
+
langLabel = "Language: English";
|
|
381
|
+
langColor = "text-blue-400";
|
|
382
|
+
} else if (isNamaste) {
|
|
383
|
+
displayText = "Namaste Deepa Activated..";
|
|
384
|
+
langLabel = "Language: Nepali";
|
|
385
|
+
langColor = "text-red-400";
|
|
386
|
+
}
|
|
387
|
+
|
|
366
388
|
const card = document.createElement('div');
|
|
367
|
-
card.className = "
|
|
389
|
+
card.className = "group flex flex-row items-center justify-between p-5 bg-white/[0.03] border border-white/5 rounded-2xl hover:bg-white/[0.05] hover:border-white/10 transition-all duration-300 transform animate-in fade-in slide-in-from-right-4";
|
|
368
390
|
card.innerHTML = `
|
|
369
391
|
<div class="flex items-center gap-4">
|
|
370
|
-
<div class="size-12 rounded-full
|
|
392
|
+
<div class="size-12 rounded-full overflow-hidden flex items-center justify-center bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 shadow-[0_0_20px_rgba(16,185,129,0.1)]">
|
|
371
393
|
<svg class="size-7" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
372
394
|
</div>
|
|
373
395
|
<div class="flex flex-col">
|
|
374
|
-
<span class="text-white font-bold capitalize text-base tracking-tight">${
|
|
375
|
-
<span class="text-[9px]
|
|
396
|
+
<span class="text-white font-bold capitalize text-base tracking-tight">${displayText}</span>
|
|
397
|
+
<span class="text-[9px] ${langColor} font-black uppercase tracking-[0.1em]">${langLabel}</span>
|
|
376
398
|
</div>
|
|
377
399
|
</div>
|
|
378
400
|
<div class="flex flex-col items-end gap-1">
|
|
@@ -384,6 +406,7 @@
|
|
|
384
406
|
if (ui.stack.children.length > 5) ui.stack.lastElementChild.remove();
|
|
385
407
|
}
|
|
386
408
|
|
|
409
|
+
|
|
387
410
|
async function toggle() {
|
|
388
411
|
if (state.isListening) {
|
|
389
412
|
state.isListening = false;
|
|
@@ -406,26 +429,62 @@
|
|
|
406
429
|
state.audioContext = new AudioContext({ sampleRate: 16000 });
|
|
407
430
|
const source = state.audioContext.createMediaStreamSource(stream);
|
|
408
431
|
|
|
409
|
-
//
|
|
410
|
-
|
|
432
|
+
// Robust processing queue to prevent re-entrancy and state corruption
|
|
433
|
+
const audioQueue = [];
|
|
434
|
+
let isProcessing = false;
|
|
435
|
+
|
|
436
|
+
async function processQueue() {
|
|
437
|
+
if (isProcessing || audioQueue.length === 0) return;
|
|
438
|
+
isProcessing = true;
|
|
411
439
|
|
|
440
|
+
while (audioQueue.length > 0) {
|
|
441
|
+
const data = audioQueue.shift();
|
|
442
|
+
|
|
443
|
+
// Python-style Cooldown Check
|
|
444
|
+
const currentTime = Date.now();
|
|
445
|
+
if (currentTime - state.lastActivationTime < state.cooldownSeconds * 1000) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const results = await state.model.predict(data);
|
|
451
|
+
for (const [n, s] of Object.entries(results)) {
|
|
452
|
+
if (s > state.threshold) {
|
|
453
|
+
const clean = n.split('/').pop().replace('.onnx', '');
|
|
454
|
+
|
|
455
|
+
// Detection Triggered
|
|
456
|
+
state.lastActivationTime = Date.now();
|
|
457
|
+
pushDetection(clean, s);
|
|
458
|
+
log(`Match Found: ${clean} (${s.toFixed(2)})`, 'MATCH');
|
|
459
|
+
|
|
460
|
+
// Python-style Reset & Clear Queue
|
|
461
|
+
if (state.model.reset) state.model.reset();
|
|
462
|
+
audioQueue.length = 0; // Clear the queue
|
|
463
|
+
break; // Only trigger one wake word at a time
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} catch (err) {
|
|
467
|
+
log(`Inference Error: ${err.message}`, 'ERROR');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
isProcessing = false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
state.processor = state.audioContext.createScriptProcessor(2048, 1, 1);
|
|
412
476
|
source.connect(state.processor);
|
|
413
477
|
state.processor.connect(state.audioContext.destination);
|
|
414
478
|
|
|
415
|
-
state.processor.onaudioprocess =
|
|
479
|
+
state.processor.onaudioprocess = (e) => {
|
|
416
480
|
if (!state.isListening) return;
|
|
417
|
-
const data = e.inputBuffer.getChannelData(0);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (s > state.threshold) {
|
|
421
|
-
const clean = n.split('/').pop().replace('.onnx', '');
|
|
422
|
-
pushDetection(clean, s);
|
|
423
|
-
log(`Match Found: ${clean} (${s.toFixed(2)})`, 'MATCH');
|
|
424
|
-
}
|
|
425
|
-
}
|
|
481
|
+
const data = new Float32Array(e.inputBuffer.getChannelData(0));
|
|
482
|
+
audioQueue.push(data);
|
|
483
|
+
processQueue();
|
|
426
484
|
};
|
|
427
485
|
|
|
428
486
|
state.isListening = true;
|
|
487
|
+
|
|
429
488
|
ui.orbRing.classList.add('animate-pulse-ring');
|
|
430
489
|
ui.orbIcon.classList.remove('opacity-40');
|
|
431
490
|
ui.orbLabel.textContent = "listening";
|
package/models/hello_deepa.onnx
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -8,14 +8,14 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
8
8
|
const packageRoot = path.join(__dirname, '..');
|
|
9
9
|
const MODELS_DIR = path.join(process.cwd(), 'models');
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const EXTERNAL_MODELS = {
|
|
12
12
|
'melspectrogram.onnx': 'https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/melspectrogram.onnx',
|
|
13
13
|
'embedding_model.onnx': 'https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/embedding_model.onnx',
|
|
14
|
-
'silero_vad.onnx': 'https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/silero_vad.onnx'
|
|
15
|
-
'hello_deepa.onnx': 'https://github.com/Firojpaudel/OpenWakeWord_npm_porting/raw/main/models/hello_deepa.onnx',
|
|
16
|
-
'namaste_deepa.onnx': 'https://github.com/Firojpaudel/OpenWakeWord_npm_porting/raw/main/models/namaste_deepa.onnx'
|
|
14
|
+
'silero_vad.onnx': 'https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/silero_vad.onnx'
|
|
17
15
|
};
|
|
18
16
|
|
|
17
|
+
const SAMPLE_MODELS = ['hello_deepa.onnx', 'hello_deepa_old.onnx', 'namaste_deepa.onnx'];
|
|
18
|
+
|
|
19
19
|
async function downloadFile(url, dest) {
|
|
20
20
|
return new Promise((resolve, reject) => {
|
|
21
21
|
https.get(url, (response) => {
|
|
@@ -55,8 +55,8 @@ async function main() {
|
|
|
55
55
|
console.log(`Created directory: ${MODELS_DIR}`);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
console.log('
|
|
59
|
-
for (const [name, url] of Object.entries(
|
|
58
|
+
console.log('Deploying base neural model binaries...');
|
|
59
|
+
for (const [name, url] of Object.entries(EXTERNAL_MODELS)) {
|
|
60
60
|
const dest = path.join(MODELS_DIR, name);
|
|
61
61
|
if (fs.existsSync(dest) && fs.statSync(dest).size > 1000000) {
|
|
62
62
|
console.log(`- ${name} already exists and validated, skipping.`);
|
|
@@ -70,34 +70,37 @@ async function main() {
|
|
|
70
70
|
console.log(`Failed: ${err.message}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
|
|
74
|
+
console.log('\nDeploying sample wake-word models from package...');
|
|
75
|
+
for (const name of SAMPLE_MODELS) {
|
|
76
|
+
const src = path.join(packageRoot, 'models', name);
|
|
77
|
+
const dest = path.join(MODELS_DIR, name);
|
|
78
|
+
copyIfExists(src, dest, 'Sample Model');
|
|
79
|
+
}
|
|
74
80
|
|
|
75
81
|
console.log('\nDeploying ONNX Runtime WebAssembly environment...');
|
|
76
82
|
const nodeModulesPath = path.join(process.cwd(), 'node_modules', 'onnxruntime-web', 'dist');
|
|
77
83
|
|
|
78
84
|
if (fs.existsSync(nodeModulesPath)) {
|
|
79
|
-
// Copy EVERYTHING starting with ort-wasm to ensure all loaders/workers are present
|
|
80
85
|
const runtimeFiles = fs.readdirSync(nodeModulesPath).filter(f =>
|
|
81
86
|
f.startsWith('ort-wasm') && (f.endsWith('.wasm') || f.endsWith('.mjs') || f.endsWith('.js'))
|
|
82
87
|
);
|
|
83
88
|
for (const file of runtimeFiles) {
|
|
84
89
|
copyIfExists(path.join(nodeModulesPath, file), path.join(MODELS_DIR, file), 'RUNTIME');
|
|
85
90
|
}
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// Also copy the main entry point if not already there
|
|
92
|
+
copyIfExists(path.join(nodeModulesPath, 'ort.mjs'), path.join(MODELS_DIR, 'ort.mjs'), 'RUNTIME_ENTRY');
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
console.log('\nDeploying optimized AI Listening Interface...');
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
copyIfExists(exampleHtml, destHtml, 'UI');
|
|
96
|
+
copyIfExists(path.join(packageRoot, 'index.html'), path.join(process.cwd(), 'index.html'), 'UI');
|
|
97
|
+
copyIfExists(path.join(packageRoot, 'openwakeword.mjs'), path.join(process.cwd(), 'openwakeword.mjs'), 'Library');
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
copyIfExists(libSrc, libDest, 'Library');
|
|
99
|
+
// Copy test.html too
|
|
100
|
+
copyIfExists(path.join(packageRoot, 'models', 'test.html'), path.join(MODELS_DIR, 'test.html'), 'Debug UI');
|
|
98
101
|
|
|
99
102
|
console.log('\n----------------------------------------------------');
|
|
100
|
-
console.log('SETUP COMPLETE (v0.1.
|
|
103
|
+
console.log('SETUP COMPLETE (v0.1.19)');
|
|
101
104
|
console.log('----------------------------------------------------');
|
|
102
105
|
console.log('Your precision AI wake word interface is ready.');
|
|
103
106
|
console.log('\nTo start the demo:');
|