primocode 1.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -88
- package/bin/primocode.js +574 -390
- package/lib/act.js +500 -495
- package/lib/api.js +39 -39
- package/lib/browser.js +492 -492
- package/lib/config.js +76 -71
- package/lib/mascot.js +121 -0
- package/lib/memory.js +179 -0
- package/lib/mouse.js +144 -0
- package/lib/platform.js +245 -0
- package/lib/tools.js +503 -503
- package/lib/ui.js +104 -132
- package/package.json +32 -31
- package/lib/jarvis.js +0 -666
package/lib/jarvis.js
DELETED
|
@@ -1,666 +0,0 @@
|
|
|
1
|
-
// PrimoCode Jarvis - servidor local (HTTP + WebSocket hand-rolled) + página browser
|
|
2
|
-
// Zero deps: http + crypto nativos. Roda em localhost:8765.
|
|
3
|
-
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
const { c } = require('./ui');
|
|
7
|
-
|
|
8
|
-
const PORT = Number(process.env.PRIMOCODE_JARVIS_PORT) || 8765;
|
|
9
|
-
const HOST = '127.0.0.1';
|
|
10
|
-
|
|
11
|
-
// ---------- página frontend (vibe cmd, terminal preto/verde-teal) ----------
|
|
12
|
-
function renderHtml(serverUrl) {
|
|
13
|
-
return `<!doctype html>
|
|
14
|
-
<html lang="pt-br">
|
|
15
|
-
<head>
|
|
16
|
-
<meta charset="utf-8" />
|
|
17
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
18
|
-
<title>JARVIS · PrimoCode</title>
|
|
19
|
-
<style>
|
|
20
|
-
:root {
|
|
21
|
-
--bg:#0a0a0f;
|
|
22
|
-
--surface:#12121a;
|
|
23
|
-
--surface-hover:#1a1a25;
|
|
24
|
-
--border:#252535;
|
|
25
|
-
--fg:#e8e8f0;
|
|
26
|
-
--fg-dim:#6b6b80;
|
|
27
|
-
--accent:#00d4aa;
|
|
28
|
-
--accent-glow:rgba(0,212,170,0.15);
|
|
29
|
-
--amber:#f0a030;
|
|
30
|
-
--err:#e05060;
|
|
31
|
-
--radius:14px;
|
|
32
|
-
--mono:'SF Mono','JetBrains Mono','Fira Code',ui-monospace,Menlo,monospace;
|
|
33
|
-
}
|
|
34
|
-
* { box-sizing:border-box; margin:0; padding:0; }
|
|
35
|
-
html, body {
|
|
36
|
-
height:100%; background:var(--bg); color:var(--fg);
|
|
37
|
-
font-family:var(--mono); font-size:14px; overflow:hidden;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
body::before {
|
|
41
|
-
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
|
|
42
|
-
background:
|
|
43
|
-
radial-gradient(ellipse 80% 50% at 50% 0%, rgba(0,212,170,0.06) 0%, transparent 60%),
|
|
44
|
-
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(240,160,48,0.04) 0%, transparent 50%);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.app {
|
|
48
|
-
position:relative; z-index:1;
|
|
49
|
-
display:grid;
|
|
50
|
-
grid-template-columns: 1fr 360px;
|
|
51
|
-
grid-template-rows: auto 1fr auto;
|
|
52
|
-
height:100vh; gap:16px; padding:20px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.header {
|
|
56
|
-
grid-column:1/-1;
|
|
57
|
-
display:flex; align-items:center; justify-content:space-between;
|
|
58
|
-
padding:14px 20px;
|
|
59
|
-
background:var(--surface);
|
|
60
|
-
border:1px solid var(--border);
|
|
61
|
-
border-radius:var(--radius);
|
|
62
|
-
}
|
|
63
|
-
.brand {
|
|
64
|
-
display:flex; align-items:center; gap:12px;
|
|
65
|
-
font-weight:600; font-size:15px; letter-spacing:0.04em;
|
|
66
|
-
}
|
|
67
|
-
.pulse {
|
|
68
|
-
width:10px; height:10px; border-radius:50%;
|
|
69
|
-
background:var(--accent);
|
|
70
|
-
box-shadow:0 0 0 0 var(--accent-glow);
|
|
71
|
-
animation:pulse 2s ease-in-out infinite;
|
|
72
|
-
}
|
|
73
|
-
@keyframes pulse {
|
|
74
|
-
0%,100% { opacity:1; box-shadow:0 0 0 0 var(--accent-glow); }
|
|
75
|
-
50% { opacity:0.6; box-shadow:0 0 0 8px transparent; }
|
|
76
|
-
}
|
|
77
|
-
.status {
|
|
78
|
-
display:flex; gap:18px; align-items:center;
|
|
79
|
-
font-size:12px; color:var(--fg-dim);
|
|
80
|
-
}
|
|
81
|
-
.status-item { display:flex; align-items:center; gap:6px; }
|
|
82
|
-
.status-dot {
|
|
83
|
-
width:7px; height:7px; border-radius:50%;
|
|
84
|
-
background:var(--fg-dim); transition:all 0.3s;
|
|
85
|
-
}
|
|
86
|
-
.status-dot.on { background:var(--accent); box-shadow:0 0 8px var(--accent); }
|
|
87
|
-
.status-dot.err { background:var(--err); box-shadow:0 0 8px var(--err); }
|
|
88
|
-
.status-label { font-weight:500; color:var(--fg); }
|
|
89
|
-
|
|
90
|
-
.chat {
|
|
91
|
-
background:var(--surface);
|
|
92
|
-
border:1px solid var(--border);
|
|
93
|
-
border-radius:var(--radius);
|
|
94
|
-
padding:20px;
|
|
95
|
-
overflow-y:auto;
|
|
96
|
-
display:flex; flex-direction:column; gap:12px;
|
|
97
|
-
scrollbar-width:thin; scrollbar-color:var(--border) transparent;
|
|
98
|
-
}
|
|
99
|
-
.chat::-webkit-scrollbar { width:5px; }
|
|
100
|
-
.chat::-webkit-scrollbar-thumb { background:var(--border); border-radius:10px; }
|
|
101
|
-
|
|
102
|
-
.msg {
|
|
103
|
-
max-width:85%; padding:12px 16px;
|
|
104
|
-
border-radius:12px; line-height:1.6;
|
|
105
|
-
animation:fadeIn 0.3s ease;
|
|
106
|
-
}
|
|
107
|
-
@keyframes fadeIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
|
|
108
|
-
|
|
109
|
-
.msg-user {
|
|
110
|
-
align-self:flex-end;
|
|
111
|
-
background:linear-gradient(135deg, rgba(0,212,170,0.12), rgba(0,212,170,0.06));
|
|
112
|
-
border:1px solid rgba(0,212,170,0.2);
|
|
113
|
-
color:var(--fg);
|
|
114
|
-
}
|
|
115
|
-
.msg-jarvis {
|
|
116
|
-
align-self:flex-start;
|
|
117
|
-
background:var(--surface-hover);
|
|
118
|
-
border:1px solid var(--border);
|
|
119
|
-
color:var(--fg);
|
|
120
|
-
}
|
|
121
|
-
.msg-system {
|
|
122
|
-
align-self:center;
|
|
123
|
-
font-size:12px; color:var(--fg-dim);
|
|
124
|
-
padding:6px 14px;
|
|
125
|
-
background:rgba(255,255,255,0.03);
|
|
126
|
-
border-radius:20px;
|
|
127
|
-
}
|
|
128
|
-
.msg-tag {
|
|
129
|
-
font-size:11px; font-weight:600; text-transform:uppercase;
|
|
130
|
-
letter-spacing:0.08em; margin-bottom:4px; opacity:0.7;
|
|
131
|
-
}
|
|
132
|
-
.msg-user .msg-tag { color:var(--accent); }
|
|
133
|
-
.msg-jarvis .msg-tag { color:var(--amber); }
|
|
134
|
-
|
|
135
|
-
.sidebar {
|
|
136
|
-
display:flex; flex-direction:column; gap:16px;
|
|
137
|
-
}
|
|
138
|
-
.card {
|
|
139
|
-
background:var(--surface);
|
|
140
|
-
border:1px solid var(--border);
|
|
141
|
-
border-radius:var(--radius);
|
|
142
|
-
padding:16px;
|
|
143
|
-
transition:border-color 0.3s;
|
|
144
|
-
}
|
|
145
|
-
.card:hover { border-color:var(--border); }
|
|
146
|
-
.card-title {
|
|
147
|
-
font-size:11px; font-weight:600; text-transform:uppercase;
|
|
148
|
-
letter-spacing:0.12em; color:var(--fg-dim); margin-bottom:12px;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.cam-box {
|
|
152
|
-
aspect-ratio:16/10; background:#000;
|
|
153
|
-
border-radius:10px; overflow:hidden; position:relative;
|
|
154
|
-
border:1px solid var(--border);
|
|
155
|
-
}
|
|
156
|
-
.cam-box video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }
|
|
157
|
-
.cam-box.off::after {
|
|
158
|
-
content:'camera off'; position:absolute; inset:0;
|
|
159
|
-
display:flex; align-items:center; justify-content:center;
|
|
160
|
-
color:var(--fg-dim); font-size:13px;
|
|
161
|
-
}
|
|
162
|
-
.cam-info {
|
|
163
|
-
display:flex; justify-content:space-between;
|
|
164
|
-
font-size:12px; color:var(--fg-dim); margin-top:8px;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.wave-box {
|
|
168
|
-
height:64px; background:#08080f;
|
|
169
|
-
border-radius:10px; border:1px solid var(--border);
|
|
170
|
-
overflow:hidden; position:relative;
|
|
171
|
-
}
|
|
172
|
-
.wave-box canvas { width:100%; height:100%; display:block; }
|
|
173
|
-
.volume-bar {
|
|
174
|
-
height:5px; background:var(--border); border-radius:3px;
|
|
175
|
-
overflow:hidden; margin-top:10px;
|
|
176
|
-
}
|
|
177
|
-
.volume-fill {
|
|
178
|
-
height:100%; width:0%; background:var(--accent);
|
|
179
|
-
border-radius:3px; transition:width 0.08s linear;
|
|
180
|
-
box-shadow:0 0 10px rgba(0,212,170,0.3);
|
|
181
|
-
}
|
|
182
|
-
.audio-info {
|
|
183
|
-
display:flex; justify-content:space-between;
|
|
184
|
-
font-size:12px; color:var(--fg-dim); margin-top:8px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.controls { display:flex; flex-wrap:wrap; gap:8px; }
|
|
188
|
-
.btn {
|
|
189
|
-
background:transparent; color:var(--fg); border:1px solid var(--border);
|
|
190
|
-
padding:8px 14px; border-radius:8px; cursor:pointer;
|
|
191
|
-
font-family:var(--mono); font-size:12px; font-weight:500;
|
|
192
|
-
transition:all 0.2s; letter-spacing:0.02em;
|
|
193
|
-
}
|
|
194
|
-
.btn:hover { border-color:var(--accent); color:var(--accent); background:var(--accent-glow); }
|
|
195
|
-
.btn.active { border-color:var(--accent); color:var(--accent); background:var(--accent-glow); }
|
|
196
|
-
.btn-danger:hover { border-color:var(--err); color:var(--err); background:rgba(224,80,96,0.08); }
|
|
197
|
-
|
|
198
|
-
.voice-select {
|
|
199
|
-
width:100%; margin-top:8px;
|
|
200
|
-
background:var(--bg); color:var(--fg);
|
|
201
|
-
border:1px solid var(--border); border-radius:8px;
|
|
202
|
-
padding:8px 12px; font-family:var(--mono); font-size:12px;
|
|
203
|
-
cursor:pointer;
|
|
204
|
-
}
|
|
205
|
-
.voice-select:focus { outline:none; border-color:var(--accent); }
|
|
206
|
-
|
|
207
|
-
.footer {
|
|
208
|
-
grid-column:1/-1;
|
|
209
|
-
display:flex; justify-content:space-between; align-items:center;
|
|
210
|
-
padding:12px 20px;
|
|
211
|
-
background:var(--surface);
|
|
212
|
-
border:1px solid var(--border);
|
|
213
|
-
border-radius:var(--radius);
|
|
214
|
-
font-size:12px; color:var(--fg-dim);
|
|
215
|
-
}
|
|
216
|
-
.footer-hint b { color:var(--fg); font-weight:500; }
|
|
217
|
-
.footer-server { font-family:var(--mono); }
|
|
218
|
-
|
|
219
|
-
.typing {
|
|
220
|
-
display:flex; gap:4px; align-items:center;
|
|
221
|
-
padding:8px 0;
|
|
222
|
-
}
|
|
223
|
-
.typing-dot {
|
|
224
|
-
width:6px; height:6px; border-radius:50%;
|
|
225
|
-
background:var(--fg-dim);
|
|
226
|
-
animation:typingBounce 1.4s ease-in-out infinite;
|
|
227
|
-
}
|
|
228
|
-
.typing-dot:nth-child(2) { animation-delay:0.2s; }
|
|
229
|
-
.typing-dot:nth-child(3) { animation-delay:0.4s; }
|
|
230
|
-
@keyframes typingBounce {
|
|
231
|
-
0%,60%,100% { transform:translateY(0); }
|
|
232
|
-
30% { transform:translateY(-4px); }
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.scroll-btn {
|
|
236
|
-
position:absolute; bottom:80px; right:380px;
|
|
237
|
-
width:36px; height:36px; border-radius:50%;
|
|
238
|
-
background:var(--surface); border:1px solid var(--border);
|
|
239
|
-
color:var(--fg); cursor:pointer; display:none;
|
|
240
|
-
align-items:center; justify-content:center;
|
|
241
|
-
font-size:16px; transition:all 0.2s;
|
|
242
|
-
}
|
|
243
|
-
.scroll-btn:hover { border-color:var(--accent); color:var(--accent); }
|
|
244
|
-
.scroll-btn.show { display:flex; }
|
|
245
|
-
</style>
|
|
246
|
-
</head>
|
|
247
|
-
<body>
|
|
248
|
-
<div class="app">
|
|
249
|
-
<div class="header">
|
|
250
|
-
<div class="brand"><span class="pulse"></span> JARVIS · PrimoCode</div>
|
|
251
|
-
<div class="status">
|
|
252
|
-
<div class="status-item"><span id="d-mic" class="status-dot"></span> mic <span id="s-mic" class="status-label">off</span></div>
|
|
253
|
-
<div class="status-item"><span id="d-cam" class="status-dot"></span> cam <span id="s-cam" class="status-label">off</span></div>
|
|
254
|
-
<div class="status-item"><span id="d-ws" class="status-dot"></span> cli <span id="s-ws" class="status-label">...</span></div>
|
|
255
|
-
<div class="status-item"><span id="d-brain" class="status-dot"></span> brain <span id="s-brain" class="status-label">idle</span></div>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<div class="chat" id="log"></div>
|
|
260
|
-
<button class="scroll-btn" id="scrollBtn" onclick="scrollToBottom()">↓</button>
|
|
261
|
-
|
|
262
|
-
<div class="sidebar">
|
|
263
|
-
<div class="card">
|
|
264
|
-
<div class="card-title">Camera</div>
|
|
265
|
-
<div class="cam-box off" id="camWrap"><video id="video" autoplay playsinline muted></video></div>
|
|
266
|
-
<div class="cam-info"><span id="camRes">—</span><span id="camState">off</span></div>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
<div class="card">
|
|
270
|
-
<div class="card-title">Audio</div>
|
|
271
|
-
<div class="wave-box"><canvas id="wave"></canvas></div>
|
|
272
|
-
<div class="volume-bar"><div class="volume-fill" id="meter"></div></div>
|
|
273
|
-
<div class="audio-info"><span id="rms">RMS 0.00</span><span id="silence">silencio 0.0s</span></div>
|
|
274
|
-
</div>
|
|
275
|
-
|
|
276
|
-
<div class="card">
|
|
277
|
-
<div class="card-title">Controles</div>
|
|
278
|
-
<div class="controls">
|
|
279
|
-
<button class="btn" id="bMic">ligar mic</button>
|
|
280
|
-
<button class="btn" id="bCam">ligar cam</button>
|
|
281
|
-
<button class="btn btn-danger" id="bStop">parar</button>
|
|
282
|
-
</div>
|
|
283
|
-
<div style="margin-top:14px;">
|
|
284
|
-
<div class="card-title" style="margin-bottom:6px;">Voz</div>
|
|
285
|
-
<select id="voiceSel" class="voice-select"></select>
|
|
286
|
-
<button class="btn" id="bTest" style="margin-top:8px; width:100%;">testar voz</button>
|
|
287
|
-
</div>
|
|
288
|
-
<div class="audio-info" style="margin-top:10px;">
|
|
289
|
-
<span>silencio auto <b>4.0s</b></span>
|
|
290
|
-
<span id="voice">—</span>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
|
|
295
|
-
<div class="footer">
|
|
296
|
-
<div class="footer-hint">Fale normalmente · silencio de 4s envia · diga <b>"o que voce ve"</b> para usar a camera · <b>Esc</b> encerra</div>
|
|
297
|
-
<div class="footer-server">server <b>${escapeHtml(serverUrl)}</b></div>
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
|
|
301
|
-
<script>
|
|
302
|
-
const SERVER = ${JSON.stringify(serverUrl)};
|
|
303
|
-
const WS_URL = 'ws://' + location.host + '/ws';
|
|
304
|
-
const SILENCE_MS = 4000;
|
|
305
|
-
const VISION_TRIGGERS = /\b(v[eê]|olh[ae]|c[aâ]mera|camera|imagem|foto|tela|mostrando|est[aá] vendo|voc[eê] v[eê])\b/i;
|
|
306
|
-
|
|
307
|
-
const $ = (id) => document.getElementById(id);
|
|
308
|
-
const logEl = $('log');
|
|
309
|
-
|
|
310
|
-
function log(kind, text) {
|
|
311
|
-
const el = document.createElement('div');
|
|
312
|
-
el.className = 'msg msg-' + kind;
|
|
313
|
-
const tag = kind==='user' ? 'Voce' : kind==='jarvis' ? 'JARVIS' : kind==='error' ? 'Erro' : '·';
|
|
314
|
-
el.innerHTML = '<div class="msg-tag">' + tag + '</div>' + escapeHtml(text);
|
|
315
|
-
logEl.appendChild(el);
|
|
316
|
-
logEl.scrollTop = logEl.scrollHeight;
|
|
317
|
-
updateScrollBtn();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); }
|
|
321
|
-
|
|
322
|
-
function setStatus(id, on, text) {
|
|
323
|
-
const d = $('d-'+id); const s = $('s-'+id);
|
|
324
|
-
if (d) { d.classList.remove('on','err'); if (on===true) d.classList.add('on'); else if (on==='err') d.classList.add('err'); }
|
|
325
|
-
if (s && text) s.textContent = text;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function scrollToBottom() { logEl.scrollTop = logEl.scrollHeight; }
|
|
329
|
-
function updateScrollBtn() {
|
|
330
|
-
const btn = $('scrollBtn');
|
|
331
|
-
const nearBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 100;
|
|
332
|
-
btn.classList.toggle('show', !nearBottom);
|
|
333
|
-
}
|
|
334
|
-
logEl.addEventListener('scroll', updateScrollBtn);
|
|
335
|
-
|
|
336
|
-
let ws = null;
|
|
337
|
-
function connectWS() {
|
|
338
|
-
try {
|
|
339
|
-
ws = new WebSocket(WS_URL);
|
|
340
|
-
ws.onopen = () => { setStatus('ws', true, 'ok'); log('system', 'conectado ao CLI'); };
|
|
341
|
-
ws.onclose = () => { setStatus('ws', 'err', 'off'); };
|
|
342
|
-
ws.onerror = () => { setStatus('ws', 'err', 'erro'); };
|
|
343
|
-
ws.onmessage = (ev) => {
|
|
344
|
-
try {
|
|
345
|
-
const msg = JSON.parse(ev.data);
|
|
346
|
-
if (msg.type === 'log') log(msg.kind || 'system', msg.text);
|
|
347
|
-
} catch {}
|
|
348
|
-
};
|
|
349
|
-
} catch (e) { setStatus('ws', 'err', 'erro'); }
|
|
350
|
-
}
|
|
351
|
-
function wsSend(obj) { try { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); } catch {} }
|
|
352
|
-
connectWS();
|
|
353
|
-
|
|
354
|
-
let recognition = null;
|
|
355
|
-
let micStream = null;
|
|
356
|
-
let audioCtx = null;
|
|
357
|
-
let analyser = null;
|
|
358
|
-
let waveRAF = null;
|
|
359
|
-
let silenceTimer = null;
|
|
360
|
-
let silenceStart = 0;
|
|
361
|
-
let currentTranscript = '';
|
|
362
|
-
let isSending = false;
|
|
363
|
-
|
|
364
|
-
function initRecognition() {
|
|
365
|
-
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
366
|
-
if (!SR) { log('error', 'esse navegador nao suporta reconhecimento de voz (use Chrome)'); return null; }
|
|
367
|
-
const r = new SR();
|
|
368
|
-
r.lang = 'pt-BR'; r.continuous = true; r.interimResults = true;
|
|
369
|
-
r.onresult = (ev) => {
|
|
370
|
-
let interim = '', final = '';
|
|
371
|
-
for (let i = ev.resultIndex; i < ev.results.length; i++) {
|
|
372
|
-
const res = ev.results[i];
|
|
373
|
-
if (res.isFinal) final += res[0].transcript; else interim += res[0].transcript;
|
|
374
|
-
}
|
|
375
|
-
if (final) currentTranscript += final;
|
|
376
|
-
if (interim || final) resetSilence();
|
|
377
|
-
};
|
|
378
|
-
r.onerror = (e) => { if (e.error !== 'no-speech' && e.error !== 'aborted') log('error', 'stt: ' + e.error); };
|
|
379
|
-
r.onend = () => { if (micOn) { try { r.start(); } catch {} } };
|
|
380
|
-
return r;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function resetSilence() { silenceStart = performance.now(); }
|
|
384
|
-
|
|
385
|
-
function tickSilence() {
|
|
386
|
-
if (!micOn || isSending) return;
|
|
387
|
-
const elapsed = performance.now() - silenceStart;
|
|
388
|
-
const sec = (elapsed / 1000).toFixed(1);
|
|
389
|
-
$('silence').textContent = 'silencio ' + sec + 's';
|
|
390
|
-
if (elapsed >= SILENCE_MS && currentTranscript.trim()) {
|
|
391
|
-
sendTranscript();
|
|
392
|
-
}
|
|
393
|
-
silenceTimer = requestAnimationFrame(tickSilence);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function sendTranscript() {
|
|
397
|
-
const text = currentTranscript.trim();
|
|
398
|
-
if (!text) return;
|
|
399
|
-
isSending = true;
|
|
400
|
-
log('user', text);
|
|
401
|
-
const useVision = VISION_TRIGGERS.test(text) && camOn;
|
|
402
|
-
wsSend({ type: 'user', text, vision: useVision });
|
|
403
|
-
currentTranscript = '';
|
|
404
|
-
setTimeout(() => { isSending = false; resetSilence(); }, 800);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function startWave(stream) {
|
|
408
|
-
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
409
|
-
const src = audioCtx.createMediaStreamSource(stream);
|
|
410
|
-
analyser = audioCtx.createAnalyser();
|
|
411
|
-
analyser.fftSize = 256;
|
|
412
|
-
src.connect(analyser);
|
|
413
|
-
const canvas = $('wave');
|
|
414
|
-
const ctx = canvas.getContext('2d');
|
|
415
|
-
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
416
|
-
|
|
417
|
-
function draw() {
|
|
418
|
-
waveRAF = requestAnimationFrame(draw);
|
|
419
|
-
analyser.getByteFrequencyData(data);
|
|
420
|
-
const w = canvas.width = canvas.offsetWidth * window.devicePixelRatio;
|
|
421
|
-
const h = canvas.height = canvas.offsetHeight * window.devicePixelRatio;
|
|
422
|
-
ctx.clearRect(0, 0, w, h);
|
|
423
|
-
const bars = 60; const barW = w / bars;
|
|
424
|
-
let sum = 0;
|
|
425
|
-
for (let i = 0; i < bars; i++) {
|
|
426
|
-
const idx = Math.floor(i * data.length / bars);
|
|
427
|
-
const val = data[idx] / 255;
|
|
428
|
-
sum += val;
|
|
429
|
-
const bh = val * h * 0.85;
|
|
430
|
-
const x = i * barW;
|
|
431
|
-
const y = (h - bh) / 2;
|
|
432
|
-
const hue = 160 + val * 40;
|
|
433
|
-
ctx.fillStyle = `hsla(${hue}, 80%, 55%, ${0.3 + val * 0.7})`;
|
|
434
|
-
ctx.beginPath();
|
|
435
|
-
ctx.roundRect(x + 1, y, barW - 2, bh, 3);
|
|
436
|
-
ctx.fill();
|
|
437
|
-
}
|
|
438
|
-
const rms = sum / bars;
|
|
439
|
-
$('rms').textContent = 'RMS ' + rms.toFixed(2);
|
|
440
|
-
$('meter').style.width = Math.min(100, rms * 140) + '%';
|
|
441
|
-
}
|
|
442
|
-
draw();
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function stopWave() {
|
|
446
|
-
if (waveRAF) cancelAnimationFrame(waveRAF);
|
|
447
|
-
if (audioCtx) audioCtx.close();
|
|
448
|
-
waveRAF = null; audioCtx = null; analyser = null;
|
|
449
|
-
$('rms').textContent = 'RMS 0.00';
|
|
450
|
-
$('meter').style.width = '0%';
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
let camOn = false;
|
|
454
|
-
async function toggleCam() {
|
|
455
|
-
if (camOn) {
|
|
456
|
-
const v = $('video'); v.pause(); v.srcObject = null;
|
|
457
|
-
$('camWrap').classList.add('off'); $('camState').textContent = 'off';
|
|
458
|
-
$('camRes').textContent = '—'; camOn = false; setStatus('cam', false, 'off');
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
try {
|
|
462
|
-
const stream = await navigator.mediaDevices.getUserMedia({ video: { width:1280, height:720 } });
|
|
463
|
-
const v = $('video'); v.srcObject = stream;
|
|
464
|
-
await v.play();
|
|
465
|
-
const t = stream.getVideoTracks()[0];
|
|
466
|
-
const s = t.getSettings();
|
|
467
|
-
$('camWrap').classList.remove('off'); $('camState').textContent = 'on';
|
|
468
|
-
$('camRes').textContent = (s.width||'—') + '×' + (s.height||'—');
|
|
469
|
-
camOn = true; setStatus('cam', true, 'on');
|
|
470
|
-
} catch (e) { log('error', 'camera: ' + e.message); }
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
let micOn = false;
|
|
474
|
-
async function toggleMic() {
|
|
475
|
-
if (micOn) {
|
|
476
|
-
if (recognition) { try { recognition.stop(); } catch {} }
|
|
477
|
-
if (micStream) { micStream.getTracks().forEach(t => t.stop()); }
|
|
478
|
-
stopWave();
|
|
479
|
-
micOn = false; setStatus('mic', false, 'off'); $('bMic').classList.remove('active'); $('bMic').textContent = 'ligar mic';
|
|
480
|
-
if (silenceTimer) cancelAnimationFrame(silenceTimer);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
try {
|
|
484
|
-
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
485
|
-
recognition = initRecognition();
|
|
486
|
-
if (recognition) recognition.start();
|
|
487
|
-
startWave(micStream);
|
|
488
|
-
resetSilence();
|
|
489
|
-
silenceTimer = requestAnimationFrame(tickSilence);
|
|
490
|
-
micOn = true; setStatus('mic', true, 'on'); $('bMic').classList.add('active'); $('bMic').textContent = 'desligar mic';
|
|
491
|
-
} catch (e) { log('error', 'mic: ' + e.message); }
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
let voices = [];
|
|
495
|
-
function loadVoices() {
|
|
496
|
-
voices = speechSynthesis.getVoices().filter(v => v.lang.startsWith('pt'));
|
|
497
|
-
const sel = $('voiceSel');
|
|
498
|
-
sel.innerHTML = '';
|
|
499
|
-
voices.forEach((v, i) => {
|
|
500
|
-
const o = document.createElement('option');
|
|
501
|
-
o.value = i; o.textContent = v.name;
|
|
502
|
-
sel.appendChild(o);
|
|
503
|
-
});
|
|
504
|
-
if (voices.length) $('voice').textContent = voices[0].name;
|
|
505
|
-
}
|
|
506
|
-
speechSynthesis.onvoiceschanged = loadVoices;
|
|
507
|
-
loadVoices();
|
|
508
|
-
|
|
509
|
-
function speak(text) {
|
|
510
|
-
if (!text || !voices.length) return;
|
|
511
|
-
const u = new SpeechSynthesisUtterance(text);
|
|
512
|
-
const idx = parseInt($('voiceSel').value, 10) || 0;
|
|
513
|
-
u.voice = voices[idx] || voices[0];
|
|
514
|
-
u.rate = 1.05; u.pitch = 1.0;
|
|
515
|
-
speechSynthesis.speak(u);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
$('bMic').onclick = toggleMic;
|
|
519
|
-
$('bCam').onclick = toggleCam;
|
|
520
|
-
$('bStop').onclick = () => { if (micOn) toggleMic(); if (camOn) toggleCam(); };
|
|
521
|
-
$('bTest').onclick = () => speak('JARVIS online. Pronto para ajudar.');
|
|
522
|
-
$('voiceSel').onchange = () => { const v = voices[$('voiceSel').value]; if (v) $('voice').textContent = v.name; };
|
|
523
|
-
|
|
524
|
-
document.addEventListener('keydown', (e) => {
|
|
525
|
-
if (e.key === 'Escape') {
|
|
526
|
-
wsSend({ type: 'shutdown' });
|
|
527
|
-
window.close();
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
log('system', 'JARVIS pronto. Clique em "ligar mic" para comecar.');
|
|
532
|
-
</script>
|
|
533
|
-
</body>
|
|
534
|
-
</html>`;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function wsAccept(key) {
|
|
538
|
-
return crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
|
|
539
|
-
}
|
|
540
|
-
function wsFrame(payload) {
|
|
541
|
-
const data = Buffer.from(payload, 'utf8');
|
|
542
|
-
const len = data.length;
|
|
543
|
-
let head;
|
|
544
|
-
if (len < 126) {
|
|
545
|
-
head = Buffer.alloc(2);
|
|
546
|
-
head[0] = 0x81; // FIN + text frame
|
|
547
|
-
head[1] = len;
|
|
548
|
-
} else if (len < 65536) {
|
|
549
|
-
head = Buffer.alloc(4);
|
|
550
|
-
head[0] = 0x81; head[1] = 126;
|
|
551
|
-
head.writeUInt16BE(len, 2);
|
|
552
|
-
} else {
|
|
553
|
-
head = Buffer.alloc(10);
|
|
554
|
-
head[0] = 0x81; head[1] = 127;
|
|
555
|
-
head.writeBigUInt64BE(BigInt(len), 2);
|
|
556
|
-
}
|
|
557
|
-
return Buffer.concat([head, data]);
|
|
558
|
-
}
|
|
559
|
-
function wsParse(buf) {
|
|
560
|
-
// retorna { text, rest } ou null se incompleto
|
|
561
|
-
if (buf.length < 2) return null;
|
|
562
|
-
const b0 = buf[0], b1 = buf[1];
|
|
563
|
-
const opcode = b0 & 0x0f;
|
|
564
|
-
const masked = (b1 & 0x80) !== 0;
|
|
565
|
-
let len = b1 & 0x7f;
|
|
566
|
-
let offset = 2;
|
|
567
|
-
if (len === 126) { if (buf.length < 4) return null; len = buf.readUInt16BE(2); offset = 4; }
|
|
568
|
-
else if (len === 127) { if (buf.length < 10) return null; len = Number(buf.readBigUInt64BE(2)); offset = 10; }
|
|
569
|
-
if (masked) { if (buf.length < offset + 4 + len) return null; }
|
|
570
|
-
else { if (buf.length < offset + len) return null; }
|
|
571
|
-
let mask = null;
|
|
572
|
-
if (masked) { mask = buf.slice(offset, offset+4); offset += 4; }
|
|
573
|
-
const payload = buf.slice(offset, offset+len);
|
|
574
|
-
if (masked) for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i%4];
|
|
575
|
-
const rest = buf.slice(offset+len);
|
|
576
|
-
if (opcode === 0x8) return { close: true, rest };
|
|
577
|
-
if (opcode === 0x1) return { text: payload.toString('utf8'), rest };
|
|
578
|
-
return { rest }; // binário/outros: descarta
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// ---------- launch(): sobe servidor local, retorna Promise que resolve quando browser fecha ----------
|
|
582
|
-
function launch(config, opts = {}) {
|
|
583
|
-
const serverUrl = (config && config.server) || 'https://primocode-server.onrender.com';
|
|
584
|
-
let clients = new Set();
|
|
585
|
-
let shutdownRequested = false;
|
|
586
|
-
|
|
587
|
-
const server = http.createServer((req, res) => {
|
|
588
|
-
if (req.url === '/' || req.url === '/index.html') {
|
|
589
|
-
const html = renderHtml(serverUrl);
|
|
590
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
591
|
-
res.end(html);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (req.url === '/health') {
|
|
595
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
596
|
-
res.end(JSON.stringify({ ok:true, server: serverUrl }));
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
res.writeHead(404); res.end('nao encontrado');
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
server.on('upgrade', (req, socket) => {
|
|
603
|
-
if (req.url !== '/ws') { socket.destroy(); return; }
|
|
604
|
-
const key = req.headers['sec-websocket-key'];
|
|
605
|
-
if (!key) { socket.destroy(); return; }
|
|
606
|
-
const accept = wsAccept(key);
|
|
607
|
-
socket.write(
|
|
608
|
-
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
609
|
-
'Upgrade: websocket\r\n' +
|
|
610
|
-
'Connection: Upgrade\r\n' +
|
|
611
|
-
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`
|
|
612
|
-
);
|
|
613
|
-
clients.add(socket);
|
|
614
|
-
let buf = Buffer.alloc(0);
|
|
615
|
-
socket.on('data', (chunk) => {
|
|
616
|
-
buf = Buffer.concat([buf, chunk]);
|
|
617
|
-
while (true) {
|
|
618
|
-
const f = wsParse(buf);
|
|
619
|
-
if (!f) break;
|
|
620
|
-
buf = f.rest;
|
|
621
|
-
if (f.close) { try { socket.end(); } catch {} clients.delete(socket); break; }
|
|
622
|
-
if (f.text) {
|
|
623
|
-
try {
|
|
624
|
-
const msg = JSON.parse(f.text);
|
|
625
|
-
if (msg.type === 'user' && typeof msg.text === 'string') {
|
|
626
|
-
process.stdout.write(`\n${c.brand('▶')} ${c.dim('voz:')} ${c.white(msg.text)}\n`);
|
|
627
|
-
} else if (msg.type === 'jarvis' && typeof msg.text === 'string') {
|
|
628
|
-
process.stdout.write(`${c.accent('◀')} ${c.dim('JARVIS:')} ${msg.text}\n`);
|
|
629
|
-
} else if (msg.type === 'shutdown') {
|
|
630
|
-
shutdownRequested = true;
|
|
631
|
-
server.close(() => {});
|
|
632
|
-
for (const s of clients) { try { s.end(); } catch {} }
|
|
633
|
-
}
|
|
634
|
-
} catch {}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
socket.on('error', () => { clients.delete(socket); });
|
|
639
|
-
socket.on('close', () => { clients.delete(socket); });
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
return new Promise((resolve, reject) => {
|
|
643
|
-
server.listen(PORT, HOST, async () => {
|
|
644
|
-
const url = `http://${HOST}:${PORT}`;
|
|
645
|
-
process.stdout.write(
|
|
646
|
-
`\n${c.brand('●')} ${c.bold('Jarvis')} ativo em ${c.accent(url)}\n` +
|
|
647
|
-
` ${c.muted('abrindo o navegador…')} ${c.dim('feche a aba ou pressione Esc para encerrar')}\n\n`
|
|
648
|
-
);
|
|
649
|
-
// abre navegador do sistema
|
|
650
|
-
try {
|
|
651
|
-
const { openInBrowser } = require('./tools');
|
|
652
|
-
openInBrowser(url);
|
|
653
|
-
} catch {
|
|
654
|
-
process.stdout.write(` ${c.muted('abra manualmente:')} ${url}\n`);
|
|
655
|
-
}
|
|
656
|
-
// handler pra Ctrl+C encerrar
|
|
657
|
-
const onSig = () => { try { server.close(); } catch {} process.exit(0); };
|
|
658
|
-
process.once('SIGINT', onSig);
|
|
659
|
-
process.once('SIGTERM', onSig);
|
|
660
|
-
});
|
|
661
|
-
server.on('error', (e) => reject(e));
|
|
662
|
-
server.on('close', () => resolve({ ok: true }));
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
module.exports = { launch };
|