primocode 11.4.0 → 11.5.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 (85) hide show
  1. package/LICENSES/edit2-terceiros.md +31 -0
  2. package/LICENSES/models-dev-MIT.txt +25 -0
  3. package/README.md +2 -2
  4. package/editor/SKILL.md +24 -0
  5. package/editor/THIRD_PARTY_NOTICES.md +1 -0
  6. package/editor/assets/baixar.json +9 -1
  7. package/editor/helpers/df_limpar.py +29 -0
  8. package/editor/helpers/edit2.py +155 -0
  9. package/editor/helpers/enquadrar_camera.py +77 -0
  10. package/editor/helpers/face_layout.py +20 -0
  11. package/editor/helpers/face_mediapipe.py +147 -0
  12. package/editor/helpers/instalar_df.sh +7 -0
  13. package/editor/helpers/legenda_ass.py +124 -0
  14. package/editor/helpers/look_ffmpeg.py +105 -0
  15. package/editor/helpers/motor_palavras.py +157 -0
  16. package/editor/helpers/primeiro_passe.py +214 -0
  17. package/editor/helpers/som_edit2.py +119 -0
  18. package/editor/helpers/transcribe.py +31 -2
  19. package/editor/pyproject.toml +9 -0
  20. package/editor/references/edit2.md +22 -0
  21. package/editor/uv.lock +3342 -214
  22. package/kits/3d/LICENSES/README.md +11 -0
  23. package/kits/3d/animacoes.js +87 -0
  24. package/kits/3d/base.js +44 -0
  25. package/kits/3d/catalogo.js +38 -0
  26. package/kits/3d/cenario.js +162 -0
  27. package/kits/3d/exemplos/asfalto.mjs +91 -0
  28. package/kits/3d/maquinas.js +97 -0
  29. package/kits/3d/montador.js +142 -0
  30. package/kits/3d/package.json +1 -0
  31. package/kits/3d/personagem.js +86 -0
  32. package/kits/3d/remotion/Maquete.jsx +42 -0
  33. package/kits/3d/remotion/Root.jsx +10 -0
  34. package/kits/3d/render.mjs +73 -0
  35. package/lib/cli/comando-api.js +30 -8
  36. package/lib/cli/modelos-lista.js +58 -0
  37. package/lib/cli/sessao.js +11 -0
  38. package/lib/cli/subcomandos.js +3 -2
  39. package/lib/outra-pasta.js +1 -1
  40. package/lib/permissao-sistema.js +60 -0
  41. package/lib/permission/perigo.js +1 -0
  42. package/lib/permission/permissao.js +1 -0
  43. package/lib/permission/sem-perguntar.js +29 -0
  44. package/lib/plugin/desktop.js +5 -4
  45. package/lib/plugin/estudio/cota.js +4 -2
  46. package/lib/plugin/estudio/dialogo3d.js +71 -0
  47. package/lib/plugin/estudio/effectcraft.js +1 -1
  48. package/lib/plugin/estudio/glifos.js +2 -0
  49. package/lib/plugin/estudio/gsap-local.js +43 -0
  50. package/lib/plugin/estudio/index.js +63 -10
  51. package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
  52. package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
  53. package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
  54. package/lib/plugin/estudio/maquina-diretor.js +1 -1
  55. package/lib/plugin/estudio/maquina-elementos.js +2 -2
  56. package/lib/plugin/estudio/maquina.js +26 -8
  57. package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
  58. package/lib/plugin/estudio/motion-plano/critico.js +59 -0
  59. package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
  60. package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
  61. package/lib/plugin/estudio/motion-plano/index.js +75 -0
  62. package/lib/plugin/estudio/motion-plano/llm.js +59 -0
  63. package/lib/plugin/estudio/motion-plano/motor.js +50 -0
  64. package/lib/plugin/estudio/motion-plano/passos.js +38 -0
  65. package/lib/plugin/estudio/motion-plano/pele.js +36 -0
  66. package/lib/plugin/estudio/motion-plano/plano.js +152 -0
  67. package/lib/plugin/estudio/motion-plano/regua.js +100 -0
  68. package/lib/plugin/estudio/personagem.js +3 -1
  69. package/lib/plugin/estudio/referencias.js +1 -1
  70. package/lib/provider/catalogo.js +161 -0
  71. package/lib/provider/local/catalogo-embutido.json.gz +0 -0
  72. package/lib/provider/local/motor.js +140 -27
  73. package/lib/provider/local/orcamento.js +193 -0
  74. package/lib/provider/local/provedor.js +127 -14
  75. package/lib/provider/local/recorte.js +170 -0
  76. package/lib/provider/local/revezamento.js +121 -0
  77. package/lib/session/runner/act.js +28 -3
  78. package/lib/skill/catalogo.js +5 -1
  79. package/lib/tool/tools.js +5 -1
  80. package/package.json +1 -1
  81. package/vendor/fontes/ttf/ORIGEM.txt +1 -0
  82. package/vendor/fontes/ttf/inter-800.ttf +0 -0
  83. package/vendor/gsap/ORIGEM.txt +6 -0
  84. package/vendor/gsap/gsap.min.js +11 -0
  85. package/voz/primo_voz/chat.html +167 -5
