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
package/lib/entrega.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * entrega.js — relatório grande não se perde: corta e guarda o resto.
3
+ *
4
+ * Visto no pi-desktop (vastsa, LGPL-3.0; só a ideia, código nosso): o relatório
5
+ * do subagente tem teto, e o que passa do teto vai para um arquivo, com o
6
+ * caminho no fim do texto. Antes, o `entregaDoAgente` fazia `.slice(0, 4000)`
7
+ * e o resto sumia: o especialista escrevia 9 mil caracteres de achados e o
8
+ * agente principal lia a metade, sem saber que faltava metade.
9
+ *
10
+ * O corte cai no fim de um parágrafo (ou de uma linha), nunca no meio de uma
11
+ * palavra. O arquivo mora na pasta temporária e é liberado para leitura, então
12
+ * o `read_file` do agente principal alcança.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const path = require('path');
20
+
21
+ const TETO = 4000;
22
+
23
+ function pontoDeCorte(texto, teto) {
24
+ const trecho = texto.slice(0, teto);
25
+ for (const sep of ['\n\n', '\n', '. ']) {
26
+ const i = trecho.lastIndexOf(sep);
27
+ if (i > teto * 0.6) return i + (sep === '. ' ? 1 : 0);
28
+ }
29
+ return teto;
30
+ }
31
+
32
+ /**
33
+ * @returns {{texto: string, arquivo: string|null, cortado: boolean}}
34
+ * `gravar(texto, quem)` devolve o caminho do arquivo (injetável no teste).
35
+ */
36
+ function limitar(texto, { teto = TETO, quem = 'agente', gravar = gravarNaTemp } = {}) {
37
+ const t = String(texto == null ? '' : texto).trim();
38
+ if (t.length <= teto) return { texto: t, arquivo: null, cortado: false };
39
+ const corte = pontoDeCorte(t, teto);
40
+ let arquivo = null;
41
+ try { arquivo = gravar(t, quem); } catch { /* sem disco: corta e avisa mesmo assim */ }
42
+ const aviso = arquivo
43
+ ? `\n\n(…relatório cortado: ${t.length} caracteres, faltam ${t.length - corte}. O texto inteiro está em ${arquivo} — leia com read_file se precisar do resto.)`
44
+ : `\n\n(…relatório cortado: ${t.length} caracteres, faltam ${t.length - corte}. Não deu para guardar o resto; peça de novo ao agente, mais curto.)`;
45
+ return { texto: t.slice(0, corte).trimEnd() + aviso, arquivo, cortado: true };
46
+ }
47
+
48
+ function gravarNaTemp(texto, quem) {
49
+ const dir = path.join(os.tmpdir(), 'primocode-entregas');
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ const nome = `${Date.now()}-${String(quem).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) || 'agente'}.md`;
52
+ const arquivo = path.join(dir, nome);
53
+ fs.writeFileSync(arquivo, texto, 'utf8');
54
+ try { require('./tools').liberarLeitura(dir); } catch { /* sem tools: o arquivo existe do mesmo jeito */ }
55
+ return arquivo;
56
+ }
57
+
58
+ module.exports = { limitar, TETO };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * conferencia-visual.js — o olho DEPOIS do render: o quadro de cada cena, lido de verdade.
3
+ *
4
+ * conferir({quadros, pl, guarda, visao}) → {olho: 'vision'|'caixas', problemas: [{cena, tipo, texto}], leituras}
5
+ *
6
+ * Com o Vision do macOS (lib/estudio/visao.js): o texto que aparece no quadro (OCR) e os rostos. Reprova:
7
+ * borda texto a menos de 3% da beira do quadro (colado na borda)
8
+ * rosto texto em cima de um rosto
9
+ * encavalado duas linhas de texto uma em cima da outra
10
+ * Sempre (com ou sem o Vision): as caixas finais da guarda do kit (linhas "GUARDA {json}") conferidas de novo
11
+ * (maquina-guarda.verificarCaixas): sobreposição e fora da zona segura. Sem Vision, é só isso ("caixas").
12
+ * Puro na decisão (julgarLeitura): testável sem quadro nem Vision.
13
+ */
14
+ 'use strict';
15
+
16
+ const G = require('./maquina-guarda.js');
17
+
18
+ const LIM = { borda: 0.03, conf: 0.5, alturaMin: 0.012, rostoMin: 0.004 };
19
+ const area = (c) => Math.max(0, c[2] - c[0]) * Math.max(0, c[3] - c[1]);
20
+ const inter = (a, b) => Math.max(0, Math.min(a[2], b[2]) - Math.max(a[0], b[0])) * Math.max(0, Math.min(a[3], b[3]) - Math.max(a[1], b[1]));
21
+
22
+ /** Os problemas de UM quadro lido pelo Vision ({w, h, textos:[{texto, conf, caixa}], rostos:[caixa]}). */
23
+ function julgarLeitura(q, cena, { foto = null } = {}) {
24
+ const out = [];
25
+ if (!q || !q.ok || !q.w || !q.h) return out;
26
+ const W = q.w, H = q.h;
27
+ const textos = (q.textos || []).filter((t) => t.conf >= LIM.conf && (t.caixa[3] - t.caixa[1]) >= H * LIM.alturaMin && String(t.texto).replace(/[^A-Za-zÀ-ÿ0-9]/g, '').length >= 2);
28
+ // com a caixa da foto (já na escala do quadro), só vale rosto DENTRO dela: o fundo desfocado é a mesma foto ampliada e também tem rosto
29
+ const naFoto = (r) => !foto || ((r[0] + r[2]) / 2 >= foto[0] && (r[0] + r[2]) / 2 <= foto[2] && (r[1] + r[3]) / 2 >= foto[1] && (r[1] + r[3]) / 2 <= foto[3]);
30
+ const rostos = (q.rostos || []).filter((r) => area(r) >= W * H * LIM.rostoMin && naFoto(r));
31
+ for (const t of textos) {
32
+ const c = t.caixa;
33
+ if (c[0] < W * LIM.borda || c[2] > W * (1 - LIM.borda) || c[1] < H * LIM.borda || c[3] > H * (1 - LIM.borda)) out.push({ cena, tipo: 'borda', texto: `texto colado na borda: "${t.texto}"` });
34
+ for (const r of rostos) if (inter(c, r) > 0.15 * area(c)) out.push({ cena, tipo: 'rosto', texto: `texto sobre o rosto: "${t.texto}"` });
35
+ }
36
+ for (let i = 0; i < textos.length; i++) for (let j = i + 1; j < textos.length; j++) {
37
+ const a = textos[i].caixa, b = textos[j].caixa;
38
+ if (inter(a, b) > 0.3 * Math.min(area(a), area(b))) out.push({ cena, tipo: 'encavalado', texto: `textos encavalados: "${textos[i].texto}" × "${textos[j].texto}"` });
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** As linhas "GUARDA {json}" que o kit escreveu (uma por cena). */
44
+ function lerGuarda(saida) {
45
+ return String(saida || '').split('\n').filter((l) => /^GUARDA \{/.test(l)).map((l) => { try { return JSON.parse(l.slice(7)); } catch { return null; } }).filter(Boolean);
46
+ }
47
+
48
+ /** `quadros[i]` é o quadro da cena i. `guarda` = lerGuarda(saida). `visao` injetável (testes); null = sem olho. */
49
+ function conferir({ quadros = [], pl = {}, guarda = [], visao } = {}) {
50
+ const problemas = [];
51
+ const legenda = !!(pl.legenda && pl.legenda.chunks && pl.legenda.chunks.length);
52
+ for (const g of guarda) {
53
+ for (const v of G.verificarCaixas(g.caixas, g.w || pl.w, g.h || pl.h, { legenda })) problemas.push({ cena: g.cena, tipo: /fora|trilho/.test(v) ? 'zona' : 'sobreposicao', texto: v });
54
+ }
55
+ let V = visao;
56
+ if (V === undefined) { try { V = require('./visao.js'); } catch { V = null; } }
57
+ let olho = 'caixas', leituras = null;
58
+ if (V && V.disponivel() && quadros.length) {
59
+ const r = V.ler(quadros);
60
+ if (r && r.ok) {
61
+ olho = 'vision'; leituras = r.quadros;
62
+ r.quadros.forEach((q) => {
63
+ const i = quadros.indexOf(q.arquivo);
64
+ if (i < 0) return;
65
+ // a caixa da foto (layout) levada pro quadro: escala do quadro, câmera ~0,94 em volta do centro, 20% de folga
66
+ const g = guarda.find((x) => x.cena === i), fc = g && (g.caixas || []).find((c) => c.tipo === 'foto');
67
+ let foto = null;
68
+ if (fc && q.w) { const W0 = g.w || pl.w, H0 = g.h || pl.h, k = q.w / W0, cx = W0 / 2, cy = H0 / 2, e = (x, c0) => (c0 + (x - c0) * 0.94) * k; const fw = (fc.c[2] - fc.c[0]) * 0.1 * k, fh = (fc.c[3] - fc.c[1]) * 0.1 * k; foto = [e(fc.c[0], cx) - fw, e(fc.c[1], cy) - fh, e(fc.c[2], cx) + fw, e(fc.c[3], cy) + fh]; }
69
+ problemas.push(...julgarLeitura(q, i, { foto }));
70
+ });
71
+ }
72
+ }
73
+ return { olho, problemas, leituras };
74
+ }
75
+
76
+ module.exports = { conferir, julgarLeitura, lerGuarda, LIM };
@@ -77,9 +77,14 @@ const PEDE_EFEITO = /\b(tir[ae]r?|remov\w*|troc\w*)\s+(o\s+)?fundo\b|\bsepar\w*\
77
77
  arquivo de vídeo em qualquer lugar da frase é edição. */
78
78
  const EDITA_ARQUIVO = /\b(edit\w*|edi[cç][aã]o|p[oõ]e\s+(ele\s+)?pra\s+editar)\b[\s\S]*\.(mp4|mov|mkv|webm|m4v)\b|\.(mp4|mov|mkv|webm|m4v)\b[\s\S]*\b(edit\w*|edi[cç][aã]o)\b/i;
79
79
 
80
+ /* A gravação da pessoa FALANDO, sem caminho de arquivo na frase (20/10, a edição completa do
81
+ editor_pro): "edita meu vídeo", "faz um reels disso", "coloca legenda e efeitos nele". Fala de
82
+ uma gravação que JÁ existe ("disso", "meu vídeo", "nele"), não de criar um vídeo do zero. */
83
+ const PEDE_EDICAO_FALADA = /\bedit\w*\s+(o\s+|esse\s+|este\s+)?(meu\s+)?(v[ií]deo|grava[cç][aã]o)\s+(meu|que\s+eu\s+gravei)\b|\bedit\w*\s+(o\s+)?meu\s+v[ií]deo\b|\b(faz|fa[cç]a|transform\w*|vir\w*)\s+(um\s+|uma\s+|em\s+)?(reels?|tiktok|shorts?|criativo|corte)\s+(disso|desse|dessa|deste|desta|com\s+(esse|essa|este|esta|a\s+minha|o\s+meu))\b|\bcoloc\w*\s+(uma\s+)?legendas?\s+e\s+(uns\s+)?efeitos?\b|\b(legend\w*|efeitos?)\b[\s\S]{0,30}\b(no\s+meu\s+v[ií]deo|na\s+minha\s+grava[cç][aã]o|nele|nessa\s+grava[cç][aã]o)\b/i;
84
+
80
85
  function precisaDeEdicao(texto) {
81
86
  const t = String(texto || '');
82
- return PEDE_EDICAO.test(t) || PEDE_EFEITO.test(t) || EDITA_ARQUIVO.test(t);
87
+ return PEDE_EDICAO.test(t) || PEDE_EFEITO.test(t) || EDITA_ARQUIVO.test(t) || PEDE_EDICAO_FALADA.test(t);
83
88
  }
84
89
 
85
90
  // ── 2. as decisões que não são do modelo ─────────────────────────────────
@@ -154,7 +154,9 @@ function registrar({ id, assunto, caminho, formato, duracao, assinatura, entrega
154
154
  try { fs.mkdirSync(path.dirname(arq), { recursive: true }); fs.writeFileSync(arq, JSON.stringify(l.slice(-200), null, 2)); } catch { /* sem disco: segue */ }
155
155
  return item;
156
156
  }
157
- const listar = (arq) => ler(arq).filter((v) => v.caminho && fs.existsSync(v.caminho)).sort((a, b) => String(b.data).localeCompare(String(a.data)));
157
+ // mais novo primeiro; no empate de data (dois no mesmo milissegundo), vale a ordem de gravação: o último registrado vem antes
158
+ const listar = (arq) => ler(arq).map((v, i) => ({ v, i })).filter(({ v }) => v.caminho && fs.existsSync(v.caminho))
159
+ .sort((a, b) => String(b.v.data).localeCompare(String(a.v.data)) || b.i - a.i).map(({ v }) => v);
158
160
  function abrirNoSistema(alvo) {
159
161
  if (process.env.PRIMOCODE_SEM_NAVEGADOR === '1') return false; // app do Primo (voz): navegador só com clique explícito
160
162
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -11,6 +11,8 @@
11
11
  * pesos(gosto) multiplicadores pro diretor (tudo 1 = neutro)
12
12
  * resumo(gosto) 1 a 3 frases pro modelo ler
13
13
  * abrirTela() servidor local 127.0.0.1 + token; resolve {ok, gosto} | {ok:false, motivo}
14
+ * antesDoMotion() no terminal abre o navegador; no app (-p com PRIMOCODE_APP=1) escreve
15
+ * `primocode:abrir-gosto <url>` no stderr e o app (desktop ou Primo.app) abre a tela
14
16
  */
15
17
  'use strict';
16
18
 
@@ -184,9 +186,44 @@ function resumo(gosto) {
184
186
  return partes.join(' ');
185
187
  }
186
188
 
187
- /** Só pergunta no primeiro pedido, com gente do outro lado: nunca em -p, CI ou teste. */
189
+ /**
190
+ * Onde a tela de gosto abre no primeiro motion (sem gosto salvo):
191
+ * 'terminal' → no navegador, como sempre (tem terminal de verdade, fora do -p);
192
+ * 'app' → o motor roda em -p dentro do app desktop ou do Primo.app (PRIMOCODE_APP=1): ele NÃO abre navegador,
193
+ * escreve no stderr a linha de protocolo `primocode:abrir-gosto <url>` e o app abre a tela;
194
+ * null → não abre (já tem gosto, gosto:false, CI, script sem app).
195
+ */
196
+ function modoTela(args = {}, env = process.env, home, tty = !!process.stdout.isTTY) {
197
+ if (tem(home) || (args && args.gosto === false) || env.CI) return null;
198
+ if (env.PRIMOCODE_APP === '1') return 'app';
199
+ if (env.PRIMOCODE_SEM_TELA || !tty) return null;
200
+ return 'terminal';
201
+ }
202
+
203
+ /** Só pergunta no primeiro pedido, com gente do outro lado (terminal ou app): nunca em CI ou script sem app. */
188
204
  function deveAbrir(args = {}, env = process.env, home) {
189
- return !tem(home) && !(args && args.gosto === false) && !env.PRIMOCODE_SEM_TELA && !env.CI && !!process.stdout.isTTY;
205
+ return modoTela(args, env, home) !== null;
206
+ }
207
+
208
+ /** A linha que o app lê no stderr do motor. */
209
+ const PROTOCOLO = 'primocode:abrir-gosto';
210
+ const linhaProtocolo = (url) => `${PROTOCOLO} ${url}\n`;
211
+ /** A URL da linha de protocolo, só se for a tela local (127.0.0.1, porta, token hex). null no resto. */
212
+ function urlDaLinha(linha) {
213
+ const m = String(linha || '').trim().match(/^primocode:abrir-gosto (http:\/\/127\.0\.0\.1:(\d{2,5})\/\?t=([0-9a-f]{32}))$/);
214
+ return m ? m[1] : null;
215
+ }
216
+
217
+ /**
218
+ * O primeiro motion: abre a tela onde der (terminal → navegador; app → linha de protocolo pro app abrir) e espera.
219
+ * Resolve o que abrirTela resolve, ou {ok:false, motivo:'nao_abre'} sem fazer nada.
220
+ */
221
+ async function antesDoMotion(args = {}, { env = process.env, home, tty, stderr = process.stderr, abrir, timeoutMs } = {}) {
222
+ const modo = modoTela(args, env, home, tty);
223
+ if (!modo) return { ok: false, motivo: 'nao_abre' };
224
+ if (modo === 'app') return abrirTela({ home, timeoutMs, abrir: (url) => stderr.write(linhaProtocolo(url)) });
225
+ try { stderr.write(require('../ui.js').c.warn(' ⎿ Antes do primeiro motion: abri uma tela pra aprender o seu gosto (até 10 min). Feche ou pule pra seguir.') + '\n'); } catch { /* sem terminal */ }
226
+ return abrirTela({ home, timeoutMs, abrir });
190
227
  }
191
228
 
192
229
  function lerCorpo(req, limite = 20 * 1024) {
@@ -221,8 +258,12 @@ function abrirTela({ home, abrir, timeoutMs = 10 * 60 * 1000, porta = 0 } = {})
221
258
  if (u.searchParams.get('t') !== token) return dizer(403, 'token inválido', 'text/plain; charset=utf-8');
222
259
  return dizer(200, html.split('__TOKEN__').join(token).split('__DADOS__').join(dados), 'text/html; charset=utf-8');
223
260
  }
224
- if (req.method === 'POST' && (u.pathname === '/salvar' || u.pathname === '/pular')) {
261
+ if (req.method === 'POST' && (u.pathname === '/salvar' || u.pathname === '/pular' || u.pathname === '/fechar')) {
225
262
  if (req.headers['x-gosto-token'] !== token) return dizer(403, { ok: false, error: 'token inválido' });
263
+ if (u.pathname === '/fechar') { // o app fechou a janela sem salvar: o motion segue, nada é gravado (o próximo pergunta de novo)
264
+ dizer(200, { ok: true });
265
+ return terminar({ ok: false, motivo: 'fechou' });
266
+ }
226
267
  if (u.pathname === '/pular') {
227
268
  salvar({ pulou: true }, home);
228
269
  dizer(200, { ok: true });
@@ -261,4 +302,4 @@ function abrirTela({ home, abrir, timeoutMs = 10 * 60 * 1000, porta = 0 } = {})
261
302
 
262
303
  const _teste = { texto, MAX_TEXTO, aplicarNao };
263
304
 
264
- module.exports = { ESTILOS, GOSTEI, NAO_QUERO, arquivo, ler, salvar, tem, normalizar, pesos, resumo, abrirTela, deveAbrir, _teste };
305
+ module.exports = { ESTILOS, GOSTEI, NAO_QUERO, arquivo, ler, salvar, tem, normalizar, pesos, resumo, abrirTela, deveAbrir, modoTela, antesDoMotion, PROTOCOLO, linhaProtocolo, urlDaLinha, _teste };
@@ -719,11 +719,8 @@ async function ferramentaMotion(args) {
719
719
  receita = r.projeto;
720
720
  }
721
721
  // primeiro motion: aprende o gosto antes de renderizar (a pessoa pode fechar ou pular; o render segue de qualquer jeito)
722
- const gosto = require('./gosto.js');
723
- if (gosto.deveAbrir(args)) {
724
- try { console.error(require('../ui.js').c.warn(' ⎿ Antes do primeiro motion: abri uma tela pra aprender o seu gosto (até 10 min). Feche ou pule pra seguir.')); } catch { /* sem terminal */ }
725
- try { await gosto.abrirTela(); } catch { /* sem tela, segue */ }
726
- }
722
+ // no terminal abre o navegador; no app (desktop ou Primo.app, -p com PRIMOCODE_APP=1) o app abre a tela pela linha de protocolo
723
+ try { await require('./gosto.js').antesDoMotion(args); } catch { /* sem tela, segue */ }
727
724
  return require('./cota.js').comCota(() => renderMotion(args, p, receita));
728
725
  }
729
726
 
@@ -737,6 +734,12 @@ async function renderMotion(args, p, receita) {
737
734
  args.cenas = args.cenas.map((c, i) => (i === args.cenas.length - 1 && c && !c.logo ? { ...c, logo: kitM.logo } : c));
738
735
  }
739
736
  }
737
+ // As fotos do pedido: as que a IA passou em `fotos` e as que o terminal achou no texto do pedido (referencias.imagens()).
738
+ // Antes só o prepararRoteiro (Remotion) as lia: no motion elas viravam "referência" e nenhuma peça aceitava foto.
739
+ const doPedido = referencias.imagens();
740
+ const dadas = Array.isArray(args.fotos) ? args.fotos : (args.fotos ? [args.fotos] : []);
741
+ const todas = [...new Set([...dadas, ...doPedido])];
742
+ if (todas.length) args = { ...args, fotos: todas, ...(dadas.length ? {} : { fotosImplicitas: true }) };
740
743
  const r = await require('./maquina.js').motion(args, { pasta: p.pasta, receita, deps: { narrar } });
741
744
  if (r.ok) projeto.anotar(p.id, 'motion', `${r.duracao}s, ${r.cenas.length} cenas, ${r.lufs} LUFS`);
742
745
  if (r.ok) entregar(args, p, r);
@@ -259,13 +259,27 @@ function kAtmosfera(comp, P) {
259
259
  }
260
260
 
261
261
  /* O título é SEMPRE a camada de cima da cena (nada atravessa a caixa dele): as palavras vão para o topo da pilha. */
262
- function kTituloPorCima() {
262
+ function kTituloPorCima(comp) {
263
263
  var i, T = KIT.titulosL || [];
264
+ if (comp && T.length && KIT.tituloB != null) kPlacaDoTitulo(comp);
264
265
  for (i = T.length - 1; i >= 0; i--) { try { T[i].moveToBeginning(); } catch (e) { } }
265
266
  KIT.titulosL = [];
266
267
  }
268
+ /* A GUARDA do título sobre peça que sangra (mapa, parede, feed, pan, zoom: passam por trás do título de propósito): uma placa da cor do
269
+ fundo da cena atrás da faixa do título, com a borda de baixo em degradê. O título nunca fica em cima de rua, grade ou cartão. */
270
+ function kPlacaDoTitulo(comp) {
271
+ var i, sangra = false, c = KIT.cams[comp.name];
272
+ for (i = 1; i <= comp.numLayers && !sangra; i++) { var L = comp.layer(i); if (/^(mapa|parede|feed|pan|zoom)/.test(L.name) && L.parent && kEhCam(L.parent)) sangra = true; }
273
+ if (!sangra || !c || !c.camNo || !KIT.pal) return;
274
+ var W = comp.width, H = comp.height, u = Math.min(W, H) / 1080, B = KIT.tituloB, top = -H * 0.2;
275
+ var pl = forma(comp, 'retangulo', { x: W / 2, y: (top + B) / 2, w: W * 1.6, h: B - top, cor: KIT.pal.bg, parent: c.camNo, nome: 'fundo-titulo' });
276
+ var deg = forma(comp, 'retangulo', { x: W / 2, y: B + 45 * u, w: W * 1.6, h: 90 * u, cor: KIT.pal.bg, alfa: 55, parent: c.camNo, nome: 'fundo-titulo-degrade' });
277
+ try { var mk = deg.property('ADBE Mask Parade').addProperty('ADBE Mask Atom'); mk.property('ADBE Mask Shape').setValue(kRetShape(-W, -60 * u, W, 20 * u, 0)); mk.property('ADBE Mask Feather').setValue([0, 50 * u]); } catch (e) { }
278
+ KIT.guardaPlaca = (KIT.guardaPlaca || 0) + 1;
279
+ writeLn('GUARDA ' + JSON.stringify({ cena: KIT.cenaIdx, w: W, h: H, log: ['título sobre peça que sangra: placa da cor do fundo atrás do título'], caixas: [] }));
280
+ }
267
281
  // fecha(comp): aplica a câmera da comp, a atmosfera (luz, flash, look) e confere o layout. Chame uma vez, depois de montar a cena.
268
- function fecha(comp) { var c = KIT.cams[comp.name]; kTituloPorCima(); kAjustaConteudo(comp, c); if (KIT.ambienteFn) KIT.ambienteFn(comp, c); if (c) kAplicaCamera(c); kAtmosfera(comp, KIT.pal || { ac: '#FFFFFF' }); kConfereCena(comp); kAuditoria(comp); }
282
+ function fecha(comp) { var c = KIT.cams[comp.name]; kTituloPorCima(comp); kAjustaConteudo(comp, c); kGuarda(comp); if (KIT.ambienteFn) KIT.ambienteFn(comp, c); if (c) kAplicaCamera(c); kAtmosfera(comp, KIT.pal || { ac: '#FFFFFF' }); kConfereCena(comp); kAuditoria(comp); }
269
283
 
270
284
  // ---------------------------------------------------------------- formas
271
285
  function kMascaraRect(L, x0, y0, x1, y1, r) {
@@ -507,7 +521,7 @@ function texto(comp, str, o) {
507
521
  if (seg) { // encaixa na área segura: 8% nos lados; no 9:16 respeita as faixas de UI do Reels (14% em cima, 20% embaixo)
508
522
  var zs = kZonaTexto(comp), mxp = zs.x, myp = zs.top, myb = zs.bot;
509
523
  x = Math.max(mxp + wMax / 2, Math.min(W - mxp - wMax / 2, x));
510
- y = Math.max(myp + bloco / 2, Math.min(H - myb - bloco / 2, y));
524
+ y = Math.max(myp + bloco / 2 + (o.titulo ? (KIT.tituloDesce || 0) * H : 0), Math.min(H - myb - bloco / 2, y)); // tituloDesce: a câmera levou o título pra cima da faixa do Reels
511
525
  x = Math.round(x / 8) * 8; y = Math.round(y / 8) * 8; // grade de 8 px
512
526
  }
513
527
  wMax = 0;
@@ -558,6 +572,23 @@ function kQuebra(ws, sw, maxW) {
558
572
  }
559
573
  return linhas;
560
574
  }
575
+ /* Quanto tempo (nominal) a entrada leva até a palavra estar INTEIRA na tela. 'voa' e 'letra' montam letra por letra (em ordem
576
+ aleatória no 'voa'): a palavra só se lê no fim. Quem tira a palavra da tela antes disso mostra "q … ho" no lugar de "quentinho". */
577
+ function kTempoMontagem(modo, txt) {
578
+ var n = Math.max(1, String(txt || '').replace(/\*/g, '').length);
579
+ if (modo == 'voa') return 0.3 + n * 0.06;
580
+ if (modo == 'letra') return 0.12 + n * 0.045;
581
+ if (modo == 'mascara') return 0.4;
582
+ if (modo == 'sobe') return 0.35;
583
+ return 0.2; // 'palavra': inteira desde o primeiro quadro, crescendo; aos 0,2 s já passa de 90%
584
+ }
585
+ /* A entrada que cabe na janela da palavra (entra em ti, sai em fim): a pedida, se a palavra fica inteira por `leitura` s; senão a
586
+ mais rápida que deixe ler; no último caso, 'palavra' (inteira desde o começo, só cresce). */
587
+ function kEntradaQueCabe(modo, txt, janela, leitura) {
588
+ var ord = [modo, 'mascara', 'sobe', 'palavra'], i;
589
+ for (i = 0; i < ord.length; i++) if (kTempoMontagem(ord[i], txt) + (leitura || 0) <= janela) return ord[i];
590
+ return 'palavra';
591
+ }
561
592
  function kAnimaPalavra(comp, L, modo, ti, x, y, w, tam, linha, idx, txt) {
562
593
  kMarcaEntrada(ti);
563
594
  var drift = Math.max(2, tam / 55);
@@ -680,9 +711,13 @@ function celular(comp, o) {
680
711
 
681
712
  /* botao(comp, {x,y,w,h,rotulo,cor,corTexto,parent,t0}) + botao.aperta(t): o botão que afunda e volta. */
682
713
  function botao(comp, o) {
714
+ // a pílula cabe o rótulo: o texto() não desce de 3,6% da altura (fora de aparelho), então mede ANTES e alarga (nunca texto vazando do botão)
715
+ var tamB = (o.tam || 44) * KIT.ajuste * KIT.ajusteCena; if (kEhCam(o.parent || null) || !o.parent) tamB = Math.max(tamB, comp.height * 0.036);
716
+ var wr = kMedir(comp, String(o.rotulo || 'Toque aqui'), o.fonte || (KIT.marca && KIT.marca.titulo) || 'Poppins', tamB);
717
+ o.w = Math.min(comp.width * 0.84, Math.max(o.w || 360, wr / 0.86 + tamB * 0.4)); o.h = Math.max(o.h || 110, tamB * 1.6);
683
718
  var b = forma(comp, 'pilula', { x: o.x, y: o.y, w: o.w || 360, h: o.h || 110, cor: o.cor || '#FFFFFF', parent: o.parent, nome: 'botao', entrada: o.entrada === false ? null : 'pop', t0: o.t0 || 0, quique: 0.6 });
684
719
  var tx = texto(comp, o.rotulo || 'Toque aqui', { x: o.x, y: o.y, tamanho: o.tam || 44, cor: o.corTexto || '#000000', parent: o.parent, entrada: 'sobe', t0: (o.t0 || 0) + 0.08, largura: (o.w || 360) * 0.86, maxLinhas: 1, fonte: o.fonte });
685
- return { forma: b, texto: tx, aperta: function (t) {
720
+ return { forma: b, texto: tx, w: o.w, h: o.h, aperta: function (t) {
686
721
  // 100%: o valor lido de uma propriedade com chaves é o do tempo 0 (a mola de entrada começa em 0)
687
722
  curva(kS(b), [[t, [100, 100], null, 25], [t + 0.07, [90, 90], 70, 40], [t + 0.25, [100, 100], 70, null]]);
688
723
  som(t, 'clique', 0.9);
@@ -809,9 +844,11 @@ function elemento(comp, e, o) {
809
844
  if (typeof e == 'number') e = kElem(e);
810
845
  var w = o.w || 300, h = o.h || w, L = null;
811
846
  if ((!o.parent || kEhCam(o.parent)) && w < comp.width * 0.12 && o.nome != 'ambiente') { var kk = comp.width * 0.12 / w; w *= kk; h *= kk; o.w = w; o.h = h; } // ícone de apoio ≥ 12% da largura
812
- if ((!o.parent || kEhCam(o.parent)) && o.x != null) { var ze0 = kZonaTexto(comp, false); o.x = Math.max(ze0.x + w / 2, Math.min(comp.width - ze0.x - w / 2, Math.round(o.x / 8) * 8)); o.y = Math.max(ze0.top + h / 2, Math.min(comp.height - ze0.bot - h / 2, Math.round(o.y / 8) * 8)); if (o.nome != 'ambiente') KIT.rects.push({ espaco: (o.parent && o.parent.name == 'CONTEUDO') ? 'c' : 't', cena: KIT.cenaIdx, x0: o.x - w / 2, x1: o.x + w / 2, y0: o.y - h / 2, y1: o.y + h / 2, t0: 0, t1: 1, texto: false, sobrepoe: true, nome: 'elemento ' + (e && e.nome || '') }); }
847
+ var rectEl = null, fx = o.folgaX || 0, fy = o.folgaY || 0; // folga: o balanço do elemento (a guarda reserva o envelope inteiro)
848
+ if ((!o.parent || kEhCam(o.parent)) && o.x != null) { var ze0 = kZonaTexto(comp, false); o.x = Math.max(ze0.x + w / 2, Math.min(comp.width - ze0.x - w / 2, Math.round(o.x / 8) * 8)); o.y = Math.max(ze0.top + h / 2, Math.min(comp.height - ze0.bot - h / 2, Math.round(o.y / 8) * 8)); if (o.nome != 'ambiente') { rectEl = { espaco: (o.parent && o.parent.name == 'CONTEUDO') ? 'c' : 't', cena: KIT.cenaIdx, x0: o.x - w / 2 - fx, x1: o.x + w / 2 + fx, y0: o.y - h / 2 - fy, y1: o.y + h / 2 + fy, t0: 0, t1: 1, texto: false, sobrepoe: true, nome: 'elemento ' + (e && e.nome || ''), camadas: [], infla: (w + 2 * fx) / w }; KIT.rects.push(rectEl); } }
813
849
  if (!e) {
814
850
  L = forma(comp, 'circulo', { x: o.x, y: o.y, w: Math.min(w, h) * 0.85, cor: o.corFallback || '#8A8A8A', parent: o.parent, nome: 'elemento-reserva' });
851
+ if (rectEl) rectEl.camadas.push(L);
815
852
  } else {
816
853
  KIT.cache = KIT.cache || {};
817
854
  var it = KIT.cache[e.arquivo];
@@ -823,9 +860,10 @@ function elemento(comp, e, o) {
823
860
  }
824
861
  if (!it) return elemento(comp, null, o);
825
862
  L = kPoeEl(comp, it, e, o, o.t0 && e.tipo == 'seq' ? o.t0 : 0);
863
+ if (rectEl) rectEl.camadas.push(L);
826
864
  if (e.tipo == 'seq' && it.duration > 0.1) { // o GIF acaba em ~1 s: repete até o fim da cena
827
865
  var n, dur = it.duration, ini = (o.t0 || 0) + dur;
828
- for (n = 0; n < 12 && ini < comp.duration; n++, ini += dur) kPoeEl(comp, it, e, o, ini);
866
+ for (n = 0; n < 12 && ini < comp.duration; n++, ini += dur) { var Lr = kPoeEl(comp, it, e, o, ini); if (rectEl) rectEl.camadas.push(Lr); }
829
867
  }
830
868
  KIT.nElem = (KIT.nElem || 0) + 1;
831
869
  }
@@ -962,7 +1000,7 @@ function kConfereCena(comp) {
962
1000
  }
963
1001
  var W = comp.width, H = comp.height, R = KIT.rects, i, j, a, b, zt = kZonaTexto(comp, true), ze = kZonaTexto(comp, false), mx = ze.x - 2, my = ze.top - 2;
964
1002
  var mine = [];
965
- for (i = 0; i < R.length; i++) if (R[i].cena == KIT.cenaIdx && !R[i].conferido) { R[i].conferido = true; mine.push(kRectLayout(R[i])); }
1003
+ for (i = 0; i < R.length; i++) if (R[i].cena == KIT.cenaIdx && !R[i].conferido && !R[i].fora) { R[i].conferido = true; mine.push(kRectLayout(R[i])); }
966
1004
  for (i = 0; i < mine.length; i++) {
967
1005
  a = mine[i];
968
1006
  if (a.banda) continue;
@@ -987,6 +1025,57 @@ function kConfereCena(comp) {
987
1025
  }
988
1026
  }
989
1027
 
1028
+ // ---------------------------------------------------------------- a guarda de layout (kit-ae/guarda.jsx), com as caixas reais
1029
+ /* kGuarda(comp): no fecha() de cada cena, ANTES do render. Junta as caixas registradas (texto medido com sourceRectAtTime, elementos
1030
+ com o envelope do balanço, a foto) e roda gConsertar: ícone sobre texto/foto/outro ícone muda de lugar, encolhe ou sai; a foto que
1031
+ encosta no texto sobe ou desce; o que não tem saída vira "AVISO cena N: guarda: troca a peça". Move por expressão (value + delta),
1032
+ que soma às chaves e ao balanço sem mexer neles. Escreve "GUARDA {json}" com o log e as caixas finais (o Node confere depois). */
1033
+ function kZonaGuarda(comp) {
1034
+ var W = comp.width, H = comp.height, z = gZona(W, H, { legenda: KIT.enqDy > 0 }), a = kParaLayout(comp, z.x0, z.y0), b = kParaLayout(comp, z.x1, z.y1), tr = null;
1035
+ if (z.trilho) { var t = kParaLayout(comp, z.trilho[0], z.trilho[1]); tr = [t.x, t.y, W * 2, H * 2]; }
1036
+ return { x0: a.x, y0: a.y, x1: b.x, y1: b.y, trilho: tr };
1037
+ }
1038
+ function kExpr(prop) { var e = ''; try { e = prop.expression || ''; } catch (er) { e = ''; } return e ? '(' + e + ')' : 'value'; }
1039
+ function kDesloca(L, dx, dy, k) {
1040
+ try {
1041
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { var p = kP(L); p.expression = 'var gv = ' + kExpr(p) + '; gv.length > 2 ? [gv[0] + ' + dx.toFixed(1) + ', gv[1] + ' + dy.toFixed(1) + ', gv[2]] : [gv[0] + ' + dx.toFixed(1) + ', gv[1] + ' + dy.toFixed(1) + ']'; }
1042
+ if (k && Math.abs(k - 1) > 0.005) { var sc = kS(L); sc.expression = 'var gs = ' + kExpr(sc) + '; gs.length > 2 ? [gs[0] * ' + k.toFixed(4) + ', gs[1] * ' + k.toFixed(4) + ', gs[2]] : [gs[0] * ' + k.toFixed(4) + ', gs[1] * ' + k.toFixed(4) + ']'; }
1043
+ } catch (e) { KIT.avisos.push('cena ' + KIT.cenaIdx + ': a guarda não conseguiu mover ' + L.name + ': ' + e); }
1044
+ }
1045
+ function kSome(L) { try { kO(L).expression = '0'; } catch (e) { } try { L.enabled = false; } catch (e2) { } }
1046
+ function kGuarda(comp) {
1047
+ if (KIT.guarda === false) return;
1048
+ var W = comp.width, H = comp.height, R = KIT.rects, itens = [], refs = [], i, j;
1049
+ for (i = 0; i < R.length; i++) {
1050
+ var r0 = R[i];
1051
+ if (r0.cena != KIT.cenaIdx || r0.banda || r0.fora) continue;
1052
+ if (r0.texto && r0.livre) continue; // texto dentro de aparelho (celular, janela): mora no desenho dele
1053
+ var r = kRectLayout(r0), tipo;
1054
+ if (r0.texto) tipo = 'texto';
1055
+ else if (r0.guarda == 'foto') tipo = 'foto';
1056
+ else if (!r0.camadas || !r0.camadas.length || KIT.cenaHeroi || (r.x1 - r.x0) / (r0.infla || 1) > W * 0.3) tipo = 'cartao';
1057
+ else tipo = 'icone';
1058
+ itens.push({ id: i, tipo: tipo, nome: tipo == 'texto' ? 'texto "' + String(r0.nome).slice(0, 24) + '"' : String(r0.nome || tipo).replace(/^elemento /, 'ícone '), c: [r.x0, r.y0, r.x1, r.y1], fixo: tipo == 'texto' || tipo == 'cartao', movel: tipo == 'foto', prio: tipo == 'icone' ? 1 : 5, infla: r0.infla || 1 });
1059
+ refs.push(r0);
1060
+ }
1061
+ if (!itens.length) return;
1062
+ var u = Math.min(W, H) / 1080;
1063
+ var res = gConsertar(itens, W, H, { zona: kZonaGuarda(comp), so: 'icones', folga: (KIT.guardaFolga || 10) * u, escalaMundo: KIT.enq * 1.03 });
1064
+ var caixas = [];
1065
+ for (j = 0; j < res.itens.length; j++) {
1066
+ var a = res.itens[j], q = refs[j], esc = (q.espaco == 'c' && KIT.remap) ? KIT.remap.s : 1, cl = q.camadas || [], m;
1067
+ caixas.push({ tipo: a.tipo, nome: a.nome, c: [Math.round(a.c[0]), Math.round(a.c[1]), Math.round(a.c[2]), Math.round(a.c[3])], fora: a.removido ? 1 : 0 });
1068
+ if (a.removido) { for (m = 0; m < cl.length; m++) kSome(cl[m]); q.fora = true; continue; }
1069
+ if (Math.abs(a.dx) > 0.5 || Math.abs(a.dy) > 0.5 || Math.abs(a.k - 1) > 0.005) {
1070
+ for (m = 0; m < cl.length; m++) kDesloca(cl[m], a.dx / esc, a.dy / esc, a.k);
1071
+ var cx = (q.x0 + q.x1) / 2 + a.dx / esc, cy = (q.y0 + q.y1) / 2 + a.dy / esc, hw = (q.x1 - q.x0) / 2 * a.k, hh = (q.y1 - q.y0) / 2 * a.k;
1072
+ q.x0 = cx - hw; q.x1 = cx + hw; q.y0 = cy - hh; q.y1 = cy + hh;
1073
+ }
1074
+ }
1075
+ if (res.trocar) KIT.avisos.push('cena ' + KIT.cenaIdx + ': guarda: troca a peça (' + res.log[res.log.length - 1] + ')');
1076
+ writeLn('GUARDA ' + JSON.stringify({ cena: KIT.cenaIdx, w: W, h: H, log: res.log, caixas: caixas, violacoes: res.violacoes }));
1077
+ }
1078
+
990
1079
  // ---------------------------------------------------------------- quando as coisas entram (a regra dos 30–40%)
991
1080
  /* kEntradaMax(comp): o instante (nominal) em que a ÚLTIMA camada de conteúdo fica visível, lido das chaves de opacidade, e o que
992
1081
  kEntrada/kAnimaPalavra registraram. Cursor, ondas, partículas e atmosfera não contam: não são elementos novos. */
@@ -1039,6 +1128,13 @@ function kCaixaMundo(L, t, cache) {
1039
1128
  for (i = 0; i < 4; i++) { X.push(m[0] * xs[i] + m[2] * ys[i] + m[4]); Y.push(m[1] * xs[i] + m[3] * ys[i] + m[5]); }
1040
1129
  return [Math.min(X[0], X[1], X[2], X[3]), Math.min(Y[0], Y[1], Y[2], Y[3]), Math.max(X[0], X[1], X[2], X[3]), Math.max(Y[0], Y[1], Y[2], Y[3])];
1041
1130
  }
1131
+ /* a caixa da setinha no mundo: o desenho dela (sourceRectAtTime da forma) com escala, pais e câmera */
1132
+ function kCaixaCursorMundo(L, t, cache) {
1133
+ var q = L.sourceRectAtTime(t, false), m = kMatMundo(L, t, cache), r = [q.left, q.top, q.left + q.width, q.top + q.height];
1134
+ var xs = [r[0], r[2], r[0], r[2]], ys = [r[1], r[1], r[3], r[3]], i, X = [], Y = [];
1135
+ for (i = 0; i < 4; i++) { X.push(m[0] * xs[i] + m[2] * ys[i] + m[4]); Y.push(m[1] * xs[i] + m[3] * ys[i] + m[5]); }
1136
+ return [Math.min(X[0], X[1], X[2], X[3]), Math.min(Y[0], Y[1], Y[2], Y[3]), Math.max(X[0], X[1], X[2], X[3]), Math.max(Y[0], Y[1], Y[2], Y[3])];
1137
+ }
1042
1138
  var KIT_SKIP_CADEIA = /^(feed|mapa|parede|pan|zoom|celular|janela|papel|carimbo|slot|carta)/;
1043
1139
  function kAuditoria(comp) {
1044
1140
  if (KIT.auditoria === false) return;
@@ -1046,25 +1142,26 @@ function kAuditoria(comp) {
1046
1142
  var cand = [];
1047
1143
  for (i = 1; i <= comp.numLayers; i++) {
1048
1144
  var L = comp.layer(i), nm = L.name;
1049
- var ehTexto = nm.indexOf('palavra ') == 0, ehEl = nm.indexOf('el ') == 0 || nm == 'elemento-reserva' || nm == 'ambiente';
1050
- if (!ehTexto && !ehEl) continue;
1145
+ var ehTexto = nm.indexOf('palavra ') == 0, ehEl = nm.indexOf('el ') == 0 || nm == 'elemento-reserva' || nm == 'ambiente', ehCur = nm == 'cursor';
1146
+ if (!ehTexto && !ehEl && !ehCur) continue;
1051
1147
  var skip = false, P = L.parent;
1052
1148
  while (P) { if (KIT_SKIP_CADEIA.test(P.name)) { skip = true; break; } P = P.parent; }
1053
1149
  var nominal = (L.comment != null && L.comment !== '') ? parseFloat(L.comment) : 0;
1054
1150
  if (isNaN(nominal)) nominal = 0;
1055
- cand.push({ L: L, nome: nm, texto: ehTexto, skip: skip, entra: nominal, amb: nm == 'ambiente' });
1151
+ cand.push({ L: L, nome: nm, texto: ehTexto, cursor: ehCur, skip: skip && !ehCur, entra: ehCur ? 0 : nominal, amb: nm == 'ambiente' });
1056
1152
  }
1057
1153
  var passo = 0.1 * f, dur = comp.duration, fim = dur - 0.55 * f, t, amostras = 0, nFora = 0, nCol = 0, vistos = {}, exs = 0;
1058
1154
  for (t = 0.3 * f; t < fim; t += passo) {
1059
1155
  var cache = {}, vis = [];
1060
1156
  for (i = 0; i < cand.length; i++) {
1061
1157
  var c = cand[i];
1062
- if (c.skip || t < c.entra + 0.5 * f) continue;
1158
+ if (c.skip || (!c.cursor && t < c.entra + 0.5 * f)) continue;
1063
1159
  try {
1064
1160
  var tr = c.L.property('ADBE Transform Group');
1065
1161
  if (tr.property('ADBE Opacity').valueAtTime(t, false) < 40) continue;
1066
1162
  if (t < c.L.inPoint || t >= c.L.outPoint) continue;
1067
- var bx = kCaixaMundo(c.L, t, cache);
1163
+ var bx = c.cursor ? kCaixaCursorMundo(c.L, t, cache) : kCaixaMundo(c.L, t, cache);
1164
+ if (c.cursor) { var bx2 = kCaixaCursorMundo(c.L, t + 0.05 * f, {}); if (Math.abs(bx2[0] - bx[0]) + Math.abs(bx2[1] - bx[1]) > 4) continue; } // em trânsito: só conta parado (no clique)
1068
1165
  if (bx[2] - bx[0] < 2 || bx[3] - bx[1] < 2) continue;
1069
1166
  var h0 = bx[3] - bx[1];
1070
1167
  vis.push({ c: c, b: bx, tinta: c.texto ? [bx[0], bx[1] + h0 * 0.14, bx[2], bx[3] - h0 * 0.14] : bx });
@@ -1075,13 +1172,22 @@ function kAuditoria(comp) {
1075
1172
  for (i = 0; i < vis.length; i++) {
1076
1173
  var a = vis[i], b = a.b;
1077
1174
  var zz = a.c.texto ? zT : zE, limX0 = zz.x0, limX1 = zz.x1, limY0 = zz.y0, limY1 = zz.y1;
1078
- if (b[0] < limX0 - 8 || b[2] > limX1 + 8 || b[1] < limY0 - 8 || b[3] > limY1 + 8) {
1175
+ if (!a.c.cursor && b[0] < limX0 - 12 || b[2] > limX1 + 12 || b[1] < limY0 - 12 || b[3] > limY1 + 12) { // 12 px (1%): a respiração da câmera passa disso e não encosta na borda
1079
1176
  foraT = true;
1080
1177
  var kf = 'f:' + a.c.nome;
1081
1178
  if (!vistos[kf] && exs < 8) { vistos[kf] = 1; exs++; KIT.avisos.push('cena ' + KIT.cenaIdx + ': ' + (a.c.texto ? 'texto' : 'elemento') + ' fora da área segura (quadro ' + (t / f).toFixed(1) + ' s): ' + a.c.nome.replace('palavra ', '') + ' [' + Math.round(b[0]) + ',' + Math.round(b[1]) + ',' + Math.round(b[2]) + ',' + Math.round(b[3]) + ']'); }
1082
1179
  }
1083
1180
  for (j = i + 1; j < vis.length; j++) {
1084
1181
  var bt = a.tinta, o2 = vis[j].tinta, ox = Math.min(bt[2], o2[2]) - Math.max(bt[0], o2[0]), oy = Math.min(bt[3], o2[3]) - Math.max(bt[1], o2[1]);
1182
+ if (a.c.cursor || vis[j].c.cursor) { // o cursor é elemento: nunca em cima de letra (com elemento, pode)
1183
+ var tx2 = a.c.cursor ? vis[j].c : a.c;
1184
+ if (tx2.texto && ox > 2 && oy > 2) {
1185
+ colT = true;
1186
+ var kq = 'q:' + tx2.nome;
1187
+ if (!vistos[kq] && exs < 8) { vistos[kq] = 1; exs++; KIT.avisos.push('cena ' + KIT.cenaIdx + ': cursor sobre o texto (quadro ' + (t / f).toFixed(1) + ' s): ' + tx2.nome.replace('palavra ', '')); }
1188
+ }
1189
+ continue;
1190
+ }
1085
1191
  if (ox > 5 && oy > 5) {
1086
1192
  var A1 = (bt[2] - bt[0]) * (bt[3] - bt[1]), A2 = (o2[2] - o2[0]) * (o2[3] - o2[1]);
1087
1193
  if (ox * oy > 0.05 * Math.min(A1, A2)) {