primocode 10.2.0 → 10.3.0-beta.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.
@@ -51,6 +51,31 @@ const PALETAS = {
51
51
  };
52
52
  const ESCURAS = ['noite', 'breu', 'preto', 'cobalto'], CLARAS = ['lavanda', 'creme', 'laranja', 'branco'];
53
53
 
54
+ /* AS FONTES (09/10): "vício em fontes" (Joel) — todo fluido saía em Inter +
55
+ Instrument Serif + JetBrains Mono. Agora é um TRIO do cardápio: o grotesco
56
+ leve das palavras (o kit usa 300 a 600), a serifa itálica do acento e o mono
57
+ do terminal. A memória da variedade ('fonte-fluido') não repete o trio dos
58
+ últimos vídeos; o modelo pode pedir um pelo nome (roteiro.fontesFluido) e o
59
+ pedido vale, menos quando acabou de sair. `css` é o pedido ao Google Fonts
60
+ (conferido: todos respondem 200). Sem escolha salva, o trio aprovado. */
61
+ const TRIOS = {
62
+ aprovado: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono', css: ['wght@300..700', 'ital@0;1', 'wght@400;500'] },
63
+ editorial: { texto: 'Hanken Grotesk', serifa: 'Fraunces', mono: 'Martian Mono', css: ['wght@300..700', 'ital,wght@0,400;1,400', 'wght@400;500'] },
64
+ revista: { texto: 'Figtree', serifa: 'Playfair Display', mono: 'IBM Plex Mono', css: ['wght@300..700', 'ital,wght@0,400;1,400', 'wght@400;500'] },
65
+ carater: { texto: 'Bricolage Grotesque', serifa: 'Bodoni Moda', mono: 'Space Mono', css: ['wght@300..700', 'ital,wght@0,400;1,400', 'wght@400'] },
66
+ tecnico: { texto: 'Mona Sans', serifa: 'Instrument Serif', mono: 'Fragment Mono', css: ['wght@300..700', 'ital@0;1', 'ital@0;1'] },
67
+ classico: { texto: 'Albert Sans', serifa: 'DM Serif Display', mono: 'DM Mono', css: ['wght@300..700', 'ital@0;1', 'wght@400;500'] },
68
+ };
69
+
70
+ /** O trio de um projeto novo, pelos pontos da variedade. Roda uma vez por projeto (index.js guarda). */
71
+ function escolherFontes(pedido = null, { aleatorio = Math.random } = {}) {
72
+ const variedade = require('./variedade.js');
73
+ const nome = TRIOS[String(pedido || '').toLowerCase()] ? String(pedido).toLowerCase() : null;
74
+ // O pedido do modelo vale mais (1), os outros servem (0,5): penaliza o recente, não proíbe.
75
+ return variedade.sortear(Object.keys(TRIOS), 'fonte-fluido', { aleatorio, adequacao: (k) => (!nome || k === nome ? 1 : 0.5) });
76
+ }
77
+ const trioDe = (roteiro) => TRIOS[roteiro.fontesFluido] || TRIOS.aprovado;
78
+
54
79
  const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
55
80
 
56
81
  /** A paleta: a pedida pelo nome; senão o tema (claro/escuro); senão a semente. */
@@ -295,7 +320,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], imag
295
320
  return {
296
321
  versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
297
322
  marcador: roteiro.marcador || ['brilho', 'numero', 'traco', 'ponto', 'nenhum'][hash('marcador' + semente) % 5],
298
- fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
323
+ fontes: (({ texto, serifa, mono }) => ({ texto, serifa, mono }))(trioDe(roteiro)),
299
324
  logo: marca.logo || null,
300
325
  batidas,
301
326
  };
