primocode 10.16.0 → 10.17.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 (234) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +11 -2
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +36 -8
  198. package/lib/catalogo.js +4 -0
  199. package/lib/chamada-torta.js +81 -0
  200. package/lib/editor-pro.js +786 -0
  201. package/lib/entrega.js +58 -0
  202. package/lib/estudio/conferencia-visual.js +76 -0
  203. package/lib/estudio/diretor.js +6 -1
  204. package/lib/estudio/exportar.js +3 -1
  205. package/lib/estudio/gosto.js +45 -4
  206. package/lib/estudio/index.js +8 -5
  207. package/lib/estudio/kit-ae/base.jsx +119 -13
  208. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  209. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  210. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  211. package/lib/estudio/maquina-diretor.js +61 -7
  212. package/lib/estudio/maquina-foto.js +177 -0
  213. package/lib/estudio/maquina-guarda.js +207 -0
  214. package/lib/estudio/maquina-layout.js +22 -1
  215. package/lib/estudio/maquina.js +60 -11
  216. package/lib/estudio/referencias.js +1 -1
  217. package/lib/estudio/visao/visao.swift +97 -0
  218. package/lib/estudio/visao.js +60 -0
  219. package/lib/local/cerebro.json +192 -6
  220. package/lib/local/motor.js +5 -2
  221. package/lib/nuvem.js +11 -1
  222. package/lib/passagem.js +50 -0
  223. package/lib/perigo.js +19 -11
  224. package/lib/protecao.js +134 -0
  225. package/lib/titulo-conversa.js +46 -0
  226. package/lib/tools.js +3 -0
  227. package/lib/ui.js +1 -1
  228. package/package.json +3 -2
  229. package/skills/editar-meu-video/SKILL.md +6 -2
  230. package/skills/editar-video/SKILL.md +113 -0
  231. package/studio/pessoa.py +52 -0
  232. package/voz/primo_voz/app.py +43 -4
  233. package/voz/primo_voz/chat.html +17 -6
  234. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * maquina-foto.js — a FOTO que a pessoa manda vira peça do motion (a primitiva "foto" do kit-ae).
