primocode 10.0.0 → 10.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.
Files changed (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. package/studio/voz_kokoro.py +98 -0
package/lib/tools.js CHANGED
@@ -209,9 +209,35 @@ function grepFiles({ pattern, glob, ignoreCase, contexto = 0, maxResultados = 10
209
209
  return { ok: true, arquivosVistos, total: resultados.length, resultados, truncado: resultados.length >= maxResultados };
210
210
  }
211
211
 
212
- async function runShell(command, timeout = 60000, cwd) {
212
+ /* ── O SHELL DO AGENTE NASCE SEM SEGREDO ──────────────────────────────────
213
+ Antes, o run_shell herdava o process.env inteiro: o agente rodava `env` e
214
+ lia o NPM_TOKEN, a chave da Anthropic, o GH_TOKEN de quem usa — e o que o
215
+ agente lê pode ir parar no modelo, num arquivo ou numa página (prompt
216
+ injection). Peça 3 do OpenBot (comum/desmontados/copilotkit-openbot.md):
217
+ o shell do agente não herda o ambiente do processo pai.
218
+
219
+ Filtro por NOME, não lista do que passa: PATH, HOME, LANG, TERM, SHELL,
220
+ USER, NODE_*, nvm etc. continuam — sem eles `npm`, `node` e `git` quebram.
221
+ Na dúvida o nome sai: um token a mais escondido custa pouco, um a menos
222
+ vaza. Só o caminho do agente (execute → run_shell) é filtrado; os usos
223
+ internos (abrir terminal, desinstalar) seguem com o ambiente inteiro. */
224
+ const NOME_SECRETO = /(TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|AUTH|COOKIE|SESSION)/i;
225
+ const PREFIXO_SECRETO = /^(AWS_|OPENAI_|ANTHROPIC_|GEMINI_|GOOGLE_API|GROQ_|OPENROUTER_|AZURE_|HF_|HUGGING)/i;
226
+ const SEMPRE_PASSA = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'PWD', 'SSH_AUTH_SOCK']);
227
+
228
+ function ambienteLimpo(env = process.env) {
229
+ const limpo = {};
230
+ for (const [nome, valor] of Object.entries(env)) {
231
+ if (valor === undefined) continue;
232
+ if (!SEMPRE_PASSA.has(nome) && (NOME_SECRETO.test(nome) || PREFIXO_SECRETO.test(nome))) continue;
233
+ limpo[nome] = valor;
234
+ }
235
+ return limpo;
236
+ }
237
+
238
+ async function runShell(command, timeout = 60000, cwd, env) {
213
239
  return new Promise((resolve) => {
214
- exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
240
+ exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024, env: env || process.env }, (error, stdout, stderr) => {
215
241
  if (error) resolve({ ok: false, code: error.code ?? 1, stdout, stderr, killed: error.killed });
216
242
  else resolve({ ok: true, code: 0, stdout, stderr });
217
243
  });
@@ -402,6 +428,13 @@ function readFile({ path: rawPath, offset, limit }, projectDir) {
402
428
  };
403
429
  }
404
430
 
431
+ /* Arquivo pequeno vem INTEIRO, mesmo pedido aos pedaços: o modelo lia um
432
+ app.js de 393 linhas de 50 em 50, cada pedaço uma ida ao servidor
433
+ (teste da loja, 01/10). Inteiro, uma vez, custa menos. */
434
+ if (todas.length <= 400 && bruto.length <= 40000) {
435
+ return { ok: true, content: todas.map((l, i) => `${i + 1}\t${l}`).join('\n'), linhas: todas.length, bytes: bruto.length,
436
+ inteiro: true, dica: 'arquivo pequeno: veio inteiro, com o número das linhas — não precisa ler de novo' };
437
+ }
405
438
  const inicio = Math.max(0, (offset || 1) - 1);
406
439
  const fim = Math.min(todas.length, inicio + (limit || 200));
407
440
  const recorte = todas.slice(inicio, fim).map((l, i) => `${inicio + i + 1}\t${l}`).join('\n');
@@ -553,13 +586,21 @@ async function despachar(name, args, ctx) {
553
586
  case 'grep':
554
587
  return grepFiles(args, projectDir);
555
588
  case 'run_shell':
556
- return runShell(args.command, args.timeout_ms || 60000, projectDir);
589
+ return runShell(args.command, args.timeout_ms || 60000, projectDir, ambienteLimpo());
557
590
  case 'run_in_new_terminal':
558
591
  return runInNewTerminal(args.command, args.cwd || projectDir);
559
592
  case 'open_in_browser': {
560
593
  const alvo = /^https?:\/\//i.test(args.target || '')
561
594
  ? args.target
562
595
  : resolveInProject(args.target, projectDir);
596
+ /* Site da web abre À VISTA no Chrome controlado (o cursor vai na
597
+ barra e digita). Arquivo do projeto continua no navegador padrão. */
598
+ if (/^https?:/i.test(alvo) && process.env.PRIMOCODE_BROWSER_HEADLESS !== '1' && require('./cursor').disponivel()) {
599
+ try {
600
+ const r = await browser.abrirVisivel(alvo);
601
+ if (r && r.ok !== false) return { ok: true, opener: 'chrome à vista', url: r.url, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR };
602
+ } catch { /* cai no navegador padrão */ }
603
+ }
563
604
  return openInBrowser(alvo);
564
605
  }
565
606
  case 'multi_edit':
@@ -570,11 +611,14 @@ async function despachar(name, args, ctx) {
570
611
  // Pesquisa de referência: abre documentação e exemplos sobre o
571
612
  // pedido e devolve o TEXTO para o modelo aprender antes de
572
613
  // escrever. Aparece no terminal, com as fontes — nada oculto.
573
- case 'pesquisar_referencia':
574
- return referencia.pesquisar({
614
+ case 'pesquisar_referencia': {
615
+ const r = await referencia.pesquisar({
575
616
  consulta: args.consulta || args.query || '',
576
617
  paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
577
618
  });
619
+ // Escopo fechado ("só do site x.com"): o buscador devolve o mundo; aqui só passa o site.
620
+ return require('./estudio/referencias.js').filtrar(r);
621
+ }
578
622
 
579
623
  // memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
580
624
  // não alcançam por design (resolveInProject bloqueia ../ de propósito).
@@ -595,7 +639,8 @@ async function despachar(name, args, ctx) {
595
639
  const alvo = /^https?:\/\//i.test(args.url || '')
596
640
  ? args.url
597
641
  : 'file://' + resolveInProject(args.url, projectDir);
598
- const r = await browser.open(alvo);
642
+ // Endereço da web abre À VISTA: o cursor vai na barra e digita (browser.abrirVisivel).
643
+ const r = /^https?:/i.test(alvo) ? await browser.abrirVisivel(alvo) : await browser.open(alvo);
599
644
  return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR } : r;
600
645
  }
601
646
  case 'browser_click': {
@@ -692,7 +737,7 @@ async function despachar(name, args, ctx) {
692
737
  * lib/estudio/ para o porquê de cada peça.
693
738
  */
694
739
  case 'estudio_projeto':
695
- return estudio.ferramentaProjeto(args);
740
+ return estudio.ferramentaProjeto(args, ctx);
696
741
  case 'estudio_plano':
697
742
  return estudio.ferramentaPlano(args);
698
743
  case 'estudio_midia':
@@ -834,7 +879,7 @@ async function uninstall() {
834
879
  }
835
880
 
836
881
  module.exports = {
837
- fmtBytes, treeDir, runShell, openInBrowser, runInNewTerminal,
882
+ fmtBytes, treeDir, runShell, ambienteLimpo, openInBrowser, runInNewTerminal,
838
883
  execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
839
884
  multiEdit, writeTodos, getTodos,
840
885
  };
package/lib/ui.js CHANGED
@@ -310,11 +310,11 @@ const TIPS = [
310
310
  ];
311
311
 
312
312
  const NEWS = [
313
- 'Digite enquanto ele trabalha: o recado entra na tarefa depois do passo atual',
314
- 'Mais rápido: não relê o que escreveu, e a espera aparece com os segundos',
315
- 'primocode video help — o editor de vídeo que o agente opera sozinho',
316
- 'Motion novo: vox, marca e lancamento · /voice: fale, e ele responde em voz',
317
- 'Esc Esc a qualquer momento para o agente no meio do trabalho',
313
+ 'A IA decide o caminho: skills e equipe num cardápio, ela escolhe o que usa',
314
+ 'Até 8 agentes em paralelo, cada um volta com uma entrega — e passa a vez',
315
+ 'Edite falando no vídeo: primocode video auto aplica o que você diz, na hora',
316
+ 'Colou texto grande? Vira etiqueta [Colado #1 · N linhas] na caixa',
317
+ 'Escopo fechado de verdade: "só do site x.com" filtra a pesquisa',
318
318
  ];
319
319
 
320
320
  // ── Cabeçalho ────────────────────────────────────────────────────────────
@@ -419,6 +419,21 @@ class Spinner {
419
419
  // Fora de um terminal (pipe, CI) a animação só sujaria a saída.
420
420
  if (!process.stdout.isTTY) return this;
421
421
  this.desde = Date.now();
422
+ /* Com a caixa de digitar no ar, o spinner desenha DENTRO dela (ver
423
+ mostrarSpinner em lib/entrada.js): escrever "\r" por cima apagava
424
+ a caixa e a barra de status enquanto ele girava. */
425
+ const caixa = Spinner.caixa;
426
+ if (caixa && caixa.mostrarSpinner && caixa.desenhado !== undefined) {
427
+ this.naCaixa = caixa;
428
+ this.timer = setInterval(() => {
429
+ if (this.textos.length > 1) this.text = this.textos[Math.floor((Date.now() - this.desde) / 4000) % this.textos.length];
430
+ const seg = Math.floor((Date.now() - this.desde) / 1000);
431
+ const extra = (seg >= 3 ? ` · ${seg}s` : '') + (this.detalhe ? ` · ${this.detalhe}` : '');
432
+ caixa.mostrarSpinner(`${c.muted(this.frames[this.i])} ${c.muted(this.text)}${c.dim(extra)}`);
433
+ this.i = (this.i + 1) % this.frames.length;
434
+ }, 120);
435
+ return this;
436
+ }
422
437
  this.timer = setInterval(() => {
423
438
  // Com mais de uma frase, troca a cada 4s. A frase nova pode ser mais
424
439
  // curta que a anterior: o resto da linha velha ficaria na tela, então
@@ -439,10 +454,29 @@ class Spinner {
439
454
  return this;
440
455
  }
441
456
  stop() {
457
+ if (this.naCaixa) {
458
+ if (this.timer) { clearInterval(this.timer); this.timer = null; }
459
+ this.naCaixa.mostrarSpinner('');
460
+ this.naCaixa = null;
461
+ return;
462
+ }
442
463
  if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(Math.max(80, this.largura + 4))}\r`); }
443
464
  }
444
465
  }
445
466
 
467
+ /* O TÍTULO DO TERMINAL: é por ele que o Orca (e qualquer terminal com abas)
468
+ sabe que este painel é um agente e se está trabalhando. O Orca lê o glifo
469
+ de spinner em braile como "trabalhando" e o nome como identidade — então
470
+ "⠋ PrimoCode — tarefa" enquanto trabalha, "PrimoCode" parado. Vai pelo
471
+ canal cru da caixa: pelo stdout embrulhado, um escape sem quebra de linha
472
+ deixaria a caixa apagada (o mesmo defeito do spinner). */
473
+ function titulo(texto) {
474
+ if (!process.stdout.isTTY || process.env.PRIMOCODE_SEM_TITULO === '1') return;
475
+ const limpo = String(texto || '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 80);
476
+ const escrever = Spinner.caixa && Spinner.caixa.escreverCru ? Spinner.caixa.escreverCru : (s) => process.stdout.write(s);
477
+ try { escrever(`\x1b]0;${limpo}\x07`); } catch { /* terminal fechado */ }
478
+ }
479
+
446
480
  // O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
447
481
  const ROTULOS = {
448
482
  read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
@@ -616,4 +650,5 @@ function friendlyError(raw) {
616
650
  }
617
651
 
618
652
 
619
- module.exports = { c, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
653
+ module.exports = {
654
+ titulo, c, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
@@ -0,0 +1,329 @@
1
+ /**
2
+ * auto.js — `primocode video auto`: o vídeo é o pedido.
3
+ *
4
+ * "Quero que ele leia o que eu falo no vídeo e aplique cada pedido no
5
+ * momento em que eu digo." (01/10)
6
+ *
7
+ * A pessoa grava falando o que quer ("separe em camadas", "coloca o logo do
8
+ * Claude aqui", "com o estalar de dedos me coloca numa praia") e o editor
9
+ * acha cada pedido na transcrição e o ancora no que o rastro viu: a palavra,
10
+ * a mão que aparece logo depois, o estalo que vem em seguida, o juntar das
11
+ * mãos. Nada de quadro escrito à mão.
12
+ *
13
+ * planejar(dados) → [{op, args(segundos da origem), motivo}] pura (o teste roda)
14
+ * OPS.auto.run → junta os dados, executa o plano, devolve o que fez
15
+ *
16
+ * Tudo em SEGUNDOS DA ORIGEM até a hora de executar: os cortes (silêncio,
17
+ * fim) vão por último e a legenda depois deles, já nos cortes.
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const PED = require('./pedido.js');
23
+
24
+ const tira = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
25
+ const r2 = (n) => Math.round(n * 100) / 100;
26
+
27
+ /* O que o Whisper ouve no lugar do que a pessoa disse — visto nos dois
28
+ primeiros vídeos de verdade: "cloud", "Claudio", "xgbt", "estravar",
29
+ "instruar de dedos". Normaliza antes de qualquer regra. */
30
+ const OUVIDO = [
31
+ [/\b(claudio|claudia|cloud|clode|clod|claud|cloude)\b/g, 'claude'],
32
+ [/\b(x ?gbt|x ?gpt|chat ?gbt|chat ?gpt|chat ?jpt|tchat ?gpt|chatgbt|gbt)\b/g, 'chatgpt'],
33
+ [/\bgoogle\s+(chrome|drive)\b/g, '$1'],
34
+ [/\b(estr[ae]l\w*|estrav\w*|estal\w*|instru\w*|estral\w*)\s+(?:os|nos|dos|de|o|do)?\s*dedos?\b/g, 'estalar o dedo'],
35
+ [/\b(estal\w*|estral\w*)\b/g, 'estalar'],
36
+ ];
37
+ function normal(t) {
38
+ let s = tira(t).replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim();
39
+ for (const [re, por] of OUVIDO) s = s.replace(re, por);
40
+ return s;
41
+ }
42
+
43
+ /* "colocar um foguete aqui na palma da minha mão": qualquer coisa na mão, não
44
+ só as logos. Vira o modelo, se houver, ou a placa com o nome. */
45
+ const NA_MAO = /\b(?:coloc\w*|po[er]\w*|bot\w*|aparec\w*)\s+(?:um|uma|o|a)\s+([a-z]{3,20})(?:\s+[a-z]{3,20})?\s+(?:aqui\s+)?(?:na|em|sobre|nas)\s+(?:a\s+)?(?:palma|mao|maos)\b/g;
46
+ const NAO_OBJETO = /^(logo|imagem|foto|fundo|legenda|praia|cerebro|video|mao|coisa)$/;
47
+ function objetoNaMao(t) {
48
+ const out = [];
49
+ let m;
50
+ NA_MAO.lastIndex = 0;
51
+ while ((m = NA_MAO.exec(t))) if (!NAO_OBJETO.test(m[1])) out.push({ object: m[1], pos: m.index, mao: null });
52
+ return out;
53
+ }
54
+
55
+ /** As palavras viram frases: ponto final, ou pausa de mais de 0,7 s. */
56
+ function frases(palavras) {
57
+ const out = [];
58
+ let atual = [];
59
+ const fecha = () => { if (atual.length) out.push({ ws: atual, de: atual[0].de, ate: atual[atual.length - 1].ate }); atual = []; };
60
+ palavras.forEach((p, i) => {
61
+ const ant = palavras[i - 1];
62
+ if (ant && p.de - ant.ate > 0.7) fecha();
63
+ atual.push(p);
64
+ if (/[.!?]$/.test(String(p.texto).trim())) fecha();
65
+ });
66
+ fecha();
67
+ for (const f of out) f.t = normal(f.ws.map((w) => w.texto).join(' '));
68
+ return out;
69
+ }
70
+
71
+ /** O segundo em que a frase diz `re` (a primeira palavra que casa), ou o começo dela. */
72
+ function quando(f, re) {
73
+ for (let i = 0; i < f.ws.length; i++) {
74
+ const um = normal(f.ws[i].texto);
75
+ const dois = normal(f.ws.slice(i, i + 3).map((w) => w.texto).join(' '));
76
+ if (re.test(um) || (re.test(dois) && !re.test(normal(f.ws.slice(i + 1, i + 3).map((w) => w.texto).join(' '))))) return f.ws[i].de;
77
+ }
78
+ return f.de;
79
+ }
80
+
81
+ const RE = {
82
+ camadas: /\bcamadas?\b|\btres dimens|\b(tir\w*|remov\w*|recort\w*|separ\w*)\s+(?:me\s+)?(?:o\s+|do\s+|meu\s+)?fundo\b|\bme\s+(tir\w*|recort\w*)\s+do\s+fundo\b/,
83
+ volta: /\bvolt\w*\b|\bnormal\b/,
84
+ estalo: /\bestalar\b/,
85
+ juntar: /\bjunt\w*\b|\buni\w*\b/,
86
+ cerebro: /\bcerebro\b/,
87
+ silencio: /\bsilencios?\b/,
88
+ fim: /\b(encerr|acab|termin|finaliz)\w*\b/,
89
+ legenda: /\blegendas?\b/,
90
+ acima: /\b(em cima|acima|encima|sobre|por cima)\s+da\s+(?:minha\s+)?cabeca\b|\baqui em cima\b|\bla em cima\b/,
91
+ imagem: /\b(?:imagem|foto|fundo)\s+(?:de|do|da)\s+(?:(?:um|uma)\s+)?([a-z]{3,20})\b/,
92
+ };
93
+
94
+ /** O fundo pedido na frase: um lugar conhecido, ou "imagem de um tubarão". */
95
+ function fundoPedido(t) {
96
+ const l = PED.lugar(t);
97
+ if (l && !/^(tras|atras|aqui)$/.test(l)) return l;
98
+ const m = t.match(RE.imagem);
99
+ return m && !/^(tras|atras|aqui|novo|nova)$/.test(m[1]) ? m[1] : null;
100
+ }
101
+
102
+ /**
103
+ * dados: {palavras:[{texto,de,ate}], estalos:[{t,score,mao}], juntaAbre:[{junta,abre}],
104
+ * maos:{left:[[de,ate]],right:[[de,ate]]}, silencios:[{de,ate}], dur} — tudo em segundos.
105
+ * → {passos:[{op,args,motivo}], legenda:{color?,position?,desde?}}
106
+ */
107
+ function planejar(d) {
108
+ const fs = frases(d.palavras || []);
109
+ const passos = [];
110
+ const bons = (d.estalos || []).filter((e) => e.mao || e.score >= 20);
111
+ const usados = new Set();
112
+ /* O estalo da janela é o MAIS FORTE dela, não o primeiro: o dedo que se
113
+ prepara faz um "tec" fraco antes do estalo de verdade (visto: 10,3 s
114
+ com 17 antes do 11,8 s com 21). */
115
+ const estaloDepois = (t, janela) => {
116
+ const c = bons.filter((x) => x.t >= t - 0.2 && x.t <= t + janela && !usados.has(x.t)).sort((a, b) => b.score - a.score)[0];
117
+ if (!c) return undefined;
118
+ usados.add(c.t);
119
+ return c.t;
120
+ };
121
+ const ehPedido = (f) => RE.camadas.test(f.t) || !!fundoPedido(f.t) || PED.objetosCitados(f.t).length || RE.juntar.test(f.t) || RE.silencio.test(f.t) || RE.fim.test(f.t);
122
+
123
+ // 1. As camadas em 3D: da palavra até o "volta" (ou o próximo pedido), 2,5–5 s.
124
+ const iCam = fs.findIndex((f) => RE.camadas.test(f.t) && !fundoPedido(f.t));
125
+ if (iCam >= 0) {
126
+ const de = Math.max(0, quando(fs[iCam], /camada|fundo|tir|recort|separ/) - 0.2);
127
+ const prox = fs.slice(iCam + 1).find((f) => f.de > de + 1 && (RE.volta.test(f.t) || ehPedido(f)));
128
+ const ate = Math.min(d.dur, de + 5, Math.max(de + 2.5, prox ? prox.de : de + 4));
129
+ passos.push({ op: 'separate_layers', args: { show3d: true, from: r2(de), to: r2(ate) }, motivo: `"${fs[iCam].t}"` });
130
+ }
131
+
132
+ // 2. Os objetos: na palavra, na mão que aparece logo ali (a outra, se a primeira já tem um).
133
+ const objetos = [];
134
+ const maoEm = (t, ocupadas) => {
135
+ const cand = [];
136
+ for (const lado of ['left', 'right']) {
137
+ for (const [a, b] of (d.maos && d.maos[lado]) || []) {
138
+ if (b < t - 0.2 || a > t + 3) continue;
139
+ cand.push({ lado, a: Math.max(a, t), b });
140
+ }
141
+ }
142
+ cand.sort((x, y) => (ocupadas.has(x.lado) - ocupadas.has(y.lado)) || x.a - y.a);
143
+ return cand[0] || null;
144
+ };
145
+ for (const f of fs) {
146
+ for (const o of [...PED.objetosCitados(f.t), ...objetoNaMao(f.t)]) {
147
+ const nome = o.object === 'chatgpt' ? /chatgpt|gpt/ : new RegExp(o.object.slice(0, 5));
148
+ const t = quando(f, nome);
149
+ if (objetos.some((x) => x.object === o.object && Math.abs(x.t - t) < 4)) continue;
150
+ const ocupadas = new Set(objetos.filter((x) => x.ate === null || x.ate > t).map((x) => x.mao));
151
+ const m = maoEm(t, ocupadas);
152
+ const mao = o.mao === 'left' || o.mao === 'right' ? o.mao : m ? m.lado : (ocupadas.has('left') ? 'right' : 'auto');
153
+ objetos.push({ object: o.object, t, de: m && m.lado === mao ? m.a : t, mao, ate: null, motivo: `"${f.t}"` });
154
+ }
155
+ }
156
+
157
+ // 3. Juntar as mãos (com objeto na mão): a junção que vem depois da frase; o cérebro se for dito.
158
+ let fusao = null;
159
+ if (objetos.length) {
160
+ const iJ = fs.findIndex((f) => (RE.juntar.test(f.t) || RE.cerebro.test(f.t)) && f.ate > objetos[0].t);
161
+ if (iJ >= 0) {
162
+ const f = fs[iJ];
163
+ const ev = (d.juntaAbre || []).find((e) => e.junta >= f.de - 1.5 && e.junta > objetos[0].de + 0.5);
164
+ if (ev) {
165
+ const cerebro = [f, fs[iJ + 1]].some((x) => x && RE.cerebro.test(x.t));
166
+ fusao = { junta: ev.junta, abre: ev.abre, then: cerebro ? 'brain' : 'none', motivo: `"${f.t}"` };
167
+ }
168
+ }
169
+ }
170
+ for (const o of objetos) {
171
+ o.ate = fusao && fusao.junta > o.de ? fusao.junta : Math.min(d.dur, o.de + 4);
172
+ if (o.ate > o.de + 0.3) passos.push({ op: 'attach_3d', args: { object: o.object, hand: o.mao, from: r2(o.de), to: r2(o.ate) }, motivo: o.motivo });
173
+ }
174
+ if (fusao) passos.push({ op: 'merge_on_join', args: { junta: r2(fusao.junta), ...(fusao.abre !== null ? { abre: r2(fusao.abre) } : {}), then: fusao.then }, motivo: fusao.motivo });
175
+
176
+ // 4. Fundos novos: no estalo, se a frase (ou a anterior) fala dele, ou se um estalo vem logo depois; senão na palavra.
177
+ const fundos = [];
178
+ fs.forEach((f, i) => {
179
+ const onde = fundoPedido(f.t);
180
+ if (!onde || !/\b(coloc\w*|poe|pon\w*|bot\w*|lev\w*|troc\w*|mud\w*|fundo|imagem|foto|me\s+em)\b/.test(f.t)) return;
181
+ const t = quando(f, new RegExp(onde.split(' ')[0].slice(0, 5)));
182
+ /* Um estalo logo depois do lugar é o gatilho, dito ou não. Se o estalo
183
+ foi dito ANTES do lugar ("se estalar os dedos ela coloca um
184
+ tubarão"), ele pode vir uns segundos depois da frase. */
185
+ const antes = f.t.slice(0, f.t.indexOf(onde.split(' ')[0]));
186
+ const falaEstalo = RE.estalo.test(antes) || (fs[i - 1] && RE.estalo.test(fs[i - 1].t) && !fundoPedido(fs[i - 1].t));
187
+ let e = estaloDepois(t, 3);
188
+ if (e === undefined && falaEstalo) e = estaloDepois(f.de, 10);
189
+ // "só aqui", "somente aqui": o fundo vale para a frase, depois volta o original.
190
+ const so = /\b(somente|so|apenas)\s+(aqui|agora|nessa parte|nesse momento)\b/.test(f.t) ? Math.min(d.dur, f.ate + 0.6) : null;
191
+ fundos.push({ with: onde, at: e !== undefined ? e : t, ...(so ? { ate: so } : {}), motivo: `"${f.t}"` + (e !== undefined ? ' (no estalo)' : '') });
192
+ });
193
+ fundos.forEach((b, i) => {
194
+ const until = b.ate !== undefined ? b.ate : fundos[i + 1] ? fundos[i + 1].at : null;
195
+ passos.push({ op: 'replace_background', args: { with: b.with, at: r2(b.at), ...(until !== null ? { until: r2(until) } : {}) }, motivo: b.motivo });
196
+ });
197
+
198
+ // 5. Legenda: sempre (é reels); a cor e a posição se forem pedidas, a partir do estalo se ele for citado.
199
+ const legenda = {};
200
+ const iL = fs.findIndex((f) => RE.legenda.test(f.t));
201
+ if (iL >= 0) {
202
+ const janela = fs.slice(iL, iL + 3);
203
+ const cor = janela.map((f) => PED.corPedida(f.t)).find(Boolean);
204
+ if (cor) legenda.color = cor;
205
+ const fp = janela.find((f) => RE.acima.test(f.t));
206
+ if (fp) {
207
+ legenda.position = 'above_head';
208
+ if (RE.estalo.test(fp.t)) { const e = estaloDepois(fp.de, 8); if (e !== undefined) legenda.desde = e; }
209
+ // "a legenda que tá aqui… aqui em cima": sobe quando ela diz onde.
210
+ else if (/\b(aqui|la) em cima\b/.test(fp.t)) legenda.desde = quando(fp, /cima/);
211
+ } else {
212
+ /* "a legenda daqui ficar aqui", apontando: o pedido é mudar de lugar, e
213
+ o lugar que se aponta em quem fala para a câmera é perto do rosto —
214
+ a legenda sobe para a cabeça no segundo "aqui". (01/10) */
215
+ const fd = janela.find((f) => /\bdaqui\b[\s\S]*\b(ficar|ir|pra|para)\b[\s\S]*\baqui\b|\b(mud|muda|mover|move|pass)\w*\b[\s\S]*\baqui\b/.test(f.t));
216
+ if (fd) {
217
+ legenda.position = 'above_head';
218
+ const aquis = fd.ws.filter((w) => /^aqui/.test(normal(w.texto)));
219
+ legenda.desde = (aquis[aquis.length - 1] || fd.ws[0]).de;
220
+ }
221
+ }
222
+ }
223
+
224
+ // 6. Cortes, por último: o silêncio que a pessoa cita, e o fim que ela anuncia.
225
+ const cortes = [];
226
+ const iS = fs.findIndex((f) => RE.silencio.test(f.t) && /\b(cort\w*|tir\w*|remov\w*|apag\w*|retir\w*)\b/.test(f.t));
227
+ if (iS >= 0) {
228
+ const t0 = fs[iS].de;
229
+ const sil = juntarSilencios(d.silencios || []).filter((s) => s.de >= t0 && s.de <= t0 + 15 && s.ate - s.de >= 1.5)
230
+ .sort((a, b) => (b.ate - b.de) - (a.ate - a.de))[0];
231
+ if (sil) cortes.push({ de: r2(sil.de + 0.1), ate: r2(sil.ate - 0.1), motivo: `"${fs[iS].t}" (${r2(sil.ate - sil.de)} s parado)` });
232
+ }
233
+ const iF = fs.findIndex((f) => RE.fim.test(f.t) && /\bvideo\b/.test(f.t));
234
+ if (iF >= 0) {
235
+ const f = fs[iF];
236
+ const ultima = fs[fs.length - 1].ate;
237
+ const e = RE.estalo.test(f.t) ? estaloDepois(f.de, 6) : undefined;
238
+ const fim = Math.max(ultima, e !== undefined ? e : 0) + 0.4;
239
+ if (d.dur - fim > 0.8) cortes.push({ de: r2(fim), ate: r2(d.dur), motivo: `"${f.t}"` });
240
+ }
241
+ if (cortes.length) passos.push({ op: 'cortes', args: { trechos: cortes.map(({ de, ate }) => [de, ate]) }, motivo: cortes.map((c) => c.motivo).join(' · ') });
242
+
243
+ return { passos, legenda };
244
+ }
245
+
246
+ /** Silêncios separados por um respiro (< 0,4 s) são um só. */
247
+ function juntarSilencios(sil) {
248
+ const out = [];
249
+ for (const s of [...sil].sort((a, b) => a.de - b.de)) {
250
+ const u = out[out.length - 1];
251
+ if (u && s.de - u.ate < 0.4) u.ate = Math.max(u.ate, s.ate);
252
+ else out.push({ de: s.de, ate: s.ate });
253
+ }
254
+ return out;
255
+ }
256
+
257
+ const OPS = {
258
+ auto: {
259
+ uso: 'auto {asset?,dryRun?} — O VÍDEO É O PEDIDO: lê o que a pessoa FALA na gravação ("separe em camadas", "coloca o logo do Claude aqui", "com o estalar de dedos me coloca numa praia", "junta e vira um cérebro", "cortou o silêncio", "o vídeo acaba") e aplica cada efeito no momento em que ela diz, ancorado na palavra, na mão, no estalo e no juntar das mãos; legenda sempre. Use logo depois do import quando pedirem "edite esse vídeo" sem dizer o quê',
260
+ async run(ctx, a) {
261
+ const T = require('./timeline.js');
262
+ const C = require('./camadas.js');
263
+ const R = require('./rastro.js');
264
+ const RT = require('./rastrear.js');
265
+ const TR = require('./transcript.js');
266
+ const FXO = require('./ops-fx.js');
267
+ const tl = ctx.tl;
268
+ if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
269
+ const asset = C.base(tl, a.asset);
270
+ const inicio = Date.now();
271
+ // O lento e independente junto: rastreio (MediaPipe em paralelo) e transcrição.
272
+ const [rs, tr] = await Promise.all([RT.garantir(ctx.p.dir, asset), TR.obter(ctx.p.dir, asset)]);
273
+ if (!rs.ok) return rs;
274
+ if (!tr.ok) return { ok: false, error: 'sem a fala não sei o que fazer: ' + tr.error };
275
+ const fps = rs.rastro.fps;
276
+ const trilhas = R.trilhasDasMaos(rs.rastro);
277
+ const trechos = (t) => R.trechosVisiveis(t, Math.round(fps * 0.3)).map(([x, y]) => [x / fps, y / fps]);
278
+ const dados = {
279
+ palavras: tr.palavras,
280
+ estalos: (await FXO.estalosDe(ctx, asset)).map((e) => ({ t: e.t, score: e.score, mao: e.mao })),
281
+ juntaAbre: R.juntaEAbre(trilhas, { rastro: rs.rastro }).map((e) => ({ junta: e.junta / fps, abre: e.abre === null ? null : e.abre / fps })),
282
+ maos: { left: trechos(trilhas.left), right: trechos(trilhas.right) },
283
+ silencios: await require('./render.js').silenciosDe(ctx.p.dir, asset, { threshold: -35, minSilence: 0.8 }),
284
+ dur: asset.seconds,
285
+ };
286
+ const plano = planejar(dados);
287
+ const q = (s) => C.origemParaQuadro(ctx.tl, asset.id, s);
288
+ const ver = plano.passos.map((p) => ({ op: p.op, motivo: p.motivo, args: p.args }));
289
+ if (a.dryRun) return { ok: true, plano: ver, legenda: plano.legenda, segundos: 'da gravação' };
290
+
291
+ const { OPS: TODAS } = require('./ops.js');
292
+ const feitos = [];
293
+ const roda = async (op, args, motivo) => {
294
+ const res = await TODAS[op].run(ctx, args);
295
+ feitos.push({ op, motivo, ok: !!res && res.ok !== false, ...(res && res.ok === false ? { error: res.error } : {}) });
296
+ return res;
297
+ };
298
+ for (const p of plano.passos) {
299
+ const g = p.args;
300
+ if (p.op === 'separate_layers') await roda(p.op, { show3d: true, from: q(g.from), to: q(g.to) }, p.motivo);
301
+ else if (p.op === 'attach_3d') await roda(p.op, { object: g.object, hand: g.hand, from: q(g.from), to: q(g.to) }, p.motivo);
302
+ else if (p.op === 'merge_on_join') await roda(p.op, { junta: q(g.junta), ...(g.abre !== undefined ? { abre: q(g.abre) } : {}), then: g.then }, p.motivo);
303
+ else if (p.op === 'replace_background') await roda(p.op, { with: g.with, at: q(g.at), ...(g.until !== undefined ? { until: q(g.until) } : {}) }, p.motivo);
304
+ else if (p.op === 'cortes') {
305
+ const ranges = g.trechos.map(([de, ate]) => [q(de), Math.min(T.duracao(ctx.tl), q(Math.min(ate, asset.seconds - 0.001)) + 1)]).filter(([x, y]) => x !== null && y > x);
306
+ if (ranges.length) await roda('ripple_delete_ranges', { ranges }, p.motivo);
307
+ }
308
+ }
309
+ // A legenda, já nos cortes; a posição a partir do estalo, se ele foi citado.
310
+ await roda('add_captions', { maxWords: 3, animation: 'highlightPop' }, 'legenda (reels)');
311
+ const L = plano.legenda;
312
+ if (L.color || L.position) {
313
+ const leg = T.todosOsClipes(ctx.tl).filter((x) => x.clip.caption).map((x) => x.clip);
314
+ const desde = L.desde !== undefined ? q(L.desde) : null;
315
+ if (L.color) await roda('style_captions', { color: L.color }, 'cor pedida');
316
+ const ids = leg.filter((c) => desde === null || c.startFrame >= desde).map((c) => c.id);
317
+ if (L.position && ids.length) await roda('style_captions', { ids, position: L.position }, desde !== null ? 'em cima da cabeça, desde o momento dito' : 'em cima da cabeça');
318
+ }
319
+ const falhou = feitos.filter((f) => !f.ok);
320
+ return { ok: !falhou.length, feitos, tookS: Math.round((Date.now() - inicio) / 100) / 10,
321
+ ...(falhou.length ? { error: falhou.map((f) => `${f.op}: ${f.error}`).join('; ') } : {}),
322
+ depois: 'inspect para OLHAR os momentos; export renderiza (o 3D sai no export)' };
323
+ },
324
+ },
325
+ };
326
+
327
+ const EX = { auto: '{}' };
328
+
329
+ module.exports = { planejar, frases, normal, fundoPedido, juntarSilencios, OPS, EX };