@@ -334,7 +359,7 @@ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
334
359
  const plano = {
335
360
  formato: { largura, altura, fps: FPS },
336
361
  fundoGeral: P.fundo[0],
337
- fontesVariaveis: { Inter: 'wght@300..700', 'Instrument Serif': 'ital@0;1', 'JetBrains Mono': 'wght@400;500' },
362
+ fontesVariaveis: (({ texto, serifa, mono, css }) => ({ [texto]: css[0], [serifa]: css[1], [mono]: css[2] }))(trioDe(roteiro)),
338
363
  cenas: [{
339
364
  duracao: DUR,
340
365
  fundo: P.fundo[0],
@@ -349,4 +374,4 @@ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
349
374
  return { ok: true, plano, partitura, resumo: { cenas: 1, batidas: partitura.batidas.length, imagens: partitura.batidas.filter((b) => b.imagem).map((b) => b.imagem.jeito), segundos: DUR, fps: FPS, paleta: P.nome, atraso: partitura.atraso, sincronia: partitura.sincronia } };
350
375
  }
351
376
 
352
- module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };
377
+ module.exports = { montarFluido, partituraDe, caixaNormal, escolherFontes, PALETAS, TRIOS, POR_PALAVRA, MINIMO };
@@ -209,12 +209,132 @@ const ICONE_DE = {
209
209
 
210
210
  /** O humor do roteiro: as palavras dele contra as de cada humor (empate: a semente). */
211
211
  function humorDe(roteiro, s) {
212
- const txt = new Set(tokens((roteiro.partes || []).map((p) => [p.titulo, p.texto, p.fala, p.sub, ...(p.itens || [])].filter(Boolean).join(' ')).join(' ')
213
- + ' ' + ((roteiro.marca && roteiro.marca.nome) || '') + ' ' + (roteiro.pedido || '')));
212
+ const n = acertosDeHumor(roteiro);
214
213
  const nota = {};
215
- for (const [h, d] of Object.entries(HUMORES)) nota[h] = d.palavras.split(' ').filter((w) => txt.has(w)).length + s.prox() * 0.5;
214
+ for (const h of Object.keys(HUMORES)) nota[h] = n[h] + s.prox() * 0.5;
216
215
  return Object.entries(nota).sort((a, b) => b[1] - a[1])[0][0];
217
216
  }
217
+ /** Quantas palavras de cada humor o roteiro diz. */
218
+ function acertosDeHumor(roteiro) {
219
+ const txt = new Set(tokens((roteiro.partes || []).map((p) => [p.titulo, p.texto, p.fala, p.sub, ...(p.itens || [])].filter(Boolean).join(' ')).join(' ')
220
+ + ' ' + ((roteiro.marca && roteiro.marca.nome) || '') + ' ' + (roteiro.pedido || '')));
221
+ return Object.fromEntries(Object.entries(HUMORES).map(([h, d]) => [h, d.palavras.split(' ').filter((w) => txt.has(w)).length]));
222
+ }
223
+
224
+ /* AS ESCOLHAS DO PROJETO (09/10): "vício em cores, músicas, fontes… ele
225
+ escolhe sempre as mesmas coisas" (Joel). A paleta, as fontes e o gênero
226
+ saíam só da semente — sem lembrar do vídeo anterior — e o humor caía em
227
+ "tecnologia" com uma palavra só ("dados", "rápido"), porque é a lista mais
228
+ longa. Agora as quatro escolhas passam pela memória da variedade.js:
229
+ - o humor: adequação (acertos + 1) / (máximo + 1). Com o assunto claro
230
+ (6 palavras de IA), os outros valem ~0,15 e a tecnologia fica; com uma
231
+ palavra só, os outros valem 0,5 e a memória gira. Penaliza, não proíbe;
232
+ - paleta, fontes e gênero: as do humor valem 1, as dos outros humores
233
+ valem menos (ainda curadas, nunca as de cara de IA) — o que é do assunto
234
+ vem primeiro, e quando já saiu nos últimos vídeos a vizinha ganha.
235
+ Roda UMA vez por projeto (index.js guarda em p.escolhasGerativo): mandar
236
+ o roteiro de novo não sorteia outro vídeo. A partitura segue pura — ela só
237
+ lê roteiro.escolhasGerativo, e a marca continua mandando por cima. */
238
+ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
239
+ const variedade = require('./variedade.js');
240
+ const marca = roteiro.marca || {};
241
+ const n = acertosDeHumor(roteiro);
242
+ const max = Math.max(...Object.values(n));
243
+ const humor = variedade.sortear(Object.keys(HUMORES), 'humor-gerativo', { aleatorio, adequacao: (h) => (n[h] + 1) / (max + 1) });
244
+ const de = (campo, outros) => { const m = {}; for (const [h, d] of Object.entries(HUMORES)) for (const x of d[campo]) if (!BANIDAS.includes(x) && (m[x] == null || h === humor)) m[x] = h === humor ? 1 : outros; return m; };
245
+ const escolher = (m, tipo, prefixo) => variedade.sortear(Object.keys(m), tipo, { aleatorio, prefixo, adequacao: m });
246
+ const out = { humor };
247
+ // A paleta é o sistema de 5 campos (é o que aparece). Com o fundo da marca, nem sorteia.
248
+ if (!ehCor((marca.cores || {}).fundo)) {
249
+ const m = {};
250
+ for (const [h, lista] of Object.entries(CAMPOS)) for (const k of lista) m[k.join(' ')] = h === humor ? 1 : 0.6;
251
+ out.paleta = escolher(m, 'paleta-gerativo', '').split(' ');
252
+ }
253
+ const fontesMarca = marca.fontes || {};
254
+ if (!fontesMarca.titulo) out.display = escolher(de('displays', 0.5), 'fonte-gerativo', 'display:');
255
+ if (!fontesMarca.texto) out.texto = escolher(de('textos', 0.5), 'fonte-gerativo', 'texto:');
256
+ out.mono = escolher(de('monos', 0.5), 'fonte-gerativo', 'mono:');
257
+ out.genero = escolher(de('generos', 0.35), 'genero-gerativo', '');
258
+ const um = (lista, tipo, prefixo) => variedade.sortear(lista, tipo, { aleatorio, prefixo });
259
+ const o3d = OBJETO3D_DO_HUMOR[humor] != null ? OBJETO3D_DO_HUMOR[humor] : 0.7;
260
+ // Um pouco de acaso na adequação: sem ele, a memória vira rodízio fixo (esfera+explosão, polígono+toro, de novo).
261
+ const fitH = (h) => (HEROIS_OBJETO.includes(h) ? o3d : 1 - o3d / 2) * (0.75 + 0.25 * aleatorio());
262
+ /* A FORMA ABSTRATA (09/10, "partículas girando numa conversa de padaria?"):
263
+ só no assunto abstrato ou de tecnologia, e com força pelo quanto o
264
+ roteiro É do assunto (uma palavra só não basta). UMA forma por vídeo —
265
+ é identidade, não sorteio por batida — ou 'nenhum', que a memória também gira. */
266
+ const forca = (ABSTRATO_DO_HUMOR[humor] || 0) * Math.min(1, n[humor] / 3);
267
+ const h1 = forca > 0 ? variedade.sortear([...HEROIS_OBJETO, ...HEROIS_CAMPO, 'nenhum'], 'heroi-gerativo', { aleatorio, adequacao: (h) => (h === 'nenhum' ? 1 - forca : forca * fitH(h)) }) : 'nenhum';
268
+ out.herois = { favoritos: h1 === 'nenhum' ? [] : [h1] };
269
+ // 'produto' e 'marca' quem decide é o conteúdo (o 3D pedido, a marca), não o gosto do projeto.
270
+ const favC = [];
271
+ for (let k = 0; k < 3; k++) favC.push(variedade.sortear(COMPOSICOES.filter((c) => c !== 'produto' && c !== 'marca' && !favC.includes(c)), 'composicao-gerativo', { aleatorio }));
272
+ out.composicoes = { favoritas: favC };
273
+ /* A MOLDURA (09/10): o quadro fixo do reel de referência — cantos, o assunto
274
+ no alto, a seção, o tempo embaixo — é identidade, mas em todo vídeo vira
275
+ vício. Sai do cardápio com memória: 'sem' pesa um pouco mais. */
276
+ out.moldura = variedade.sortear(MOLDURAS, 'moldura-gerativo', { aleatorio, adequacao: { sem: 1, cantos: 0.95, completa: 0.95 } });
277
+ // O bokeh combina com luz de tela (tecnologia, energia, natureza); numa padaria, quase nunca.
278
+ const bokeh = ['tecnologia', 'energia', 'natureza'].includes(humor) ? 0.6 : 0.2;
279
+ out.ambiente = variedade.sortear(AMBIENTES, 'ambiente-gerativo', { aleatorio, adequacao: { limpo: 1, luz: 0.85, bokeh } });
280
+ out.acento = variedade.sortear(ACENTOS_JEITO, 'acento-gerativo', { aleatorio, adequacao: { traco: 1, viagem: 0.7 } });
281
+ out.icones = { arranjo: um(ARRANJOS_ICONE, 'arranjo-icone', 'arranjo:'), ritmo: um(RITMOS_ICONE, 'arranjo-icone', 'ritmo:'), material: um(MATERIAIS_ICONE, 'material-icone', '') };
282
+ return out;
283
+ }
284
+ /* OS ÍCONES (09/10, "vício na forma que os ícones são distribuídos"): o
285
+ ícone ia sempre à direita (deitado) ou acima (em pé), nas primeiras batidas
286
+ que tinham palavra-chave, num de 3 materiais. Agora o projeto escolhe:
287
+ - o ARRANJO: 'lado' (o de antes), 'outro-lado' (esquerda / abaixo),
288
+ 'alterna' (um de cada lado, batida a batida) ou 'canto' (menor, no canto);
289
+ - o RITMO: 'seguidos' (o de antes), 'alternados' (uma batida sem ícone entre
290
+ dois) ou 'esparsos' (duas);
291
+ - o MATERIAL do kit/tres.jsx.
292
+ O kit tenta o lugar pedido e, sem espaço, o próximo da lista (nunca por cima do texto). */
293
+ /* O HERÓI E AS COMPOSIÇÕES entre vídeos (09/10, "vício no 3D… efeitos mais 3D
294
+ dependendo do vídeo"): dentro do vídeo eles já não se repetiam, mas o vídeo
295
+ seguinte recomeçava igual. Cada projeto escolhe 2 heróis e 3 composições
296
+ FAVORITOS pela memória (pesam mais no sorteio; os outros continuam valendo).
297
+ O assunto decide quanto 3D: tecnologia e energia puxam os objetos (esfera,
298
+ toro, polígono, explosão); editorial e luxo, os campos. */
299
+ const HEROIS_OBJETO = ['explosao', 'poligono', 'esfera', 'toro'], HEROIS_CAMPO = ['truchet', 'aneis', 'grade', 'rastro'];
300
+ const OBJETO3D_DO_HUMOR = { tecnologia: 1, energia: 1, natureza: 0.8, luxo: 0.6, editorial: 0.4 };
301
+ /* Quanto o assunto aceita forma abstrata (o resto: a tipografia e o ícone do conteúdo). */
302
+ const ABSTRATO_DO_HUMOR = { tecnologia: 1, energia: 0.7, natureza: 0.5, luxo: 0, editorial: 0 };
303
+ const ARRANJOS_ICONE = ['lado', 'outro-lado', 'alterna', 'canto'];
304
+ const MOLDURAS = ['sem', 'cantos', 'completa'];
305
+ /* A BOLINHA (09/10): "todo vídeo tem aquela bolinha" (Joel). O ambiente:
306
+ 'limpo' (só grão e vinheta), 'luz' (o brilho e o feixe que varre) ou
307
+ 'bokeh' (as manchas de luz desfocadas na frente). O acento: 'traco' (cada
308
+ traço se desenha e some) ou 'viagem' (o ponto que nasce no meio, viaja
309
+ entre as batidas e se recolhe — o de antes). */
310
+ const AMBIENTES = ['limpo', 'luz', 'bokeh'];
311
+ const ACENTOS_JEITO = ['traco', 'viagem'];
312
+ const RITMOS_ICONE = ['seguidos', 'alternados', 'esparsos'];
313
+ const MATERIAIS_ICONE = ['argila', 'verniz', 'metal', 'perola', 'acetinado'];
314
+
315
+ /** Só o que existe nos cardápios: um p.escolhasGerativo velho ou editado à mão não quebra o vídeo. */
316
+ function escolhasValidas(e) {
317
+ if (!e || typeof e !== 'object') return { herois: [], composicoes: [] };
318
+ const todas = (campo) => Object.values(HUMORES).flatMap((d) => d[campo]);
319
+ return {
320
+ humor: HUMORES[e.humor] ? e.humor : null,
321
+ moldura: MOLDURAS.includes(e.moldura) ? e.moldura : null,
322
+ ambiente: AMBIENTES.includes(e.ambiente) ? e.ambiente : null,
323
+ acento: ACENTOS_JEITO.includes(e.acento) ? e.acento : null,
324
+ paleta: Array.isArray(e.paleta) && e.paleta.length === 5 && e.paleta.every(ehCor) ? e.paleta : null,
325
+ display: VARIAVEIS[e.display] && todas('displays').includes(e.display) ? e.display : null,
326
+ texto: todas('textos').includes(e.texto) ? e.texto : null,
327
+ mono: todas('monos').includes(e.mono) ? e.mono : null,
328
+ genero: todas('generos').includes(e.genero) ? e.genero : null,
329
+ herois: (e.herois && Array.isArray(e.herois.favoritos) ? e.herois.favoritos : []).filter((h) => HEROIS_OBJETO.includes(h) || HEROIS_CAMPO.includes(h)),
330
+ composicoes: (e.composicoes && Array.isArray(e.composicoes.favoritas) ? e.composicoes.favoritas : []).filter((c) => COMPOSICOES.includes(c)),
331
+ icones: e.icones && ARRANJOS_ICONE.includes(e.icones.arranjo) ? {
332
+ arranjo: e.icones.arranjo,
333
+ ritmo: RITMOS_ICONE.includes(e.icones.ritmo) ? e.icones.ritmo : 'seguidos',
334
+ material: MATERIAIS_ICONE.includes(e.icones.material) ? e.icones.material : null,
335
+ } : null,
336
+ };
337
+ }
218
338
  /* As que têm largura (wdth): é nelas que a palavra estica. */
219
339
  const CINETICAS = ['Roboto Flex', 'Anybody', 'Archivo', 'Mona Sans'];
220
340
 
@@ -452,10 +572,13 @@ const luz = (h) => { const n = parseInt(String(h).slice(1), 16); return ((n >> 1
452
572
  const contraste = (a, b) => { const la = luz(a) + 0.05, lb = luz(b) + 0.05; return la > lb ? la / lb : lb / la; };
453
573
  const PALETAS = HUMORES.tecnologia.paletas;
454
574
 
455
- function paletaDe(marca, s, humor = null, roteiro = {}) {
575
+ function paletaDe(marca, s, humor = null, roteiro = {}, fixa = null) {
456
576
  const c = marca.cores || {};
457
577
  const op = humor ? HUMORES[humor].paletas : PALETAS;
458
- const [f0, t0, a0] = op[Math.floor(s.prox() * op.length) % op.length];
578
+ // O sorteio acontece sempre: a sequência da semente não muda com ou sem escolha salva.
579
+ const sorteada = op[Math.floor(s.prox() * op.length) % op.length];
580
+ // A escolha do projeto (escolherVisual) é um sistema de 5 campos: 0 = tinta escura, 1 = assinatura, 2 = papel.
581
+ const [f0, t0, a0] = fixa ? [fixa[0], fixa[2], fixa[1]] : sorteada;
459
582
  let base = { fundo: f0, tinta: t0, acento: a0 };
460
583
  // A cor que o assunto diz manda no acento (se não sumir no fundo).
461
584
  const ditas = tokens((roteiro.partes || []).map((p) => [p.titulo, p.destaque].join(' ')).join(' ')).filter((w) => CORES_DITAS[w]);
@@ -478,9 +601,10 @@ const CAMPOS = {
478
601
  natureza: [['#0F2A2A', '#F2A541', '#F2EDE2', '#2546D9', '#E4573D'], ['#13233F', '#2546D9', '#F2EDE2', '#F2A541', '#C8553D']],
479
602
  luxo: [['#120D0B', '#C9A24A', '#F0E6D6', '#5B1A1F', '#2F4F4F']],
480
603
  };
481
- function camposDe(marca, humor, s, dita = null) {
604
+ function camposDe(marca, humor, s, dita = null, fixa = null) {
482
605
  const c = marca.cores || {};
483
- let k = [...s.um(CAMPOS[humor] || CAMPOS.tecnologia)];
606
+ const sorteado = s.um(CAMPOS[humor] || CAMPOS.tecnologia);
607
+ let k = [...(fixa || sorteado)];
484
608
  // A cor que o assunto diz vira a assinatura (se não for a de outro papel).
485
609
  if (dita && !ehCor(c.destaque)) { const j = k.indexOf(dita); if (j > 0) k[j] = k[1]; k[1] = dita; }
486
610
  // A marca: o fundo dela, o destaque dela e a tinta dela tomam os lugares de mesmo papel.
@@ -492,15 +616,56 @@ function camposDe(marca, humor, s, dita = null) {
492
616
  if (ehCor(c.destaque) && k[1] !== c.destaque) k[1] = c.destaque;
493
617
  return k;
494
618
  }
495
- /** As cores de uma batida pelo campo dela: texto no contraste máximo, acento noutra cor do sistema. */
496
- function coresDoCampo(campos, i, s) {
619
+ /* A IDENTIDADE TRAVADA (09/10): "as cores do nada estão preto e do nada estão
620
+ branco, cores nada a ver uma diferente da outra" (Joel, sobre o Pix na
621
+ padaria). O sistema de 5 campos virava 4 fundos trocando quase toda batida.
622
+ Agora o vídeo usa UMA identidade: 2 chãos (o escuro e o claro do sistema) e
623
+ 1 acento (a assinatura), e mais nada. Os campos 3 e 4 ficam de fora. */
624
+ function identidadeDe(campos, marca = {}) {
625
+ const c = marca.cores || {};
626
+ let chaos = [campos[0], campos[2]];
627
+ // O fundo da marca é sempre um chão (mesmo o de meio-tom, que o camposDe punha na assinatura).
628
+ if (ehCor(c.fundo) && !chaos.includes(c.fundo)) chaos[luz(c.fundo) < 0.5 ? 0 : 1] = c.fundo;
629
+ chaos = chaos.sort((a, b) => luz(a) - luz(b));
630
+ return { chaos, acento: campos[1] };
631
+ }
632
+ /** O acento no chão: se some nele, escurece (chão claro) ou clareia (chão escuro) no MESMO matiz. */
633
+ function acentoNoChao(acento, fundo) {
634
+ if (contraste(acento, fundo) >= 2.6) return acento;
635
+ const alvo = luz(fundo) > 0.5 ? '#000000' : '#ffffff';
636
+ for (let k = 0.1; k <= 0.9; k += 0.05) { const x = misturarCor(acento, alvo, k); if (contraste(x, fundo) >= 2.6) return x; }
637
+ return misturarCor(acento, alvo, 0.9);
638
+ }
639
+ /** As cores de uma batida pelo chão dela: texto no outro chão, acento o da identidade. */
640
+ function coresDoCampo(campos, i, s, id = null) {
641
+ const I = id || identidadeDe(campos);
497
642
  const fundo = campos[i];
498
- const tintas = [campos[0], campos[2]].sort((a, b) => contraste(b, fundo) - contraste(a, fundo));
499
- const texto = tintas[0];
500
- const outras = campos.filter((c, j) => j !== i && c !== texto && contraste(c, fundo) >= 2.2);
501
- const acento = outras.length ? (outras.includes(campos[1]) && i !== 1 ? campos[1] : s.um(outras)) : texto;
643
+ const texto = [...I.chaos].sort((a, b) => contraste(b, fundo) - contraste(a, fundo))[0];
644
+ const acento = acentoNoChao(I.acento, fundo);
502
645
  return { fundo, texto, fraco: misturarCor(texto, fundo, 0.45), acento, contorno: misturarCor(texto, fundo, 0.6) };
503
646
  }
647
+ /* AS SEÇÕES: o fundo troca só na virada de seção. Quem diz a seção é o
648
+ roteiro (parte.secao, o nome dela); sem isso, as partes se dividem em 1 a 4
649
+ blocos seguidos, uns 3 por bloco. Devolve o índice de cada parte e os nomes. */
650
+ function secoesDe(partes) {
651
+ const n = partes.length;
652
+ if (partes.some((p) => p && typeof p.secao === 'string' && p.secao.trim())) {
653
+ const nomes = [], idx = [];
654
+ let atual = null;
655
+ partes.forEach((p, i) => {
656
+ const nome = (p.secao && String(p.secao).trim()) || atual || '';
657
+ if (i === 0 || nome !== atual) nomes.push(nome);
658
+ atual = nome;
659
+ idx.push(nomes.length - 1);
660
+ });
661
+ return { idx, nomes };
662
+ }
663
+ const k = n <= 3 ? 1 : n <= 6 ? 2 : n <= 10 ? 3 : 4;
664
+ const idx = partes.map((_, i) => Math.floor((i * k) / n));
665
+ // Sem nome dado, a seção leva o título da primeira parte dela.
666
+ const nomes = Array.from({ length: k }, (_, j) => { const p = partes[idx.indexOf(j)] || {}; return String(p.titulo || p.texto || p.sim || p.rotulo || p.valor || '').trim(); });
667
+ return { idx, nomes };
668
+ }
504
669
 
505
670
  /** As cores de uma batida pelo fundo dela: base (a marca) ou acento (o fundo vira a cor). */
506
671
  function coresDaBatida(P, fundo) {
@@ -523,7 +688,9 @@ const NOTAS = [220, 246.94, 261.63, 293.66, 329.63, 349.23, 392, 440];
523
688
  function sonsDoVideo(roteiro = {}) {
524
689
  const semente = hash('som:' + JSON.stringify((roteiro.partes || []).map((p) => [p.titulo, p.fala])) + ((roteiro.marca && roteiro.marca.nome) || ''));
525
690
  const s = sorteador(semente);
526
- const timbre = HUMORES[humorDe(roteiro, sorteador(hash('humor')))] ? HUMORES[humorDe(roteiro, s)].som : 'analogico';
691
+ const humorSom = humorDe(roteiro, s);
692
+ const E = escolhasValidas(roteiro.escolhasGerativo);
693
+ const timbre = HUMORES[E.humor || humorSom] ? HUMORES[E.humor || humorSom].som : 'analogico';
527
694
  const raiz = s.um(NOTAS);
528
695
  const escala = [1, 1.189, 1.335, 1.498, 1.782, 2]; // pentatônica menor sobre a raiz
529
696
  const nota = () => Math.round(raiz * s.um(escala) * (s.chance(0.3) ? 2 : 1));
@@ -587,8 +754,11 @@ function sonsDoVideo(roteiro = {}) {
587
754
  function musicaDe(roteiro = {}) {
588
755
  const semente = hash('musica:' + JSON.stringify((roteiro.partes || []).map((p) => [p.titulo, p.fala])));
589
756
  const s = sorteador(semente);
590
- const h = humorDe(roteiro, s);
591
- return { humor: h, genero: s.um(HUMORES[h].generos), semente };
757
+ const E = escolhasValidas(roteiro.escolhasGerativo);
758
+ const sorteado = humorDe(roteiro, s);
759
+ const h = E.humor || sorteado;
760
+ const genero = s.um(HUMORES[h].generos);
761
+ return { humor: h, genero: E.genero || genero, semente };
592
762
  }
593
763
  /* ── O SOM DESENHADO (30/09, terceira volta): "os efeitos repetem e são os
594
764
  genéricos que toda IA usa". Então:
@@ -697,10 +867,10 @@ const LEGADO = { passagem: ['whoosh', 'sopro'], golpe: ['impacto', 'toque'], nas
697
867
  * @param {Array} op.tempos [{de, ate, palavras:[{texto,de,ate}]}] por parte
698
868
  * @returns {{ok, plano, partitura, resumo}}
699
869
  */
700
- function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
870
+ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null, icones = {} }) {
701
871
  const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
702
872
  if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
703
- const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
873
+ const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos, icones });
704
874
  const DUR = partitura.duracao;
705
875
 
706
876
  // ── o som: efeitos nas transformações, a música abaixa sob a voz ─────
@@ -741,16 +911,21 @@ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], n
741
911
  }
742
912
 
743
913
  /** A partitura, sem áudio nem plano — é o que os testes olham. */
744
- function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
914
+ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icones = {} }) {
745
915
  const partes = roteiro.partes || [];
746
916
  const marca = roteiro.marca || {};
747
917
  const W = largura, H = altura, deitado = W >= H;
748
918
  const semente = roteiro.semente != null ? hash('semente:' + roteiro.semente)
749
919
  : hash(JSON.stringify([partes.map((p) => [p.tipo, p.titulo, p.texto, p.fala, p.valor, p.itens, p.nao, p.sim, p.destaque]), marca.nome || '']));
750
920
  const s = sorteador(semente);
751
- const humor = humorDe(roteiro, s);
921
+ /* As escolhas do projeto (escolherVisual, com memória entre vídeos). Sem
922
+ elas — projeto antigo, teste — vale a semente, como antes. Os sorteios
923
+ da semente acontecem igual nos dois casos: o resto do vídeo não muda. */
924
+ const E = escolhasValidas(roteiro.escolhasGerativo);
925
+ const humorSorteado = humorDe(roteiro, s);
926
+ const humor = E.humor || humorSorteado;
752
927
  const H0 = HUMORES[humor];
753
- const P = paletaDe(marca, s, humor, roteiro);
928
+ const P = paletaDe(marca, s, humor, roteiro, E.paleta);
754
929
 
755
930
  /* ── as fontes: NADA de cara de IA. A da marca vale se tiver caráter (e
756
931
  eixos para animar); a genérica da marca vira texto, e o título ganha
@@ -758,11 +933,16 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
758
933
  const pedida = (marca.fontes && marca.fontes.titulo) || '';
759
934
  const pedidaTexto = (marca.fontes && marca.fontes.texto) || '';
760
935
  const boa = (f) => f && VARIAVEIS[f] && !BANIDAS.includes(f) && VARIAVEIS[f].eixos.wght;
761
- const display = boa(pedida) ? pedida : s.um(H0.displays);
936
+ const ou = (salva, sorteada) => salva || sorteada;
937
+ const display = boa(pedida) ? pedida : ou(E.display, s.um(H0.displays));
762
938
  const cinetica = eixoDe(display, 'wdth') && eixoDe(display, 'wdth')[0] <= 75 ? display : s.um(H0.cineticas);
763
- const texto = pedidaTexto ? pedidaTexto : (pedida && BANIDAS.includes(pedida) ? pedida : s.um(H0.textos));
939
+ const texto = pedidaTexto ? pedidaTexto : (pedida && BANIDAS.includes(pedida) ? pedida : ou(E.texto, s.um(H0.textos)));
764
940
  const serifa = VARIAVEIS[display].serifa ? display : s.um(H0.serifas);
765
- const fontes = { display, cinetica, texto, mono: s.um(H0.monos), serifa };
941
+ /* DUAS FONTES (09/10, a identidade): o título e o texto. A cinética, a
942
+ serifa da citação e o rótulo eram mais três faces — o vídeo saía com 4
943
+ ou 5 famílias. Os sorteios continuam (a semente não muda o resto). */
944
+ void cinetica; void serifa; void ou(E.mono, s.um(H0.monos));
945
+ const fontes = { display, cinetica: display, texto, mono: texto, serifa: display };
766
946
 
767
947
  // ── o quadro: margens e respiro ─────────────────────────────────────
768
948
  // A margem conta com a câmera: ela empurra até ~8% e desliza, e nada pode sair do quadro.
@@ -821,6 +1001,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
821
1001
  if (recentes.slice(-3).includes(comp)) q *= 0.35;
822
1002
  q *= Math.pow(0.72, usadas[comp] || 0);
823
1003
  if (comp === 'parede' && (usadas.parede || 0) >= 1) q *= 0.25;
1004
+ if (E.composicoes.includes(comp)) q *= 2.2; // a favorita do projeto (escolherVisual)
824
1005
  w[nome] = q;
825
1006
  }
826
1007
  return s.pesado(w) || s.pesado({ pilha: anterior === 'pilha' ? 0 : 1, gigante: anterior === 'gigante' ? 0 : 1, palavras: anterior === 'palavras' ? 0 : 1 });
@@ -879,72 +1060,92 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
879
1060
  registra(compor(nome, br, ctx));
880
1061
  });
881
1062
 
882
- /* ── os campos: o fundo inteiro troca de cor a cada batida (quase sempre),
883
- nunca igual ao anterior nem ao de duas antes; a fala e o conteúdo da
884
- mesma parte às vezes dividem o campo. O produto 3D pede o campo escuro. */
1063
+ /* ── os chãos: a identidade do vídeo (2 chãos e 1 acento) e o fundo que
1064
+ troca só na virada de seção, sempre entre os dois chãos. Quem começa é
1065
+ o fundo da marca; sem marca, a semente. ─────────────────────────────── */
885
1066
  const ditaCor = tokens(partes.map((p) => [p.titulo, p.destaque].join(' ')).join(' ')).map((w) => CORES_DITAS[w]).find(Boolean) || null;
886
- const campos = camposDe(marca, humor, s, ditaCor);
887
- batidas.forEach((b, i) => {
888
- if (i === 0) { b.campo = 0; return; }
889
- const a = batidas[i - 1];
890
- if (a.parte === b.parte && s.chance(0.4)) { b.campo = a.campo; return; }
891
- if (b.produto3d) { b.campo = a.campo === 0 ? 3 : 0; return; }
892
- const evita = [a.campo, i > 1 ? batidas[i - 2].campo : -1];
893
- const op = campos.map((_, j) => j).filter((j) => !evita.includes(j));
894
- b.campo = s.um(op);
895
- });
896
- for (const b of batidas) { b.fundo = 'campo' + b.campo; b.cores = coresDoCampo(campos, b.campo, s); }
1067
+ const campos = camposDe(marca, humor, s, ditaCor, E.paleta);
1068
+ const ident = identidadeDe(campos, marca);
1069
+ // O campo continua um índice (o kit e as passagens leem): 0 = o chão escuro, 2 = o claro.
1070
+ campos[0] = ident.chaos[0]; campos[2] = ident.chaos[1];
1071
+ const fm = (marca.cores || {}).fundo;
1072
+ const comeca = ehCor(fm) ? (fm === campos[0] ? 0 : 2) : (s.chance(0.5) ? 0 : 2);
1073
+ const secoes = secoesDe(partes);
1074
+ batidas.forEach((b) => { b.secao = secoes.idx[b.parte] || 0; b.campo = b.secao % 2 === 0 ? comeca : 2 - comeca; });
1075
+ for (const b of batidas) { b.fundo = 'campo' + b.campo; b.cores = coresDoCampo(campos, b.campo, s, ident); }
897
1076
 
898
- /* ── os ÍCONES 3D: onde a parte pediria um ícone (lista, número,
899
- recurso), um objeto de geometria no material do vídeo. A palavra
900
- decide qual; nunca o mesmo duas vezes, e no máximo em ~45% das batidas
901
- — ícone em toda batida vira papel de parede de IA. ───────────────── */
902
- const material = s.um(['argila', 'verniz', 'metal']);
1077
+ /* ── O HERÓI DE CADA BATIDA VEM DO CONTEÚDO (09/10): "é uma conversa sobre
1078
+ pão e ele vai naquela bolinha" (Joel). Quem diz é o roteiro: parte.heroi
1079
+ (ou parte.icone) — um ícone do Iconify ("mdi:bread-slice", o desenho
1080
+ chega em `icones`), um 3D do kit (raio, relogio…), uma forma abstrata
1081
+ pelo nome, ou "nenhum". Sem isso: o ícone 3D quando a fala cita um
1082
+ objeto do catálogo (ICONE_DE, sugestão), no máximo em ~45% das batidas;
1083
+ e a forma abstrata só no assunto abstrato, a escolhida do projeto, em
1084
+ até 1 de cada 3 batidas. Nada serve: a tipografia carrega a batida. ── */
1085
+ const pedidoDe = partes.map((p) => { const h = p.heroi != null ? p.heroi : p.icone; return typeof h === 'string' && h.trim() ? h.trim() : null; });
1086
+ const ABSTRATOS = [...HEROIS_OBJETO, ...HEROIS_CAMPO];
1087
+ const ICONES3D = new Set(Object.values(ICONE_DE));
1088
+ const materialSorteado = s.um(['argila', 'verniz', 'metal']);
1089
+ // O arranjo do projeto (escolherVisual); sem ele, o de antes: um lado só, ícones seguidos.
1090
+ const EI = E.icones || { arranjo: 'lado', ritmo: 'seguidos' };
1091
+ const material = EI.material || materialSorteado;
1092
+ const lado1 = deitado ? 'direita' : 'acima', lado2 = deitado ? 'esquerda' : 'abaixo';
1093
+ const reserva = deitado ? ['direita', 'acima', 'abaixo', 'esquerda'] : ['acima', 'abaixo', 'direita', 'esquerda'];
1094
+ const folga = { seguidos: 1, alternados: 2, esparsos: 3 }[EI.ritmo] || 1;
903
1095
  const iconesUsados = new Set();
904
1096
  const tetoIcones = Math.ceil(batidas.length * 0.45);
905
- for (const b of batidas) {
906
- if (iconesUsados.size >= tetoIcones) break;
1097
+ let ultimoIcone = -99, nIcones = 0;
1098
+ const atendidas = new Set();
1099
+ // O svg do Iconify vem com 1em de lado: sai do tamanho da caixa.
1100
+ const svgLimpo = (svg) => String(svg).replace(/\swidth="[^"]*"/, ' width="100%"').replace(/\sheight="[^"]*"/, ' height="100%"');
1101
+ const porIcone = (b, ic, extra = {}) => {
1102
+ const primeiro = { lado: lado1, 'outro-lado': lado2, canto: 'canto', alterna: nIcones % 2 ? lado2 : lado1 }[EI.arranjo] || lado1;
1103
+ const lados = [primeiro, ...reserva.filter((l) => l !== primeiro)];
1104
+ const prim = b.linhas[0];
1105
+ const escala = primeiro === 'canto' ? 0.6 : 1;
1106
+ b.icone3d = { icone: ic, lado: primeiro, lados, tamanho: Math.round((deitado ? 0.4 * H : 0.38 * W) * escala), material,
1107
+ cor: b.cores.texto, cor2: b.cores.acento, luz: b.cores.acento, t: r3(Math.min(b.ate - 1, (prim ? prim.t : b.de) + 0.15)), ...extra };
1108
+ b.acentos += 1;
1109
+ if (b.acentos > REGRAS.acentos) { b.icone3d.cor2 = misturarCor(b.cores.texto, b.cores.fundo, 0.3); b.acentos -= 1; }
1110
+ iconesUsados.add(ic); nIcones++;
1111
+ };
1112
+ for (const [bi, b] of batidas.entries()) {
1113
+ const pedido = pedidoDe[b.parte];
1114
+ if (pedido) {
1115
+ // O pedido vale numa batida só da parte: a do conteúdo (a fala solta fica com a tipografia).
1116
+ if (b.origem !== 'conteudo' || atendidas.has(b.parte) || b.produto3d) continue;
1117
+ atendidas.add(b.parte);
1118
+ if (/^(nenhum|nenhuma|nada|none)$/i.test(pedido)) continue;
1119
+ if (ABSTRATOS.includes(pedido)) { b.heroiPedido = pedido; continue; }
1120
+ if (ICONES3D.has(pedido)) { porIcone(b, pedido); ultimoIcone = bi; continue; }
1121
+ if (/^[a-z0-9-]+:[a-z0-9-]+$/i.test(pedido) && icones[pedido]) {
1122
+ porIcone(b, pedido, { svg: svgLimpo(icones[pedido]), cor: b.cores.acento, cor2: b.cores.texto });
1123
+ ultimoIcone = bi;
1124
+ }
1125
+ continue;
1126
+ }
1127
+ if (nIcones >= tetoIcones) continue;
1128
+ if (bi - ultimoIcone < folga) continue;
907
1129
  if (b.produto3d || ['parede', 'marca', 'citacao', 'divisao'].includes(b.comp)) continue;
908
1130
  const ditas = tokens(b.linhas.map((l) => l.texto).join(' ') + ' ' + ((partes[b.parte] && partes[b.parte].fala) || ''));
909
1131
  const ic = ditas.map((w) => ICONE_DE[w]).find((x) => x && !iconesUsados.has(x));
910
1132
  if (!ic) continue;
911
- iconesUsados.add(ic);
912
- const prim = b.linhas[0];
913
- b.icone3d = { icone: ic, lado: deitado ? 'direita' : 'acima', tamanho: Math.round(deitado ? 0.4 * H : 0.38 * W), material,
914
- cor: b.cores.texto, cor2: b.cores.acento, luz: b.cores.acento, t: r3(Math.min(b.ate - 1, (prim ? prim.t : b.de) + 0.15)) };
915
- b.acentos += 1;
916
- if (b.acentos > REGRAS.acentos) { b.icone3d.cor2 = misturarCor(b.cores.texto, b.cores.fundo, 0.3); b.acentos -= 1; }
1133
+ porIcone(b, ic);
1134
+ ultimoIcone = bi;
917
1135
  }
918
1136
 
919
- /* ── o HERÓI GRÁFICO: toda batida tem uma ideia visual grande além do
920
- texto, como no reel de referência — a explosão radial que nasce de um
921
- ponto, o polígono que vira estrela e quadrado com partículas em órbita,
922
- o campo de truchet com uma onda de cor, a esfera e o toro de pontos em
923
- 3D, os anéis de órbita, a grade que deforma, os rastros de velocidade.
924
- Procedurais, da semente, na paleta da batida; nunca o mesmo seguido. ── */
925
- const OBJETOS = ['explosao', 'poligono', 'esfera', 'toro'], CAMPOS_H = ['truchet', 'aneis', 'grade', 'rastro'];
926
- const usosH = {};
927
- let ultimoH = null;
928
- const recentesH = [];
1137
+ // A forma abstrata: a que o modelo pediu, ou a do projeto no assunto que aceita.
1138
+ const OBJETOS = HEROIS_OBJETO;
1139
+ const favorito = (ABSTRATO_DO_HUMOR[humor] || 0) > 0 ? E.herois[0] : null;
1140
+ const tetoH = Math.ceil(batidas.length / 3);
1141
+ let nH = 0, ultimoH = -9;
929
1142
  batidas.forEach((b, i) => {
930
- const ocupado = b.icone3d || b.produto3d;
931
- let op = ocupado || ['gigante', 'parede', 'divisao'].includes(b.comp) ? CAMPOS_H : [...OBJETOS, ...CAMPOS_H];
932
- if (b.comp === 'parede') op = ['aneis', 'rastro', 'grade'];
933
- const w = {};
934
- for (const h of op) {
935
- let q = OBJETOS.includes(h) ? 2.2 : 1;
936
- if (recentesH.slice(-3).includes(h)) q = 0;
937
- if (h === 'truchet' && (usosH.truchet || 0) >= 1) q = 0;
938
- if ((h === 'esfera' || h === 'toro') && (usosH[h] || 0) >= 1) q = 0;
939
- q *= Math.pow(0.5, usosH[h] || 0);
940
- w[h] = q;
941
- }
942
- // Tudo recente: o menos recente de todos os de campo.
943
- const reserva = () => CAMPOS_H.filter((x) => !recentesH.slice(-3).includes(x))[0] || CAMPOS_H[0];
944
- let h = i === 0 && b.comp === 'marca' ? 'explosao' : s.pesado(w) || reserva();
945
- usosH[h] = (usosH[h] || 0) + 1;
946
- ultimoH = h;
947
- recentesH.push(h);
1143
+ let h = b.heroiPedido || null;
1144
+ if (!h && favorito && !b.icone3d && !b.produto3d && !pedidoDe[b.parte] && nH < tetoH && i - ultimoH >= 2
1145
+ && !(OBJETOS.includes(favorito) && ['gigante', 'parede', 'divisao'].includes(b.comp))) h = favorito;
1146
+ delete b.heroiPedido;
1147
+ if (!h) return;
1148
+ nH++; ultimoH = i;
948
1149
  const comCor = b.acentos < REGRAS.acentos;
949
1150
  if (comCor) b.acentos += 1;
950
1151
  b.heroi = { tipo: h, modo: OBJETOS.includes(h) ? 'objeto' : 'campo', semente: hash(b.id + h) % 9973,
@@ -959,7 +1160,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
959
1160
  const a = batidas[i - 1];
960
1161
  let tr;
961
1162
  // O campo troca por corte seco, por cortina ou pela inundação do acento — independente do gesto do texto.
962
- b.troca = a.campo === b.campo ? null : s.pesado({ corte: 5, cortina: 2.5, inunda: 1.5 });
1163
+ // A primeira virada de seção é um corte seco (o reel corta); as outras, sorteadas.
1164
+ b.troca = a.campo === b.campo ? null : !batidas.slice(1, i).some((x) => x.troca) ? 'corte' : s.pesado({ corte: 5, cortina: 2.5, inunda: 1.5 });
963
1165
  if (b.troca === 'inunda' && s.chance(0.5)) tr = 'inunda';
964
1166
  else {
965
1167
  const tese = b.linhas.find((l) => l.papel === 'tese' || l.papel === 'numero');
@@ -1067,13 +1269,34 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
1067
1269
  // Partículas em três profundidades: o espaço respira mesmo no repouso.
1068
1270
  particulas: Array.from({ length: 34 }, (_, i) => ({ x: r1(s.prox() * W), y: r1(s.prox() * H), z: [0.25, 0.5, 0.85][i % 3], r: r1((1.2 + s.prox() * 2.4) * base), v: r1(s.entre(6, 22) * base) })) };
1069
1271
  if (roteiro.particulas !== true) fundo.particulas = [];
1272
+ // O ambiente e o jeito do acento: o roteiro, a escolha do projeto, ou o limpo e o traço.
1273
+ fundo.ambiente = AMBIENTES.includes(roteiro.ambiente) ? roteiro.ambiente : E.ambiente || 'limpo';
1274
+ fundo.acento = ACENTOS_JEITO.includes(roteiro.acento) ? roteiro.acento : E.acento || 'traco';
1070
1275
 
1071
1276
  return {
1072
1277
  versao: 1, semente, fps: FPS, humor,
1073
- musica: { genero: s.um(H0.generos), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
1278
+ musica: { genero: ou(E.genero, s.um(H0.generos)), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
1074
1279
  paleta: { ...P, fundo: campos[0], tinta: campos[2], acento: campos[1] }, campos, fontes, eixos: Object.fromEntries(Object.values(fontes).map((f) => [f, (VARIAVEIS[f] || {}).eixos || { wght: [400, 700] }])),
1075
1280
  margem: M, fundo, batidas, camera, produtos,
1076
- hud: roteiro.hud === true ? { marca: String((marca.nome || roteiro.titulo || 'PrimoCode')).toUpperCase().slice(0, 24), ano: '2026' } : null,
1281
+ identidade: { chaos: ident.chaos, acento: ident.acento, comeca: campos[comeca], fontes: [display, texto] }, secoes: secoes.nomes,
1282
+ hud: molduraDe(roteiro, E, { W, H, deitado, marca, secoes: secoes.nomes }),
1283
+ };
1284
+ }
1285
+
1286
+ /* A moldura do vídeo: o pedido do roteiro (moldura, ou o hud antigo) manda;
1287
+ sem pedido, a escolha do projeto; sem as duas, nenhuma. A ÁREA SEGURA: no
1288
+ 9:16 o celular cobre o alto (status, nome do perfil) e o pé (legenda,
1289
+ botões) — a moldura fica dentro; no 16:9, margem fina e igual. */
1290
+ function molduraDe(roteiro, E, { W, H, deitado, marca, secoes }) {
1291
+ const m = roteiro.moldura;
1292
+ const tipo = m === false ? 'sem' : m === true ? 'completa' : MOLDURAS.includes(m) ? m : roteiro.hud === true ? 'completa' : E.moldura || 'sem';
1293
+ if (tipo === 'sem') return null;
1294
+ const assunto = String(marca.nome || roteiro.titulo || 'PrimoCode').split(/\s+[—–-]\s+/)[0];
1295
+ const d = Math.round(Math.min(W, H) * 0.04);
1296
+ return {
1297
+ tipo, marca: assunto.toUpperCase().slice(0, deitado ? 32 : 22), ano: '2026',
1298
+ secoes: secoes.map((n, i) => (String(n || '').toUpperCase().slice(0, deitado ? 26 : 16) || 'PARTE ' + (i + 1))),
1299
+ area: deitado ? { x: d, topo: d, base: d } : { x: Math.round(W * 0.06), topo: Math.round(H * 0.08), base: Math.round(H * 0.15) },
1077
1300
  };
1078
1301
  }
1079
1302
 
@@ -1290,7 +1513,8 @@ function compor(comp, br, ctx) {
1290
1513
  b.bloco = { x: W / 2, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: larguraUtil, gap: 0.1 };
1291
1514
  linha(txt, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(900, VARIAVEIS[fonte].eixos.wght[1]) }, maiusc });
1292
1515
  b.textura = { texto: (maiusc ? txt.toUpperCase() : txt), tamanho: tam, fonte, linhas: Math.ceil(H / (tam * 1.02)) + 3, velocidade: Math.round(s.entre(40, 90) * base) };
1293
- b.acento.push({ forma: s.chance(0.5) ? 'moldura' : 'sublinha', alvo: { linha: b.linhas[0].id } });
1516
+ // Moldura em volta da palavra é vício de "cara de IA" (a caixa na palavra-chave); sai em 08/10.
1517
+ b.acento.push({ forma: s.chance(0.5) ? 'ponto' : 'sublinha', alvo: { linha: b.linhas[0].id } });
1294
1518
  break;
1295
1519
  }
1296
1520
  default: throw new Error('composição desconhecida: ' + comp);
@@ -1466,6 +1690,6 @@ function contarAcentos(b) {
1466
1690
  }
1467
1691
 
1468
1692
  module.exports = {
1469
- montarGerativo, partituraDe, sonsDoVideo, musicaDe, desenharSom, humorDe, corGenerica, BANIDAS, HUMORES, REGRAS, VARIAVEIS, COMPOSICOES, COMPORTAMENTOS, SAIDAS, ACENTOS, FPS,
1470
- especEixos, sorteador, hash, larguraEm, conteudoDe, casar,
1693
+ montarGerativo, partituraDe, sonsDoVideo, musicaDe, desenharSom, humorDe, escolherVisual, MOLDURAS, AMBIENTES, ACENTOS_JEITO, ARRANJOS_ICONE, RITMOS_ICONE, MATERIAIS_ICONE, corGenerica, BANIDAS, HUMORES, REGRAS, VARIAVEIS, COMPOSICOES, COMPORTAMENTOS, SAIDAS, ACENTOS, FPS,
1694
+ especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO,
1471
1695
  };
@@ -49,7 +49,7 @@ function traduzir(roteiro) {
49
49
  partes.forEach((p, i) => {
50
50
  const tipo = p.tipo || 'frase';
51
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 };
52
+ const base = { fala: p.fala, ...(p.mudo ? { mudo: true } : {}), ...(p.falaForte ? { falaForte: p.falaForte } : {}), ...(p.falante ? { falante: p.falante } : {}), marcar };
53
53
  let q;
54
54
  // Os tipos que o cinema já conhece passam como estão.
55
55
  if (['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'].includes(tipo)) q = { ...p };