@@ -0,0 +1,60 @@
1
+ /**
2
+ * permissao-sistema.js — falta permissão do macOS: parar na PRIMEIRA falha.
3
+ *
4
+ * O que aconteceu: sem Acessibilidade/Gravação de Tela, o agente tentava abrir o
5
+ * navegador, falhava, e tentava de novo (5+ vezes) — cada volta com o mesmo erro.
6
+ * Aqui o erro é reconhecido, vira uma frase clara ("falta a permissão X") e o laço
7
+ * do agente (act.js) para na hora. No app desktop, a linha de protocolo
8
+ * `primocode:permissao <id>` no stderr abre a tela de permissões.
9
+ *
10
+ * Puro de propósito: sem I/O, o teste mede a política inteira.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ /** As ferramentas que mexem no computador: só nelas um erro vira "falta permissão". */
16
+ const FERRAMENTA_DE_COMPUTADOR = /^(desktop_|browser_|open_in_browser$)/;
17
+
18
+ const NOMES = { accessibility: 'Acessibilidade', screen: 'Gravação de Tela', automation: 'Automação' };
19
+
20
+ /** Texto de erro do macOS → qual permissão falta. A ordem importa: a mais específica primeiro. */
21
+ const PADROES = [
22
+ ['automation', /-1743|not authori[sz]ed to send apple events|apple events? (is|are) not allowed/i],
23
+ ['accessibility', /-25211|-1719|-1002|not allowed assistive access|assistive access|is not allowed to send keystrokes|osascript[^\n]*n(ã|a)o tem permiss(ã|a)o|acessibilidade/i],
24
+ ['screen', /could not create image from display|screen ?recording|grava(ç|c)(ã|a)o de tela|not authori[sz]ed to capture screen/i],
25
+ ];
26
+
27
+ function ehFerramentaDeComputador(nome) {
28
+ return FERRAMENTA_DE_COMPUTADOR.test(String(nome || ''));
29
+ }
30
+
31
+ /** Qual permissão falta, ou null. Só vale no macOS e para resultado com ok === false. */
32
+ function detectar(nome, resultado, plataforma = process.platform) {
33
+ if (plataforma !== 'darwin' || !ehFerramentaDeComputador(nome)) return null;
34
+ if (!resultado || resultado.ok !== false) return null;
35
+ const texto = [resultado.error, resultado.stderr, resultado.message].filter(Boolean).join('\n');
36
+ if (!texto) return null;
37
+ for (const [id, re] of PADROES) if (re.test(texto)) return id;
38
+ return null;
39
+ }
40
+
41
+ /** A frase que volta pro modelo e pra pessoa. Diz o que fazer e proíbe tentar de novo. */
42
+ function mensagem(id) {
43
+ const nome = NOMES[id] || id;
44
+ return `Falta a permissão ${nome} do macOS. Abri a tela de permissões do PrimoCode: ative ${nome} e peça de novo. `
45
+ + 'NÃO tente outra vez nem outra ferramenta de computador/navegador até a pessoa ativar.';
46
+ }
47
+
48
+ const PROTOCOLO = 'primocode:permissao';
49
+ const linhaProtocolo = (id) => `${PROTOCOLO} ${id}\n`;
50
+ function idDaLinha(linha) {
51
+ const m = String(linha || '').trim().match(/^primocode:permissao (accessibility|screen|automation)$/);
52
+ return m ? m[1] : null;
53
+ }
54
+
55
+ /** O resultado trocado: mesmo `ok:false`, erro claro e a marca `permissao`. */
56
+ function comoErro(id, resultado) {
57
+ return { ...resultado, ok: false, permissao: id, error: mensagem(id) };
58
+ }
59
+
60
+ module.exports = { detectar, mensagem, comoErro, linhaProtocolo, idDaLinha, ehFerramentaDeComputador, NOMES, PADROES, PROTOCOLO };
@@ -350,6 +350,7 @@ async function portao(estado, { modo, nome, args, projectDir, subagent = false,
350
350
  const travado = trava.recusa(nome, args, projectDir);
351
351
  if (travado) return { ok: false, motivo: travado.motivo, error: travado.error };
352
352
  if (m === 'livre') return { ok: true };
353
+ if (m !== 'plano' && require('./sem-perguntar.js').ativo()) return { ok: true }; // --yolo: sem perguntas (o modo plano continua só lendo)
353
354
  if (m === 'plano') {
354
355
  const mexe = ESCRITAS.has(nome) || nome === 'mcp_conectar'
355
356
  || (COMANDOS.has(nome) && !somenteLeitura((args || {}).command));
@@ -93,6 +93,7 @@ function descrever(nome, args) {
93
93
  */
94
94
  async function pedir(estado, nome, args, perguntar) {
95
95
  if (!precisa(nome)) return { ok: true };
96
+ if (require('./sem-perguntar.js').ativo()) return { ok: true }; // --yolo: sem perguntas
96
97
  if (estado.liberado) return { ok: true };
97
98
 
98
99
  /* Negar uma vez vale para a sessão. Sem isto, quem disse não leva a mesma
@@ -0,0 +1,29 @@
1
+ /**
2
+ * sem-perguntar.js — o modo `--dangerously-skip-permissions` (alias `--yolo`).
3
+ *
4
+ * Com ele o PrimoCode não pede confirmação nenhuma: controle do computador,
5
+ * comando perigoso, troca de pasta, servidor MCP do projeto. Vale por variável
6
+ * de ambiente (PRIMOCODE_SEM_PERGUNTAR=1) para chegar aos subprocessos e ao
7
+ * motor do desktop. Sem a variável, nada muda.
8
+ *
9
+ * Duas coisas continuam valendo de propósito: o /lock (arquivo travado só se
10
+ * lê) e a recusa de escrever fora da pasta do projeto (lib/tool/tools.js).
11
+ */
12
+ 'use strict';
13
+
14
+ const FLAGS = ['--dangerously-skip-permissions', '--yolo'];
15
+ const AVISO = 'modo sem perguntas: o PrimoCode vai agir sem pedir confirmação';
16
+
17
+ function ativo() { return process.env.PRIMOCODE_SEM_PERGUNTAR === '1'; }
18
+
19
+ /** Tira a flag do argv (muta) e liga a variável. Devolve true se estava lá. */
20
+ function consumir(argv) {
21
+ let achou = false;
22
+ for (let i = argv.length - 1; i >= 2; i--) {
23
+ if (FLAGS.includes(argv[i])) { argv.splice(i, 1); achou = true; }
24
+ }
25
+ if (achou) process.env.PRIMOCODE_SEM_PERGUNTAR = '1';
26
+ return achou;
27
+ }
28
+
29
+ module.exports = { ativo, consumir, FLAGS, AVISO };
@@ -35,12 +35,13 @@ const TOOLS = {
35
35
  function run(cmd, opts = {}) {
36
36
  try {
37
37
  execSync(cmd, {
38
- stdio: 'ignore',
38
+ stdio: ['ignore', 'ignore', 'pipe'],
39
39
  timeout: opts.timeout || 15000,
40
40
  env: opts.env ? { ...process.env, ...opts.env } : undefined,
41
41
  });
42
42
  return { ok: true };
43
- } catch (e) { return { ok: false, error: e.message }; }
43
+ // o stderr entra no erro: é nele que o macOS diz "falta permissão" (lib/permissao-sistema.js)
44
+ } catch (e) { return { ok: false, error: [e.message, String((e && e.stderr) || '').trim()].filter(Boolean).join('\n') }; }
44
45
  }
45
46
 
46
47
  // Igual ao run(), mas devolve o stdout — para os comandos que PERGUNTAM algo
@@ -49,12 +50,12 @@ function runOut(cmd, opts = {}) {
49
50
  try {
50
51
  const out = execSync(cmd, {
51
52
  encoding: 'utf-8',
52
- stdio: ['ignore', 'pipe', 'ignore'],
53
+ stdio: ['ignore', 'pipe', 'pipe'],
53
54
  timeout: opts.timeout || 15000,
54
55
  env: opts.env ? { ...process.env, ...opts.env } : undefined,
55
56
  });
56
57
  return { ok: true, out: String(out || '') };
57
- } catch (e) { return { ok: false, error: e.message, out: String((e && e.stdout) || '') }; }
58
+ } catch (e) { return { ok: false, error: [e.message, String((e && e.stderr) || '').trim()].filter(Boolean).join('\n'), out: String((e && e.stdout) || '') }; }
58
59
  }
59
60
 
60
61
  // Argumento de shell entre aspas simples, à prova de apóstrofo. O truque
@@ -143,10 +143,12 @@ async function comCota(render, deps = {}) {
143
143
  const r = await render();
144
144
  const extra = lim.pegarAvisos();
145
145
  if (!r || !r.ok) return r;
146
- const a = c.offline ? { ok: true, mudou: false } : (deps.aplicar || aplicar)(r.video, c.recursos);
146
+ // a marca d'água do PrimoCode só entra no vídeo do usuário se ELE pediu (Joel, 13/11): `deps.marca` vem do pedido; o teto de resolução segue valendo
147
+ const rec = deps.marca ? c.recursos : { ...c.recursos, marcaDagua: false };
148
+ const a = c.offline ? { ok: true, mudou: false } : (deps.aplicar || aplicar)(r.video, rec);
147
149
  if (!c.offline) await (deps.consultar || consultar)('gastar');
148
150
  const restam = c.restam == null ? null : Math.max(0, c.restam - 1);
149
- return { ...r, plano: { id: c.plano, restam, resolucao: c.recursos.resolucao, marcaDagua: !!(a.mudou && c.recursos.marcaDagua) },
151
+ return { ...r, plano: { id: c.plano, restam, resolucao: c.recursos.resolucao, marcaDagua: !!(a.mudou && rec.marcaDagua) },
150
152
  ...((() => {
151
153
  const a = [...(c.offline ? ['sem rede: modo local, sem cota nem marca d\'água.'] : []),
152
154
  ...(!c.offline && restam === 0 ? ['Foi o seu último vídeo do mês. Veja os planos com: primocode planos'] : []), ...extra];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * dialogo3d.js — os personagens falando, pela MESMA estrutura da maquete (kits/3d).
3
+ *
4
+ * Pega a partitura que o personagem.js já faz (quem fala, quando, o lugar, o elenco)
5
+ * e a traduz num roteiro por etapas para o montador: uma etapa por fala, a câmera
6
+ * no falante, a boca na palavra, o gesto da fala. Versão SIMPLES: sem cartões, sem
7
+ * planos de filme. O caminho antigo (kit/personagem.jsx) continua intacto; este só
8
+ * entra quando o roteiro pede `motor: 'montador'`.
9
+ *
10
+ * Render: `node kits/3d/render.mjs roteiro3d.json saida.mp4 --audio voz.mp3 --audio-pronto`.
11
+ */
12
+ 'use strict';
13
+
14
+ // A animação da biblioteca para o gesto que o personagem.js escolheu (acaoDe) e a emoção.
15
+ const GESTO = { falar: 'falar', acenar: 'falar', apontar: 'apontar', pular: 'comemorar', dancar: 'comemorar', comemorar: 'comemorar', pensar: 'pensar', andar: 'falar', correr: 'falar', sentar: 'falar' };
16
+ const EMOCAO = { surpreso: 'surpreso', pensativo: 'pensar' };
17
+
18
+ const r2 = (x) => Math.round(x * 100) / 100;
19
+ const XS = { 1: [0], 2: [-1.25, 1.25], 3: [-2.2, 0, 2.2], 4: [-3.3, -1.1, 1.1, 3.3] };
20
+
21
+ /** A aparência de um membro do elenco na biblioteca (boneco único; robô e bicho viram cor). */
22
+ function pecaDoElenco(e, i, n) {
23
+ const x = XS[Math.min(4, Math.max(1, n))][i];
24
+ const base = { id: `p${i}`, tipo: 'personagem', pos: [x, 0, 0], giro: r2(-x * 0.16), escala: 1 };
25
+ if (e.tipo === 'pessoa') return { ...base, pele: e.pele, roupa: e.roupa, cabelo: e.corCabelo };
26
+ if (e.tipo === 'robo') return { ...base, pele: e.cor || '#dd5a2b', roupa: '#cfd6df', calca: '#8b95a3', capacete: e.cor || '#dd5a2b' };
27
+ return { ...base, pele: e.cor || '#c8a27a', roupa: e.cor || '#c8a27a', escala: 0.85 }; // bicho/coisa: boneco na cor dele
28
+ }
29
+
30
+ /** O lugar (mundo.js) em peças da biblioteca: chão, árvores, casas e prédios. O resto do catálogo antigo fica de fora. */
31
+ function cenarioDoMundo(m) {
32
+ const pecas = [{ id: 'chao', tipo: 'terreno', w: 80, d: 80, cor: (m.chao && m.chao.cor) || '#b99362' }];
33
+ (m.props || []).forEach((p, i) => {
34
+ const base = { id: `c${i}`, pos: [p.x, 0, Math.min(p.z, -2.5)], giro: p.giro || 0 };
35
+ if (/^(arvore|palmeira|arbusto)$/.test(p.peca)) pecas.push({ ...base, tipo: 'arvore', escala: p.escala || 1, forma: 'redonda' });
36
+ else if (/^pinheiro/.test(p.peca)) pecas.push({ ...base, tipo: 'arvore', escala: p.escala || 1, forma: 'pinheiro' });
37
+ else if (p.peca === 'casa' || p.peca === 'celeiro') pecas.push({ ...base, tipo: 'casa', escala: p.escala || 1 });
38
+ else if (p.peca === 'predio') pecas.push({ ...base, tipo: 'predio', andares: 4 + (i % 4), pos: [p.x, 0, Math.min(p.z, -6)] });
39
+ });
40
+ return pecas;
41
+ }
42
+
43
+ /**
44
+ * @param {object} partitura a saída de personagem.partituraDe
45
+ * @returns {object} roteiro do montador (JSON puro)
46
+ */
47
+ function roteiroDeDialogo(partitura) {
48
+ const elenco = partitura.elenco, falas = partitura.falas;
49
+ const pecas = elenco.map((e, i) => pecaDoElenco(e, i, elenco.length));
50
+ const mundo = (partitura.mundos && partitura.mundos[0]) || {};
51
+ const ceu = mundo.dentro ? [mundo.parede || '#e6d6c3', mundo.parede || '#e6d6c3'] : (mundo.ceu || ['#8fd0ff', '#d8efff']);
52
+ const etapas = falas.map((f, i) => {
53
+ const ini = i === 0 ? 0 : r2(falas[i].de - 0.25);
54
+ const fim = i === falas.length - 1 ? partitura.duracao : r2(falas[i + 1].de - 0.25);
55
+ const dur = r2(Math.max(0.5, fim - ini)), x = pecas[f.quem].pos[0];
56
+ const amplo = i === 0 || (elenco.length > 1 && i % 4 === 0);
57
+ const gesto = EMOCAO[f.emocao] || GESTO[f.acao] || 'falar';
58
+ return {
59
+ id: `fala${i}`, dur, titulo: elenco[f.quem].rotulo || elenco[f.quem].nome, legenda: f.texto, inicio: ini,
60
+ voz: { de: f.de, ate: f.ate }, vozEm: r2(f.de - ini),
61
+ camera: amplo ? { de: [0, 2.2, 10.2], para: [0, 2.0, 9.4], alvo: [0, 1.0, 0], fov: 40 }
62
+ : { de: [x * 0.55, 1.6, 5.2], para: [x * 0.55, 1.55, 4.5], alvo: [x, 1.25, 0], fov: 38 },
63
+ pecas: i === 0 ? [...(mundo ? cenarioDoMundo(mundo) : []), ...pecas] : [],
64
+ acoes: [{ em: 0, dur, quem: `p${f.quem}`, faz: 'anim', nome: gesto }],
65
+ falas: [{ quem: `p${f.quem}`, de: r2(f.de - ini), ate: r2(f.ate - ini), palavras: (f.palavras || []).map((w) => ({ de: r2(w.de - ini), ate: r2(w.ate - ini) })) }],
66
+ };
67
+ });
68
+ return { ceu, horizonte: (mundo.chao && mundo.chao.cor) || '#8fc46b', modo: 'dialogo', etapas };
69
+ }
70
+
71
+ module.exports = { roteiroDeDialogo, cenarioDoMundo, pecaDoElenco, GESTO };
@@ -108,7 +108,7 @@ async function renderizar({ pasta, script, comp, fontes = [], audio, conferir =
108
108
  if (fontes && fontes.length) {
109
109
  const glifos = require('./glifos.js');
110
110
  for (const f of fontes) {
111
- try { if (await glifos.baixarFonte(f.familia, f.peso || 400)) dirs.push(glifos.CACHE); } catch { /* sem rede: segue com a fonte do sistema */ }
111
+ try { const arq = await glifos.baixarFonte(f.familia, f.peso || 400); if (arq) dirs.push(path.dirname(arq)); } catch { /* sem rede: segue com a fonte do sistema */ }
112
112
  }
113
113
  }
114
114
  if (dirs.length) env.EFFECTCRAFT_FONT_DIRS = [...new Set(dirs)].join(path.delimiter);
@@ -191,6 +191,8 @@ const CACHE = path.join(os.homedir(), '.primocode', 'studio', 'cache-fontes');
191
191
 
192
192
  async function baixarFonte(familia, peso = 700) {
193
193
  const nome = String(familia).replace(/[^a-z0-9]+/gi, '-').toLowerCase() + '-' + peso + '.ttf';
194
+ const embutida = path.join(__dirname, '..', '..', '..', 'vendor', 'fontes', 'ttf', nome); // as embutidas (Inter 800 da pele do Destrava) não dependem de rede nem do cache
195
+ if (fs.existsSync(embutida)) return embutida;
194
196
  const arq = path.join(CACHE, nome);
195
197
  if (fs.existsSync(arq)) return arq;
196
198
  // A API antiga (css, não css2) devolve .ttf; a nova devolve woff2, que não tem a glyf crua.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * gsap-local.js — o GSAP vem de um arquivo versionado (vendor/gsap/gsap.min.js), nunca de CDN.
3
+ *
4
+ * `embutir(pastaDaComposicao)` roda antes do render do Hyperframes: troca qualquer <script src> de CDN do núcleo do
5
+ * GSAP pelo arquivo local, e, se o HTML usa `gsap.` sem carregar nada, injeta o script. Puro no texto (`trocar`),
6
+ * com efeito só em `embutir` (copia o arquivo e regrava o index.html).
7
+ */
8
+ 'use strict';
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ARQUIVO = path.join(__dirname, '..', '..', '..', 'vendor', 'gsap', 'gsap.min.js');
13
+ const DESTINO_REL = 'vendor/gsap.min.js';
14
+ // <script ... src="...gsap(.min).js..."> do núcleo (não pega ScrollTrigger.min.js, MotionPathPlugin etc.)
15
+ const CDN = /<script\b([^>]*?)\bsrc\s*=\s*(["'])(https?:)?\/\/[^"']*?\/gsap(?:@[^/"']*)?(?:\/dist)?(?:\/gsap)?(?:\.min)?\.js(?:\?[^"']*)?\2([^>]*)><\/script>/gi;
16
+
17
+ /** Puro. Devolve { html, trocou, injetou }. */
18
+ function trocar(html) {
19
+ let trocou = 0;
20
+ let saida = String(html || '').replace(CDN, (_m, antes, q, _p, depois) => { trocou++; return `<script${antes}src=${q}${DESTINO_REL}${q}${depois}></script>`; });
21
+ const jaTem = new RegExp(`src\\s*=\\s*["'][^"']*gsap[^"']*\\.js`, 'i').test(saida);
22
+ let injetou = false;
23
+ if (!jaTem && /\bgsap\s*\./.test(saida)) {
24
+ const tag = `<script src="${DESTINO_REL}"></script>`;
25
+ saida = /<head[^>]*>/i.test(saida) ? saida.replace(/<head[^>]*>/i, (h) => `${h}\n${tag}`) : /<body[^>]*>/i.test(saida) ? saida.replace(/<body[^>]*>/i, (b) => `${b}\n${tag}`) : `${tag}\n${saida}`;
26
+ injetou = true;
27
+ }
28
+ return { html: saida, trocou, injetou };
29
+ }
30
+
31
+ /** Efeito: copia o arquivo e regrava o index.html da pasta. Devolve { ok, trocou, injetou, erro? }. */
32
+ function embutir(pasta) {
33
+ const idx = path.join(pasta, 'index.html');
34
+ if (!fs.existsSync(idx)) return { ok: false, erro: 'sem index.html' };
35
+ const r = trocar(fs.readFileSync(idx, 'utf8'));
36
+ if (!r.trocou && !r.injetou) return { ok: true, trocou: 0, injetou: false };
37
+ fs.mkdirSync(path.join(pasta, 'vendor'), { recursive: true });
38
+ fs.copyFileSync(ARQUIVO, path.join(pasta, DESTINO_REL));
39
+ fs.writeFileSync(idx, r.html);
40
+ return { ok: true, trocou: r.trocou, injetou: r.injetou };
41
+ }
42
+
43
+ module.exports = { trocar, embutir, ARQUIVO, DESTINO_REL };
@@ -674,7 +674,7 @@ async function ferramentaAE(args) {
674
674
  const p = args.id ? projeto.ler(args.id) : null;
675
675
  if (!args.id || !p) return semProjeto(args.id);
676
676
  if (!args.script || !args.comp) return { ok: false, error: 'estudio_ae pede script (o JSX) e comp (o nome da composição).' };
677
- return require('./cota.js').comCota(() => renderAE(args, p));
677
+ return require('./cota.js').comCota(() => renderAE(args, p), { marca: args.marcaDagua === true });
678
678
  }
679
679
 
680
680
  async function renderAE(args, p) {
@@ -707,7 +707,7 @@ function entregar(args, p, r) {
707
707
  }
708
708
 
709
709
  /** A máquina de motion: assunto + cenas em JSON viram o mp4 (maquina.js), com sons, trilha e voz opcionais. */
710
- async function ferramentaMotion(args) {
710
+ async function ferramentaMotion(args, ctx = {}, deps = {}) {
711
711
  const p = args.id ? projeto.ler(args.id) : null;
712
712
  if (!args.id || !p) return semProjeto(args.id);
713
713
  let receita = null;
@@ -721,10 +721,29 @@ async function ferramentaMotion(args) {
721
721
  // primeiro motion: aprende o gosto antes de renderizar (a pessoa pode fechar ou pular; o render segue de qualquer jeito)
722
722
  // no terminal abre o navegador; no app (desktop ou Primo.app, -p com PRIMOCODE_APP=1) o app abre a tela pela linha de protocolo
723
723
  try { await require('./gosto.js').antesDoMotion(args); } catch { /* sem tela, segue */ }
724
- return require('./cota.js').comCota(() => renderMotion(args, p, receita));
724
+ // As etapas na frente do motor (motion-plano): ajusta o pedido, estrutura, monta o plano; o plano vira `cenas`.
725
+ // `etapas:false` ou PRIMOCODE_MOTION_ETAPAS=0 liga o caminho antigo. Falhou e não é estrito: segue pelo antigo, com aviso.
726
+ const MP = require('./motion-plano/index.js');
727
+ let etapas = null, avisoEtapas = null;
728
+ if (MP.ligado(args)) {
729
+ const fotosDoPedido = [...new Set([...(Array.isArray(args.fotos) ? args.fotos : args.fotos ? [args.fotos] : []), ...referencias.imagens()])];
730
+ const r = await MP.preparar(args, { escolher: ctx.escolher, linha: ctx.linha }, { llm: deps.llm, pasta: p.pasta, fotos: fotosDoPedido });
731
+ if (r.ok) { args = r.args; etapas = r.etapas; }
732
+ else if (deps.estrito || args.estrito) return { ok: false, error: r.error, etapa: r.etapa, passos: r.passos };
733
+ else avisoEtapas = `As etapas (ajustar, estruturar, planejar) não rodaram: ${r.error} Segui pelo caminho antigo.`;
734
+ }
735
+ const saida = await require('./cota.js').comCota(() => renderMotion(args, p, receita, deps.motor), { marca: args.marcaDagua === true });
736
+ if (!saida || saida.ok === false) return saida;
737
+ const extra = {};
738
+ if (etapas) {
739
+ extra.etapas = { briefing: etapas.briefing, plano: etapas.plano, respostas: etapas.respostas };
740
+ extra.critico = MP.critico(saida.video, p.pasta);
741
+ }
742
+ if (avisoEtapas) extra.avisos = [...(saida.avisos || []), avisoEtapas];
743
+ return { ...saida, ...extra };
725
744
  }
726
745
 
727
- async function renderMotion(args, p, receita) {
746
+ async function renderMotion(args, p, receita, depsMotor = {}) {
728
747
  const narrar = async (texto, mp3, voz) => rodarPython('narrar.py', ['falar', '--texto', texto, '--saida', mp3, '--voz', String(voz || 'francisca')], { timeout: 5 * 60000 });
729
748
  // Kit de marca ativo: cores, fontes e logo entram onde o JSON não disse; o logo vai no CTA (última cena).
730
749
  const kitM = marcaKit.preencher(args.marca, { pastas: [p.pasta, process.cwd()] });
@@ -734,13 +753,16 @@ async function renderMotion(args, p, receita) {
734
753
  args.cenas = args.cenas.map((c, i) => (i === args.cenas.length - 1 && c && !c.logo ? { ...c, logo: kitM.logo } : c));
735
754
  }
736
755
  }
756
+ // Com o plano ativo e sem cor de marca nem estilo pedido, a pele é a do Destrava (creme, Inter 800, degradê rosa → violeta).
757
+ const pele = require('./motion-plano/pele.js');
758
+ if (pele.deveUsar(args, kitM)) args = pele.aplicar(args);
737
759
  // As fotos do pedido: as que a IA passou em `fotos` e as que o terminal achou no texto do pedido (referencias.imagens()).
738
760
  // Antes só o prepararRoteiro (Remotion) as lia: no motion elas viravam "referência" e nenhuma peça aceitava foto.
739
761
  const doPedido = referencias.imagens();
740
762
  const dadas = Array.isArray(args.fotos) ? args.fotos : (args.fotos ? [args.fotos] : []);
741
763
  const todas = [...new Set([...dadas, ...doPedido])];
742
764
  if (todas.length) args = { ...args, fotos: todas, ...(dadas.length ? {} : { fotosImplicitas: true }) };
743
- const r = await require('./maquina.js').motion(args, { pasta: p.pasta, receita, deps: { narrar } });
765
+ const r = await require('./maquina.js').motion(args, { pasta: p.pasta, receita, deps: { narrar, ...depsMotor } });
744
766
  if (r.ok) projeto.anotar(p.id, 'motion', `${r.duracao}s, ${r.cenas.length} cenas, ${r.lufs} LUFS`);
745
767
  if (r.ok) entregar(args, p, r);
746
768
  if (!r.ok) return r;
@@ -2413,16 +2435,47 @@ async function transcrever(p, alvo, ctx) {
2413
2435
  escondido — sem telemetria (HYPERFRAMES_NO_TELEMETRY). Depois, o mesmo do
2414
2436
  Remotion: volume de Reels, a conferência e o vídeo aberto. */
2415
2437
  const HYPERFRAMES = 'hyperframes@0.8.154';
2416
- async function renderHyperframes(p, args, ctx = {}) {
2438
+ /* Acha a composição que a IA escreveu. O contrato é "uma pasta com index.html", mas o modelo erra de três jeitos:
2439
+ passa o caminho do próprio .html, passa o HTML no lugar do caminho, ou deixa o HTML num bloco ```html no argumento.
2440
+ Os três viram a pasta certa em vez de um erro técnico. Devolve { origem } ou { erro }. */
2441
+ function acharComposicao(p, args) {
2417
2442
  const pedido = String(args.html || args.composicao || 'hyperframes').trim();
2418
- const origem = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
2419
- .find((d) => d && fs.existsSync(path.join(d, 'index.html')));
2420
- if (!origem) return { ok: false, error: `não achei ${pedido}/index.html — escreva a composição do Hyperframes numa pasta com index.html e passe html:"<pasta>".` };
2443
+ const candidatos = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido].filter(Boolean);
2444
+ const pasta = candidatos.find((d) => fs.existsSync(path.join(d, 'index.html')));
2445
+ if (pasta) return { origem: pasta };
2446
+ // o caminho do próprio arquivo .html: usa a pasta dele (se não for index.html, copia como index.html)
2447
+ const arquivo = candidatos.find((f) => /\.html?$/i.test(f) && fs.existsSync(f) && fs.statSync(f).isFile());
2448
+ if (arquivo) {
2449
+ if (/^index\.html?$/i.test(path.basename(arquivo))) return { origem: path.dirname(arquivo) };
2450
+ const dir = path.join(p.pasta, 'hyperframes-entrada');
2451
+ fs.rmSync(dir, { recursive: true, force: true });
2452
+ fs.mkdirSync(dir, { recursive: true });
2453
+ fs.copyFileSync(arquivo, path.join(dir, 'index.html'));
2454
+ return { origem: dir };
2455
+ }
2456
+ // o HTML veio no lugar do caminho (começa com <, ou num bloco de código)
2457
+ const cerca = /```(?:html)?\s*([\s\S]*?)```/i.exec(pedido);
2458
+ const texto = (cerca ? cerca[1] : pedido).trim();
2459
+ if (/^<(!doctype|html|div|section|main|body|style)/i.test(texto)) {
2460
+ const dir = path.join(p.pasta, 'hyperframes-entrada');
2461
+ fs.rmSync(dir, { recursive: true, force: true });
2462
+ fs.mkdirSync(dir, { recursive: true });
2463
+ fs.writeFileSync(path.join(dir, 'index.html'), texto);
2464
+ return { origem: dir };
2465
+ }
2466
+ return { erro: `não achei a composição em "${pedido.slice(0, 80)}". Salve o HTML num arquivo index.html dentro de uma pasta (write_file) e chame estudio_render de novo com html:"<pasta>".` };
2467
+ }
2468
+
2469
+ async function renderHyperframes(p, args, ctx = {}) {
2470
+ const achada = acharComposicao(p, args);
2471
+ if (achada.erro) return { ok: false, error: achada.erro };
2472
+ const origem = achada.origem;
2421
2473
  const destino = path.join(p.pasta, 'hyperframes');
2422
2474
  if (path.resolve(origem) !== path.resolve(destino)) {
2423
2475
  fs.rmSync(destino, { recursive: true, force: true });
2424
2476
  fs.cpSync(origem, destino, { recursive: true });
2425
2477
  }
2478
+ try { require('./gsap-local.js').embutir(destino); } catch { /* sem o troca, o Hyperframes tenta como veio */ } // GSAP local, nunca CDN
2426
2479
  const midia = path.join(destino, 'midia');
2427
2480
  if (!fs.existsSync(midia) && fs.existsSync(path.join(p.pasta, 'midia'))) fs.symlinkSync(path.join(p.pasta, 'midia'), midia);
2428
2481
  const saida = path.join(p.pasta, 'saida', `${projeto.apelidar(p.titulo)}.mp4`);
@@ -2717,7 +2770,7 @@ async function ferramentaSite(args) {
2717
2770
  return r;
2718
2771
  }
2719
2772
 
2720
- module.exports = { proporcaoDoPedido,
2773
+ module.exports = { proporcaoDoPedido, acharComposicao,
2721
2774
  vazamentoNoComeco, variarIcones, objetosDoRoteiro,
2722
2775
  ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaAE, ferramentaMotion, ferramentaEditar, ferramentaNarrar, ferramentaCriar,
2723
2776
  ferramentaVideo, ferramentaRender, ferramentaSite,
@@ -235,7 +235,7 @@ function kEstiloCamera(cam, d) {
235
235
  1) Glow (brilho do que é claro), 2) um vazamento de luz suave (a aberração cromática VR foi tirada: deixava uma faixa de 1–2% na borda direita e na base) (mancha de cor
236
236
  borrada em ADD, varrendo o quadro). A energia vem do movimento e da câmera, não de entulho. */
237
237
  function kAtmosfera(comp, P) {
238
- if (KIT.fx === false) return;
238
+ if (KIT.fx === false || KIT.pele == 'destrava') return; // a pele do Destrava é creme limpo: sem vazamento rosa em ADD nem glow por cima
239
239
  var W = comp.width, H = comp.height, u = Math.min(W, H) / 1080, pun = KIT.punch || [], cod = kPunchCodigo(pun), d = comp.duration;
240
240
  try { // 3) vazamento de luz: uma mancha, sem pulso
241
241
  var lk = comp.layers.addShape(); lk.name = 'vazamento de luz';
@@ -369,6 +369,8 @@ var CENAS = {
369
369
  parede_logos: cenaParedeLogos, prompt: cenaPrompt, odometro: cenaOdometro, medidor: cenaMedidor, carimbo: cenaCarimbo, leque: cenaLeque, ondas: cenaOndas,
370
370
  rolagem: cenaRolagem, estrelas: cenaEstrelas, foto: cenaFoto
371
371
  };
372
+ // as peças do Destrava (destrava.jsx): o plano de motion marca a cena com d.dz
373
+ var CENAS_DZ = { frase: cenaDFrase, cor: cenaDCor, foto: cenaDFoto, marca: cenaDMarca };
372
374
 
373
375
  function montar(PL) {
374
376
  var W = PL.w, H = PL.h, fps = PL.fps || 30, n = PL.cenas.length, i;
@@ -376,8 +378,9 @@ function montar(PL) {
376
378
  KIT.marca = { titulo: PL.fontes.titulo, destaque: PL.fontes.destaque, texto: PL.fontes.texto };
377
379
  KIT.tipo = PL.tipo || {};
378
380
  KIT.elementos = PL.elementos || [];
381
+ KIT.fotos = PL.fotos || [];
379
382
  KIT.voz = PL.voz || null;
380
- KIT.paletaBase = PL.paleta || null; KIT.debugFit = !!PL.debugFit; KIT.bpm = PL.bpm || 0; KIT.batidaFase = PL.batidaFase || 0; KIT.fx = PL.fx !== false;
383
+ KIT.paletaBase = PL.paleta || null; KIT.pele = PL.pele || null; KIT.debugFit = !!PL.debugFit; KIT.bpm = PL.bpm || 0; KIT.batidaFase = PL.batidaFase || 0; KIT.fx = PL.fx !== false;
381
384
  // a câmera dá zoom de até ~10%: o conteúdo é montado a 90% (87% com legenda, subido 7%) para NÃO sair da área segura quando a câmera chega perto
382
385
  KIT.enq = 0.88; KIT.enqDy = 0;
383
386
  if (PL.legenda && PL.legenda.chunks && PL.legenda.chunks.length) { KIT.enq = 0.85; KIT.enqDy = H * 0.07; }
@@ -387,7 +390,7 @@ function montar(PL) {
387
390
  var main = novaComp('MAIN', W, H, total, fps);
388
391
  KIT.main = main;
389
392
  // o fundo da MAIN é a cor de destaque: se as duas cenas se afastarem numa troca (chicote, empurra), o vão é uma faixa de cor, nunca preto
390
- main.layers.addSolid(kCor(PL.paleta && PL.paleta.destaque ? PL.paleta.destaque : '#FFFFFF'), 'fundo-main', W, H, 1);
393
+ main.layers.addSolid(kCor(PL.pele == 'destrava' && PL.paleta && PL.paleta.fundo ? PL.paleta.fundo : (PL.paleta && PL.paleta.destaque ? PL.paleta.destaque : '#FFFFFF')), 'fundo-main', W, H, 1);
391
394
  // um fundo por cena, embaixo de tudo, com a cor DA CENA: se a câmera ou a troca afastar a cena, a borda mostra a cor dela, não uma faixa de outra cor
392
395
  for (i = 0; i < n; i++) { var fb = main.layers.addSolid(kCor(PL.cenas[i].pal.bg), 'fundo-cena ' + i, W, H, 1); fb.inPoint = starts[i]; fb.outPoint = i < n - 1 ? Math.min(total, starts[i + 1] + TROCA_DUR + 0.05) : total; }
393
396
  var cam = [];
@@ -396,13 +399,13 @@ function montar(PL) {
396
399
  // a cena é montada em tempo NOMINAL (dur × f) e tocada f vezes mais rápido dentro de uma comp-embrulho: tudo entra nos primeiros ~35%
397
400
  var comp = novaComp('CENA ' + i, W, H, dur * f, fps);
398
401
  // enfeite só onde há espaço livre de verdade (nada de herói em cima de janela, prompt ou botão)
399
- KIT.cenaComElemento = !!c.elemento; KIT.semAmbiente = !!c.semAmbiente || !{ cinetico: 1, gancho: 1, numero: 1, citacao: 1, particulas: 1, odometro: 1, relogio: 1, carimbo: 1 }[c.tipo];
402
+ KIT.cenaComElemento = !!c.elemento; KIT.semAmbiente = !!c.semAmbiente || !!c.dz || !{ cinetico: 1, gancho: 1, numero: 1, citacao: 1, particulas: 1, odometro: 1, relogio: 1, carimbo: 1 }[c.tipo];
400
403
  // (até 20/10 esta linha morava DEPOIS do comentário acima, na mesma linha: o cenaIdx, o t0, o ajuste e o f nunca eram postos)
401
404
  KIT.semRotulos = !!c.semRotulos; KIT.cenaHeroi = c.tipo == 'heroi' || c.tipo == 'revela'; KIT.titulosL = []; KIT.tituloB = null; KIT.t0 = starts[i]; KIT.cenaIdx = i; KIT.estilo = c.camera || PL.camera || 'deriva'; KIT.ajusteCena = c.ajuste || 1; KIT.tituloDesce = c.tituloDesce || 0; KIT.f = f; KIT.entMax = 0;
402
405
  var P = c.pal; KIT.pal = P;
403
406
  P.fonteTitulo = PL.fontes.titulo; P.fonteTexto = PL.fontes.texto; P.fonteDestaque = PL.fontes.destaque;
404
407
  if (!CENAS[c.tipo]) KIT.avisos.push('cena ' + i + ': tipo desconhecido ' + c.tipo);
405
- (CENAS[c.tipo] || cenaGancho)(comp, P, c);
408
+ (c.dz && CENAS_DZ[c.dz] ? CENAS_DZ[c.dz] : (CENAS[c.tipo] || cenaGancho))(comp, P, c);
406
409
  writeLn('ENTRADA ' + JSON.stringify({ cena: i, max: Math.round(kEntradaMax(comp) * 100) / 100, f: f, dur: c.dur }));
407
410
  var alvo = comp;
408
411
  if (f > 1.001) {