primocode 10.15.0 → 10.15.1
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 +2 -2
- package/bin/primocode.js +9 -1
- package/lib/conta.js +1 -0
- package/lib/estudio/exportar.js +1 -0
- package/lib/mcp.js +1 -0
- package/lib/tools.js +1 -0
- package/lib/voz.js +27 -3
- package/package.json +1 -1
- package/voz/primo_voz/__main__.py +139 -20
- package/voz/primo_voz/app.py +350 -0
- package/voz/primo_voz/appmac.py +124 -0
- package/voz/primo_voz/assets/mascote.svg +1 -0
- package/voz/primo_voz/escuta.py +25 -3
- package/voz/primo_voz/fala.py +87 -0
- package/voz/primo_voz/hud.html +58 -24
- package/voz/primo_voz/hud.py +61 -10
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/tela.py +36 -54
- package/voz/primo_voz/transcrever.py +13 -2
- package/voz/primo_voz/vendor/primo-icone.png +0 -0
package/voz/primo_voz/hud.html
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>Primo</title>
|
|
6
|
+
<link rel="preload" as="script" href="vendor/spline-runtime.min.js">
|
|
7
|
+
<link rel="preload" as="fetch" crossorigin href="https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode">
|
|
8
|
+
<script src="vendor/spline-runtime.min.js"></script>
|
|
6
9
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
7
10
|
<style>
|
|
8
11
|
:root {
|
|
@@ -15,6 +18,10 @@
|
|
|
15
18
|
html,body { height:100%; background:var(--fundo); color:var(--texto); font-family:var(--sans); -webkit-font-smoothing:antialiased; user-select:none; -webkit-user-select:none; }
|
|
16
19
|
body { background:radial-gradient(ellipse 60% 55% at 50% 42%, #1a2a2c 0%, var(--fundo) 62%); overflow:hidden; }
|
|
17
20
|
.tela { height:100vh; padding:38px 28px 24px; display:grid; gap:18px; grid-template-columns:minmax(250px,22%) 1fr minmax(280px,25%); grid-template-rows:minmax(0,1fr) auto auto; }
|
|
21
|
+
@property --v { syntax:'<number>'; inherits:true; initial-value:0; }
|
|
22
|
+
.cartao { transition:transform .28s cubic-bezier(.2,.8,.2,1), box-shadow .28s, border-color .28s; animation:sobe .75s cubic-bezier(.2,.8,.2,1) backwards; animation-delay:calc(var(--i,0)*70ms); }
|
|
23
|
+
.cartao:hover { transform:translateY(-3px); border-color:#3a3a36; box-shadow:0 1px 0 rgba(255,255,255,.05) inset, 0 28px 44px -22px rgba(0,0,0,.9); }
|
|
24
|
+
@keyframes sobe { from { opacity:0; transform:translateY(18px); } }
|
|
18
25
|
.cartao { background:linear-gradient(180deg,var(--cartao2),var(--cartao)); border:1px solid var(--borda); border-radius:var(--raio); padding:18px 22px; box-shadow:0 1px 0 rgba(255,255,255,.04) inset, 0 20px 40px -24px rgba(0,0,0,.8); }
|
|
19
26
|
.rotulo { font-size:11px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--fraco); }
|
|
20
27
|
|
|
@@ -26,7 +33,12 @@
|
|
|
26
33
|
.cal .sem { color:var(--fraco); font-size:11px; font-weight:600; padding-bottom:3px; height:auto; }
|
|
27
34
|
.cal span { height:30px; display:grid; place-items:center; color:var(--apagado); border-radius:50%; width:30px; margin:auto; position:relative; }
|
|
28
35
|
.cal .fds { color:var(--fraco); }
|
|
29
|
-
.cal .hoje { background:var(--marca); color:#06191c; font-weight:700; }
|
|
36
|
+
.cal .hoje { background:var(--marca); color:#06191c; font-weight:700; animation:pulso-dia 2.6s ease-in-out infinite; }
|
|
37
|
+
@keyframes pulso-dia { 0%,100% { box-shadow:0 0 0 0 rgba(34,211,238,.45); } 50% { box-shadow:0 0 0 7px rgba(34,211,238,0); } }
|
|
38
|
+
.capa.troca { animation:capa-troca .8s cubic-bezier(.2,.8,.2,1); } @keyframes capa-troca { from { opacity:0; transform:rotate(-140deg) scale(.6); } }
|
|
39
|
+
@keyframes fade { from { opacity:0; } }
|
|
40
|
+
.faixa.troca { animation:fade .6s ease; }
|
|
41
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation:none !important; transition:none !important; } }
|
|
30
42
|
.cal .tem::after { content:""; width:4px; height:4px; border-radius:50%; background:var(--marca); position:absolute; bottom:1px; }
|
|
31
43
|
.cal .hoje.tem::after { background:#06191c; }
|
|
32
44
|
.hoje-card { flex:1; min-height:0; overflow:hidden; }
|
|
@@ -49,13 +61,22 @@
|
|
|
49
61
|
.nome p::before { content:""; display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--ok); margin-right:8px; box-shadow:0 0 10px var(--ok); }
|
|
50
62
|
.nome[data-e="pensando"] p::before, .nome[data-e="executando"] p::before { background:#f5b94a; box-shadow:0 0 10px #f5b94a; }
|
|
51
63
|
.nome[data-e="dormindo"] p::before { background:var(--fraco); box-shadow:none; }
|
|
64
|
+
.nome .pasta { font:12px var(--mono); color:var(--fraco); margin-top:2px; }
|
|
65
|
+
.nome .pasta::before { display:none; }
|
|
66
|
+
.b-entrar { margin-top:8px; background:var(--marca); color:#06191c; border:0; border-radius:999px; padding:8px 18px; font:600 13px var(--sans); cursor:pointer; }
|
|
52
67
|
.fala { position:absolute; top:0; left:50%; transform:translateX(-50%); max-width:70%; text-align:center; color:var(--apagado); font-size:13px; line-height:1.4; max-height:3em; overflow:hidden; }
|
|
53
68
|
/* robô 3D: canvas transparente sobre o palco; o SVG só aparece se o WebGL falhar */
|
|
54
69
|
#robo3d { width:100%; height:100%; display:none; -webkit-mask-image:linear-gradient(#000 82%,transparent); mask-image:linear-gradient(#000 82%,transparent); }
|
|
55
|
-
|
|
70
|
+
#svg-robo { display:none; }
|
|
71
|
+
.tres-d #robo3d { display:block; animation:entra .6s ease both; } .svg #svg-robo { display:block; animation:entra .6s ease both; }
|
|
72
|
+
@keyframes entra { from { opacity:0; } }
|
|
73
|
+
/* carregando: nenhum robô, só o lugar dele com um brilho ciano suave pulsando */
|
|
74
|
+
#carrega { display:none; width:min(30vh,260px); height:min(48vh,420px); border-radius:50% 50% 42% 42% / 38% 38% 62% 62%; background:radial-gradient(closest-side,rgba(34,211,238,.22),rgba(34,211,238,.07) 62%,transparent); filter:blur(14px); animation:carrega 2.2s ease-in-out infinite; pointer-events:none; }
|
|
75
|
+
.carregando #carrega { display:block; }
|
|
76
|
+
@keyframes carrega { 0%,100% { opacity:.35; transform:scale(.94); } 50% { opacity:.95; transform:scale(1.04); } }
|
|
56
77
|
/* robô do Spline (cena da comunidade): canvas transparente, brilho ciano por CSS, busto pelo zoom da câmera */
|
|
57
78
|
#robo-spline { width:100% !important; height:100% !important; visibility:hidden; background:transparent; --nv:0; -webkit-mask-image:linear-gradient(#000 44%,rgba(0,0,0,.55) 58%,transparent 76%); mask-image:linear-gradient(#000 44%,rgba(0,0,0,.55) 58%,transparent 76%); filter:drop-shadow(0 0 18px rgba(34,211,238,.22)); transition:filter .4s; }
|
|
58
|
-
.spline #robo-spline { visibility:visible; } .spline #robo3d, .spline #svg-robo { display:none; }
|
|
79
|
+
.spline #robo-spline { visibility:visible; animation:entra .6s ease both; } .spline #robo3d, .spline #svg-robo { display:none; }
|
|
59
80
|
.palco > #robo-spline { grid-area:1/1; }
|
|
60
81
|
.spline.e-ouvindo #robo-spline { filter:drop-shadow(0 0 26px rgba(34,211,238,.55)); }
|
|
61
82
|
.spline.e-pensando #robo-spline, .spline.e-executando #robo-spline { filter:drop-shadow(0 0 22px rgba(34,211,238,.38)); animation:sp-pensa 1.8s ease-in-out infinite; }
|
|
@@ -97,16 +118,16 @@
|
|
|
97
118
|
.capa::after { content:""; position:absolute; inset:44%; border-radius:50%; background:var(--fundo); box-shadow:0 0 0 2px #0b0b0a; }
|
|
98
119
|
.faixa { text-align:center; max-width:100%; } .faixa b { display:block; font-size:16px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .faixa small { color:var(--apagado); font-size:13px; }
|
|
99
120
|
.prog { width:100%; display:flex; align-items:center; gap:10px; font:12px var(--mono); color:var(--fraco); }
|
|
100
|
-
.prog .t { flex:1; height:4px; border-radius:2px; background:var(--borda); overflow:hidden; } .prog .t i { display:block; height:100%; width:0; background:var(--texto); border-radius:2px; }
|
|
121
|
+
.prog .t { flex:1; height:4px; border-radius:2px; background:var(--borda); overflow:hidden; } .prog .t i { display:block; height:100%; width:0; transition:width .3s linear; background:var(--texto); border-radius:2px; }
|
|
101
122
|
.ctrl { display:flex; gap:30px; align-items:center; }
|
|
102
123
|
.ctrl button { background:none; border:0; color:var(--texto); cursor:pointer; display:grid; place-items:center; }
|
|
103
124
|
.ctrl button:active { transform:scale(.9); }
|
|
104
125
|
.ctrl .play { width:50px; height:50px; border-radius:50%; background:var(--texto); color:var(--fundo); }
|
|
105
126
|
|
|
106
127
|
/* medidores */
|
|
107
|
-
.medidores { grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,1fr)
|
|
128
|
+
.medidores { grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,1fr); gap:18px; }
|
|
108
129
|
.med { display:flex; align-items:center; gap:16px; padding:14px 20px; }
|
|
109
|
-
.anel { --v:0; width:62px; height:62px; border-radius:50%; flex:none; display:grid; place-items:center; background:conic-gradient(var(--c,var(--marca)) calc(var(--v)*1%), var(--borda) 0); position:relative; }
|
|
130
|
+
.anel { transition:--v .9s cubic-bezier(.2,.8,.2,1); --v:0; width:62px; height:62px; border-radius:50%; flex:none; display:grid; place-items:center; background:conic-gradient(var(--c,var(--marca)) calc(var(--v)*1%), var(--borda) 0); position:relative; }
|
|
110
131
|
.anel::before { content:""; position:absolute; inset:6px; border-radius:50%; background:var(--cartao); }
|
|
111
132
|
.anel span { position:relative; font:600 15px var(--sans); font-variant-numeric:tabular-nums; }
|
|
112
133
|
.med b { display:block; font-size:14px; } .med small { color:var(--apagado); font-size:12px; }
|
|
@@ -117,7 +138,7 @@
|
|
|
117
138
|
/* voz */
|
|
118
139
|
.voz { grid-column:1 / -1; display:flex; align-items:center; gap:18px; padding:12px 22px; }
|
|
119
140
|
#barras { flex:1; height:46px; display:flex; align-items:center; gap:3px; }
|
|
120
|
-
#barras i { flex:1; border-radius:3px; height:8%; background:var(--marca); transition:height .
|
|
141
|
+
#barras i { flex:1; border-radius:3px; height:8%; background:var(--marca); transition:height .12s ease-out; }
|
|
121
142
|
#barras i.p { background:var(--ok); } #barras i.z { background:var(--borda); }
|
|
122
143
|
.leg { display:flex; gap:16px; font-size:12px; color:var(--apagado); white-space:nowrap; }
|
|
123
144
|
.leg span::before { content:""; display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; background:var(--marca); }
|
|
@@ -143,6 +164,7 @@
|
|
|
143
164
|
<div class="fala" id="fala"></div>
|
|
144
165
|
<div class="palco">
|
|
145
166
|
<div class="chao"></div>
|
|
167
|
+
<div id="carrega"></div>
|
|
146
168
|
<canvas id="robo3d" aria-label="Primo"></canvas>
|
|
147
169
|
<canvas id="robo-spline" aria-label="Primo"></canvas>
|
|
148
170
|
<svg id="svg-robo" viewBox="0 0 400 520" aria-label="Primo" class="desmontado">
|
|
@@ -216,7 +238,7 @@
|
|
|
216
238
|
</g>
|
|
217
239
|
</svg>
|
|
218
240
|
</div>
|
|
219
|
-
<div class="nome" id="nome" data-e="dormindo"><h1 id="nome-h">Primo</h1><p id="estado-t">em espera</p></div>
|
|
241
|
+
<div class="nome" id="nome" data-e="dormindo"><h1 id="nome-h">Primo</h1><p id="estado-t">em espera</p><p id="pasta" class="pasta"></p><button id="b-entrar" class="b-entrar" hidden>Entrar</button></div>
|
|
220
242
|
</section>
|
|
221
243
|
|
|
222
244
|
<section class="dir">
|
|
@@ -242,10 +264,6 @@
|
|
|
242
264
|
<div class="cartao med"><div class="anel" id="a-mem" style="--c:var(--marca-clara)"><span>–</span></div><div><b>Memória</b><small id="s-mem"> </small></div></div>
|
|
243
265
|
<div class="cartao med"><div class="anel" id="a-disco" style="--c:var(--apagado)"><span>–</span></div><div><b>Disco</b><small id="s-disco"> </small></div></div>
|
|
244
266
|
<div class="cartao med"><div class="anel" id="a-bat" style="--c:var(--ok)"><span>–</span></div><div><b>Bateria</b><small id="s-bat"> </small></div></div>
|
|
245
|
-
<div class="cartao rede">
|
|
246
|
-
<div class="lin"><span>REDE</span><span id="r-txt">↓ 0 KB/s · ↑ 0 KB/s</span></div>
|
|
247
|
-
<svg viewBox="0 0 300 44" preserveAspectRatio="none"><path id="r-down" d="" fill="none" stroke="#22d3ee" stroke-width="2"/><path id="r-up" d="" fill="none" stroke="#7ad7a3" stroke-width="1.5" opacity=".85"/></svg>
|
|
248
|
-
</div>
|
|
249
267
|
</section>
|
|
250
268
|
|
|
251
269
|
<section class="cartao voz">
|
|
@@ -256,7 +274,6 @@
|
|
|
256
274
|
</main>
|
|
257
275
|
|
|
258
276
|
<script src="vendor/three-robo.min.js"></script>
|
|
259
|
-
<script src="vendor/spline-runtime.min.js"></script>
|
|
260
277
|
<script>
|
|
261
278
|
const $ = (id) => document.getElementById(id);
|
|
262
279
|
const post = (o) => { try { window.webkit.messageHandlers.primo.postMessage(JSON.stringify(o)); } catch (e) {} };
|
|
@@ -353,12 +370,13 @@ const R3 = (function () {
|
|
|
353
370
|
/* ── robô do Spline (runtime local, cena na rede). Falhou ou passou de 6 s: fica o RobotExpressive ── */
|
|
354
371
|
const SP = (function () {
|
|
355
372
|
const CENA = 'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode';
|
|
356
|
-
const X = { ok: false, tentou: false, app: null, bot: null };
|
|
373
|
+
const X = { ok: false, tentou: false, app: null, bot: null, falhou: false };
|
|
357
374
|
X.iniciar = () => {
|
|
358
375
|
if (X.tentou || !window.SplineApp) return; X.tentou = true;
|
|
359
376
|
let acabou = false;
|
|
360
|
-
const cai = (why) => { if (acabou) return; acabou = true; try { X.app && X.app.stop(); } catch (e) {} X.ok = false; post({ acao: 'erro', arg: 'spline: ' + why + ' (fallback RobotExpressive)' }); classeTela(); };
|
|
377
|
+
const cai = (why) => { if (acabou) return; acabou = true; try { X.app && X.app.stop(); } catch (e) {} X.ok = false; X.falhou = true; post({ acao: 'erro', arg: 'spline: ' + why + ' (fallback RobotExpressive)' }); classeTela(); };
|
|
361
378
|
const tempo = setTimeout(() => cai('mais de 6 s'), 6000);
|
|
379
|
+
X.cancela = () => { acabou = true; clearTimeout(tempo); X.falhou = true; try { X.app && X.app.stop(); } catch (e) {} };
|
|
362
380
|
try {
|
|
363
381
|
const app = new window.SplineApp($('robo-spline')); X.app = app;
|
|
364
382
|
app.load(CENA).then(() => {
|
|
@@ -372,6 +390,7 @@ const SP = (function () {
|
|
|
372
390
|
} catch (e) { clearTimeout(tempo); cai(String(e)); }
|
|
373
391
|
};
|
|
374
392
|
// leve: janela oculta/minimizada = para o render
|
|
393
|
+
setTimeout(() => X.iniciar(), 0); // já na carga da página, antes de a ponte do Python falar
|
|
375
394
|
document.addEventListener('visibilitychange', () => { if (!X.app || !X.ok) return; if (document.hidden) X.app.stop(); else X.app.play(); });
|
|
376
395
|
// falando: o robô respira no ritmo da voz (escala do objeto "Bot" da cena) + brilho ciano pelo CSS (--nv)
|
|
377
396
|
X.quadro = (agora) => {
|
|
@@ -427,7 +446,7 @@ function desenharHoje() {
|
|
|
427
446
|
function desenharClima(c) {
|
|
428
447
|
if (!c || c.temp === undefined) return;
|
|
429
448
|
$('c-cidade').textContent = c.cidade || '';
|
|
430
|
-
$('c-temp')
|
|
449
|
+
rolar($('c-temp'), c.temp, '°');
|
|
431
450
|
$('c-cond').textContent = c.icone + ' ' + c.cond;
|
|
432
451
|
$('c-mm').textContent = 'Máx. ' + c.max + '° · Mín. ' + c.min + '°';
|
|
433
452
|
$('c-dias').innerHTML = (c.dias || []).slice(0, 4).map((d) => {
|
|
@@ -444,6 +463,7 @@ $('m-play').innerHTML = ICO_PLAY;
|
|
|
444
463
|
function desenharMusica(m) {
|
|
445
464
|
S.musica = m && m.faixa ? m : null; S.musicaT = Date.now();
|
|
446
465
|
if (!S.musica) { $('m-faixa').textContent = 'Nada tocando'; $('m-artista').textContent = 'abra o Spotify ou a Música'; $('disco').classList.remove('toca'); $('capa').style.backgroundImage = ''; $('m-dur').textContent = '0:00'; $('m-play').innerHTML = ICO_PLAY; return; }
|
|
466
|
+
if (m.faixa !== S.faixaAnt) { S.faixaAnt = m.faixa; for (const e of [$('capa'), document.querySelector('.faixa')]) { e.classList.remove('troca'); void e.offsetWidth; e.classList.add('troca'); } }
|
|
447
467
|
$('m-faixa').textContent = m.faixa; $('m-artista').textContent = m.artista; $('m-dur').textContent = mmss(m.dur);
|
|
448
468
|
const tocando = m.estado === 'playing';
|
|
449
469
|
$('disco').classList.toggle('toca', tocando);
|
|
@@ -469,17 +489,23 @@ document.querySelectorAll('.ctrl button').forEach((b) => b.onclick = (e) => {
|
|
|
469
489
|
const fmtBytes = (b) => b >= 1e6 ? (b / 1e6).toFixed(1) + ' MB/s' : Math.round(b / 1e3) + ' KB/s';
|
|
470
490
|
const hist = { down: Array(60).fill(0), up: Array(60).fill(0) };
|
|
471
491
|
function caminho(a, max) { return a.map((v, i) => (i ? 'L' : 'M') + (i * 300 / (a.length - 1)).toFixed(1) + ' ' + (40 - 36 * Math.min(1, v / max)).toFixed(1)).join(' '); }
|
|
472
|
-
|
|
492
|
+
const RM = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
493
|
+
/* número que rola até o valor (60 fps, ease-out); sem animação se a pessoa pediu menos movimento */
|
|
494
|
+
function rolar(el, alvo, suf) {
|
|
495
|
+
suf = suf || ''; alvo = Number(alvo); const de = el._n === undefined ? 0 : el._n; el._n = alvo;
|
|
496
|
+
cancelAnimationFrame(el._r);
|
|
497
|
+
if (RM || de === alvo || isNaN(alvo)) { el.textContent = alvo + suf; return; }
|
|
498
|
+
const t0 = performance.now();
|
|
499
|
+
const f = (t) => { const k = Math.min(1, (t - t0) / 800), e = 1 - Math.pow(1 - k, 3); el.textContent = Math.round(de + (alvo - de) * e) + suf; if (k < 1) el._r = requestAnimationFrame(f); };
|
|
500
|
+
el._r = requestAnimationFrame(f);
|
|
501
|
+
}
|
|
502
|
+
function anel(id, pct, texto, cor) { const e = $(id); e.style.setProperty('--v', Math.max(0, Math.min(100, pct))); const m = String(texto).match(/^(-?\d+)(.*)$/); if (m) rolar(e.firstChild, m[1], m[2]); else e.firstChild.textContent = texto; if (cor) e.style.setProperty('--c', cor); }
|
|
473
503
|
function desenharMaquina(m) {
|
|
474
504
|
anel('a-cpu', m.cpu, m.cpu + '%'); $('s-cpu').textContent = m.nucleos + ' núcleos';
|
|
475
505
|
if (m.mem && m.mem.total_gb) { anel('a-mem', m.mem.pct, m.mem.pct + '%'); $('s-mem').textContent = String(m.mem.usada_gb).replace('.', ',') + ' de ' + m.mem.total_gb + ' GB'; }
|
|
476
506
|
if (m.disco && m.disco.livre_gb !== undefined) { anel('a-disco', m.disco.pct, m.disco.pct + '%'); $('s-disco').textContent = m.disco.livre_gb + ' GB livres'; }
|
|
477
507
|
if (m.bat && m.bat.pct !== undefined) { anel('a-bat', m.bat.pct, String(m.bat.pct), m.bat.pct <= 20 && !m.bat.tomada ? 'var(--erro)' : 'var(--ok)'); $('s-bat').textContent = m.bat.tomada ? 'na tomada' : 'na bateria'; }
|
|
478
508
|
else { $('s-bat').textContent = 'sem bateria'; }
|
|
479
|
-
hist.down.push(m.down || 0); hist.down.shift(); hist.up.push(m.up || 0); hist.up.shift();
|
|
480
|
-
const mx = Math.max(2e4, ...hist.down, ...hist.up);
|
|
481
|
-
$('r-down').setAttribute('d', caminho(hist.down, mx)); $('r-up').setAttribute('d', caminho(hist.up, mx));
|
|
482
|
-
$('r-txt').textContent = '↓ ' + fmtBytes(m.down || 0) + ' · ↑ ' + fmtBytes(m.up || 0);
|
|
483
509
|
}
|
|
484
510
|
|
|
485
511
|
/* ── barras da voz: o histórico rola; ciano = a pessoa falou, verde = o Primo falou ── */
|
|
@@ -495,7 +521,11 @@ function empurraBarra() {
|
|
|
495
521
|
}
|
|
496
522
|
|
|
497
523
|
/* ── o robô ── */
|
|
498
|
-
function classeTela() {
|
|
524
|
+
function classeTela() {
|
|
525
|
+
// enquanto o Spline carrega: só o brilho (nada de robô). O fallback só entra se o Spline falhou de verdade.
|
|
526
|
+
const f = SP.ok ? ' spline' : !SP.falhou ? ' carregando' : R3.ok ? ' tres-d' : ' svg';
|
|
527
|
+
$('tela').className = 'tela e-' + S.estado + f;
|
|
528
|
+
}
|
|
499
529
|
function setEstado(e) {
|
|
500
530
|
if (!e || e === S.estado) return;
|
|
501
531
|
S.estado = e; classeTela(); $('nome').dataset.e = e;
|
|
@@ -506,6 +536,7 @@ function setEstado(e) {
|
|
|
506
536
|
$('foco').style.opacity = e === 'falando' ? 0 : .9;
|
|
507
537
|
}
|
|
508
538
|
$('centro').onclick = () => post({ acao: 'ptt' });
|
|
539
|
+
$('b-entrar').onclick = (e) => { e.stopPropagation(); post({ acao: 'entrar' }); };
|
|
509
540
|
|
|
510
541
|
/* simples: o visor vira equalizador de 13 barras quando fala; olha de lado quando pensa */
|
|
511
542
|
const EQ = []; (function () { const g = $('eq'); for (let i = 0; i < 13; i++) { const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); r.setAttribute('x', 136 + i * 9.8); r.setAttribute('width', 6); r.setAttribute('rx', 3); r.setAttribute('fill', '#a5f3fc'); g.appendChild(r); EQ.push(r); } })();
|
|
@@ -525,8 +556,10 @@ function laco(t) { SP.quadro(t); if (!R3.ok && !SP.ok) animaEq(t); requestAnimat
|
|
|
525
556
|
|
|
526
557
|
/* ── a ponte com o Python ── */
|
|
527
558
|
window.primo = (m) => {
|
|
559
|
+
if (m.pasta !== undefined) $('pasta').textContent = m.pasta;
|
|
560
|
+
if (m.login !== undefined) $('b-entrar').hidden = !!m.login;
|
|
528
561
|
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome; if (S.estado === 'dormindo') $('estado-t').textContent = 'diga "' + m.nome + '"'; }
|
|
529
|
-
if (m.robo === '
|
|
562
|
+
if (m.robo === 'expressive' && !SP.ok) { if (SP.cancela) SP.cancela(); SP.falhou = true; classeTela(); }
|
|
530
563
|
if (m.evento) R3.evento(m.evento);
|
|
531
564
|
if (m.boot) montar();
|
|
532
565
|
if (m.estado) setEstado(m.estado);
|
|
@@ -539,7 +572,8 @@ window.primo = (m) => {
|
|
|
539
572
|
if (m.maquina) desenharMaquina(m.maquina);
|
|
540
573
|
};
|
|
541
574
|
|
|
542
|
-
|
|
575
|
+
document.querySelectorAll('.cartao').forEach((c, i) => c.style.setProperty('--i', i));
|
|
576
|
+
viraDia(); desenharHoje(); setEstado('dormindo'); classeTela();
|
|
543
577
|
setInterval(() => { viraDia(); andarMusica(); }, 250);
|
|
544
578
|
setInterval(empurraBarra, 50);
|
|
545
579
|
setTimeout(pisca, 3000);
|
package/voz/primo_voz/hud.py
CHANGED
|
@@ -29,9 +29,10 @@ from primo_voz.sons import Sons
|
|
|
29
29
|
AQUI = Path(__file__).resolve().parent
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def main(demo: bool = False) -> int:
|
|
32
|
+
def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None) -> int:
|
|
33
|
+
"""entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout."""
|
|
33
34
|
try:
|
|
34
|
-
from AppKit import (NSApplication,
|
|
35
|
+
from AppKit import (NSApplication, NSApplicationActivationPolicyRegular, NSImage, NSMenu, NSMenuItem, NSBackingStoreBuffered, NSColor,
|
|
35
36
|
NSEvent, NSScreen, NSWindow)
|
|
36
37
|
from Foundation import NSMakeRect, NSObject, NSTimer, NSURL
|
|
37
38
|
import objc
|
|
@@ -44,6 +45,13 @@ def main(demo: bool = False) -> int:
|
|
|
44
45
|
fila: "queue.Queue[str]" = queue.Queue()
|
|
45
46
|
sons = Sons(os.environ.get("PRIMOCODE_SEM_SONS") != "1")
|
|
46
47
|
|
|
48
|
+
def emitir(obj: dict) -> None:
|
|
49
|
+
if saida:
|
|
50
|
+
saida(obj)
|
|
51
|
+
else:
|
|
52
|
+
sys.stdout.write(json.dumps(obj) + "\n")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
|
|
47
55
|
def js(msg: dict) -> None:
|
|
48
56
|
fila.put("window.primo(" + json.dumps(msg, ensure_ascii=False) + ")")
|
|
49
57
|
|
|
@@ -75,6 +83,22 @@ def main(demo: bool = False) -> int:
|
|
|
75
83
|
else:
|
|
76
84
|
self.jan.orderOut_(None)
|
|
77
85
|
|
|
86
|
+
class Dono(NSObject):
|
|
87
|
+
def windowShouldClose_(self, _): # fechar o painel só esconde; o PrimoCode segue
|
|
88
|
+
p.visivel = False
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
def applicationWillTerminate_(self, _n):
|
|
92
|
+
if ao_sair:
|
|
93
|
+
try:
|
|
94
|
+
ao_sair()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def applicationShouldHandleReopen_hasVisibleWindows_(self, _a, _v): # clique no Dock traz de volta
|
|
99
|
+
p.visivel = True
|
|
100
|
+
return True
|
|
101
|
+
|
|
78
102
|
class Msg(NSObject, protocols=[objc.protocolNamed("WKScriptMessageHandler")]):
|
|
79
103
|
def userContentController_didReceiveScriptMessage_(self, ucc, msg):
|
|
80
104
|
try:
|
|
@@ -87,9 +111,8 @@ def main(demo: bool = False) -> int:
|
|
|
87
111
|
sys.stderr.flush()
|
|
88
112
|
elif a == "sair":
|
|
89
113
|
p.visivel = False
|
|
90
|
-
elif a
|
|
91
|
-
|
|
92
|
-
sys.stdout.flush()
|
|
114
|
+
elif a in ("ptt", "entrar"):
|
|
115
|
+
emitir({"acao": a})
|
|
93
116
|
elif a == "print": # esconde o HUD para o print pegar a tela de verdade
|
|
94
117
|
p.visivel = False
|
|
95
118
|
threading.Timer(0.6, lambda: pn.executar_acao(m)).start()
|
|
@@ -105,7 +128,26 @@ def main(demo: bool = False) -> int:
|
|
|
105
128
|
threading.Thread(target=_fazer, daemon=True).start()
|
|
106
129
|
|
|
107
130
|
app = NSApplication.sharedApplication()
|
|
108
|
-
app.setActivationPolicy_(
|
|
131
|
+
app.setActivationPolicy_(NSApplicationActivationPolicyRegular) # app de verdade: Dock e Cmd+Tab
|
|
132
|
+
try: # nome e ícone do Primo, mesmo fora do bundle
|
|
133
|
+
from AppKit import NSBundle
|
|
134
|
+
info = NSBundle.mainBundle().infoDictionary()
|
|
135
|
+
info["CFBundleName"] = info["CFBundleDisplayName"] = "Primo"
|
|
136
|
+
icone = AQUI / "vendor" / "primo-icone.png"
|
|
137
|
+
if icone.exists():
|
|
138
|
+
app.setApplicationIconImage_(NSImage.alloc().initWithContentsOfFile_(str(icone)))
|
|
139
|
+
except Exception as e:
|
|
140
|
+
sys.stderr.write(f"hud: ícone: {e}\n")
|
|
141
|
+
barra = NSMenu.alloc().init()
|
|
142
|
+
raiz = NSMenuItem.alloc().init()
|
|
143
|
+
barra.addItem_(raiz)
|
|
144
|
+
menu = NSMenu.alloc().init()
|
|
145
|
+
for rot, sel, tec in (("Sobre o Primo", "orderFrontStandardAboutPanel:", ""), (None, None, None),
|
|
146
|
+
("Esconder o Primo", "hide:", "h"), (None, None, None), ("Sair do Primo", "terminate:", "q")):
|
|
147
|
+
menu.addItem_(NSMenuItem.separatorItem() if rot is None else
|
|
148
|
+
NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(rot, sel, tec))
|
|
149
|
+
raiz.setSubmenu_(menu)
|
|
150
|
+
app.setMainMenu_(barra)
|
|
109
151
|
mouse = NSEvent.mouseLocation()
|
|
110
152
|
alvo_tela = next((s for s in NSScreen.screens() if s.frame().origin.x <= mouse.x < s.frame().origin.x + s.frame().size.width
|
|
111
153
|
and s.frame().origin.y <= mouse.y < s.frame().origin.y + s.frame().size.height), NSScreen.mainScreen())
|
|
@@ -115,6 +157,10 @@ def main(demo: bool = False) -> int:
|
|
|
115
157
|
jan.setTitlebarAppearsTransparent_(True)
|
|
116
158
|
jan.setTitleVisibility_(1) # sem texto de título
|
|
117
159
|
jan.setTitle_("Primo")
|
|
160
|
+
jan.setReleasedWhenClosed_(False)
|
|
161
|
+
dono = Dono.alloc().init()
|
|
162
|
+
jan.setDelegate_(dono)
|
|
163
|
+
app.setDelegate_(dono)
|
|
118
164
|
jan.setMinSize_((900, 600))
|
|
119
165
|
jan.setOpaque_(True)
|
|
120
166
|
jan.setBackgroundColor_(NSColor.colorWithSRGBRed_green_blue_alpha_(0.043, 0.043, 0.039, 1.0))
|
|
@@ -176,7 +222,7 @@ def main(demo: bool = False) -> int:
|
|
|
176
222
|
k = 0
|
|
177
223
|
while True:
|
|
178
224
|
try:
|
|
179
|
-
if k % 12 == 0:
|
|
225
|
+
if k % 12 == 0 or evs["permissao"] != "ok": # sem acesso, olha a cada 5 s (a pessoa pode ter liberado)
|
|
180
226
|
evs = td.eventos_do_mes()
|
|
181
227
|
js({"calendario": {"permissao": evs["permissao"], "dias": sorted({e["dia"] for e in evs["eventos"]})}})
|
|
182
228
|
if evs["permissao"] == "ok":
|
|
@@ -188,7 +234,7 @@ def main(demo: bool = False) -> int:
|
|
|
188
234
|
threading.Thread(target=agenda, daemon=True).start()
|
|
189
235
|
|
|
190
236
|
def ler():
|
|
191
|
-
for l in sys.stdin:
|
|
237
|
+
for l in (entrada if entrada is not None else sys.stdin):
|
|
192
238
|
try:
|
|
193
239
|
m = json.loads(l)
|
|
194
240
|
except ValueError:
|
|
@@ -197,7 +243,8 @@ def main(demo: bool = False) -> int:
|
|
|
197
243
|
js(m)
|
|
198
244
|
if "estado" in m or "nivel" in m:
|
|
199
245
|
js({"barras": td.barras(p.estado, p.nivel)})
|
|
200
|
-
|
|
246
|
+
if entrada is None:
|
|
247
|
+
os._exit(0)
|
|
201
248
|
|
|
202
249
|
def encenar():
|
|
203
250
|
roteiro = [("dormindo", "", "", 3), ("ouvindo", "roda os testes do projeto", "", 4),
|
|
@@ -214,8 +261,12 @@ def main(demo: bool = False) -> int:
|
|
|
214
261
|
time.sleep(0.06)
|
|
215
262
|
if est == "falando":
|
|
216
263
|
js({"evento": "sucesso"})
|
|
217
|
-
if
|
|
264
|
+
if entrada is not None:
|
|
265
|
+
threading.Thread(target=ler, daemon=True).start()
|
|
266
|
+
elif "--livre" not in sys.argv:
|
|
218
267
|
threading.Thread(target=encenar if demo else ler, daemon=True).start()
|
|
268
|
+
if ao_iniciar:
|
|
269
|
+
ao_iniciar(js, p)
|
|
219
270
|
app.run()
|
|
220
271
|
return 0
|
|
221
272
|
|
|
@@ -16,7 +16,7 @@ import re
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Optional
|
|
18
18
|
|
|
19
|
-
PADRAO = {"nome": "Primo", "pessoa": "", "luz": True, "sons": True, "robo": "spline"}
|
|
19
|
+
PADRAO = {"nome": "Primo", "pessoa": "", "luz": True, "sons": True, "robo": "spline", "voz": "francisca", "tts": "edge"}
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def arquivo() -> Path:
|
package/voz/primo_voz/tela.py
CHANGED
|
@@ -116,68 +116,52 @@ def calendario_permissao() -> str:
|
|
|
116
116
|
from EventKit import EKEventStore
|
|
117
117
|
except ImportError:
|
|
118
118
|
return "sem"
|
|
119
|
-
st = EKEventStore.authorizationStatusForEntityType_(0)
|
|
120
|
-
|
|
119
|
+
st = int(EKEventStore.authorizationStatusForEntityType_(0))
|
|
120
|
+
# 4 = só escrita (um pedido antigo concedeu menos do que o Primo precisa): o macOS não deixa "subir" por código,
|
|
121
|
+
# então tratamos como pendente e o pedido zera a decisão do próprio app antes de pedir o acesso completo.
|
|
122
|
+
return {3: "ok", 0: "pendente", 4: "pendente", 1: "negada", 2: "negada"}.get(st, "negada")
|
|
121
123
|
|
|
122
124
|
|
|
123
125
|
def pedir_calendario() -> None:
|
|
124
|
-
"""Dispara o pedido do macOS (uma vez). Não bloqueia."""
|
|
126
|
+
"""Dispara o pedido do macOS (uma vez). Não bloqueia. O resultado vai para o log (stderr)."""
|
|
127
|
+
if time.time() - _ek.get("pedindo", 0) < 120: # o aviso do macOS já está na tela esperando resposta
|
|
128
|
+
return
|
|
129
|
+
_ek["pedindo"] = time.time()
|
|
130
|
+
|
|
131
|
+
def fim(ok, err):
|
|
132
|
+
_ek["pedindo"] = 0
|
|
133
|
+
sys.stderr.write(f"calendario: pedido respondido ok={bool(ok)} erro={err} status={calendario_permissao()}\n")
|
|
134
|
+
sys.stderr.flush()
|
|
125
135
|
try:
|
|
136
|
+
from EventKit import EKEventStore
|
|
137
|
+
if int(EKEventStore.authorizationStatusForEntityType_(0)) == 4:
|
|
138
|
+
r = subprocess.run(["tccutil", "reset", "Calendar", "ai.primocode.primo"], capture_output=True, text=True)
|
|
139
|
+
sys.stderr.write(f"calendario: acesso só de escrita; zerando a decisão do Primo ({r.returncode})\n")
|
|
140
|
+
_ek.pop("s", None)
|
|
126
141
|
s = _store()
|
|
142
|
+
sys.stderr.write(f"calendario: pedindo acesso (status antes={calendario_permissao()}, bundle={_bundle()})\n")
|
|
143
|
+
sys.stderr.flush()
|
|
127
144
|
if hasattr(s, "requestFullAccessToEventsWithCompletion_"):
|
|
128
|
-
s.requestFullAccessToEventsWithCompletion_(
|
|
145
|
+
s.requestFullAccessToEventsWithCompletion_(fim)
|
|
129
146
|
else:
|
|
130
|
-
s.requestAccessToEntityType_completion_(0,
|
|
131
|
-
except Exception:
|
|
132
|
-
|
|
147
|
+
s.requestAccessToEntityType_completion_(0, fim)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
sys.stderr.write(f"calendario: falhou ao pedir: {e}\n")
|
|
133
150
|
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
set out to ""
|
|
142
|
-
tell application "Calendar"
|
|
143
|
-
repeat with c in calendars
|
|
144
|
-
repeat with e in (every event of c whose start date >= d1 and start date < d2)
|
|
145
|
-
set out to out & (summary of e) & "|||" & ((start date of e) - agora) & "|||" & ((end date of e) - agora) & "|||" & (allday event of e) & linefeed
|
|
146
|
-
end repeat
|
|
147
|
-
end repeat
|
|
148
|
-
end tell
|
|
149
|
-
return out'''
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _eventos_osascript(agora: float) -> dict:
|
|
153
|
-
"""Plano B quando o EventKit não consegue pedir a permissão (Python sem pacote de app): o app Calendário via osascript."""
|
|
154
|
-
r = subprocess.run(["osascript", "-e", _OSA_CAL], capture_output=True, text=True, timeout=25)
|
|
155
|
-
if r.returncode != 0:
|
|
156
|
-
return {"permissao": "negada" if "-1743" in r.stderr else "pendente", "eventos": []}
|
|
157
|
-
out = []
|
|
158
|
-
for l in r.stdout.splitlines():
|
|
159
|
-
p = l.split("|||")
|
|
160
|
-
if len(p) == 4:
|
|
161
|
-
try:
|
|
162
|
-
a, b = agora + float(p[1]), agora + float(p[2])
|
|
163
|
-
except ValueError:
|
|
164
|
-
continue
|
|
165
|
-
out.append({"titulo": p[0] or "(sem título)", "inicio": a, "fim": b, "dia_todo": p[3].strip() == "true",
|
|
166
|
-
"dia": time.localtime(a).tm_mday})
|
|
167
|
-
out.sort(key=lambda e: e["inicio"])
|
|
168
|
-
return {"permissao": "ok", "eventos": out}
|
|
152
|
+
def _bundle() -> str:
|
|
153
|
+
try:
|
|
154
|
+
from Foundation import NSBundle
|
|
155
|
+
return str(NSBundle.mainBundle().bundleIdentifier())
|
|
156
|
+
except Exception:
|
|
157
|
+
return "?"
|
|
169
158
|
|
|
170
159
|
|
|
171
160
|
def eventos_do_mes(agora: Optional[float] = None) -> dict:
|
|
172
161
|
"""{'permissao','eventos':[{titulo,inicio,fim,dia_todo,dia}]} do mês inteiro (hoje e dias com ponto)."""
|
|
173
162
|
perm = calendario_permissao()
|
|
174
|
-
if perm
|
|
175
|
-
|
|
176
|
-
pedir_calendario()
|
|
177
|
-
try:
|
|
178
|
-
return _eventos_osascript(agora or time.time())
|
|
179
|
-
except Exception:
|
|
180
|
-
return {"permissao": perm, "eventos": []}
|
|
163
|
+
if perm == "pendente":
|
|
164
|
+
pedir_calendario()
|
|
181
165
|
if perm != "ok":
|
|
182
166
|
return {"permissao": perm, "eventos": []}
|
|
183
167
|
from Foundation import NSDate
|
|
@@ -327,11 +311,9 @@ def acao(m: dict) -> bool:
|
|
|
327
311
|
"""Ações da tela que só ela entende. True = tratada."""
|
|
328
312
|
a, arg = m.get("acao"), str(m.get("arg") or "")
|
|
329
313
|
if a == "calendario":
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if r["permissao"] != "ok":
|
|
335
|
-
subprocess.Popen(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation"])
|
|
314
|
+
if calendario_permissao() == "pendente":
|
|
315
|
+
pedir_calendario() # o pedido do macOS sai com o nome do app (Primo)
|
|
316
|
+
else:
|
|
317
|
+
subprocess.Popen(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"])
|
|
336
318
|
return True
|
|
337
319
|
return False
|
|
@@ -27,7 +27,7 @@ import threading
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from typing import Optional
|
|
29
29
|
|
|
30
|
-
MODELO = os.environ.get("PRIMOCODE_WHISPER_MODELO", "
|
|
30
|
+
MODELO = os.environ.get("PRIMOCODE_WHISPER_MODELO", "base").strip() or "base"
|
|
31
31
|
PASTA = Path.home() / ".primocode" / "modelos" / "whisper"
|
|
32
32
|
|
|
33
33
|
_modelo = None
|
|
@@ -69,6 +69,8 @@ def aquecer() -> None:
|
|
|
69
69
|
def _ir():
|
|
70
70
|
try:
|
|
71
71
|
carregar()
|
|
72
|
+
import numpy as np # uma passada em silêncio: a primeira frase de verdade já não paga o aquecimento
|
|
73
|
+
list(_modelo.transcribe(np.zeros(16000, dtype=np.float32), language="pt", beam_size=1)[0])
|
|
72
74
|
except Exception:
|
|
73
75
|
pass
|
|
74
76
|
threading.Thread(target=_ir, daemon=True).start()
|
|
@@ -112,9 +114,18 @@ def aproveitavel(segmento) -> bool:
|
|
|
112
114
|
|
|
113
115
|
def transcrever(wav: Path, idioma: str = "pt") -> str:
|
|
114
116
|
modelo = carregar()
|
|
117
|
+
entrada: object = str(wav)
|
|
118
|
+
try: # WAV 16 kHz mono: entrega o áudio já decodificado (não depende do `av`, que às vezes destoa do faster-whisper)
|
|
119
|
+
import wave
|
|
120
|
+
import numpy as np
|
|
121
|
+
with wave.open(str(wav), "rb") as w:
|
|
122
|
+
if w.getframerate() == 16000 and w.getnchannels() == 1 and w.getsampwidth() == 2:
|
|
123
|
+
entrada = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
|
|
124
|
+
except Exception:
|
|
125
|
+
entrada = str(wav)
|
|
115
126
|
# vad_filter corta o silêncio das pontas: sem ele, o Whisper às vezes
|
|
116
127
|
# "ouve" uma frase inteira inventada no silêncio ("Obrigado por assistir").
|
|
117
|
-
segmentos, _ = modelo.transcribe(
|
|
128
|
+
segmentos, _ = modelo.transcribe(entrada, language=idioma, beam_size=1,
|
|
118
129
|
vad_filter=True, condition_on_previous_text=False)
|
|
119
130
|
return limpar(" ".join(s.text.strip() for s in segmentos if aproveitavel(s)))
|
|
120
131
|
|
|
Binary file
|