primocode 10.10.0 → 10.11.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.10.0
1
+ # PrimoCode v10.10.1
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.10.0
12
+ ▐▛███▜▌ Primo Code v10.10.1
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/catalogo.js CHANGED
@@ -93,8 +93,9 @@ 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
- + 'componente:"componentes/x.jsx" numa parte = a cena que VOCÊ escreveu em React (a voz, a trilha e os sons seguem do Estúdio). '
97
- + 'estilo (deixe VAZIO: o estilo da casa, o gerativo com as cores da marca, é o ponto de partida; outro só se a pessoa pedir pelo nome): 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 — a COISA que a frase nomeia, 2 a 4 palavras, não a frase inteira; rota:"tipografia" em número/citação, "produto" quando a frase fala do produto; ancora:"palavra" — o clipe troca 0,2 s depois dela) | 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 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; 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
+ + 'componente:"componentes/x.jsx" numa parte = a cena que VOCÊ escreveu em React — é o caminho do motion (skill Motion de primeira): uma ideia desenhada por fala; a voz, a trilha, os sons e o fundo seguem do Estúdio; cena que não compila volta em cenasQuebradas. '
97
+ + 'semNarracao:true no roteiro (ou pedido "sem narração") = vídeo só com motion, trilha e efeitos sonoros: sem estudio_narrar, cada parte dura o seu "duracao" (s) ou o tempo de ler a tela, e um som leve entra em cada corte e entrada. '
98
+ + 'estilo (deixe VAZIO: o gerativo, com as suas cenas em componente e o estiloVisual que você montou; outro só se a pessoa pedir pelo nome): 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 — a COISA que a frase nomeia, 2 a 4 palavras, não a frase inteira; rota:"tipografia" em número/citação, "produto" quando a frase fala do produto; ancora:"palavra" — o clipe troca 0,2 s depois dela) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som — estiloVisual {humor, paleta, display, texto, mono, genero, ambiente, acento, material} é o estilo que VOCÊ monta pelo assunto (cardápio: estudio_midia acao:estilos; vazio, o PrimoCode sorteia); 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 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; 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) | '
98
99
  + '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 (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D no visual de blocos/voxel de render de cinema — é a escolha para animação 3D, personagem, bicho, boneco, avatar, mascote, podcast, entrevista ou alguém FALANDO em 3D; cada parte é UMA fala {falante, texto, emocao?, destaque?, acao?, cenario?}, sem tipo). NADA É FIXO, monte o que a pessoa pediu: cena:"texto livre do lugar" (praia, cidade, floresta, parque, fazenda, neve, deserto, espaço, fundo do mar, escritório, sala, cozinha, escola, palco… + hora: "à noite", "ao pôr do sol") ou cena:{lugar, hora, elementos:["palmeira","barraca","violão",…]} — elemento fora do catálogo vira ícone 3D em blocos; parte.cenario muda o lugar a partir daquela fala. Até 4 falantes; personagens:{"nome":{tipo:pessoa|robo|animal|coisa, especie (animal: cachorro|gato|cavalo|vaca|porco|ovelha|raposa|lobo|urso|panda|coelho|leao|tigre|elefante|girafa|tartaruga|galinha|pato|passaro|papagaio|coruja|pinguim|peixe|tubarao|golfinho|dinossauro|sapo|macaco), icone (coisa: "pizza", "café", "polvo" — qualquer objeto/bicho que fala), pessoa: pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba; robo: cor}} — sem tipo, o nome decide (nome de bicho vira bicho, de IA vira o mascote laranja, de objeto vira coisa); figurantes:["cachorro","pessoa"] (quem está na cena sem falar); BICHO NÃO FALA: a fala de um bicho sai da narração e ele fica na cena — só fala se a pessoa pedir ("cachorro falante") ou personagens.x.fala:true; a voz de cada falante sai do GÊNERO do nome (personagens.x.genero:f|m manda), uma voz por personagem do começo ao fim; bigode:true e cabeca:"cubo" (o rosto de pixel antigo) em personagens; sem lugar no pedido, o set é a cobertura à noite (janela da cidade, cartões de interface na frente). acao em cada parte: acenar|apontar|pular|dancar|comemorar|pensar|andar|correr. emocao feliz|serio|surpreso|pensativo|animado|curioso; destaque:"texto curto"; paineis:["título",…] só no set de telas (o padrão quando o pedido não diz lugar); visual:"liso" só se pedirem o boneco liso antigo). '
99
100
  + 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
100
101
  + '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. '
@@ -110,8 +111,11 @@ const GRUPOS = [
110
111
  + 'o render empurra para dentro, encolhe o texto largo e afasta o colado; enfeite de fundo marque decorativo: true.' },
111
112
 
