primocode 10.1.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.
@@ -113,12 +113,47 @@ const norm = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toL
113
113
  function acharFonte(nome) { const n = norm(nome); return FONTES.find((f) => norm(f.nome) === n) || null; }
114
114
  function acharPaleta(nome) { const n = norm(nome).replace(/\s+/g, '-'); return PALETAS.find((p) => p.nome === n) || null; }
115
115
 
116
- /* O sorteio que pula os recentes. Se a lista inteira já foi usada, vale
117
- qualquer uma — nunca fica sem. */
118
- function sortear(lista, recentes, chave, sorte) {
119
- const livres = lista.filter((x) => !recentes.includes(chave(x)));
120
- const de = livres.length ? livres : lista;
121
- return de[Math.floor(sorte() * de.length) % de.length];
116
+ /* O CASAMENTO DA FONTE COM O ROTEIRO (07/10): "a IA deve escolher fontes e
117
+ combinações de acordo com assunto, tom, público e ritmo" (Joel). Antes eram
118
+ dois sorteios independentes. Agora:
119
+ - o CLIMA do roteiro (o mesmo que escolhe a trilha, variedade.climaDe) diz
120
+ quanto cada tipo de título serve (0 a 1);
121
+ - o tipo do título diz que tipo de texto faz par com ele (contraste, não
122
+ duas fontes que brigam);
123
+ - a memória penaliza o recente pela distância, não corta: o que serve ao
124
+ tom ganha do que só é novo (os pontos de variedade.js). */
125
+ const TOM_DO_TITULO = {
126
+ tecnologia: { display: 1, larga: 0.9, condensada: 0.6, texto: 0.7, serifa: 0.2 },
127
+ energetico: { condensada: 1, display: 0.9, larga: 0.6, texto: 0.5, serifa: 0.2 },
128
+ epico: { condensada: 1, serifa: 0.8, display: 0.6, larga: 0.5, texto: 0.3 },
129
+ calmo: { serifa: 1, texto: 0.9, display: 0.5, condensada: 0.3, larga: 0.2 },
130
+ alegre: { display: 1, larga: 0.8, texto: 0.6, condensada: 0.4, serifa: 0.4 },
131
+ emocional: { serifa: 1, texto: 0.7, display: 0.5, condensada: 0.4, larga: 0.2 },
132
+ misterio: { condensada: 0.9, serifa: 0.9, larga: 0.6, display: 0.5, texto: 0.4 },
133
+ };
134
+ // O par do texto, pelo tipo do título: o contraste que funciona.
135
+ const PAR_DO_TEXTO = {
136
+ condensada: { texto: 1, mono: 0.8, serifa: 0.6, display: 0.4 },
137
+ display: { texto: 1, serifa: 0.7, mono: 0.6, display: 0.2 },
138
+ larga: { mono: 1, texto: 0.9, serifa: 0.4, display: 0.2 },
139
+ serifa: { texto: 1, mono: 0.7, display: 0.5, serifa: 0.3 },
140
+ texto: { serifa: 1, mono: 0.9, texto: 0.3, display: 0.5 },
141
+ };
142
+
143
+ /** O tom do roteiro para a fonte: o clima (variedade.climaDe). Neutro serve tudo igual. */
144
+ function tomDe(roteiro, estilo = '') { return require('./variedade.js').climaDe(roteiro || {}, estilo); }
145
+
146
+ /* A escolha por pontos: 2 × adequação − recência (1 para o último usado,
147
+ metade a cada quarto da memória). Entre os quase empatados, sorteia. */
148
+ function escolher(lista, recentes, chave, sorte, adequa = () => 1, lembra = 10) {
149
+ const meia = Math.max(1, lembra / 4);
150
+ const pts = lista.map((x) => {
151
+ const pos = recentes.indexOf(chave(x));
152
+ return { x, p: 2 * adequa(x) - (pos < 0 ? 0 : Math.pow(0.5, pos / meia)) };
153
+ });
154
+ const melhor = Math.max(...pts.map((q) => q.p));
155
+ const quase = pts.filter((q) => q.p >= melhor - 0.05).map((q) => q.x);
156
+ return quase[Math.floor(sorte() * quase.length) % quase.length];
122
157
  }
123
158
 
124
159
  /**
@@ -126,7 +161,7 @@ function sortear(lista, recentes, chave, sorte) {
126
161
  * trocar nada que a marca já tem. Devolve o que escolheu e de onde veio.
127
162
  * memoria: null não lê nem grava o disco (os testes); sorte: () => [0,1).
128
163
  */
129
- function vestir(roteiro, { memoria = ARQ, sorte = Math.random } = {}) {
164
+ function vestir(roteiro, { memoria = ARQ, sorte = Math.random, estilo = '' } = {}) {
130
165
  if (!roteiro || typeof roteiro !== 'object') return { mudou: false };
131
166
  const marca = roteiro.marca || {};
132
167
  const cores = marca.cores || {};
@@ -136,17 +171,22 @@ function vestir(roteiro, { memoria = ARQ, sorte = Math.random } = {}) {
136
171
 
137
172
  if (!(marca.fontes && marca.fontes.titulo)) {
138
173
  // A IA escolheu pelo nome: vale qualquer família do Google Fonts, mesmo fora do cardápio.
139
- const titulo = visual.titulo ? String(visual.titulo) : sortear(TITULO, mem.titulo, (f) => f.nome, sorte).nome;
174
+ const tom = TOM_DO_TITULO[tomDe(roteiro, estilo)] || null;
175
+ const titulo = visual.titulo ? String(visual.titulo)
176
+ : escolher(TITULO, mem.titulo, (f) => f.nome, sorte, (f) => (tom ? tom[f.tipo] ?? 0.3 : 1), LEMBRA.titulo).nome;
177
+ const tipoTitulo = (acharFonte(titulo) || {}).tipo;
178
+ const par = PAR_DO_TEXTO[tipoTitulo] || null;
140
179
  const textoLista = TEXTO.filter((f) => f.nome !== titulo);
141
- const texto = visual.texto ? String(visual.texto) : sortear(textoLista, mem.texto, (f) => f.nome, sorte).nome;
180
+ const texto = visual.texto ? String(visual.texto)
181
+ : escolher(textoLista, mem.texto, (f) => f.nome, sorte, (f) => (par ? par[f.tipo] ?? 0.2 : 1), LEMBRA.texto).nome;
142
182
  marca.fontes = { ...(marca.fontes || {}), titulo, texto };
143
183
  mem.titulo = [titulo, ...mem.titulo.filter((x) => x !== titulo)].slice(0, LEMBRA.titulo);
144
184
  mem.texto = [texto, ...mem.texto.filter((x) => x !== texto)].slice(0, LEMBRA.texto);
145
- Object.assign(escolha, { mudou: true, titulo, texto, fonteDaIA: !!visual.titulo });
185
+ Object.assign(escolha, { mudou: true, titulo, texto, fonteDaIA: !!visual.titulo, tom: tomDe(roteiro, estilo) });
146
186
  }
147
187
  // A cor da marca manda: com destaque ou gradiente declarado, a paleta não entra.
148
188
  if (!(marca.gradiente && marca.gradiente.length >= 2) && !cores.destaque) {
149
- const p = acharPaleta(visual.paleta) || sortear(PALETAS, mem.paleta, (x) => x.base, sorte);
189
+ const p = acharPaleta(visual.paleta) || escolher(PALETAS, mem.paleta, (x) => x.base, sorte, () => 1, LEMBRA.paleta);
150
190
  marca.gradiente = [...p.grad];
151
191
  marca.cores = { ...cores, fundo: cores.fundo || p.fundo, destaque: p.grad[1], apoio: p.grad[0] };
152
192
  mem.paleta = [p.base, ...mem.paleta.filter((x) => x !== p.base)].slice(0, LEMBRA.paleta);
@@ -171,4 +211,4 @@ function cardapio() {
171
211
  };
172
212
  }
173
213
 
174
- module.exports = { vestir, cardapio, larguraDe, acharFonte, acharPaleta, FONTES, PALETAS, BASES, TITULO, TEXTO, luz, ARQ };
214
+ module.exports = { vestir, cardapio, tomDe, TOM_DO_TITULO, PAR_DO_TEXTO, larguraDe, acharFonte, acharPaleta, FONTES, PALETAS, BASES, TITULO, TEXTO, luz, ARQ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * decisao.js — o que cada frase da edição pede: movimento, recorte, som — ou
3
+ * nada, e a cena respira.
4
+ *
5
+ * "Não quero simplesmente adicionar efeitos. Quero melhorar a decisão de
6
+ * edição. Nem todo momento precisa de movimento, transição ou efeito. Às
7
+ * vezes a melhor decisão é deixar a cena respirar." (Joel, 07/10)
8
+ *
9
+ * Antes a edição era um rodízio: zoom por `i % 3`, a pessoa recortada em
10
+ * `i % 4 === 2`, whoosh em `i % 3 === 1`. A frase mais forte do vídeo podia
11
+ * cair num "parado", e uma frase de ligação ganhava o soco. Aqui:
12
+ * 1. INTENSIDADE (0 a 1) de cada frase: a palavra mais forte, número,
13
+ * exclamação e pergunta, frase curta de efeito, a pausa antes dela (o
14
+ * silêncio prepara o golpe), o gancho do começo e o fecho;
15
+ * 2. MOVIMENTO pela intensidade, com três regras de editor: o golpe é raro
16
+ * (no máximo ~1 em 3 frases), depois do golpe vem um respiro, e o mesmo
17
+ * movimento não se repete em seguida;
18
+ * 3. o RECORTE (a pessoa na frente da palavra gigante) vai para as frases
19
+ * mais fortes, longe uma da outra; o WHOOSH só anuncia o soco.
20
+ * Puro: frases entram, decisões saem. `aleatorio` decide os empates.
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const r2 = (n) => Math.round(n * 100) / 100;
26
+ const limpa = (t) => String(t || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9%]/g, '');
27
+
28
+ /* Os movimentos de câmera (zoom em quadros-chave, no tempo da cena):
29
+ parado nada — a cena respira
30
+ empurra aproxima devagar (tensão que cresce)
31
+ soco entra já fechado e segura (o golpe)
32
+ recua abre devagar (alívio, conclusão)
33
+ reenquadra fecha num ponto fora do centro (mudança de assunto) */
34
+ const MOVIMENTOS = {
35
+ parado: () => ({ zoom: [{ t: 0, v: 1.0 }, { t: 1, v: 1.0 }] }),
36
+ empurra: (d, k) => ({ zoom: [{ t: 0, v: 1.0 }, { t: d, v: r2(1.04 + 0.06 * k), e: 'linear' }] }),
37
+ soco: (d, k) => ({ zoom: [{ t: 0, v: r2(1.14 + 0.08 * k) }, { t: d, v: r2(1.17 + 0.08 * k), e: 'linear' }] }),
38
+ recua: (d, k) => ({ zoom: [{ t: 0, v: r2(1.1 + 0.05 * k) }, { t: d, v: 1.02, e: 'suave' }] }),
39
+ // O x é em px: 3,5% do lado menor cabe na sobra de um zoom de 1,12.
40
+ reenquadra: (d, k, lado, base = 1080) => ({ zoom: [{ t: 0, v: r2(1.12 + 0.04 * k) }, { t: d, v: r2(1.15 + 0.04 * k), e: 'suave' }],
41
+ x: [{ t: 0, v: Math.round((lado ? 1 : -1) * base * 0.03) }, { t: d, v: Math.round((lado ? 1 : -1) * base * 0.035), e: 'suave' }] }),
42
+ };
43
+
44
+ /** A intensidade de cada frase (lista de palavras {de, ate, texto}), de 0 a 1. */
45
+ function intensidades(grupos, peso = (w) => limpa(w).length) {
46
+ const n = grupos.length;
47
+ const brutos = grupos.map((g, i) => {
48
+ const texto = g.map((p) => p.texto).join(' ');
49
+ const forte = Math.max(0, ...g.map((p) => peso(p.texto)));
50
+ let s = Math.min(1, forte / 14) * 0.45;
51
+ if (/\d/.test(texto)) s += 0.2;
52
+ if (/!/.test(texto)) s += 0.2;
53
+ if (/\?/.test(texto)) s += 0.1;
54
+ if (g.length <= 4) s += 0.15; // frase curta é frase de efeito
55
+ const pausa = i > 0 ? g[0].de - grupos[i - 1][grupos[i - 1].length - 1].ate : 0;
56
+ if (pausa > 0.45) s += Math.min(0.2, pausa * 0.25); // o silêncio prepara o golpe
57
+ if (i === 0) s += 0.15; // o gancho
58
+ if (i === n - 1) s += 0.1; // o fecho
59
+ return s;
60
+ });
61
+ const max = Math.max(...brutos, 1e-9), min = Math.min(...brutos);
62
+ return brutos.map((s) => r2(max - min < 1e-6 ? 0.5 : (s - min) / (max - min)));
63
+ }
64
+
65
+ /**
66
+ * As decisões da edição: uma por frase.
67
+ * @returns {Array<{intensidade:number, movimento:string, recorte:boolean, whoosh:boolean}>}
68
+ */
69
+ function decidir(grupos, { peso, aleatorio = Math.random, maxRecortes = 3 } = {}) {
70
+ const ints = intensidades(grupos, peso);
71
+ const n = ints.length;
72
+ // O golpe é raro: só as frases do terço de cima (pelo menos 1, no máximo n/3).
73
+ const ordem = [...ints.keys()].sort((a, b) => ints[b] - ints[a]);
74
+ const golpes = new Set(ordem.slice(0, Math.max(1, Math.floor(n / 3))).filter((i) => ints[i] >= 0.6 || i === ordem[0]));
75
+ const out = [];
76
+ for (let i = 0; i < n; i++) {
77
+ const ant = out[i - 1];
78
+ let mov;
79
+ if (golpes.has(i) && !(ant && ant.movimento === 'soco')) mov = 'soco';
80
+ else if (ant && ant.movimento === 'soco') mov = 'parado'; // depois do golpe, respira
81
+ else if (i === n - 1) mov = ints[i] > 0.4 ? 'recua' : 'parado';
82
+ else {
83
+ // O resto pelo que a frase pede, sem repetir o anterior.
84
+ const pede = ints[i] > 0.45 ? ['reenquadra', 'empurra'] : ints[i] > 0.2 ? ['empurra', 'parado', 'reenquadra'] : ['parado', 'empurra'];
85
+ const opcoes = pede.filter((m) => !ant || m !== ant.movimento);
86
+ mov = opcoes[Math.floor(aleatorio() * Math.min(2, opcoes.length)) % opcoes.length];
87
+ }
88
+ out.push({ intensidade: ints[i], movimento: mov, recorte: false, whoosh: mov === 'soco' && i > 0 });
89
+ }
90
+ // O recorte: as frases mais fortes, com pelo menos 3 frases entre um e outro.
91
+ const escolhidos = [];
92
+ for (const i of ordem) {
93
+ if (escolhidos.length >= maxRecortes || ints[i] < 0.5) break;
94
+ if (i === 0 || escolhidos.some((j) => Math.abs(j - i) < 3)) continue;
95
+ escolhidos.push(i);
96
+ out[i].recorte = true;
97
+ }
98
+ return out;
99
+ }
100
+
101
+ /** A câmera da cena para o movimento decidido. `lado` alterna o reenquadramento. */
102
+ function camera(movimento, dur, intensidade = 0.5, lado = 0, base = 1080) {
103
+ return (MOVIMENTOS[movimento] || MOVIMENTOS.parado)(dur, Math.max(0, Math.min(1, intensidade)), lado, base);
104
+ }
105
+
106
+ module.exports = { intensidades, decidir, camera, MOVIMENTOS };
@@ -115,9 +115,62 @@ function ritmoPara(segundos) {
115
115
  palavrasPorCena: Math.round(media * 2.6) }; // ~155 palavras/minuto de narração
116
116
  }
117
117
 
118
+ // ── 2b. a estrutura: um cardápio, e a memória do que já saiu ─────────────
119
+
120
+ /* "o roteiro vicia: todo vídeo começa igual, não escolhe outra estrutura"
121
+ (Joel, 09/10). A regra 3 do diretor tinha UM arco só — prende em 2
122
+ segundos, entrega, pede ação — e o produtor nem isso via: todo roteiro
123
+ saía com a mesma abertura de impacto e o mesmo miolo de lista. Aqui não
124
+ tem regex escolhendo pelo pedido: o modelo recebe o CARDÁPIO (cada
125
+ estrutura com o "quando serve" e o jeito de abrir) e o que os últimos
126
+ vídeos já usaram. Quem escolhe é ele; o código só lembra. */
127
+ const ESTRUTURAS = [
128
+ { id: 'historia', nome: 'história', quando: 'tem uma pessoa, um caso real ou um antes e depois', abre: 'uma cena concreta: alguém, um lugar, um momento ("Às 6h, a Ana abriu o caixa e…")' },
129
+ { id: 'problema-solucao', nome: 'problema → solução', quando: 'o assunto resolve uma dor clara', abre: 'a dor dita como quem sente ("Você perde duas horas por semana com…")' },
130
+ { id: 'lista', nome: 'lista', quando: 'são dicas, razões ou passos que se sustentam sozinhos', abre: 'a promessa com o número ("3 erros que…")' },
131
+ { id: 'pergunta-resposta', nome: 'pergunta → resposta', quando: 'o público já tem uma dúvida', abre: 'a pergunta, direta, sem a resposta ainda' },
132
+ { id: 'dialogo', nome: 'diálogo', quando: 'o pedido fala em duas pessoas, duas vozes, conversa ou entrevista', abre: 'a fala de um, e o outro responde' },
133
+ { id: 'demonstracao', nome: 'demonstração', quando: 'o valor está em VER funcionando', abre: 'o resultado pronto primeiro; depois, como se chega lá' },
134
+ { id: 'mito-verdade', nome: 'contraste (mito × verdade)', quando: 'existe uma crença comum que está errada', abre: 'o mito, dito como se fosse verdade' },
135
+ { id: 'dado-surpresa', nome: 'dado que surpreende', quando: 'há um número que muda o jeito de ver o assunto', abre: 'o número sozinho, e a frase que diz por que ele importa' },
136
+ ];
137
+
138
+ const vari = () => require('./variedade.js'); // tardio: a memória mora em ~/.primocode/studio
139
+ const idEstrutura = (x) => String(x || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
140
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
141
+
142
+ /** As últimas estruturas e aberturas que saíram em vídeo, mais nova primeiro. */
143
+ function recentes(n = 3) {
144
+ return { estruturas: vari().usadas('estrutura').slice(0, n), aberturas: vari().usadas('abertura').slice(0, n) };
145
+ }
146
+
147
+ /** O cardápio em texto, para o prompt — com o que foi usado por último. */
148
+ function cardapioEstruturas({ usadas = recentes() } = {}) {
149
+ const linhas = ESTRUTURAS.map((e) => `- ${e.id} (${e.nome}): se ${e.quando}. Abre com ${e.abre}.`);
150
+ if (usadas.estruturas.length) linhas.push(`Usadas recentemente: ${usadas.estruturas.join(', ')} — prefira outra, a menos que o pedido peça essa.`);
151
+ if (usadas.aberturas.length) linhas.push(`Os últimos vídeos começaram assim: ${usadas.aberturas.map((a) => `"${a}"`).join(' · ')} — não comece do mesmo jeito.`);
152
+ linhas.push('Nenhuma serve? Invente a estrutura e dê um nome a ela.');
153
+ return 'CARDÁPIO DE ESTRUTURAS (você escolhe pelo pedido):\n' + linhas.join('\n');
154
+ }
155
+
156
+ /**
157
+ * Depois que o roteiro virou plano: guarda a estrutura que o modelo escolheu
158
+ * e como o vídeo abriu. Aceita o roteiro (partes/fala) ou o plano do diretor
159
+ * (cenas/narracao). Não julga a escolha — só lembra dela.
160
+ */
161
+ function registrarEstrutura(r) {
162
+ const x = r && typeof r === 'object' ? r : {};
163
+ const estrutura = idEstrutura(x.estrutura) || null;
164
+ const p0 = (x.partes || x.cenas || [])[0] || {};
165
+ const abertura = String(p0.fala || p0.narracao || p0.titulo || '').split(/\s+/).filter(Boolean).slice(0, 8).join(' ') || null;
166
+ if (estrutura) vari().marcar('estrutura', estrutura);
167
+ if (abertura) vari().marcar('abertura', abertura);
168
+ return { estrutura, abertura };
169
+ }
170
+
118
171
  // ── 3. o plano ───────────────────────────────────────────────────────────
119
172
 
120
- const CAMPOS_CENA = ['titulo', 'narracao', 'duracao', 'aTela', 'fundo', 'transicao',
173
+ const CAMPOS_CENA = ['titulo', 'falante', 'narracao', 'duracao', 'aTela', 'fundo', 'transicao',
121
174
  'elementos', 'midia', 'camera', 'chaves', 'notas'];
122
175
 
123
176
  /**
@@ -170,6 +223,7 @@ function normalizarPlano(bruto, { segundos } = {}) {
170
223
  tom: p.tom || 'neutro',
171
224
  publico: p.publico || null,
172
225
  promessa: p.promessa || null, // o que a pessoa leva do vídeo
226
+ estrutura: idEstrutura(p.estrutura) || null,
173
227
  segundos: total,
174
228
  voz: vozPara({ tom: p.tom, idioma: p.idioma || 'pt-BR',
175
229
  genero: p.voz && p.voz.genero }),
@@ -181,6 +235,9 @@ function normalizarPlano(bruto, { segundos } = {}) {
181
235
  midia: limpas.flatMap((c, i) => c.midia.map((m) => ({ cena: i + 1, ...m }))),
182
236
  creditos: !!p.creditos,
183
237
  };
238
+ // Duas vozes (falantes.js): o mapa e quem fala cada cena, quando o diretor escreveu diálogo.
239
+ const { vozes } = require('./falantes.js').normalizar({ vozes: p.vozes, partes: limpas });
240
+ if (vozes) plano.vozes = vozes;
184
241
  return { plano, defeitos, ok: defeitos.length === 0 };
185
242
  }
186
243
 
@@ -197,6 +254,7 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
197
254
 
198
255
  {
199
256
  "titulo": "…",
257
+ "estrutura": "o id que você escolheu no cardápio abaixo (ou o nome da que inventou)",
200
258
  "formato": "${formato}",
201
259
  "proporcao": "16:9 | 9:16 | 1:1 | 4:5",
202
260
  "visual": "a direção de arte em uma ou duas frases: paleta (diga as cores em hex), que ar tem (limpo, denso, luminoso, sujo), que tipografia pede. NÃO existe lista de temas — descreva o que este vídeo tem de próprio.",
@@ -204,9 +262,11 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
204
262
  "publico": "para quem é",
205
263
  "promessa": "o que a pessoa leva ao terminar de ver",
206
264
  "trilha": { "busca": "termo de busca da música", "humor": "…", "volume": 0.18 },
265
+ "vozes": { "nome de quem fala": "antonio | thalita | francisca" },
207
266
  "cenas": [
208
267
  {
209
268
  "titulo": "o que aparece grande na tela",
269
+ "falante": "quem fala esta cena (um nome de vozes) — só quando houver mais de uma voz",
210
270
  "narracao": "a frase FALADA, escrita para ser lida em voz alta",
211
271
  "duracao": 3.5,
212
272
  "aTela": "o que se vê nesta cena, descrito como você descreveria para um motion designer: onde está cada coisa, que tamanho tem, o que domina o quadro",
@@ -223,13 +283,16 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
223
283
  REGRAS QUE SEPARAM UM VÍDEO BOM DE UM GENÉRICO:
224
284
  1. NARRAÇÃO É FALA, NÃO LEGENDA. "Vantagens" não é narração. Escreva a frase que a pessoa ouve.
225
285
  2. O QUE SE OUVE E O QUE SE VÊ NÃO SE REPETEM. A tela mostra a prova (número, imagem, logo); a voz conta o que ela significa.
226
- 3. ARCO. Primeira cena prende em 2 segundos, o miolo entrega, a última pede uma ação. Sem "Introdução / Desenvolvimento / Conclusão".
286
+ 3. ESTRUTURA. Escolha no cardápio do fim a que serve a ESTE pedido; a primeira cena abre do jeito dela e prende em 2 segundos, a última pede uma ação. Sem "Introdução / Desenvolvimento / Conclusão".
227
287
  4. NÚMERO É CONCRETO. Nada de "muitos clientes": ou tem o número, ou a frase muda.
228
288
  5. RITMO VARIADO. Cenas de durações diferentes e transições diferentes. Tudo em fade parece slide.
229
289
  5b. NADA DE GAVETA. Não existe tema pronto, layout pronto nem fundo pronto: é você que decide a cor, a tipografia e a composição DESTE vídeo. Dois vídeos seus sobre assuntos diferentes não podem sair parecidos.
230
290
  6. MÍDIA COM PROPÓSITO. Só peça imagem ou logo quando ela PROVA algo. Diga o termo de busca de verdade ("painel solar em telhado residencial", não "imagem legal").
231
291
  7. MOVIMENTO. Em cada cena diga em "chaves" o movimento principal — é ele que tira a cara de apresentação parada.
232
- 8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar".`;
292
+ 8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar".
293
+ 9. UMA VOZ OU DUAS. Se o pedido fala em dois narradores, duas vozes, diálogo, conversa ou entrevista, escreva como DIÁLOGO: dê nome a cada um em "vozes" (com vozes diferentes) e diga em cada cena quem fala ("falante"). Com uma voz só, deixe "vozes" e "falante" de fora.
294
+
295
+ ${cardapioEstruturas()}`;
233
296
  };
234
297
 
235
298
  const REVISAO = (defeitos) => `O plano voltou com defeitos de FORMA. Corrija SÓ o que está listado e devolva o JSON inteiro de novo, no mesmo formato:
@@ -289,9 +352,11 @@ function pauta(plano) {
289
352
  linhas.push(`PLANO DE PRODUÇÃO — ${p.titulo}`);
290
353
  linhas.push(`formato ${p.formato} · ${p.proporcao} · ${p.segundos}s · tom ${p.tom}`);
291
354
  if (p.visual) linhas.push(`visual: ${p.visual}`);
355
+ if (p.estrutura) linhas.push(`estrutura: ${p.estrutura} (mande em roteiro.estrutura)`);
292
356
  if (p.promessa) linhas.push(`promessa: ${p.promessa}`);
293
357
  if (p.publico) linhas.push(`público: ${p.publico}`);
294
358
  linhas.push(`voz: ${p.voz && p.voz.perfil} · ${p.voz && p.voz.instrucao} · ${p.voz && p.voz.idioma}`);
359
+ if (p.vozes) linhas.push(`vozes: ${Object.entries(p.vozes).map(([k, v]) => `${k} = ${v}`).join(', ')} (mande em roteiro.vozes, e falante em cada parte)`);
295
360
  if (p.trilha) linhas.push(`trilha: ${p.trilha.busca || p.trilha.arquivo} (volume ${p.trilha.volume})`);
296
361
  linhas.push('');
297
362
  (p.cenas || []).forEach((c, i) => {
@@ -299,7 +364,7 @@ function pauta(plano) {
299
364
  if (c.titulo) linhas.push(` na tela: ${c.titulo}`);
300
365
  if (c.aTela) linhas.push(` composição: ${c.aTela}`);
301
366
  if (c.fundo) linhas.push(` fundo: ${typeof c.fundo === 'string' ? c.fundo : JSON.stringify(c.fundo)}`);
302
- linhas.push(` narração: ${c.narracao}`);
367
+ linhas.push(` narração${c.falante ? ` (${c.falante})` : ''}: ${c.narracao}`);
303
368
  if (c.camera && c.camera !== 'nenhuma') linhas.push(` câmera: ${c.camera}`);
304
369
  if (c.chaves) linhas.push(` movimento: ${c.chaves}`);
305
370
  (c.midia || []).forEach((m) => linhas.push(
@@ -314,6 +379,7 @@ function pauta(plano) {
314
379
 
315
380
  module.exports = {
316
381
  precisaDeProducao, precisaDeEdicao, vozPara, ritmoPara,
382
+ ESTRUTURAS, recentes, cardapioEstruturas, registrarEstrutura,
317
383
  normalizarPlano, extrairJSON, planejar, pauta,
318
384
  DIRETOR, REVISAO,
319
385
  };
@@ -26,6 +26,8 @@
26
26
 
27
27
  'use strict';
28
28
 
29
+ const decisao = require('./decisao.js');
30
+
29
31
  // ── as palavras que não carregam ideia ───────────────────────────────────
30
32
  const VAZIAS = new Set((
31
33
  'a o as os um uma uns umas de do da dos das no na nos nas em por para pra pro com sem que se e '
@@ -88,6 +90,25 @@ const ICONES = [
88
90
  [['surpre', 'chocad', 'absurd'], 'exploding-head'],
89
91
  ];
90
92
 
93
+ /* AS OUTRAS FORMAS DE DIZER O MESMO (07/10): "não quero que o sistema fique
94
+ repetindo os mesmos ícones". Cada conceito tinha UM emoji — todo vídeo de
95
+ dinheiro saía com o mesmo saco de dinheiro. O ícone automático continua
96
+ saindo daqui pelo primeiro nome (o de ICONES); a passada de variedade do
97
+ vídeo (index.js, variarIcones) troca pelo que os últimos vídeos menos
98
+ usaram, o mesmo conceito com o mesmo ícone dentro do vídeo. Nomes
99
+ conferidos no fluent-emoji-flat do Iconify em 07/10. */
100
+ const ALTERNATIVAS = {
101
+ robot: ['mechanical-arm', 'desktop-computer'], brain: ['thought-balloon', 'light-bulb'], 'light-bulb': ['sparkles', 'glowing-star', 'gem-stone'],
102
+ 'money-bag': ['dollar-banknote', 'coin', 'money-with-wings', 'credit-card'], stopwatch: ['hourglass-not-done', 'alarm-clock', 'mantelpiece-clock', 'watch'],
103
+ 'chart-increasing': ['seedling', 'mountain', 'chequered-flag'], 'bar-chart': ['clipboard', 'hundred-points'], rocket: ['high-voltage', 'airplane'],
104
+ fire: ['collision', 'high-voltage', 'dizzy'], 'check-mark-button': ['check-mark', 'thumbs-up', 'hundred-points'], 'cross-mark': ['stop-sign', 'no-entry'],
105
+ laptop: ['desktop-computer', 'keyboard'], 'globe-showing-americas': ['globe-with-meridians', 'satellite-antenna'], 'speaker-high-volume': ['microphone', 'headphone', 'megaphone'],
106
+ memo: ['pencil', 'open-book', 'clipboard'], books: ['open-book', 'pencil'], 'speech-balloon': ['speaking-head', 'thought-balloon', 'megaphone'],
107
+ gear: ['hammer-and-wrench', 'toolbox', 'link'], calendar: ['spiral-calendar'], trophy: ['sports-medal', 'crown', 'glowing-star'],
108
+ 'flexed-biceps': ['high-voltage'], handshake: ['people-hugging', 'link'], locked: ['shield', 'key'], briefcase: ['bank', 'classical-building'],
109
+ 'party-popper': ['confetti-ball', 'sparkles'], 'red-heart': ['sparkling-heart'], 'shopping-cart': ['receipt', 'credit-card'], 'clapper-board': ['video-camera', 'camera'],
110
+ };
111
+
91
112
  /* O termo de busca da IMAGEM DE APOIO, pelo emoji que a palavra ganhou. Em
92
113
  inglês de propósito: os acervos abertos respondem muito melhor assim. */
93
114
  const FOTO = {
@@ -225,14 +246,6 @@ function blocos(palavras, maximo) {
225
246
  return out;
226
247
  }
227
248
 
228
- // O zoom de cada cena, em rodízio: parado, punch-in, empurrando devagar.
229
- function camera(i, dur) {
230
- const tipo = i % 3;
231
- if (tipo === 0) return { zoom: [{ t: 0, v: 1 }, { t: dur, v: 1.04, e: 'linear' }] };
232
- if (tipo === 1) return { zoom: [{ t: 0, v: 1.18 }, { t: dur, v: 1.21, e: 'linear' }] };
233
- return { zoom: [{ t: 0, v: 1.07 }, { t: dur, v: 1.13, e: 'suave' }] };
234
- }
235
-
236
249
  const ESTILOS = {
237
250
  viral: { icones: true, letreiro: true, sons: true, barra: true, apoio: true, recorte: true, fundoRecorte: ['#1B1F3B', '#3A1C71'], destaque: '#FFD400', forte: '#00E0A4' },
238
251
  limpo: { icones: false, letreiro: false, sons: false, barra: false, apoio: true, recorte: false, fundoRecorte: ['#111111', '#333333'], destaque: '#FFFFFF', forte: '#FFD400' },
@@ -251,8 +264,6 @@ const ESTILOS = {
251
264
  * @param {object} op.sons { pop, whoosh } — caminhos na pasta do projeto, opcionais
252
265
  * @param {Set} op.iconesOk os nomes de ícone que existem de verdade (sem isso, todos valem)
253
266
  */
254
- // Em quais cenas a pessoa é recortada, com a palavra gigante atrás dela.
255
- const temRecorte = (i) => i % 4 === 2;
256
267
 
257
268
  function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {}, marca = null }) {
258
269
  const e = ESTILOS[estilo] || ESTILOS.viral;
@@ -263,6 +274,10 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
263
274
  const base = Math.min(largura, altura);
264
275
  const grupos = frases(lista);
265
276
  const total = duracao || lista[lista.length - 1].ate + 0.3;
277
+ /* O QUE CADA FRASE PEDE (decisao.js): movimento, recorte e whoosh pela
278
+ intensidade dela, não pelo rodízio i % 3 — e depois do golpe, respiro. */
279
+ const decisoes = decisao.decidir(grupos, { peso });
280
+ let ladoQuadro = 0;
266
281
  const cenas = [];
267
282
  const efeitos = [];
268
283
  const infos = []; // onde cada cena começa e a palavra forte dela — para o recorte
@@ -283,12 +298,13 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
283
298
  deOrigem: r2(ini), ateOrigem: r2(ini + dur), volume: 1,
284
299
  }];
285
300
 
286
- infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null });
301
+ infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null,
302
+ recorte: decisoes[i].recorte, intensidade: decisoes[i].intensidade });
287
303
 
288
304
  /* A PALAVRA ATRÁS DA PESSOA: o fundo do quarto some, entra a cor da
289
305
  marca, a palavra forte aparece enorme e a pessoa, recortada, fica
290
306
  na frente dela. É o efeito que mais denuncia edição de videomaker. */
291
- const rec = e.recorte && temRecorte(i) ? recortes[i] : null;
307
+ const rec = e.recorte && decisoes[i].recorte ? recortes[i] : null;
292
308
  if (rec && forte) {
293
309
  const vida = r2(rec.ate - rec.de);
294
310
  const some = [{ t: 0, v: 0 }, { t: 0.1, v: 1 }, { t: vida - 0.12, v: 1 }, { t: vida, v: 0 }];
@@ -403,10 +419,12 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
403
419
  });
