primocode 8.42.2 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +42 -195
- package/lib/catalogo.js +100 -61
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +576 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +644 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +49 -6
- package/lib/marca.js +4 -1
- package/lib/markdown.js +376 -0
- package/lib/marketplace.js +189 -0
- package/lib/pasta.js +15 -4
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +106 -187
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +199 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +383 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/lib/tools.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
const catalogo = require('./catalogo.js');
|
|
11
|
+
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const os = require('os');
|
|
@@ -14,8 +16,7 @@ const { exec, execFile } = require('child_process');
|
|
|
14
16
|
const browser = require('./browser');
|
|
15
17
|
const desktop = require('./desktop');
|
|
16
18
|
const referencia = require('./referencia');
|
|
17
|
-
const
|
|
18
|
-
const acervo = require('./acervo');
|
|
19
|
+
const estudio = require('./estudio');
|
|
19
20
|
const cfg = require('./config');
|
|
20
21
|
|
|
21
22
|
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
@@ -171,11 +172,6 @@ async function runShell(command, timeout = 60000, cwd) {
|
|
|
171
172
|
// execFile com array de argumentos nunca passa por shell nenhum: "target" vira
|
|
172
173
|
// UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
|
|
173
174
|
// `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
|
|
174
|
-
// Quando o auto-abrir do Studio abriu pela última vez — o freio das mil abas.
|
|
175
|
-
let ultimaAberturaStudio = 0;
|
|
176
|
-
// Uma rodada de melhoria por tarefa (ver o aviso em studio_criar).
|
|
177
|
-
let melhorouStudio = false;
|
|
178
|
-
|
|
179
175
|
function openInBrowser(target) {
|
|
180
176
|
const p = os.platform();
|
|
181
177
|
const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
|
|
@@ -199,14 +195,43 @@ async function runInNewTerminal(command, cwd) {
|
|
|
199
195
|
// mesmo tempo (osascript, cmd.exe, sh) — "command" continua livre de
|
|
200
196
|
// propósito, é o próprio contrato desta tool (abrir terminal com um
|
|
201
197
|
// comando de shell de verdade).
|
|
202
|
-
|
|
198
|
+
/* A CONTRABARRA NÃO PODE ENTRAR NESTA LISTA NO WINDOWS.
|
|
199
|
+
Ela estava, e com isso a tool NUNCA funcionou lá: a pasta de trabalho é
|
|
200
|
+
sempre C:\Users\<nome>\primocode\<projeto> (path.join no win32 usa "\"),
|
|
201
|
+
então TODO caminho casava e a resposta era "Pasta com caractere não
|
|
202
|
+
permitido" — culpando a pasta criada pelo próprio produto, antes de
|
|
203
|
+
tentar abrir terminal nenhum. "sobe o servidor" quebrava 100% das vezes.
|
|
204
|
+
A contrabarra só é perigosa onde ela é ESCAPE (sh, osascript); no cmd.exe
|
|
205
|
+
ela é separador de caminho e nada mais. */
|
|
206
|
+
const proibidos = p === 'win32' ? /["'`;&|%^<>\r\n]/ : /["'`;&|$\\\r\n]/;
|
|
207
|
+
if (proibidos.test(pastaAlvo)) {
|
|
203
208
|
return { ok: false, error: `Pasta com caractere não permitido: ${pastaAlvo}` };
|
|
204
209
|
}
|
|
205
210
|
let cmd;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
let pastaDoProcesso;
|
|
212
|
+
if (p === 'darwin') {
|
|
213
|
+
/* O Terminal.app é outro processo, já rodando: ele NÃO herda a pasta
|
|
214
|
+
daqui, então o `cd` tem de ir no script mesmo. O espaço é escapado
|
|
215
|
+
com contrabarra — "\\ " neste template vira "\ " dentro da string do
|
|
216
|
+
AppleScript, que é o escape que o shell entende. Sem isso, uma pasta
|
|
217
|
+
como ~/Documents/Meus Projetos virava dois argumentos e o cd falhava
|
|
218
|
+
calado: o terminal abria na pasta errada e o comando rodava no lugar
|
|
219
|
+
errado, que é pior que não abrir. */
|
|
220
|
+
cmd = `osascript -e 'tell application "Terminal" to do script "cd ${pastaAlvo.replace(/ /g, '\\\\ ')} && ${command}"'`;
|
|
221
|
+
} else if (p === 'win32') {
|
|
222
|
+
// Aspas em volta do caminho: "C:\Users\Joel Silva\..." é comum, e sem
|
|
223
|
+
// elas o cmd corta no espaço. A aspa dupla já está barrada acima, então
|
|
224
|
+
// não há como sair delas.
|
|
225
|
+
cmd = `start cmd /k "cd /d "${pastaAlvo}" && ${command}"`;
|
|
226
|
+
} else {
|
|
227
|
+
/* No Linux o terminal é filho DESTE processo e herda a pasta — então
|
|
228
|
+
o caminho não precisa entrar na string de shell nenhuma. É a solução
|
|
229
|
+
mais segura das três: o que não é interpolado não pode ser mal
|
|
230
|
+
escapado. */
|
|
231
|
+
cmd = `x-terminal-emulator -e "bash -c '${command}; exec bash'"`;
|
|
232
|
+
pastaDoProcesso = pastaAlvo;
|
|
233
|
+
}
|
|
234
|
+
const r = await runShell(cmd, 60000, pastaDoProcesso);
|
|
210
235
|
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
|
|
211
236
|
}
|
|
212
237
|
|
|
@@ -270,6 +295,13 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
270
295
|
substituicoes: replace_all ? ocorrencias : 1,
|
|
271
296
|
linhasAntes: original.split('\n').length,
|
|
272
297
|
linhasDepois: atualizado.split('\n').length,
|
|
298
|
+
// As duas versões, para a TELA desenhar o diff. O `_` na frente marca
|
|
299
|
+
// o que é só nosso: o act.js apaga estas chaves antes de a conversa
|
|
300
|
+
// seguir, porque mandar o arquivo de volta ao modelo custaria
|
|
301
|
+
// centenas de tokens para ensinar a ele o texto que ele acabou de
|
|
302
|
+
// escrever.
|
|
303
|
+
_antes: original,
|
|
304
|
+
_depois: atualizado,
|
|
273
305
|
};
|
|
274
306
|
}
|
|
275
307
|
|
|
@@ -304,6 +336,7 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
304
336
|
if (!Array.isArray(edits) || !edits.length) return { ok: false, error: 'edits (lista) é obrigatório' };
|
|
305
337
|
|
|
306
338
|
let conteudo = fs.readFileSync(filePath, 'utf-8');
|
|
339
|
+
const original = conteudo;
|
|
307
340
|
const linhasAntes = conteudo.split('\n').length;
|
|
308
341
|
let substituicoes = 0;
|
|
309
342
|
|
|
@@ -329,6 +362,8 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
329
362
|
substituicoes,
|
|
330
363
|
linhasAntes,
|
|
331
364
|
linhasDepois: conteudo.split('\n').length,
|
|
365
|
+
_antes: original,
|
|
366
|
+
_depois: conteudo,
|
|
332
367
|
};
|
|
333
368
|
}
|
|
334
369
|
|
|
@@ -401,9 +436,20 @@ async function despachar(name, args, ctx) {
|
|
|
401
436
|
const filePath = resolveInProject(args.path, projectDir);
|
|
402
437
|
const content = args.content || '';
|
|
403
438
|
const created = !fs.existsSync(filePath);
|
|
439
|
+
// Reescrever um arquivo que já existe é a operação mais
|
|
440
|
+
// perigosa do agente: o conteúdo antigo some inteiro. Ler o
|
|
441
|
+
// que estava lá ANTES de escrever é o que permite mostrar na
|
|
442
|
+
// tela o que foi perdido.
|
|
443
|
+
let antes = '';
|
|
444
|
+
if (!created) { try { antes = fs.readFileSync(filePath, 'utf-8'); } catch { antes = ''; } }
|
|
404
445
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
405
446
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
406
|
-
return {
|
|
447
|
+
return {
|
|
448
|
+
ok: true, created,
|
|
449
|
+
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
450
|
+
bytes: content.length,
|
|
451
|
+
...(created ? {} : { _antes: antes, _depois: content }),
|
|
452
|
+
};
|
|
407
453
|
}
|
|
408
454
|
case 'edit_file':
|
|
409
455
|
return editFile(args, projectDir);
|
|
@@ -441,16 +487,6 @@ async function despachar(name, args, ctx) {
|
|
|
441
487
|
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
442
488
|
});
|
|
443
489
|
|
|
444
|
-
// Música e efeito sonoro da internet, com licença e crédito. O
|
|
445
|
-
// arquivo vem para a pasta do projeto e sobe para o estúdio;
|
|
446
|
-
// volta a URL "/midia/…" pronta para trilha.src ou som.arquivo.
|
|
447
|
-
case 'buscar_audio':
|
|
448
|
-
return require('./audio-livre.js').buscarAudio(args, { projectDir, studio });
|
|
449
|
-
|
|
450
|
-
// A logo e a identidade visual da empresa, lidas do site dela.
|
|
451
|
-
case 'marca_da_empresa':
|
|
452
|
-
return require('./marca.js').marcaDaEmpresa(args, { projectDir, studio });
|
|
453
|
-
|
|
454
490
|
// memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
|
|
455
491
|
// não alcançam por design (resolveInProject bloqueia ../ de propósito).
|
|
456
492
|
// Esta é a única porta para lá, sem reabrir a sandbox.
|
|
@@ -514,171 +550,36 @@ async function despachar(name, args, ctx) {
|
|
|
514
550
|
return desktop.scroll(args.dy != null ? args.dy : 300);
|
|
515
551
|
case 'desktop_size':
|
|
516
552
|
return desktop.getScreenSize();
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
case 'studio_criar': {
|
|
523
|
-
const r = await studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
|
|
524
|
-
// Criou? ABRE NO NAVEGADOR, sozinho. O dono: "quando ele faz um
|
|
525
|
-
// vídeo, uma apresentação, abre automático — a pessoa não vai
|
|
526
|
-
// saber abrir". E cada "abre pra mim" digitado depois custava
|
|
527
|
-
// uma tarefa inteira de tokens para o modelo redescobrir o
|
|
528
|
-
// link. Abrir é determinístico: não se gasta modelo com isso.
|
|
529
|
-
// Fica anotado na memória geral: é assim que "lembra do
|
|
530
|
-
// vídeo tal" funciona na conversa seguinte. O id é o que
|
|
531
|
-
// permite reabrir ou ajustar sem recriar.
|
|
532
|
-
if (r && r.ok !== false && r.id) {
|
|
533
|
-
cfg.lembrarCriacao({ ferramenta: r.ferramenta || args.ferramenta,
|
|
534
|
-
titulo: r.titulo || args.titulo, id: r.id });
|
|
535
|
-
}
|
|
536
|
-
if (r && r.ok !== false && r.ver) {
|
|
537
|
-
// UMA aba por tarefa. O modelo às vezes cria três versões
|
|
538
|
-
// seguidas, e abrir todas enchia o navegador do usuário de
|
|
539
|
-
// abas — "abre 1000 versões". A primeira abre; as outras
|
|
540
|
-
// são avisadas a ATUALIZAR a que já existe.
|
|
541
|
-
const agora = Date.now();
|
|
542
|
-
if (agora - ultimaAberturaStudio > 45000) {
|
|
543
|
-
ultimaAberturaStudio = agora;
|
|
544
|
-
try { openInBrowser(r.ver); } catch {}
|
|
545
|
-
r.abertoNoNavegador = true;
|
|
546
|
-
// O estúdio revisa a peça e devolve o que ficou
|
|
547
|
-
// monótono (mesmo fundo em tudo, transição repetida,
|
|
548
|
-
// sem trilha). Sem este ramo o aviso mandaria "chame
|
|
549
|
-
// finish, não ajuste" — e a revisão morreria ali, que
|
|
550
|
-
// é justamente o "fica tudo genérico".
|
|
551
|
-
//
|
|
552
|
-
// UMA rodada de conserto, e acabou: mais que isso
|
|
553
|
-
// vira o agente polindo sozinho enquanto o usuário
|
|
554
|
-
// espera.
|
|
555
|
-
if (Array.isArray(r.podeMelhorar) && r.podeMelhorar.length && !melhorouStudio) {
|
|
556
|
-
melhorouStudio = true;
|
|
557
|
-
r.aviso = 'JÁ ABERTO no navegador. Antes de finalizar, faça UMA correção: '
|
|
558
|
-
+ `use studio_atualizar no id "${r.id}" mandando SÓ as chaves que mudam, `
|
|
559
|
-
+ 'para resolver os pontos de "podeMelhorar". Depois chame finish. '
|
|
560
|
-
+ 'Não crie outro arquivo e não repita esta correção.';
|
|
561
|
-
} else {
|
|
562
|
-
r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
|
|
563
|
-
}
|
|
564
|
-
} else {
|
|
565
|
-
r.aviso = `Você JÁ criou uma versão nesta tarefa, e ela está aberta no navegador. NÃO crie outra: `
|
|
566
|
-
+ `para mudar algo, use studio_atualizar com o id "${r.id}". Chame finish AGORA.`;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return r;
|
|
570
|
-
}
|
|
571
|
-
case 'studio_ler':
|
|
572
|
-
return studio.ler(args.id);
|
|
573
|
-
case 'studio_atualizar':
|
|
574
|
-
return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
|
|
575
|
-
case 'studio_versoes':
|
|
576
|
-
return studio.versoesDe({ id: args.id, restaurar: args.restaurar });
|
|
577
|
-
case 'studio_listar':
|
|
578
|
-
return studio.listar();
|
|
579
|
-
case 'studio_midia':
|
|
580
|
-
// Sem arquivo, lista o que já foi enviado; com arquivo, envia.
|
|
581
|
-
return args.arquivo
|
|
582
|
-
? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
|
|
583
|
-
: studio.midiaListar();
|
|
584
|
-
/* ── MÍDIA DE VERDADE, VINDA DA INTERNET ──────────────────
|
|
585
|
-
*
|
|
586
|
-
* Buscar, baixar, recortar o fundo e já deixar na galeria do
|
|
587
|
-
* estúdio. O caminho inteiro é uma tool só do ponto de vista do
|
|
588
|
-
* agente (procurar → baixar), e o `paraEstudio` é o que evita a
|
|
589
|
-
* volta clássica: baixar num canto do disco e depois descobrir
|
|
590
|
-
* que a cena precisa de uma URL "/midia/...".
|
|
591
|
-
*
|
|
592
|
-
* A licença desce junto e fica gravada ao lado do arquivo. Um
|
|
593
|
-
* vídeo de cliente com foto CC BY sem crédito é problema do
|
|
594
|
-
* cliente, e descobrir isso depois de publicar é tarde. */
|
|
595
|
-
case 'midia_procurar':
|
|
596
|
-
return acervo.procurar(args.consulta || args.q || '', {
|
|
597
|
-
tipo: args.tipo, quantos: args.quantos, orientacao: args.orientacao,
|
|
598
|
-
licenca: args.licenca,
|
|
599
|
-
});
|
|
600
|
-
case 'midia_baixar': {
|
|
601
|
-
const r = await acervo.baixar(args.url, { titulo: args.titulo, meta: {
|
|
602
|
-
autor: args.autor || null, licenca: args.licenca || null,
|
|
603
|
-
fonte: args.fonte || null, pagina: args.pagina || null,
|
|
604
|
-
} });
|
|
605
|
-
if (!r.ok) return r;
|
|
606
|
-
let arquivo = r.arquivo;
|
|
607
|
-
if (args.semFundo) {
|
|
608
|
-
const corte = await acervo.tirarFundo(arquivo);
|
|
609
|
-
if (corte.ok) { arquivo = corte.arquivo; r.recorte = corte.metodo; r.avisoRecorte = corte.aviso; }
|
|
610
|
-
else r.avisoRecorte = corte.error;
|
|
611
|
-
}
|
|
612
|
-
r.arquivo = arquivo;
|
|
613
|
-
if (args.paraEstudio !== false) {
|
|
614
|
-
const g = await studio.midiaEnviar({ arquivo, nome: args.nome });
|
|
615
|
-
// O "/midia/..." é o que vai em `src` e em `fundo.imagem`.
|
|
616
|
-
if (g && g.ok !== false) r.src = g.url || g.src || null;
|
|
617
|
-
else r.avisoGaleria = (g && g.error) || 'não deu para mandar para a galeria';
|
|
618
|
-
}
|
|
619
|
-
return r;
|
|
620
|
-
}
|
|
621
|
-
case 'midia_sem_fundo':
|
|
622
|
-
return acervo.tirarFundo(resolveInProject(args.arquivo, projectDir), { destino: args.destino });
|
|
623
|
-
case 'midia_creditos':
|
|
624
|
-
return { ok: true, creditos: acervo.creditos() };
|
|
625
|
-
|
|
626
|
-
case 'studio_analisar': {
|
|
627
|
-
// Ouvir o vídeo para achar as pausas. A conta acontece num
|
|
628
|
-
// navegador (é quem sabe decodificar MP4 sem instalar nada) —
|
|
629
|
-
// e ESCONDIDO: a conta não é da pessoa, e uma aba piscando no
|
|
630
|
-
// navegador dela é encanamento aparecendo. Sem Chrome nem
|
|
631
|
-
// Chromium nesta máquina, cai no navegador dela, como antes.
|
|
632
|
-
const visivel = process.env.PRIMOCODE_ANALISE_VISIVEL === '1';
|
|
633
|
-
const abrir = visivel ? openInBrowser : async (url) => {
|
|
634
|
-
try { return await browser.abrirEscondido(url); }
|
|
635
|
-
catch { openInBrowser(url); return null; }
|
|
636
|
-
};
|
|
637
|
-
return studio.analisarAudio({
|
|
638
|
-
arquivo: args.arquivo, pausa: args.pausa, manter: args.manter,
|
|
639
|
-
repetida: args.repetida, abrir, escondido: visivel ? null : browser.rodarEscondido,
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
case 'studio_exportar':
|
|
643
|
-
// O mp4 sem clique: a página do player roda escondida e gera
|
|
644
|
-
// pelo mesmo caminho do botão. Ver lib/studio.js.
|
|
645
|
-
// Com o Remotion mesclado: o vídeo quadro a quadro dele, o
|
|
646
|
-
// áudio do Studio. PRIMOCODE_SEM_REMOTION=1 desliga.
|
|
647
|
-
return studio.exportarVideo({
|
|
648
|
-
id: args.id, destino: args.destino, fps: args.fps,
|
|
649
|
-
escondido: browser.rodarEscondido, pasta: projectDir || process.cwd(),
|
|
650
|
-
remotion: process.env.PRIMOCODE_SEM_REMOTION !== '1',
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
/* A MESMA peça, virando projeto Remotion.
|
|
654
|
-
*
|
|
655
|
-
* O Studio continua sendo o caminho normal: exporta em segundos,
|
|
656
|
-
* sem instalar nada. Isto é para quando o usuário pede Remotion
|
|
657
|
-
* pelo nome, ou quando a peça precisa do que só ele dá — render
|
|
658
|
-
* determinístico, vídeo longo, fps e codec escolhidos, ou o
|
|
659
|
-
* projeto em React para a pessoa mexer.
|
|
553
|
+
/* ── O ESTÚDIO ─────────────────────────────────────────────────
|
|
554
|
+
* Sete ferramentas, e nenhuma delas fala com servidor nenhum: o
|
|
555
|
+
* Studio antigo subia um Python em localhost:7777 porque tinha
|
|
556
|
+
* uma interface web para servir. A interface morreu — a IA faz
|
|
557
|
+
* tudo e abre o vídeo pronto — e o servidor morreu com ela.
|
|
660
558
|
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
559
|
+
* Cada peça é uma PASTA, cada render é o Remotion. Ver
|
|
560
|
+
* lib/estudio/ para o porquê de cada peça.
|
|
561
|
+
*/
|
|
562
|
+
case 'estudio_projeto':
|
|
563
|
+
return estudio.ferramentaProjeto(args);
|
|
564
|
+
case 'estudio_plano':
|
|
565
|
+
return estudio.ferramentaPlano(args);
|
|
566
|
+
case 'estudio_midia':
|
|
567
|
+
return estudio.ferramentaMidia(args);
|
|
568
|
+
case 'estudio_narrar':
|
|
569
|
+
return estudio.ferramentaNarrar(args);
|
|
570
|
+
case 'estudio_video':
|
|
571
|
+
// A transcrição precisa do servidor (a chave é dele), então o
|
|
572
|
+
// contexto do pedido atravessa até aqui.
|
|
573
|
+
return estudio.ferramentaVideo(args, ctx);
|
|
574
|
+
case 'estudio_render':
|
|
575
|
+
return estudio.ferramentaRender(args, {
|
|
576
|
+
...ctx,
|
|
577
|
+
// O vídeo pronto abre para a pessoa assistir. É a única
|
|
578
|
+
// coisa que abre: foi o vídeo que ela pediu.
|
|
579
|
+
abrir: (arquivo) => { try { openInBrowser(arquivo); } catch {} },
|
|
675
580
|
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// é escolha do usuário, não efeito colateral de criar a pasta.
|
|
679
|
-
const feito = await remotion.renderizar({ pasta: destino });
|
|
680
|
-
return Object.assign(r, { render: feito });
|
|
681
|
-
}
|
|
581
|
+
case 'estudio_site':
|
|
582
|
+
return estudio.ferramentaSite(args);
|
|
682
583
|
|
|
683
584
|
case 'desktop_double_click':
|
|
684
585
|
return desktop.doubleClick(args.x, args.y);
|
|
@@ -698,7 +599,25 @@ async function despachar(name, args, ctx) {
|
|
|
698
599
|
});
|
|
699
600
|
|
|
700
601
|
default:
|
|
701
|
-
|
|
602
|
+
/* O MODELO PRECISA CONSEGUIR SE CORRIGIR AQUI.
|
|
603
|
+
"Tool não implementada: studio_criar" não diz o que existe,
|
|
604
|
+
e o que acontecia era o agente desistir e responder ao
|
|
605
|
+
usuário que a ferramenta necessária "não está disponível".
|
|
606
|
+
Dizendo QUAIS existem — e a mais parecida — ele chama a
|
|
607
|
+
certa na volta seguinte, na mesma tarefa.
|
|
608
|
+
|
|
609
|
+
Isto é rede, não conserto: a causa é a lista do servidor
|
|
610
|
+
divergir do catálogo daqui, e ela se conserta lá. Mas a rede
|
|
611
|
+
é o que separa uma tarefa que se recupera de uma que morre
|
|
612
|
+
com o usuário esperando. */
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: `A ferramenta "${name}" não existe.`,
|
|
616
|
+
parecidas: catalogo.parecidasCom(name),
|
|
617
|
+
comoResolver: 'chame uma das ferramentas que existem de verdade — as "parecidas" '
|
|
618
|
+
+ 'acima são as de nome mais próximo. Não invente nome de ferramenta, e não '
|
|
619
|
+
+ 'desista da tarefa por causa disto.',
|
|
620
|
+
};
|
|
702
621
|
}
|
|
703
622
|
}
|
|
704
623
|
}
|
package/lib/ui.js
CHANGED
|
@@ -160,9 +160,10 @@ const TIPS = [
|
|
|
160
160
|
];
|
|
161
161
|
|
|
162
162
|
const NEWS = [
|
|
163
|
-
'Vídeo com DIRETOR: roteiro, narração
|
|
164
|
-
'Ele busca e baixa imagem, logo e música — com a licença junto',
|
|
165
|
-
'
|
|
163
|
+
'Vídeo com DIRETOR: roteiro, narração e voz decididos antes de produzir',
|
|
164
|
+
'Ele busca e baixa imagem, logo e música — com a licença junto do arquivo',
|
|
165
|
+
'/voice abre o Primo Code Voice · /marketplace instala skills',
|
|
166
|
+
'Do "high" para cima ele PESQUISA referências antes de escrever',
|
|
166
167
|
'Esc Esc a qualquer momento para o agente no meio do trabalho',
|
|
167
168
|
];
|
|
168
169
|
|
|
@@ -313,6 +314,9 @@ function actionHeader(name, args) {
|
|
|
313
314
|
const label = ROTULOS[name] || name;
|
|
314
315
|
let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
|
|
315
316
|
argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
|
|
317
|
+
// Uma tool sem argumento nenhum caía no JSON.stringify e desenhava "{}"
|
|
318
|
+
// ao lado do nome — ruído que não diz nada.
|
|
319
|
+
if (argStr === '{}' || argStr === 'null' || argStr === 'undefined') argStr = '';
|
|
316
320
|
if (argStr.length > 96) argStr = argStr.slice(0, 95) + '…';
|
|
317
321
|
return ` ${c.brand('●')} ${c.bold(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
318
322
|
}
|
package/lib/voz.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voz.js — o lado Node do modo voz.
|
|
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.
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const { spawn, execFileSync } = require('child_process');
|
|
32
|
+
|
|
33
|
+
const PASTA = path.join(__dirname, '..', 'voz');
|
|
34
|
+
|
|
35
|
+
/* A instrução de FORMA. Ela não muda o que o agente faz — muda como ele
|
|
36
|
+
conta. O que está proibido aqui é o que não existe em voz: marcador,
|
|
37
|
+
caminho, código, e parágrafo que ninguém segura de ouvido. */
|
|
38
|
+
const COMO_FALAR = `[MODO VOZ] A sua resposta vai ser LIDA EM VOZ ALTA para a pessoa, e ela não vê tela nenhuma.
|
|
39
|
+
|
|
40
|
+
Responda em no máximo 3 frases curtas, em português falado. Nada de lista com
|
|
41
|
+
marcador, nada de caminho de arquivo, nada de bloco de código, nada de
|
|
42
|
+
markdown — nada disso existe no ouvido de alguém.
|
|
43
|
+
|
|
44
|
+
Diga o que você FEZ e o que mudou, não como. Se algo deu errado, diga em uma
|
|
45
|
+
frase o que foi e o que você precisa.
|
|
46
|
+
|
|
47
|
+
Faça o trabalho inteiro normalmente, com as ferramentas de sempre. O que muda
|
|
48
|
+
é só o jeito de contar no fim.`;
|
|
49
|
+
|
|
50
|
+
/** Onde está o Python 3. Mesmo problema do Estúdio, mesma cura. */
|
|
51
|
+
function python() {
|
|
52
|
+
const nomes = process.platform === 'win32'
|
|
53
|
+
? ['python', 'py', 'python3']
|
|
54
|
+
: ['python3', 'python', '/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3'];
|
|
55
|
+
for (const n of nomes) {
|
|
56
|
+
try {
|
|
57
|
+
const v = execFileSync(n, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }).toString();
|
|
58
|
+
if (/Python 3/.test(v)) return n;
|
|
59
|
+
} catch { /* próximo */ }
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* O que falta para o modo voz abrir — e QUAL é a falta.
|
|
66
|
+
*
|
|
67
|
+
* A distinção é a diferença entre um recado que resolve e um que faz a pessoa
|
|
68
|
+
* dar voltas. Dois casos não se resolvem com pip:
|
|
69
|
+
*
|
|
70
|
+
* · `sounddevice` instalado, mas sem o PortAudio do sistema. O import
|
|
71
|
+
* explode com "PortAudio library not found", e mandar `pip install
|
|
72
|
+
* sounddevice` ali é mandar instalar o que já está instalado.
|
|
73
|
+
* · `tkinter` vem com o Python, mas algumas distribuições Linux o separam
|
|
74
|
+
* num pacote do sistema — e `pip install tkinter` não existe.
|
|
75
|
+
*
|
|
76
|
+
* Por isso a checagem lê a MENSAGEM do erro, e não só o fato de ter falhado.
|
|
77
|
+
*/
|
|
78
|
+
function faltando(py) {
|
|
79
|
+
const falta = [];
|
|
80
|
+
for (const [modulo, pacote] of [['numpy', 'numpy'], ['sounddevice', 'sounddevice'], ['tkinter', null]]) {
|
|
81
|
+
try {
|
|
82
|
+
execFileSync(py, ['-c', `import ${modulo}`], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 20000 });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
const saida = String((e.stderr || '') + (e.stdout || ''));
|
|
85
|
+
if (/portaudio/i.test(saida)) {
|
|
86
|
+
falta.push({ modulo, pacote: null, sistema: true, porque: 'o PortAudio, que é biblioteca do sistema' });
|
|
87
|
+
} else {
|
|
88
|
+
falta.push({ modulo, pacote });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return falta;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Como instalar uma biblioteca de sistema, neste sistema. */
|
|
96
|
+
const COMO_SISTEMA = {
|
|
97
|
+
portaudio: {
|
|
98
|
+
darwin: 'brew install portaudio',
|
|
99
|
+
linux: 'sudo apt install libportaudio2 (ou o equivalente da sua distribuição)',
|
|
100
|
+
win32: 'no Windows ele vem junto com o sounddevice — reinstale com pip',
|
|
101
|
+
},
|
|
102
|
+
tkinter: {
|
|
103
|
+
darwin: 'brew install python-tk',
|
|
104
|
+
linux: 'sudo apt install python3-tk',
|
|
105
|
+
win32: 'reinstale o Python pelo python.org, marcando "tcl/tk"',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Confere o que é preciso, sem instalar nada.
|
|
111
|
+
*
|
|
112
|
+
* O tkinter é o caso especial: ele vem com o Python, mas em algumas
|
|
113
|
+
* distribuições Linux ele foi separado num pacote do SISTEMA — e `pip install
|
|
114
|
+
* tkinter` não existe. Dizer "instale com pip" ali seria mandar a pessoa a um
|
|
115
|
+
* lugar onde não há nada.
|
|
116
|
+
*/
|
|
117
|
+
function conferir() {
|
|
118
|
+
const py = python();
|
|
119
|
+
if (!py) {
|
|
120
|
+
return { ok: false, error: 'o modo voz precisa do Python 3, e não achei nenhum nesta máquina.',
|
|
121
|
+
comoResolver: 'instale em python.org e abra o terminal de novo' };
|
|
122
|
+
}
|
|
123
|
+
const falta = faltando(py);
|
|
124
|
+
if (!falta.length) return { ok: true, python: py };
|
|
125
|
+
|
|
126
|
+
const pip = falta.filter((f) => f.pacote).map((f) => f.pacote);
|
|
127
|
+
const linhas = [];
|
|
128
|
+
if (pip.length) linhas.push(`${py} -m pip install ${pip.join(' ')}`);
|
|
129
|
+
if (falta.some((f) => f.modulo === 'sounddevice' && f.sistema)) {
|
|
130
|
+
linhas.push(COMO_SISTEMA.portaudio[process.platform] || COMO_SISTEMA.portaudio.linux);
|
|
131
|
+
}
|
|
132
|
+
if (falta.some((f) => f.modulo === 'tkinter')) {
|
|
133
|
+
linhas.push(COMO_SISTEMA.tkinter[process.platform] || COMO_SISTEMA.tkinter.linux);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
python: py,
|
|
139
|
+
falta: falta.map((f) => f.modulo),
|
|
140
|
+
error: `falta o que o modo voz precisa: ${falta.map((f) => f.porque || f.modulo).join(', ')}`,
|
|
141
|
+
comoResolver: linhas.join('\n '),
|
|
142
|
+
// Só o que o pip resolve entra no "instalar agora": oferecer instalar
|
|
143
|
+
// o que ele não instala é prometer o que não se cumpre.
|
|
144
|
+
instalavel: pip,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Instala o que dá para instalar com pip. O tkinter não passa por aqui. */
|
|
149
|
+
function instalar(py, pacotes, aoFalar = () => {}) {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
aoFalar(`instalando ${pacotes.join(', ')} (uma vez, alguns minutos)`);
|
|
152
|
+
const p = spawn(py, ['-m', 'pip', 'install', '--quiet', ...pacotes]);
|
|
153
|
+
let erro = '';
|
|
154
|
+
p.stderr.on('data', (d) => { erro += d.toString(); });
|
|
155
|
+
p.on('close', (code) => resolve(code === 0
|
|
156
|
+
? { ok: true }
|
|
157
|
+
: { ok: false, error: erro.slice(-400).trim() || `pip saiu com ${code}` }));
|
|
158
|
+
p.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Abre o modo voz e fica de pé até a janela fechar.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} op
|
|
166
|
+
* @param {string} op.python
|
|
167
|
+
* @param {string} op.servidor
|
|
168
|
+
* @param {string} op.token
|
|
169
|
+
* @param {string} op.voz
|
|
170
|
+
* @param {(texto:string)=>Promise<string>} op.pensar o agente de verdade
|
|
171
|
+
* @param {(texto:string)=>void} op.mostrar o que aparece no terminal
|
|
172
|
+
*/
|
|
173
|
+
function abrir({ python: py, servidor, token, voz, pensar, mostrar = () => {} }) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const p = spawn(py, ['-m', 'primo_voz'], {
|
|
176
|
+
cwd: PASTA,
|
|
177
|
+
env: {
|
|
178
|
+
...process.env,
|
|
179
|
+
PYTHONPATH: PASTA,
|
|
180
|
+
PRIMOCODE_SERVER: servidor,
|
|
181
|
+
PRIMOCODE_TOKEN: token || '',
|
|
182
|
+
PRIMOCODE_VOZ: voz || 'antonio',
|
|
183
|
+
PYTHONUNBUFFERED: '1',
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const manda = (obj) => {
|
|
188
|
+
try { p.stdin.write(JSON.stringify(obj) + '\n'); } catch { /* já fechou */ }
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let buffer = '';
|
|
192
|
+
let ocupado = false;
|
|
193
|
+
|
|
194
|
+
p.stdout.on('data', async (d) => {
|
|
195
|
+
buffer += d.toString();
|
|
196
|
+
const linhas = buffer.split('\n');
|
|
197
|
+
buffer = linhas.pop();
|
|
198
|
+
for (const linha of linhas) {
|
|
199
|
+
if (!linha.trim()) continue;
|
|
200
|
+
let msg;
|
|
201
|
+
try { msg = JSON.parse(linha); } catch { continue; }
|
|
202
|
+
|
|
203
|
+
if (msg.tipo === 'fim') { try { p.kill(); } catch {} return; }
|
|
204
|
+
if (msg.tipo === 'erro') { mostrar(String(msg.texto || '')); continue; }
|
|
205
|
+
if (msg.tipo !== 'pedido') continue;
|
|
206
|
+
|
|
207
|
+
/* Um pedido por vez. A pessoa pode falar de novo enquanto ele
|
|
208
|
+
trabalha — e dois agentes rodando ao mesmo tempo na mesma
|
|
209
|
+
pasta é a receita para um escrever por cima do outro. */
|
|
210
|
+
if (ocupado) { manda({ tipo: 'resposta', texto: 'Ainda estou terminando o anterior.' }); continue; }
|
|
211
|
+
ocupado = true;
|
|
212
|
+
|
|
213
|
+
mostrar(String(msg.texto || ''));
|
|
214
|
+
manda({ tipo: 'estado', estado: 'pensando' });
|
|
215
|
+
try {
|
|
216
|
+
const resposta = await pensar(String(msg.texto || ''));
|
|
217
|
+
manda({ tipo: 'resposta', texto: resposta || 'Feito.' });
|
|
218
|
+
} catch (e) {
|
|
219
|
+
manda({ tipo: 'erro', texto: `Não consegui: ${e.message}` });
|
|
220
|
+
} finally {
|
|
221
|
+
ocupado = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// O Python fala com o terminal pela saída de ERRO, de propósito: o
|
|
227
|
+
// stdout inteiro é do protocolo, e uma linha solta ali quebraria a
|
|
228
|
+
// conversa entre os dois processos.
|
|
229
|
+
p.stderr.on('data', (d) => {
|
|
230
|
+
const texto = d.toString().trim();
|
|
231
|
+
if (texto) mostrar(texto, true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
p.on('close', () => resolve({ ok: true }));
|
|
235
|
+
p.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { conferir, instalar, abrir, python, COMO_FALAR, PASTA };
|