primocode 10.12.0 → 10.14.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 (207) hide show
  1. package/README.md +74 -12
  2. package/bin/primocode.js +148 -8
  3. package/lib/act.js +48 -1
  4. package/lib/catalogo.js +12 -1
  5. package/lib/config.js +1 -0
  6. package/lib/estudio/assinaturas/gerativo.json +1 -1
  7. package/lib/estudio/camadas.js +4 -0
  8. package/lib/estudio/cota.js +150 -0
  9. package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
  10. package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
  11. package/lib/estudio/dados/projetos/clube-agent.json +120 -0
  12. package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
  13. package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
  14. package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
  15. package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
  16. package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
  17. package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
  18. package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
  19. package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
  20. package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
  21. package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
  22. package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
  23. package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
  24. package/lib/estudio/dados/projetos/clube-clube.json +108 -0
  25. package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
  26. package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
  27. package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
  28. package/lib/estudio/dados/projetos/clube-dub.json +119 -0
  29. package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
  30. package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
  31. package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
  32. package/lib/estudio/dados/projetos/clube-frame.json +108 -0
  33. package/lib/estudio/dados/projetos/clube-glass.json +118 -0
  34. package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
  35. package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
  36. package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
  37. package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
  38. package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
  39. package/lib/estudio/dados/projetos/clube-morph.json +119 -0
  40. package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
  41. package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
  42. package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
  43. package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
  44. package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
  45. package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
  46. package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
  47. package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
  48. package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
  49. package/lib/estudio/dados/projetos/clube-promo.json +114 -0
  50. package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
  51. package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
  52. package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
  53. package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
  54. package/lib/estudio/dados/projetos/clube-robo.json +125 -0
  55. package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
  56. package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
  57. package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
  58. package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
  59. package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
  60. package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
  61. package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
  62. package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
  63. package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
  64. package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
  65. package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
  66. package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
  67. package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
  68. package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
  69. package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
  70. package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
  71. package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
  72. package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
  73. package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
  74. package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
  75. package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
  76. package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
  77. package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
  78. package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
  79. package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
  80. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
  81. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
  82. package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
  83. package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
  84. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
  85. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
  86. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
  87. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
  88. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
  89. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
  90. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
  91. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
  92. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
  93. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
  94. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
  95. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
  96. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
  97. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
  98. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
  99. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
  100. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
  101. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
  102. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
  103. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
  104. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
  105. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
  106. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
  107. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
  108. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
  109. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
  110. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
  111. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
  112. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
  113. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
  114. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
  115. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
  116. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
  117. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
  118. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
  119. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
  120. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
  121. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
  122. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
  123. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
  124. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
  125. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
  126. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
  127. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
  128. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
  129. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
  130. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
  131. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
  132. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
  133. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
  134. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
  135. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
  136. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
  137. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
  138. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
  139. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
  140. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
  141. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
  142. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
  143. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
  144. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
  145. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
  146. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
  147. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
  148. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
  149. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
  150. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
  151. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
  152. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
  153. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
  154. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
  155. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
  156. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
  157. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
  158. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
  159. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
  160. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
  161. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
  162. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
  163. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
  164. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
  165. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
  166. package/lib/estudio/diretor.js +2 -1
  167. package/lib/estudio/editor.js +285 -0
  168. package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
  169. package/lib/estudio/effectcraft.js +152 -0
  170. package/lib/estudio/exportar.js +181 -0
  171. package/lib/estudio/gerativo.js +44 -15
  172. package/lib/estudio/index.js +126 -5
  173. package/lib/estudio/kit/gerativo.jsx +15 -0
  174. package/lib/estudio/kit-ae/base.jsx +1146 -0
  175. package/lib/estudio/kit-ae/cenas.jsx +395 -0
  176. package/lib/estudio/kit-ae/primitivas.jsx +911 -0
  177. package/lib/estudio/kokoro.js +3 -1
  178. package/lib/estudio/maquina-diretor.js +393 -0
  179. package/lib/estudio/maquina-elementos.js +134 -0
  180. package/lib/estudio/maquina-layout.js +198 -0
  181. package/lib/estudio/maquina-som.js +478 -0
  182. package/lib/estudio/maquina-trilha.js +160 -0
  183. package/lib/estudio/maquina-voz.js +429 -0
  184. package/lib/estudio/maquina.js +516 -0
  185. package/lib/estudio/projetos.js +60 -0
  186. package/lib/estudio/remotion.js +6 -4
  187. package/lib/estudio/revisor.js +61 -0
  188. package/lib/estudio/voicestudio.js +81 -0
  189. package/lib/headless.js +71 -0
  190. package/lib/hooks.js +72 -0
  191. package/lib/local/cerebro.json +398 -12
  192. package/lib/local/entrada.js +46 -2
  193. package/lib/marca-kit.js +309 -0
  194. package/lib/mcp.js +286 -6
  195. package/lib/perigo.js +31 -3
  196. package/lib/planos.js +47 -19
  197. package/lib/preparar.js +111 -0
  198. package/lib/retomar.js +55 -0
  199. package/lib/sandbox.js +197 -0
  200. package/lib/skills.js +3 -1
  201. package/lib/tools.js +13 -4
  202. package/lib/ui.js +1 -1
  203. package/lib/video/auto.js +18 -3
  204. package/package.json +4 -2
  205. package/skills/after-effects/SKILL.md +52 -0
  206. package/skills/maquina-de-motion/SKILL.md +84 -0
  207. package/skills/motion-de-primeira/SKILL.md +33 -51