3
+ *
4
+ * Antes (20/10): a foto do pedido virava só "referência" (cores, em index.js:importarReferencia) e nenhuma primitiva
5
+ * aceitava imagem: o motion não tinha onde pôr a foto e o pedido travava. Agora, ANTES do render e uma vez só:
6
+ *
7
+ * 1. confere o arquivo (formato, tamanho, se abre) e diz o porquê em português quando não serve;
8
+ * 2. copia para <projeto>/midia/fotos/ (o motion nunca aponta para fora do projeto), já com o lado maior em ≤ 2400 px;
9
+ * 3. faz o fundo desfocado (o plano de trás do parallax) com o ffmpeg;
10
+ * 4. tenta o recorte da pessoa: o Vision do macOS primeiro (lib/estudio/visao.js), o MediaPipe (studio/pessoa.py) depois;
11
+ * sem pessoa ou sem nenhum dos dois, a foto vai inteira, com moldura arredondada.
12
+ *
13
+ * preparar({fotos, pasta, deps}) → {fotos:[{arquivo, fundo, recorte, w, h, nome, de, moldura}], recusadas:[{arquivo, motivo}]}
14
+ *
15
+ * `deps` (injetáveis; testes): sondar(arq) → {w,h}|null, recortar(entrada, saida) → {ok, semPessoa?}, converter(entrada, saida) → bool.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { spawnSync } = require('child_process');
23
+
24
+ const FORMATOS = ['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'];
25
+ const MAX_BYTES = 25 * 1024 * 1024;
26
+ const MAX_LADO = 2400;
27
+ const MAX_FOTOS = 6;
28
+
29
+ const slug = (s) => String(s).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/gi, '-').toLowerCase().replace(/^-|-$/g, '').slice(0, 30) || 'foto';
30
+
31
+ function ff(args, timeout = 60000) {
32
+ const r = spawnSync('ffmpeg', ['-y', '-hide_banner', '-loglevel', 'error', ...args], { encoding: 'utf8', timeout });
33
+ return r.status === 0;
34
+ }
35
+
36
+ /** Largura e altura pelo ffprobe (o que o ffmpeg abre, o motion abre). null = não abre. */
37
+ function sondar(arq) {
38
+ const r = spawnSync('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height', '-of', 'csv=p=0:s=x', arq], { encoding: 'utf8', timeout: 20000 });
39
+ const m = r.status === 0 && String(r.stdout).trim().match(/^(\d+)x(\d+)/);
40
+ return m ? { w: Number(m[1]), h: Number(m[2]) } : null;
41
+ }
42
+
43
+ /** HEIC/HEIF → JPG: sips (macOS) ou o ffmpeg, se ele leu HEIC. false = sem conversor. */
44
+ function converter(entrada, saida) {
45
+ if (process.platform === 'darwin') {
46
+ const r = spawnSync('sips', ['-s', 'format', 'jpeg', entrada, '--out', saida], { encoding: 'utf8', timeout: 60000 });
47
+ if (r.status === 0 && fs.existsSync(saida)) return true;
48
+ }
49
+ return ff(['-i', entrada, '-frames:v', '1', saida]) && fs.existsSync(saida);
50
+ }
51
+
52
+ function extensao(arq) { return path.extname(String(arq || '')).replace('.', '').toLowerCase(); }
53
+
54
+ /** Confere uma foto SEM mexer nela. {ok:true, ext, bytes} ou {ok:false, error} (em português, dizendo o que fazer). */
55
+ function validar(arq, { sondar: sd = sondar } = {}) {
56
+ const nome = path.basename(String(arq || '')) || String(arq || '');
57
+ if (!arq || typeof arq !== 'string') return { ok: false, error: 'faltou o caminho da foto.' };
58
+ const ext = extensao(arq);
59
+ if (!FORMATOS.includes(ext)) {
60
+ return { ok: false, error: `a foto "${nome}" é ${ext ? '.' + ext : 'de um tipo sem extensão'}: aceito ${FORMATOS.map((f) => '.' + f).join(', ')}. Salve ou exporte como JPG ou PNG e mande de novo.` };
61
+ }
62
+ let st;
63
+ try { st = fs.statSync(arq); } catch { return { ok: false, error: `não achei a foto "${nome}" em ${path.dirname(arq)}. Confira o caminho.` }; }
64
+ if (!st.isFile()) return { ok: false, error: `"${nome}" não é um arquivo de foto.` };
65
+ if (st.size === 0) return { ok: false, error: `a foto "${nome}" está vazia (0 bytes).` };
66
+ if (st.size > MAX_BYTES) return { ok: false, error: `a foto "${nome}" tem ${(st.size / 1048576).toFixed(1)} MB: o máximo é ${MAX_BYTES / 1048576} MB. Reduza o tamanho e mande de novo.` };
67
+ if (ext !== 'heic' && ext !== 'heif') {
68
+ const d = sd(arq);
69
+ if (!d || !(d.w > 0 && d.h > 0)) return { ok: false, error: `não consegui abrir a foto "${nome}": o arquivo está corrompido ou não é uma imagem de verdade. Tente exportar de novo.` };
70
+ if (d.w < 200 || d.h < 200) return { ok: false, error: `a foto "${nome}" é pequena demais (${d.w}x${d.h}): precisa ter pelo menos 200 px de lado para não ficar borrada no vídeo.` };
71
+ }
72
+ return { ok: true, ext, bytes: st.size };
73
+ }
74
+
75
+ function acharPython() {
76
+ for (const p of [process.env.PRIMOCODE_PYTHON, 'python3', 'python'].filter(Boolean)) {
77
+ const r = spawnSync(p, ['--version'], { encoding: 'utf8', timeout: 10000 });
78
+ if (r.status === 0) return p;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /** Apara a margem transparente do recorte (o que sobra é a pessoa): grava no próprio arquivo e devolve {w,h}; null se não deu. */
84
+ function aparar(png) {
85
+ const r = spawnSync('ffmpeg', ['-hide_banner', '-i', png, '-vf', 'alphaextract,cropdetect=limit=24:round=2:skip=0', '-f', 'null', '-'], { encoding: 'utf8', timeout: 60000 });
86
+ const todos = [...String(r.stderr || '').matchAll(/crop=(\d+):(\d+):(\d+):(\d+)/g)];
87
+ if (!todos.length) return null;
88
+ const [, w, h, x, y] = todos[todos.length - 1].map(Number);
89
+ if (!(w >= 64 && h >= 64)) return null;
90
+ const tmp = png.replace(/\.png$/, '-aparado.png');
91
+ if (!ff(['-i', png, '-vf', `crop=${w}:${h}:${x}:${y}`, tmp]) || !fs.existsSync(tmp)) return null;
92
+ fs.renameSync(tmp, png);
93
+ return { w, h };
94
+ }
95
+
96
+ /** O recorte pelo MediaPipe (studio/pessoa.py). {ok:true} | {ok:false, semPessoa?, semMediaPipe?, error}. Nunca lança. */
97
+ function recortarMediaPipe(entrada, saida) {
98
+ const py = acharPython();
99
+ if (!py) return { ok: false, error: 'sem Python 3 para o recorte' };
100
+ const r = spawnSync(py, [path.join(__dirname, '..', '..', 'studio', 'pessoa.py'), 'recortar-foto', '--entrada', entrada, '--saida', saida], { encoding: 'utf8', timeout: 90000 });
101
+ try { return { ...JSON.parse(String(r.stdout || '').trim().split('\n').pop()), motor: 'mediapipe' }; } catch { return { ok: false, error: String(r.stderr || r.stdout || 'o recorte não respondeu').slice(-200) }; }
102
+ }
103
+
104
+ /**
105
+ * O recorte da pessoa, do melhor pro último caso: 1) o Vision do macOS (VNGeneratePersonSegmentationRequest, sem instalar
106
+ * nada; lib/estudio/visao.js), 2) o MediaPipe (studio/pessoa.py), 3) nenhum → a foto vai inteira, com moldura.
107
+ * {ok:true, motor} | {ok:false, semPessoa?, error}. Nunca lança. `deps.visao` é injetável (testes).
108
+ */
109
+ function recortarPessoa(entrada, saida, { visao } = {}) {
110
+ let V = visao;
111
+ if (V === undefined) { try { V = require('./visao.js'); } catch { V = null; } }
112
+ if (V && V.disponivel()) {
113
+ const r = V.recortar(entrada, saida);
114
+ if (r && r.ok && fs.existsSync(saida)) return { ok: true, motor: 'vision', cobertura: r.cobertura };
115
+ if (r && r.semPessoa) return { ok: false, semPessoa: true, motor: 'vision', error: 'sem pessoa na foto' };
116
+ }
117
+ return recortarMediaPipe(entrada, saida);
118
+ }
119
+
120
+ /**
121
+ * Prepara as fotos do pedido. Nunca lança: o que não serve vai em `recusadas`, com o porquê.
122
+ * `opc.max` limita quantas entram (padrão 6).
123
+ */
124
+ async function preparar({ fotos = [], pasta, deps = {}, max = MAX_FOTOS } = {}) {
125
+ const out = { fotos: [], recusadas: [] };
126
+ if (!pasta) return { ...out, recusadas: [{ arquivo: '', motivo: 'faltou a pasta do projeto.' }] };
127
+ const lista = (Array.isArray(fotos) ? fotos : [fotos]).filter(Boolean).map((f) => String(f).replace(/^file:\/\//, ''));
128
+ const unicas = [...new Set(lista)];
129
+ const sd = deps.sondar || sondar;
130
+ const dir = path.join(pasta, 'midia', 'fotos');
131
+ for (const orig of unicas) {
132
+ if (out.fotos.length >= max) { out.recusadas.push({ arquivo: orig, motivo: `o motion usa até ${max} fotos: "${path.basename(orig)}" ficou de fora.` }); continue; }
133
+ const v = validar(orig, { sondar: sd });
134
+ if (!v.ok) { out.recusadas.push({ arquivo: orig, motivo: v.error }); continue; }
135
+ try {
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ const n = out.fotos.length + 1;
138
+ const base = `foto-${n}-${slug(path.basename(orig, path.extname(orig)))}`;
139
+ let entrada = orig;
140
+ if (v.ext === 'heic' || v.ext === 'heif') {
141
+ const jpgTmp = path.join(dir, base + '-heic.jpg');
142
+ const ok = (deps.converter || converter)(orig, jpgTmp);
143
+ if (!ok || !fs.existsSync(jpgTmp)) {
144
+ out.recusadas.push({ arquivo: orig, motivo: `a foto "${path.basename(orig)}" é HEIC e esta máquina não tem conversor (no macOS vem o sips; no Windows/Linux, instale o ffmpeg com suporte a HEIC). Exporte como JPG e mande de novo.` });
145
+ continue;
146
+ }
147
+ entrada = jpgTmp;
148
+ }
149
+ // cópia dentro do projeto, no lado máximo de 2400 px (a rotação do EXIF o ffmpeg já aplica)
150
+ const alvo = path.join(dir, base + (v.ext === 'png' ? '.png' : '.jpg'));
151
+ const filtro = `scale='if(gt(iw,ih),min(${MAX_LADO},iw),-2)':'if(gt(iw,ih),-2,min(${MAX_LADO},ih))'`;
152
+ const ok = ff(['-i', entrada, '-frames:v', '1', '-vf', filtro, ...(v.ext === 'png' ? [] : ['-q:v', '2']), alvo]);
153
+ // sem ffmpeg ou ele recusou: copia como veio (o kit ainda tenta abrir)
154
+ if (!ok || !fs.existsSync(alvo) || fs.statSync(alvo).size === 0) fs.copyFileSync(entrada, alvo);
155
+ if (entrada !== orig) fs.rmSync(entrada, { force: true });
156
+ const dims = sd(alvo);
157
+ const d = dims || { w: 1080, h: 1080 };
158
+ // o plano de trás do parallax: pequeno e muito desfocado (o kit amplia)
159
+ const fundo = path.join(dir, base + '-fundo.jpg');
160
+ const okF = ff(['-i', alvo, '-frames:v', '1', '-vf', 'scale=480:-2,gblur=sigma=16', '-q:v', '4', fundo]) && fs.existsSync(fundo);
161
+ // o recorte da pessoa, uma vez; sem pessoa ou sem MediaPipe: foto inteira com moldura
162
+ let recorte = null, motivoRecorte = null, rdims = null, motorRecorte = null;
163
+ if (deps.recortar !== false) {
164
+ const png = path.join(dir, base + '-recorte.png');
165
+ const rc = await (deps.recortar || ((e, s2) => recortarPessoa(e, s2, { visao: deps.visao })))(alvo, png);
166
+ if (rc && rc.ok && fs.existsSync(png)) { recorte = png; motorRecorte = rc.motor || null; rdims = (deps.aparar || aparar)(png) || sd(png); }
167
+ else motivoRecorte = (rc && rc.semPessoa) ? 'sem pessoa na foto' : ((rc && rc.error) || 'sem recorte');
168
+ }
169
+ out.fotos.push({ arquivo: alvo, fundo: okF ? fundo : alvo, recorte, w: d.w, h: d.h, nome: path.basename(orig), de: orig, moldura: !recorte, ...(rdims ? { rw: rdims.w, rh: rdims.h } : {}), ...(motorRecorte ? { recorteMotor: motorRecorte } : {}), ...(motivoRecorte ? { semRecorte: motivoRecorte } : {}) });
170
+ } catch (e) {
171
+ out.recusadas.push({ arquivo: orig, motivo: `não consegui preparar a foto "${path.basename(orig)}": ${e.message}` });
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
177
+ module.exports = { FORMATOS, MAX_BYTES, MAX_LADO, MAX_FOTOS, validar, preparar, sondar, converter, recortarPessoa, recortarMediaPipe, aparar, slug, extensao };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * maquina-guarda.js — a GUARDA de layout no Node, ANTES do render.
3
+ *
4
+ * As regras e os consertos moram em kit-ae/guarda.jsx (ES3), carregado aqui pelo `vm`: o MESMO código roda no kit (no fecha() de
5
+ * cada cena, com as caixas reais do After Effects) e aqui (sobre o plano, com a largura medida da fonte). Uma fonte da verdade.
6
+ *
7
+ * medirTexto(str, {fonte, peso, tamanho}) → largura em px (avanços do .ttf do cache; sem a fonte, estimativa)
8
+ * blocoTexto(str, opc, pl) → {w, h, tam, linhas}: a mesma quebra e o mesmo encolhe do texto() do kit
9
+ * zonaLayout(pl) → a zona segura no espaço do layout (antes da câmera)
10
+ * planejarFoto(cena, pl) → cena.layout {foto, halo, titulo, icones, particulas} + cena.guarda (log)
11
+ * guardar(pl) → roda a guarda em todas as cenas; devolve [{cena, log}]
12
+ * verificarCaixas(caixas, W, H, opc) → o que ainda viola (a conferência por caixa, sem o olho do Vision)
13
+ * core → as funções do guarda.jsx (gConsertar, gVerificar, gZona, ...)
14
+ */
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const vm = require('vm');
20
+
21
+ const ARQ = path.join(__dirname, 'kit-ae', 'guarda.jsx');
22
+ const core = (() => {
23
+ const ctx = vm.createContext({ Math });
24
+ vm.runInContext(fs.readFileSync(ARQ, 'utf8') + '\n;this.__g = { GUARDA, gZona, gConsertar, gVerificar, gToca, gCabe, gCaixa, gEscala, gMove, gW, gH, gCx, gCy, gLugarLivre, gCursor, gCaixaCursor, gDentroAlvo, gTextoNoCartao };', ctx, { filename: ARQ });
25
+ return ctx.__g;
26
+ })();
27
+
28
+ // ── medir texto pela fonte ────────────────────────────────────────────────
29
+ const fontes = new Map();
30
+ function abrirFonte(familia, peso) {
31
+ const k = familia + '|' + peso;
32
+ if (fontes.has(k)) return fontes.get(k);
33
+ let f = null;
34
+ try {
35
+ const G = require('./glifos.js');
36
+ const base = String(familia).replace(/[^a-z0-9]+/gi, '-').toLowerCase();
37
+ for (const p of [peso, String(peso).replace(/i$/, ''), 800, 700, 400]) {
38
+ const arq = path.join(G.CACHE, base + '-' + p + '.ttf');
39
+ if (fs.existsSync(arq)) { f = G.abrirFonte(fs.readFileSync(arq)); break; }
40
+ }
41
+ } catch { f = null; }
42
+ fontes.set(k, f);
43
+ return f;
44
+ }
45
+ /** Largura (px) de `str` na fonte, pelos avanços do hmtx; sem a fonte no cache, 0,58 em por caractere (Poppins pesada). */
46
+ function medirTexto(str, { fonte = 'Poppins', peso = 800, tamanho = 100, tracking = 0 } = {}) {
47
+ const s = String(str || '');
48
+ const f = abrirFonte(fonte, peso);
49
+ let em = 0;
50
+ if (f) { for (const ch of s) em += f.avanco(f.glifo(ch.codePointAt(0))) / f.porEm; }
51
+ else em = s.length * 0.58;
52
+ return em * tamanho + (tracking / 1000) * tamanho * Math.max(0, s.length - 1);
53
+ }
54
+ function quebra(ws, sw, maxW) {
55
+ const linhas = [[]]; let larg = 0;
56
+ ws.forEach((w, i) => {
57
+ const add = w + (linhas[linhas.length - 1].length ? sw : 0);
58
+ if (larg + add > maxW && linhas[linhas.length - 1].length) { linhas.push([i]); larg = w; }
59
+ else { linhas[linhas.length - 1].push(i); larg += add; }
60
+ });
61
+ return linhas;
62
+ }
63
+ const SO_400 = new Set(['Anton', 'Archivo Black', 'Instrument Serif', 'Bebas Neue', 'Caveat']);
64
+ /** O bloco de texto como o texto() do kit monta (base.jsx): mesmo mínimo, mesma quebra equilibrada, mesmo encolhe de 8%. */
65
+ function blocoTexto(str, { tamanho = 90, largura, maxLinhas = 4, principal = false, entreLinhas } = {}, pl = {}) {
66
+ const W = pl.w || 1080, H = pl.h || 1920, tall = H / W > 1.6, princ = principal && tall;
67
+ const T = pl.tipo || {}, fontesPl = pl.fontes || {};
68
+ const fonte = fontesPl.titulo || 'Poppins', fonteD = T.realce === 'italico' ? (fontesPl.destaque || fonte) : fonte;
69
+ const pesoT = SO_400.has(fonte) ? 400 : 800, pesoD = T.realce === 'italico' ? (SO_400.has(fonteD) ? '400i' : '700i') : pesoT;
70
+ let s = String(str || ''); if (T.caixa === 'alta') s = s.toUpperCase();
71
+ const pal = s.replace(/\s+/g, ' ').split(' ').filter(Boolean).map((t) => ({ t: t.replace(/\*/g, ''), kw: t.charAt(0) === '*' }));
72
+ if (!pal.length) return { w: 0, h: 0, tam: 0, linhas: 0 };
73
+ let tam = tamanho * (pl.ajuste || 1) * (pl.ajusteCena || 1);
74
+ tam = Math.max(tam, H * 0.036); if (princ) tam = Math.max(tam, 120);
75
+ const maxW = Math.min(largura || W * 0.84, W * 0.84), maxLin = maxLinhas + (princ ? 1 : 0), trk = T.tracking || 0;
76
+ let linhas, ws, sw, tent = 0;
77
+ for (;;) {
78
+ ws = pal.map((p) => medirTexto(p.t, { fonte: p.kw ? fonteD : fonte, peso: p.kw ? pesoD : pesoT, tamanho: tam, tracking: trk }));
79
+ sw = Math.max(tam * 0.27, medirTexto('a a', { fonte, peso: pesoT, tamanho: tam }) - medirTexto('aa', { fonte, peso: pesoT, tamanho: tam }));
80
+ const mx = Math.max(...ws);
81
+ linhas = quebra(ws, sw, maxW);
82
+ if (linhas.length > 1) { const tot = ws.reduce((a, w) => a + w + sw, 0); const l2 = quebra(ws, sw, Math.min(maxW, Math.max(mx, tot / linhas.length * 1.12))); if (l2.length <= linhas.length) linhas = l2; }
83
+ if ((mx <= maxW && linhas.length <= maxLin) || tent > 14 || tam < (princ ? 112 : H * 0.036)) break;
84
+ tam *= 0.92; tent++;
85
+ }
86
+ const lh = tam * (entreLinhas || T.entreLinhas || 1.12);
87
+ const w = Math.max(...linhas.map((l) => l.reduce((a, i, k) => a + ws[i] + (k ? sw : 0), 0)));
88
+ return { w, h: linhas.length * lh, tam, linhas: linhas.length };
89
+ }
90
+
91
+ // ── a zona no espaço do layout ────────────────────────────────────────────
92
+ /** A zona segura do MUNDO traduzida para o LAYOUT (antes da câmera), como o kParaLayout do kit: enq × 1,12 de zoom máximo. */
93
+ function zonaLayout(pl) {
94
+ const W = pl.w, H = pl.h, legenda = !!(pl.legenda && pl.legenda.chunks && pl.legenda.chunks.length);
95
+ const z = core.gZona(W, H, { legenda });
96
+ const enq = legenda ? 0.85 : 0.88, dy = legenda ? H * 0.07 : 0, f = enq * 1.12, cx = W / 2, cy = H / 2;
97
+ const X = (x) => cx + (x - cx) / f, Y = (y) => cy + (y - cy + dy) / f;
98
+ return { x0: X(z.x0), x1: X(z.x1), y0: Y(z.y0), y1: Y(z.y1), trilho: z.trilho ? [X(z.trilho[0]), Y(z.trilho[1]), W * 2, H * 2] : null };
99
+ }
100
+
101
+ /** Quanto do layout chega à tela: o kit monta o conteúdo a 88% (85% com legenda) e a câmera respira até ~6% (média ~0,9). */
102
+ function escalaMundo(pl) { const leg = !!(pl.legenda && pl.legenda.chunks && pl.legenda.chunks.length); return (leg ? 0.85 : 0.88) * 1.03; }
103
+
104
+ // ── a cena "foto" ─────────────────────────────────────────────────────────
105
+ /** Quanto a caixa cresce no passeio (escala e deslocamento por movimento): o envelope de todos os quadros-chave. */
106
+ function inflarFoto(recorte, mov, W, H) {
107
+ if (!recorte) return { kx: 1.04, ky: 1.04, dx: 0, dy: 0 }; // moldura: o passeio é DENTRO dela; só a rotação 3D (perspectiva) e a sombra
108
+ const esc = 1.06; // o kit (cenaFoto com layout) anda de 1,0 a 1,06 ou de 1,06 a 0,97
109
+ const lateral = mov === 'passeio' || mov === 'orbita';
110
+ return { kx: esc * 1.03, ky: esc * 1.03, dx: lateral ? W * 0.025 : 0, dy: lateral ? 0 : H * 0.015 };
111
+ }
112
+
113
+ /**
114
+ * Planeja a cena "foto": a foto o maior possível (55–80% da largura no 9:16), o título sem encostar nela, os ícones só em espaço livre.
115
+ * Escreve `cena.layout` (o kit lê) e `cena.guarda` (o log). Sem caber, encolhe o título; sem saída, a cena troca de peça.
116
+ */
117
+ function planejarFoto(c, pl, { tentativa = 0 } = {}) {
118
+ const F = c.foto;
119
+ if (!F) return null;
120
+ const W = pl.w, H = pl.h, v = H >= W * 0.95, alto = H / W > 1.6, u = Math.min(W, H) / 1080;
121
+ const z = zonaLayout(pl), folga = core.GUARDA.folga * u;
122
+ const mundo = escalaMundo(pl); // layout → o que o espectador vê (a câmera monta a 88% e respira até ~6%)
123
+ const recorte = !!F.recorte, fw = recorte ? (F.rw || F.w) : F.w, fh0 = recorte ? (F.rh || F.h) : F.h;
124
+ // foto alta (retrato, celular em pé, pessoa de corpo inteiro): no 9:16 a caixa vai até 4:5 e o resto é cortado (embaixo no
125
+ // recorte, com degradê; no meio na moldura, onde o passeio anda) — senão a foto não cabe grande junto do título
126
+ const ASP = v ? [1.25, 1.1, 1.0, 0.9] : [1.6, 1.3]; // sem caber, a caixa fica mais baixa (corta mais) antes de desistir
127
+ const maxAsp = ASP[Math.min(tentativa, ASP.length - 1)], corte = fh0 / fw > maxAsp ? maxAsp / (fh0 / fw) : 1, fh = fh0 * corte;
128
+ const inf = inflarFoto(recorte, c.movimento || 'aproxima', W, H);
129
+ const log = [];
130
+ // o título, medido na fonte (o kit pode mudar alguns px: a guarda do kit confere de novo com a caixa real)
131
+ const tamT = (v ? 86 : 74) * (c.ajuste || 1);
132
+ // no 9:16 o título fica entre a margem esquerda e o trilho de botões do Reels (centro ~30 px à esquerda: não se nota, e ganha uma linha)
133
+ const xDir = z.trilho ? Math.min(z.x1, z.trilho[0]) : z.x1;
134
+ const largT = v ? Math.min(W * 0.84, xDir - z.x0 - 8) : W * 0.42;
135
+ const tb = c.titulo ? blocoTexto(c.titulo, { tamanho: tamT, largura: largT, maxLinhas: 3, principal: true }, { ...pl, ajusteCena: 1 }) : null;
136
+ const tw = tb ? tb.w * 1.04 : 0, th = tb ? tb.h * 1.06 : 0;
137
+ const itens = [];
138
+ let fotoW, fotoH, px, py, tx, ty;
139
+ if (v) {
140
+ const zh = z.y1 - z.y0, livre = zh - (tb ? th + folga : 0);
141
+ fotoW = W * (alto ? Math.max(core.GUARDA.fotoMin, core.GUARDA.fotoMax * (c.fotoMenor || 1)) / mundo : 0.6); fotoH = fotoW * fh / fw; // fotoMenor: o olho achou texto no rosto
142
+ // a foto alta demais pro espaço: encolhe pela altura (o guarda reclama se ficar abaixo de 55%)
143
+ const envH = (h) => h * inf.ky + 2 * inf.dy;
144
+ if (envH(fotoH) > livre * 0.94) { fotoH = (livre * 0.94 - 2 * inf.dy) / inf.ky; fotoW = fotoH * fw / fh; }
145
+ // título embaixo (no fim da zona), a foto em cima dele com folga; o espaço que sobra fica em cima, pros ícones
146
+ ty = tb ? z.y1 - th / 2 : null; tx = z.trilho ? (z.x0 + xDir) / 2 : W / 2;
147
+ const baseFoto = tb ? ty - th / 2 - folga : z.y1;
148
+ py = baseFoto - envH(fotoH) / 2; px = W / 2;
149
+ } else {
150
+ fotoW = Math.min(W * 0.4, (H * 0.74) * fw / fh); fotoH = fotoW * fh / fw;
151
+ px = W * 0.3; py = H / 2; tx = W * 0.73; ty = H / 2;
152
+ }
153
+ const fotoItem = { id: 'foto', tipo: 'foto', nome: 'a foto', c: core.gCaixa(px, py, fotoW * inf.kx + 2 * inf.dx, fotoH * inf.ky + 2 * inf.dy), infla: inf.kx + 2 * inf.dx / fotoW, prio: 9 };
154
+ itens.push(fotoItem);
155
+ const titItem = tb ? { id: 'titulo', tipo: 'texto', nome: 'o título', c: core.gCaixa(tx, ty, tw, th), prio: 10 } : null;
156
+ if (titItem) itens.push(titItem);
157
+ // os ícones (elementos reais do assunto) começam onde o kit punha; a guarda tira de cima do texto e da foto
158
+ const nEl = Math.min((pl.elementos || []).length, v ? 3 : 2), S = Math.min(W, H) * (v ? 0.2 : 0.24);
159
+ const posV = [[W * 0.2, H * 0.17], [W * 0.84, H * 0.5], [W * 0.18, H * 0.58]], posH = [[W * 0.54, H * 0.2], [W * 0.5, H * 0.8]];
160
+ const ondula = (12 + 0.05 * W / u) * u, inflaI = 1.15; // o balanço (seno) e o quique da entrada
161
+ for (let i = 0; i < nEl; i++) {
162
+ const p = (v ? posV : posH)[i];
163
+ itens.push({ id: 'icone' + i, idx: i, tipo: 'icone', nome: 'ícone ' + ((pl.elementos[i] && pl.elementos[i].nome) || i + 1), c: core.gCaixa(p[0], p[1], S * inflaI + 2 * ondula, S * inflaI + 2 * 13 * u), infla: (S * inflaI + 2 * ondula) / S, prio: 3 - i });
164
+ }
165
+ const r = core.gConsertar(itens, W, H, { zona: z, folga, escalaMundo: mundo });
166
+ log.push(...r.log);
167
+ if (r.trocar) {
168
+ if (tentativa < ASP.length - 1 && fh0 / fw > ASP[tentativa + 1]) return planejarFoto(c, pl, { tentativa: tentativa + 1 });
169
+ c.guarda = [...log, 'a foto e o título não cabem juntos: a cena vira herói (sem a foto)'];
170
+ c.tipo = 'heroi'; c.layout = null;
171
+ return c.guarda;
172
+ }
173
+ if (corte < 1) log.unshift(`foto alta: a caixa vai até ${maxAsp === 1.25 ? '4:5' : '1:' + maxAsp} e ${Math.round((1 - corte) * 100)}% da altura fica de fora (${recorte ? 'embaixo, com degradê' : 'o passeio anda por ela'})`);
174
+ const fi = r.itens.find((x) => x.id === 'foto');
175
+ const k = fi.k, fW = fotoW * k, fH = fotoH * k;
176
+ const cxF = (fi.c[0] + fi.c[2]) / 2, cyF = (fi.c[1] + fi.c[3]) / 2;
177
+ const ti = r.itens.find((x) => x.id === 'titulo');
178
+ const fotoPct = Math.round(fW * mundo / W * 100); // no mundo (o que se vê)
179
+ c.layout = {
180
+ foto: { x: Math.round(cxF), y: Math.round(cyF), w: Math.round(fW), h: Math.round(fH), ...(corte < 1 ? { corte: Math.round(corte * 1000) / 1000 } : {}) },
181
+ // o halo abraça a foto (nunca vaza pro título): círculo atrás da cabeça no recorte, placa arredondada na moldura
182
+ halo: recorte ? { x: Math.round(cxF), y: Math.round(cyF - fH * 0.06), d: Math.round(Math.min(fW * 0.92, fH * 1.02)) } : { x: Math.round(cxF), y: Math.round(cyF), w: Math.round(fW * 1.04), h: Math.round(fH * 1.04) },
183
+ titulo: ti ? { x: Math.round((ti.c[0] + ti.c[2]) / 2), y: Math.round((ti.c[1] + ti.c[3]) / 2), largura: Math.round(largT), tam: Math.round(tamT * 100) / 100 } : null,
184
+ icones: r.itens.filter((x) => x.tipo === 'icone').map((x) => (x.removido ? { i: x.idx, fora: true } : { i: x.idx, x: Math.round((x.c[0] + x.c[2]) / 2), y: Math.round((x.c[1] + x.c[3]) / 2), w: Math.round(S * x.k) })),
185
+ particulas: [Math.round(fi.c[0]), Math.round(fi.c[1]), Math.round(fi.c[2]), Math.round(fi.c[3])],
186
+ fotoPct,
187
+ };
188
+ c.guarda = log.length ? log : [`ok: foto ${fotoPct}% da largura, nada encostado`];
189
+ return c.guarda;
190
+ }
191
+
192
+ /** A guarda em todas as cenas do plano (hoje a "foto" é planejada aqui; as outras, a guarda do kit conserta com a caixa real). */
193
+ function guardar(pl) {
194
+ const out = [];
195
+ (pl.cenas || []).forEach((c, i) => {
196
+ if (c.tipo === 'foto' && c.foto) { const l = planejarFoto(c, pl); if (l && l.length) out.push({ cena: i, log: l }); }
197
+ });
198
+ return out;
199
+ }
200
+
201
+ /** As caixas finais (a linha GUARDA do kit) conferidas de novo: o que ainda viola. */
202
+ function verificarCaixas(caixas, W, H, { legenda = false } = {}) {
203
+ const itens = (caixas || []).map((x, i) => ({ id: i, tipo: x.tipo, nome: x.nome, c: x.c, removido: !!x.fora, livre: !!x.livre }));
204
+ return core.gVerificar(itens, W, H, { zona: zonaLayout({ w: W, h: H, legenda: legenda ? { chunks: [1] } : null }), folga: 0 });
205
+ }
206
+
207
+ module.exports = { core, escalaMundo, medirTexto, blocoTexto, zonaLayout, inflarFoto, planejarFoto, guardar, verificarCaixas };
@@ -143,7 +143,11 @@ function corrigir(plano, r, { tentativa = 1, paletaFundo = null } = {}) {
143
143
  const l = por[k];
144
144
  const comAmbiente = l.some((x) => /ambiente/.test(x));
145
145
  if (comAmbiente && !c.semAmbiente) { c.semAmbiente = true; mudou.push(`cena ${k}: sem o elemento de enfeite (${l.find((x) => /ambiente/.test(x))})`); }
146
- const outros = l.filter((x) => !/ambiente/.test(x) && !/herói pequeno|texto principal pequeno/.test(x));
146
+ // texto que a câmera leva pra cima da faixa de cima (o título do topo, com chicote/pulso): o título desce 1,2% em vez de
147
+ // encolher (encolher não tira ele do topo: só deixa a letra pequena)
148
+ const emCima = l.filter((x) => { const m = x.match(/texto fora da área segura[^[]*\[(-?\d+),(-?\d+),(-?\d+),(-?\d+)\]/); return m && Number(m[2]) < (plano.h || 1920) * 0.16 && Number(m[4]) < (plano.h || 1920) * 0.5; });
149
+ if (emCima.length && (c.tituloDesce || 0) < 0.05) { c.tituloDesce = Math.round(((c.tituloDesce || 0) + 0.012) * 1000) / 1000; mudou.push(`cena ${k}: o título desce ${Math.round(c.tituloDesce * 1000) / 10}% (${emCima[0]})`); }
150
+ const outros = l.filter((x) => !/ambiente/.test(x) && !/herói pequeno|texto principal pequeno/.test(x) && !/^guarda: /.test(x) && !(emCima.includes(x) && (c.tituloDesce || 0) <= 0.05));
147
151
  if (outros.length) {
148
152
  if ((c.ajuste || 1) <= 0.9 || c.semRotulos === undefined && (c.ajuste || 1) < 1) { // 2ª vez: some o que é opcional (rótulos e enfeite) em vez de encolher o texto até ficar ilegível
149
153
  if (!c.semRotulos) { c.semRotulos = true; c.semAmbiente = true; mudou.push(`cena ${k}: sem rótulos opcionais e sem enfeite (${outros[0]})`); }
@@ -179,6 +183,23 @@ function corrigir(plano, r, { tentativa = 1, paletaFundo = null } = {}) {
179
183
  c.f = Math.round(Math.min(2.4, Math.max((c.f || 1) * 1.2, (t.entrou * (c.f || 1)) / (0.36 * c.dur))) * 100) / 100;
180
184
  mudou.push(`cena ${t.cena}: entrada tardia (${t.entrou} s de ${t.dur} s), acelera ${c.f}×`);
181
185
  }
186
+ // a guarda do kit não achou saída: a peça troca (a foto encolhe o título antes; na 2ª vez vira herói)
187
+ for (const k of Object.keys(por)) {
188
+ const c = plano.cenas[Number(k)];
189
+ if (!c || !por[k].some((x) => /^guarda: troca a peça/.test(x))) continue;
190
+ if (c.tipo === 'foto' && !c.trocouGuarda) { c.trocouGuarda = true; c.ajuste = Math.round((c.ajuste || 1) * 0.85 * 100) / 100; mudou.push(`cena ${k}: guarda sem saída, título ${Math.round(c.ajuste * 100)}%`); }
191
+ else if (c.tipo !== 'cinetico') { mudou.push(`cena ${k}: guarda sem saída, ${c.tipo} vira cinetico`); c.tipo = 'cinetico'; c.elemento = false; }
192
+ }
193
+ // o olho depois do render (conferencia-visual.js): texto colado na borda, sobre o rosto, encavalado; caixa sobreposta ou fora da zona
194
+ const vistas = new Set();
195
+ for (const p of (r.pixels && r.pixels.visual) || []) {
196
+ const c = plano.cenas[p.cena];
197
+ if (!c || vistas.has(p.cena + p.tipo)) continue;
198
+ vistas.add(p.cena + p.tipo);
199
+ if (p.tipo === 'rosto' && c.tipo === 'foto') { c.fotoMenor = Math.round((c.fotoMenor || 1) * 0.9 * 100) / 100; mudou.push(`cena ${p.cena}: ${p.texto}; foto ${Math.round(c.fotoMenor * 100)}%`); }
200
+ else if (p.tipo === 'sobreposicao' && !c.semAmbiente) { c.semAmbiente = true; c.semRotulos = true; mudou.push(`cena ${p.cena}: ${p.texto}; sem enfeite e sem rótulos opcionais`); }
201
+ else { c.ajuste = Math.round((c.ajuste || 1) * 0.9 * 100) / 100; mudou.push(`cena ${p.cena}: ${p.texto}; corpo ${Math.round(c.ajuste * 100)}%`); }
202
+ }
182
203
  for (const b of r.bordas || []) {
183
204
  const c = plano.cenas[b.cena];
184
205
  if (!c) continue;
@@ -68,6 +68,25 @@ function paletaDoAssunto(assunto) {
68
68
  return achou || PALETAS[hashTexto(a) % PALETAS.length];
69
69
  }
70
70
 
71
+ /** Saturação (HSL, 0–1) e luminosidade de uma cor: separa cor de verdade de preto, branco e cinza. */
72
+ function saturacao(h) {
73
+ const c = (rgb(h) || [0, 0, 0]).map((v) => v / 255);
74
+ const mx = Math.max(...c), mn = Math.min(...c), l = (mx + mn) / 2;
75
+ if (mx === mn) return { s: 0, l };
76
+ return { s: (mx - mn) / (1 - Math.abs(2 * l - 1)), l };
77
+ }
78
+ /**
79
+ * O cartão EM DESTAQUE de uma cena (o último da lista, o painel, a barra que ganha). Todo cartão vem da paleta da cena:
80
+ * o destaque só vira cartão quando é COR (saturada, nem quase preta nem quase branca); senão — o fundo de destaque
81
+ * ganha a tinta escura como acento e o cartão saía PRETO — o cartão é o brilho (o fundo puxado para o acento), e
82
+ * o texto em cima é o que mais contrasta com ele.
83
+ */
84
+ function cartaoDestaque(bg, fg, ac, brilho) {
85
+ const { s, l } = saturacao(ac);
86
+ const cor = s >= 0.35 && l >= 0.18 && l <= 0.88 ? ac : brilho;
87
+ return { cardAc: cor, sobreCardAc: melhorCor(cor, [fg, bg, '#FFFFFF', '#14110F']) };
88
+ }
89
+
71
90
  /** As 3 variações da paleta: normal, fundo profundo e fundo de destaque, sempre com contraste de leitura. */
72
91
  function variacoes(base) {
73
92
  const fundo = base.fundo, tinta = base.tinta, dest = base.destaque;
@@ -77,7 +96,8 @@ function variacoes(base) {
77
96
  if (contraste(fg, bg) < 4.5) fg = melhorCor(bg, ['#FFFFFF', '#111111']);
78
97
  if (contraste(ac, bg) < 3) ac = melhorCor(bg, [dest, fg, tinta, fundo]);
79
98
  const mudo = misturar(bg, fg, 0.66);
80
- return { bg, fg, ac, mudo: contraste(mudo, bg) >= 3 ? mudo : fg, card: misturar(bg, fg, 0.1), brilho: misturar(bg, ac, 0.32) };
99
+ const brilho = misturar(bg, ac, 0.32);
100
+ return { bg, fg, ac, mudo: contraste(mudo, bg) >= 3 ? mudo : fg, card: misturar(bg, fg, 0.1), brilho, ...cartaoDestaque(bg, fg, ac, brilho) };
81
101
  };
82
102
  return {
83
103
  normal: fix(fundo, tinta, dest),
@@ -114,7 +134,7 @@ function normalizarCena(c, i) {
114
134
  return diretor.completar(c, tipo, { assunto: c.titulo || '', meta: diretor.METAFORAS.geral, proxima: null });
115
135
  }
116
136
 
117
- const maxDur = (tipo) => (tipo === 'celular' || tipo === 'prompt' ? 5 : 4.2);
137
+ const maxDur = (tipo) => (tipo === 'celular' || tipo === 'prompt' ? 5 : tipo === 'foto' ? 4.8 : 4.2);
118
138
 
119
139
  /**
120
140
  * Do pedido ao PLANO que o kit entende (sem rede nem disco; o histórico de assinaturas vem por `opc.historico`).
@@ -128,10 +148,15 @@ function plano(args = {}, opc = {}) {
128
148
  const [w, h] = FORMATOS[formato];
129
149
  const alvo = Math.max(6, Math.min(60, Number(args.duracao) || duracaoDaFicha(ficha) || 15));
130
150
  const elementos = opc.elementos || [];
131
- const dir = diretor.dirigir({ ...args, duracao: alvo }, { historico: opc.historico || [], semente: opc.semente != null ? opc.semente : null, temElemento: opc.elementos ? opc.elementos.length > 0 : true });
151
+ // só o que mora no projeto vai pro plano: o caminho de origem (`de`) fica de fora, pra nada apontar pra fora da pasta
152
+ const fotos = Array.isArray(opc.fotos) ? opc.fotos.filter((f) => f && f.arquivo).map(({ de, ...resto }) => resto) : [];
153
+ const dir = diretor.dirigir({ ...args, duracao: alvo, fotos }, { historico: opc.historico || [], semente: opc.semente != null ? opc.semente : null, temElemento: opc.elementos ? opc.elementos.length > 0 : true });
132
154
  const cenas = dir.cenas;
155
+ // a cena "foto" leva a foto preparada (arquivo no projeto, fundo desfocado, recorte) e o passeio que o diretor sorteou
156
+ cenas.forEach((c) => { if (c.tipo === 'foto') { const f = fotos[(c.foto || 0) % Math.max(1, fotos.length)]; if (f) c.foto = { ...f }; else c.foto = null; } });
133
157
 
134
158
  // o ritmo: sem pedido de duração por cena, ajusta ao alvo (cada cena entre 1,4 e 4,2 s)
159
+ cenas.forEach((c) => { if (c.tipo === 'foto') c.dur = Math.min(maxDur('foto'), c.dur * 1.5); }); // a foto precisa de tempo pro passeio
135
160
  const soma = cenas.reduce((s, c) => s + c.dur, 0);
136
161
  if (!args.cenas || args.duracao) {
137
162
  const k = alvo / soma;
@@ -169,7 +194,7 @@ function plano(args = {}, opc = {}) {
169
194
  const duracao = Math.round(cenas.reduce((s, c) => s + c.dur, 0) * 100) / 100;
170
195
  cenas.forEach((c, i) => { c.inicio = Math.round(cenas.slice(0, i).reduce((s, x) => s + x.dur, 0) * 1000) / 1000; });
171
196
  return { w, h, fps: 30, formato, fontes, tipo, paleta, trocas: dir.trocas, cenas, ajuste: 1, receita: receita ? receita.id : null, duracao,
172
- camera: dir.camera, elementos, bpm: 0, voz: null, legenda: null, direcao: dir.assinatura, semente: dir.semente, variacoes: v };
197
+ camera: dir.camera, elementos, fotos, bpm: 0, voz: null, legenda: null, direcao: dir.assinatura, semente: dir.semente, variacoes: v };
173
198
  }
174
199
 
175
200
  /** A voz manda: cada cena começa na 1ª palavra da fala dela e dura até a próxima. `voz` = resultado de falarEMedir. */
@@ -226,9 +251,9 @@ function ajustarCortes(pl, batidas, { tolerancia = 0.28 } = {}) {
226
251
  }
227
252
 
228
253
  const lerKit = (n) => fs.readFileSync(path.join(KIT, n), 'utf8');
229
- /** O JSX completo: o kit, as primitivas, as cenas e a chamada `montar(PLANO)`. */
254
+ /** O JSX completo: o kit, a guarda de layout, as primitivas, as cenas e a chamada `montar(PLANO)`. */
230
255
  function script(pl) {
231
- return lerKit('base.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
256
+ return lerKit('base.jsx') + '\n' + lerKit('guarda.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
232
257
  }
233
258
  /** Os tempos medidos de cada cena (início de cada uma e o instante de conferir). */
234
259
  function tempos(pl) {
@@ -367,6 +392,18 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
367
392
  const historico = deps.historico || diretor.lerHistorico(arqHist);
368
393
  const assunto = limpa(args.assunto || (args.cenas[0] && (args.cenas[0].titulo || args.cenas[0].texto)) || '', 160);
369
394
 
395
+ // 0. as fotos do pedido: validadas, copiadas pro projeto, com fundo desfocado e recorte (uma vez, antes do render)
396
+ const pedidasFotos = Array.isArray(args.fotos) ? args.fotos : (args.fotos ? [args.fotos] : []);
397
+ let fotos = [], fotosRecusadas = [];
398
+ if (pedidasFotos.length) {
399
+ const F = require('./maquina-foto.js');
400
+ const pf = await F.preparar({ fotos: pedidasFotos, pasta, deps: deps.foto || {} });
401
+ fotos = pf.fotos; fotosRecusadas = pf.recusadas;
402
+ // a pessoa mandou foto (em `fotos`) e NENHUMA serve: diz o porquê em vez de entregar um vídeo sem a foto dela.
403
+ // As fotos achadas no texto do pedido (fotosImplicitas) só avisam: o vídeo sai sem elas.
404
+ if (!fotos.length && !args.fotosImplicitas) return { ok: false, error: 'nenhuma das fotos serve: ' + fotosRecusadas.map((x) => x.motivo).join(' '), fotosRecusadas };
405
+ }
406
+
370
407
  // 1. os elementos reais do assunto
371
408
  const meta = diretor.achar(assunto + ' ' + (args.metafora || ''));
372
409
  const pedidos = Array.isArray(args.elementos) ? args.elementos : (args.elementos ? [args.elementos] : []);
@@ -378,8 +415,9 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
378
415
  // o gosto salvo da pessoa entra como PESO no sorteio do diretor (não como molde)
379
416
  const G = require('./gosto.js');
380
417
  const gostoP = deps.gosto !== undefined ? deps.gosto : G.pesos(G.ler());
381
- let pl = plano({ ...args, assunto, gosto: gostoP }, { receita, historico, elementos });
382
- const avisosDeMotor = [];
418
+ let pl = plano({ ...args, assunto, gosto: gostoP }, { receita, historico, elementos, fotos });
419
+ const avisosDeMotor = fotosRecusadas.map((x) => 'foto recusada: ' + x.motivo);
420
+ if (fotos.length && !pl.cenas.some((c) => c.tipo === 'foto')) avisosDeMotor.push('as fotos não entraram em nenhuma cena (o roteiro fixou todas as cenas com outro tipo).');
383
421
  if (elementos.length < 3) avisosDeMotor.push(`só ${elementos.length} elemento(s) real(is) do assunto (o pedido é ≥ 3)`);
384
422
 
385
423
  // 3. a narração (opcional) manda no tempo
@@ -438,7 +476,12 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
438
476
  };
439
477
  const lerAudit = (saida) => String(saida || '').split('\n').filter((l) => /^AUDIT \{/.test(l)).map((l) => { try { return JSON.parse(l.slice(6)); } catch { return null; } }).filter(Boolean);
440
478
  const tm0 = tempos(pl);
479
+ // a GUARDA de layout: antes de cada render, o plano é conferido e consertado (maquina-guarda.js: a cena "foto" sai planejada com a
480
+ // fonte medida); no kit, o fecha() de cada cena roda a mesma guarda com as caixas reais. Depois do render, o olho (Vision) confere.
481
+ const GU = require('./maquina-guarda.js'), CV = require('./conferencia-visual.js');
482
+ let guardaPlano = [], guardaKit = [], olho = null;
441
483
  for (let tentativa = 1; tentativa <= 5; tentativa++) {
484
+ guardaPlano = GU.guardar(pl);
442
485
  const tm = tempos(pl);
443
486
  // quadros: 1 por cena (no instante de ler) + 2 em cada troca (+0,08 s e +0,22 s) para pegar borda de comp aparecendo e borrão
444
487
  const trTempos = []; pl.cenas.slice(1).forEach((c) => trTempos.push(Math.round((c.inicio + 0.08) * 100) / 100, Math.round((c.inicio + 0.22) * 100) / 100));
@@ -460,11 +503,16 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
460
503
  quadrosCena.forEach((q, i) => { const px = layout.lerPixels(q); if (!px) return; nitidezCena.push(layout.nitidez(px)); const fb = layout.faixaDeBorda(px); if (fb) pxChecks.bordaDaComp.push({ cena: i, ...fb }); const pc = pl.cenas[i].pal; const so = layout.solidosForaDaPaleta(px, [pc.bg, pc.fg, pc.ac, pc.mudo, pc.card]); if (so >= 0.025) pxChecks.solidoForaDaPaleta.push({ cena: i, fracao: Math.round(so * 1000) / 1000 }); });
461
504
  const nitMed = nitidezCena.slice().sort((a, b) => a - b)[Math.floor(nitidezCena.length / 2)] || 0;
462
505
  quadrosTroca.forEach((q, k) => { const px = layout.lerPixels(q); if (!px) return; const cutIdx = Math.floor(k / 2) + 1; const nt = layout.nitidez(px); if (nitMed > 0 && layout.varLum(px) > 400 && nt < nitMed * 0.01) pxChecks.borrao.push({ cena: cutIdx, nitidez: nt, referencia: nitMed }); });
506
+ guardaKit = CV.lerGuarda(r.saida);
507
+ olho = CV.conferir({ quadros: quadrosCena, pl, guarda: guardaKit, visao: deps.visao });
508
+ pxChecks.visual = olho.problemas;
463
509
  const graves = c.baixoContraste.filter((b) => b.contraste < 3);
464
510
  const contagem = classificar(avisos, auditorias, entradas, vazios, c.baixoContraste, tardias);
465
511
  contagem.bordaDaComp = pxChecks.bordaDaComp.length; contagem.solidoForaDaPaleta = pxChecks.solidoForaDaPaleta.length; contagem.borrao = pxChecks.borrao.length;
512
+ contagem.visual = olho.problemas.length; contagem.consertosDaGuarda = guardaPlano.reduce((n, g) => n + g.log.filter((x) => !/^ok:/.test(x)).length, 0) + guardaKit.reduce((n, g) => n + g.log.length, 0);
466
513
  passadas.push({ passada: tentativa, ...contagem });
467
- conferencia = { tentativas: tentativa, quadros: (r.quadros || []).length, pixels: pxChecks, vazios, baixoContraste: c.baixoContraste, bordas: c.bordas, avisos, auditoria: auditorias, elementosNaCena: nReais, entradas: entradas.map((x) => ({ cena: x.cena, entrou: Math.round(x.max / (x.f || 1) * 100) / 100, dur: x.dur, f: x.f })), entradaTardia: tardias, ok: !vazios.length && !graves.length && !avisos.length && !tardias.length && !pxChecks.bordaDaComp.length && !pxChecks.solidoForaDaPaleta.length && !pxChecks.borrao.length };
514
+ conferencia = { tentativas: tentativa, quadros: (r.quadros || []).length, pixels: pxChecks, vazios, baixoContraste: c.baixoContraste, bordas: c.bordas, avisos, auditoria: auditorias, elementosNaCena: nReais, entradas: entradas.map((x) => ({ cena: x.cena, entrou: Math.round(x.max / (x.f || 1) * 100) / 100, dur: x.dur, f: x.f })), entradaTardia: tardias, ok: !vazios.length && !graves.length && !avisos.length && !tardias.length && !pxChecks.bordaDaComp.length && !pxChecks.solidoForaDaPaleta.length && !pxChecks.borrao.length && !olho.problemas.length,
515
+ guarda: { plano: guardaPlano, kit: guardaKit.filter((g) => g.log.length).map((g) => ({ cena: g.cena, log: g.log })), olho: olho.olho } };
468
516
  if (conferencia.ok || tentativa === 5) break;
469
517
  const mudou = layout.corrigir(pl, { avisos, vazios, baixoContraste: c.baixoContraste, bordas: [], tardias, pixels: pxChecks }, { tentativa, paletaFundo: pl.variacoes && pl.variacoes.fundo });
470
518
  historicoDeTentativas.push({ tentativa, mudou });
@@ -505,10 +553,11 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
505
553
  try { sv = require('./editor.js').salvarSpec(pasta, { pl, args, trilha: trilha && { arquivo: trilha.arquivo, inicio: trilhaInicio }, voz: voz && { mp3: voz.mp3 }, video, previa: !!args.previa }) || {}; } catch { /* sem versão */ }
506
554
  return {
507
555
  ok: true, video: sv.video || video, versao: sv.versao, previa: !!args.previa, quadros: r.quadros || [], folha, duracao: dur,
508
- lufs: mix.ok ? mix.lufs : null, formato: pl.formato, cenas: pl.cenas.map((c) => ({ tipo: c.tipo, dur: c.dur, papel: c.papel, camera: c.camera })),
556
+ lufs: mix.ok ? mix.lufs : null, formato: pl.formato, cenas: pl.cenas.map((c) => ({ tipo: c.tipo, dur: c.dur, papel: c.papel, camera: c.camera, ...(c.tipo === 'foto' ? { movimento: c.movimento } : {}) })),
509
557
  trocas: pl.trocas, tipografia: pl.tipo.nome, camera: pl.camera, paleta: pl.direcao.paleta,
510
558
  sons: eventos.length, sonsBrutos: eventosBrutos.length, sfxReais, mix: mix.mix || null, trilha: trilha ? { titulo: trilha.titulo, autor: trilha.autor, creditos: trilha.creditos, bpm: trilha.bpm, nota: trilha.nota, lufs: trilha.lufs, inicio: trilhaInicio, cortesNaBatida: analise && analise.cortesNaBatida, pessoa: !!trilha.pessoa } : null, semMusica,
511
559
  voz: !!voz, vozMotor: voz && voz.motor, vozMedida: voz && voz.medido, conferencia, receita: pl.receita, projeto: r.projeto, assinatura,
560
+ fotos: fotos.map((f) => ({ arquivo: path.relative(pasta, f.arquivo).replace(/\\/g, '/'), recorte: !!f.recorte, moldura: !!f.moldura })), fotosRecusadas,
512
561
  elementos: elementos.map((e) => ({ nome: e.nome, tipo: e.tipo, licenca: e.licenca })), creditos: (el && el.creditos) || [],
513
562
  avisos: avisosDeMotor,
514
563
  ...(gostoP && gostoP.overshoot != null ? { gosto: { overshoot: gostoP.overshoot } } : {}),
@@ -517,4 +566,4 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
517
566
  };
518
567
  }
519
568
 
520
- module.exports = { filtrarSons, FORMATOS, TIPOS, TROCAS, PALETAS, plano, aplicarVoz, ajustarCortes, roteiroPadrao, normalizarCena, paletaDoAssunto, variacoes, contraste, script, tempos, lerSons, lerAvisos, lerElementos, lerEntradas, mixar, variancia, medirLufs, motion, marcarChave, fontesDoPlano, repartirFala, LIMIAR_VAZIO };
569
+ module.exports = { cartaoDestaque, saturacao, filtrarSons, FORMATOS, TIPOS, TROCAS, PALETAS, plano, aplicarVoz, ajustarCortes, roteiroPadrao, normalizarCena, paletaDoAssunto, variacoes, contraste, script, tempos, lerSons, lerAvisos, lerElementos, lerEntradas, mixar, variancia, medirLufs, motion, marcarChave, fontesDoPlano, repartirFala, LIMIAR_VAZIO };
@@ -169,7 +169,7 @@ function instrucaoDasImagens(lista = IMAGENS) {
169
169
  return `[REFERÊNCIA VISUAL] A pessoa mandou ${lista.length} imagem(ns): ${lista.map((f) => require('path').basename(f)).join(', ')}. `
170
170
  + 'Elas SÃO a referência do pedido, mesmo em "só motion". Se o pedido é vídeo, o Estúdio já guarda cada uma no projeto e tira as '
171
171
  + 'cores dela (com site, as cores do site mandam). Escreva o roteiro seguindo o que a referência mostra e diga no fim, '
172
- + 'em uma linha, o que dela o vídeo usou.';
172
+ + 'em uma linha, o que dela o vídeo usou. No estudio_motion a foto também entra NA CENA (peça "foto": fundo desfocado, foto ou recorte da pessoa em 3D, passeio de câmera); passe os caminhos em "fotos" ou deixe o Estúdio pegar os do pedido.';
173
173
  }
174
174
 
175
175
  module.exports = { definir, atual, limpar, semImagem, termos, entender, inferir, instrucao, dominios, filtrar,