primocode 10.0.9 → 10.2.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 (46) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +25 -28
  3. package/lib/act.js +5 -2
  4. package/lib/catalogo.js +7 -4
  5. package/lib/config.js +10 -2
  6. package/lib/estudio/assets.js +86 -6
  7. package/lib/estudio/batida.js +184 -0
  8. package/lib/estudio/biblioteca.js +214 -0
  9. package/lib/estudio/cinema.js +37 -17
  10. package/lib/estudio/composicao.js +2 -1
  11. package/lib/estudio/decisao.js +106 -0
  12. package/lib/estudio/edicao.js +36 -17
  13. package/lib/estudio/estilo.js +35 -8
  14. package/lib/estudio/fluido.js +352 -0
  15. package/lib/estudio/gerativo.js +21 -3
  16. package/lib/estudio/hibrido.js +16 -12
  17. package/lib/estudio/impacto.js +66 -31
  18. package/lib/estudio/index.js +245 -42
  19. package/lib/estudio/kit/fluido.jsx +614 -0
  20. package/lib/estudio/kit/gerativo.jsx +15 -5
  21. package/lib/estudio/kit/index.jsx +2 -1
  22. package/lib/estudio/kit/tres.jsx +88 -21
  23. package/lib/estudio/kit.js +3 -1
  24. package/lib/estudio/kokoro.js +111 -0
  25. package/lib/estudio/lancamento.js +3 -4
  26. package/lib/estudio/legibilidade.js +20 -0
  27. package/lib/estudio/referencias.js +36 -25
  28. package/lib/estudio/remotion.js +53 -6
  29. package/lib/estudio/revisor.js +117 -0
  30. package/lib/estudio/variacao3d.js +46 -0
  31. package/lib/estudio/variedade.js +140 -8
  32. package/lib/portao-skill.js +150 -0
  33. package/lib/skills.js +15 -1
  34. package/lib/tools.js +31 -5
  35. package/lib/video/filters.js +6 -1
  36. package/lib/video/ops.js +4 -3
  37. package/lib/video/timeline.js +1 -1
  38. package/lib/video/zoom.js +275 -0
  39. package/package.json +2 -2
  40. package/skills/escrever-em-portugues/SKILL.md +3 -0
  41. package/skills/interface-revisada/SKILL.md +7 -1
  42. package/skills/motion-de-primeira/SKILL.md +3 -5
  43. package/skills/projeto-no-github/SKILL.md +6 -0
  44. package/skills/publicar-na-internet/SKILL.md +3 -0
  45. package/skills/site-com-motion/SKILL.md +2 -0
  46. package/studio/voz_kokoro.py +98 -0
@@ -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