@@ -0,0 +1,152 @@
1
+ /**
2
+ * effectcraft.js — o motor After Effects do Estúdio.
3
+ *
4
+ * O EffectCraft (github.com/storytold/effectcraft, MIT/Apache-2.0) roda JSX
5
+ * (ExtendScript) no modelo de objetos do After Effects, sem o After Effects:
6
+ * `script` monta o projeto (.ecproj), `render` gera o mp4 e `render-frame` um
7
+ * quadro para a IA conferir. O PrimoCode baixa o binário uma vez, confere o
8
+ * sha256 e guarda em ~/.primocode/bin/effectcraft-<versão>/.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+ const { spawnSync } = require('child_process');
18
+
19
+ const VERSAO = '0.11.1';
20
+ const BASE = 'https://github.com/storytold/effectcraft/releases/download/v' + VERSAO + '/';
21
+ const SHA_MAC = '8eae2e1e9c13e03d1bc2460501a90d43fc570b37e9b386294c0720eb12aad0f0';
22
+ const PASTA_BIN = () => path.join(os.homedir(), '.primocode', 'bin', 'effectcraft-' + VERSAO);
23
+
24
+ /** O asset da release para esta máquina: { arquivo, sha (null = ler do SHA256SUMS.txt) }. */
25
+ function asset(plataforma = process.platform, arq = process.arch) {
26
+ if (plataforma === 'darwin') return { arquivo: `effectcraft-cli-${VERSAO}-macos-universal.zip`, sha: SHA_MAC };
27
+ if (plataforma === 'linux') return { arquivo: `effectcraft-${VERSAO}-linux-${arq === 'arm64' ? 'aarch64' : 'x86_64'}.tar.gz`, sha: null };
28
+ if (plataforma === 'win32') return { arquivo: `effectcraft-${VERSAO}-windows-${arq === 'arm64' ? 'arm64' : 'x64'}-portable.zip`, sha: null };
29
+ return null;
30
+ }
31
+
32
+ function achar(dir, nome) {
33
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
34
+ const p = path.join(dir, e.name);
35
+ if (e.isDirectory()) { const r = achar(p, nome); if (r) return r; }
36
+ else if (e.name === nome || e.name === nome + '.exe') return p;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ async function baixar(url) {
42
+ const r = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(180000) });
43
+ if (!r.ok) throw new Error('HTTP ' + r.status + ' em ' + url);
44
+ return Buffer.from(await r.arrayBuffer());
45
+ }
46
+
47
+ /** O caminho do executável (baixa se faltar). `{ok, bin}` ou `{ok:false, error}`. */
48
+ async function garantir({ bin } = {}) {
49
+ const dado = bin || process.env.PRIMOCODE_EFFECTCRAFT_BIN;
50
+ if (dado) return fs.existsSync(dado) ? { ok: true, bin: dado } : { ok: false, error: 'binário do EffectCraft não existe: ' + dado };
51
+ const dir = PASTA_BIN();
52
+ const ja = fs.existsSync(dir) ? achar(dir, 'effectcraft-cli') : null;
53
+ if (ja) return { ok: true, bin: ja };
54
+ const a = asset();
55
+ if (!a) return { ok: false, error: 'o EffectCraft não tem binário para ' + process.platform };
56
+ try {
57
+ let sha = a.sha;
58
+ if (!sha) {
59
+ const sums = (await baixar(BASE + 'SHA256SUMS.txt')).toString('utf8');
60
+ const linha = sums.split('\n').find((l) => l.trim().endsWith(a.arquivo));
61
+ sha = linha && linha.trim().split(/\s+/)[0];
62
+ if (!sha) return { ok: false, error: 'SHA256SUMS.txt da release não lista ' + a.arquivo };
63
+ }
64
+ const buf = await baixar(BASE + a.arquivo);
65
+ const got = crypto.createHash('sha256').update(buf).digest('hex');
66
+ if (got !== sha) return { ok: false, error: `sha256 do EffectCraft não confere (esperado ${sha}, veio ${got})` };
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ const tmp = path.join(dir, a.arquivo);
69
+ fs.writeFileSync(tmp, buf);
70
+ const x = a.arquivo.endsWith('.zip')
71
+ ? (process.platform === 'win32' ? spawnSync('tar', ['-xf', tmp, '-C', dir]) : spawnSync('unzip', ['-oq', tmp, '-d', dir]))
72
+ : spawnSync('tar', ['-xzf', tmp, '-C', dir]);
73
+ fs.rmSync(tmp, { force: true });
74
+ if (x.status !== 0) return { ok: false, error: 'não consegui extrair o EffectCraft: ' + String(x.stderr || x.error || '').slice(0, 300) };
75
+ const exe = achar(dir, 'effectcraft-cli');
76
+ if (!exe) return { ok: false, error: 'o executável effectcraft-cli não estava no pacote' };
77
+ if (process.platform !== 'win32') fs.chmodSync(exe, 0o755);
78
+ return { ok: true, bin: exe };
79
+ } catch (e) {
80
+ return { ok: false, error: 'não consegui baixar o EffectCraft (sem rede?): ' + (e && e.message || e) };
81
+ }
82
+ }
83
+
84
+ function rodar(bin, args, env, timeout) {
85
+ const r = spawnSync(bin, args, { encoding: 'utf8', env, timeout, maxBuffer: 32 * 1024 * 1024 });
86
+ const saida = ((r.stdout || '') + (r.stderr || '')).trim();
87
+ if (r.error) return { ok: false, saida: (r.error.code === 'ETIMEDOUT' ? 'estourou o tempo' : r.error.message) + '\n' + saida };
88
+ return { ok: r.status === 0, saida };
89
+ }
90
+
91
+ /**
92
+ * Roda o JSX, renderiza o mp4 e os quadros de conferência.
93
+ * `fontes`: [{familia, peso}] baixadas do Google Fonts e passadas em EFFECTCRAFT_FONT_DIRS.
94
+ * `previa`: rascunho em meia resolução e qualidade draft (segundos, não minutos), para aprovar antes do final.
95
+ */
96
+ async function renderizar({ pasta, script, comp, fontes = [], audio, conferir = [], bin, timeouts = {}, semVideo = false, previa = false } = {}) {
97
+ if (!pasta || !script || !comp) return { ok: false, error: 'faltou pasta, script ou comp' };
98
+ const g = await garantir({ bin });
99
+ if (!g.ok) return g;
100
+ const ae = path.join(pasta, 'ae');
101
+ fs.mkdirSync(ae, { recursive: true });
102
+ const jsx = path.join(ae, 'cena.jsx');
103
+ const projeto = path.join(ae, 'projeto.ecproj');
104
+ fs.writeFileSync(jsx, script);
105
+
106
+ const env = { ...process.env };
107
+ const dirs = [];
108
+ if (fontes && fontes.length) {
109
+ const glifos = require('./glifos.js');
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 */ }
112
+ }
113
+ }
114
+ if (dirs.length) env.EFFECTCRAFT_FONT_DIRS = [...new Set(dirs)].join(path.delimiter);
115
+
116
+ const s = rodar(g.bin, ['script', jsx, '--empty', '--save-as', projeto], env, timeouts.script || 120000);
117
+ if (!s.ok || !fs.existsSync(projeto)) return { ok: false, error: 'o script JSX falhou:\n' + s.saida.slice(-1500) };
118
+
119
+ const bruto = path.join(pasta, audio ? 'video-ae-mudo.mp4' : 'video-ae.mp4');
120
+ if (semVideo) { // só a conferência: o script rodou (avisos, auditoria) e os quadros saem; o vídeo fica para a passada final
121
+ const quadros0 = [];
122
+ for (const t of conferir || []) {
123
+ const png = path.join(ae, `quadro-${t}.png`);
124
+ const q = rodar(g.bin, ['render-frame', '--project', projeto, '--comp', comp, '--time', String(t), '--out', png, '--max-side', '640'], env, timeouts.quadro || 60000);
125
+ if (q.ok && fs.existsSync(png)) quadros0.push(png);
126
+ }
127
+ return { ok: true, video: null, quadros: quadros0, projeto, saida: s.saida, semVideo: true };
128
+ }
129
+ const r = rodar(g.bin, ['render', '--project', projeto, '--comp', comp, '--out', bruto, '--format', 'h264', ...(previa ? ['--resolution', 'half', '--quality', 'draft'] : [])], env, timeouts.render || (previa ? 180000 : 600000));
130
+ if (!r.ok || !fs.existsSync(bruto)) return { ok: false, error: 'o render falhou:\n' + r.saida.slice(-1500) };
131
+
132
+ const quadros = [];
133
+ for (const t of conferir || []) {
134
+ const png = path.join(ae, `quadro-${t}.png`);
135
+ const q = rodar(g.bin, ['render-frame', '--project', projeto, '--comp', comp, '--time', String(t), '--out', png, '--max-side', '640'], env, timeouts.quadro || 60000);
136
+ if (q.ok && fs.existsSync(png)) quadros.push(png);
137
+ }
138
+
139
+ let video = bruto;
140
+ const aud = audio ? path.resolve(pasta, audio) : null;
141
+ if (aud && fs.existsSync(aud)) {
142
+ const final = path.join(pasta, 'video-ae.mp4');
143
+ const m = rodar('ffmpeg', ['-y', '-i', bruto, '-i', aud, '-c:v', 'copy', '-c:a', 'aac', '-shortest', final], process.env, timeouts.audio || 120000);
144
+ if (m.ok && fs.existsSync(final)) { video = final; fs.rmSync(bruto, { force: true }); }
145
+ } else if (audio) {
146
+ fs.renameSync(bruto, path.join(pasta, 'video-ae.mp4'));
147
+ video = path.join(pasta, 'video-ae.mp4');
148
+ }
149
+ return { ok: true, video, quadros, projeto, saida: s.saida };
150
+ }
151
+
152
+ module.exports = { VERSAO, asset, garantir, renderizar };
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+ /**
3
+ * exportar.js — o PACOTE PRA POSTAR do estudio_motion, depois do render.
4
+ * exportar({video, pasta, assunto, formatos, cenas, palavras, trilha, sfx, assinatura}) → {ok, dir, arquivos}
5
+ * srt(palavras) → texto .srt (usa os mesmos pedaços da legenda do vídeo)
6
+ * postTxt({...}) → gancho + 3 linhas + CTA + 5 hashtags + créditos CC BY
7
+ * creditosDe(trilha, sfx) → linhas de atribuição (só licenças que exigem)
8
+ * galeria: registrar / listar / abrir / main(['abrir'|'pasta', n])
9
+ * Nunca estica: 9:16, 1:1 e 16:9 saem com o fundo desfocado e o vídeo centralizado.
10
+ */
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { spawnSync } = require('child_process');
15
+
16
+ const DIMS = { '9:16': [1080, 1920], '1:1': [1080, 1080], '16:9': [1920, 1080] };
17
+ const ARQ_GALERIA = () => process.env.PRIMOCODE_VIDEOS || path.join(os.homedir(), '.primocode', 'studio', 'videos.json');
18
+ const PARADAS = new Set('para como uma umas uns com que dos das nos nas por mais sobre entre seu sua isso esse essa este esta pelo pela muito tudo cada sem'.split(' '));
19
+
20
+ const ff = (args) => spawnSync('ffmpeg', ['-y', '-v', 'error', ...args], { encoding: 'utf8' });
21
+ function sondar(arq) {
22
+ const r = spawnSync('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height:format=duration', '-of', 'json', arq], { encoding: 'utf8' });
23
+ try { const j = JSON.parse(r.stdout); const s = j.streams[0]; return { w: s.width, h: s.height, dur: parseFloat(j.format.duration) }; } catch { return null; }
24
+ }
25
+
26
+ // ── legenda .srt ─────────────────────────────────────────────────────────
27
+ const tempoSrt = (s) => {
28
+ const ms = Math.max(0, Math.round(s * 1000));
29
+ const p = (n, l = 2) => String(n).padStart(l, '0');
30
+ return `${p(Math.floor(ms / 3600000))}:${p(Math.floor(ms / 60000) % 60)}:${p(Math.floor(ms / 1000) % 60)},${p(ms % 1000, 3)}`;
31
+ };
32
+ function srt(palavras, { max = 3 } = {}) {
33
+ if (!Array.isArray(palavras) || !palavras.length) return '';
34
+ // agrupamento próprio (a legenda do vídeo junta frases inteiras; o .srt das redes quebra em trechos curtos de até `max` palavras)
35
+ const chunks = [];
36
+ palavras.forEach((p, i) => {
37
+ const prev = palavras[i - 1], atual = chunks[chunks.length - 1];
38
+ if (!atual || !prev || atual.palavras.length >= max || /[.!?;:]$/.test(prev.w) || p.t - prev.t2 > 0.25) chunks.push({ t: p.t, t2: p.t2, palavras: [p] });
39
+ else { atual.palavras.push(p); atual.t2 = p.t2; }
40
+ });
41
+ return chunks.map((c, i) => {
42
+ const fim = Math.max(c.t2, c.t + 0.4);
43
+ return `${i + 1}\n${tempoSrt(c.t)} --> ${tempoSrt(fim)}\n${c.palavras.map((p) => String(p.w).replace(/\*/g, '')).join(' ')}\n`;
44
+ }).join('\n');
45
+ }
46
+
47
+ // ── os créditos que exigem atribuição (CC BY, BY-SA...) ──────────────────
48
+ function fichaDe(arq) {
49
+ if (!arq) return null;
50
+ for (const f of [arq.replace(/\.[a-z0-9]+$/i, '') + '.licenca.json', arq + '.licenca.json']) {
51
+ try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { /* próxima */ }
52
+ }
53
+ return null;
54
+ }
55
+ const exigeCredito = (lic) => /^(cc[- ]?by|by)/i.test(String(lic || '').trim()) && !/^(cc0|zero)/i.test(String(lic || ''));
56
+ function creditosDe(trilha, sfx = []) {
57
+ const out = [];
58
+ const fichaT = trilha && fichaDe(trilha.arquivo);
59
+ if (trilha && trilha.creditos && !(fichaT && !exigeCredito(fichaT.licenca) && !/by/i.test(String(trilha.creditos)))) out.push(`Trilha: ${typeof trilha.creditos === 'string' ? trilha.creditos : (trilha.titulo || '') + (trilha.autor ? ' — ' + trilha.autor : '')}`);
60
+ else if (fichaT && exigeCredito(fichaT.licenca)) out.push(`Trilha: "${fichaT.titulo}"${fichaT.autor ? ' — ' + fichaT.autor : ''} (${String(fichaT.licenca).toUpperCase()}${fichaT.origem ? ', ' + fichaT.origem : ''})`);
61
+ const vistos = new Set();
62
+ for (const arq of sfx) {
63
+ const f = fichaDe(arq);
64
+ if (!f || !exigeCredito(f.licenca)) continue;
65
+ const l = `SFX: "${f.titulo}"${f.autor ? ' — ' + f.autor : ''} (${String(f.licenca).toUpperCase()}${f.origem ? ', ' + f.origem : ''})`;
66
+ if (!vistos.has(l)) { vistos.add(l); out.push(l); }
67
+ }
68
+ return out;
69
+ }
70
+
71
+ // ── o texto do post ──────────────────────────────────────────────────────
72
+ const limpo = (s) => String(s || '').replace(/\*/g, '').replace(/\s+/g, ' ').trim();
73
+ function hashtags(assunto, n = 5) {
74
+ const norm = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
75
+ const tags = [];
76
+ for (const w of limpo(assunto).split(/\s+/)) { const t = norm(w); if (t.length >= 4 && !PARADAS.has(t) && !tags.includes(t)) tags.push(t); }
77
+ for (const t of ['motion', 'video', 'reels', 'design', 'criatividade']) if (!tags.includes(t)) tags.push(t);
78
+ return tags.slice(0, n).map((t) => '#' + t);
79
+ }
80
+ function postTxt({ assunto, cenas = [], creditos = [] } = {}) {
81
+ const txt = (c) => limpo(c.titulo || c.texto || c.fala || '');
82
+ const gancho = cenas.find((c) => c.papel === 'gancho') || cenas[0] || {};
83
+ const cta = [...cenas].reverse().find((c) => c.papel === 'cta') || null;
84
+ const meio = cenas.filter((c) => c !== gancho && c !== cta).map(txt).filter(Boolean);
85
+ const linhas = meio.slice(0, 3);
86
+ while (linhas.length < 3) linhas.push(['Feito em motion, do roteiro ao corte.', 'Cada cena no tempo da batida.', 'Salve pra rever depois.'][linhas.length]);
87
+ const chamada = cta ? limpo(cta.botao || cta.titulo || cta.texto) : '';
88
+ const partes = [txt(gancho) || limpo(assunto), '', ...linhas.map((l) => '• ' + l), '', chamada ? `${chamada} 👇` : 'Curtiu? Salva e manda pra quem precisa.', '', hashtags(assunto).join(' ')];
89
+ if (creditos.length) partes.push('', 'Créditos:', ...creditos);
90
+ return partes.join('\n') + '\n';
91
+ }
92
+
93
+ // ── as versões de formato (fundo desfocado + vídeo centralizado) ─────────
94
+ function recompor(video, saida, [W, H]) {
95
+ const vf = `split[a][b];[a]scale=${W}:${H}:force_original_aspect_ratio=increase,crop=${W}:${H},boxblur=30:5[bg];[b]scale=${W}:${H}:force_original_aspect_ratio=decrease[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2,setsar=1,format=yuv420p`;
96
+ const r = ff(['-i', video, '-vf', vf, '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-c:a', 'copy', '-movflags', '+faststart', saida]);
97
+ return r.status === 0 && fs.existsSync(saida);
98
+ }
99
+
100
+ // ── a capa: o quadro de maior contraste, com título (meio de cada cena) ──
101
+ function capa(video, saida, { cenas = [], dur } = {}) {
102
+ const layout = require('./maquina-layout.js');
103
+ const total = dur || (sondar(video) || {}).dur || 0;
104
+ let t = 0;
105
+ const tempos = cenas.length ? cenas.map((c) => { const x = t + (c.dur || 1) * 0.6; t += c.dur || 1; return x; }) : [0.2, 0.4, 0.6, 0.8].map((k) => k * total);
106
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'capa-'));
107
+ let melhor = null;
108
+ try {
109
+ tempos.filter((x) => x < total).forEach((x, i) => {
110
+ const png = path.join(tmp, `q${i}.png`);
111
+ if (ff(['-ss', x.toFixed(2), '-i', video, '-frames:v', '1', png]).status !== 0 || !fs.existsSync(png)) return;
112
+ let m; try { m = layout.medirQuadro(png); } catch { m = null; }
113
+ if (!m) return;
114
+ const pontos = m.contraste + Math.min(m.variancia, 60) / 30 - (i === 0 ? 0.3 : 0); // título legível + algo na tela
115
+ if (!melhor || pontos > melhor.pontos) melhor = { png, pontos, t: x };
116
+ });
117
+ if (!melhor) return null;
118
+ const r = ff(['-i', melhor.png, '-vf', 'scale=1080:-2', '-q:v', '2', saida]);
119
+ return r.status === 0 && fs.existsSync(saida) ? { arquivo: saida, t: melhor.t } : null;
120
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
121
+ }
122
+
123
+ // ── o pacote ─────────────────────────────────────────────────────────────
124
+ function exportar(o = {}) {
125
+ const { video, pasta, assunto = '', formatos = [], cenas = [], palavras = [], trilha = null, sfx = [] } = o;
126
+ if (!video || !fs.existsSync(video)) return { ok: false, error: 'sem vídeo para exportar.' };
127
+ const dir = path.join(pasta || path.dirname(video), 'entrega');
128
+ fs.mkdirSync(dir, { recursive: true });
129
+ const arquivos = [], avisos = [];
130
+ const final = path.join(dir, 'video.mp4');
131
+ fs.copyFileSync(video, final); arquivos.push(final);
132
+ const info = sondar(video);
133
+ for (const f of [...new Set(formatos || [])]) {
134
+ if (!DIMS[f]) { avisos.push(`formato desconhecido: ${f}`); continue; }
135
+ const s = path.join(dir, `video-${f.replace(':', 'x')}.mp4`);
136
+ if (recompor(video, s, DIMS[f])) arquivos.push(s); else avisos.push(`não consegui gerar o ${f}`);
137
+ }
138
+ const legenda = srt(palavras);
139
+ if (legenda) { const s = path.join(dir, 'legenda.srt'); fs.writeFileSync(s, legenda); arquivos.push(s); }
140
+ const c = capa(video, path.join(dir, 'capa.jpg'), { cenas, dur: info && info.dur });
141
+ if (c) arquivos.push(c.arquivo); else avisos.push('a capa não saiu');
142
+ const creditos = creditosDe(trilha, sfx);
143
+ const post = path.join(dir, 'post.txt');
144
+ fs.writeFileSync(post, postTxt({ assunto, cenas, creditos })); arquivos.push(post);
145
+ return { ok: true, dir, arquivos, creditos, ...(avisos.length ? { avisos } : {}) };
146
+ }
147
+
148
+ // ── a galeria: ~/.primocode/studio/videos.json ───────────────────────────
149
+ function ler(arq = ARQ_GALERIA()) { try { const j = JSON.parse(fs.readFileSync(arq, 'utf8')); return Array.isArray(j) ? j : []; } catch { return []; } }
150
+ function registrar({ id, assunto, caminho, formato, duracao, assinatura, entrega }, arq = ARQ_GALERIA()) {
151
+ const l = ler(arq).filter((v) => v.caminho !== caminho);
152
+ const item = { id: id || path.basename(path.dirname(caminho)), assunto: limpo(assunto), data: new Date().toISOString(), caminho, formato, duracao, assinatura: assinatura || null, entrega: entrega || null };
153
+ l.push(item);
154
+ try { fs.mkdirSync(path.dirname(arq), { recursive: true }); fs.writeFileSync(arq, JSON.stringify(l.slice(-200), null, 2)); } catch { /* sem disco: segue */ }
155
+ return item;
156
+ }
157
+ const listar = (arq) => ler(arq).filter((v) => v.caminho && fs.existsSync(v.caminho)).sort((a, b) => String(b.data).localeCompare(String(a.data)));
158
+ function abrirNoSistema(alvo) {
159
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
160
+ const args = process.platform === 'win32' ? ['/c', 'start', '""', alvo] : [alvo];
161
+ return spawnSync(cmd, args, { stdio: 'ignore' }).status === 0;
162
+ }
163
+ const mmss = (s) => `${Math.floor(s / 60)}:${String(Math.round(s % 60)).padStart(2, '0')}`;
164
+ function formatarLista(l) {
165
+ if (!l.length) return ' Nenhum vídeo ainda. Peça um ao estúdio de motion.';
166
+ return ['', ...l.map((v, i) => ` ${String(i + 1).padStart(2)}. ${limpo(v.assunto).slice(0, 44).padEnd(44)} ${String(v.formato || '').padEnd(5)} ${mmss(v.duracao || 0).padStart(5)} ${String(v.data).slice(0, 10)}`), '', ' primocode videos abrir <n> · primocode videos pasta <n>'].join('\n');
167
+ }
168
+ /** `primocode videos [abrir|pasta <n>]` e /videos. Devolve o texto; `abrir` é injetável nos testes. */
169
+ function main(args = [], { arq, abrir = abrirNoSistema } = {}) {
170
+ const l = listar(arq);
171
+ const [acao, n] = args;
172
+ if (acao === 'abrir' || acao === 'pasta') {
173
+ const v = l[parseInt(n, 10) - 1];
174
+ if (!v) return { ok: false, texto: ` Não achei o vídeo ${n || ''}. Veja a lista com: primocode videos` };
175
+ const alvo = acao === 'abrir' ? v.caminho : (v.entrega || path.dirname(v.caminho));
176
+ return { ok: abrir(alvo), alvo, texto: ` Abrindo ${alvo}` };
177
+ }
178
+ return { ok: true, texto: formatarLista(l) };
179
+ }
180
+
181
+ module.exports = { exportar, srt, postTxt, creditosDe, hashtags, capa, recompor, registrar, listar, ler, main, formatarLista, DIMS };
@@ -510,7 +510,9 @@ function tituloDaFala(fala) {
510
510
 
511
511
  function conteudoDe(p, marca = {}) {
512
512
  const titulo = p.titulo || p.texto || tituloDaFala(p.fala);
513
- const itens = itensDe(p);
513
+ // O item que repete o título ("Sem troco" / "sem troco", 15/10) sai: o quadro dizia a mesma coisa duas vezes.
514
+ const igual = (a, b) => tokens(a).join(' ') === tokens(b).join(' ');
515
+ const itens = itensDe(p).filter((it) => !igual(it, titulo));
514
516
  if (p.tipo === 'numero' && numeroDe(p.valor)) return { tipo: 'numero', valor: String(p.valor), rotulo: p.rotulo || titulo, sub: p.sub || '' };
515
517
  if (itens.length >= 2) return { tipo: 'lista', titulo: p.tipo === 'cores' ? titulo : titulo, itens: itens.slice(0, 5), numerada: p.tipo === 'passos' };
516
518
  if (p.tipo === 'contraste' && (p.nao || p.sim)) return { tipo: 'contraste', nao: p.nao || '', sim: p.sim || titulo };
@@ -528,7 +530,7 @@ const textoDoConteudo = (c) => [c.titulo, c.sub, c.valor, c.rotulo, c.nao, c.sim
528
530
  const COMPOSICOES = ['gigante', 'pilha', 'divisao', 'contador', 'cascata', 'parede', 'citacao', 'marca', 'palavras', 'produto'];
529
531
  const COMPORTAMENTOS = ['carimbo', 'mascara', 'onda', 'decodifica', 'peso', 'largura', 'tracking', 'desfoque', 'flap', 'pontos', 'contorno', 'monta'];
530
532
  const SAIDAS = ['pontos', 'evapora', 'atravessa', 'desliza', 'comprime', 'explode', 'chicote', 'inunda', 'corte'];
531
- const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta'];
533
+ const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta', 'visto'];
532
534
 
533
535
  function pesosDe(c, { curto, palavras, primeira, ultima, tem3d, pergunta, exclama, numeroNoTitulo }) {
534
536
  switch (c.tipo) {
@@ -1055,13 +1057,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1055
1057
  const M = Math.round(W * (deitado ? 0.085 : 0.095));
1056
1058
  const larguraUtil = W - 2 * M;
1057
1059
  const base = Math.min(W, H) / 1080;
1058
- const tamRotulo = Math.round((deitado ? 24 : 30) * base);
1060
+ /* (15/10, as referências) O rótulo de 24–30 px (1,6% da altura no 9:16)
1061
+ não se lia no celular ("NO COADOR", "pra você, de graça"). O rótulo
1062
+ fica em ~2% da altura, e a segunda voz (o sub, a frase de apoio) em
1063
+ 3–4%, como a anotação das referências. */
1064
+ const tamRotulo = Math.round(Math.max((deitado ? 24 : 30) * base, H * (deitado ? 0.026 : 0.02)));
1059
1065
  const tem3d = !!roteiro.modelo3d;
1060
1066
  const ws = temposDasPartes(partes, tempos);
1061
1067
 
1062
1068
  // ── os limites de cada parte na linha do tempo ────────────────────────
1063
1069
  const inicio = [];
1064
- partes.forEach((_, i) => inicio.push(i === 0 ? 0 : r3(Math.max(inicio[i - 1] + 1.4, ws[i].de - 0.12))));
1070
+ // 0,9 s (eram 1,4): nas referências a cena dura 1 a 2 s; com fala rápida a parte segue a voz.
1071
+ partes.forEach((_, i) => inicio.push(i === 0 ? 0 : r3(Math.max(inicio[i - 1] + 0.9, ws[i].de - 0.12))));
1065
1072
  const ultimaFala = ws[ws.length - 1];
1066
1073
  const duracao = r1(Math.max(inicio[inicio.length - 1] + 2.0, (ultimaFala.ate || 0) + 1.15));
1067
1074
  const fimParte = partes.map((_, i) => (i < partes.length - 1 ? inicio[i + 1] : duracao));
@@ -1544,14 +1551,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1544
1551
  resto); no fim, cada gesto vira o seu par limpo. */
1545
1552
  /* O substituto é um limpo que não aparece nas 3 batidas de cada lado: a
1546
1553
  passagem e o gesto da tese não voltam antes de 3 batidas. */
1547
- const TORTOS = ['decodifica', 'flap', 'onda', 'monta'];
1548
- const GESTOS_LIMPOS = ['mascara', 'desfoque', 'tracking', 'pontos', 'contorno', 'carimbo'];
1549
- const SAIDAS_LIMPAS = ['evapora', 'desliza', 'comprime', 'pontos', 'atravessa'];
1554
+ /* (15/10, as referências) Os pontos, o evapora e o comprime também
1555
+ espalhavam as letras ("dad|o", "· p o n d e", parecia glitch). Nas
1556
+ referências a frase sai inteira: desliza, atravessa ou corta seco. */
1557
+ const TORTOS = ['decodifica', 'flap', 'onda', 'monta', 'pontos'];
1558
+ const GESTOS_LIMPOS = ['mascara', 'desfoque', 'tracking', 'contorno', 'carimbo'];
1559
+ const SAIDAS_LIMPAS = ['desliza', 'atravessa', 'corte'];
1560
+ const SAIDAS_SUJAS = ['explode', 'chicote', 'pontos', 'evapora', 'comprime'];
1550
1561
  const vizinhas = (i) => batidas.filter((_, j) => j !== i && Math.abs(j - i) <= 3);
1551
1562
  const principal = (b) => (b.linhas || []).find((l) => (l.papel === 'tese' || l.papel === 'numero') && !l.marca);
1552
1563
  const comGesto = batidas.filter(principal);
1553
1564
  batidas.forEach((b, i) => {
1554
- if (b.saida === 'explode' || b.saida === 'chicote') {
1565
+ if (SAIDAS_SUJAS.includes(b.saida)) {
1555
1566
  const perto = vizinhas(i).map((x) => x.saida);
1556
1567
  b.saida = SAIDAS_LIMPAS.find((x) => !perto.includes(x)) || 'corte';
1557
1568
  if (batidas[i + 1]) batidas[i + 1].entrada = b.saida;
@@ -1626,6 +1637,11 @@ function molduraDe(roteiro, E, { W, H, deitado, marca, secoes }) {
1626
1637
  const pesoDe = (fonte) => (VARIAVEIS[fonte] && VARIAVEIS[fonte].eixos.wght) || [400, 400];
1627
1638
 
1628
1639
  // ── as composições: de conteúdo + contexto, uma batida ─────────────────
1640
+ /** A segunda voz (sub, a frase de apoio): 3–4% da altura, como a anotação das referências. */
1641
+ function tamSegunda({ H, deitado, tamRotulo }) {
1642
+ return Math.round(Math.max(tamRotulo, H * (deitado ? 0.042 : 0.03)));
1643
+ }
1644
+
1629
1645
  function compor(comp, br, ctx) {
1630
1646
  const { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes } = ctx;
1631
1647
  const c = br.conteudo;
@@ -1739,6 +1755,8 @@ function compor(comp, br, ctx) {
1739
1755
  else {
1740
1756
  const lt = comTese || teses[teses.length - 1];
1741
1757
  b.acento.push({ forma, alvo: { linha: lt.id, palavra: comTese ? lt.palavras.findIndex((p) => p.papel === 'tese') : null } });
1758
+ // O círculo de caneta na tese ganha o ✓ (15/10, as referências), sem gastar sorteio.
1759
+ if (forma === 'anel' && comTese) b.acento.push({ forma: 'visto', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') } });
1742
1760
  }
1743
1761
  }
1744
1762
  // O ThreeCanvas só aceita pixel inteiro; ele ocupa a altura toda, para o chão não cortar.
@@ -1759,7 +1777,13 @@ function compor(comp, br, ctx) {
1759
1777
  }
1760
1778
  for (const t of fit.linhas) { const l = linha(t, 'apoio', { tamanho: fit.tamanho, eixos: { wght: 560 } }); misturarPesos(l, { maiorTese: 1.14 }); }
1761
1779
  const lt = b.linhas.find((l) => l.palavras.some((p) => p.papel === 'tese'));
1762
- if (lt) { tese(lt); b.acento.push({ forma: s.chance(0.6) ? 'sublinha' : 'anel', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') } }); }
1780
+ if (lt) {
1781
+ tese(lt);
1782
+ const alvo = { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') };
1783
+ const forma = s.chance(0.6) ? 'sublinha' : 'anel';
1784
+ b.acento.push({ forma, alvo });
1785
+ if (forma === 'anel') b.acento.push({ forma: 'visto', alvo: { ...alvo } });
1786
+ }
1763
1787
  else pontoFinal(b.linhas[b.linhas.length - 1]);
1764
1788
  break;
1765
1789
  }
@@ -1774,7 +1798,7 @@ function compor(comp, br, ctx) {
1774
1798
  const l = linha(c.valor, 'numero', { fonte, tamanho: tam, eixos });
1775
1799
  const tRot = Math.max(30 * base, Math.min(tam * 0.22, 64 * base));
1776
1800
  if (c.rotulo) linha(c.rotulo, 'apoio', { tamanho: tRot, eixos: { wght: 600 }, fonte: fontes.display });
1777
- if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.08, eixos: { wght: 400 } });
1801
+ if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho: tamSegunda(ctx), tracking: 0.02, eixos: { wght: 400 } });
1778
1802
  b.contador = { linha: l.id, ...n, dur: 0.9 };
1779
1803
  b.acento.push({ forma: c.valor.length <= 4 ? 'anel' : 'sublinha', alvo: { linha: l.id } });
1780
1804
  break;
@@ -1844,7 +1868,7 @@ function compor(comp, br, ctx) {
1844
1868
  const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.3 : 0.16) * H, { fonte, wght: 800 });
1845
1869
  b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
1846
1870
  linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, pesoDe(fonte)[1]) }, comportamento: 'contorno' }).marca = true;
1847
- if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.2, maiusc: true, eixos: { wght: 400 } });
1871
+ if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamSegunda(ctx), tracking: 0.06, eixos: { wght: 400 } });
1848
1872
  b.acento.push({ forma: 'ponto', alvo: { linha: b.linhas[0].id, antes: true } });
1849
1873
  break;
1850
1874
  }
@@ -1952,9 +1976,10 @@ function tempoDaBatida(b, palavras, ctx) {
1952
1976
  l.t = r3(Math.max(i === 0 ? b.de + 0.1 : t + REGRAS.stagger[0] * Q, falado != null ? falado : (i === 0 ? b.de + 0.12 : t + passo)));
1953
1977
  t = l.t;
1954
1978
  });
1955
- /* Quadro morto não: a primeira coisa da batida entra em até 0,35 s, mesmo
1956
- que a voz ainda não a tenha dito (o resto segue a voz). */
1957
- if (ordem[0] && ordem[0].t > b.de + 0.35) ordem[0].t = r3(b.de + 0.35);
1979
+ /* Quadro morto não: a primeira coisa da batida entra em até 0,12 s, mesmo
1980
+ que a voz ainda não a tenha dito (o resto segue a voz). Eram 0,35 s, e
1981
+ com a saída da anterior o teste de 15/10 tinha 23% de quadro vazio. */
1982
+ if (ordem[0] && ordem[0].t > b.de + 0.12) ordem[0].t = r3(b.de + 0.12);
1958
1983
  for (let i = 1; i < ordem.length; i++) if (ordem[i].t < ordem[i - 1].t + REGRAS.stagger[0] * Q) ordem[i].t = r3(ordem[i - 1].t + REGRAS.stagger[0] * Q);
1959
1984
  // Nada entra tarde demais para ser lido: de trás para frente, com o stagger mínimo.
1960
1985
  for (let i = ordem.length - 1; i >= 0; i--) {
@@ -1985,7 +2010,11 @@ function tempoDaBatida(b, palavras, ctx) {
1985
2010
  let tw = l.t;
1986
2011
  l.palavras.forEach((p, j) => {
1987
2012
  const alvo = qs[j] != null ? qs[j] : (j === 0 ? l.t : tw + 2 * Q);
1988
- p.t = r3(Math.min(Math.max(j === 0 ? l.t : tw + 2 * Q, alvo), Math.max(l.t, fim)));
2013
+ /* Quadro morto não, também na palavra (15/10): a voz pode dizer
2014
+ outra coisa antes ("Aponta a câmera, paga, e o pão…"); a primeira
2015
+ palavra da batida entra com a linha, as outras seguem a voz. */
2016
+ const abre = j === 0 && l === ordem[0];
2017
+ p.t = r3(abre ? l.t : Math.min(Math.max(j === 0 ? l.t : tw + 2 * Q, alvo), Math.max(l.t, fim)));
1989
2018
  tw = p.t;
1990
2019
  });
1991
2020
  }