primocode 10.4.3 → 10.4.7
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 +1 -1
- package/lib/catalogo.js +1 -1
- package/lib/claude-engine.js +1 -1
- package/lib/entrada.js +4 -0
- package/lib/estudio/batida.js +95 -10
- package/lib/estudio/gerativo.js +173 -29
- package/lib/estudio/index.js +53 -5
- package/lib/estudio/kit/gerativo.jsx +42 -4
- package/lib/estudio/kit/tres.jsx +156 -0
- package/lib/local/motor-cli.js +14 -11
- package/lib/tela.js +3 -1
- package/lib/voz.js +2 -0
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +60 -0
- package/voz/primo_voz/orbe.py +444 -0
package/lib/estudio/index.js
CHANGED
|
@@ -896,6 +896,32 @@ function alinharPartes(lista, palavras, total) {
|
|
|
896
896
|
});
|
|
897
897
|
}
|
|
898
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Onde o roteiro COMEÇA na fala transcrita, em segundos — 0 se começa logo.
|
|
901
|
+
*
|
|
902
|
+
* Dois jeitos de o Gemini vazar a direção, os dois vistos no vídeo do Joel
|
|
903
|
+
* (10/10): falar "Rápido e contínuo." ANTES do roteiro, e falar NO LUGAR da
|
|
904
|
+
* primeira frase ("Rápido e contínuo. Você pergunta…", sem o "IA é pra tirar
|
|
905
|
+
* dúvida"). Então: acha o primeiro par de palavras seguidas que existe no
|
|
906
|
+
* roteiro (não só o par do começo), e corta o que veio antes — mas só se o que
|
|
907
|
+
* veio antes for, na maior parte, palavra da DIREÇÃO. Fala do roteiro que o
|
|
908
|
+
* transcritor errou não é vazamento e não sai.
|
|
909
|
+
*/
|
|
910
|
+
function vazamentoNoComeco(lista, palavras, direcao = '') {
|
|
911
|
+
const norm = (t) => String(t || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
|
|
912
|
+
const roteiro = norm(lista.slice(0, 2).map((x) => x.texto).join(' ')).split(' ').filter(Boolean);
|
|
913
|
+
if (roteiro.length < 2 || !palavras.length) return 0;
|
|
914
|
+
const pares = new Set(roteiro.slice(0, -1).map((w, i) => w + ' ' + roteiro[i + 1]));
|
|
915
|
+
const ditas = palavras.slice(0, 14).map((w) => norm(w.texto));
|
|
916
|
+
let i = 0;
|
|
917
|
+
while (i < ditas.length - 1 && !pares.has(ditas[i] + ' ' + ditas[i + 1])) i++;
|
|
918
|
+
if (i === 0 || i >= ditas.length - 1) return 0;
|
|
919
|
+
const daDirecao = new Set(norm(direcao).split(' ').filter(Boolean));
|
|
920
|
+
const antes = ditas.slice(0, i).join(' ').split(' ').filter(Boolean);
|
|
921
|
+
const vazou = !direcao || antes.filter((w) => daDirecao.has(w)).length / antes.length >= 0.6;
|
|
922
|
+
return vazou ? Math.max(0, palavras[i].de - 0.08) : 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
899
925
|
async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
900
926
|
try {
|
|
901
927
|
const cfg = require('../config').loadConfig();
|
|
@@ -931,9 +957,25 @@ async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
|
931
957
|
const chars = lista.reduce((n, x) => n + x.texto.length, 0);
|
|
932
958
|
if (med.segundos < chars / 22) return { ok: false, error: `a voz veio cortada (${med.segundos.toFixed(1)} s para ${chars} caracteres)` };
|
|
933
959
|
const tr = await api.requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
934
|
-
|
|
960
|
+
let palavras = (tr && tr.palavras) || [];
|
|
935
961
|
if (!palavras.length) return { ok: false, error: 'sem a transcrição da voz para achar as partes' };
|
|
936
|
-
|
|
962
|
+
let segundos = med.segundos;
|
|
963
|
+
/* A DIREÇÃO LIDA EM VOZ ALTA. O servidor manda a direção ("leia RÁPIDO e
|
|
964
|
+
contínuo…") e o texto no mesmo pedido, e o Gemini às vezes fala o
|
|
965
|
+
começo dela: o vídeo do Joel abria com "Rápido e contínuo." antes do
|
|
966
|
+
roteiro (10/10). Se o roteiro só começa depois de algumas palavras,
|
|
967
|
+
o que vem antes é vazamento — e sai do áudio. */
|
|
968
|
+
const corte = vazamentoNoComeco(lista, palavras, direcao);
|
|
969
|
+
if (corte > 0.15) {
|
|
970
|
+
const cortado = mp3.replace(/\.mp3$/, '-c.mp3');
|
|
971
|
+
const k = await video.rodar(video.ffmpeg(), ['-y', '-ss', corte.toFixed(3), '-i', mp3, '-c:a', 'libmp3lame', '-q:a', '3', cortado], { timeout: 120000 });
|
|
972
|
+
if (k.ok && fs.existsSync(cortado)) {
|
|
973
|
+
fs.renameSync(cortado, mp3);
|
|
974
|
+
palavras = palavras.filter((w) => w.de >= corte - 0.02).map((w) => ({ ...w, de: Math.max(0, w.de - corte), ate: Math.max(0, w.ate - corte) }));
|
|
975
|
+
segundos = Math.max(0, segundos - corte);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return { ok: true, voz: `gemini ${r.voz}`, segundos, partes: alinharPartes(lista, palavras, segundos), ...(corte > 0.15 ? { cortouDirecao: corte } : {}) };
|
|
937
979
|
} catch (e) {
|
|
938
980
|
return { ok: false, error: e.message };
|
|
939
981
|
}
|
|
@@ -1011,7 +1053,10 @@ async function prepararDepoimento(p, c) {
|
|
|
1011
1053
|
|
|
1012
1054
|
/* A narração com cache (a mesma fala, na mesma direção, não é gerada duas vezes). */
|
|
1013
1055
|
async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
1014
|
-
|
|
1056
|
+
/* 'v2' (10/10): as narrações guardadas antes do corte da direção lida em
|
|
1057
|
+
voz alta ("Rápido e contínuo.") ficam para trás — reaproveitar uma delas
|
|
1058
|
+
trazia o vazamento de volta mesmo com o conserto. */
|
|
1059
|
+
const chave = require('crypto').createHash('sha1').update(JSON.stringify(['v2', lista.map((x) => x.texto), voz || '', direcao || ''])).digest('hex').slice(0, 16);
|
|
1015
1060
|
const dir = path.join(os.homedir(), '.primocode', 'studio', 'cache-voz');
|
|
1016
1061
|
const cm = path.join(dir, chave + '.mp3'), cj = path.join(dir, chave + '.json');
|
|
1017
1062
|
if (fs.existsSync(cm) && fs.existsSync(cj)) { fs.copyFileSync(cm, mp3); return { ok: true, ...JSON.parse(fs.readFileSync(cj, 'utf8')) }; }
|
|
@@ -1497,7 +1542,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1497
1542
|
/* O gerativo pede voz com CORPO ("narração encorpada, extravagante",
|
|
1498
1543
|
30/09): cheia, quente, perto do microfone — e o ritmo segue 1,12×. */
|
|
1499
1544
|
/* (A primeira versão pedia "trailer" e "ênfase sustentada": a voz saiu 25% mais lenta, medido em 30/09.) */
|
|
1500
|
-
|
|
1545
|
+
/* Sem a ênfase em caixa alta logo no começo ("rápido" gritado): era
|
|
1546
|
+
exatamente o trecho que o Gemini lia em voz alta (10/10). */
|
|
1547
|
+
? 'Leia em português do Brasil como locutor de reels de tecnologia, num ritmo rápido e sem pausas, com voz cheia, quente e encorpada, '
|
|
1501
1548
|
+ 'bem perto do microfone, muita energia e sorriso na voz. Ataque firme e ênfase curta e forte nas palavras de impacto; '
|
|
1502
1549
|
+ 'quase sem pausa entre as frases. Nunca arraste, nunca leia devagar nem solene.'
|
|
1503
1550
|
// O fluido é o vídeo de produto: voz limpa e segura, e rápida ("mais rápido", 06/10).
|
|
@@ -2042,7 +2089,8 @@ async function ferramentaSite(args) {
|
|
|
2042
2089
|
return r;
|
|
2043
2090
|
}
|
|
2044
2091
|
|
|
2045
|
-
module.exports = {
|
|
2092
|
+
module.exports = {
|
|
2093
|
+
vazamentoNoComeco, variarIcones,
|
|
2046
2094
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
2047
2095
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
2048
2096
|
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
15
15
|
import { delayRender, continueRender } from 'remotion';
|
|
16
16
|
import { SNAPPY, CURVAS_KIT, acaso, suave, alfa, mistura } from './base';
|
|
17
|
-
import { Produto3D, Icone3D } from './tres';
|
|
17
|
+
import { Produto3D, Icone3D, Heroi3D } from './tres';
|
|
18
18
|
|
|
19
19
|
const ed = CURVAS_KIT.editorial, curvaCam = CURVAS_KIT.camera, arrasto = CURVAS_KIT.arrasto, acelera = CURVAS_KIT.acelera;
|
|
20
20
|
const c01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
@@ -193,7 +193,9 @@ function leiaute(P, med) {
|
|
|
193
193
|
if (!l.encher || !med[l.id]) continue;
|
|
194
194
|
const k = l.encher + (l.coluna != null ? l.coluna : '');
|
|
195
195
|
// O grid manda: a tese ocupa até ~74% da largura do bloco; só a sangria vai a 97%.
|
|
196
|
-
|
|
196
|
+
/* (10/10) A tese gigante, a parede e a marca ENCHEM o bloco (97%): com fonte
|
|
197
|
+
larga, os 74% de antes paravam "CLAUDE CODE" em 9% da altura. O resto, 80%. */
|
|
198
|
+
const s = Math.min(l.max || l.tamanho, (bl.largura * (b.cheio || ['gigante', 'parede', 'marca'].includes(b.comp) ? 0.97 : 0.8)) / med[l.id].w);
|
|
197
199
|
grupo[k] = grupo[k] == null ? s : Math.min(grupo[k], s);
|
|
198
200
|
}
|
|
199
201
|
const tam = (l) => {
|
|
@@ -950,9 +952,24 @@ export function Gerativo({ el, t, fps }) {
|
|
|
950
952
|
e do texto — e calmo: desenhado por cima, brigava com os dois (05/10). */}
|
|
951
953
|
{noAr.filter((x) => x.heroi && x.heroi.modo === 'campo' && L[x.id]).map((x) => {
|
|
952
954
|
const bl = L[x.id].bloco, some = 1 - c01((t - x.ate) / 0.14);
|
|
955
|
+
let campo3d = null;
|
|
956
|
+
if (x.heroi.tres) {
|
|
957
|
+
/* As colunas NUNCA encostam no texto: só existem numa faixa abaixo
|
|
958
|
+
(ou acima, de ponta-cabeça) do bloco MEDIDO, com folga. O campo mora
|
|
959
|
+
mais fundo que o texto (translateZ -200), então na tela ele encolhe
|
|
960
|
+
para o centro mais que o texto: a folga medida aqui só cresce lá. */
|
|
961
|
+
const folga = Math.max(x.vao || 0, 0.07 * H);
|
|
962
|
+
const ds = DUR_SAIDA[x.saida] || 0;
|
|
963
|
+
const saiDe = x.ate - ds - 0.15;
|
|
964
|
+
if (H - (bl.y1 + folga) >= 0.34 * H) campo3d = { op: 0.7, faixa: { y0: bl.y1 + folga, y1: H }, saiDe };
|
|
965
|
+
else if (bl.y - folga >= 0.34 * H) campo3d = { op: 0.7, faixa: { y0: 0, y1: bl.y - folga }, teto: true, saiDe };
|
|
966
|
+
if (!campo3d) return null; // sem faixa livre: sem colunas
|
|
967
|
+
}
|
|
953
968
|
return (
|
|
954
969
|
<div key={'campo' + x.id} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, transform: 'translateZ(-200px)', opacity: some }}>
|
|
955
|
-
|
|
970
|
+
{x.heroi.tres
|
|
971
|
+
? <Heroi3D h={x.heroi} t={t} de={x.de} ate={x.ate} W={W} H={H} slot={campo3d} />
|
|
972
|
+
: <Heroi b={x} t={t} P={P} slot={{ cx: (bl.x + bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R: Math.min(W, H) * 0.32, op: 0.18 }} />}
|
|
956
973
|
</div>
|
|
957
974
|
);
|
|
958
975
|
})}
|
|
@@ -998,10 +1015,31 @@ export function Gerativo({ el, t, fps }) {
|
|
|
998
1015
|
// O lugar do herói: o vazio do bloco (como o ícone); sem vazio, atrás do texto e mais fraco.
|
|
999
1016
|
const bl = lay.bloco, M = P.margem;
|
|
1000
1017
|
if (x.heroi.modo === 'campo') return null; // desenhado atrás, antes do produto
|
|
1001
|
-
const
|
|
1018
|
+
const tres = x.heroi.modo === 'objeto3d';
|
|
1019
|
+
// O 3D é GRANDE ("um 3d bem presente"): o raio pede o vazio inteiro.
|
|
1020
|
+
const R = Math.min(W, H) * (tres ? (W >= H ? 0.26 : 0.22) : (W >= H ? 0.2 : 0.17));
|
|
1002
1021
|
let slot = null;
|
|
1003
1022
|
// A área segura (margem.js), com o vão do bloco; partitura antiga: a margem do mundo.
|
|
1004
1023
|
const S = x.seguro || { x0: M, y0: M * 1.4, x1: W - M, y1: H - M * 1.4 }, V = x.vao || 0;
|
|
1024
|
+
if (tres) {
|
|
1025
|
+
/* O 3D NUNCA cruza o texto: mora no maior retângulo livre ao lado do
|
|
1026
|
+
bloco MEDIDO (com o vão e uma folga), dentro da área segura; o raio
|
|
1027
|
+
é o círculo que cabe nele, e o Heroi3D garante que a projeção inteira
|
|
1028
|
+
(anel, tubo, dolly, perspectiva) fica dentro desse raio. Sem
|
|
1029
|
+
retângulo grande o bastante: sem 3D nesta batida. */
|
|
1030
|
+
const folga = Math.max(V, 0.035 * Math.min(W, H));
|
|
1031
|
+
const livres = [
|
|
1032
|
+
{ x0: bl.x1 + folga, y0: S.y0, x1: S.x1, y1: S.y1 },
|
|
1033
|
+
{ x0: S.x0, y0: S.y0, x1: bl.x - folga, y1: S.y1 },
|
|
1034
|
+
{ x0: S.x0, y0: S.y0, x1: S.x1, y1: bl.y - folga },
|
|
1035
|
+
{ x0: S.x0, y0: bl.y1 + folga, x1: S.x1, y1: S.y1 },
|
|
1036
|
+
].map((q) => ({ ...q, r: Math.min(q.x1 - q.x0, q.y1 - q.y0) / 2 })).sort((a, c) => c.r - a.r);
|
|
1037
|
+
const q = livres[0];
|
|
1038
|
+
if (!q || q.r < Math.min(W, H) * 0.12) return null;
|
|
1039
|
+
const ds = DUR_SAIDA[x.saida] || 0;
|
|
1040
|
+
slot = { cx: (q.x0 + q.x1) / 2, cy: (q.y0 + q.y1) / 2, R: Math.min(q.r, Math.min(W, H) * 0.3), saiDe: x.ate - ds - 0.12 };
|
|
1041
|
+
return <Heroi3D h={x.heroi} t={t} de={x.de} ate={x.ate} W={W} H={H} slot={slot} />;
|
|
1042
|
+
}
|
|
1005
1043
|
if (S.x1 - bl.x1 - V >= R * 2.3) slot = { cx: bl.x1 + V + (S.x1 - bl.x1 - V) / 2, cy: (bl.y + bl.y1) / 2, R };
|
|
1006
1044
|
else if (bl.y - V - S.y0 >= R * 2.2) slot = { cx: W / 2, cy: S.y0 + (bl.y - V - S.y0) / 2, R };
|
|
1007
1045
|
else if (S.y1 - bl.y1 - V >= R * 2.2) slot = { cx: W / 2, cy: bl.y1 + V + (S.y1 - bl.y1 - V) / 2, R };
|
package/lib/estudio/kit/tres.jsx
CHANGED
|
@@ -375,4 +375,160 @@ export function Icone3D({ el, t }) {
|
|
|
375
375
|
);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
|
|
379
|
+
/* ── OS HERÓIS EM 3D DE VERDADE (10/10) ───────────────────────────────────
|
|
380
|
+
* "As animações são muito iguais, quero um 3d bem presente" (Joel, com um
|
|
381
|
+
* reel de motion de referência). O herói abstrato era SVG — pontos girando
|
|
382
|
+
* num plano. Estes três são three.js: perspectiva, profundidade, luz e
|
|
383
|
+
* câmera que anda. Grandes: o raio vem do vazio do bloco (gerativo.jsx).
|
|
384
|
+
*
|
|
385
|
+
* globo3d a esfera de pontos e malha, girando inclinada, com um anel
|
|
386
|
+
* de órbita; os pontos de perto maiores (perspectiva real);
|
|
387
|
+
* fita3d o nó que se torce, em material com verniz e a luz de
|
|
388
|
+
* recorte na cor de acento;
|
|
389
|
+
* colunas3d o campo de colunas que sobe em onda, sobrevoado pela câmera
|
|
390
|
+
* e sumindo na névoa da cor do fundo — fica ATRÁS do texto.
|
|
391
|
+
*
|
|
392
|
+
* Canvas do quadro inteiro, transparente: o objeto é posto no lugar do
|
|
393
|
+
* vazio convertendo pixel → mundo (câmera fixa, fov 30, a 12 unidades).
|
|
394
|
+
*/
|
|
395
|
+
const FOV3D = 30, DIST3D = 12;
|
|
396
|
+
const VIS3D = 2 * DIST3D * Math.tan((FOV3D / 2) * Math.PI / 180);
|
|
397
|
+
const mola3d = (x) => { const k = Math.max(0, Math.min(1, x)); return 1 - Math.pow(1 - k, 3) * Math.cos(k * 4.2); };
|
|
398
|
+
|
|
399
|
+
function CameraFixa({ pos, alvo, teto }) {
|
|
400
|
+
const { camera } = useThree();
|
|
401
|
+
// De ponta-cabeça: o campo pende do alto (o texto mora embaixo).
|
|
402
|
+
camera.up.set(0, teto ? -1 : 1, 0);
|
|
403
|
+
camera.position.set(pos[0], pos[1], pos[2]);
|
|
404
|
+
camera.near = 0.1; camera.far = 80;
|
|
405
|
+
camera.lookAt(alvo[0], alvo[1], alvo[2]);
|
|
406
|
+
camera.updateProjectionMatrix();
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function Globo({ r, cor, cor2, sem, tl }) {
|
|
411
|
+
const geo = useMemo(() => {
|
|
412
|
+
const N = 1100, pos = new Float32Array(N * 3), col = new Float32Array(N * 3);
|
|
413
|
+
const c1 = new THREE.Color(cor2), c2 = new THREE.Color(cor);
|
|
414
|
+
const ouro = Math.PI * (3 - Math.sqrt(5));
|
|
415
|
+
for (let i = 0; i < N; i++) {
|
|
416
|
+
const y = 1 - (i / (N - 1)) * 2, rr = Math.sqrt(1 - y * y), a = i * ouro;
|
|
417
|
+
pos.set([Math.cos(a) * rr * r, y * r, Math.sin(a) * rr * r], i * 3);
|
|
418
|
+
const c = (i * 7 + sem) % 6 === 0 ? c2 : c1;
|
|
419
|
+
col.set([c.r, c.g, c.b], i * 3);
|
|
420
|
+
}
|
|
421
|
+
const g = new THREE.BufferGeometry();
|
|
422
|
+
g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
423
|
+
g.setAttribute('color', new THREE.BufferAttribute(col, 3));
|
|
424
|
+
return g;
|
|
425
|
+
}, [r, cor, cor2, sem]);
|
|
426
|
+
return (
|
|
427
|
+
<group rotation={[0.38 + Math.sin(tl * 0.5) * 0.08, tl * 0.55 + sem * 0.01, 0.12]}>
|
|
428
|
+
<points geometry={geo}><pointsMaterial size={r * 0.06} vertexColors sizeAttenuation /></points>
|
|
429
|
+
<mesh><sphereGeometry args={[r * 0.99, 32, 20]} /><meshBasicMaterial color={cor2} wireframe transparent opacity={0.22} /></mesh>
|
|
430
|
+
<mesh rotation={[1.2, 0.3, tl * 0.4]}><torusGeometry args={[r * 1.14, r * 0.008, 8, 160]} /><meshBasicMaterial color={cor} transparent opacity={0.85} /></mesh>
|
|
431
|
+
<mesh position={[Math.cos(tl * 1.3) * r * 1.14, 0, Math.sin(tl * 1.3) * r * 1.14]} rotation={[1.2, 0.3, 0]}>
|
|
432
|
+
<sphereGeometry args={[r * 0.05, 16, 12]} /><meshBasicMaterial color={cor} />
|
|
433
|
+
</mesh>
|
|
434
|
+
</group>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function Fita({ r, cor, cor2, sem, tl }) {
|
|
439
|
+
return (
|
|
440
|
+
<group rotation={[0.5 + tl * 0.35, tl * 0.6 + sem * 0.02, 0.2]}>
|
|
441
|
+
<mesh>
|
|
442
|
+
<torusKnotGeometry args={[r * 0.52, r * 0.13, 280, 32, 2 + (sem % 2), 3]} />
|
|
443
|
+
<meshPhysicalMaterial color={cor2} roughness={0.28} metalness={0.35} clearcoat={0.9} clearcoatRoughness={0.2} />
|
|
444
|
+
</mesh>
|
|
445
|
+
<mesh scale={[1.02, 1.02, 1.02]}>
|
|
446
|
+
<torusKnotGeometry args={[r * 0.52, r * 0.13, 140, 12, 2 + (sem % 2), 3]} />
|
|
447
|
+
<meshBasicMaterial color={cor} wireframe transparent opacity={0.22} />
|
|
448
|
+
</mesh>
|
|
449
|
+
</group>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function Colunas({ cor, cor2, fundo, sem, tl }) {
|
|
454
|
+
const n = 15, passo = 0.9;
|
|
455
|
+
const base = new THREE.Color(fundo).lerp(new THREE.Color(cor2), 0.22).getStyle();
|
|
456
|
+
const acesa = new THREE.Color(cor).getStyle();
|
|
457
|
+
const els = [];
|
|
458
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
|
|
459
|
+
const x = (i - (n - 1) / 2) * passo, z = (j - (n - 1) / 2) * passo - tl * 1.1;
|
|
460
|
+
const d = Math.hypot(i - n / 2, j - n / 2);
|
|
461
|
+
const onda = 0.5 + 0.5 * Math.sin(d * 0.85 - tl * 2.6 + (sem % 7));
|
|
462
|
+
const h = 0.25 + 2.2 * onda * onda;
|
|
463
|
+
const crista = onda > 0.93;
|
|
464
|
+
els.push(
|
|
465
|
+
<mesh key={i + '-' + j} position={[x, h / 2 - 1.6, z]} scale={[passo * 0.78, h, passo * 0.78]}>
|
|
466
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
467
|
+
<meshStandardMaterial color={crista ? acesa : base} roughness={0.55} metalness={0.15}
|
|
468
|
+
{...(crista ? { emissive: acesa, emissiveIntensity: 0.6 } : {})} />
|
|
469
|
+
</mesh>,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return <group>{els}</group>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* O quanto cada forma passa do raio r, no pior quadro: o anel e a lua do
|
|
476
|
+
globo (1,14 r + a lua), o nó (o tubo vai a 1,5 × 0,52 r + 0,13 r). */
|
|
477
|
+
const ALCANCE3D = { globo3d: 1.2, fita3d: 0.92 };
|
|
478
|
+
|
|
479
|
+
export function Heroi3D({ h, t, de, ate, W, H, slot }) {
|
|
480
|
+
const tl = Math.max(0, t - de), dur = Math.max(0.8, ate - de);
|
|
481
|
+
const k = Math.min(1.02, mola3d(tl / 0.8));
|
|
482
|
+
// Some ANTES de o texto sair (a saída dele voa pela tela): o herói nunca cruza a letra.
|
|
483
|
+
const saiDe = slot.saiDe != null ? slot.saiDe : ate - 0.25;
|
|
484
|
+
const sai = Math.max(0, Math.min(1, (t - saiDe) / 0.2));
|
|
485
|
+
const sem = h.semente || 0;
|
|
486
|
+
const cor = h.cor || '#ff5a1f', cor2 = h.cor2 || '#eeeeee', fundo = h.fundo || '#111111';
|
|
487
|
+
if (h.tipo === 'colunas3d') {
|
|
488
|
+
// O sobrevoo: a câmera alta, olhando para a frente e para baixo, andando sobre o campo.
|
|
489
|
+
const avanca = tl * 0.25;
|
|
490
|
+
return (
|
|
491
|
+
<div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: (slot.op != null ? slot.op : 0.85) * Math.min(1, tl / 0.5) * (1 - sai),
|
|
492
|
+
/* A FAIXA: as colunas só existem fora do texto (clip), com a borda esfumada. */
|
|
493
|
+
...(slot.faixa ? { clipPath: `inset(${slot.faixa.y0}px 0 ${H - slot.faixa.y1}px 0)`,
|
|
494
|
+
WebkitMaskImage: slot.teto ? `linear-gradient(to top, transparent ${H - slot.faixa.y1}px, #000 ${H - slot.faixa.y1 + 0.06 * H}px)` : `linear-gradient(to bottom, transparent ${slot.faixa.y0}px, #000 ${slot.faixa.y0 + 0.06 * H}px)` } : {}) }}>
|
|
495
|
+
<ThreeCanvas width={W} height={H} camera={{ position: [0, 4, 9], fov: 38 }} gl={{ antialias: true, preserveDrawingBuffer: true, alpha: true }}>
|
|
496
|
+
{/* Câmera baixa olhando quase reto: o campo ocupa a metade de baixo e some na névoa — o meio fica para o texto. */}
|
|
497
|
+
<CameraFixa teto={slot.teto} pos={[Math.sin(tl * 0.3) * 1.2, 2.3, 8 - avanca]} alvo={[0, 0.9, -4 - avanca]} />
|
|
498
|
+
<fog attach="fog" args={[fundo, 6, 19]} />
|
|
499
|
+
<ambientLight intensity={0.45} />
|
|
500
|
+
<directionalLight position={[4, 8, 5]} intensity={2.2} />
|
|
501
|
+
<directionalLight position={[-6, 2, -4]} intensity={2.6} color={cor} />
|
|
502
|
+
<Colunas cor={cor} cor2={cor2} fundo={fundo} sem={sem} tl={tl} />
|
|
503
|
+
</ThreeCanvas>
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const { cx, cy, R } = slot;
|
|
508
|
+
const wx = ((cx - W / 2) / H) * VIS3D, wy = (-(cy - H / 2) / H) * VIS3D;
|
|
509
|
+
/* R é o TETO do que aparece na tela (o vão livre ao lado do texto). O raio
|
|
510
|
+
no mundo sai dele de trás para a frente: o alcance da forma, a escala
|
|
511
|
+
máxima e a perspectiva do ponto mais perto da lente no fim do dolly. */
|
|
512
|
+
const F = ALCANCE3D[h.tipo] || 1.2, zMax = 0.45;
|
|
513
|
+
let r = (R / H) * VIS3D / (F * 1.02);
|
|
514
|
+
for (let i = 0; i < 3; i++) r = ((R / H) * VIS3D / (F * 1.02)) * ((DIST3D - zMax - F * r) / DIST3D);
|
|
515
|
+
// O dolly: o objeto chega do fundo e vem devagar para a lente durante a batida.
|
|
516
|
+
const z = -3.5 * (1 - Math.min(1, k)) + zMax * Math.min(1, tl / dur);
|
|
517
|
+
const esc = 0.55 + 0.45 * k;
|
|
518
|
+
return (
|
|
519
|
+
<div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: (slot.op != null ? slot.op : 1) * Math.min(1, tl / 0.3) * (1 - sai) }}>
|
|
520
|
+
<ThreeCanvas width={W} height={H} camera={{ position: [0, 0, DIST3D], fov: FOV3D }} gl={{ antialias: true, preserveDrawingBuffer: true, alpha: true }}>
|
|
521
|
+
<Ambiente forca={0.9} />
|
|
522
|
+
<ambientLight intensity={0.3} />
|
|
523
|
+
<directionalLight position={[3, 4, 6]} intensity={2.6} />
|
|
524
|
+
<directionalLight position={[-5, -1, -3]} intensity={3.2} color={cor} />
|
|
525
|
+
<pointLight position={[0, 0, 5]} intensity={0.6} />
|
|
526
|
+
<group position={[wx, wy, z]} scale={[esc, esc, esc]}>
|
|
527
|
+
{h.tipo === 'fita3d' ? <Fita r={r} cor={cor} cor2={cor2} sem={sem} tl={tl} /> : <Globo r={r} cor={cor} cor2={cor2} sem={sem} tl={tl} />}
|
|
528
|
+
</group>
|
|
529
|
+
</ThreeCanvas>
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
378
534
|
export const TRES = { produto3d: Produto3D, icone3d: Icone3D };
|
package/lib/local/motor-cli.js
CHANGED
|
@@ -16,11 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
'use strict';
|
|
18
18
|
|
|
19
|
-
const { execFileSync } = require('child_process');
|
|
20
19
|
const cerebro = require('./cerebro.json');
|
|
21
20
|
|
|
22
|
-
const SEM_APARECER = { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
|
|
23
|
-
|
|
24
21
|
function motorDe(id) {
|
|
25
22
|
if (id === 'codex') {
|
|
26
23
|
/* O padrão antigo do motor (gpt-5-mini) é recusado por conta de
|
|
@@ -33,13 +30,19 @@ function motorDe(id) {
|
|
|
33
30
|
return require('../claude-engine.js');
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
/** Uma pergunta de texto, resposta de texto. Sem protocolo, sem ferramenta.
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
/** Uma pergunta de texto, resposta de texto. Sem protocolo, sem ferramenta.
|
|
34
|
+
*
|
|
35
|
+
* ASSÍNCRONA. Era `execFileSync('claude', …)`: enquanto o Claude pensava, o
|
|
36
|
+
* PrimoCode inteiro parava — a tela cheia congelava em "Trabalhando · 0s",
|
|
37
|
+
* sem animação, sem Esc Esc, sem dar para digitar. O agente (requestAct) já
|
|
38
|
+
* rodava em paralelo; o chat e o resumo eram os que travavam. E o Codex
|
|
39
|
+
* devolvia "[object Promise]": a chamada dele já era assíncrona e o
|
|
40
|
+
* resultado virava texto sem esperar. */
|
|
41
|
+
async function perguntar(id, texto) {
|
|
42
|
+
if (id === 'codex') return String((await motorDe('codex').chamarCodex(texto)) || '').trim();
|
|
39
43
|
const claude = motorDe('claude-code');
|
|
40
|
-
const saida =
|
|
41
|
-
|
|
42
|
-
});
|
|
44
|
+
const saida = await claude.rodarClaude(['-p', '--output-format', 'json', ...claude.argsDoMotor()], texto,
|
|
45
|
+
{ timeout: 180000, tentativas: 1 });
|
|
43
46
|
try { return String(JSON.parse(saida).result || '').trim(); } catch { return String(saida || '').trim(); }
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -54,7 +57,7 @@ function criar(id) {
|
|
|
54
57
|
const conversa = turnos.filter((m) => m && m.role && typeof m.content === 'string')
|
|
55
58
|
.map((m) => `${m.role === 'user' ? 'PESSOA' : 'VOCÊ'}: ${m.content}`).join('\n');
|
|
56
59
|
try {
|
|
57
|
-
const resposta = perguntar(id, `${cerebro.SYSTEM_CHAT}\n\nResponda só à última fala da PESSOA.\n\n${conversa}`);
|
|
60
|
+
const resposta = await perguntar(id, `${cerebro.SYSTEM_CHAT}\n\nResponda só à última fala da PESSOA.\n\n${conversa}`);
|
|
58
61
|
if (resposta) emit({ delta: resposta });
|
|
59
62
|
} catch (e) {
|
|
60
63
|
emit({ erro: e.message });
|
|
@@ -69,7 +72,7 @@ function criar(id) {
|
|
|
69
72
|
+ novas.map((m) => `${m.role === 'user' ? 'USUÁRIO' : m.role === 'assistant' ? 'AGENTE' : 'FERRAMENTA'}: `
|
|
70
73
|
+ String(typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '')).slice(0, 1200)).join('\n');
|
|
71
74
|
try {
|
|
72
|
-
return { resumo: perguntar(id, `${cerebro.SYSTEM_RESUMO}\n\n${corpo}`) || String(resumo || '') };
|
|
75
|
+
return { resumo: (await perguntar(id, `${cerebro.SYSTEM_RESUMO}\n\n${corpo}`)) || String(resumo || '') };
|
|
73
76
|
} catch (e) {
|
|
74
77
|
return { resumo: String(resumo || ''), erro: e.message };
|
|
75
78
|
}
|
package/lib/tela.js
CHANGED
|
@@ -172,7 +172,9 @@ function quadro(e) {
|
|
|
172
172
|
while (linhas.length < H - 1) linhas.push('');
|
|
173
173
|
linhas.length = H - 1;
|
|
174
174
|
const esq = ' ' + c.dim(e.pasta || '');
|
|
175
|
-
|
|
175
|
+
// Folga à direita: terminais como o Orca põem um botão flutuante no
|
|
176
|
+
// canto de baixo, e ele cobria a versão ("10.").
|
|
177
|
+
const dir = c.dim(e.versao || '') + ' ';
|
|
176
178
|
linhas.push(esq + ' '.repeat(Math.max(1, W - visibleWidth(esq) - visibleWidth(dir))) + dir);
|
|
177
179
|
return {
|
|
178
180
|
linhas: linhas.map((l) => caber(l, W)),
|
package/lib/voz.js
CHANGED
|
@@ -278,6 +278,8 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
|
|
|
278
278
|
pronto,
|
|
279
279
|
falar: (texto) => { const t = paraFalar(texto); if (t) manda({ tipo: 'falar', texto: t }); },
|
|
280
280
|
calar: () => manda({ tipo: 'calar' }),
|
|
281
|
+
// A bolinha na tela: 'esperando' quando o terminal termina sem falar.
|
|
282
|
+
estado: (e) => manda({ tipo: 'estado', estado: e }),
|
|
281
283
|
};
|
|
282
284
|
}
|
|
283
285
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.4.
|
|
3
|
+
"version": "10.4.7",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/vazamento.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -44,6 +44,50 @@ sys.path.insert(0, str(AQUI.parent))
|
|
|
44
44
|
from primo_voz import escuta, transcrever # noqa: E402
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class Bolinha:
|
|
48
|
+
"""A orb (orbe.py) num processo à parte: este só diz o estado a ela.
|
|
49
|
+
|
|
50
|
+
Sem tkinter, ou se ela morrer, o modo voz segue igual — a bolinha é o
|
|
51
|
+
rosto, não o ouvido. Mudanças repetidas não são reenviadas, e o nível da
|
|
52
|
+
voz vai no máximo 15 vezes por segundo."""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self.p = None
|
|
56
|
+
self.ultimo = None
|
|
57
|
+
self.quando = 0.0
|
|
58
|
+
if os.environ.get("PRIMOCODE_SEM_ORB") == "1":
|
|
59
|
+
return
|
|
60
|
+
try:
|
|
61
|
+
self.p = subprocess.Popen([sys.executable, "-m", "primo_voz.orbe"], cwd=str(AQUI.parent),
|
|
62
|
+
stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
63
|
+
env={**os.environ, "PYTHONPATH": str(AQUI.parent)}, text=True)
|
|
64
|
+
except Exception:
|
|
65
|
+
self.p = None
|
|
66
|
+
|
|
67
|
+
def estado(self, estado: str, nivel: float = None) -> None:
|
|
68
|
+
if not self.p or self.p.poll() is not None:
|
|
69
|
+
return
|
|
70
|
+
agora = time.time()
|
|
71
|
+
if estado == self.ultimo and (nivel is None or agora - self.quando < 1 / 15):
|
|
72
|
+
return
|
|
73
|
+
self.ultimo, self.quando = estado, agora
|
|
74
|
+
msg = {"estado": estado}
|
|
75
|
+
if nivel is not None:
|
|
76
|
+
msg["nivel"] = round(min(1.0, nivel), 3)
|
|
77
|
+
try:
|
|
78
|
+
self.p.stdin.write(json.dumps(msg) + "\n")
|
|
79
|
+
self.p.stdin.flush()
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def fechar(self) -> None:
|
|
84
|
+
if self.p:
|
|
85
|
+
try:
|
|
86
|
+
self.p.stdin.close()
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
47
91
|
def falar_para_o_node(obj: dict) -> None:
|
|
48
92
|
"""Uma linha de JSON no stdout. Nada mais pode sair por aí."""
|
|
49
93
|
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
|
@@ -100,6 +144,7 @@ class Voz:
|
|
|
100
144
|
transcrever.aquecer()
|
|
101
145
|
self.gravador = escuta.Gravador()
|
|
102
146
|
self.corte = escuta.Corte()
|
|
147
|
+
self.bolinha = Bolinha()
|
|
103
148
|
self._blocos: "queue.Queue" = queue.Queue()
|
|
104
149
|
self._falas: "queue.Queue" = queue.Queue()
|
|
105
150
|
self._vivo = True
|
|
@@ -138,6 +183,7 @@ class Voz:
|
|
|
138
183
|
self.parar()
|
|
139
184
|
return
|
|
140
185
|
falar_para_o_node({"tipo": "pronto"})
|
|
186
|
+
self.bolinha.estado("esperando")
|
|
141
187
|
while self._vivo:
|
|
142
188
|
self.gravador.comecar()
|
|
143
189
|
acabou = False
|
|
@@ -147,6 +193,10 @@ class Voz:
|
|
|
147
193
|
except queue.Empty:
|
|
148
194
|
continue
|
|
149
195
|
acabou = self.gravador.alimentar(bloco)
|
|
196
|
+
# A bolinha acende com a voz da pessoa, no tamanho da voz.
|
|
197
|
+
if self.gravador._falando and not self._falando:
|
|
198
|
+
lim = max(1e-6, self.gravador._limiar())
|
|
199
|
+
self.bolinha.estado("ouvindo", min(1.0, escuta.rms(bloco) / lim * 0.35))
|
|
150
200
|
if not self._vivo:
|
|
151
201
|
break
|
|
152
202
|
if self.gravador.encerrou_por == "ninguem-falou" or self.gravador.segundos < 0.4:
|
|
@@ -154,9 +204,14 @@ class Voz:
|
|
|
154
204
|
wav = self.gravador.gravar_wav(Path(tempfile.gettempdir()) / "primo-voz-fala.wav")
|
|
155
205
|
if not wav:
|
|
156
206
|
continue
|
|
207
|
+
self.bolinha.estado("pensando")
|
|
157
208
|
texto = transcrever.limpar(self._transcrever(wav))
|
|
158
209
|
if texto:
|
|
210
|
+
# Pensando até o terminal dizer o contrário (a resposta em
|
|
211
|
+
# voz, ou {"tipo":"estado"} quando ele termina sem falar).
|
|
159
212
|
falar_para_o_node({"tipo": "ouvi", "texto": texto})
|
|
213
|
+
else:
|
|
214
|
+
self.bolinha.estado("esperando")
|
|
160
215
|
mic.fechar()
|
|
161
216
|
|
|
162
217
|
def _transcrever(self, wav: Path) -> str:
|
|
@@ -213,10 +268,12 @@ class Voz:
|
|
|
213
268
|
return
|
|
214
269
|
self._falando = True
|
|
215
270
|
self._parar_fala.clear()
|
|
271
|
+
self.bolinha.estado("falando", 0.6)
|
|
216
272
|
try:
|
|
217
273
|
self._tocar(mp3)
|
|
218
274
|
finally:
|
|
219
275
|
self._falando = False
|
|
276
|
+
self.bolinha.estado("esperando")
|
|
220
277
|
self.corte.nivel_dele = 0.0
|
|
221
278
|
# O que o microfone pegou enquanto ele falava é a voz dele.
|
|
222
279
|
self._esvaziar_blocos()
|
|
@@ -283,6 +340,8 @@ class Voz:
|
|
|
283
340
|
self._falas.get_nowait()
|
|
284
341
|
except queue.Empty:
|
|
285
342
|
break
|
|
343
|
+
elif tipo == "estado":
|
|
344
|
+
self.bolinha.estado(str(msg.get("estado") or "esperando"))
|
|
286
345
|
elif tipo == "fim":
|
|
287
346
|
break
|
|
288
347
|
self.parar()
|
|
@@ -292,6 +351,7 @@ class Voz:
|
|
|
292
351
|
return
|
|
293
352
|
self._vivo = False
|
|
294
353
|
self._parar_fala.set()
|
|
354
|
+
self.bolinha.fechar()
|
|
295
355
|
falar_para_o_node({"tipo": "fim"})
|
|
296
356
|
|
|
297
357
|
def rodar(self) -> None:
|