primocode 10.0.0 → 10.1.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 (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. package/studio/voz_kokoro.py +98 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * hibrido.js — o cinema com o kit por cima: foto real que não para, e o
3
+ * motion que parece feito à mão.
4
+ *
5
+ * "Deve funcionar como um mix da versão antiga (o reels da imersão) e a
6
+ * última versão — trabalhar juntas, absurdas, coisas que nem parece que
7
+ * foi IA que fez." (29/09)
8
+ *
9
+ * O que cada lado traz:
10
+ * do cinema (cinema.js) foto e vídeo REAIS ocupando a tela, escurecidos,
11
+ * trocando por baixo do mesmo texto, escrito como a
12
+ * pessoa escreveu, com a palavra da tese na serifa; o
13
+ * número que conta; a virada que bate no fundo; a
14
+ * marca só no fim.
15
+ * do kit (kit/*.jsx) a câmera que empurra e desliza em toda cena; a
16
+ * líquido entre blocos; grão e vinheta. A etiqueta
17
+ * da fonte, o HUD e as partículas só a pedido
18
+ * (roteiro.hud, roteiro.particulas).
19
+ *
20
+ * Três passos, e nenhum repete o que o cinema já faz:
21
+ * traduzir(roteiro) qualquer roteiro (vox, motion, composição) vira os
22
+ * tipos do cinema; o que o cinema não usa (a fonte, o
23
+ * destaque) fica guardado por parte.
24
+ * termosDeBusca(roteiro) o que procurar de b-roll para cada parte.
25
+ * enfeitar(plano, …) o kit por cima do plano que o cinema montou.
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const r2 = (n) => Math.round(n * 100) / 100;
31
+ const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
32
+ const limpo = (x) => String(x || '').replace(/\s+/g, ' ').trim();
33
+
34
+ /* O b-roll quando a parte não diz o que procurar. Genérico de propósito: é o
35
+ fundo que se move, não a prova — a prova é o texto e a foto da marca. */
36
+ const BUSCA_PADRAO = ['technology abstract light', 'people working on laptop', 'hands typing keyboard close up',
37
+ 'data center servers', 'city night timelapse', 'team meeting office', 'developer code screen', 'futuristic digital network'];
38
+
39
+ /** O roteiro de qualquer estilo → o roteiro do cinema. Devolve {roteiro, extras}. */
40
+ function traduzir(roteiro) {
41
+ const partes = (roteiro && roteiro.partes) || [];
42
+ const saida = [];
43
+ const extras = [];
44
+ // A virada: a parte marcada com climax, ou o último contraste antes do fim.
45
+ let iClimax = partes.findIndex((p) => p.climax);
46
+ if (iClimax < 0) {
47
+ for (let i = partes.length - 2; i > 0; i--) if (partes[i].tipo === 'contraste') { iClimax = i; break; }
48
+ }
49
+ partes.forEach((p, i) => {
50
+ const tipo = p.tipo || 'frase';
51
+ const marcar = [p.destaque, ...(p.marcar || [])].filter(Boolean);
52
+ const base = { fala: p.fala, ...(p.mudo ? { mudo: true } : {}), ...(p.falaForte ? { falaForte: p.falaForte } : {}), marcar };
53
+ let q;
54
+ // Os tipos que o cinema já conhece passam como estão.
55
+ if (['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'].includes(tipo)) q = { ...p };
56
+ else if (i === iClimax) q = { tipo: 'climax', linhas: [p.nao, p.sim].filter(Boolean).map(limpo), ...base };
57
+ // A abertura é o título, grande; o subtítulo vira a etiqueta digitada (a fonte).
58
+ else if (tipo === 'abertura') q = { tipo: 'montagem', linhas: [limpo(p.titulo)], ritmo: 0.6, ...base };
59
+ else if (tipo === 'titulo' || tipo === 'imagem') q = { tipo: 'montagem', linhas: [p.titulo || p.texto, p.sub].filter(Boolean).map(limpo), ...base };
60
+ else if (tipo === 'numero' || tipo === 'grafico') {
61
+ // "30% mais rápido": o cinema conta o número e escreve o resto embaixo.
62
+ q = { tipo: 'numeros', itens: [limpo(`${p.valor || ''} ${p.rotulo || p.titulo || ''}`)], ...base };
63
+ } else if (tipo === 'passos' || tipo === 'lista') {
64
+ /* Na escada, o item é o NOME ("Sonnet 4"); a data e o detalhe depois do
65
+ "·" ou do "—" estão na fala. Cinco itens de cinco palavras eram 7 s
66
+ de escada pela regra de 0,3 s por palavra. */
67
+ const itens = (p.itens || []).map((x) => limpo(typeof x === 'string' ? x : (x.titulo || x.texto || '')).split(/\s+[·—–-]\s+/)[0]).filter(Boolean);
68
+ q = { tipo: 'escada', itens: itens.slice(0, 5), ...(p.titulo ? { final: limpo(p.titulo) } : {}), ...base };
69
+ } else if (tipo === 'citacao') q = { tipo: 'frase', linhas: [`“${limpo(p.texto)}”`], ...base };
70
+ else if (tipo === 'contraste') q = { tipo: 'montagem', linhas: [p.nao, p.sim].filter(Boolean).map(limpo), ...base };
71
+ else if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') q = { tipo: 'final', linhas: [p.titulo || p.texto, p.link].filter(Boolean).map(limpo), ...base };
72
+ else q = { tipo: 'montagem', linhas: [p.titulo || p.texto || p.fala].filter(Boolean).map(limpo), ...base };
73
+ saida.push(q);
74
+ extras.push({ tipoOriginal: tipo, fonte: tipo === 'abertura' && p.sub ? p.sub : (p.fonte || (tipo === 'citacao' ? p.autor : '')), autor: p.autor || '', busca: p.busca || '' });
75
+ });
76
+ return { roteiro: { ...roteiro, partes: saida, estilo: 'hibrido' }, extras };
77
+ }
78
+
79
+ /** O que procurar de b-roll para cada parte: a busca dela, a do roteiro, ou o padrão. */
80
+ function termosDeBusca(roteiro, extras = []) {
81
+ const global = [].concat(roteiro.busca || []).filter(Boolean);
82
+ // Escopo fechado: só os termos da pessoa, nunca o padrão genérico.
83
+ const fechado = require('./referencias.js').atual().modo === 'fechado';
84
+ const lista = global.length ? global : fechado ? [] : BUSCA_PADRAO;
85
+ const termos = new Set();
86
+ (roteiro.partes || []).forEach((p, i) => {
87
+ const b = (extras[i] && extras[i].busca) || p.busca;
88
+ if (b) termos.add(String(b));
89
+ });
90
+ for (const t of lista) termos.add(t);
91
+ return [...termos].slice(0, 8);
92
+ }
93
+
94
+ /* A câmera de cada cena do cinema: empurra e desliza, alternando o lado. A
95
+ foto já se move por dentro (os movimentos do cinema); a câmera move o
96
+ CONJUNTO — é a soma dos dois que dá a profundidade. */
97
+ function camera(d, i, deitado, forte) {
98
+ const lado = i % 2 ? 1 : -1;
99
+ const z = forte ? 1.14 : 1.07;
100
+ const desliza = (deitado ? 26 : 18);
101
+ return {
102
+ zoom: [k(0, 1.0), k(Math.max(0.3, d - 0.25), z, 'camera'), k(d, z + 0.04, 'acelera')],
103
+ x: [k(0, -lado * desliza), k(d, lado * desliza, 'camera')],
104
+ rotacao: [k(0, -lado * 0.35), k(d, lado * 0.35, 'camera')],
105
+ };
106
+ }
107
+
108
+ /** O kit por cima do plano do cinema. Mexe no plano recebido e o devolve. */
109
+ /* SEM CARA DE IA (05/10): o HUD (cantos, timecode, fps, rótulos nas bordas)
110
+ e as partículas eram exatamente a lista do "ruim" que o Joel juntou. Agora
111
+ só entram quando o roteiro pede: roteiro.hud === true, roteiro.particulas === true. */
112
+ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura, hud = false, particulas = false } = {}) {
113
+ if (!plano || !Array.isArray(plano.cenas)) return plano;
114
+ const W = largura || (plano.formato && plano.formato.largura) || 1920;
115
+ const H = altura || (plano.formato && plano.formato.altura) || 1080;
116
+ const deitado = W >= H;
117
+ const base = Math.min(W, H);
118
+ const M = Math.round(base * 0.08);
119
+ const grad = marca.gradiente && marca.gradiente.length >= 2 ? marca.gradiente : ['#2E81ED', '#9567DB'];
120
+ const ACENTO = (marca.cores && marca.cores.destaque) || grad[1];
121
+
122
+ /* As cenas do cinema não são uma por parte: o depoimento com três clipes
123
+ vira três cenas. O mapa cena → parte sai das partes já traduzidas. */
124
+ const parteDaCena = [];
125
+ partes.forEach((p, i) => {
126
+ const n = p.tipo === 'depoimento' ? Math.max(1, (p.clipes || []).length) : 1;
127
+ for (let q = 0; q < n; q++) parteDaCena.push(i);
128
+ });
129
+ plano.cenas.forEach((cena, c) => {
130
+ const iParte = parteDaCena[c] != null ? parteDaCena[c] : c;
131
+ const ex = extras[iParte] || {};
132
+ const d = cena.duracao || 2;
133
+ const tipo = ex.tipoOriginal || '';
134
+ const els = cena.elementos || (cena.elementos = []);
135
+ const forte = tipo === 'numero' || tipo === 'contraste' || tipo === 'abertura';
136
+ if (!cena.camera) cena.camera = camera(d, c, deitado, forte);
137
+
138
+ // A FONTE que se digita, com a barra do acento (é rótulo de borda: vem com o HUD).
139
+ if (hud && ex.fonte && d > 1.4) {
140
+ els.push({ tipo: 'kit', peca: 'fonte', texto: String(ex.fonte).slice(0, 48), t: Math.min(0.9, d * 0.35),
141
+ x: M, y: Math.round(H - M * (deitado ? 1.1 : 2.6)), tamanho: Math.round(base * 0.016), cor: 'rgba(245,245,247,0.72)', corAcento: ACENTO });
142
+ }
143
+ // O NÚMERO estoura em partículas quando assenta.
144
+ if (particulas && (tipo === 'numero' || tipo === 'grafico')) {
145
+ els.push({ tipo: 'kit', peca: 'particulas', t: 0.45, x: Math.round(W / 2), y: Math.round(H * 0.42), raio: Math.round(base * 0.34), n: 46,
146
+ tamanho: Math.max(5, Math.round(base * 0.007)), cores: ['#ffffff', ACENTO, grad[0]] });
147
+ }
148
+ // O FIM: a marca entra com um brilho de partículas.
149
+ if (particulas && (tipo === 'cta' || tipo === 'logo' || tipo === 'fim')) {
150
+ els.push({ tipo: 'kit', peca: 'particulas', t: 0.2, x: Math.round(W / 2), y: Math.round(H * 0.36), raio: Math.round(base * 0.45), n: 60,
151
+ tamanho: Math.max(4, Math.round(base * 0.006)), cores: ['#ffffff', ACENTO], forma: 'circulo' });
152
+ }
153
+ /* O LÍQUIDO entre blocos: a cada três partes a passagem é a mancha que
154
+ cobre a tela, não o esmaecer. Mais que isso vira truque. */
155
+ if (c > 0 && iParte % 3 === 0 && cena.transicao && cena.transicao.tipo !== 'corte' && tipo !== 'cta') {
156
+ cena.transicao = { tipo: 'liquido', duracao: 0.5, cor: ACENTO };
157
+ }
158
+ });
159
+
160
+ // O HUD de motion designer (cantos, timecode e a marca): só a pedido.
161
+ if (hud) plano.hud = plano.hud || { marca: String(marca.nome || '').toUpperCase().slice(0, 24) || undefined };
162
+ else delete plano.hud;
163
+ // O acabamento de filme um pouco mais presente que o do cinema.
164
+ plano.acabamento = { ...(plano.acabamento || {}), grao: Math.max(0.06, (plano.acabamento || {}).grao || 0), vinheta: Math.max(0.55, (plano.acabamento || {}).vinheta || 0) };
165
+ plano.estilo = 'hibrido';
166
+ return plano;
167
+ }
168
+
169
+ module.exports = { traduzir, termosDeBusca, enfeitar, BUSCA_PADRAO };
@@ -24,6 +24,7 @@
24
24
 
