primocode 10.4.3 → 10.4.7

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.4.3
1
+ # PrimoCode v10.4.7
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v10.4.3
12
+ ▐▛███▜▌ Primo Code v10.4.7
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/bin/primocode.js CHANGED
@@ -2875,7 +2875,7 @@ async function main() {
2875
2875
  vozNaTarefa = false;
2876
2876
  const ultima = state.history.slice().reverse().find((m) => m.role === 'assistant');
2877
2877
  if (ultima && ultima.content) vozEmEspera.falar(ultima.content);
2878
- }
2878
+ } else if (vozEmEspera && vozEmEspera.estado) vozEmEspera.estado('esperando');
2879
2879
  // Recado que chegou tarde demais para a tarefa (ela já tinha
2880
2880
  // acabado): vira o próximo pedido, na ordem. Nada se perde.
2881
2881
  queue.unshift(...sobrasDaCaixa());
package/lib/catalogo.js CHANGED
@@ -93,7 +93,7 @@ const GRUPOS = [
93
93
  + 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
94
94
  + 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
95
95
  + 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
96
- + 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa|false: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — ponha só se o vídeo pede cara de peça de design; vazio, o PrimoCode alterna com memória e às vezes não põe; heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; procure com estudio_midia acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
96
+ + 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; procure com estudio_midia acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
97
97
  + 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (o vídeo GRAVADO em 3D — cada parte é UMA fala {falante, texto, emocao?, destaque?}, sem tipo: personagens modelados — gente ou robô — conversando num set de podcast/estúdio/escritório, a boca segue a voz, gestos, e a câmera corta entre aberto, ombro e close; use quando o pedido fala em personagem, boneco, avatar, podcast, entrevista, ou alguém FALANDO em 3D. Com dois falantes (falante em cada parte) eles conversam; personagens:{"nome":{tipo:robo|pessoa, pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba, rotulo}} — nome de IA vira robô sozinho; emocao feliz|serio|surpreso|pensativo|animado|curioso e destaque:"texto curto" (o cartão na tela) em cada parte; cenario podcast|estudio|escritorio|noite). '
98
98
  + 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
99
99
  + 'voz (thalita|antonio|francisca). DUAS VOZES (o pedido fala em dois narradores, diálogo, conversa, entrevista): roteiro.vozes {"nome":"antonio","outro":"thalita"} e falante:"nome" em cada parte — cada um sai na sua voz. Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
@@ -668,6 +668,6 @@ module.exports = {
668
668
  MODELO_CLAUDE, argsDoModelo, argsDoMotor,
669
669
  SKILL_DIR, SKILL_PATH, garantirSkill,
670
670
  temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
671
- garantirEngine, requestAct, emitirDecisao,
671
+ garantirEngine, requestAct, emitirDecisao, rodarClaude,
672
672
  _emitirDecisao: emitirDecisao,
673
673
  };
package/lib/entrada.js CHANGED
@@ -191,6 +191,10 @@ function montar(e) {
191
191
  if (st.linhas) {
192
192
  for (const linha of st.linhas) out.push(' ' + truncate(linha, largura - 2));
193
193
  } else {
194
+ /* Uma linha vazia entre o painel e o rodapé. Colado, o rodapé parecia
195
+ SOBREPOSTO ao painel em terminais de entrelinha apertada (o Orca):
196
+ o fundo do painel encostava nas letras da pasta. */
197
+ out.push('');
194
198
  const esq = st.esquerda || '';
195
199
  const dir = st.direita || '';
196
200
  const sobra = largura - 2 - visibleWidth(esq) - visibleWidth(dir);
@@ -141,7 +141,16 @@ const CONFIANCA_MINIMA = 0.7; // abaixo disso a batida é chute: a faixa entra
141
141
 
142
142
  const TRANSICOES_FORTES = new Set(['soco', 'flash', 'chicote', 'zoom', 'giro']);
143
143
 
144
- /** Os cortes do plano no tempo do vídeo: [{t, peso}]. Corte de impacto pesa 3; o maior impacto vira `impacto`. */
144
+ /**
145
+ * Os cortes do plano no tempo do vídeo: [{t, peso}]. Corte de impacto pesa 3; o maior impacto vira `impacto`.
146
+ *
147
+ * O GERATIVO É UMA CENA SÓ: as batidas dele moram dentro da partitura do kit
148
+ * (cena.elementos[].partitura.batidas). Contando só as cenas, o vídeo dele
149
+ * tinha 0 cortes, e o log dizia "0 de 0 cortes caem na batida" (10/10). Cada
150
+ * batida (menos a primeira) é um corte; a troca de campo (fundo de cor) e a
151
+ * tese gigante pesam 3. O corte leva a referência da batida para ser
152
+ * ENCOSTADO na batida da música depois (melhorInicio).
153
+ */
145
154
  function cortesDoPlano(plano) {
146
155
  const cortes = [];
147
156
  let t = 0;
@@ -150,35 +159,111 @@ function cortesDoPlano(plano) {
150
159
  const tr = cena.transicao && (cena.transicao.tipo || cena.transicao);
151
160
  cortes.push({ t: r3(t), peso: cena.impacto || TRANSICOES_FORTES.has(tr) ? 3 : 1 });
152
161
  }
162
+ for (const el of cena.elementos || []) {
163
+ const bs = el && el.partitura && Array.isArray(el.partitura.batidas) ? el.partitura.batidas : null;
164
+ if (!bs) continue;
165
+ bs.forEach((b, j) => {
166
+ if (j === 0) return;
167
+ const forte = b.troca || b.comp === 'gigante' || b.campoCor;
168
+ // A palavra que a batida mostra: a entrada da primeira linha (sem linha, a batida mesmo).
169
+ const ts = (b.linhas || []).map((l) => l.t).filter((x) => typeof x === 'number');
170
+ const palavra = ts.length ? Math.min(...ts) : b.de;
171
+ cortes.push({ t: r3(t + b.de), peso: forte ? 3 : 1, ref: { bs, j, base: t, palavra } });
172
+ });
173
+ }
153
174
  t += Number(cena.duracao) || 0;
154
175
  }
155
- return cortes;
176
+ return cortes.sort((a, b) => a.t - b.t);
177
+ }
178
+
179
+ /** A batida mais perto de `t` numa lista ordenada: {bt, d}. */
180
+ function maisPerto(lista, t) {
181
+ let lo = 0, hi = lista.length - 1;
182
+ while (lo < hi) { const m = (lo + hi) >> 1; if (lista[m] < t) lo = m + 1; else hi = m; }
183
+ const a = lista[lo], b = lo > 0 ? lista[lo - 1] : Infinity;
184
+ return Math.abs(a - t) <= Math.abs(b - t) ? { bt: a, d: Math.abs(a - t) } : { bt: b, d: Math.abs(b - t) };
185
+ }
186
+
187
+ /* Quanto a troca de tela pode se afastar da PALAVRA que ela mostra: a fala
188
+ continua onde estava, e a troca adianta ou atrasa a palavra em no máximo
189
+ 150 ms — abaixo do que o olho percebe como fora de sincronia. A janela é
190
+ medida da palavra, não do corte de antes (que já vinha ~120 ms adiantado). */
191
+ const EMPURRAO = 0.15;
192
+ /** A janela [de, até] em que o corte pode cair, no tempo do vídeo. */
193
+ const janelaDo = (c) => (c.ref ? [c.ref.base + c.ref.palavra - EMPURRAO, c.ref.base + c.ref.palavra + EMPURRAO] : [c.t, c.t]);
194
+ /** A batida da música (no tempo do vídeo) dentro da janela, a mais perto do corte; ou null. */
195
+ function batidaNaJanela(bats, c, o) {
196
+ const [a, z] = janelaDo(c);
197
+ let lo = 0, hi = bats.length;
198
+ while (lo < hi) { const m = (lo + hi) >> 1; if (bats[m] - o < a) lo = m + 1; else hi = m; }
199
+ let melhor = null;
200
+ for (let i = lo; i < bats.length && bats[i] - o <= z; i++) {
201
+ const v = bats[i] - o;
202
+ if (!melhor || Math.abs(v - c.t) < Math.abs(melhor - c.t)) melhor = v;
203
+ }
204
+ return melhor;
156
205
  }
206
+ const MIN_BATIDA = 0.6; // nenhuma batida fica mais curta que isto depois do empurrão
157
207
 
158
208
  /**
159
209
  * De onde a faixa começa (em s) para o máximo de cortes cair numa batida.
160
210
  * Procura em passos de 10 ms, de 0 a `maximo`; empate fica com o início mais
161
211
  * cedo. O corte mais pesado ganha bônus se cair num acento.
212
+ *
213
+ * Os cortes do gerativo (com `ref`) contam como "na batida" até EMPURRAO de
214
+ * distância, porque depois da escolha eles são ENCOSTADOS nela: a batida do
215
+ * vídeo anda (b.de e o ate da anterior), e o golpe da tese também. O plano é
216
+ * mudado no lugar — é ele que vai para o render.
162
217
  */
163
218
  function melhorInicio(cortes, analise, { maximo = 0, tolerancia = 0.05 } = {}) {
164
219
  const bats = (analise && analise.batidas) || [];
165
220
  if (!cortes.length || bats.length < 4 || (analise.confianca != null && analise.confianca < CONFIANCA_MINIMA)) return { inicio: 0, alinhados: 0, total: cortes.length };
166
- const perto = (lista, t) => {
167
- let lo = 0, hi = lista.length - 1;
168
- while (lo < hi) { const m = (lo + hi) >> 1; if (lista[m] < t) lo = m + 1; else hi = m; }
169
- return Math.min(Math.abs(lista[lo] - t), lo > 0 ? Math.abs(lista[lo - 1] - t) : Infinity);
170
- };
221
+ const perto = (lista, t) => maisPerto(lista, t).d;
171
222
  const maior = cortes.reduce((a, b) => (b.peso > a.peso ? b : a), cortes[0]);
172
223
  const acentos = (analise.acentos || []);
173
224
  let melhor = { inicio: 0, pontos: -1 };
174
225
  for (let o = 0; o <= Math.max(0, maximo) + 1e-9; o += 0.01) {
175
226
  let pontos = 0;
176
- for (const c of cortes) pontos += c.peso * Math.exp(-0.5 * Math.pow(perto(bats, c.t + o) / tolerancia, 2));
227
+ for (const c of cortes) {
228
+ if (c.ref) {
229
+ const v = batidaNaJanela(bats, c, o);
230
+ if (v != null) pontos += c.peso * (1 - 0.3 * Math.min(1, Math.abs(v - c.t) / (2 * EMPURRAO)));
231
+ } else pontos += c.peso * Math.exp(-0.5 * Math.pow(perto(bats, c.t + o) / tolerancia, 2));
232
+ }
177
233
  if (maior.peso > 1 && acentos.length && perto(acentos, maior.t + o) < 0.1) pontos += 2;
178
234
  if (pontos > melhor.pontos + 1e-6) melhor = { inicio: r3(o), pontos };
179
235
  }
180
- const alinhados = cortes.filter((c) => perto(bats, c.t + melhor.inicio) <= tolerancia).length;
181
- return { inicio: melhor.inicio, alinhados, total: cortes.length };
236
+ const o = melhor.inicio;
237
+ // O empurrão: cada corte do gerativo vai para a batida mais perto, se couber.
238
+ let empurrados = 0;
239
+ for (const c of cortes) {
240
+ if (!c.ref) continue;
241
+ const { bs, j, base } = c.ref;
242
+ const v = batidaNaJanela(bats, c, o);
243
+ if (v == null || Math.abs(v - c.t) <= 1e-3) continue;
244
+ const delta = v - c.t;
245
+ const b = bs[j], a = bs[j - 1];
246
+ const novo = b.de + delta;
247
+ if (novo - a.de < MIN_BATIDA || b.ate - novo < MIN_BATIDA) continue;
248
+ const colado = Math.abs(a.ate - b.de) < 0.02;
249
+ b.de = r3(novo);
250
+ if (colado) a.ate = b.de;
251
+ c.t = r3(base + b.de);
252
+ empurrados++;
253
+ }
254
+ // O golpe (a tese que bate) também cai na batida, sem sair da batida dele.
255
+ const vistos = new Set();
256
+ for (const c of cortes) {
257
+ if (!c.ref || vistos.has(c.ref.bs)) continue;
258
+ vistos.add(c.ref.bs);
259
+ for (const b of c.ref.bs) for (const g of b.golpes || []) {
260
+ const { bt, d } = maisPerto(bats, c.ref.base + g.t + o);
261
+ const alvo = bt - o - c.ref.base;
262
+ if (d <= EMPURRAO && alvo > b.de + 0.05 && alvo < b.ate - 0.1) g.t = r3(alvo);
263
+ }
264
+ }
265
+ const alinhados = cortes.filter((c) => perto(bats, c.t + o) <= tolerancia).length;
266
+ return { inicio: o, alinhados, total: cortes.length, empurrados };
182
267
  }
183
268
 
184
269
  module.exports = { analisarPCM, analisar, cortesDoPlano, melhorInicio, TAXA, CONFIANCA_MINIMA };
@@ -265,7 +265,7 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
265
265
  roteiro É do assunto (uma palavra só não basta). UMA forma por vídeo —
266
266
  é identidade, não sorteio por batida — ou 'nenhum', que a memória também gira. */
267
267
  const forca = (ABSTRATO_DO_HUMOR[humor] || 0) * Math.min(1, n[humor] / 3);
268
- 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
+ const h1 = forca > 0 ? variedade.sortear([...HEROIS_3D, ...HEROIS_OBJETO, ...HEROIS_CAMPO, 'nenhum'], 'heroi-gerativo', { aleatorio, adequacao: (h) => (h === 'nenhum' ? 1 - forca : HEROIS_3D.includes(h) ? forca * 1.3 : forca * fitH(h)) }) : 'nenhum';
269
269
  out.herois = { favoritos: h1 === 'nenhum' ? [] : [h1] };
270
270
  // 'produto' e 'marca' quem decide é o conteúdo (o 3D pedido, a marca), não o gosto do projeto.
271
271
  const favC = [];
@@ -274,7 +274,10 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
274
274
  /* A MOLDURA (09/10): o quadro fixo do reel de referência — cantos, o assunto
275
275
  no alto, a seção, o tempo embaixo — é identidade, mas em todo vídeo vira
276
276
  vício. Sai do cardápio com memória: 'sem' pesa um pouco mais. */
277
- out.moldura = variedade.sortear(MOLDURAS, 'moldura-gerativo', { aleatorio, adequacao: { sem: 1, cantos: 0.95, completa: 0.95 } });
277
+ /* (10/10) "Essa borda entrega que foi IA que fez" (Joel): os cantos, o
278
+ "01 · seção" e o tempo embaixo são a assinatura de vídeo gerado. Saiu
279
+ do sorteio: só entra quando o roteiro PEDE (moldura: cantos|completa). */
280
+ out.moldura = 'sem';
278
281
  // O bokeh combina com luz de tela (tecnologia, energia, natureza); numa padaria, quase nunca.
279
282
  const bokeh = ['tecnologia', 'energia', 'natureza'].includes(humor) ? 0.6 : 0.2;
280
283
  out.ambiente = variedade.sortear(AMBIENTES, 'ambiente-gerativo', { aleatorio, adequacao: { limpo: 1, luz: 0.85, bokeh } });
@@ -298,6 +301,18 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
298
301
  O assunto decide quanto 3D: tecnologia e energia puxam os objetos (esfera,
299
302
  toro, polígono, explosão); editorial e luxo, os campos. */
300
303
  const HEROIS_OBJETO = ['explosao', 'poligono', 'esfera', 'toro'], HEROIS_CAMPO = ['truchet', 'aneis', 'grade', 'rastro'];
304
+ /* O 3D DE VERDADE (10/10, "quero um 3d bem presente", com o reel de
305
+ referência): three.js, perspectiva, luz — grande na tela, não um enfeite.
306
+ globo3d: a esfera de pontos e malha girando em profundidade; fita3d: o nó
307
+ que se torce, com material e luz de recorte; colunas3d: o campo de colunas
308
+ que a câmera sobrevoa, atrás do texto. (kit/tres.jsx, Heroi3D) */
309
+ const HEROIS_3D = ['globo3d', 'fita3d', 'colunas3d'];
310
+ const CAMPOS_3D = ['colunas3d'];
311
+ /* A FAMÍLIA de cada herói: dois heróis da mesma família seguidos são o
312
+ "mesmo desenho" para quem assiste (o anel de pontos com a forma no meio
313
+ saiu em 4 batidas seguidas no vídeo do Joel, 10/10). */
314
+ const FAMILIA_HEROI = { explosao: 'raio', poligono: 'orbita', aneis: 'orbita', esfera: 'malha', toro: 'malha', globo3d: 'malha',
315
+ fita3d: 'fita', colunas3d: 'campo3d', truchet: 'azulejo', grade: 'grade', rastro: 'rastro' };
301
316
  const OBJETO3D_DO_HUMOR = { tecnologia: 1, energia: 1, natureza: 0.8, luxo: 0.6, editorial: 0.4 };
302
317
  /* Quanto o assunto aceita forma abstrata (o resto: a tipografia e o ícone do conteúdo). */
303
318
  const ABSTRATO_DO_HUMOR = { tecnologia: 1, energia: 0.7, natureza: 0.5, luxo: 0, editorial: 0 };
@@ -327,7 +342,7 @@ function escolhasValidas(e) {
327
342
  texto: todas('textos').includes(e.texto) ? e.texto : null,
328
343
  mono: todas('monos').includes(e.mono) ? e.mono : null,
329
344
  genero: todas('generos').includes(e.genero) ? e.genero : null,
330
- herois: (e.herois && Array.isArray(e.herois.favoritos) ? e.herois.favoritos : []).filter((h) => HEROIS_OBJETO.includes(h) || HEROIS_CAMPO.includes(h)),
345
+ herois: (e.herois && Array.isArray(e.herois.favoritos) ? e.herois.favoritos : []).filter((h) => HEROIS_OBJETO.includes(h) || HEROIS_CAMPO.includes(h) || HEROIS_3D.includes(h)),
331
346
  composicoes: (e.composicoes && Array.isArray(e.composicoes.favoritas) ? e.composicoes.favoritas : []).filter((c) => COMPOSICOES.includes(c)),
332
347
  icones: e.icones && ARRANJOS_ICONE.includes(e.icones.arranjo) ? {
333
348
  arranjo: e.icones.arranjo,
@@ -570,6 +585,9 @@ function frasesDe(fala, palavras) {
570
585
 
571
586
  // ── as cores ────────────────────────────────────────────────────────────
572
587
  const luz = (h) => { const n = parseInt(String(h).slice(1), 16); return ((n >> 16 & 255) * 0.299 + (n >> 8 & 255) * 0.587 + (n & 255) * 0.114) / 255; };
588
+ /** A luminância relativa do WCAG (com o gama) e o contraste por ela. */
589
+ const lumWcag = (h) => { const n = parseInt(String(h).slice(1), 16); const c = [n >> 16 & 255, n >> 8 & 255, n & 255].map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; };
590
+ const contrasteWcag = (a, b) => { const la = lumWcag(a) + 0.05, lb = lumWcag(b) + 0.05; return la > lb ? la / lb : lb / la; };
573
591
  const contraste = (a, b) => { const la = luz(a) + 0.05, lb = luz(b) + 0.05; return la > lb ? la / lb : lb / la; };
574
592
  const PALETAS = HUMORES.tecnologia.paletas;
575
593
 
@@ -999,6 +1017,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
999
1017
  const comp = nome === 'rajada' ? 'gigante' : nome === 'duelo' ? 'pilha' : nome;
1000
1018
  let q = p;
1001
1019
  if (comp === anterior || evita.includes(comp)) q = 0;
1020
+ // No máximo 2 vezes no vídeo (10/10: "cascata" saiu 3 vezes no vídeo do Joel).
1021
+ if ((usadas[comp] || 0) >= 2) q = 0;
1002
1022
  if (recentes.slice(-3).includes(comp)) q *= 0.35;
1003
1023
  q *= Math.pow(0.72, usadas[comp] || 0);
1004
1024
  if (comp === 'parede' && (usadas.parede || 0) >= 1) q *= 0.25;
@@ -1033,6 +1053,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1033
1053
  if (bi === alvo3d && !usadas.produto) nome = 'produto';
1034
1054
  const ctx = { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes, P, palavras: br.palavras || c.palavras || [], parte: br.parte, marca, roteiro, memoria };
1035
1055
  if (nome === 'rajada' && br.ate - br.de < 1.6 * c.itens.length) nome = 'cascata';
1056
+ // A cascata que veio da rajada também respeita o teto de 2 (e nunca segue outra cascata).
1057
+ if (nome === 'cascata' && ((usadas.cascata || 0) >= 2 || anterior === 'cascata')) nome = anterior === 'pilha' ? 'palavras' : 'pilha';
1036
1058
  if (nome === 'rajada') {
1037
1059
  // Cada item vira a sua batida; o título da lista fica como rótulo.
1038
1060
  const n = c.itens.length;
@@ -1075,6 +1097,45 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1075
1097
  batidas.forEach((b) => { b.secao = secoes.idx[b.parte] || 0; b.campo = b.secao % 2 === 0 ? comeca : 2 - comeca; });
1076
1098
  for (const b of batidas) { b.fundo = 'campo' + b.campo; b.cores = coresDoCampo(campos, b.campo, s, ident); }
1077
1099
 
1100
+ /* ── OS CAMPOS DE COR CHAPADA (10/10) ────────────────────────────────
1101
+ "O vídeo fica bege e grafite do começo ao fim" — o reel de referência
1102
+ troca o quadro INTEIRO para laranja, azul elétrico, lima. Os campos 3 e
1103
+ 4 tinham saído em 09/10 porque entravam a esmo ("cores nada a ver uma
1104
+ com a outra"); voltam com regra: só DUAS cores fortes — o acento do
1105
+ vídeo e a ousada do mesmo sistema (campos[3]) —, em ~1 de cada 3,5
1106
+ batidas (pelo menos 2 num vídeo de 6+), nunca duas seguidas, nunca a
1107
+ primeira nem a marca. Tinta de contraste alto; o corte é seco (o fundo
1108
+ troca no quadro da batida, que o batida.js encosta na música). */
1109
+ // A segunda cor é a ousada do sistema MAIS LONGE no matiz do acento: duas laranjas lado a lado
1110
+ // (o terracota e o #FF4A1C, render real de 10/10) não fazem o contraste do reel (laranja × azul).
1111
+ const matiz = (h) => { const n = parseInt(String(h).slice(1), 16); const r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, bb = (n & 255) / 255;
1112
+ const mx = Math.max(r, g, bb), mn = Math.min(r, g, bb), d = mx - mn; if (!d) return 0;
1113
+ const hh = mx === r ? ((g - bb) / d) % 6 : mx === g ? (bb - r) / d + 2 : (r - g) / d + 4; return (hh * 60 + 360) % 360; };
1114
+ const longe = (x) => { const d = Math.abs(matiz(x) - matiz(ident.acento)); return Math.min(d, 360 - d); };
1115
+ const ousada = [campos[3], campos[4]].filter(ehCor).sort((x, y) => longe(y) - longe(x))[0];
1116
+ const fortes = [ident.acento, ousada].filter((x, i, a) => ehCor(x) && a.indexOf(x) === i);
1117
+ const nCor = batidas.length >= 6 ? Math.max(2, Math.round(batidas.length / 3.5)) : batidas.length >= 4 ? 1 : 0;
1118
+ const candCor = batidas.map((b, i) => i).filter((i) => i > 0 && !['marca', 'produto'].includes(batidas[i].comp));
1119
+ const pesoCor = { gigante: 3, palavras: 2.5, contador: 2.5, pilha: 2, parede: 1.5, divisao: 1, cascata: 0.8, citacao: 0.8 };
1120
+ const comCor = [];
1121
+ for (const i of candCor.sort((x, y) => (pesoCor[batidas[y].comp] || 1) - (pesoCor[batidas[x].comp] || 1) || x - y)) {
1122
+ if (comCor.length >= nCor || !fortes.length) break;
1123
+ if (comCor.some((j) => Math.abs(j - i) < 2)) continue;
1124
+ comCor.push(i);
1125
+ }
1126
+ comCor.sort((x, y) => x - y).forEach((i, k) => {
1127
+ const b = batidas[i];
1128
+ const fundo = fortes[k % fortes.length], outra = fortes[(k + 1) % fortes.length];
1129
+ /* A tinta pelo contraste WCAG (luminância relativa, com o gama): a `luz`
1130
+ daqui acha o azul elétrico "médio" e punha texto PRETO nele — o olho
1131
+ lê muito melhor o branco. */
1132
+ const tinta = [...ident.chaos, '#0B0B0B', '#FAFAF7'].sort((x, y) => contrasteWcag(y, fundo) - contrasteWcag(x, fundo))[0];
1133
+ const acento = outra && outra !== fundo && contrasteWcag(outra, fundo) >= 2.6 ? outra : tinta;
1134
+ b.cores = { fundo, texto: tinta, fraco: misturarCor(tinta, fundo, 0.32), acento, contorno: misturarCor(tinta, fundo, 0.5) };
1135
+ b.campoCor = true;
1136
+ b.fundo = 'cor';
1137
+ });
1138
+
1078
1139
  /* ── O HERÓI DE CADA BATIDA VEM DO CONTEÚDO (09/10): "é uma conversa sobre
1079
1140
  pão e ele vai naquela bolinha" (Joel). Quem diz é o roteiro: parte.heroi
1080
1141
  (ou parte.icone) — um ícone do Iconify ("mdi:bread-slice", o desenho
@@ -1084,7 +1145,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1084
1145
  e a forma abstrata só no assunto abstrato, a escolhida do projeto, em
1085
1146
  até 1 de cada 3 batidas. Nada serve: a tipografia carrega a batida. ── */
1086
1147
  const pedidoDe = partes.map((p) => { const h = p.heroi != null ? p.heroi : p.icone; return typeof h === 'string' && h.trim() ? h.trim() : null; });
1087
- const ABSTRATOS = [...HEROIS_OBJETO, ...HEROIS_CAMPO];
1148
+ const ABSTRATOS = [...HEROIS_OBJETO, ...HEROIS_CAMPO, ...HEROIS_3D];
1088
1149
  const ICONES3D = new Set(Object.values(ICONE_DE));
1089
1150
  const materialSorteado = s.um(['argila', 'verniz', 'metal']);
1090
1151
  // O arranjo do projeto (escolherVisual); sem ele, o de antes: um lado só, ícones seguidos.
@@ -1135,24 +1196,100 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1135
1196
  ultimoIcone = bi;
1136
1197
  }
1137
1198
 
1138
- // A forma abstrata: a que o modelo pediu, ou a do projeto no assunto que aceita.
1199
+ /* ── A FORMA ABSTRATA, BATIDA A BATIDA (10/10) ───────────────────────
1200
+ Era UMA forma por vídeo, repetida a cada duas batidas: o anel com a
1201
+ forma no meio saiu em 4 batidas seguidas e "as animações são muito
1202
+ iguais" (Joel). Agora cada batida que recebe forma tira a sua de um
1203
+ cardápio, com três regras:
1204
+ 1. nunca a mesma FAMÍLIA (FAMILIA_HEROI) em duas batidas seguidas;
1205
+ 2. cada forma no máximo 2 vezes no vídeo (uma repetição);
1206
+ 3. no assunto que aceita (tecnologia, energia, natureza), pelo menos
1207
+ 1 de cada 3 batidas é 3D DE VERDADE (HEROIS_3D) — grande, com
1208
+ perspectiva e luz, não o enfeite pequeno de antes.
1209
+ A forma pedida pelo roteiro (parte.heroi) continua valendo onde pediu. */
1139
1210
  const OBJETOS = HEROIS_OBJETO;
1140
- const favorito = (ABSTRATO_DO_HUMOR[humor] || 0) > 0 ? E.herois[0] : null;
1141
- const tetoH = Math.ceil(batidas.length / 3);
1142
- let nH = 0, ultimoH = -9;
1211
+ /* O assunto aceita forma abstrata quando o humor é de tecnologia, energia
1212
+ ou natureza E o roteiro É do assunto (3 palavras do humor, como na
1213
+ escolha do projeto) — a padaria que fala de Pix não vira reel de tech. */
1214
+ const acertos = acertosDeHumor(roteiro)[humor] || 0;
1215
+ // E o projeto escolheu ter forma (escolherVisual, E.herois): sem essa escolha, o herói vem só do conteúdo.
1216
+ const aceita = (E.herois || []).length > 0 && (ABSTRATO_DO_HUMOR[humor] || 0) >= 0.5 && acertos >= 3;
1217
+ const nB = batidas.length;
1218
+ const usoH = {}, usoFam = {};
1219
+ let famAnterior = null, ultimoH = -9;
1220
+ // A parede de texto e a marca já cobrem o quadro: forma ali é ruído.
1221
+ const livreParaHeroi = (b) => !b.icone3d && !b.produto3d && !pedidoDe[b.parte] && b.comp !== 'marca' && b.comp !== 'parede';
1222
+ const elegiveis = aceita ? batidas.map((b, i) => (livreParaHeroi(b) ? i : -1)).filter((i) => i >= 0) : [];
1223
+ // Quantas batidas ganham forma: metade do vídeo (as outras ficam com a tipografia e o ícone).
1224
+ const quantas = Math.min(elegiveis.length, Math.ceil(nB / 2));
1225
+ const escolhidas = new Set();
1226
+ for (let k = 0; k < quantas; k++) escolhidas.add(elegiveis[Math.floor((k * elegiveis.length) / quantas)]);
1227
+ const meta3d = aceita ? Math.min(quantas, Math.ceil(nB / 3)) : 0;
1228
+ let n3d = 0, feitas = 0;
1229
+ const favoritos = new Set(E.herois || []);
1230
+ const sortearHeroi = (b, i, so3d) => {
1231
+ const lista = so3d ? HEROIS_3D : [...HEROIS_3D, ...HEROIS_OBJETO, 'truchet', 'aneis', 'rastro'];
1232
+ const w = {};
1233
+ const pode = (h) => w[h] > 0;
1234
+ for (const h of lista.concat([...favoritos].filter((f) => !lista.includes(f) && (!so3d || HEROIS_3D.includes(f))))) {
1235
+ if ((usoH[h] || 0) >= 2) continue;
1236
+ const fam = FAMILIA_HEROI[h];
1237
+ if (fam === famAnterior && i - ultimoH <= 2) continue;
1238
+ // Cada família uma vez no vídeo: a esfera plana depois do globo 3D lia como repetição.
1239
+ if ((usoFam[fam] || 0) >= 1) continue;
1240
+ // As formas planas que pedem o vazio ao lado não cabem na batida sem vazio.
1241
+ if (OBJETOS.includes(h) && ['gigante', 'parede', 'divisao'].includes(b.comp)) continue;
1242
+ // O campo 3D e a parede de texto brigam: as duas cobrem o quadro.
1243
+ if ((CAMPOS_3D.includes(h) || HEROIS_CAMPO.includes(h)) && b.comp === 'parede') continue;
1244
+ // O 3D que mora AO LADO do texto (globo, fita) precisa de um lado livre: a tese gigante,
1245
+ // o contraste e a citação ocupam a largura — ali ele não teria onde ficar sem cruzar a letra.
1246
+ if (HEROIS_3D.includes(h) && !CAMPOS_3D.includes(h) && ['gigante', 'divisao', 'citacao', 'contador'].includes(b.comp)) continue;
1247
+ let p = HEROIS_3D.includes(h) ? 1.6 : 1;
1248
+ if (favoritos.has(h)) p *= 2;
1249
+ p *= Math.pow(0.4, usoH[h] || 0);
1250
+ w[h] = p;
1251
+ }
1252
+ // A forma que o projeto escolheu é a identidade: entra na primeira batida em que cabe.
1253
+ const fav = [...favoritos].find((f) => !usoH[f] && pode(f));
1254
+ if (fav) return fav;
1255
+ // A batida pede 3D e a escolhida é plana: vem a versão 3D da mesma família (o toro vira o globo).
1256
+ if (so3d) {
1257
+ const irma = [...favoritos].map((f) => HEROIS_3D.find((h) => FAMILIA_HEROI[h] === FAMILIA_HEROI[f])).find((h) => h && pode(h));
1258
+ if (irma) return irma;
1259
+ }
1260
+ return s.pesado(w);
1261
+ };
1143
1262
  batidas.forEach((b, i) => {
1144
1263
  let h = b.heroiPedido || null;
1145
- if (!h && favorito && !b.icone3d && !b.produto3d && !pedidoDe[b.parte] && nH < tetoH && i - ultimoH >= 2
1146
- && !(OBJETOS.includes(favorito) && ['gigante', 'parede', 'divisao'].includes(b.comp))) h = favorito;
1147
1264
  delete b.heroiPedido;
1265
+ if (!h && escolhidas.has(i)) {
1266
+ const faltam3d = meta3d - n3d, restam = quantas - feitas;
1267
+ h = sortearHeroi(b, i, faltam3d > 0 && faltam3d >= restam) || sortearHeroi(b, i, false);
1268
+ feitas++;
1269
+ }
1148
1270
  if (!h) return;
1149
- nH++; ultimoH = i;
1271
+ usoH[h] = (usoH[h] || 0) + 1;
1272
+ usoFam[FAMILIA_HEROI[h] || h] = (usoFam[FAMILIA_HEROI[h] || h] || 0) + 1;
1273
+ famAnterior = FAMILIA_HEROI[h] || h; ultimoH = i;
1274
+ const tres = HEROIS_3D.includes(h);
1275
+ if (tres) n3d++;
1150
1276
  const comCor = b.acentos < REGRAS.acentos;
1151
1277
  if (comCor) b.acentos += 1;
1152
- b.heroi = { tipo: h, modo: OBJETOS.includes(h) ? 'objeto' : 'campo', semente: hash(b.id + h) % 9973,
1153
- cor: comCor ? b.cores.acento : misturarCor(b.cores.texto, b.cores.fundo, 0.25), cor2: b.cores.texto };
1278
+ b.heroi = { tipo: h, modo: CAMPOS_3D.includes(h) ? 'campo' : tres ? 'objeto3d' : OBJETOS.includes(h) ? 'objeto' : 'campo',
1279
+ tres, semente: hash(b.id + h) % 9973,
1280
+ cor: comCor ? b.cores.acento : misturarCor(b.cores.texto, b.cores.fundo, 0.25), cor2: b.cores.texto, fundo: b.cores.fundo };
1154
1281
  });
1155
1282
 
1283
+ /* O 3D COMO ASSUNTO (10/10, "uma coisa gigante por tela"): na batida com
1284
+ globo ou fita, o texto vai para a metade esquerda (no 9:16, para o alto)
1285
+ e a outra metade fica inteira para o 3D — que o kit faz do tamanho do
1286
+ vão (até ~60% da altura), sem nunca cruzar a letra. */
1287
+ for (const b of batidas) {
1288
+ if (!b.heroi || b.heroi.modo !== 'objeto3d' || !b.bloco || b.bloco.colunas || b.bloco.blocos) continue;
1289
+ if (deitado) b.bloco = { ...b.bloco, x: M, alinha: 'esq', largura: Math.min(b.bloco.largura, W * 0.46) };
1290
+ else b.bloco = { ...b.bloco, y: H * 0.3, largura: Math.min(b.bloco.largura, larguraUtil) };
1291
+ }
1292
+
1156
1293
  // ── as passagens: como uma batida vira a outra ──────────────────────
1157
1294
  let ultimaSaida = null, desdeCorte = 9;
1158
1295
  const saidasRecentes = [];
@@ -1361,9 +1498,12 @@ function compor(comp, br, ctx) {
1361
1498
  const fonte = s.chance(0.5) ? fontes.cinetica : fontes.display;
1362
1499
  const eixos = { wght: Math.min(VARIAVEIS[fonte].eixos.wght[1], wTese) };
1363
1500
  const op = { fonte, wght: eixos.wght, maiusc };
1364
- const nMax = deitado ? 2 : Math.min(4, txt.split(/\s+/).length);
1501
+ // A frase de 3+ palavras pode ir em 3 linhas no 16:9: cresce na altura em vez de encolher na largura.
1502
+ const nMax = deitado ? Math.min(3, Math.max(2, txt.split(/\s+/).length)) : Math.min(4, txt.split(/\s+/).length);
1365
1503
  const alvo = deitado ? larguraUtil * 0.96 : larguraUtil;
1366
- const fit = caberEmLinhas(txt, alvo, nMax, (deitado ? 0.3 : 0.2) * H, op);
1504
+ /* UMA COISA GIGANTE POR TELA (10/10, comparado ao reel de referência: "o
1505
+ texto pequeno no meio do bege"). A tese gigante enche a largura. */
1506
+ const fit = caberEmLinhas(txt, alvo, nMax, (deitado ? 0.42 : 0.26) * H, op);
1367
1507
  const alinha = deitado && s.chance(0.35) ? 'esq' : 'centro';
1368
1508
  b.bloco = { x: alinha === 'esq' ? M : W / 2, y: H * vert(deitado ? [0.5, 0.52, 0.47] : [0.47, 0.5, 0.55]), alinha, vert: 'meio', largura: alvo, gap: 0.04 };
1369
1509
  if (c.selo) linha(c.selo, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.16, maiusc: true, eixos: { wght: 500 } });
@@ -1384,11 +1524,11 @@ function compor(comp, br, ctx) {
1384
1524
  }
1385
1525
  case 'pilha': case 'produto': {
1386
1526
  const noProduto = comp === 'produto';
1387
- const lg = noProduto ? (deitado ? W * 0.42 : larguraUtil) : (deitado ? W * s.entre(0.62, 0.74) : larguraUtil);
1527
+ const lg = noProduto ? (deitado ? W * 0.42 : larguraUtil) : (deitado ? W * s.entre(0.74, 0.84) : larguraUtil);
1388
1528
  const principal = c.tipo === 'numero' ? c.valor : (c.titulo || c.nome || c.nao || c.sim || c.texto || '');
1389
1529
  const nL = 3;
1390
1530
  const op = { fonte: fontes.display, wght: wTese };
1391
- const fit = caberEmLinhas(principal, lg, nL, (deitado ? 0.15 : 0.085) * H * (noProduto ? 0.85 : 1), op);
1531
+ const fit = caberEmLinhas(principal, lg, nL, (deitado ? 0.23 : 0.115) * H * (noProduto ? 0.85 : 1), op);
1392
1532
  const y = noProduto ? (deitado ? H * 0.52 : H * 0.72) : H * vert(deitado ? [0.42, 0.5, 0.58] : [0.44, 0.52, 0.6]);
1393
1533
  b.bloco = { x: M, y, alinha: 'esq', vert: 'meio', largura: lg, gap: 0.16 };
1394
1534
  const rot = c.selo || (c.tipo === 'cta' ? '' : '');
@@ -1426,9 +1566,9 @@ function compor(comp, br, ctx) {
1426
1566
  case 'palavras': {
1427
1567
  const txt = c.texto || c.titulo || '';
1428
1568
  const alinha = s.chance(0.55) ? 'esq' : 'centro';
1429
- const lg = deitado ? W * 0.8 : larguraUtil;
1569
+ const lg = deitado ? W * 0.86 : larguraUtil;
1430
1570
  const op = { fonte: fontes.display, wght: 600 };
1431
- const fit = caberEmLinhas(txt, lg, deitado ? 3 : 4, (deitado ? 0.12 : 0.075) * H, op);
1571
+ const fit = caberEmLinhas(txt, lg, deitado ? 3 : 4, (deitado ? 0.18 : 0.1) * H, op);
1432
1572
  b.bloco = { x: alinha === 'esq' ? M : W / 2, y: H * vert([0.48, 0.52]), alinha, vert: 'meio', largura: lg, gap: 0.1 };
1433
1573
  // Sem destaque, a tese é a palavra mais longa que não é função.
1434
1574
  if (!destaqueTk.length) {
@@ -1463,7 +1603,7 @@ function compor(comp, br, ctx) {
1463
1603
  // Fonte larga (Unbounded) aperta a lista: os itens vão na fonte de texto.
1464
1604
  const fonte = VARIAVEIS[fontes.display].largura > 1.1 ? fontes.texto : fontes.display;
1465
1605
  const op = { fonte, wght: 560 };
1466
- const maxT = (deitado ? 0.09 : 0.042) * H;
1606
+ const maxT = (deitado ? 0.12 : 0.056) * H;
1467
1607
  const nL = itens.length <= 3 ? 2 : 1;
1468
1608
  const tam = Math.min(...itens.map((it) => caberEmLinhas((c.numerada ? '00 ' : '') + it, lg, nL, maxT, op).tamanho));
1469
1609
  b.bloco = { x: M + 30 * base, y: H * vert(deitado ? [0.5, 0.52] : [0.5, 0.54]), alinha: 'esq', vert: 'meio', largura: lg, gap: 0.42 };
@@ -1483,8 +1623,8 @@ function compor(comp, br, ctx) {
1483
1623
  case 'divisao': {
1484
1624
  const lado = deitado ? (larguraUtil - M * 0.6) / 2 : larguraUtil;
1485
1625
  const op = { fonte: fontes.display, wght: wTese };
1486
- const fN = caberEmLinhas(c.nao, lado, deitado ? 3 : 2, (deitado ? 0.1 : 0.065) * H, { ...op, wght: 400 });
1487
- const fS = caberEmLinhas(c.sim, lado, deitado ? 3 : 3, (deitado ? 0.12 : 0.08) * H, op);
1626
+ const fN = caberEmLinhas(c.nao, lado, deitado ? 3 : 2, (deitado ? 0.13 : 0.08) * H, { ...op, wght: 400 });
1627
+ const fS = caberEmLinhas(c.sim, lado, deitado ? 3 : 3, (deitado ? 0.17 : 0.1) * H, op);
1488
1628
  if (deitado) {
1489
1629
  b.bloco = { x: M, y: H * 0.5, alinha: 'esq', vert: 'meio', largura: lado, gap: 0.1, colunas: [M, M + lado + M * 0.6] };
1490
1630
  } else {
@@ -1502,7 +1642,7 @@ function compor(comp, br, ctx) {
1502
1642
  const fonte = fontes.serifa;
1503
1643
  const lg = deitado ? W * 0.78 : larguraUtil;
1504
1644
  const q = String(c.texto || '').replace(/^["“]+|["”]+$/g, '');
1505
- const fit = caberEmLinhas(q, lg, deitado ? 4 : 5, (deitado ? 0.12 : 0.065) * H, { fonte, wght: 420 });
1645
+ const fit = caberEmLinhas(q, lg, deitado ? 4 : 5, (deitado ? 0.15 : 0.08) * H, { fonte, wght: 420 });
1506
1646
  b.bloco = { x: M, y: H * vert([0.48, 0.52]), alinha: 'esq', vert: 'meio', largura: lg, gap: 0.16 };
1507
1647
  linha('“', 'aspas', { fonte, tamanho: fit.tamanho * 1.6, eixos: { wght: 800 }, cor: 'acento' });
1508
1648
  fit.linhas.forEach((t) => { const l = linha(t, 'apoio', { fonte, tamanho: fit.tamanho, eixos: { wght: 420, ...(VARIAVEIS[fonte].eixos.SOFT ? { SOFT: 100, WONK: 1 } : {}) } }); l.palavras.forEach((p) => { if (p.papel === 'tese') { p.wght = 700; } }); });
@@ -1516,7 +1656,7 @@ function compor(comp, br, ctx) {
1516
1656
  const nome = c.nome || c.titulo || (ctx.marca.nome || '');
1517
1657
  const fonte = fontes.display;
1518
1658
  const lg = deitado ? W * 0.7 : larguraUtil * 0.9;
1519
- const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.22 : 0.13) * H, { fonte, wght: 800 });
1659
+ const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.3 : 0.16) * H, { fonte, wght: 800 });
1520
1660
  b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
1521
1661
  linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, VARIAVEIS[fonte].eixos.wght[1]) }, comportamento: 'contorno' }).marca = true;
1522
1662
  if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.2, maiusc: true, eixos: { wght: 400 } });
@@ -1527,7 +1667,7 @@ function compor(comp, br, ctx) {
1527
1667
  const txt = c.titulo || c.nome || '';
1528
1668
  const fonte = fontes.cinetica;
1529
1669
  const maiusc = true;
1530
- const tam = tamanhoQueCabe(txt, larguraUtil, (deitado ? 0.2 : 0.1) * H, { fonte, wght: 900, maiusc });
1670
+ const tam = tamanhoQueCabe(txt, larguraUtil, (deitado ? 0.26 : 0.13) * H, { fonte, wght: 900, maiusc });
1531
1671
  b.bloco = { x: W / 2, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: larguraUtil, gap: 0.1 };
1532
1672
  linha(txt, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(900, VARIAVEIS[fonte].eixos.wght[1]) }, maiusc });
1533
1673
  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) };
@@ -1578,8 +1718,12 @@ function compor(comp, br, ctx) {
1578
1718
  componente mede e cresce o grupo principal até a largura do bloco,
1579
1719
  com o teto vertical daqui (o respiro de 30–40% continua garantido). */
1580
1720
  // A palavra-herói de ponta a ponta (o CLAUDE do reel): teto alto, a largura manda.
1581
- const tetoV = { gigante: deitado ? 0.44 : 0.24, marca: deitado ? 0.3 : 0.16, parede: deitado ? 0.26 : 0.13, contador: deitado ? 0.52 : 0.22,
1582
- pilha: deitado ? 0.22 : 0.12, produto: deitado ? 0.14 : 0.08, palavras: deitado ? 0.12 : 0.08, cascata: deitado ? 0.1 : 0.05, divisao: deitado ? 0.13 : 0.085, citacao: deitado ? 0.12 : 0.07 }[b.comp] || 0.1;
1721
+ /* (10/10) "Uma coisa gigante por tela": os tetos subiram — a frase curta
1722
+ chega a ~14–18% da altura em altura de maiúscula, a lista a ~9%. Era o
1723
+ teto daqui (0,12 × 0,72 da altura) que deixava "Entregue a próxima
1724
+ tarefa" pequena no meio do bege, não o tamanho que a composição pedia. */
1725
+ const tetoV = { gigante: deitado ? 0.5 : 0.28, marca: deitado ? 0.34 : 0.18, parede: deitado ? 0.3 : 0.15, contador: deitado ? 0.55 : 0.25,
1726
+ pilha: deitado ? 0.3 : 0.15, produto: deitado ? 0.16 : 0.09, palavras: deitado ? 0.22 : 0.12, cascata: deitado ? 0.14 : 0.07, divisao: deitado ? 0.18 : 0.1, citacao: deitado ? 0.16 : 0.09 }[b.comp] || 0.12;
1583
1727
  // A SANGRIA (a palavra de ponta a ponta) é um momento, não o padrão: até duas por vídeo, só em palavra curta.
1584
1728
  const memS = ctx.memoria || {};
1585
1729
  if (['gigante', 'parede'].includes(b.comp) && (memS.cheios || 0) < 2 && b.linhas.filter((l) => l.papel === 'tese').length <= 2
@@ -1591,7 +1735,7 @@ function compor(comp, br, ctx) {
1591
1735
  if (!enche(l)) continue;
1592
1736
  l.encher = l.papel === 'item' ? 'item' : 'principal';
1593
1737
  // O respiro do guia: fora dos (no máximo dois) momentos de sangria, o bloco usa ~55% da altura e a tese não encosta em nada.
1594
- l.max = Math.round(b.cheio ? Math.min(tetoV * H, (H * 0.66) / (nEnche * 1.05)) : Math.min(tetoV * H * 0.72, (H * 0.5) / (nEnche * 1.05)));
1738
+ l.max = Math.round(b.cheio ? Math.min(tetoV * H, (H * 0.7) / (nEnche * 1.05)) : Math.min(tetoV * H * 0.9, (H * 0.62) / (nEnche * 1.05)));
1595
1739
  }
1596
1740
  // ── o tempo de cada linha e palavra: quando a voz diz ────────────────
1597
1741
  tempoDaBatida(b, palavras, ctx);
@@ -1709,5 +1853,5 @@ function contarAcentos(b) {
1709
1853
 
1710
1854
  module.exports = {
1711
1855
  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,
1712
- especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO,
1856
+ especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO, HEROIS_3D, FAMILIA_HEROI,
1713
1857
  };