primocode 9.10.0 → 10.0.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 +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +41 -18
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/remotion.js +87 -43
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +3 -4
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/voz.js
CHANGED
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* voz.js — o lado Node do modo voz.
|
|
3
3
|
*
|
|
4
|
-
* O
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* O ouvido e a boca são Python (voz/primo_voz): microfone, Whisper, a voz
|
|
5
|
+
* que fala. O que mora aqui é a ponta do terminal: instalar o que falta,
|
|
6
|
+
* subir o processo, receber o que a pessoa DISSE e mandar falar a resposta.
|
|
7
7
|
*
|
|
8
|
-
* ──
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* texto para o `runAct` de sempre: as mesmas ferramentas, as mesmas skills, a
|
|
17
|
-
* mesma permissão, os mesmos conectores.
|
|
18
|
-
*
|
|
19
|
-
* ── O PEDIDO SAI DIFERENTE, E SÓ ISSO ────────────────────────────────────
|
|
20
|
-
* Uma resposta boa de ler é ruim de ouvir: lista com marcador, caminho de
|
|
21
|
-
* arquivo, bloco de código e trecho longo só funcionam para o olho. Então o
|
|
22
|
-
* pedido vai com uma instrução curta de FORMA — não de conteúdo. O agente
|
|
23
|
-
* continua fazendo o mesmo trabalho; ele só conta de outro jeito.
|
|
8
|
+
* ── O MODO VOZ NÃO PENSA ─────────────────────────────────────────────────
|
|
9
|
+
* Até a 9.x o /voice tinha um cérebro próprio: chamava o agente por conta
|
|
10
|
+
* dele, ao lado do terminal, com a própria instrução de forma, o próprio
|
|
11
|
+
* roteador de conversa e o próprio portão de permissão. Dois agentes na mesma
|
|
12
|
+
* pasta, e duas versões de cada regra — a correção de amanhã ia para uma só.
|
|
13
|
+
* Na 10.0 a fala é só uma forma de DIGITAR: o que foi dito entra na mesma
|
|
14
|
+
* fila, na mesma caixa de recados e no mesmo roteador do terminal, e a
|
|
15
|
+
* resposta final é lida em voz alta (paraFalar).
|
|
24
16
|
*/
|
|
25
17
|
|
|
26
18
|
'use strict';
|
|
@@ -45,28 +37,31 @@ const PASTA = path.join(__dirname, '..', 'voz');
|
|
|
45
37
|
*/
|
|
46
38
|
const DICA_COMPUTADOR = `[COMPUTADOR] Você pode usar o mouse, o teclado, as janelas e os ícones do computador da pessoa com as ferramentas desktop_* (desktop_open_app, desktop_click_element, desktop_click, desktop_type, desktop_key para atalho, desktop_scroll). Cada movimento aparece na tela como o SEU cursor. Para abrir um app, use desktop_open_app; para clicar em algo, prefira desktop_click_element pelo nome (leia os nomes com desktop_elements); coordenadas são em pontos da tela (desktop_size). Se pedirem para "mostrar o seu mouse/cursor", MEXA o cursor com desktop_move — nunca crie uma página ou arquivo para isso.`;
|
|
47
39
|
|
|
48
|
-
/*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
40
|
+
/* ── O QUE SAI EM VOZ ──────────────────────────────────────────────────
|
|
41
|
+
* O agente responde para a TELA, como sempre — o modo voz não muda o que ele
|
|
42
|
+
* faz nem como ele escreve. O que vai para o ouvido é um resumo falado da
|
|
43
|
+
* mesma resposta: sem markdown, sem código, sem caminho, no máximo três
|
|
44
|
+
* frases. Quem quiser o detalhe olha o terminal, que é onde ele está. */
|
|
45
|
+
function paraFalar(texto, { frases = 3, teto = 420 } = {}) {
|
|
46
|
+
let t = String(texto || '')
|
|
47
|
+
.replace(/```[\s\S]*?```/g, ' ') // bloco de código
|
|
48
|
+
.replace(/\s+(em|no|na|de|do|da|para)\s+`[^`]*[\/\\.][^`]*`/g, '') // "em `/pasta/x`" some inteiro
|
|
49
|
+
.replace(/`([^`]*)`/g, (_, x) => (/[\/\\.]/.test(x) ? '' : x)) // caminho some, palavra fica
|
|
50
|
+
.replace(/https?:\/\/\S+/g, '') // link
|
|
51
|
+
.replace(/(^|\s)[~.]?\/[\w.\-\/]+/g, ' ') // caminho solto
|
|
52
|
+
.replace(/^\s*(#+|[-*•]|\d+[.)])\s+/gm, '') // título e marcador
|
|
53
|
+
.replace(/[*_>|]/g, '')
|
|
54
|
+
.replace(/\(\^‿\^\)[^\n]*/g, '') // o mascote
|
|
55
|
+
.replace(/\s+/g, ' ')
|
|
56
|
+
.trim();
|
|
57
|
+
const partes = t.match(/[^.!?…]+[.!?…]*/g) || [];
|
|
58
|
+
let fala = '';
|
|
59
|
+
for (const p of partes.slice(0, frases)) {
|
|
60
|
+
if ((fala + p).length > teto) break;
|
|
61
|
+
fala += p;
|
|
62
|
+
}
|
|
63
|
+
return (fala || t.slice(0, teto)).trim();
|
|
64
|
+
}
|
|
70
65
|
|
|
71
66
|
/** Onde está o Python 3. Mesmo problema do Estúdio, mesma cura. */
|
|
72
67
|
function python() {
|
|
@@ -86,19 +81,17 @@ function python() {
|
|
|
86
81
|
* O que falta para o modo voz abrir — e QUAL é a falta.
|
|
87
82
|
*
|
|
88
83
|
* A distinção é a diferença entre um recado que resolve e um que faz a pessoa
|
|
89
|
-
* dar voltas.
|
|
84
|
+
* dar voltas. Um caso não se resolve com pip:
|
|
90
85
|
*
|
|
91
86
|
* · `sounddevice` instalado, mas sem o PortAudio do sistema. O import
|
|
92
87
|
* explode com "PortAudio library not found", e mandar `pip install
|
|
93
88
|
* sounddevice` ali é mandar instalar o que já está instalado.
|
|
94
|
-
* · `tkinter` vem com o Python, mas algumas distribuições Linux o separam
|
|
95
|
-
* num pacote do sistema — e `pip install tkinter` não existe.
|
|
96
89
|
*
|
|
97
90
|
* Por isso a checagem lê a MENSAGEM do erro, e não só o fato de ter falhado.
|
|
98
91
|
*/
|
|
99
92
|
function faltando(py) {
|
|
100
93
|
const falta = [];
|
|
101
|
-
for (const [modulo, pacote] of [['numpy', 'numpy'], ['sounddevice', 'sounddevice']
|
|
94
|
+
for (const [modulo, pacote] of [['numpy', 'numpy'], ['sounddevice', 'sounddevice']]) {
|
|
102
95
|
try {
|
|
103
96
|
execFileSync(py, ['-c', `import ${modulo}`], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 20000 });
|
|
104
97
|
} catch (e) {
|
|
@@ -120,21 +113,9 @@ const COMO_SISTEMA = {
|
|
|
120
113
|
linux: 'sudo apt install libportaudio2 (ou o equivalente da sua distribuição)',
|
|
121
114
|
win32: 'no Windows ele vem junto com o sounddevice — reinstale com pip',
|
|
122
115
|
},
|
|
123
|
-
tkinter: {
|
|
124
|
-
darwin: 'brew install python-tk',
|
|
125
|
-
linux: 'sudo apt install python3-tk',
|
|
126
|
-
win32: 'reinstale o Python pelo python.org, marcando "tcl/tk"',
|
|
127
|
-
},
|
|
128
116
|
};
|
|
129
117
|
|
|
130
|
-
/**
|
|
131
|
-
* Confere o que é preciso, sem instalar nada.
|
|
132
|
-
*
|
|
133
|
-
* O tkinter é o caso especial: ele vem com o Python, mas em algumas
|
|
134
|
-
* distribuições Linux ele foi separado num pacote do SISTEMA — e `pip install
|
|
135
|
-
* tkinter` não existe. Dizer "instale com pip" ali seria mandar a pessoa a um
|
|
136
|
-
* lugar onde não há nada.
|
|
137
|
-
*/
|
|
118
|
+
/** Confere o que é preciso, sem instalar nada. */
|
|
138
119
|
function conferir() {
|
|
139
120
|
const py = python();
|
|
140
121
|
if (!py) {
|
|
@@ -156,9 +137,6 @@ function conferir() {
|
|
|
156
137
|
if (falta.some((f) => f.modulo === 'sounddevice' && f.sistema)) {
|
|
157
138
|
linhas.push(COMO_SISTEMA.portaudio[process.platform] || COMO_SISTEMA.portaudio.linux);
|
|
158
139
|
}
|
|
159
|
-
if (falta.some((f) => f.modulo === 'tkinter')) {
|
|
160
|
-
linhas.push(COMO_SISTEMA.tkinter[process.platform] || COMO_SISTEMA.tkinter.linux);
|
|
161
|
-
}
|
|
162
140
|
|
|
163
141
|
return {
|
|
164
142
|
ok: false,
|
|
@@ -172,7 +150,7 @@ function conferir() {
|
|
|
172
150
|
};
|
|
173
151
|
}
|
|
174
152
|
|
|
175
|
-
/** Instala o que dá para instalar com pip.
|
|
153
|
+
/** Instala o que dá para instalar com pip. */
|
|
176
154
|
/* AS TENTATIVAS, EM ORDEM — e por que uma só não serve.
|
|
177
155
|
*
|
|
178
156
|
* O Python do Homebrew e o do Debian/Ubuntu RECUSAM `pip install` no ambiente
|
|
@@ -232,20 +210,24 @@ function instalar(py, pacotes, aoFalar = () => {}) {
|
|
|
232
210
|
}
|
|
233
211
|
|
|
234
212
|
/**
|
|
235
|
-
*
|
|
213
|
+
* Liga o ouvido e a boca (voz/primo_voz). O processo não pensa: devolve o que
|
|
214
|
+
* ouviu por `aoOuvir` e fala o que receber em `falar`.
|
|
236
215
|
*
|
|
237
216
|
* @param {object} op
|
|
238
217
|
* @param {string} op.python
|
|
239
218
|
* @param {string} op.servidor
|
|
240
219
|
* @param {string} op.token
|
|
241
220
|
* @param {string} op.voz
|
|
242
|
-
* @param {(texto:string)=>
|
|
243
|
-
* @param {(texto:string)=>void} op.mostrar
|
|
244
|
-
* @param {
|
|
245
|
-
* @returns {{processo, pronto: Promise
|
|
221
|
+
* @param {(texto:string)=>void} op.aoOuvir o que a pessoa disse, já transcrito
|
|
222
|
+
* @param {(texto:string, deErro?:boolean)=>void} op.mostrar recado para o terminal
|
|
223
|
+
* @param {()=>void} [op.aoPronto] o microfone abriu
|
|
224
|
+
* @returns {{processo, pronto: Promise, falar:(t:string)=>void, calar:()=>void}}
|
|
246
225
|
*/
|
|
247
|
-
function abrir({ python: py, servidor, token, voz,
|
|
226
|
+
function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {}, aoPronto = () => {} }) {
|
|
248
227
|
let p;
|
|
228
|
+
const manda = (obj) => {
|
|
229
|
+
try { p.stdin.write(JSON.stringify(obj) + '\n'); } catch { /* já fechou */ }
|
|
230
|
+
};
|
|
249
231
|
const pronto = new Promise((resolve) => {
|
|
250
232
|
p = spawn(py, ['-m', 'primo_voz'], {
|
|
251
233
|
cwd: PASTA,
|
|
@@ -255,24 +237,14 @@ function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {}, e
|
|
|
255
237
|
PRIMOCODE_SERVER: servidor,
|
|
256
238
|
PRIMOCODE_TOKEN: token || '',
|
|
257
239
|
PRIMOCODE_VOZ: voz || 'antonio',
|
|
258
|
-
// Dormir até ouvir o nome, em vez de já entrar ouvindo.
|
|
259
|
-
PRIMOCODE_VOZ_ESPERA: esperando ? '1' : '',
|
|
260
240
|
PYTHONUNBUFFERED: '1',
|
|
261
241
|
// O Whisper deixa um semáforo para o sistema recolher na saída,
|
|
262
|
-
// e o Python avisa disso no stderr
|
|
263
|
-
// terminal da pessoa. É inofensivo; o aviso é que não é.
|
|
242
|
+
// e o Python avisa disso no stderr. É inofensivo; o aviso não é.
|
|
264
243
|
PYTHONWARNINGS: 'ignore::UserWarning:multiprocessing.resource_tracker',
|
|
265
244
|
},
|
|
266
245
|
});
|
|
267
|
-
|
|
268
|
-
const manda = (obj) => {
|
|
269
|
-
try { p.stdin.write(JSON.stringify(obj) + '\n'); } catch { /* já fechou */ }
|
|
270
|
-
};
|
|
271
|
-
|
|
272
246
|
let buffer = '';
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
p.stdout.on('data', async (d) => {
|
|
247
|
+
p.stdout.on('data', (d) => {
|
|
276
248
|
buffer += d.toString();
|
|
277
249
|
const linhas = buffer.split('\n');
|
|
278
250
|
buffer = linhas.pop();
|
|
@@ -280,136 +252,33 @@ function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {}, e
|
|
|
280
252
|
if (!linha.trim()) continue;
|
|
281
253
|
let msg;
|
|
282
254
|
try { msg = JSON.parse(linha); } catch { continue; }
|
|
283
|
-
|
|
284
255
|
if (msg.tipo === 'fim') { try { p.kill(); } catch {} return; }
|
|
285
|
-
if (msg.tipo === '
|
|
286
|
-
if (msg.tipo
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
trabalha — e dois agentes rodando ao mesmo tempo na mesma
|
|
290
|
-
pasta é a receita para um escrever por cima do outro. */
|
|
291
|
-
if (ocupado) { manda({ tipo: 'resposta', texto: 'Ainda estou terminando o anterior.' }); continue; }
|
|
292
|
-
ocupado = true;
|
|
293
|
-
|
|
294
|
-
mostrar(String(msg.texto || ''));
|
|
295
|
-
manda({ tipo: 'estado', estado: 'pensando' });
|
|
296
|
-
try {
|
|
297
|
-
const resposta = await pensar(String(msg.texto || ''));
|
|
298
|
-
manda({ tipo: 'resposta', texto: resposta || 'Feito.' });
|
|
299
|
-
} catch (e) {
|
|
300
|
-
manda({ tipo: 'erro', texto: `Não consegui: ${e.message}` });
|
|
301
|
-
} finally {
|
|
302
|
-
ocupado = false;
|
|
256
|
+
if (msg.tipo === 'pronto') { try { aoPronto(); } catch {} continue; }
|
|
257
|
+
if (msg.tipo === 'erro') { mostrar(String(msg.texto || ''), true); continue; }
|
|
258
|
+
if (msg.tipo === 'ouvi' && String(msg.texto || '').trim()) {
|
|
259
|
+
try { aoOuvir(String(msg.texto).trim()); } catch (e) { mostrar(e.message, true); }
|
|
303
260
|
}
|
|
304
261
|
}
|
|
305
262
|
});
|
|
306
|
-
|
|
307
263
|
// O Python fala com o terminal pela saída de ERRO, de propósito: o
|
|
308
|
-
// stdout inteiro é do protocolo
|
|
309
|
-
// conversa entre os dois processos.
|
|
264
|
+
// stdout inteiro é do protocolo.
|
|
310
265
|
p.stderr.on('data', (d) => {
|
|
311
266
|
const texto = d.toString().trim();
|
|
312
267
|
if (texto) mostrar(texto, true);
|
|
313
268
|
});
|
|
314
|
-
|
|
315
269
|
p.on('close', () => resolve({ ok: true }));
|
|
316
270
|
p.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
317
271
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/* ── O RECONHECEDOR LOCAL DO NOME ────────────────────────────────────────
|
|
330
|
-
*
|
|
331
|
-
* Duas funções, e a razão de existirem é a mesma: no modo de espera o
|
|
332
|
-
* microfone fica ABERTO o tempo todo, e há dois jeitos de saber se alguém
|
|
333
|
-
* disse "ei, primo" — mandar tudo o que ele ouve para um servidor, ou
|
|
334
|
-
* reconhecer a palavra aqui dentro.
|
|
335
|
-
*
|
|
336
|
-
* O primeiro é mais fácil de escrever e significa a conversa da casa inteira
|
|
337
|
-
* subindo para a rede o dia inteiro. O segundo custa ~31 MB de modelo, uma
|
|
338
|
-
* vez. Não é escolha difícil, e não é escolha que o programa faça sozinho:
|
|
339
|
-
* `conferirEspera` só RESPONDE o que falta; quem pergunta à pessoa é o CLI.
|
|
340
|
-
*/
|
|
341
|
-
function conferirEspera(py) {
|
|
342
|
-
const perguntar = `
|
|
343
|
-
import json, sys
|
|
344
|
-
sys.path.insert(0, ${JSON.stringify(PASTA)})
|
|
345
|
-
try:
|
|
346
|
-
from primo_voz import desperta
|
|
347
|
-
except Exception as e:
|
|
348
|
-
print(json.dumps({"ok": False, "error": "não consegui ler o módulo: %s" % e})); raise SystemExit
|
|
349
|
-
try:
|
|
350
|
-
import vosk # noqa: F401
|
|
351
|
-
temVosk = True
|
|
352
|
-
except ImportError:
|
|
353
|
-
temVosk = False
|
|
354
|
-
print(json.dumps({
|
|
355
|
-
"ok": bool(temVosk and desperta.tem_modelo()),
|
|
356
|
-
"temVosk": temVosk,
|
|
357
|
-
"temModelo": desperta.tem_modelo(),
|
|
358
|
-
"onde": str(desperta.caminho_do_modelo()),
|
|
359
|
-
}))
|
|
360
|
-
`;
|
|
361
|
-
try {
|
|
362
|
-
const saida = execFileSync(py, ['-c', perguntar],
|
|
363
|
-
{ stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000 }).toString();
|
|
364
|
-
const r = JSON.parse(saida.trim().split('\n').pop());
|
|
365
|
-
if (r.ok) return { ok: true };
|
|
366
|
-
// Falta só o modelo (ou o vosk, que o pip resolve): dá para oferecer.
|
|
367
|
-
return { ok: false, baixavel: true, temVosk: r.temVosk, onde: r.onde,
|
|
368
|
-
error: r.error || 'o reconhecedor local do nome não está instalado' };
|
|
369
|
-
} catch (e) {
|
|
370
|
-
return { ok: false, baixavel: false,
|
|
371
|
-
error: `não consegui conferir o reconhecedor: ${String(e.message || e).slice(-160)}` };
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** Instala o vosk (se faltar) e baixa o modelo. Uma vez, ~31 MB. */
|
|
376
|
-
function baixarEspera(py) {
|
|
377
|
-
return new Promise((resolve) => {
|
|
378
|
-
const script = `
|
|
379
|
-
import json, subprocess, sys
|
|
380
|
-
sys.path.insert(0, ${JSON.stringify(PASTA)})
|
|
381
|
-
try:
|
|
382
|
-
import vosk # noqa: F401
|
|
383
|
-
except ImportError:
|
|
384
|
-
# As mesmas estratégias do narrar.py: o pip do sistema recusa instalar
|
|
385
|
-
# em Debian/Ubuntu e no Homebrew (PEP 668), e uma tentativa só, calada,
|
|
386
|
-
# é a falha que ninguém vê.
|
|
387
|
-
for args in (["-m","pip","install","--user","--quiet","vosk"],
|
|
388
|
-
["-m","pip","install","--user","--quiet","--break-system-packages","vosk"],
|
|
389
|
-
["-m","pip","install","--quiet","vosk"],
|
|
390
|
-
["-m","pip","install","--quiet","--break-system-packages","vosk"]):
|
|
391
|
-
r = subprocess.run([sys.executable] + args, capture_output=True)
|
|
392
|
-
if r.returncode == 0:
|
|
393
|
-
break
|
|
394
|
-
try:
|
|
395
|
-
import vosk # noqa: F401
|
|
396
|
-
except ImportError as e:
|
|
397
|
-
print(json.dumps({"ok": False, "error": "não consegui instalar o vosk: %s" % e}))
|
|
398
|
-
raise SystemExit
|
|
399
|
-
from primo_voz import desperta
|
|
400
|
-
print(json.dumps(desperta.baixar_modelo()))
|
|
401
|
-
`;
|
|
402
|
-
const p = spawn(py, ['-c', script]);
|
|
403
|
-
let fora = '';
|
|
404
|
-
let erro = '';
|
|
405
|
-
p.stdout.on('data', (d) => { fora += d.toString(); });
|
|
406
|
-
p.stderr.on('data', (d) => { erro += d.toString(); });
|
|
407
|
-
p.on('close', () => {
|
|
408
|
-
try { resolve(JSON.parse(fora.trim().split('\n').pop())); }
|
|
409
|
-
catch { resolve({ ok: false, error: erro.slice(-300).trim() || 'o download não devolveu resposta' }); }
|
|
410
|
-
});
|
|
411
|
-
p.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
412
|
-
});
|
|
272
|
+
/* O processo volta junto com a promessa: o /voice não bloqueia o
|
|
273
|
+
terminal, e quem chamou precisa de um jeito de encerrar depois (/voice
|
|
274
|
+
parar, ou a saída do programa) sem matar pelo nome — o que atropelaria
|
|
275
|
+
outra sessão do PrimoCode aberta na mesma máquina. */
|
|
276
|
+
return {
|
|
277
|
+
processo: p,
|
|
278
|
+
pronto,
|
|
279
|
+
falar: (texto) => { const t = paraFalar(texto); if (t) manda({ tipo: 'falar', texto: t }); },
|
|
280
|
+
calar: () => manda({ tipo: 'calar' }),
|
|
281
|
+
};
|
|
413
282
|
}
|
|
414
283
|
|
|
415
284
|
/* ── PREPARAR TUDO, SEM PERGUNTAR ────────────────────────────────────────
|
|
@@ -421,20 +290,18 @@ print(json.dumps(desperta.baixar_modelo()))
|
|
|
421
290
|
* parava em cada falta com um comando para a pessoa copiar. Agora ele mesmo
|
|
422
291
|
* resolve o que dá para resolver, nesta ordem:
|
|
423
292
|
*
|
|
424
|
-
* 1. bibliotecas do SISTEMA (
|
|
293
|
+
* 1. bibliotecas do SISTEMA (o PortAudio) — pelo Homebrew no
|
|
425
294
|
* macOS; no Linux, `sudo -n` (só funciona sem senha; senão, o comando);
|
|
426
295
|
* 2. pacotes do Python — os obrigatórios (numpy, sounddevice) e os que
|
|
427
|
-
* deixam tudo
|
|
428
|
-
* 3.
|
|
296
|
+
* deixam tudo local (faster-whisper; o PyObjC do cursor no macOS);
|
|
297
|
+
* 3. o modelo do Whisper (transcrição local).
|
|
429
298
|
*
|
|
430
299
|
* Só o que é OBRIGATÓRIO faltar impede o modo voz de abrir. Faltar um opcional
|
|
431
300
|
* vira um aviso de uma linha, e o modo voz abre mesmo assim.
|
|
432
301
|
*/
|
|
433
302
|
const OPCIONAIS = () => [
|
|
434
303
|
['faster_whisper', 'faster-whisper', 'a transcrição local (Whisper)'],
|
|
435
|
-
['vosk', 'vosk', 'o reconhecedor local do "ei, primo"'],
|
|
436
304
|
...(process.platform === 'darwin' ? [
|
|
437
|
-
['AppKit', 'pyobjc-framework-Cocoa', 'a orb transparente do macOS'],
|
|
438
305
|
['Quartz', 'pyobjc-framework-Quartz', 'o cursor do Primo na tela'],
|
|
439
306
|
['Vision', 'pyobjc-framework-Vision', 'ler o texto da tela (clicar pelo nome em qualquer app)'],
|
|
440
307
|
] : []),
|
|
@@ -473,7 +340,6 @@ async function instalarSistema(py, falta, aoFalar) {
|
|
|
473
340
|
const avisos = [];
|
|
474
341
|
const precisa = [];
|
|
475
342
|
if (falta.some((f) => f.modulo === 'sounddevice' && f.sistema)) precisa.push('portaudio');
|
|
476
|
-
if (falta.some((f) => f.modulo === 'tkinter')) precisa.push('tkinter');
|
|
477
343
|
if (!precisa.length) return avisos;
|
|
478
344
|
|
|
479
345
|
if (process.platform === 'darwin') {
|
|
@@ -483,24 +349,20 @@ async function instalarSistema(py, falta, aoFalar) {
|
|
|
483
349
|
+ precisa.join(' e ') + ' sozinho — instale em brew.sh e rode /voice de novo.');
|
|
484
350
|
return avisos;
|
|
485
351
|
}
|
|
486
|
-
|
|
487
|
-
// serve ao python3.14, e só a ele.
|
|
488
|
-
let versao = '';
|
|
489
|
-
try { versao = execFileSync(py, ['-c', 'import sys;print("%d.%d"%sys.version_info[:2])']).toString().trim(); } catch {}
|
|
490
|
-
const formulas = precisa.map((x) => (x === 'tkinter' ? (versao ? `python-tk@${versao}` : 'python-tk') : x));
|
|
352
|
+
const formulas = precisa;
|
|
491
353
|
aoFalar(`instalando ${formulas.join(', ')} pelo Homebrew`);
|
|
492
354
|
const r = await rodar(b, ['install', ...formulas]);
|
|
493
355
|
if (!r.ok) avisos.push(`o Homebrew não instalou ${formulas.join(', ')}: ${porqueOPipFalhou(r.saida)}`);
|
|
494
356
|
return avisos;
|
|
495
357
|
}
|
|
496
358
|
if (process.platform === 'linux') {
|
|
497
|
-
const pacotes = precisa.map((
|
|
359
|
+
const pacotes = precisa.map(() => 'libportaudio2');
|
|
498
360
|
aoFalar(`instalando ${pacotes.join(', ')}`);
|
|
499
361
|
const r = await rodar('sudo', ['-n', 'apt-get', 'install', '-y', ...pacotes]);
|
|
500
362
|
if (!r.ok) avisos.push(`precisa de senha para instalar: sudo apt install ${pacotes.join(' ')}`);
|
|
501
363
|
return avisos;
|
|
502
364
|
}
|
|
503
|
-
avisos.push(
|
|
365
|
+
avisos.push(COMO_SISTEMA.portaudio.win32);
|
|
504
366
|
return avisos;
|
|
505
367
|
}
|
|
506
368
|
|
|
@@ -568,17 +430,7 @@ async function preparar(aoFalar = () => {}) {
|
|
|
568
430
|
const w = await baixarWhisper(py);
|
|
569
431
|
if (!w.ok) avisos.push(`a transcrição local não ficou pronta (${w.error}) — vai pelo servidor.`);
|
|
570
432
|
}
|
|
571
|
-
|
|
572
|
-
const espera = conferirEspera(py);
|
|
573
|
-
if (espera.ok) esperando = true;
|
|
574
|
-
else if (espera.baixavel) {
|
|
575
|
-
aoFalar('baixando o reconhecedor do "ei, primo" (~31 MB)');
|
|
576
|
-
const r = await baixarEspera(py);
|
|
577
|
-
if (r.ok) esperando = true;
|
|
578
|
-
else avisos.push(`sem o "ei, primo": ${r.error} — o modo voz abre já ouvindo.`);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
return { ok: true, python: py, esperando, avisos };
|
|
433
|
+
return { ok: true, python: py, avisos };
|
|
582
434
|
}
|
|
583
435
|
|
|
584
|
-
module.exports = { DICA_COMPUTADOR,
|
|
436
|
+
module.exports = { DICA_COMPUTADOR, paraFalar, conferir, instalar, preparar, porqueOPipFalhou, abrir, python, PASTA };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
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/
|
|
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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"bin/",
|
|
29
29
|
"lib/",
|
|
30
|
-
"app/",
|
|
31
30
|
"studio/",
|
|
32
31
|
"skills/",
|
|
33
32
|
"voz/",
|
|
@@ -11,7 +11,7 @@ primeira decisão é **não repetir o vídeo anterior**.
|
|
|
11
11
|
|
|
12
12
|
Não faça nada disto, em nenhum vídeo:
|
|
13
13
|
|
|
14
|
-
- gradiente roxo-para-azul
|
|
14
|
+
- gradiente roxo-para-azul, e gradiente de duas
|
|
15
15
|
cores que não têm relação com o assunto;
|
|
16
16
|
- tudo centralizado, do primeiro ao último quadro;
|
|
17
17
|
- uma fonte só, em todos os tamanhos;
|
|
@@ -59,9 +59,8 @@ Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
|
|
|
59
59
|
- **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
|
|
60
60
|
app nasce do tamanho e da COR do ícone.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Vídeo por roteiro já sai assim (estilo padrão `motion`); escreva à mão só o que o roteiro não descreve.
|
|
62
|
+
Por roteiro, o estilo segue o assunto: `motion` (padrão), `vox` (explicar; dê `destaque` e `fonte`),
|
|
63
|
+
`marca`, `lancamento`; `visual` troca a peça de uma parte.
|
|
65
64
|
|
|
66
65
|
## Cor e tipo
|
|
67
66
|
|