25
25
  const { caber, paleta } = require('./composicao.js');
26
26
  const { iconeDe, peso } = require('./edicao.js');
27
+ const { minimoLeitura, sobra } = require('./legibilidade.js');
27
28
 
28
29
  const r2 = (n) => Math.round(n * 100) / 100;
29
30
  const limpa = (t) => String(t || '').replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}%!?]+$/gu, '');
@@ -72,11 +73,12 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
72
73
  const icOk = (ic) => ic && (!iconesOk || iconesOk.has(ic));
73
74
 
74
75
  // A palavra que bate: letra gigante, a palavra forte na cor de destaque.
75
- const bater = (els, texto, { y = null, corTexto = cor.tinta, destaque = cor.destaque, max = (deitado ? H * 0.2 : W * 0.2), linhas = 3, glitch = false, girar = 0 } = {}) => {
76
- const t = String(texto || '').toUpperCase();
77
- /* Caixa alta em peso 900 é ~20% mais larga que a letra comum, e a
78
- batida ainda cresce 5%: mede numa caixa mais estreita (medido em
79
- 25/09: "CONSTRUÇÃO" e "DEMONSTRAÇÃO" saíam pela borda). */
76
+ const bater = (els, texto, { y = null, corTexto = cor.tinta, destaque = cor.destaque, max = (deitado ? H * 0.2 : W * 0.2), linhas = 3, glitch = false, girar = 0, entra = 0.22 } = {}) => {
77
+ // Como a pessoa escreveu: caixa alta forçada era a "cara de IA" (05/10).
78
+ const t = String(texto || '');
79
+ /* Peso 900 é mais largo que a letra comum, e a batida ainda cresce 5%:
80
+ mede numa caixa mais estreita (medido em 25/09: "CONSTRUÇÃO" e
81
+ "DEMONSTRAÇÃO" saíam pela borda). */
80
82
  const fit = caber(t, { largura: W0 * 0.8, linhas, max, fonte: F.titulo });
81
83
  const h = fit.tamanho * 0.98 * fit.linhas;
82
84
  const ws = t.split(/\s+/);
@@ -85,12 +87,14 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
85
87
  els.push({
86
88
  tipo: 'texto', texto: t, fonte: F.titulo, peso: 900, tamanho: fit.tamanho, cor: corTexto, entrelinha: 0.98,
87
89
  x: M, y: Math.round(y == null ? (H - h) / 2 : y), largura: W0, alinhamento: 'center', espacamento: -1,
88
- ...(forte >= 0 && ws.length > 1 ? { revelar: ws.map(() => 0), destaques: [forte], corDestaque: destaque } : {}),
90
+ // A palavra forte é UM acento: a serifa em itálico, que chega um instante depois.
91
+ ...(forte >= 0 && ws.length > 1 ? { revelar: ws.map(() => 0), destaques: [forte], corDestaque: destaque, fonteDestaque: 'Instrument Serif' } : {}),
89
92
  ...(glitch ? { glitch: { forca: 14, dur: 0.3 } } : {}),
90
93
  anima: {
91
- escala: [{ t: 0, v: 1.45 }, { t: 0.13, v: 0.97, e: 'saida' }, { t: 0.22, v: 1, e: 'suave' }, { t: 3, v: 1.05, e: 'linear' }],
92
- opacidade: [{ t: 0, v: 0 }, { t: 0.05, v: 1 }],
93
- rotacao: [{ t: 0, v: girar * 2 }, { t: 0.2, v: girar, e: 'saida' }],
94
+ // A entrada encolhe junto (entra < 0,22) quando a batida é curta: a leitura não.
95
+ escala: [{ t: 0, v: 1.45 }, { t: r2(entra * 0.6), v: 0.97, e: 'saida' }, { t: r2(entra), v: 1, e: 'suave' }, { t: 3, v: 1.05, e: 'linear' }],
96
+ opacidade: [{ t: 0, v: 0 }, { t: r2(entra * 0.23), v: 1 }],
97
+ rotacao: [{ t: 0, v: girar * 2 }, { t: r2(entra * 0.9), v: girar, e: 'saida' }],
94
98
  },
95
99
  });
96
100
  return h;
@@ -126,18 +130,51 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
126
130
  let bs = batidas((tp.palavras || []).filter((w) => w && w.texto), fim);
127
131
  if (!bs.length) bs = [[{ de: ini, ate: fim, texto: p.titulo || p.sim || p.valor || '' }]];
128
132
  const itens = (p.itens || []).map((it) => (typeof it === 'string' ? it : it.texto));
133
+ const semNaoE = (t) => String(t || '').replace(/^\s*(n[aã]o\s+[ée]\s+|[ée]\s+)/i, '');
134
+ // Os textos que a batida k (de n) mostra — os mesmos que o desenho abaixo usa.
135
+ const textosDe = (b, k, n) => {
136
+ const falado = b.map((w) => limpa(w.texto)).join(' ');
137
+ if (p.tipo === 'numero' && k === 0) return [String(p.valor), p.rotulo || ''];
138
+ if ((p.tipo === 'lista' || p.tipo === 'passos') && itens.length) {
139
+ const it = itens[Math.min(k, itens.length - 1)];
140
+ return [p.tipo === 'passos' ? `${k + 1}. ${it}` : it, k === 0 ? p.titulo || '' : ''];
141
+ }
142
+ if (p.tipo === 'cta' && k === n - 1) return [p.titulo || falado, p.link || ''];
143
+ if (p.tipo === 'contraste') return [k < n / 2 ? `não é ${semNaoE(p.nao)}` : semNaoE(p.sim)];
144
+ // O título no fim só se der tempo de ler; senão, a fala dessa batida.
145
+ return [k === n - 1 && p.titulo && k > 0 && vao(k) >= minimoLeitura(p.titulo) + 0.08 ? p.titulo : falado];
146
+ };
147
+ /* A lei do /brag: cada batida fica no ar a entrada mínima mais
148
+ max(0,8 s; 0,3 s por palavra) — dentro do tempo da parte, sem esticar
149
+ o vídeo. Batida curta pega a folga da vizinha (o corte anda um pouco);
150
+ sem folga, gruda nela. */
151
+ const inis = bs.map((b, k) => (k === 0 ? ini : b[0].de - 0.04));
152
+ const vao = (k) => (k < bs.length - 1 ? inis[k + 1] : fim) - inis[k];
153
+ const precisa = (k) => Math.max(...textosDe(bs[k], k, bs.length).filter(Boolean).map(minimoLeitura))
154
+ + (p.tipo === 'cta' && k === bs.length - 1 && p.link ? 0.23 : 0) + 0.08; // o link entra depois do título
155
+ for (let k = 0; bs.length > 1 && k < bs.length;) {
156
+ const falta = precisa(k) - vao(k);
157
+ if (falta <= 0) { k++; continue; }
158
+ if (k + 1 < bs.length && vao(k + 1) - falta >= precisa(k + 1)) { inis[k + 1] = r2(inis[k + 1] + falta); k++; continue; }
159
+ if (k > 0 && vao(k - 1) - falta >= precisa(k - 1)) { inis[k] = r2(inis[k] - falta); k++; continue; }
160
+ const a = k > 0 ? k - 1 : 0;
161
+ bs.splice(a, 2, [...bs[a], ...bs[a + 1]]);
162
+ inis.splice(a + 1, 1);
163
+ k = 0;
164
+ }
129
165
 
130
166
  // Impacto no começo de cada parte; o riser anuncia a chamada final.
131
167
  som(ini, 'impacto', i === 0 ? 0.9 : 0.6);
132
168
  if (p.tipo === 'cta') som(ini - 2, 'riser', 0.6);
133
169
 
134
170
  bs.forEach((b, k) => {
135
- const de = k === 0 ? ini : b[0].de - 0.04;
136
- const ate = k < bs.length - 1 ? bs[k + 1][0].de - 0.04 : fim;
171
+ const de = inis[k];
172
+ const ate = k < bs.length - 1 ? inis[k + 1] : fim;
137
173
  const falado = b.map((w) => limpa(w.texto)).join(' ');
138
174
  const els = [];
139
175
  const cena = { fundo: { tipo: 'cor', valor: cor.fundo }, elementos: els };
140
176
  const ultimo = k === bs.length - 1;
177
+ const entra = r2(sobra(vao(k), precisa(k) - 0.08, 0.08, 0.22));
141
178
  let tela; // 'palavra' | 'foto' | 'marca'
142
179
 
143
180
  if (p.tipo === 'numero' && k === 0) {
@@ -148,8 +185,8 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
148
185
  const fit = caber(valor, { largura: W0, linhas: 1, max: H * (deitado ? 0.55 : 0.32), fonte: F.titulo });
149
186
  els.push({ tipo: 'texto', texto: valor, fonte: F.titulo, peso: 900, tamanho: fit.tamanho, cor: '#FFFFFF', x: M, y: Math.round(H * 0.5 - fit.tamanho * 0.62), largura: W0, alinhamento: 'center',
150
187
  ...(m ? { contar: { ate: Number(m[2].replace(/\./g, '').replace(',', '.')), dur: 0.7, antes: m[1], depois: m[3] } } : {}),
151
- anima: { escala: [{ t: 0, v: 1.6 }, { t: 0.18, v: 1, e: 'saida' }] } });
152
- bater(els, p.rotulo || '', { y: H * 0.5 + fit.tamanho * 0.45, max: H * 0.07, linhas: 2, corTexto: '#FFFFFF', destaque: '#FFFFFF' });
188
+ anima: { escala: [{ t: 0, v: 1.6 }, { t: r2(Math.min(0.18, entra)), v: 1, e: 'saida' }] } });
189
+ bater(els, p.rotulo || '', { y: H * 0.5 + fit.tamanho * 0.45, max: H * 0.07, linhas: 2, corTexto: '#FFFFFF', destaque: '#FFFFFF', entra });
153
190
  cena.tremor = [{ de: 0, ate: 0.5, forca: 26 }];
154
191
  tela = 'marca';
155
192
  } else if ((p.tipo === 'lista' || p.tipo === 'passos') && itens.length) {
@@ -159,9 +196,9 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
159
196
  const rot = p.tipo === 'passos' ? `${k + 1}. ${it}` : it;
160
197
  const temE = emoji(els, it, { y: H * (deitado ? 0.28 : 0.3) });
161
198
  // No gradiente, o destaque é o escuro da marca: laranja sobre azul some.
162
- bater(els, rot, { y: temE ? H * (deitado ? 0.48 : 0.45) : null, max: deitado ? H * 0.11 : W * 0.12, linhas: 4, glitch: k === 0,
199
+ bater(els, rot, { y: temE ? H * (deitado ? 0.48 : 0.45) : null, max: deitado ? H * 0.11 : W * 0.12, linhas: 4, glitch: k === 0, entra,
163
200
  ...(k % 2 === 1 ? { corTexto: '#FFFFFF', destaque: cor.fundo } : {}) });
164
- if (k === 0 && p.titulo) els.push({ tipo: 'texto', texto: String(p.titulo).toUpperCase(), fonte: F.texto, peso: 800, tamanho: Math.round(Math.min(W, H) * 0.045), cor: cor.destaque, x: M, y: M * 2.2, largura: W0, alinhamento: 'center', espacamento: 3 });
201
+ if (k === 0 && p.titulo) els.push({ tipo: 'texto', texto: String(p.titulo), fonte: F.texto, peso: 700, tamanho: Math.round(Math.min(W, H) * 0.04), cor: cor.destaque, x: M, y: M * 2.2, largura: W0, alinhamento: 'center' });
165
202
  tela = k % 2 ? 'marca' : 'palavra';
166
203
  cena.socos = [{ t: 0, forca: 0.14 }];
167
204
  som(de, 'estalo', 0.55);
@@ -173,7 +210,7 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
173
210
  els.push({ tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(W / 2 - R / 2), y: Math.round(H * 0.4 - R / 2), cor: grad[1], opacidade: 0.4,
174
211
  anima: { desfoque: [{ t: 0, v: Math.round(R * 0.2) }], escala: [{ t: 0, v: 0.7 }, { t: 1, v: 1.1, e: 'suave' }] } });
175
212
  const yT = H * 0.26;
176
- const hT = bater(els, p.titulo || falado, { y: yT, max: deitado ? H * 0.14 : W * 0.14, linhas: 3, corTexto: '#FFFFFF', destaque: cor.destaque });
213
+ const hT = bater(els, p.titulo || falado, { y: yT, max: deitado ? H * 0.14 : W * 0.14, linhas: 3, corTexto: '#FFFFFF', destaque: cor.destaque, entra });
177
214
  if (p.link) {
178
215
  const fl = Math.round(Math.min(W, H) * 0.055);
179
216
  const wP = Math.min(W0, String(p.link).length * fl * 0.62 + fl * 2.4);
@@ -193,39 +230,37 @@ function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], na
193
230
  // O rodízio das três telas, com a primeira batida sempre na palavra.
194
231
  const ciclo = ['palavra', 'foto', 'marca', 'foto'];
195
232
  tela = k === 0 ? (p.tipo === 'imagem' || i === 0 ? 'foto' : 'palavra') : ciclo[(nBatida + k) % ciclo.length];
196
- const semNaoE = (t) => String(t || '').replace(/^\s*(n[aã]o\s+[ée]\s+|[ée]\s+)/i, '');
197
- const texto = p.tipo === 'contraste' ? (k < bs.length / 2 ? `não é ${semNaoE(p.nao)}` : semNaoE(p.sim))
198
- : (ultimo && p.titulo && k > 0 ? p.titulo : falado);
233
+ const texto = textosDe(b, k, bs.length)[0];
199
234
  if (tela === 'foto' && fotos.length) {
200
235
  fotoCheia(els, proxFoto());
201
- bater(els, texto, { y: H * (deitado ? 0.56 : 0.6), max: deitado ? H * 0.12 : W * 0.13, linhas: 3 });
236
+ bater(els, texto, { y: H * (deitado ? 0.56 : 0.6), max: deitado ? H * 0.12 : W * 0.13, linhas: 3, entra });
202
237
  if (k % 2 === 0) cena.flashes = [0];
203
238
  } else if (tela === 'marca') {
204
239
  cena.fundo = { tipo: 'gradiente', cores: [grad[0], grad[1]], angulo: 135 };
205
240
  const temE = emoji(els, texto, { y: H * (deitado ? 0.26 : 0.28) });
206
- bater(els, texto, { y: temE ? H * (deitado ? 0.46 : 0.44) : null, corTexto: '#FFFFFF', destaque: cor.fundo, max: deitado ? H * 0.16 : W * 0.16 });
241
+ bater(els, texto, { y: temE ? H * (deitado ? 0.46 : 0.44) : null, corTexto: '#FFFFFF', destaque: cor.fundo, max: deitado ? H * 0.16 : W * 0.16, entra });
207
242
  } else {
208
243
  tela = 'palavra';
209
244
  const temE = emoji(els, texto, { y: H * (deitado ? 0.25 : 0.27) });
210
245
  bater(els, texto, { y: temE ? H * (deitado ? 0.45 : 0.43) : null, glitch: p.tipo === 'contraste' || k % 3 === 0,
211
- girar: k % 2 ? -2 : 2, corTexto: p.tipo === 'contraste' && k < bs.length / 2 ? 'rgba(255,255,255,0.55)' : cor.tinta });
246
+ girar: k % 2 ? -2 : 2, entra, corTexto: p.tipo === 'contraste' && k < bs.length / 2 ? 'rgba(255,255,255,0.55)' : cor.tinta });
212
247
  cena.socos = [{ t: 0, forca: 0.2 }];
213
248
  }
214
249
  if (k === 0) cena.tremor = [{ de: 0, ate: 0.35, forca: 18 }];
215
250
  som(de, tela === 'foto' ? 'whoosh' : 'estalo', tela === 'foto' ? 0.5 : 0.5);
216
251
  }
217
252
  if (!(p.tipo === 'cta' && ultimo)) logoTopo(els);
218
- /* A FAIXA: o subtítulo da parte (é onde moram a data e o local) em
219
- letra pequena e espaçada no alto — o anúncio não pode esconder a data. */
253
+ /* A LINHA DE CIMA: o subtítulo da parte (é onde moram a data e o local)
254
+ em letra pequena no alto — o anúncio não pode esconder a data. Sem a
255
+ faixa de cor atrás (caixa de cor era a "cara de IA", 05/10): só o
256
+ texto, que sobe e aparece. */
220
257
  if (p.sub && p.tipo !== 'numero' && !(p.tipo === 'lista' || p.tipo === 'passos')) {
221
- const fk = Math.round(Math.min(W, H) * 0.042);
222
- els.push({ tipo: 'forma', forma: 'retangulo', x: M, y: Math.round(H * (deitado ? 0.14 : 0.1)), largura: W0, altura: Math.round(fk * 2), cor: cor.destaque, raio: Math.round(fk),
223
- anima: { escalaX: [{ t: 0.05, v: 0 }, { t: 0.3, v: 1, e: 'saida' }] }, origem: 'center' });
258
+ const fk = Math.round(Math.min(W, H) * 0.036);
224
259
  // Com largura, e não pela âncora: sem largura o texto quebrava na metade da tela.
225
- const tk = caber(String(p.sub).toUpperCase(), { largura: W0 * 0.78, linhas: 1, max: fk, fonte: F.texto }).tamanho;
226
- els.push({ tipo: 'texto', texto: String(p.sub).toUpperCase(), fonte: F.texto, peso: 800, tamanho: tk, entrelinha: 1,
227
- cor: cor.sobreDestaque === '#FFFFFF' ? '#FFFFFF' : '#121218', x: M, largura: W0, alinhamento: 'center', y: Math.round(H * (deitado ? 0.14 : 0.1) + fk - tk * 0.5), espacamento: 2,
228
- anima: { opacidade: [{ t: 0.2, v: 0 }, { t: 0.35, v: 1 }] } });
260
+ const tk = caber(String(p.sub), { largura: W0 * 0.78, linhas: 1, max: fk, fonte: F.texto }).tamanho;
261
+ els.push({ tipo: 'texto', texto: String(p.sub), fonte: F.texto, peso: 600, tamanho: tk, entrelinha: 1,
262
+ cor: tela === 'marca' ? '#FFFFFF' : cor.destaque, x: M, largura: W0, alinhamento: 'center', y: Math.round(H * (deitado ? 0.14 : 0.1)),
263
+ anima: { opacidade: [{ t: 0.2, v: 0 }, { t: 0.5, v: 1 }], y: [{ t: 0.2, v: 12 }, { t: 0.6, v: 0, e: 'saida' }] } });
229
264
  }
230
265
 
231
266
  /* Uma transição curta a cada tanto; o resto é corte seco — é o