112
113
  { tool: 'estudio_midia', escreve: true, faz: 'procura e baixa ícone, imagem, música e efeito sonoro da internet, e lista as fontes',
113
- args: 'acao: biblioteca | movimentos | cenarios | salvar_cenario | imagem | icones | icones3d | animacoes | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
114
+ args: 'acao: biblioteca | estilos | salvar_estilo | movimentos | cenarios | salvar_cenario | imagem | icones | icones3d | animacoes | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
114
115
  + 'movimentos termo:"quique" categoria:entrada|saida|enfase|loop: os 100+ movimentos prontos com nome; no elemento, movimento:"<nome>" (ou {nome, em, duracao}, ou uma lista) vira quadros-chave sozinho. Varie: não use o mesmo em todo elemento. Pode inventar o seu em anima. '
116
+ + 'estilos: as peças do gerativo (humor, paleta, fontes, trilha, ambiente, acento, material) — dezenas de milhares de combinações, e os estilos que a pessoa salvou. '
117
+ + 'Monte o do vídeo pelo ASSUNTO em roteiro.estiloVisual; misture um salvo com {base:"<nome>", ...}; crie paleta nova com 5 cores. '
118
+ + 'No fim, pergunte "posso salvar este estilo pra usar nos próximos vídeos?" e, SÓ com o sim, salvar_estilo com nome, pecas {…} e autorizado:true. '
115
119
  + 'cenarios: os lugares prontos do vídeo 3D e os que a pessoa já salvou. Pode CRIAR um cenário novo na cena (lugar base + ceu, chao, parede, luz, acento, hora, elementos); '
116
120
  + 'no fim do vídeo pergunte "posso salvar este cenário pra usar na próxima vez?" e, SÓ com o sim, salvar_cenario com nome, cenario {…} e autorizado:true. Depois, lugar:"<nome>" traz ele de volta. '
117
121
  + 'biblioteca: o cardápio INTEIRO do que existe (ícone 2D, ícone 3D, animação pronta, efeito sonoro, música, fonte, personagem 3D) — olhe antes de escolher. '
@@ -466,7 +466,8 @@ const ATALHOS = [
466
466
  ['Ctrl+C', 'limpa o que foi digitado; com a caixa vazia, sai'],
467
467
  ['Ctrl+D', 'sai (com a caixa vazia)'],
468
468
  ['Ctrl+L', 'limpa a tela'],
469
- ['PgUp · PgDn', 'rola a conversa'],
469
+ ['↑ ↓ · roda · PgUp PgDn', 'rolam a conversa (na tela cheia)'],
470
+ ['Option+↑ · Option+↓', 'o pedido anterior · o próximo (na tela cheia)'],
470
471
  ['arrastar o mouse', 'seleciona texto; Cmd+C (Mac) ou Ctrl+Shift+C copia'],
471
472
  ['Ctrl+A · Ctrl+E', 'vai para o começo · o fim da linha'],
472
473
  ['Ctrl+U · Ctrl+K', 'apaga antes · depois do cursor'],
package/lib/entrada.js CHANGED
@@ -587,6 +587,20 @@ class Entrada extends EventEmitter {
587
587
  this.desenhado = { linhas: q.linhas, cursorLinha: q.cursor.linha };
588
588
  }
589
589
 
590
+ /* A RODA QUE VIRAVA HISTÓRICO (13 e 14/10: "quando eu tento subir o
591
+ histórico ele fica mostrando meus prompts"). Sem capturar o mouse (por
592
+ causa da seleção de texto), o terminal manda a roda como setas ↑↓. O
593
+ ritmo não separa as duas coisas: medido no Orca (14/10), a roda devagar
594
+ chega uma seta a cada 50–250 ms, e o ↑ segurado no teclado a cada 83 ms.
595
+ Então, na tela cheia, a seta ROLA a conversa (2 linhas), sempre; o
596
+ pedido anterior fica no Option+↑/↓ (ou Ctrl/Shift, o que o terminal mandar). */
597
+ _setaNaCheia(dir, key) {
598
+ // ESC ESC [A é o Option dos terminais com "Option como Meta": o Node entrega como seta pura.
599
+ const comOption = key && (key.meta || key.ctrl || key.shift || String(key.sequence || '').startsWith('\x1b\x1b'));
600
+ if (comOption) return this._historico(dir > 0 ? -1 : 1);
601
+ return this._rolar(dir * 2);
602
+ }
603
+
590
604
  _rolar(passo) {
591
605
  this.rolagem = Math.max(0, this.rolagem + passo);
592
606
  this._pintar();
@@ -878,12 +892,14 @@ class Entrada extends EventEmitter {
878
892
 
879
893
  case 'up':
880
894
  if (this.menu) { this.menu.sel = Math.max(0, this.menu.sel - 1); return this._redesenhar(); }
895
+ if (this.cheia) return this._setaNaCheia(1, key);
881
896
  return this._historico(-1);
882
897
  case 'down':
883
898
  if (this.menu) {
884
899
  this.menu.sel = Math.min(this.menu.itens.length - 1, this.menu.sel + 1);
885
900
  return this._redesenhar();
886
901
  }
902
+ if (this.cheia) return this._setaNaCheia(-1, key);
887
903
  return this._historico(1);
888
904
 
889
905
  default:
@@ -893,6 +909,8 @@ class Entrada extends EventEmitter {
893
909
 
894
910
  _ctrl(key) {
895
911
  switch (key.name) {
912
+ case 'up': return this.cheia ? this._setaNaCheia(1, key) : this._historico(-1);
913
+ case 'down': return this.cheia ? this._setaNaCheia(-1, key) : this._historico(1);
896
914
  case 'c':
897
915
  // Com texto no campo, Ctrl+C limpa o que foi digitado — sair
898
916
  // por acidente no meio de um pedido longo é perder o pedido.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * cena.js — confere a cena que a IA escreveu antes do render.
3
+ *
4
+ * "só quero motions nesse nível" (Joel, 14/10, com as referências: cada
5
+ * cena é uma ideia desenhada para a fala, escrita pelo modelo).
6
+ *
7
+ * O caminho do motion é a IA escrever cada cena em React (parte.componente).
8
+ * Um erro de sintaxe num .jsx derrubava o bundle do Remotion e o vídeo
9
+ * inteiro. Aqui a cena é compilada (o esbuild que já mora no remotion-base)
10
+ * e os imports são conferidos: a cena quebrada sai, a parte cai no gerativo,
11
+ * e o erro volta para a IA consertar e mandar de novo.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ const PERMITIDOS = /^(react|remotion|three|@remotion\/[\w-]+|\.{1,2}\/.+)$/;
21
+
22
+ let esbuild;
23
+ function compilador() {
24
+ if (esbuild !== undefined) return esbuild;
25
+ try { esbuild = require(path.join(os.homedir(), '.primocode', 'studio', 'remotion-base', 'node_modules', 'esbuild')); } catch { esbuild = null; }
26
+ return esbuild;
27
+ }
28
+
29
+ /** {ok} ou {ok:false, erro} — o erro em uma linha, com a linha do arquivo. */
30
+ function conferirCena(arquivo, { compilar = compilador() } = {}) {
31
+ let codigo;
32
+ try { codigo = fs.readFileSync(arquivo, 'utf8'); } catch (e) { return { ok: false, erro: `não li ${path.basename(arquivo)}: ${e.message}` }; }
33
+ if (!/export\s+default\b/.test(codigo)) return { ok: false, erro: 'falta o export default da função da cena ({el, t, fps}).' };
34
+ const fora = [...codigo.matchAll(/\bfrom\s+['"]([^'"]+)['"]|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => m[1] || m[2]).filter((x) => !PERMITIDOS.test(x));
35
+ if (fora.length) return { ok: false, erro: `import fora do permitido: ${[...new Set(fora)].join(', ')} (só react, remotion, @remotion/*, three).` };
36
+ if (!compilar) return { ok: true, semCompilador: true };
37
+ try {
38
+ compilar.transformSync(codigo, { loader: /\.tsx$/i.test(arquivo) ? 'tsx' : 'jsx', jsx: 'automatic', logLevel: 'silent' });
39
+ return { ok: true };
40
+ } catch (e) {
41
+ const x = (e.errors && e.errors[0]) || {};
42
+ const onde = x.location ? `linha ${x.location.line}: ` : '';
43
+ return { ok: false, erro: `${onde}${x.text || String(e.message).split('\n')[0]}` };
44
+ }
45
+ }
46
+
47
+ module.exports = { conferirCena, PERMITIDOS };
@@ -0,0 +1,25 @@
1
+ // Título com uma faixa de luz que atravessa as letras, e um brilho (bloom) que respira depois.
2
+ // el.dados (ou el): titulo, cores {fundo, texto, acento}, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { spring, interpolate, useVideoConfig } from 'remotion';
5
+
6
+ export default function BrilhoVarredura({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const entra = spring({ frame: Math.round(t * fps), fps, config: { damping: 18, stiffness: 120 } });
12
+ const faixa = interpolate(t, [0.35, 1.25], [-40, 140], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
13
+ const respira = 0.5 + 0.5 * Math.sin(t * 2.4);
14
+ const tam = Math.min(W, H) * (d.tamanho || 0.13);
15
+ const base = { fontFamily: (d.fontes && d.fontes.display) || 'serif', fontSize: tam, lineHeight: 1.02, fontWeight: 800, letterSpacing: '-0.02em', textAlign: 'center', maxWidth: W * 0.84 };
16
+ return (
17
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#0a0a0a', display: 'grid', placeItems: 'center' }}>
18
+ <div style={{ position: 'relative', transform: `translateY(${((1 - entra) * tam * 0.6).toFixed(1)}px)`, opacity: entra }}>
19
+ <div style={{ ...base, position: 'absolute', inset: 0, color: c.acento || '#ffd166', filter: `blur(${(tam * 0.18).toFixed(0)}px)`, opacity: 0.25 + 0.35 * respira * entra }}>{d.titulo}</div>
20
+ <div style={{ ...base, position: 'relative', color: 'transparent', WebkitBackgroundClip: 'text', backgroundClip: 'text',
21
+ backgroundImage: `linear-gradient(105deg, ${c.texto || '#fff'} ${faixa - 18}%, ${c.acento || '#ffd166'} ${faixa - 4}%, #ffffff ${faixa}%, ${c.acento || '#ffd166'} ${faixa + 4}%, ${c.texto || '#fff'} ${faixa + 18}%)` }}>{d.titulo}</div>
22
+ </div>
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,30 @@
1
+ // Transição em que três camadas de cor inundam a tela em ondas, e o título aparece por baixo.
2
+ // el.dados (ou el): titulo, cores {fundo, texto, acento}, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { interpolate, useVideoConfig } from 'remotion';
5
+
6
+ const sai = (x) => 1 - Math.pow(1 - x, 4);
7
+
8
+ export default function CorInunda({ el, t }) {
9
+ const d = el.dados || el;
10
+ const v = useVideoConfig();
11
+ const W = d.largura || v.width, H = d.altura || v.height;
12
+ const c = d.cores || {};
13
+ const camadas = [c.texto || '#f4efe6', c.fundo || '#111', c.acento || '#ff4d2e'];
14
+ const onda = (k, i) => {
15
+ const pts = [];
16
+ for (let j = 0; j <= 24; j++) { const x = (j / 24) * W; pts.push(`${x.toFixed(1)},${(H * (1 - k) + Math.sin(j * 0.7 + t * 6 + i) * 40 * (1 - k) * k * 4).toFixed(1)}`); }
17
+ return `M0,${H} L${pts.join(' L')} L${W},${H} Z`;
18
+ };
19
+ const titulo = interpolate(t, [0.75, 1.15], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
20
+ const tam = Math.min(W, H) * (d.tamanho || 0.12);
21
+ return (
22
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#111', overflow: 'hidden' }}>
23
+ <svg width={W} height={H} style={{ position: 'absolute', inset: 0 }}>
24
+ {camadas.map((cor, i) => <path key={i} d={onda(sai(Math.max(0, Math.min(1, (t - i * 0.16) / 0.7))) * 1.08, i)} fill={cor} />)}
25
+ </svg>
26
+ <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', color: c.fundo || '#111', fontFamily: (d.fontes && d.fontes.display) || 'serif',
27
+ fontSize: tam, fontWeight: 800, textAlign: 'center', padding: W * 0.08, opacity: titulo, transform: `scale(${(1.08 - 0.08 * titulo).toFixed(4)})`, letterSpacing: `${((1 - titulo) * 0.2).toFixed(3)}em` }}>{d.titulo}</div>
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,28 @@
1
+ // Entrada com física: antecipa (recua), cai com peso, amassa no pouso (squash & stretch) e assenta com rebote.
2
+ // el.dados (ou el): titulo (ou imagem), cores, fontes {display}, largura, altura, atraso (s).
3
+ import React from 'react';
4
+ import { Img, staticFile, useVideoConfig } from 'remotion';
5
+
6
+ export default function MolaComPeso({ el, t }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const u = t - (d.atraso || 0);
12
+ const queda = 0.42;
13
+ let y, sx = 1, sy = 1;
14
+ if (u < 0.12) { y = -H * 0.62 - Math.sin((u / 0.12) * Math.PI) * 18; }
15
+ else if (u < 0.12 + queda) { const k = (u - 0.12) / queda; y = -H * 0.62 * (1 - k * k); sy = 1 + 0.18 * k; sx = 1 - 0.1 * k; }
16
+ else { const p = u - 0.12 - queda; const amort = Math.exp(-p * 6); y = -Math.abs(Math.sin(p * 9)) * 60 * amort; sy = 1 - 0.22 * Math.cos(p * 18) * amort; sx = 1 + 0.16 * Math.cos(p * 18) * amort; }
17
+ const tam = Math.min(W, H) * (d.tamanho || 0.14);
18
+ return (
19
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#f4efe6', display: 'grid', placeItems: 'center' }}>
20
+ <div style={{ transform: `translateY(${y.toFixed(1)}px) scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`, transformOrigin: '50% 100%' }}>
21
+ {d.imagem ? <Img src={staticFile(d.imagem)} style={{ width: tam * 4, objectFit: 'contain' }} />
22
+ : <div style={{ fontFamily: (d.fontes && d.fontes.display) || 'serif', fontSize: tam, fontWeight: 900, color: c.texto || '#111' }}>{d.titulo}</div>}
23
+ </div>
24
+ <div style={{ position: 'absolute', top: '50%', width: tam * 2.4, height: tam * 0.18, borderRadius: '50%', background: '#000', marginTop: tam * 0.62,
25
+ opacity: Math.max(0, 0.25 * (1 + y / (H * 0.62))), filter: 'blur(10px)', transform: `scaleX(${(0.4 + 0.6 * (1 + y / (H * 0.62))).toFixed(3)})` }} />
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,37 @@
1
+ // A peça do site entra em 3D: deitada, sobe, BATE (amassa, treme, flash) e o fundo se abre a partir dela.
2
+ // el.dados (ou el): imagem (PNG sem fundo, do estudio_site elemento), cores {fundo, texto, acento}, largura, altura.
3
+ import React from 'react';
4
+ import { Img, staticFile, spring, interpolate, useVideoConfig } from 'remotion';
5
+
6
+ export default function PecaImpacto3D({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const f = Math.round(t * fps);
12
+ const sobe = spring({ frame: f, fps, config: { mass: 1.1, damping: 13, stiffness: 90 } });
13
+ const tBate = 0.9;
14
+ const pos = Math.max(0, t - tBate);
15
+ const amassa = pos > 0 ? Math.exp(-pos * 9) * Math.cos(pos * 38) * 0.07 : 0;
16
+ const treme = pos > 0 ? Math.exp(-pos * 7) * Math.sin(pos * 70) * 14 : 0;
17
+ const flash = pos > 0 ? Math.exp(-pos * 10) : 0;
18
+ const abre = interpolate(pos, [0.05, 0.9], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: (x) => 1 - Math.pow(1 - x, 3) });
19
+ const deitado = interpolate(sobe, [0, 1], [82, 0]);
20
+ const y = interpolate(sobe, [0, 1], [H * 0.18, 0]);
21
+ const lado = Math.min(W, H) * (d.escala || 0.62);
22
+ return (
23
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, overflow: 'hidden', background: c.fundo || '#0b0b0c', perspective: 1400 }}>
24
+ <div style={{ position: 'absolute', inset: 0, clipPath: `circle(${(abre * 120).toFixed(2)}% at 50% 50%)`,
25
+ background: `radial-gradient(circle at 50% 50%, ${c.acento || '#ff5a1f'}33, transparent 60%), repeating-linear-gradient(0deg, ${c.texto || '#fff'}0d 0 1px, transparent 1px 48px), repeating-linear-gradient(90deg, ${c.texto || '#fff'}0d 0 1px, transparent 1px 48px)` }} />
26
+ <div style={{ position: 'absolute', left: '50%', top: '50%', width: lado * (1 + abre * 0.6), height: lado * 0.12, borderRadius: '50%',
27
+ transform: `translate(-50%, ${lado * 0.42}px)`, background: '#000', opacity: 0.35 * sobe, filter: 'blur(24px)' }} />
28
+ <div style={{ position: 'absolute', left: '50%', top: '50%', width: lado, height: lado,
29
+ transform: `translate(-50%, -50%) translate(${treme.toFixed(1)}px, ${y.toFixed(1)}px) rotateX(${deitado.toFixed(2)}deg) scale(${(1 + amassa).toFixed(4)}, ${(1 - amassa).toFixed(4)})`,
30
+ transformOrigin: '50% 100%', transformStyle: 'preserve-3d' }}>
31
+ {d.imagem ? <Img src={staticFile(d.imagem)} style={{ width: '100%', height: '100%', objectFit: 'contain', filter: `drop-shadow(0 30px 40px rgba(0,0,0,.45))` }} />
32
+ : <div style={{ width: '100%', height: '40%', marginTop: '30%', borderRadius: 28, background: c.texto || '#fff' }} />}
33
+ </div>
34
+ <div style={{ position: 'absolute', inset: 0, background: `radial-gradient(circle at 50% 55%, ${c.acento || '#fff'}, transparent 45%)`, opacity: flash * 0.55, mixBlendMode: 'screen' }} />
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,29 @@
1
+ // Luz volumétrica: feixes giram devagar atrás do título, a cor do acento pulsa e partículas sobem na luz.
2
+ // el.dados (ou el): titulo, cores, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { spring, random, useVideoConfig } from 'remotion';
5
+
6
+ export default function RaiosDeLuz({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const acento = c.acento || '#ffb547';
12
+ const entra = spring({ frame: Math.round(t * fps), fps, config: { damping: 20, stiffness: 70 } });
13
+ const tam = Math.min(W, H) * (d.tamanho || 0.12);
14
+ const giro = t * 6;
15
+ return (
16
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#07070a', overflow: 'hidden' }}>
17
+ <div style={{ position: 'absolute', left: '50%', top: '50%', width: Math.max(W, H) * 2, height: Math.max(W, H) * 2, transform: `translate(-50%,-50%) rotate(${giro}deg)`,
18
+ background: `repeating-conic-gradient(from 0deg, ${acento}2e 0deg 6deg, transparent 6deg 22deg)`, opacity: entra * (0.7 + 0.3 * Math.sin(t * 1.7)),
19
+ maskImage: 'radial-gradient(circle, #000 0%, transparent 55%)', WebkitMaskImage: 'radial-gradient(circle, #000 0%, transparent 55%)' }} />
20
+ {Array.from({ length: 28 }).map((_, i) => {
21
+ const x = random('x' + i) * W, vel = 30 + random('v' + i) * 70, r = 2 + random('r' + i) * 4;
22
+ const y = (H + 40 - ((t * vel + random('y' + i) * H) % (H + 80)));
23
+ return <div key={i} style={{ position: 'absolute', left: x, top: y, width: r, height: r, borderRadius: '50%', background: acento, opacity: 0.25 + 0.5 * random('o' + i), filter: 'blur(1px)' }} />;
24
+ })}
25
+ <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center', padding: W * 0.08, fontFamily: (d.fontes && d.fontes.display) || 'serif',
26
+ fontSize: tam, fontWeight: 800, color: c.texto || '#fff', textShadow: `0 0 ${tam * 0.4}px ${acento}88`, opacity: entra, transform: `scale(${(0.92 + 0.08 * entra).toFixed(4)})` }}>{d.titulo}</div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,30 @@
1
+ // Texto riscado que funciona com quebra de linha: um traço por linha, em sequência, e o texto esmaece.
2
+ // el.dados (ou el): titulo (use \n para quebrar, ou passe linhas: []), cores, fontes {display}, largura, altura.
3
+ import React from 'react';
4
+ import { spring, useVideoConfig } from 'remotion';
5
+
6
+ export default function RiscoMultilinha({ el, t, fps }) {
7
+ const d = el.dados || el;
8
+ const v = useVideoConfig();
9
+ const W = d.largura || v.width, H = d.altura || v.height;
10
+ const c = d.cores || {};
11
+ const linhas = d.linhas || String(d.titulo || '').split('\n');
12
+ const tam = Math.min(W, H) * (d.tamanho || 0.11);
13
+ const t0 = d.riscaEm != null ? d.riscaEm : 0.7;
14
+ return (
15
+ <div style={{ position: 'absolute', inset: 0, width: W, height: H, background: c.fundo || '#0d0d0d', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: tam * 0.1 }}>
16
+ {linhas.map((ln, i) => {
17
+ const entra = spring({ frame: Math.round((t - i * 0.08) * fps), fps, config: { damping: 16 } });
18
+ const k = spring({ frame: Math.round((t - t0 - i * 0.14) * fps), fps, config: { damping: 22, stiffness: 160 } });
19
+ return (
20
+ <div key={i} style={{ position: 'relative', fontFamily: (d.fontes && d.fontes.display) || 'serif', fontSize: tam, fontWeight: 700, lineHeight: 1.05,
21
+ color: c.texto || '#fff', opacity: entra * (1 - 0.45 * k), transform: `translateY(${((1 - entra) * 30).toFixed(1)}px)` }}>
22
+ {ln}
23
+ <div style={{ position: 'absolute', left: '-3%', top: '54%', height: Math.max(6, tam * 0.08), width: `${(106 * k).toFixed(2)}%`, borderRadius: 99,
24
+ background: c.acento || '#ff3b30', transform: `rotate(${-2 + i * 1.5}deg)`, transformOrigin: '0 50%' }} />
25
+ </div>
26
+ );
27
+ })}
28
+ </div>
29
+ );
30
+ }
@@ -33,6 +33,21 @@ const catalogoArq = () => path.join(pasta(), 'catalogo.json');
33
33
  const PACOTES = /^(react|react\/jsx-runtime|remotion|@remotion\/[a-z-]+|three|three\/examples\/jsm\/.+|@react-three\/fiber)$/;
34
34
  const TETO = 150 * 1024;
35
35
 
36
+ /* OS DE FÁBRICA (14/10, "a IA pega sempre os mesmos; quero motions bem
37
+ profissionais e diversificados"): o cardápio nascia vazio, e a IA voltava
38
+ aos mesmos três truques. Estes vêm com o PrimoCode e entram no mesmo
39
+ cardápio, com a mesma memória de variedade. Guardado com o mesmo nome vence. */
40
+ const fabricaPasta = path.join(__dirname, 'efeitos-fabrica');
41
+ const FABRICA = [
42
+ { nome: 'peca-impacto-3d', descricao: 'a peça do site (PNG sem fundo do estudio_site elemento) entra deitada em 3D, sobe, BATE (amassa, treme, flash) e o fundo se abre a partir dela', tags: ['3d', 'site', 'impacto'] },
43
+ { nome: 'brilho-varredura', descricao: 'título com uma faixa de luz que atravessa as letras e um brilho que respira', tags: ['brilho', 'titulo'] },
44
+ { nome: 'cor-inunda', descricao: 'três ondas de cor inundam a tela e o título surge por baixo — transição de cor', tags: ['cor', 'transicao'] },
45
+ { nome: 'risco-multilinha', descricao: 'texto riscado com quebra de linha: um traço por linha, em sequência, e o texto esmaece (o "não" do contraste)', tags: ['risco', 'contraste'] },
46
+ { nome: 'mola-com-peso', descricao: 'entrada com física: antecipa, cai com peso, amassa no pouso e assenta com rebote (texto ou imagem)', tags: ['fisica', 'entrada'] },
47
+ { nome: 'raios-de-luz', descricao: 'feixes de luz giram atrás do título, o acento pulsa e partículas sobem na luz', tags: ['luz', 'brilho', 'titulo'] },
48
+ ];
49
+ const deFabrica = (nome) => FABRICA.find((x) => x.nome === nome && fs.existsSync(path.join(fabricaPasta, x.nome + '.jsx')));
50
+
36
51
  function ler() {
37
52
  try { const c = JSON.parse(fs.readFileSync(catalogoArq(), 'utf8')); return Array.isArray(c) ? c : []; } catch { return []; }
38
53
  }
@@ -72,7 +87,8 @@ function guardar({ pasta: proj, arquivo, nome, descricao, tags = [] } = {}) {
72
87
 
73
88
  /** O cardápio: os guardados, os menos usados primeiro (memória da variedade). */
74
89
  function cardapio({ aleatorio = Math.random } = {}) {
75
- const itens = ler().filter((x) => fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
90
+ const meus = ler().filter((x) => fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
91
+ const itens = [...meus, ...FABRICA.filter((f) => deFabrica(f.nome) && !meus.some((m) => m.nome === f.nome))];
76
92
  return variedade.pontuar(itens, 'efeito', { chave: (x) => x.nome })
77
93
  .map((p) => ({ ...p, pontos: p.pontos + aleatorio() * 0.05 }))
78
94
  .sort((a, b) => b.pontos - a.pontos)
@@ -81,8 +97,9 @@ function cardapio({ aleatorio = Math.random } = {}) {
81
97
 
82
98
  /** Copia o efeito para o projeto e devolve o elemento pronto. Marca o uso. */
83
99
  function reusar({ pasta: proj, nome } = {}) {
84
- const item = ler().find((x) => x.nome === nome);
85
- const de = item && path.join(pasta(), item.nome + '.jsx');
100
+ const meu = ler().find((x) => x.nome === nome && fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
101
+ const item = meu || deFabrica(nome);
102
+ const de = item && path.join(meu ? pasta() : fabricaPasta, item.nome + '.jsx');
86
103
  if (!item || !fs.existsSync(de)) return { ok: false, error: `não há efeito "${nome}" guardado — veja o cardápio (acao: efeitos).` };
87
104
  if (!proj) return { ok: false, error: 'diga o projeto (id).' };
88
105
  const src = `efeitos/${item.nome}.jsx`;
@@ -92,4 +109,4 @@ function reusar({ pasta: proj, nome } = {}) {
92
109
  return { ok: true, src, descricao: item.descricao, elemento: { tipo: 'componente', src, x: 0, y: 0 } };
93
110
  }
94
111
 
95
- module.exports = { guardar, cardapio, reusar, conferirFonte, pasta };
112
+ module.exports = { guardar, cardapio, reusar, conferirFonte, pasta, FABRICA };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * estilos.js — os milhares de estilos do gerativo, montados por peça.
3
+ *
4
+ * "volta pro remotion, terão 10 mil estilos e a IA pode misturar eles e
5
+ * criar novos." (Joel, 14/10)
6
+ *
7
+ * Um estilo é uma combinação de peças: humor, paleta (o sistema de 5 campos),
8
+ * fonte de título, de texto e mono, gênero da trilha, ambiente, acento e o
9
+ * material dos ícones. As peças são as do gerativo.js, então a conta sai do
10
+ * código (total()), e não de uma lista escrita à mão.
11
+ *
12
+ * Quem escolhe é a IA: ela pede o cardápio (estudio_midia acao:"estilos"),
13
+ * monta o estilo pelo assunto e passa roteiro.estiloVisual. O que ela deixar
14
+ * vazio, o sorteio com memória completa (escolherVisual). Ela também pode
15
+ * misturar um estilo salvo (base:"<nome>") e criar uma paleta nova (5 cores).
16
+ * O estilo que ficou bom só é guardado com o sim da pessoa, como os cenários.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+ const gerativo = require('./gerativo.js');
25
+ const { chave } = require('./cenarios.js');
26
+
27
+ const arquivo = () => path.join(os.homedir(), '.primocode', 'estilos.json');
28
+ const ehCor = (c) => typeof c === 'string' && /^#[0-9a-f]{6}$/i.test(c);
29
+ const unicos = (campo) => [...new Set(Object.values(gerativo.HUMORES).flatMap((h) => h[campo]))];
30
+
31
+ /** As paletas com nome: "comida-1" é o primeiro sistema de 5 campos do humor comida. */
32
+ function paletas() {
33
+ const out = {};
34
+ for (const [h, lista] of Object.entries(gerativo.CAMPOS)) lista.forEach((k, i) => { out[`${h}-${i + 1}`] = k; });
35
+ return out;
36
+ }
37
+
38
+ function pecas() {
39
+ return {
40
+ humor: Object.keys(gerativo.HUMORES),
41
+ paleta: Object.keys(paletas()),
42
+ display: unicos('displays').filter((f) => !gerativo.BANIDAS.includes(f) && gerativo.VARIAVEIS[f]),
43
+ texto: unicos('textos'),
44
+ mono: unicos('monos'),
45
+ genero: unicos('generos'),
46
+ ambiente: gerativo.AMBIENTES,
47
+ acento: gerativo.ACENTOS_JEITO,
48
+ material: gerativo.MATERIAIS_ICONE,
49
+ };
50
+ }
51
+
52
+ /** Quantos estilos dá pra montar só com as peças prontas (sem contar as paletas que a IA cria). */
53
+ function total() {
54
+ return Object.values(pecas()).reduce((n, lista) => n * lista.length, 1);
55
+ }
56
+
57
+ /**
58
+ * Confere as peças que a IA escolheu. Peça que não existe vira aviso, com as
59
+ * opções, e não entra (o sorteio cobre). A paleta pode ser um nome ou 5 cores.
60
+ */
61
+ function validar(pedido) {
62
+ const P = pecas(), out = {}, avisos = [];
63
+ for (const [campo, v] of Object.entries(pedido || {})) {
64
+ if (campo === 'base' || v == null || v === '') continue;
65
+ if (campo === 'paleta') {
66
+ if (Array.isArray(v)) {
67
+ if (v.length !== 5 || !v.every(ehCor)) avisos.push('paleta nova precisa de 5 cores #rrggbb: escura, assinatura, papel e duas ousadas.');
68
+ else if (v.some(gerativo.corGenerica)) avisos.push(`paleta ${v.join(' ')}: tem cor de cara de IA (violeta de SaaS ou neon); troque.`);
69
+ else out.paleta = v.map((c) => c.toUpperCase());
70
+ } else if (paletas()[v]) out.paleta = paletas()[v];
71
+ else avisos.push(`paleta "${v}" não existe: ${P.paleta.join(', ')} ou 5 cores suas.`);
72
+ continue;
73
+ }
74
+ if (!P[campo]) { avisos.push(`"${campo}" não é peça de estilo: ${Object.keys(P).join(', ')}.`); continue; }
75
+ if (P[campo].includes(v)) out[campo] = v;
76
+ else avisos.push(`${campo} "${v}" não existe: ${P[campo].join(', ')}.`);
77
+ }
78
+ return { pecas: out, avisos };
79
+ }
80
+
81
+ /**
82
+ * O estilo do vídeo a partir do que a IA pediu (objeto, ou o nome de um salvo).
83
+ * Devolve as escolhasGerativo prontas: o que ela não disse, o sorteio completa.
84
+ */
85
+ function montar(pedido, roteiro = {}, { aleatorio } = {}) {
86
+ let p = typeof pedido === 'string' ? { base: pedido } : { ...(pedido || {}) };
87
+ const avisos = [];
88
+ if (p.base) {
89
+ const salvo = ler(p.base);
90
+ if (salvo) p = { ...salvo.pecas, ...p };
91
+ else avisos.push(`não existe estilo salvo "${p.base}".`);
92
+ }
93
+ const v = validar(p);
94
+ avisos.push(...v.avisos);
95
+ const humor = v.pecas.humor;
96
+ const sorteio = gerativo.escolherVisual(humor ? { ...roteiro, pedido: `${roteiro.pedido || ''} ${gerativo.HUMORES[humor].palavras}` } : roteiro, { aleatorio });
97
+ const escolhas = { ...sorteio };
98
+ for (const campo of ['humor', 'paleta', 'display', 'texto', 'mono', 'genero', 'ambiente', 'acento']) if (v.pecas[campo]) escolhas[campo] = v.pecas[campo];
99
+ if (v.pecas.material) escolhas.icones = { ...(sorteio.icones || {}), material: v.pecas.material };
100
+ return { escolhas, escolhidas: Object.keys(v.pecas), avisos };
101
+ }
102
+
103
+ function lerTudo() {
104
+ try { const j = JSON.parse(fs.readFileSync(arquivo(), 'utf8')); return j && typeof j === 'object' ? j : {}; } catch { return {}; }
105
+ }
106
+
107
+ function ler(nome) {
108
+ const k = chave(nome);
109
+ return k ? lerTudo()[k] || null : null;
110
+ }
111
+
112
+ function listar() {
113
+ return Object.entries(lerTudo()).map(([k, e]) => ({ chave: k, nome: e.nome, descricao: e.descricao || '', pecas: e.pecas }));
114
+ }
115
+
116
+ /** Guarda o estilo. `autorizado` é a resposta da pessoa: sem ela, nada é escrito. */
117
+ function salvar({ nome, pecas: pedido, descricao, autorizado } = {}) {
118
+ if (autorizado !== true) {
119
+ return { ok: false, error: 'falta o sim da pessoa.', comoResolver: 'pergunte "posso salvar este estilo pra usar nos próximos vídeos?" e só chame de novo com autorizado:true se ela disser que sim.' };
120
+ }
121
+ const k = chave(nome);
122
+ if (!k) return { ok: false, error: 'dê um nome ao estilo (ex: "padaria de manhã").' };
123
+ const v = validar(pedido);
124
+ if (!Object.keys(v.pecas).length) return { ok: false, error: 'nenhuma peça válida para guardar.', avisos: v.avisos };
125
+ const tudo = lerTudo();
126
+ const novo = { nome: String(nome).slice(0, 60), pecas: v.pecas, salvo: new Date().toISOString() };
127
+ if (typeof descricao === 'string' && descricao.trim()) novo.descricao = descricao.slice(0, 200);
128
+ tudo[k] = novo;
129
+ fs.mkdirSync(path.dirname(arquivo()), { recursive: true });
130
+ fs.writeFileSync(arquivo(), JSON.stringify(tudo, null, 2));
131
+ return { ok: true, chave: k, estilo: novo, avisos: v.avisos, comoUsar: `no próximo vídeo, roteiro.estiloVisual:"${k}" traz este estilo (ou {base:"${k}", ...} para misturar).` };
132
+ }
133
+
134
+ /** O cardápio que a IA lê antes de montar o estilo. */
135
+ function cardapio() {
136
+ return {
137
+ ok: true,
138
+ total: total(),
139
+ pecas: { ...pecas(), paleta: paletas() },
140
+ salvos: listar(),
141
+ comoUsar: 'monte o estilo pelo ASSUNTO e passe roteiro.estiloVisual {humor, paleta, display, texto, mono, genero, ambiente, acento, material} no estilo gerativo. '
142
+ + 'Paleta: um nome da lista ou 5 cores suas [escura, assinatura, papel, ousada, ousada]. Misture: {base:"<salvo>", display:"..."} troca só uma peça. '
143
+ + 'Peça vazia, o PrimoCode completa sem repetir os últimos vídeos. Ficou bom? Pergunte se pode salvar e use acao:"salvar_estilo".',
144
+ };
145
+ }
146
+
147
+ module.exports = { pecas, paletas, total, validar, montar, salvar, ler, listar, cardapio, arquivo };
@@ -192,6 +192,15 @@ const HUMORES = {
192
192
  paletas: [['#2A0E12', '#F0E6D6', '#C9A24A'], ['#F1EBE1', '#1B1A17', '#8C1C13']],
193
193
  generos: ['jazz', 'soul-rnb', 'classical'], som: 'papel',
194
194
  },
195
+ /* (14/10) A padaria caía no editorial — música clássica e mono de jornal —
196
+ porque nenhum humor era de comida. */
197
+ comida: {
198
+ palavras: 'pao paes padaria padeiro cafe cafeteria bolo bolos doce doces confeitaria forno fornada fermentacao massa farinha receita cozinha comida restaurante pizza lanche almoco jantar sabor chef croissant',
199
+ displays: ['Fraunces', 'Playfair Display', 'Bricolage Grotesque'], textos: ['Hanken Grotesk', 'Familjen Grotesk'], monos: ['Space Mono', 'Martian Mono'],
200
+ serifas: ['Instrument Serif', 'Fraunces'], cineticas: ['Anybody', 'Archivo'],
201
+ paletas: [['#F3E6CF', '#3B2416', '#B5651D'], ['#EFE3D0', '#2A1A12', '#C8553D']],
202
+ generos: ['acoustic', 'jazz', 'soul-rnb'], som: 'organico',
203
+ },
195
204
  };
196
205
  /* A cor que o próprio assunto diz ("por que o céu é AZUL") vira o acento. */
197
206
  const CORES_DITAS = { azul: '#2546D9', vermelho: '#C8291E', verde: '#2F7D4F', laranja: '#FF5F1F', amarelo: '#E8B92F', dourado: '#C9A24A', rosa: '#E86A8A', coral: '#FF6F59' };
@@ -208,11 +217,15 @@ const ICONE_DE = {
208
217
  notebook: 'celular', pronto: 'check', feito: 'check', certo: 'check', proximo: 'seta', passo: 'seta', adiante: 'seta', ricocheteia: 'seta',
209
218
  };
210
219
 
220
+ /* O humor novo (comida, 14/10) só entra quando o roteiro diz as palavras dele:
221
+ não gasta a semente (a mesma semente segue dando o mesmo vídeo nos projetos
222
+ que já existem) nem tira a vez dos outros num roteiro que não é de comida. */
223
+ const SO_COM_PALAVRA = new Set(['comida']);
211
224
  /** O humor do roteiro: as palavras dele contra as de cada humor (empate: a semente). */
212
225
  function humorDe(roteiro, s) {
213
226
  const n = acertosDeHumor(roteiro);
214
227
  const nota = {};
215
- for (const h of Object.keys(HUMORES)) nota[h] = n[h] + s.prox() * 0.5;
228
+ for (const h of Object.keys(HUMORES)) nota[h] = SO_COM_PALAVRA.has(h) ? (n[h] ? n[h] + 0.25 : -1) : n[h] + s.prox() * 0.5;
216
229
  return Object.entries(nota).sort((a, b) => b[1] - a[1])[0][0];
217
230
  }
218
231
  /** Quantas palavras de cada humor o roteiro diz. */
@@ -241,7 +254,7 @@ function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
241
254
  const marca = require('./marca.js').marcaEmObjeto(roteiro.marca);
242
255
  const n = acertosDeHumor(roteiro);
243
256
  const max = Math.max(...Object.values(n));
244
- const humor = variedade.sortear(Object.keys(HUMORES), 'humor-gerativo', { aleatorio, adequacao: (h) => (n[h] + 1) / (max + 1) });
257
+ const humor = variedade.sortear(Object.keys(HUMORES).filter((h) => !SO_COM_PALAVRA.has(h) || n[h]), 'humor-gerativo', { aleatorio, adequacao: (h) => (n[h] + 1) / (max + 1) });
245
258
  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; };
246
259
  const escolher = (m, tipo, prefixo) => variedade.sortear(Object.keys(m), tipo, { aleatorio, prefixo, adequacao: m });
247
260
  const out = { humor };
@@ -524,7 +537,8 @@ function pesosDe(c, { curto, palavras, primeira, ultima, tem3d, pergunta, exclam
524
537
  case 'contraste': return { divisao: 5, duelo: 2.6 };
525
538
  case 'citacao': return { citacao: 5, pilha: 1 };
526
539
  case 'marca': return { marca: primeira || ultima ? 4.5 : 3, gigante: 2.4, parede: curto ? 2 : 0 };
527
- case 'cta': return { pilha: 3, gigante: curto ? 2.6 : 0.6, parede: c.titulo.length <= 16 ? 1.6 : 0, marca: 1, produto: tem3d ? 3.5 : 0 };
540
+ // A marca é UMA linha: "Todo dia às 7h – Padaria Pão Quente" saía em 40 px (14/10). Chamada longa empilha.
541
+ case 'cta': return { pilha: 3, gigante: curto ? 2.6 : 0.6, parede: c.titulo.length <= 16 ? 1.6 : 0, marca: c.titulo.length <= 18 ? 1 : 0, produto: tem3d ? 3.5 : 0 };
528
542
  case 'produto': return tem3d ? { produto: 9, pilha: 0.5 } : { pilha: 3, gigante: curto ? 2.5 : 0.5, parede: c.titulo.length <= 12 ? 1.2 : 0 };
529
543
  case 'falado': return { palavras: 4, gigante: palavras <= 3 ? 2.6 : 0, pilha: palavras <= 9 ? 1.2 : 0 };
530
544
  default: return {
@@ -628,6 +642,7 @@ const CAMPOS = {
628
642
  energia: [['#0B0B0B', '#FF4F00', '#F2EEE6', '#1D3BD1', '#C9F23A'], ['#101010', '#E8C547', '#F4EBDD', '#E5322D', '#2A57FF']],
629
643
  natureza: [['#0F2A2A', '#F2A541', '#F2EDE2', '#2546D9', '#E4573D'], ['#13233F', '#2546D9', '#F2EDE2', '#F2A541', '#C8553D']],
630
644
  luxo: [['#120D0B', '#C9A24A', '#F0E6D6', '#5B1A1F', '#2F4F4F']],
645
+ comida: [['#2A1A12', '#C47A2C', '#F3E6CF', '#7A2E1C', '#E8C27A'], ['#1F1611', '#B5651D', '#F5ECDD', '#5E7D3A', '#E9B44C']],
631
646
  };
632
647
  function camposDe(marca, humor, s, dita = null, fixa = null) {
633
648
  const c = marca.cores || {};
@@ -2024,4 +2039,5 @@ function contarAcentos(b) {
2024
2039
  module.exports = { tituloDaFala,
2025
2040
  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,
2026
2041
  especEixos, sorteador, hash, larguraEm, conteudoDe, casar, HEROIS_OBJETO, HEROIS_CAMPO, HEROIS_3D, FAMILIA_HEROI,
2042
+ CAMPOS, escolhasValidas,
2027
2043
  };