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
@@ -94,7 +94,9 @@ async function narrar(lista, mp3, { voz, uv, espeak = acharEspeak(), executar =
94
94
  roteiro pula as outras. */
95
95
  function ordem(voz, env = process.env, { soEdge = false } = {}) {
96
96
  if (soEdge) return ['edge'];
97
- return querKokoro(voz, env) ? ['kokoro', 'edge'] : ['gemini', 'edge', 'kokoro'];
97
+ if (querKokoro(voz, env)) return ['kokoro', 'edge'];
98
+ // O VoiceStudio local vem primeiro (15/10); fechado, a cadeia segue para o Gemini na hora.
99
+ return env.PRIMOCODE_SEM_VOICESTUDIO ? ['gemini', 'edge', 'kokoro'] : ['voicestudio', 'gemini', 'edge', 'kokoro'];
98
100
  }
99
101
 
100
102
  /* A voz do Gemini no Edge: o narrar.py só conhece antonio, francisca e
@@ -0,0 +1,393 @@
1
+ /**
2
+ * maquina-diretor.js — o diretor do estudio_motion: do assunto a UMA direção própria, nunca um molde.
3
+ *
4
+ * O vocabulário (primitivas de cena, transições, estilos de câmera, sistemas tipográficos) vem das 68
5
+ * receitas do Clube e dos scripts AE dos 5 projetos. O modelo escolhe pelo JSON (tipo da cena, transições,
6
+ * câmera, tipografia, metáfora). O diretor COMPLETA o que faltar, com uma semente por vídeo, e confere a
7
+ * ASSINATURA do vídeo contra os últimos (~/.primocode/studio/assinaturas.json): se parece demais, sorteia de novo.
8
+ *
9
+ * dirigir(args, {historico, semente}) → { metafora, semente, cenas, trocas, camera, tipografia, ritmo, paleta, assinatura }
10
+ * similaridade(a, b) → 0..1
11
+ * lerHistorico() / registrar(assinatura)
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ // ── o vocabulário ────────────────────────────────────────────────────────
21
+
22
+ /** As primitivas: família (para não pôr duas iguais em fila), papéis em que servem e o que exigem. */
23
+ const PRIMITIVAS = {
24
+ gancho: { fam: 'texto', entra: 1.4, papeis: ['gancho', 'cta', 'virada'], dur: 2.4 },
25
+ cinetico: { fam: 'texto', entra: 2.2, papeis: ['gancho', 'virada', 'cta'], dur: 2.6 },
26
+ particulas: { fam: 'texto', entra: 1.3, papeis: ['gancho', 'virada'], dur: 2.6 },
27
+ odometro: { fam: 'texto', entra: 2.4, papeis: ['gancho', 'virada'], dur: 3.0, itens: 3 },
28
+ citacao: { fam: 'texto', entra: 2.2, papeis: ['prova', 'virada'], dur: 2.8 },
29
+ cta: { fam: 'texto', entra: 1.1, papeis: ['cta'], dur: 2.8 },
30
+ carimbo: { fam: 'objeto', entra: 2.3, papeis: ['virada', 'prova', 'cta'], dur: 2.6 },
31
+ revela: { fam: 'objeto', entra: 1.5, papeis: ['gancho', 'virada', 'processo'], dur: 2.8, elemento: true },
32
+ heroi: { fam: 'objeto', entra: 2.5, papeis: ['gancho', 'processo', 'virada'], dur: 3.0, elemento: true },
33
+ explosao: { fam: 'objeto', entra: 1.8, papeis: ['processo', 'virada'], dur: 3.0, elemento: true, itens: 3 },
34
+ icones: { fam: 'objeto', entra: 1.7, papeis: ['processo'], dur: 2.8, itens: 3 },
35
+ leque: { fam: 'objeto', entra: 1.4, papeis: ['processo', 'prova'], dur: 3.0, elemento: true, itens: 3 },
36
+ parede_logos: { fam: 'objeto', entra: 1.3, papeis: ['prova', 'virada'], dur: 2.6, elemento: true },
37
+ celular: { fam: 'device', entra: 2.3, papeis: ['processo', 'prova'], dur: 3.4 },
38
+ mergulho: { fam: 'device', entra: 1.2, papeis: ['virada', 'processo'], dur: 3.2, elemento: true },
39
+ notificacoes: { fam: 'device', entra: 2.2, papeis: ['problema', 'prova', 'processo'], dur: 3.2, itens: 3 },
40
+ chat: { fam: 'device', entra: 1.8, papeis: ['problema', 'processo', 'prova'], dur: 3.4, itens: 3 },
41
+ interface: { fam: 'tela', entra: 1.7, papeis: ['processo', 'prova', 'gancho'], dur: 3.2, itens: 3 },
42
+ cursor: { fam: 'tela', entra: 3.4, papeis: ['processo'], dur: 3.6 },
43
+ prompt: { fam: 'tela', entra: 3.4, papeis: ['processo', 'gancho'], dur: 4.0, itens: 3 },
44
+ rolagem: { fam: 'tela', entra: 1.8, papeis: ['problema', 'prova'], dur: 3.0, elemento: true, itens: 3 },
45
+ numero: { fam: 'dados', entra: 2.0, papeis: ['prova', 'virada'], dur: 2.6 },
46
+ barras: { fam: 'dados', entra: 2.4, papeis: ['prova', 'problema'], dur: 3.0, itens: 4 },
47
+ linha: { fam: 'dados', entra: 2.5, papeis: ['prova', 'problema'], dur: 3.0 },
48
+ medidor: { fam: 'dados', entra: 2.5, papeis: ['prova', 'virada'], dur: 3.0 },
49
+ calendario: { fam: 'dados', entra: 1.7, papeis: ['problema', 'prova', 'processo'], dur: 3.0 },
50
+ funil: { fam: 'dados', entra: 2.4, papeis: ['processo', 'prova', 'problema'], dur: 3.4, itens: 3 },
51
+ relogio: { fam: 'dados', entra: 2.2, papeis: ['problema', 'gancho', 'virada'], dur: 3.0 },
52
+ lista: { fam: 'lista', entra: 2.1, papeis: ['processo', 'prova'], dur: 2.8, itens: 3 },
53
+ cards_pulam: { fam: 'lista', entra: 2.2, papeis: ['processo', 'problema', 'prova'], dur: 3.0, elemento: true, itens: 3 },
54
+ laco: { fam: 'fluxo', entra: 1.7, papeis: ['processo'], dur: 3.4, elemento: true, itens: 3 },
55
+ linha_tempo: { fam: 'fluxo', entra: 1.9, papeis: ['processo', 'prova'], dur: 3.2, itens: 3 },
56
+ camera_nulls: { fam: 'fluxo', entra: 3.0, papeis: ['processo'], dur: 3.6, elemento: true, itens: 3 },
57
+ mapa: { fam: 'lugar', entra: 2.7, papeis: ['problema', 'prova', 'processo'], dur: 3.0, itens: 3 },
58
+ split: { fam: 'comparacao', entra: 2.6, papeis: ['problema', 'virada'], dur: 3.2, elemento: true },
59
+ ondas: { fam: 'som', entra: 1.0, papeis: ['gancho', 'processo', 'virada'], dur: 2.8, elemento: true },
60
+ estrelas: { fam: 'social', entra: 2.7, papeis: ['prova'], dur: 3.2, elemento: true },
61
+ };
62
+ const TIPOS = Object.keys(PRIMITIVAS);
63
+
64
+ const TRANSICOES = ['encolhe', 'empurra', 'zoom', 'persiana', 'chicote', 'desfoque', 'cortina', 'giro', 'tremor', 'abre', 'dissolve', 'corte']; // sem íris/pontos: o círculo claro crescendo parecia flash
65
+ const CAMERAS = ['deriva', 'pulso', 'trilho', 'orbita', 'push', 'chicote', 'cinema']; // sem câmera na mão: tremedeira fora
66
+
67
+ /** Sistemas tipográficos: fontes + como a palavra entra + como a palavra-chave é realçada. */
68
+ const TIPOGRAFIAS = {
69
+ editorial: { fontes: { titulo: 'Poppins', texto: 'Plus Jakarta Sans', destaque: 'Playfair Display' }, entrada: 'palavra', realce: 'italico', passo: 0.1 },
70
+ poster: { fontes: { titulo: 'Anton', texto: 'Barlow Condensed', destaque: 'Anton' }, entrada: 'mascara', realce: 'caixa', caixa: 'alta', passo: 0.12, entreLinhas: 1.02 },
71
+ tecnica: { fontes: { titulo: 'Space Grotesk', texto: 'JetBrains Mono', destaque: 'Space Grotesk' }, entrada: 'letra', realce: 'sublinha', passo: 0.16, tracking: -10 },
72
+ manuscrita: { fontes: { titulo: 'Poppins', texto: 'Nunito', destaque: 'Caveat' }, entrada: 'sobe', realce: 'cor', passo: 0.1 },
73
+ pesada: { fontes: { titulo: 'Archivo Black', texto: 'Inter', destaque: 'Archivo Black' }, entrada: 'palavra', realce: 'caixa', passo: 0.09 },
74
+ serifa_fina: { fontes: { titulo: 'Instrument Serif', texto: 'Manrope', destaque: 'Instrument Serif' }, entrada: 'mascara', realce: 'italico', passo: 0.14, entreLinhas: 1.05 },
75
+ arredondada: { fontes: { titulo: 'Fredoka', texto: 'Nunito', destaque: 'Fredoka' }, entrada: 'sobe', realce: 'cor', passo: 0.1 },
76
+ geometrica: { fontes: { titulo: 'Outfit', texto: 'DM Sans', destaque: 'Outfit' }, entrada: 'sobe', realce: 'sublinha', passo: 0.08 },
77
+ };
78
+
79
+ // ── as metáforas: o que o assunto É, e o que ele pede ───────────────────
80
+
81
+ const PALETAS = {
82
+ forno: { fundo: '#FFF1DC', tinta: '#3B1F0E', destaque: '#E8590C' },
83
+ rose: { fundo: '#FBEEE8', tinta: '#3A2230', destaque: '#D9486B' },
84
+ grafite: { fundo: '#0F1412', tinta: '#F2F5EF', destaque: '#C8F03C' },
85
+ floresta: { fundo: '#14301F', tinta: '#EDF5E4', destaque: '#F2B544' },
86
+ petroleo: { fundo: '#0E3B43', tinta: '#FFF4E6', destaque: '#FF8A5B' },
87
+ coral: { fundo: '#FFF6F0', tinta: '#1A1A1A', destaque: '#FF4B2B' },
88
+ tijolo: { fundo: '#F4E4D2', tinta: '#2B1710', destaque: '#B5472C' },
89
+ menta: { fundo: '#E6F4EC', tinta: '#12332A', destaque: '#0F9D6B' },
90
+ ouro: { fundo: '#111111', tinta: '#F6EBD0', destaque: '#E3B23C' },
91
+ limao: { fundo: '#FFF9C9', tinta: '#1B1B12', destaque: '#E5481E' },
92
+ mar: { fundo: '#0B2B33', tinta: '#EAF6F3', destaque: '#35D0BA' },
93
+ };
94
+
95
+ const METAFORAS = {
96
+ padaria: {
97
+ re: /p[aã]o|padaria|forno|confeit|bolo|caf[eé]|cozinha|restaurante|doce|pizza|lanche|hamb|sorvete|a[cç]a[ií]/i,
98
+ nome: 'forno que acorda a rua',
99
+ elementos: ['bread', 'croissant', 'alarm clock', 'hot beverage', 'cooking'],
100
+ animado: ['fire'], termos2d: [],
101
+ paletas: ['forno', 'tijolo', 'limao'],
102
+ nao: ['interface', 'prompt', 'cursor', 'rolagem', 'funil', 'medidor', 'parede_logos', 'camera_nulls', 'mergulho', 'laco', 'linha', 'ondas'],
103
+ prefere: { relogio: 3, heroi: 3, mapa: 2, estrelas: 2, cards_pulam: 2, explosao: 2, revela: 2, carimbo: 2, calendario: 1.5, leque: 2, particulas: 2, interface: 0.4, prompt: 0.2, cursor: 0.3, funil: 0.5, camera_nulls: 0.7 },
104
+ cameras: ['push', 'cinema', 'deriva'], tipografias: ['editorial', 'manuscrita', 'serifa_fina', 'arredondada'],
105
+ trocas: ['zoom', 'desfoque', 'encolhe', 'dissolve', 'cortina'],
106
+ musica: 'warm acoustic happy', itens: ['Pão francês', 'Croissant', 'Café coado', 'Bolo caseiro'],
107
+ },
108
+ clinica: {
109
+ re: /cl[ií]nica|est[eé]tica|beleza|sal[aã]o|cabelo|unha|spa|pele|sa[uú]de|m[eé]dic|dent|cliente|agenda|sumid|recuper/i,
110
+ nome: 'agenda que volta a encher',
111
+ elementos: ['calendar', 'speech balloon', 'sparkles', 'lotion bottle', 'heart', 'woman getting massage'],
112
+ animado: ['sparkles'], termos2d: [],
113
+ paletas: ['rose', 'menta', 'coral'],
114
+ nao: ['prompt', 'cursor', 'parede_logos', 'camera_nulls', 'ondas', 'mergulho'],
115
+ prefere: { calendario: 3, notificacoes: 3, chat: 3, funil: 2.5, split: 2.5, mapa: 1.5, estrelas: 2, barras: 1.5, linha: 1.5, cards_pulam: 1.5, relogio: 1, interface: 0.6, prompt: 0.3, cursor: 0.4, heroi: 1 },
116
+ cameras: ['trilho', 'orbita', 'deriva', 'pulso'], tipografias: ['serifa_fina', 'geometrica', 'editorial', 'arredondada'],
117
+ trocas: ['persiana', 'abre', 'desfoque', 'dissolve', 'cortina'],
118
+ musica: 'soft piano calm', itens: ['Lembrete enviado', 'Cliente respondeu', 'Horário confirmado', 'Retorno agendado'],
119
+ },
120
+ financas: {
121
+ re: /finan|dinheiro|invest|banco|cr[eé]dito|gasto|or[cç]amento|cart[aã]o|poupan|moeda|economi|controle/i,
122
+ nome: 'extrato que vira jogo ganho',
123
+ elementos: ['money bag', 'coin', 'credit card', 'chart increasing', 'bank', 'piggy bank'],
124
+ animado: ['money-with-wings'], termos2d: [],
125
+ paletas: ['floresta', 'ouro', 'mar'],
126
+ nao: ['mapa', 'relogio', 'ondas', 'leque', 'revela', 'particulas', 'prompt'],
127
+ prefere: { barras: 3, linha: 3, medidor: 3, numero: 2.5, funil: 2, calendario: 2, odometro: 2.5, cards_pulam: 2, interface: 2, notificacoes: 2, relogio: 0.6, mapa: 0.5, estrelas: 1, heroi: 1.5, carimbo: 1.5 },
128
+ cameras: ['pulso', 'trilho', 'push', 'orbita'], tipografias: ['tecnica', 'geometrica', 'pesada', 'poster'],
129
+ trocas: ['tremor', 'corte', 'chicote', 'empurra', 'zoom'],
130
+ musica: 'lofi upbeat chill', itens: ['Salário', 'Contas', 'Reserva', 'Sobra'],
131
+ },
132
+ software: {
133
+ re: /app\b|saas|software|c[oó]digo|program|tecnolog|\bia\b|intelig|primocode|sistema|plataforma|dados|site|digital|startup|sem saber/i,
134
+ nome: 'tela que se monta sozinha',
135
+ elementos: ['rocket', 'laptop', 'robot', 'light bulb', 'gear', 'mobile phone'],
136
+ animado: ['thinking-face'], termos2d: [],
137
+ paletas: ['grafite', 'mar', 'ouro'],
138
+ nao: ['mapa', 'relogio', 'leque', 'estrelas', 'calendario', 'carimbo'],
139
+ prefere: { interface: 3, cursor: 3, prompt: 3, mergulho: 2.5, camera_nulls: 2, rolagem: 2, parede_logos: 2, cinetico: 2, laco: 2, linha_tempo: 1.5, chat: 1.5, ondas: 1.2, relogio: 0.4, mapa: 0.3, estrelas: 0.8, carimbo: 1.2 },
140
+ cameras: ['push', 'pulso', 'chicote'], tipografias: ['tecnica', 'geometrica', 'pesada', 'editorial'],
141
+ trocas: ['corte', 'chicote', 'zoom', 'persiana', 'empurra', 'giro'],
142
+ musica: 'electronic technology minimal', itens: ['Você descreve', 'A IA monta', 'Você publica'],
143
+ },
144
+ geral: {
145
+ re: /./,
146
+ nome: 'a ideia em cena',
147
+ elementos: ['star', 'light bulb', 'sparkles', 'rocket'],
148
+ animado: [], termos2d: [],
149
+ paletas: ['petroleo', 'coral', 'grafite', 'tijolo', 'menta'],
150
+ nao: [], prefere: {},
151
+ cameras: CAMERAS, tipografias: Object.keys(TIPOGRAFIAS),
152
+ trocas: TRANSICOES,
153
+ musica: 'upbeat instrumental', itens: ['Passo um', 'Passo dois', 'Passo três'],
154
+ },
155
+ };
156
+ const achar = (assunto) => {
157
+ const a = String(assunto || '');
158
+ for (const k of ['padaria', 'clinica', 'financas', 'software']) if (METAFORAS[k].re.test(a)) return { chave: k, ...METAFORAS[k] };
159
+ return { chave: 'geral', ...METAFORAS.geral };
160
+ };
161
+
162
+ // ── semente e sorteio ────────────────────────────────────────────────────
163
+
164
+ const hashTexto = (s) => { let h = 7; for (const c of String(s)) h = (h * 31 + c.charCodeAt(0)) >>> 0; return h; };
165
+ function mulberry(a) {
166
+ return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
167
+ }
168
+ /** Sorteio ponderado: itens [[valor, peso]]. */
169
+ function sortear(rnd, itens) {
170
+ const total = itens.reduce((s, [, p]) => s + Math.max(0, p), 0);
171
+ if (total <= 0) return itens[0][0];
172
+ let x = rnd() * total;
173
+ for (const [v, p] of itens) { x -= Math.max(0, p); if (x <= 0) return v; }
174
+ return itens[itens.length - 1][0];
175
+ }
176
+
177
+ // ── o histórico de assinaturas ───────────────────────────────────────────
178
+
179
+ const arquivoHistorico = () => process.env.PRIMOCODE_ASSINATURAS || path.join(os.homedir(), '.primocode', 'studio', 'assinaturas.json');
180
+ function lerHistorico(arq = arquivoHistorico()) {
181
+ try { const j = JSON.parse(fs.readFileSync(arq, 'utf8')); return Array.isArray(j) ? j : []; } catch { return []; }
182
+ }
183
+ function registrar(assinatura, arq = arquivoHistorico()) {
184
+ const h = lerHistorico(arq);
185
+ h.push({ ...assinatura, quando: new Date().toISOString() });
186
+ try { fs.mkdirSync(path.dirname(arq), { recursive: true }); fs.writeFileSync(arq, JSON.stringify(h.slice(-40), null, 2)); } catch { /* sem disco: segue */ }
187
+ return h.length;
188
+ }
189
+ const jaccard = (a, b) => { const A = new Set(a), B = new Set(b); if (!A.size && !B.size) return 1; let i = 0; for (const x of A) if (B.has(x)) i++; return i / (A.size + B.size - i); };
190
+ /** 0 (nada a ver) a 1 (o mesmo vídeo). Pesos: cenas, ordem, trocas, câmera, tipografia, metáfora, ritmo. */
191
+ function similaridade(a, b) {
192
+ if (!a || !b) return 0;
193
+ const ordem = (() => { const n = Math.min(a.primitivas.length, b.primitivas.length); if (!n) return 0; let k = 0; for (let i = 0; i < n; i++) if (a.primitivas[i] === b.primitivas[i]) k++; return k / n; })();
194
+ return 0.3 * jaccard(a.primitivas, b.primitivas) + 0.15 * ordem + 0.15 * jaccard(a.trocas, b.trocas) + 0.12 * (a.camera === b.camera ? 1 : 0)
195
+ + 0.12 * (a.tipografia === b.tipografia ? 1 : 0) + 0.08 * (a.metafora === b.metafora ? 1 : 0) + 0.04 * (a.ritmo === b.ritmo ? 1 : 0) + 0.04 * (a.paleta === b.paleta ? 1 : 0);
196
+ }
197
+ const LIMITE_PARECIDO = 0.42;
198
+
199
+ // ── o roteiro de papéis ──────────────────────────────────────────────────
200
+
201
+ const MOLDES = [
202
+ ['gancho', 'problema', 'processo', 'prova', 'cta'],
203
+ ['gancho', 'processo', 'prova', 'virada', 'cta'],
204
+ ['gancho', 'prova', 'problema', 'processo', 'cta'],
205
+ ['problema', 'virada', 'processo', 'prova', 'cta'],
206
+ ['gancho', 'problema', 'virada', 'processo', 'prova', 'cta'],
207
+ ];
208
+ function papeisPara(duracao, rnd, evita) {
209
+ let m = MOLDES.filter((x) => x.join() !== evita);
210
+ if (duracao < 11) m = m.filter((x) => x.length <= 5);
211
+ const base = m[Math.floor(rnd() * m.length)].slice();
212
+ // vídeos mais longos ganham mais um passo e mais uma prova
213
+ if (duracao >= 18) base.splice(base.indexOf('cta'), 0, 'processo');
214
+ if (duracao >= 26) base.splice(base.indexOf('cta'), 0, 'prova');
215
+ return base;
216
+ }
217
+
218
+ // ── completar os campos de cada cena ─────────────────────────────────────
219
+
220
+ const limpa = (s, max) => String(s == null ? '' : s).replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
221
+ const PARADAS = new Set('de da do das dos na no nas nos em um uma uns umas e ou que com por para pra ao aos às as os o a se sem'.split(' '));
222
+ function marcarChave(titulo) {
223
+ if (!titulo || titulo.includes('*')) return titulo;
224
+ const ps = titulo.split(' ');
225
+ let melhor = -1, tam = 0;
226
+ ps.forEach((p, i) => { const l = p.replace(/[^\p{L}\p{N}]/gu, ''); if (l.length >= 4 && !PARADAS.has(l.toLowerCase()) && l.length > tam) { melhor = i; tam = l.length; } });
227
+ if (melhor < 0) return titulo;
228
+ ps[melhor] = '*' + ps[melhor] + '*';
229
+ return ps.join(' ');
230
+ }
231
+ const primeirasPalavras = (s, n) => String(s || '').replace(/\*/g, '').split(/\s+/).filter(Boolean).slice(0, n).join(' ');
232
+ const itensDe = (v, n, m) => (Array.isArray(v) ? v : []).map((x) => limpa(typeof x === 'object' && x ? (x.titulo || x.texto || x.nome || '') : x, m)).filter(Boolean).slice(0, n);
233
+
234
+ /** Põe em `c` o que a primitiva `tipo` precisa e o modelo não mandou. `ctx`: {assunto, meta, proxima}. */
235
+ function completar(c, tipo, ctx) {
236
+ const spec = PRIMITIVAS[tipo];
237
+ const out = { ...c, tipo };
238
+ const base = limpa(c.titulo || c.texto || primeirasPalavras(c.fala, 6) || ctx.assunto, 80);
239
+ out.titulo = marcarChave(base);
240
+ out.sub = limpa(c.sub, 70) || undefined;
241
+ let it = itensDe(c.itens, 6, 28);
242
+ if (spec.itens && it.length < spec.itens) it = (it.length ? it.concat(ctx.meta.itens) : ctx.meta.itens).slice(0, Math.max(spec.itens, it.length));
243
+ out.itens = it;
244
+ out.valor = limpa(c.valor, 12) || (tipo === 'relogio' ? '7:00' : tipo === 'medidor' ? '82%' : tipo === 'estrelas' ? '4,9' : '100');
245
+ out.rotulo = limpa(c.rotulo, 60) || undefined;
246
+ out.antes = limpa(c.antes, 40) || 'Do jeito difícil';
247
+ out.depois = marcarChave(limpa(c.depois, 40) || 'Do jeito simples');
248
+ out.texto = limpa(c.texto || c.titulo, 140) || undefined;
249
+ out.autor = limpa(c.autor, 40) || undefined;
250
+ out.botao = limpa(c.botao, 26) || 'Começar agora';
251
+ out.toques = itensDe(c.toques, 4, 24);
252
+ if (ctx.proxima) { out.prox = limpa(ctx.proxima.titulo || ctx.proxima.texto, 40).replace(/\*/g, ''); }
253
+ out.fala = c.fala ? String(c.fala) : undefined;
254
+ out.dur = Math.max(1.4, Math.min(tipo === 'celular' || tipo === 'prompt' ? 5 : 4.2, Number(c.dur || c.duracao) || spec.dur));
255
+ return out;
256
+ }
257
+
258
+ // ── a direção ────────────────────────────────────────────────────────────
259
+
260
+ const ALIAS = { antes: 'split', antesdepois: 'split', antes_depois: 'split', icone: 'icones', numero: 'numero', grafico: 'barras', grafico_barras: 'barras', grafico_linha: 'linha', cartas: 'leque', mundo: 'camera_nulls', tela: 'interface', cinetica: 'cinetico', logos: 'parede_logos', brasas: 'particulas', timeline: 'linha_tempo', notificacao: 'notificacoes', funil_vendas: 'funil', ponteiro: 'cursor', digitacao: 'prompt', scroll: 'rolagem', coracao: 'estrelas', prova_social: 'estrelas' };
261
+ const normTipo = (t) => { const s = String(t || '').toLowerCase().replace(/[\s-]+/g, '_'); return ALIAS[s] || s; };
262
+
263
+ /**
264
+ * Escolhe a primitiva de um papel: afinidade do assunto × novidade (o que os últimos vídeos usaram pesa menos) ×
265
+ * variedade dentro do vídeo (nada repetido, nada da mesma família em fila).
266
+ */
267
+ function escolherPrimitiva(papel, ctx, rnd) {
268
+ const usados = ctx.usados, ultimaFam = ctx.ultimaFam;
269
+ const recentes = new Map();
270
+ ctx.historico.slice(-5).forEach((h, i, arr) => (h.primitivas || []).forEach((p) => recentes.set(p, (recentes.get(p) || 0) + 1 + i / arr.length)));
271
+ const nao = new Set(ctx.meta.nao || []);
272
+ const cand = TIPOS.filter((t) => PRIMITIVAS[t].papeis.includes(papel) && !usados.has(t) && !nao.has(t) && (!PRIMITIVAS[t].elemento || ctx.temElemento));
273
+ const lista = (cand.length ? cand : TIPOS.filter((t) => !usados.has(t) && PRIMITIVAS[t].papeis.includes(papel))).map((t) => {
274
+ let peso = 1;
275
+ peso *= ctx.meta.prefere[t] != null ? ctx.meta.prefere[t] : 1;
276
+ peso *= Math.pow(0.55, recentes.get(t) || 0);
277
+ if (PRIMITIVAS[t].fam === ultimaFam) peso *= 0.25;
278
+ if (PRIMITIVAS[t].papeis[0] === papel) peso *= 1.3;
279
+ return [t, peso];
280
+ });
281
+ return lista.length ? sortear(rnd, lista) : 'gancho';
282
+ }
283
+
284
+ /**
285
+ * dirigir: completa tudo o que o pedido não disse. `args`: assunto, cenas (cada uma com papel/tipo/campos), transicoes|trocas,
286
+ * camera, tipografia, metafora, duracao, semente, marca. Devolve a direção e a assinatura (sem gravar: quem grava é o chamador).
287
+ */
288
+ function dirigir(args = {}, { historico = lerHistorico(), semente = null, temElemento = true } = {}) {
289
+ const assunto = limpa(args.assunto, 160) || 'Seu projeto';
290
+ const meta = achar(assunto + ' ' + (args.metafora || ''));
291
+ const duracao = Math.max(6, Math.min(60, Number(args.duracao) || 15));
292
+ const hist = historico.filter((h) => h && h.primitivas);
293
+ let melhor = null;
294
+ const base = semente != null ? Number(semente) : (args.semente != null ? Number(args.semente) : (Date.now() % 100000) + hashTexto(assunto) % 1000);
295
+ const fixa = semente != null || args.semente != null;
296
+ for (let tent = 0; tent < 14; tent++) {
297
+ const sem = base + tent * 7919;
298
+ const rnd = mulberry(sem);
299
+ const d = montarDirecao({ args, assunto, meta, duracao, hist, rnd, temElemento });
300
+ d.semente = sem;
301
+ d.assinatura.semente = sem;
302
+ d.assinatura.parecido = hist.length ? Math.max(...hist.slice(-6).map((h) => similaridade(d.assinatura, h))) : 0;
303
+ if (!melhor || d.assinatura.parecido < melhor.assinatura.parecido) melhor = d;
304
+ d.tentativas = tent + 1;
305
+ if (d.assinatura.parecido <= LIMITE_PARECIDO || (fixa && tent === 0 && args.cenas && args.cenas.length && args.cenas.every((c) => c && c.tipo))) { melhor = d; break; }
306
+ }
307
+ melhor.assinatura.repetiu = melhor.assinatura.parecido > LIMITE_PARECIDO;
308
+ return melhor;
309
+ }
310
+
311
+ function montarDirecao({ args, assunto, meta, duracao, hist, rnd, temElemento }) {
312
+ const ultimo = hist[hist.length - 1];
313
+ const ctx = { assunto, meta, hist, historico: hist, usados: new Set(), ultimaFam: null, temElemento };
314
+ // ritmo: duração média por cena
315
+ const ritmo = args.ritmo || sortear(rnd, [['rapido', 1], ['medio', 1.4], ['lento', 0.8]].map(([r, p]) => [r, p * (ultimo && ultimo.ritmo === r ? 0.4 : 1)]));
316
+ const media = ritmo === 'rapido' ? 2.5 : ritmo === 'lento' ? 3.5 : 3.0; // cena de 2,5 a 3,5 s: o corte seco da câmera dá o pulso a cada ~1 s
317
+
318
+ // as cenas pedidas (ou os papéis sorteados)
319
+ let pedidas = Array.isArray(args.cenas) && args.cenas.length ? args.cenas.slice(0, 24).map((c) => (c && typeof c === 'object' ? c : { titulo: String(c || '') })) : null;
320
+ if (!pedidas) {
321
+ const n = Math.max(4, Math.round(duracao / media));
322
+ let papeis = papeisPara(duracao, rnd, ultimo && ultimo.molde);
323
+ while (papeis.length > n + 1 && papeis.includes('processo')) papeis.splice(papeis.lastIndexOf('processo'), 1);
324
+ pedidas = papeis.map((papel) => ({ papel }));
325
+ }
326
+ const papeisUsados = [];
327
+ const cenas = [];
328
+ pedidas.forEach((c, i) => {
329
+ let tipo = normTipo(c.tipo);
330
+ const papel = c.papel || (i === 0 ? 'gancho' : i === pedidas.length - 1 ? 'cta' : ['problema', 'processo', 'prova', 'virada'][i % 4]);
331
+ papeisUsados.push(papel);
332
+ if (!PRIMITIVAS[tipo]) tipo = papel === 'cta' ? 'cta' : escolherPrimitiva(papel, ctx, rnd); // todo vídeo termina num CTA de verdade (botão grande)
333
+ ctx.usados.add(tipo);
334
+ ctx.ultimaFam = PRIMITIVAS[tipo].fam;
335
+ cenas.push({ c, tipo, papel });
336
+ });
337
+ // o assunto aparece em OBJETOS: pelo menos 2 cenas com primitiva que usa o elemento real (as que o modelo fixou ficam como estão)
338
+ if (temElemento) {
339
+ const nao = new Set(meta.nao || []);
340
+ const conta = () => cenas.filter((x) => PRIMITIVAS[x.tipo].elemento).length;
341
+ for (let i = 0; i < cenas.length && conta() < 2; i++) {
342
+ const x = cenas[(i * 2 + 1) % cenas.length];
343
+ if (PRIMITIVAS[x.tipo].elemento || (x.c && PRIMITIVAS[normTipo(x.c.tipo)])) continue;
344
+ const cand = TIPOS.filter((t) => PRIMITIVAS[t].elemento && PRIMITIVAS[t].papeis.includes(x.papel) && !nao.has(t) && !cenas.some((y) => y.tipo === t));
345
+ if (cand.length) { ctx.usados.delete(x.tipo); x.tipo = sortear(rnd, cand.map((t) => [t, meta.prefere[t] != null ? meta.prefere[t] : 1])); ctx.usados.add(x.tipo); }
346
+ }
347
+ }
348
+ const out = cenas.map(({ c, tipo, papel }, i) => {
349
+ const prox = cenas[i + 1];
350
+ const r = completar(c, tipo, { assunto, meta, proxima: prox && prox.c });
351
+ r.papel = papel;
352
+ return r;
353
+ });
354
+
355
+ // transições: pesadas pelo assunto, nunca a mesma em fila, nunca igual à dos últimos vídeos demais
356
+ const recT = new Map(); hist.slice(-4).forEach((h) => (h.trocas || []).forEach((t) => recT.set(t, (recT.get(t) || 0) + 1)));
357
+ const pedidasT = args.transicoes || args.trocas;
358
+ const trocas = [];
359
+ for (let i = 1; i < out.length; i++) {
360
+ let t = Array.isArray(pedidasT) && TRANSICOES.includes(pedidasT[i - 1]) ? pedidasT[i - 1] : null;
361
+ if (!t) {
362
+ if (out[i - 1].tipo === 'mergulho') t = rnd() < 0.5 ? 'corte' : 'dissolve';
363
+ else t = sortear(rnd, TRANSICOES.map((x) => [x, (['dissolve', 'desfoque', 'encolhe', 'mascara', 'abre', 'cortina'].includes(x) ? 0.4 : 1.4) * (meta.trocas.includes(x) ? 3 : 1) * Math.pow(0.6, recT.get(x) || 0) * (x === trocas[trocas.length - 1] ? 0 : 1) * (trocas.slice(-3).includes(x) ? 0.3 : 1)]));
364
+ }
365
+ trocas.push(t);
366
+ }
367
+
368
+ // câmera: um estilo principal + um secundário em algumas cenas
369
+ const recC = new Map(); hist.slice(-3).forEach((h) => recC.set(h.camera, (recC.get(h.camera) || 0) + 1));
370
+ const camera = CAMERAS.includes(args.camera) ? args.camera : sortear(rnd, CAMERAS.map((x) => [x, (meta.cameras.includes(x) ? 3 : 1) * Math.pow(0.45, recC.get(x) || 0)]));
371
+ const camera2 = sortear(rnd, CAMERAS.filter((x) => x !== camera).map((x) => [x, meta.cameras.includes(x) ? 2 : 1]));
372
+ out.forEach((c, i) => { c.camera = c.camera && CAMERAS.includes(c.camera) ? c.camera : (i % 3 === 2 ? camera2 : camera); });
373
+
374
+ // tipografia
375
+ const recTi = new Map(); hist.slice(-3).forEach((h) => recTi.set(h.tipografia, (recTi.get(h.tipografia) || 0) + 1));
376
+ const tipografia = TIPOGRAFIAS[args.tipografia] ? args.tipografia : sortear(rnd, Object.keys(TIPOGRAFIAS).map((x) => [x, (meta.tipografias.includes(x) ? 3 : 0.8) * Math.pow(0.4, recTi.get(x) || 0)]));
377
+
378
+ // paleta
379
+ const recP = new Map(); hist.slice(-3).forEach((h) => recP.set(h.paleta, (recP.get(h.paleta) || 0) + 1));
380
+ const paleta = PALETAS[args.paleta] ? args.paleta : sortear(rnd, meta.paletas.map((x) => [x, Math.pow(0.35, recP.get(x) || 0)]));
381
+ const ordemPal = sortear(rnd, [[0, 1], [1, 1], [2, 1]]);
382
+
383
+ const assinatura = {
384
+ metafora: meta.nome, chave: meta.chave, assunto, primitivas: out.map((c) => c.tipo), trocas, camera, camera2, tipografia, ritmo, paleta,
385
+ molde: papeisUsados.join(','), cenas: out.length, tipos_unicos: new Set(out.map((c) => c.tipo)).size,
386
+ };
387
+ return { metafora: meta, cenas: out, trocas, camera, tipografia, ritmo, paleta, ordemPal, assinatura };
388
+ }
389
+
390
+ /** Quantos itens do vocabulário (para o teste e para o relatório). */
391
+ const VOCABULARIO = () => ({ primitivas: TIPOS.length, transicoes: TRANSICOES.length, cameras: CAMERAS.length, tipografias: Object.keys(TIPOGRAFIAS).length });
392
+
393
+ module.exports = { PRIMITIVAS, TIPOS, TRANSICOES, CAMERAS, TIPOGRAFIAS, METAFORAS, PALETAS, achar, dirigir, similaridade, lerHistorico, registrar, completar, marcarChave, normTipo, escolherPrimitiva, VOCABULARIO, LIMITE_PARECIDO, hashTexto, mulberry, arquivoHistorico };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * maquina-elementos.js — os ELEMENTOS REAIS do assunto: o pão do vídeo da padaria, o relógio, o calendário.
3
+ *
4
+ * Regra da casa: todo elemento tem que ter a ver com o assunto, e a escolha é do modelo (cardápio) com o diretor
5
+ * completando. Daqui saem arquivos que o EffectCraft importa (PNG, SVG ou sequência de PNG):
6
+ *
7
+ * · ícone 3D Fluent (MIT), pelo índice do pacote (colecao.js), reamostrado 3× para não ficar mole no quadro;
8
+ * · animação Noto (CC BY 4.0): o GIF vira sequência de PNG (o EffectCraft não importa Lottie: medido, "data files
9
+ * can't be layers");
10
+ * · logos (Wikimedia, SVG) e print do site (print-site.js), quando o pedido traz `logos` ou `site`.
11
+ *
12
+ * Cada arquivo guarda a licença ao lado (.json). Sem rede, usa o que já está na pasta do projeto.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { spawnSync } = require('child_process');
20
+
21
+ const slug = (s) => String(s).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/gi, '-').toLowerCase().replace(/^-|-$/g, '').slice(0, 40) || 'el';
22
+
23
+ function ff(args) {
24
+ const r = spawnSync('ffmpeg', ['-y', '-hide_banner', '-loglevel', 'error', ...args], { encoding: 'utf8', timeout: 60000 });
25
+ return r.status === 0;
26
+ }
27
+
28
+ /** Reamostra o PNG 3D (256 px) para 768 px com lanczos e um realce leve, mantendo o alfa. */
29
+ function ampliar(origem, destino) {
30
+ fs.mkdirSync(path.dirname(destino), { recursive: true });
31
+ const ok = ff(['-i', origem, '-vf', 'format=rgba,scale=768:768:flags=lanczos,unsharp=5:5:0.7:5:5:0.0', '-pix_fmt', 'rgba', destino]);
32
+ if (ok && fs.existsSync(destino)) return destino;
33
+ fs.copyFileSync(origem, destino);
34
+ return destino;
35
+ }
36
+
37
+ async function icone3d(termo, pasta) {
38
+ const colecao = require('./colecao.js');
39
+ const r = colecao.procurarIcones3d(termo, { quantas: 3 });
40
+ if (!r.ok || !r.icones.length) return null;
41
+ for (const ic of r.icones) {
42
+ const b = await colecao.baixar(ic.id, { pasta });
43
+ if (!b.ok) continue;
44
+ const origem = path.join(pasta, b.src);
45
+ const destino = path.join(pasta, 'midia', 'elementos', slug(ic.nome) + '.png');
46
+ ampliar(origem, destino);
47
+ const ficha = { fonte: ic.fonte, licenca: 'MIT', termo, nome: ic.nome };
48
+ try { fs.writeFileSync(destino.replace(/\.png$/, '.json'), JSON.stringify(ficha, null, 2)); } catch { /* sem ficha */ }
49
+ return { nome: ic.nome, termo, tipo: 'png', arquivo: destino, fonte: ic.fonte, licenca: 'MIT' };
50
+ }
51
+ return null;
52
+ }
53
+
54
+ async function animacao(termo, pasta) {
55
+ const colecao = require('./colecao.js');
56
+ const r = colecao.procurarAnimacoes(termo, { quantas: 2 });
57
+ if (!r.ok || !r.animacoes.length) return null;
58
+ const a = r.animacoes[0];
59
+ const dir = path.join(pasta, 'midia', 'elementos', slug(a.nome) + '-seq');
60
+ const primeiro = path.join(dir, 'f001.png');
61
+ if (!fs.existsSync(primeiro)) {
62
+ const gif = path.join(pasta, 'midia', 'elementos', slug(a.nome) + '.gif');
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ const resp = await fetch(a.gif, { headers: { 'user-agent': 'primocode-estudio' }, signal: AbortSignal.timeout(20000) });
65
+ if (!resp.ok) return null;
66
+ fs.writeFileSync(gif, Buffer.from(await resp.arrayBuffer()));
67
+ if (!ff(['-i', gif, '-fps_mode', 'passthrough', path.join(dir, 'f%03d.png')]) || !fs.existsSync(primeiro)) return null;
68
+ try { fs.writeFileSync(path.join(dir, 'licenca.json'), JSON.stringify({ fonte: 'Noto Emoji Animation (Google)', licenca: 'CC BY 4.0', creditos: a.creditos, termo }, null, 2)); } catch { /* sem ficha */ }
69
+ }
70
+ return { nome: a.nome, termo, tipo: 'seq', arquivo: primeiro, fonte: 'Noto Emoji Animation (Google)', licenca: 'CC BY 4.0', creditos: a.creditos };
71
+ }
72
+
73
+ /** Logo (SVG) da marca, pelo Wikimedia Commons (acervo.js). */
74
+ async function logo(marca, pasta) {
75
+ const acervo = require('./acervo.js');
76
+ const achados = await acervo.wikimediaLogos(marca, { quantos: 3 });
77
+ for (const l of achados || []) {
78
+ if (!/\.(svg|png)(\?|$)/i.test(l.url || '')) continue;
79
+ const b = await acervo.baixar(l.url, { titulo: marca + ' logo', meta: { licenca: l.licenca, autor: l.autor, fonte: 'wikimedia' }, pasta: path.join(pasta, 'midia', 'elementos') });
80
+ if (b.ok) return { nome: marca + ' (logo)', termo: marca, tipo: /\.svg$/i.test(b.arquivo) ? 'svg' : 'png', arquivo: b.arquivo, fonte: 'Wikimedia Commons', licenca: l.licenca || '?' };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ async function printDoSite(url, pasta) {
86
+ const ps = require('./print-site.js');
87
+ const r = await ps.capturarSite(url, { pasta });
88
+ const arq = r && r.ok && r.src ? path.join(pasta, r.src) : null;
89
+ if (arq && fs.existsSync(arq)) return { nome: 'print do site', termo: url, tipo: 'png', arquivo: arq, fonte: url, licenca: 'do dono do site' };
90
+ return null;
91
+ }
92
+
93
+ /** Termos em inglês, na ordem: os pedidos, os da metáfora e as palavras do assunto (que o dicionário PT→EN conhece). */
94
+ function termosDoAssunto({ assunto, pedidos = [], meta = null }) {
95
+ const colecao = require('./colecao.js');
96
+ const out = [];
97
+ const poe = (t) => { t = String(t || '').trim(); if (t && !out.includes(t)) out.push(t); };
98
+ pedidos.forEach(poe);
99
+ for (const w of String(assunto || '').toLowerCase().split(/[^\p{L}]+/u)) if (w.length > 2 && colecao.PT_EN[w]) poe(colecao.PT_EN[w]);
100
+ ((meta && meta.elementos) || []).forEach(poe);
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * Reúne `minimo` a `max` elementos reais. `deps.icone3d/animacao` são injetáveis (testes). Nunca lança.
106
+ * Devolve {ok, elementos, creditos, faltou}.
107
+ */
108
+ async function reunir({ assunto, pedidos = [], meta = null, pasta, minimo = 3, max = 6, rede = true, logos = [], site = null, animados = true, deps = {} } = {}) {
109
+ if (!pasta) return { ok: false, error: 'faltou a pasta do projeto.', elementos: [] };
110
+ const ic3 = deps.icone3d || icone3d, an = deps.animacao || animacao;
111
+ const termos = termosDoAssunto({ assunto, pedidos, meta });
112
+ const elementos = [];
113
+ const tem = (e) => e && !elementos.some((x) => x.arquivo === e.arquivo || x.nome === e.nome);
114
+ try {
115
+ if (site && rede) { const e = await (deps.print || printDoSite)(site, pasta).catch(() => null); if (tem(e)) elementos.push(e); }
116
+ for (const m of logos || []) { if (!rede) break; const e = await (deps.logo || logo)(m, pasta).catch(() => null); if (tem(e)) elementos.push(e); }
117
+ for (const t of termos) {
118
+ if (elementos.length >= max) break;
119
+ const e = await ic3(t, pasta).catch(() => null); // o ícone 3D mora no pacote; só o PNG desce da rede
120
+ if (tem(e)) elementos.push(e);
121
+ }
122
+ if (animados && rede && meta && meta.animado) {
123
+ for (const t of meta.animado.slice(0, 1)) { const e = await an(t, pasta).catch(() => null); if (tem(e) && elementos.length < max + 1) elementos.push(e); }
124
+ }
125
+ for (const t of ['sparkles', 'star', 'check mark button', 'light bulb']) { // última reserva: ainda é "real", só menos específico
126
+ if (elementos.length >= minimo) break;
127
+ const e = await ic3(t, pasta).catch(() => null); if (tem(e)) elementos.push(e);
128
+ }
129
+ } catch (e) { return { ok: elementos.length >= minimo, error: e.message, elementos, creditos: [], faltou: Math.max(0, minimo - elementos.length) }; }
130
+ const creditos = elementos.filter((e) => e.creditos || /CC/i.test(e.licenca)).map((e) => ({ nome: e.nome, licenca: e.licenca, creditos: e.creditos || null }));
131
+ return { ok: elementos.length >= minimo, elementos, creditos, faltou: Math.max(0, minimo - elementos.length), termos };
132
+ }
133
+
134
+ module.exports = { reunir, icone3d, animacao, logo, printDoSite, termosDoAssunto, ampliar, slug };