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.
Files changed (60) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/estilo.js +51 -0
  12. package/lib/estudio/glifos.js +215 -0
  13. package/lib/estudio/index.js +41 -18
  14. package/lib/estudio/kit/base.jsx +285 -0
  15. package/lib/estudio/kit/index.jsx +11 -0
  16. package/lib/estudio/kit/marca.jsx +741 -0
  17. package/lib/estudio/kit/vox.jsx +357 -0
  18. package/lib/estudio/kit.js +72 -0
  19. package/lib/estudio/lancamento.js +413 -0
  20. package/lib/estudio/motion.js +2 -5
  21. package/lib/estudio/plano.js +28 -3
  22. package/lib/estudio/remotion.js +87 -43
  23. package/lib/estudio/ritmo.js +77 -0
  24. package/lib/estudio/vox.js +385 -0
  25. package/lib/fala.js +1 -94
  26. package/lib/pipeline.js +55 -56
  27. package/lib/repeticao.js +3 -1
  28. package/lib/skills.js +8 -2
  29. package/lib/snapshot.js +111 -0
  30. package/lib/tools.js +79 -20
  31. package/lib/ui.js +13 -6
  32. package/lib/video/analise.js +89 -0
  33. package/lib/video/cli.js +74 -0
  34. package/lib/video/delta.js +90 -0
  35. package/lib/video/edits.js +379 -0
  36. package/lib/video/filters.js +247 -0
  37. package/lib/video/ops.js +322 -0
  38. package/lib/video/projeto.js +133 -0
  39. package/lib/video/render.js +221 -0
  40. package/lib/video/texto.js +142 -0
  41. package/lib/video/timeline.js +310 -0
  42. package/lib/video/transcript.js +124 -0
  43. package/lib/voz.js +75 -223
  44. package/package.json +2 -3
  45. package/skills/motion-de-primeira/SKILL.md +3 -4
  46. package/studio/texto.py +208 -0
  47. package/voz/primo_voz/__main__.py +96 -236
  48. package/voz/primo_voz/transcrever.py +31 -1
  49. package/app/ansi.js +0 -55
  50. package/app/app.css +0 -500
  51. package/app/app.js +0 -1017
  52. package/app/icone.svg +0 -6
  53. package/app/index.html +0 -220
  54. package/app/primo-marca.js +0 -192
  55. package/lib/app.js +0 -615
  56. package/lib/icone.js +0 -263
  57. package/lib/instalar-app.js +0 -231
  58. package/lib/painel.js +0 -291
  59. package/voz/primo_voz/desperta.py +0 -195
  60. 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 aplicativozinho da orb é Python (ver voz/primo_voz). O que mora aqui é a
5
- * outra ponta: subir o processo, conversar com ele por JSON, e — a parte que
6
- * importa — entregar cada pedido ao MESMO agente do terminal.
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
- * ── POR QUE O CÉREBRO NÃO FOI PARA O PYTHON ──────────────────────────────
9
- * O Jarvis mantinha uma sessão do Claude Code num processo filho e conversava
10
- * por stream-json. Copiar aquele desenho aqui significaria uma segunda
11
- * implementação do laço do agente, das quarenta ferramentas, do portão de
12
- * permissão do computador e da corrente de conectores — em outra linguagem.
13
- *
14
- * Duas implementações do mesmo laço é a garantia de que uma delas está errada,
15
- * e de que a correção de amanhã vai para só uma. Então o modo voz manda o
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
- /* A instrução de FORMA. Ela não muda o que o agente faz — muda como ele
49
- conta. O que está proibido aqui é o que não existe em voz: marcador,
50
- caminho, código, e parágrafo que ninguém segura de ouvido. */
51
- const COMO_FALAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA. A pessoa está olhando a tela: ela vê o SEU cursor (o mouse e o teclado do computador dela, que você controla) se mexendo em tempo real.
52
-
53
- Responda em no máximo 3 frases curtas, em português falado. Nada de lista com
54
- marcador, nada de caminho de arquivo, nada de bloco de código, nada de
55
- markdown — nada disso existe no ouvido de alguém.
56
-
57
- Diga o que você FEZ e o que mudou, não como. Se algo deu errado, diga em uma
58
- frase o que foi e o que você precisa.
59
-
60
- ${DICA_COMPUTADOR}
61
-
62
- Faça o trabalho inteiro normalmente, com as ferramentas de sempre. O que muda
63
- é só o jeito de contar no fim.`;
64
-
65
- /* A forma da resposta de CONVERSA por voz ("oi", "tudo bem?", "o que você
66
- faz?"). Sem ferramenta e sem memória de tarefa: um "oi" ia para o agente
67
- inteiro, com a lista das tarefas antigas e a ordem de mexer no cursor, e
68
- ele respondia falando delas — "eu mandei oi e ele respondeu nada a ver". */
69
- const COMO_CONVERSAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA. Responda ao que a pessoa acabou de dizer, e só a isso, em uma ou duas frases curtas, em português falado, sem markdown. Não puxe assunto de tarefa antiga. Se for só um cumprimento, cumprimente de volta e pergunte o que ela quer fazer.`;
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. Dois casos não se resolvem com pip:
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'], ['tkinter', null]]) {
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. O tkinter não passa por aqui. */
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
- * Abre o modo voz e fica de pé até a janela fechar.
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)=>Promise<string>} op.pensar o agente de verdade
243
- * @param {(texto:string)=>void} op.mostrar o que aparece no terminal
244
- * @param {boolean} op.esperando fica dormindo até ouvir o nome
245
- * @returns {{processo, pronto: Promise}} o processo, e a promessa do fim
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, pensar, mostrar = () => {}, esperando = false }) {
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 — que aqui vira texto no
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
- let ocupado = false;
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 === 'erro') { mostrar(String(msg.texto || '')); continue; }
286
- if (msg.tipo !== 'pedido') continue;
287
-
288
- /* Um pedido por vez. A pessoa pode falar de novo enquanto ele
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, e uma linha solta ali quebraria a
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
- /* O PROCESSO VOLTA JUNTO COM A PROMESSA, e não é preciosismo de API.
320
- No modo de espera o /voice não bloqueia o terminal: ele sobe o ouvinte
321
- e devolve o prompt. Quem chamou precisa de um jeito de ENCERRAR aquilo
322
- depois (/voice parar, ou a saída do programa) — e, sem o handle, o
323
- único jeito seria matar pelo nome do processo, que atropela outra
324
- sessão do PrimoCode aberta na mesma máquina. */
325
- return { processo: p, pronto };
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 (PortAudio e o Tk do Python) — pelo Homebrew no
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 bonito e local (faster-whisper, vosk, PyObjC no macOS);
428
- * 3. os modelos — o Whisper (transcrição local) e o reconhecedor do nome.
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
- // O Tk do Homebrew tem versão casada com a do Python: python-tk@3.14
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((x) => (x === 'tkinter' ? 'python3-tk' : 'libportaudio2'));
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(precisa.map((x) => COMO_SISTEMA[x === 'tkinter' ? 'tkinter' : 'portaudio'].win32).join(' · '));
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
- let esperando = false;
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, COMO_CONVERSAR, conferir, instalar, preparar, porqueOPipFalhou, abrir, conferirEspera, baixarEspera, python, COMO_FALAR, PASTA };
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": "9.10.0",
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/voz-conversa.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/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/app.test.js && node test/app-janela.test.js && node test/aplicativo.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/janela.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"
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 (o mais reconhecível de todos), e gradiente de duas
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
- Espreme ao quicar e deixe a câmera andar até o fim.
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