404
420
  }
405
421
 
406
- // O whoosh entra no punch-in: é o corte que ele anuncia.
407
- if (e.sons && sons.whoosh && i > 0 && i % 3 === 1) efeitos.push({ t: r2(Math.max(0, ini - 0.12)), src: sons.whoosh, volume: 0.6 });
422
+ // O whoosh entra no soco: é o corte que ele anuncia (e só nele).
423
+ if (e.sons && sons.whoosh && decisoes[i].whoosh) efeitos.push({ t: r2(Math.max(0, ini - 0.12)), src: sons.whoosh, volume: 0.6 });
408
424
 
409
- cenas.push({ duracao: dur, camera: camera(i, dur), elementos, transicao: { tipo: 'corte' } });
425
+ const d = decisoes[i];
426
+ if (d.movimento === 'reenquadra') ladoQuadro = 1 - ladoQuadro;
427
+ cenas.push({ duracao: dur, camera: decisao.camera(d.movimento, dur, d.intensidade, ladoQuadro, base), elementos, transicao: { tipo: 'corte' }, impacto: d.movimento === 'soco' || undefined });
410
428
  });
411
429
 
412
430
  return {
@@ -473,4 +491,4 @@ async function prepararSons(pasta, video) {
473
491
  return sons;
474
492
  }
475
493
 
476
- module.exports = { montarEdicao, temRecorte, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, rotaDeApoio, DEPOIS_DA_PALAVRA, ESTILOS, ICONES, FOTO };
494
+ module.exports = { montarEdicao, ALTERNATIVAS, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, rotaDeApoio, DEPOIS_DA_PALAVRA, ESTILOS, ICONES, FOTO };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * efeitos.js — o efeito que a IA escreveu num vídeo fica para os próximos.
3
+ *
4
+ * "Quero mais animações, mais efeitos, e que possa criar tudo isso
5
+ * também." (Joel, 09/10/2026)
6
+ *
7
+ * O `componente` (plano.js) é a válvula: quando nenhuma primitiva alcança, a
8
+ * IA escreve um arquivo React na pasta do projeto. Só que ele morria com o
9
+ * projeto — o efeito bonito do vídeo de terça não existia para o de quarta,
10
+ * e a IA reescrevia do zero (ou pior, nem lembrava que tinha feito).
11
+ *
12
+ * Aqui o componente que ficou bom é GUARDADO em ~/.primocode/studio/efeitos/
13
+ * com nome e descrição, e volta como cardápio: os menos usados primeiro,
14
+ * pela mesma memória de variedade das trilhas e dos ícones (variedade.js,
15
+ * tipo 'efeito'). Guardar e reusar são escolhas do modelo; nada entra no
16
+ * vídeo sozinho.
17
+ *
18
+ * Antes de guardar, a fonte é conferida: o efeito vai rodar em OUTROS
19
+ * projetos, então import relativo (o arquivo vizinho não vai junto),
20
+ * Math.random/Date.now (cada render sai diferente) e timer (quadro
21
+ * repetido) são recusados com o motivo.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const os = require('os');
28
+ const path = require('path');
29
+ const variedade = require('./variedade.js');
30
+
31
+ const pasta = () => path.join(os.homedir(), '.primocode', 'studio', 'efeitos');
32
+ const catalogoArq = () => path.join(pasta(), 'catalogo.json');
33
+ const PACOTES = /^(react|react\/jsx-runtime|remotion|@remotion\/[a-z-]+|three|three\/examples\/jsm\/.+|@react-three\/fiber)$/;
34
+ const TETO = 150 * 1024;
35
+
36
+ function ler() {
37
+ try { const c = JSON.parse(fs.readFileSync(catalogoArq(), 'utf8')); return Array.isArray(c) ? c : []; } catch { return []; }
38
+ }
39
+
40
+ /** O que impede este código de rodar em outro projeto. [] = pode guardar. */
41
+ function conferirFonte(codigo) {
42
+ const falta = [];
43
+ const sem = String(codigo || '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
44
+ if (!/export\s+(default|function|const)/.test(sem)) falta.push('o arquivo não exporta o componente (export default function …)');
45
+ for (const m of sem.matchAll(/(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"]([^'"]+)['"]/g)) {
46
+ if (!PACOTES.test(m[1])) falta.push(`import "${m[1]}": só react, remotion, @remotion/*, three e @react-three/fiber — o arquivo vai sozinho para outro projeto`);
47
+ }
48
+ if (/Math\.random|Date\.now/.test(sem)) falta.push('Math.random/Date.now: cada render sairia diferente — use random("semente") do Remotion');
49
+ if (/setTimeout|setInterval|requestAnimationFrame/.test(sem)) falta.push('timer não existe no render quadro a quadro — anime pelo quadro (useCurrentFrame ou a prop t)');
50
+ return falta;
51
+ }
52
+
53
+ /** Guarda o componente do projeto no catálogo, com nome e descrição. */
54
+ function guardar({ pasta: proj, arquivo, nome, descricao, tags = [] } = {}) {
55
+ const slug = String(nome || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
56
+ if (slug.length < 2) return { ok: false, error: 'dê um nome ao efeito (nome), como "vidro-quebrando".' };
57
+ if (!String(descricao || '').trim()) return { ok: false, error: 'diga o que o efeito faz (descricao) — é o que você vai ler no cardápio depois.' };
58
+ if (!proj || !arquivo) return { ok: false, error: 'diga o projeto (id) e o arquivo do componente (arquivo).' };
59
+ const cheio = path.resolve(proj, arquivo);
60
+ if (!cheio.startsWith(path.resolve(proj) + path.sep) || !/\.(jsx|js|tsx)$/.test(cheio)) return { ok: false, error: 'o arquivo é um .jsx/.js dentro da pasta do projeto.' };
61
+ if (!fs.existsSync(cheio)) return { ok: false, error: `o arquivo "${arquivo}" não existe no projeto.` };
62
+ const codigo = fs.readFileSync(cheio, 'utf8');
63
+ if (codigo.length > TETO) return { ok: false, error: 'o componente passa de 150 KB — guarde a parte que é o efeito.' };
64
+ const falta = conferirFonte(codigo);
65
+ if (falta.length) return { ok: false, error: 'não guardei: ' + falta.join('; ') };
66
+ fs.mkdirSync(pasta(), { recursive: true });
67
+ fs.writeFileSync(path.join(pasta(), slug + '.jsx'), codigo);
68
+ const item = { nome: slug, descricao: String(descricao).trim().slice(0, 300), tags: [].concat(tags || []).map(String).slice(0, 8), guardado: new Date().toISOString().slice(0, 10) };
69
+ fs.writeFileSync(catalogoArq(), JSON.stringify([item, ...ler().filter((x) => x.nome !== slug)], null, 2));
70
+ return { ok: true, ...item };
71
+ }
72
+
73
+ /** O cardápio: os guardados, os menos usados primeiro (memória da variedade). */
74
+ function cardapio({ aleatorio = Math.random } = {}) {
75
+ const itens = ler().filter((x) => fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
76
+ return variedade.pontuar(itens, 'efeito', { chave: (x) => x.nome })
77
+ .map((p) => ({ ...p, pontos: p.pontos + aleatorio() * 0.05 }))
78
+ .sort((a, b) => b.pontos - a.pontos)
79
+ .map((p) => ({ nome: p.opcao.nome, descricao: p.opcao.descricao, tags: p.opcao.tags }));
80
+ }
81
+
82
+ /** Copia o efeito para o projeto e devolve o elemento pronto. Marca o uso. */
83
+ function reusar({ pasta: proj, nome } = {}) {
84
+ const item = ler().find((x) => x.nome === nome);
85
+ const de = item && path.join(pasta(), item.nome + '.jsx');
86
+ if (!item || !fs.existsSync(de)) return { ok: false, error: `não há efeito "${nome}" guardado — veja o cardápio (acao: efeitos).` };
87
+ if (!proj) return { ok: false, error: 'diga o projeto (id).' };
88
+ const src = `efeitos/${item.nome}.jsx`;
89
+ fs.mkdirSync(path.join(proj, 'efeitos'), { recursive: true });
90
+ fs.copyFileSync(de, path.join(proj, src));
91
+ variedade.marcar('efeito', item.nome);
92
+ return { ok: true, src, descricao: item.descricao, elemento: { tipo: 'componente', src, x: 0, y: 0 } };
93
+ }
94
+
95
+ module.exports = { guardar, cardapio, reusar, conferirFonte, pasta };
@@ -51,13 +51,23 @@ const normal = (e) => {
51
51
  /* O RODÍZIO: quando ninguém escolheu ("vários estilos... que não sejam padrão
52
52
  da IA", Joel 05/10), o padrão não é sempre o híbrido — gira entre os que
53
53
  servem a qualquer roteiro, sem repetir os últimos (memória 'estilo'). */
54
- const RODIZIO = ['hibrido', 'cinema', 'gerativo', 'impacto', 'fluido'];
54
+ /* Com imagem pedida (08/10): o padrão é o FLUIDO MISTO — motion com a
55
+ imagem em poucos momentos, cada uma entrando de um jeito. O híbrido e o
56
+ cinema ("10 mil imagens de banco e a mesma animação") ficam pelo nome. */
57
+ const RODIZIO = ['fluido'];
58
+ /* Sem imagem (o padrão desde 07/10), o rodízio fica nos dois motions que
59
+ nasceram das referências boas (motions-exemplos/motion-god): o fluido e o
60
+ gerativo. O impacto (emoji 3D em cada batida, cara de anúncio) saiu do
61
+ rodízio em 08/10 — "horríveis, nada a ver com as referências" — e fica
62
+ para quem o pede pelo nome (o agente ou o roteiro). */
63
+ const RODIZIO_SEM_IMAGEM = ['fluido', 'gerativo'];
55
64
 
56
- function estiloDaVez(aleatorio = Math.random) {
57
- return variedade.sortear(RODIZIO, 'estilo', { aleatorio }) || 'hibrido';
65
+ function estiloDaVez(aleatorio = Math.random, semImagem = false) {
66
+ const lista = semImagem ? RODIZIO_SEM_IMAGEM : RODIZIO;
67
+ return variedade.sortear(lista, 'estilo', { aleatorio }) || lista[0];
58
68
  }
59
69
 
60
- function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, aleatorio } = {}) {
70
+ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, semImagem = false, aleatorio } = {}) {
61
71
  const pedidoAgente = normal(agente);
62
72
  if (pedidoAgente) return pedidoAgente;
63
73
  const doRoteiro = normal(roteiro);
@@ -73,10 +83,11 @@ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = fa
73
83
  /* Anúncio, reels e o resto: o HÍBRIDO. "Um mix da versão antiga (o reels
74
84
  da imersão, no cinema) e da última (o kit) — absurdo, que nem parece
75
85
  que foi IA" (29/09). Com `girar`, o padrão roda (RODIZIO). */
76
- return girar ? estiloDaVez(aleatorio) : 'hibrido';
86
+ if (girar) return estiloDaVez(aleatorio, semImagem);
87
+ return semImagem ? 'gerativo' : 'hibrido';
77
88
  }
78
89
 
79
90
  /** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
80
91
  const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido']);
81
92
 
82
- module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO };
93
+ module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM };