openwakeword-js 0.1.21 → 0.1.27
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/dist/index.d.ts +2 -0
- package/dist/index.js +250 -250
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +2848 -0
- package/index.html +100 -79
- package/models/hello_deepa_old.onnx +0 -0
- package/models/test.html +468 -0
- package/openwakeword.mjs +250 -250
- package/package.json +7 -1
- package/scripts/download_models.js +2 -1
- package/src/index.ts +50 -26
- package/src/worker.ts +28 -0
- package/worker.mjs +2848 -0
package/models/test.html
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html class="dark" lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
|
7
|
+
<title>AI Wake Word Detector | Local Verification</title>
|
|
8
|
+
<!-- Fonts -->
|
|
9
|
+
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
10
|
+
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap"
|
|
12
|
+
rel="stylesheet" />
|
|
13
|
+
<!-- Tailwind CSS -->
|
|
14
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
15
|
+
<script id="tailwind-config">
|
|
16
|
+
tailwind.config = {
|
|
17
|
+
darkMode: "class",
|
|
18
|
+
theme: {
|
|
19
|
+
extend: {
|
|
20
|
+
colors: {
|
|
21
|
+
"primary": "#195de6",
|
|
22
|
+
"primary-dark": "#0d3aa9",
|
|
23
|
+
"background-dark": "#0c0d12",
|
|
24
|
+
"surface-dark": "#161b26",
|
|
25
|
+
"surface-glass": "rgba(255, 255, 255, 0.03)",
|
|
26
|
+
},
|
|
27
|
+
fontFamily: {
|
|
28
|
+
"display": ["Space Grotesk", "sans-serif"],
|
|
29
|
+
"mono": ["ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Courier New", "monospace"],
|
|
30
|
+
},
|
|
31
|
+
borderRadius: { "DEFAULT": "1rem", "lg": "2rem", "xl": "3rem", "full": "9999px" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
<style>
|
|
37
|
+
.bg-noise {
|
|
38
|
+
position: fixed;
|
|
39
|
+
top: 0;
|
|
40
|
+
left: 0;
|
|
41
|
+
width: 100vw;
|
|
42
|
+
height: 100vh;
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
z-index: 50;
|
|
45
|
+
opacity: 0.03;
|
|
46
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Premium Slider Fix */
|
|
50
|
+
input[type=range] {
|
|
51
|
+
-webkit-appearance: none;
|
|
52
|
+
appearance: none;
|
|
53
|
+
background: transparent;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 24px;
|
|
57
|
+
/* Ensure enough hit area */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
input[type=range]::-webkit-slider-thumb {
|
|
61
|
+
-webkit-appearance: none;
|
|
62
|
+
height: 20px;
|
|
63
|
+
width: 20px;
|
|
64
|
+
border-radius: 50%;
|
|
65
|
+
background: #ffffff;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
margin-top: -8px;
|
|
68
|
+
/* Perfectly centered on 4px track */
|
|
69
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
70
|
+
transition: transform 0.1s ease-out, box-shadow 0.1s ease-out;
|
|
71
|
+
border: none;
|
|
72
|
+
position: relative;
|
|
73
|
+
z-index: 2;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
input[type=range]:active::-webkit-slider-thumb {
|
|
77
|
+
transform: scale(1.15);
|
|
78
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6), 0 0 0 4px rgba(25, 93, 230, 0.2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
input[type=range]::-webkit-slider-runnable-track {
|
|
82
|
+
width: 100%;
|
|
83
|
+
height: 4px;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
background: rgba(255, 255, 255, 0.1);
|
|
86
|
+
border-radius: 999px;
|
|
87
|
+
border: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@keyframes pulse-ring {
|
|
91
|
+
0% {
|
|
92
|
+
transform: scale(0.95);
|
|
93
|
+
box-shadow: 0 0 0 0 rgba(25, 93, 230, 0.4);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
70% {
|
|
97
|
+
transform: scale(1.1);
|
|
98
|
+
box-shadow: 0 0 0 30px rgba(25, 93, 230, 0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
100% {
|
|
102
|
+
transform: scale(0.95);
|
|
103
|
+
box-shadow: 0 0 0 0 rgba(25, 93, 230, 0);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.animate-pulse-ring {
|
|
108
|
+
animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@keyframes breathe {
|
|
112
|
+
|
|
113
|
+
0%,
|
|
114
|
+
100% {
|
|
115
|
+
opacity: 0.6;
|
|
116
|
+
transform: scale(0.98);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
50% {
|
|
120
|
+
opacity: 1;
|
|
121
|
+
transform: scale(1.02);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.animate-breathe {
|
|
126
|
+
animation: breathe 3s ease-in-out infinite;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
130
|
+
width: 4px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
134
|
+
background: transparent;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
138
|
+
background: rgba(255, 255, 255, 0.1);
|
|
139
|
+
border-radius: 2px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.detection-card {
|
|
143
|
+
animation: card-in 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@keyframes card-in {
|
|
147
|
+
from {
|
|
148
|
+
opacity: 0;
|
|
149
|
+
transform: translateY(15px) scale(0.98);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
to {
|
|
153
|
+
opacity: 1;
|
|
154
|
+
transform: translateY(0) scale(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Logic to hide slider overlap */
|
|
159
|
+
.slider-container {
|
|
160
|
+
position: relative;
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#sliderFill {
|
|
166
|
+
pointer-events: none;
|
|
167
|
+
z-index: 1;
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
170
|
+
|
|
171
|
+
</head>
|
|
172
|
+
|
|
173
|
+
<body
|
|
174
|
+
class="bg-background-dark text-slate-100 font-display min-h-screen antialiased selection:bg-primary selection:text-white overflow-x-hidden">
|
|
175
|
+
<div class="bg-noise"></div>
|
|
176
|
+
|
|
177
|
+
<div class="relative flex min-h-screen w-full flex-col items-center py-10 px-4 sm:px-6">
|
|
178
|
+
<main class="w-full max-w-[600px] flex flex-col gap-10 z-10">
|
|
179
|
+
<!-- Header -->
|
|
180
|
+
<header class="flex flex-col items-center text-center gap-2">
|
|
181
|
+
<div
|
|
182
|
+
class="inline-flex items-center justify-center size-12 rounded-[1.5rem] bg-surface-glass backdrop-blur-md border border-white/5 mb-4 shadow-lg">
|
|
183
|
+
<svg class="size-6 text-primary" fill="none" stroke="currentColor" stroke-width="2"
|
|
184
|
+
viewBox="0 0 24 24">
|
|
185
|
+
<path d="M12 3v18M17 8v8M7 8v8M21 12v0M3 12v0" stroke-linecap="round" stroke-linejoin="round" />
|
|
186
|
+
</svg>
|
|
187
|
+
</div>
|
|
188
|
+
<h1 class="text-3xl md:text-4xl font-light tracking-tight text-white/90">Local Verification</h1>
|
|
189
|
+
<div
|
|
190
|
+
class="flex items-center gap-2 text-[10px] text-slate-400 font-bold tracking-[0.2em] uppercase bg-white/5 px-4 py-1.5 rounded-full border border-white/5">
|
|
191
|
+
<span id="statusLed" class="size-1.5 rounded-full bg-slate-500"></span>
|
|
192
|
+
<span id="statusTitle">Local Debug Session</span>
|
|
193
|
+
</div>
|
|
194
|
+
</header>
|
|
195
|
+
|
|
196
|
+
<!-- Visualizer Orb Section -->
|
|
197
|
+
<section class="relative py-12 flex flex-col items-center justify-center">
|
|
198
|
+
<div id="orbGlow"
|
|
199
|
+
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-primary/20 blur-[100px] rounded-full pointer-events-none transition-all duration-700">
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="relative group">
|
|
203
|
+
<div id="orbRing" class="absolute inset-0 rounded-full bg-primary/20 blur-xl"></div>
|
|
204
|
+
<div class="absolute -inset-4 rounded-full border border-primary/20 opacity-30 scale-110"></div>
|
|
205
|
+
|
|
206
|
+
<div
|
|
207
|
+
class="relative flex items-center justify-center size-40 rounded-full bg-gradient-to-b from-surface-dark to-black shadow-[0_0_40px_-10px_rgba(25,93,230,0.3)] border border-white/10 overflow-hidden transition-all duration-500">
|
|
208
|
+
<div
|
|
209
|
+
class="absolute inset-0 bg-gradient-to-tr from-primary/20 via-transparent to-transparent opacity-50">
|
|
210
|
+
</div>
|
|
211
|
+
<div id="orbIcon"
|
|
212
|
+
class="relative z-10 flex flex-col items-center gap-2 animate-breathe opacity-40">
|
|
213
|
+
<svg class="size-12 text-white drop-shadow-[0_0_15px_rgba(255,255,255,0.5)]" fill="none"
|
|
214
|
+
stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
215
|
+
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
|
216
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
217
|
+
<line x1="12" x2="12" y1="19" y2="23" />
|
|
218
|
+
<line x1="8" x2="16" y1="23" y2="23" />
|
|
219
|
+
</svg>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="mt-8 text-center">
|
|
225
|
+
<p id="orbLabel" class="text-2xl font-light tracking-wide text-white/50 lowercase">standby</p>
|
|
226
|
+
<p id="orbSublabel"
|
|
227
|
+
class="text-xs text-slate-500 mt-2 font-mono tracking-wider uppercase opacity-60">repository
|
|
228
|
+
local node</p>
|
|
229
|
+
</div>
|
|
230
|
+
</section>
|
|
231
|
+
|
|
232
|
+
<!-- Controls Panel -->
|
|
233
|
+
<section
|
|
234
|
+
class="flex flex-col gap-6 bg-surface-glass backdrop-blur-xl rounded-[2.5rem] p-8 border border-white/5 shadow-2xl">
|
|
235
|
+
<div class="flex flex-col gap-5 px-2">
|
|
236
|
+
<div class="flex justify-between items-end">
|
|
237
|
+
<label class="text-[11px] font-bold text-slate-500 uppercase tracking-[0.1em]">Sensitivity
|
|
238
|
+
Threshold</label>
|
|
239
|
+
<span id="thresholdVal"
|
|
240
|
+
class="text-xs font-mono text-primary bg-primary/10 px-2 py-0.5 rounded border border-primary/20">0.50</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="slider-container relative h-6">
|
|
243
|
+
<input id="thresholdSlider" class="w-full relative z-2" max="0.99" min="0.01" step="0.01"
|
|
244
|
+
type="range" value="0.50" />
|
|
245
|
+
<div id="sliderFill"
|
|
246
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 h-[4px] bg-primary rounded-l-full"
|
|
247
|
+
style="width: 50%;"></div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<button id="toggleBtn"
|
|
252
|
+
class="group relative w-full h-16 bg-primary hover:bg-primary-dark active:scale-[0.98] transition-all duration-300 rounded-[1.25rem] flex items-center justify-center overflow-hidden shadow-[0_20px_40px_-10px_rgba(25,93,230,0.3)]">
|
|
253
|
+
<div
|
|
254
|
+
class="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
255
|
+
</div>
|
|
256
|
+
<span id="btnText"
|
|
257
|
+
class="relative flex items-center gap-3 text-white font-bold text-lg tracking-wide uppercase">
|
|
258
|
+
<svg class="size-6 mr-1" fill="currentColor" viewBox="0 0 24 24">
|
|
259
|
+
<path d="M8 5v14l11-7z" />
|
|
260
|
+
</svg>
|
|
261
|
+
Initialize Local Hub
|
|
262
|
+
</span>
|
|
263
|
+
</button>
|
|
264
|
+
</section>
|
|
265
|
+
|
|
266
|
+
<!-- Detection Stack -->
|
|
267
|
+
<section class="flex flex-col gap-4">
|
|
268
|
+
<h3 class="text-[10px] font-black text-slate-600 uppercase tracking-[0.2em] px-6">Signal Detection Event
|
|
269
|
+
Log</h3>
|
|
270
|
+
<div id="detectionStack" class="flex flex-col gap-3">
|
|
271
|
+
<div id="emptyState"
|
|
272
|
+
class="text-center py-10 text-slate-800 text-[10px] font-bold uppercase tracking-widest border border-dashed border-white/5 rounded-[2rem]">
|
|
273
|
+
Awaiting Audio Triggers</div>
|
|
274
|
+
</div>
|
|
275
|
+
</section>
|
|
276
|
+
|
|
277
|
+
<!-- Debug Console -->
|
|
278
|
+
<div class="w-full">
|
|
279
|
+
<details open class="group rounded-[2rem] bg-black/40 border border-white/5 overflow-hidden">
|
|
280
|
+
<summary
|
|
281
|
+
class="flex cursor-pointer items-center justify-between p-5 text-slate-500 hover:text-white transition-colors select-none list-none text-[10px] font-bold uppercase tracking-[0.1em] border-b border-white/5 bg-white/[0.02]">
|
|
282
|
+
<div class="flex items-center gap-2">
|
|
283
|
+
<svg class="size-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
284
|
+
<path d="M8 9l3 3-3 3m5 0h3" stroke-linecap="round" stroke-linejoin="round" />
|
|
285
|
+
</svg>
|
|
286
|
+
Repository Diagnostic Stream
|
|
287
|
+
</div>
|
|
288
|
+
<svg class="size-4 transition-transform duration-300 group-open:rotate-180 opacity-40"
|
|
289
|
+
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
290
|
+
<path d="M19 9l-7 7-7-7" stroke-linecap="round" stroke-linejoin="round" />
|
|
291
|
+
</svg>
|
|
292
|
+
</summary>
|
|
293
|
+
<div class="p-4">
|
|
294
|
+
<div id="console"
|
|
295
|
+
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">
|
|
296
|
+
<div
|
|
297
|
+
class="opacity-40 animate-pulse font-bold text-blue-900 border border-blue-900/40 p-1 inline-block rounded mb-2">
|
|
298
|
+
OPENWAKEWORD-JS V0.1.18 READY</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</details>
|
|
302
|
+
</div>
|
|
303
|
+
</main>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<!-- Scripts -->
|
|
307
|
+
<script type="module">
|
|
308
|
+
import { Model } from '../openwakeword.mjs';
|
|
309
|
+
|
|
310
|
+
const state = {
|
|
311
|
+
isListening: false,
|
|
312
|
+
threshold: 0.50,
|
|
313
|
+
model: null,
|
|
314
|
+
audioContext: null,
|
|
315
|
+
processor: null,
|
|
316
|
+
startTime: Date.now()
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const ui = {
|
|
320
|
+
btn: document.getElementById('toggleBtn'),
|
|
321
|
+
btnText: document.getElementById('btnText'),
|
|
322
|
+
slider: document.getElementById('thresholdSlider'),
|
|
323
|
+
sliderVal: document.getElementById('thresholdVal'),
|
|
324
|
+
sliderFill: document.getElementById('sliderFill'),
|
|
325
|
+
orbRing: document.getElementById('orbRing'),
|
|
326
|
+
orbIcon: document.getElementById('orbIcon'),
|
|
327
|
+
orbLabel: document.getElementById('orbLabel'),
|
|
328
|
+
orbSublabel: document.getElementById('orbSublabel'),
|
|
329
|
+
statusLed: document.getElementById('statusLed'),
|
|
330
|
+
statusTitle: document.getElementById('statusTitle'),
|
|
331
|
+
stack: document.getElementById('detectionStack'),
|
|
332
|
+
console: document.getElementById('console'),
|
|
333
|
+
empty: document.getElementById('emptyState')
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function log(msg, level = 'INFO') {
|
|
337
|
+
const time = ((Date.now() - state.startTime) / 1000).toFixed(1);
|
|
338
|
+
const colors = {
|
|
339
|
+
INFO: 'text-blue-500 font-medium',
|
|
340
|
+
SYSTEM: 'text-purple-400 font-bold',
|
|
341
|
+
MATCH: 'text-emerald-400 font-black',
|
|
342
|
+
ERROR: 'text-red-500 font-bold bg-red-500/10 px-1 rounded'
|
|
343
|
+
};
|
|
344
|
+
const div = document.createElement('div');
|
|
345
|
+
div.className = 'flex gap-2 mb-1 border-b border-white/5 pb-1';
|
|
346
|
+
div.innerHTML = `<span class="opacity-40 min-w-[40px]">${time}s</span> <span class="${colors[level] || 'text-slate-500'} min-w-[50px] uppercase">[${level}]</span> <span class="flex-1">${msg}</span>`;
|
|
347
|
+
ui.console.appendChild(div);
|
|
348
|
+
ui.console.scrollTop = ui.console.scrollHeight;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function init() {
|
|
352
|
+
if (state.model) return true;
|
|
353
|
+
log('Neural buffers allocating...', 'SYSTEM');
|
|
354
|
+
try {
|
|
355
|
+
const base = './';
|
|
356
|
+
state.model = new Model({
|
|
357
|
+
wakewordModels: [`${base}hello_deepa.onnx`, `${base}namaste_deepa.onnx`],
|
|
358
|
+
melspectrogramModelPath: `${base}melspectrogram.onnx`,
|
|
359
|
+
embeddingModelPath: `${base}embedding_model.onnx`,
|
|
360
|
+
vadModelPath: `${base}silero_vad.onnx`,
|
|
361
|
+
inferenceFramework: 'onnx',
|
|
362
|
+
wasmPaths: base
|
|
363
|
+
});
|
|
364
|
+
await state.model.init();
|
|
365
|
+
log('Local build linked and bit-perfect.', 'SYSTEM');
|
|
366
|
+
return true;
|
|
367
|
+
} catch (e) {
|
|
368
|
+
log(`Init error: ${e.message}`, 'ERROR');
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function pushDetection(name, score) {
|
|
374
|
+
if (ui.empty) ui.empty.style.display = 'none';
|
|
375
|
+
const card = document.createElement('div');
|
|
376
|
+
card.className = "detection-card flex items-center justify-between p-5 bg-white/5 backdrop-blur-md rounded-[1.5rem] border border-white/10 shadow-lg";
|
|
377
|
+
card.innerHTML = `
|
|
378
|
+
<div class="flex items-center gap-4">
|
|
379
|
+
<div class="size-12 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-400 border border-emerald-500/20 shadow-inner">
|
|
380
|
+
<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>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="flex flex-col">
|
|
383
|
+
<span class="text-white font-bold capitalize text-base tracking-tight">${name.replace(/_/g, ' ')}</span>
|
|
384
|
+
<span class="text-[9px] text-slate-500 font-black uppercase tracking-[0.1em]">Verified Repository Signature</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="flex flex-col items-end gap-1">
|
|
388
|
+
<span class="text-[9px] font-black text-emerald-400 bg-emerald-400/10 px-3 py-1 rounded-full border border-emerald-400/20">${Math.round(score * 100)}% Match</span>
|
|
389
|
+
<span class="text-[8px] font-mono text-slate-700 uppercase">${new Date().toLocaleTimeString([], { hour12: false })}</span>
|
|
390
|
+
</div>
|
|
391
|
+
`;
|
|
392
|
+
ui.stack.prepend(card);
|
|
393
|
+
if (ui.stack.children.length > 5) ui.stack.lastElementChild.remove();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function toggle() {
|
|
397
|
+
if (state.isListening) {
|
|
398
|
+
state.isListening = false;
|
|
399
|
+
if (state.audioContext) await state.audioContext.close();
|
|
400
|
+
ui.orbRing.classList.remove('animate-pulse-ring');
|
|
401
|
+
ui.orbIcon.classList.add('opacity-40');
|
|
402
|
+
ui.orbLabel.textContent = "standby";
|
|
403
|
+
ui.orbLabel.className = "text-2xl font-light tracking-wide text-white/50 lowercase";
|
|
404
|
+
ui.btnText.innerHTML = `<svg class="size-6 mr-1" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg> RESUME SESSION`;
|
|
405
|
+
ui.statusLed.className = "size-1.5 rounded-full bg-slate-500";
|
|
406
|
+
log('Audio channel terminated.');
|
|
407
|
+
} else {
|
|
408
|
+
ui.btn.disabled = true;
|
|
409
|
+
const ok = await init();
|
|
410
|
+
ui.btn.disabled = false;
|
|
411
|
+
if (!ok) return;
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
415
|
+
state.audioContext = new AudioContext({ sampleRate: 16000 });
|
|
416
|
+
const source = state.audioContext.createMediaStreamSource(stream);
|
|
417
|
+
|
|
418
|
+
// FIX: Buffer size MUST be a power of two (1024)
|
|
419
|
+
state.processor = state.audioContext.createScriptProcessor(1024, 1, 1);
|
|
420
|
+
|
|
421
|
+
source.connect(state.processor);
|
|
422
|
+
state.processor.connect(state.audioContext.destination);
|
|
423
|
+
|
|
424
|
+
state.processor.onaudioprocess = async (e) => {
|
|
425
|
+
if (!state.isListening) return;
|
|
426
|
+
const data = e.inputBuffer.getChannelData(0);
|
|
427
|
+
const results = await state.model.predict(data);
|
|
428
|
+
for (const [n, s] of Object.entries(results)) {
|
|
429
|
+
if (s > state.threshold) {
|
|
430
|
+
const clean = n.split('/').pop().replace('.onnx', '');
|
|
431
|
+
pushDetection(clean, s);
|
|
432
|
+
log(`Signature Match: ${clean} (${s.toFixed(2)})`, 'MATCH');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
state.isListening = true;
|
|
438
|
+
ui.orbRing.classList.add('animate-pulse-ring');
|
|
439
|
+
ui.orbIcon.classList.remove('opacity-40');
|
|
440
|
+
ui.orbLabel.textContent = "listening";
|
|
441
|
+
ui.orbLabel.className = "text-2xl font-light tracking-wide text-white animate-pulse lowercase";
|
|
442
|
+
ui.btnText.innerHTML = `<svg class="size-6 mr-1" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg> TERMINATE HUD`;
|
|
443
|
+
ui.statusLed.className = "size-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]";
|
|
444
|
+
log('Capture stream active.');
|
|
445
|
+
} catch (e) {
|
|
446
|
+
log(`Channel fault: ${e.message}`, 'ERROR');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
ui.btn.onclick = toggle;
|
|
452
|
+
|
|
453
|
+
// Final Fix for Slider Aesthetics & Responsiveness
|
|
454
|
+
ui.slider.oninput = (e) => {
|
|
455
|
+
state.threshold = parseFloat(e.target.value);
|
|
456
|
+
ui.sliderVal.textContent = state.threshold.toFixed(2);
|
|
457
|
+
ui.sliderFill.style.width = `${state.threshold * 100}%`;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
ui.slider.onchange = (e) => {
|
|
461
|
+
log(`Threshold precision: ${state.threshold.toFixed(2)}`);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
log('Local environment standing by.', 'SYSTEM');
|
|
465
|
+
</script>
|
|
466
|
+
</body>
|
|
467
|
+
|
|
468
|
+
</html>
|