primocode 10.10.0 → 10.10.1

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,8 @@ 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
+ + '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
98
  + '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
99
  + 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
100
100
  + '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 +110,11 @@ const GRUPOS = [
110
110
  + 'o render empurra para dentro, encolhe o texto largo e afasta o colado; enfeite de fundo marque decorativo: true.' },
111
111
 
112
112
  { 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). '
113
+ 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
114
  + '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. '
115
+ + '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. '
116
+ + 'Monte o do vídeo pelo ASSUNTO em roteiro.estiloVisual; misture um salvo com {base:"<nome>", ...}; crie paleta nova com 5 cores. '
117
+ + '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
118
  + '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
119
  + '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
120
  + '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,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
  };
@@ -70,6 +70,8 @@ const glifos = require('./glifos.js');
70
70
  const personagem = require('./personagem.js');
71
71
  const mundo = require('./mundo.js');
72
72
  const cenarios = require('./cenarios.js');
73
+ const estilos = require('./estilos.js');
74
+ const { conferirCena } = require('./cena.js');
73
75
  const movimentos = require('./movimentos.js');
74
76
  const colecao = require('./colecao.js');
75
77
  const { escolherEstilo, LEVES } = require('./estilo.js');
@@ -428,9 +430,13 @@ async function ferramentaPlano(args) {
428
430
  const usada = diretor.registrarEstrutura(original || args.roteiro);
429
431
  projeto.salvar(args.id, { estrutura: usada.estrutura || '-' });
430
432
  }
433
+ const quebradas = args.__cenasQuebradas || [];
431
434
  return {
432
435
  ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
433
- proximo: 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
436
+ ...(quebradas.length ? { cenasQuebradas: quebradas } : {}),
437
+ proximo: quebradas.length
438
+ ? `${quebradas.length} cena(s) sua(s) não compilaram e ficaram no gerativo: conserte o arquivo (o erro está em cenasQuebradas) e mande o roteiro de novo antes do render.`
439
+ : 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
434
440
  };
435
441
  }
436
442
 
@@ -575,6 +581,10 @@ async function ferramentaMidia(args) {
575
581
  if (acao === 'salvar_cenario' || acao === 'salvar_cenário') {
576
582
  return cenarios.salvar({ nome: args.nome, cenario: args.cenario, autorizado: args.autorizado, bases: Object.keys(mundo.LUGARES) });
577
583
  }
584
+ /* OS ESTILOS (estilos.js, 14/10): as peças do gerativo como cardápio — a IA
585
+ monta, mistura e cria; guardar só com o sim da pessoa. */
586
+ if (acao === 'estilos' || acao === 'estilo') return estilos.cardapio();
587
+ if (acao === 'salvar_estilo') return estilos.salvar({ nome: args.nome, pecas: args.pecas || args.estilo, descricao: args.descricao, autorizado: args.autorizado });
578
588
  if (acao === 'biblioteca' || acao === 'cardapio' || acao === 'cardápio') return colecao.cardapio();
579
589
  if (acao === 'icones3d' || acao === 'icone3d' || acao === 'animacoes' || acao === 'animacao' || acao === 'animação' || acao === 'animações') {
580
590
  const ehAnim = /^anima/.test(acao);
@@ -1263,6 +1273,15 @@ async function sonsReais(p, video) {
1263
1273
  }
1264
1274
 
1265
1275
  const PROPORCOES = { '16:9': [1920, 1080], '9:16': [1080, 1920], '1:1': [1080, 1080], '4:5': [1080, 1350] };
1276
+ /* A proporção que o PEDIDO disse (14/10: "vertical 9:16" saiu deitado, porque
1277
+ só valia a proporção que o modelo passasse). A do modelo ainda manda. */
1278
+ function proporcaoDoPedido(pedido) {
1279
+ const t = String(pedido || '');
1280
+ if (/\b9\s*[:x×]\s*16\b|\bvertical\b|\breels?\b|\bstor(y|ies)\b|\btik\s?tok\b|\bshorts\b/i.test(t)) return '9:16';
1281
+ if (/\b4\s*[:x×]\s*5\b/.test(t)) return '4:5';
1282
+ if (/\b1\s*[:x×]\s*1\b|\bquadrad[oa]\b/i.test(t)) return '1:1';
1283
+ return null;
1284
+ }
1266
1285
 
1267
1286
  /** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
1268
1287
  /* A ASSINATURA (assinatura.js): o vídeo é medido e conferido contra as
@@ -1326,6 +1345,13 @@ async function prepararRoteiro(p, args) {
1326
1345
  fs.copyFileSync(achado, destino);
1327
1346
  pt.componente = path.relative(p.pasta, destino).replace(/\\/g, '/');
1328
1347
  } else pt.componente = path.relative(p.pasta, achado).replace(/\\/g, '/');
1348
+ // A cena quebrada não derruba o vídeo: sai, a parte fica no gerativo, e o erro volta para a IA (cena.js).
1349
+ const conf = conferirCena(path.join(p.pasta, pt.componente));
1350
+ if (!conf.ok) {
1351
+ (args.__cenasQuebradas = args.__cenasQuebradas || []).push({ componente: pt.componente, erro: conf.erro });
1352
+ projeto.anotar(p.id, 'plano', `componente ${pt.componente} quebrado (${conf.erro}): a parte ficou no gerativo`);
1353
+ delete pt.componente;
1354
+ }
1329
1355
  }
1330
1356
  // As imagens do pedido (o terminal guardou em referencias.imagens) entram sozinhas, uma vez por projeto.
1331
1357
  const jaTem = new Set((p.referencias || []).map((x) => x.de));
@@ -1354,14 +1380,6 @@ async function prepararRoteiro(p, args) {
1354
1380
  const semImagem = referencias.semImagem(ref);
1355
1381
  let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
1356
1382
  if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
1357
- /* AS FONTES DA CASA (13/10, o primocode-ele-constroi.mp4 que o Joel aprovou):
1358
- no gerativo, título em Inter e texto em JetBrains Mono, com as CORES da
1359
- marca. Só muda se o pedido falar de fonte. */
1360
- if (estilo === 'gerativo' && !/\bfontes?\b|tipografia|\bserifa/i.test(pedidoTxt)) {
1361
- roteiro.marca = { ...(roteiro.marca || {}), fontes: { titulo: 'Inter', texto: 'JetBrains Mono' } };
1362
- }
1363
- // O estilo da casa no gerativo: chão escuro com brilho, frase média, palavra que entra desfocando (gerativo.js).
1364
- if (estilo === 'gerativo') roteiro.casa = true;
1365
1383
  /* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
1366
1384
  com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
1367
1385
  o agente tenha escolhido outro estilo pelo nome. */
@@ -1393,10 +1411,27 @@ async function prepararRoteiro(p, args) {
1393
1411
  vídeos (09/10, "vício em cores, músicas, fontes"): por PROJETO, como a semente. */
1394
1412
  // O assunto do vídeo é o rótulo da moldura (quando ela sai): o título do projeto.
1395
1413
  if (estilo === 'gerativo' && !roteiro.titulo && p.titulo) roteiro.titulo = p.titulo;
1414
+ // O estilo que a IA montou (estilos.js) vence o sorteio e o que o projeto tinha guardado.
1415
+ if (estilo === 'gerativo' && roteiro.estiloVisual && !roteiro.escolhasGerativo) {
1416
+ const r = estilos.montar(roteiro.estiloVisual, roteiro);
1417
+ roteiro.escolhasGerativo = r.escolhas;
1418
+ projeto.salvar(p.id, { escolhasGerativo: r.escolhas });
1419
+ if (r.avisos.length) projeto.anotar(p.id, 'estilo', r.avisos.join(' · '));
1420
+ }
1396
1421
  if (estilo === 'gerativo' && !roteiro.escolhasGerativo) {
1397
1422
  roteiro.escolhasGerativo = p.escolhasGerativo || gerativo.escolherVisual(roteiro);
1398
1423
  if (!p.escolhasGerativo) projeto.salvar(p.id, { escolhasGerativo: roteiro.escolhasGerativo });
1399
1424
  }
1425
+ /* O ESTILO DA CASA (13/10, o primocode-ele-constroi.mp4 que o Joel aprovou):
1426
+ chão escuro com brilho, frase média, palavra que entra desfocando, Inter
1427
+ e JetBrains Mono. Vale para o vídeo de TECNOLOGIA (o do PrimoCode) quando
1428
+ a IA não montou estilo: em todo vídeo, a padaria saía igual — "animações
1429
+ tudo igual, cores sempre as mesmas, sem ícones" (14/10). */
1430
+ const casa = estilo === 'gerativo' && !roteiro.estiloVisual && (roteiro.escolhasGerativo || {}).humor === 'tecnologia';
1431
+ if (casa && !/\bfontes?\b|tipografia|\bserifa/i.test(pedidoTxt)) {
1432
+ roteiro.marca = { ...(roteiro.marca || {}), fontes: { titulo: 'Inter', texto: 'JetBrains Mono' } };
1433
+ }
1434
+ if (casa) roteiro.casa = true;
1400
1435
  if (ref.modo === 'fechado' && !roteiro.busca && referencias.termos().length) roteiro.busca = referencias.termos();
1401
1436
  /* A BIBLIOTECA (biblioteca.js): sem fonte e cor da marca, o híbrido, o
1402
1437
  cinema e o impacto caíam sempre no Inter e no azul e roxo — "os vídeos
@@ -1409,7 +1444,7 @@ async function prepararRoteiro(p, args) {
1409
1444
  // Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
1410
1445
  const ehMotion = LEVES.has(estilo);
1411
1446
  const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
1412
- const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'slides' ? PROPORCOES['16:9'] : PROPORCOES['16:9']);
1447
+ const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'video' && PROPORCOES[proporcaoDoPedido(p.pedido)]) || PROPORCOES['16:9'];
1413
1448
 
1414
1449
  const iconesOk = new Set();
1415
1450
  for (const nome of composicao.iconesDoRoteiro(roteiro)) {
@@ -1919,6 +1954,17 @@ async function prepararRoteiro(p, args) {
1919
1954
  '-af', 'afade=t=in:st=0:d=0.4,loudnorm=I=-18:TP=-2:LRA=11', '-ar', '44100', norm], { timeout: 5 * 60000 });
1920
1955
  if (c.ok && fs.existsSync(norm)) trilha = path.relative(p.pasta, norm).replace(/\\/g, '/');
1921
1956
  }
1957
+ /* SEM ÍCONE NENHUM (14/10, a padaria: "sem ícones"): o modelo grátis não
1958
+ dá herói, e o gerativo de comida não tem forma abstrata — o vídeo
1959
+ saía só com texto. Na parte em que o modelo não disse nada (nem
1960
+ "nenhum"), o ícone 3D que É a coisa que a fala nomeia ("pão" → bread). */
1961
+ const autoHerois = [];
1962
+ for (const pt of partes) {
1963
+ if (!pt || pt.heroi != null || pt.icone != null) continue;
1964
+ const id = colecao.melhorIcone3d([pt.titulo, pt.destaque, pt.fala, pt.texto].filter(Boolean).join(' '), 80);
1965
+ if (id && !autoHerois.includes(id)) { pt.heroi = id; autoHerois.push(id); }
1966
+ }
1967
+ if (autoHerois.length) projeto.anotar(p.id, 'icones', `sem herói do modelo, a biblioteca achou: ${autoHerois.join(', ')}`);
1922
1968
  /* O herói que o roteiro pediu do Iconify ("mdi:bread-slice"): o desenho é
1923
1969
  baixado aqui (fica em midia/icones); o que não baixar, a batida fica sem. */
1924
1970
  const icones = {};
@@ -2402,7 +2448,7 @@ async function ferramentaSite(args) {
2402
2448
  return r;
2403
2449
  }
2404
2450
 
2405
- module.exports = {
2451
+ module.exports = { proporcaoDoPedido,
2406
2452
  vazamentoNoComeco, variarIcones, objetosDoRoteiro,
2407
2453
  ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
2408
2454
  ferramentaVideo, ferramentaRender, ferramentaSite,
package/lib/tools.js CHANGED
@@ -594,6 +594,12 @@ async function despachar(name, args, ctx) {
594
594
  {
595
595
  switch (name) {
596
596
  case 'write_file': {
597
+ // O modelo grátis manda o caminho com outro nome (file_path, caminho) ou nenhum,
598
+ // e o write caía na própria pasta: EISDIR, e ele tentava de novo igual (14/10).
599
+ if (!args.path) args.path = args.file_path || args.filePath || args.caminho || args.arquivo || args.file || '';
600
+ if (!String(args.path).trim()) {
601
+ return { ok: false, error: 'faltou o caminho do arquivo em "path".', comoResolver: 'chame write_file de novo com path:"pasta/nome.ext" e content.' };
602
+ }
597
603
  // Vídeo e áudio NÃO se escrevem com write_file. Num pedido
598
604
  // real o agente criou um "video.mp4" de 43 bytes de texto e
599
605
  // disse "vídeo criado com sucesso" — um arquivo falso, morto,
@@ -610,6 +616,11 @@ async function despachar(name, args, ctx) {
610
616
  }
611
617
  const filePath = resolveInProject(args.path, projectDir);
612
618
  const content = args.content || '';
619
+ try {
620
+ if (fs.statSync(filePath).isDirectory()) {
621
+ return { ok: false, error: `"${args.path}" é uma PASTA, não um arquivo.`, comoResolver: `ponha o nome do arquivo no fim: path:"${String(args.path).replace(/\/+$/, '')}/index.html" (ou o nome que for).` };
622
+ }
623
+ } catch { /* não existe: vai ser criado */ }
613
624
  const created = !fs.existsSync(filePath);
614
625
  // Reescrever um arquivo que já existe é a operação mais
615
626
  // perigosa do agente: o conteúdo antigo some inteiro. Ler o
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.10.0",
3
+ "version": "10.10.1",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -22,59 +22,58 @@ Não faça nada disto, em nenhum vídeo:
22
22
 
23
23
  ## O que separa motion bom de slide animado
24
24
 
25
- **A câmera existe.** Zoom, pan e dolly são da CENA, não do elemento. Um zoom
26
- lento de 1.0 para 1.08 ao longo de uma cena dá vida a um quadro parado — e
27
- quase nenhum vídeo automático faz isso.
25
+ **A câmera existe.** Um zoom lento de 1.0 para 1.08 na cena inteira dá vida a
26
+ um quadro parado.
28
27
 
29
28
  **Nada entra do nada.** Um elemento entra vindo de algum lugar (de baixo, de
30
- trás, de fora do quadro), com `saida` ou `elastico` na suavização. Movimento
29
+ trás, de fora do quadro), com `spring` do Remotion ou easing de saída. Movimento
31
30
  linear é a assinatura da máquina: no mundo físico nada começa e para na mesma
32
31
  velocidade.
33
32
 
34
- **Profundidade.** Camadas que se movem em velocidades diferentes (parallax)
35
- custam duas linhas e mudam a cena inteira: fundo devagar, meio médio, frente
36
- rápido. Com `perspectiva` na cena, `rotacaoX`/`rotacaoY`/`z` viram 3D de
37
- verdade.
33
+ **Profundidade.** Parallax (fundo devagar, frente rápido); `perspective` com
34
+ `rotateX`/`translateZ`, ou three, para 3D de verdade.
38
35
 
39
- **Ritmo.** Cena de informação simples: 1,5 a 2,5s. Cena com frase para ler: 3
40
- a 4s. Fecho: 2s. Variar a duração é o que cria ritmo; repetir cria sono.
36
+ **Ritmo.** Informação simples 1,5–2,5 s; frase para ler 3–4 s; fecho 2 s.
41
37
 
42
38
  **Silêncio visual.** Um quadro com uma frase e mais nada vale mais que um com
43
39
  seis elementos. Espaço vazio é composição, não desperdício.
44
40
 
45
- ## Motion é contínuo, não uma tela por ideia
46
-
47
- Slide troca de tela. Motion é UM plano onde o mesmo objeto VIRA o próximo: a
48
- bolinha acende um fogo, se deforma em foguete, mergulha no celular e vira o
49
- ícone; o cursor gira e clica; o ícone salta e cobre a tela com o app dentro.
50
-
51
- Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
52
- `estudio_plano`, não uma cena por frase:
53
-
54
- - **`forma: "traco"`**: forma livre por `caminho` (`d` de SVG, caixa 0 0 100
55
- 100, M/L/C/Q/Z). `caminho` e `cor` animam:
56
- `"caminho": [{"t":1.6,"v":BOLA},{"t":2.5,"v":FOGUETE,"e":"elastico"}]`.
57
- - **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
58
- app nasce do tamanho e da COR do ícone.
59
-
60
- Sem imagem é o padrão (foto só se pedirem). Decida `clima`, o impacto (`destaque`, ≤1 em 3 partes), o
61
- respiro e o `estilo` deste roteiro: `gerativo`, `fluido`, `impacto`, `vox`, `marca`, `lancamento`; vazio, gira.
62
- `hibrido` só com imagem pedida. Leia os avisos da revisão.
41
+ ## O caminho: você escreve cada cena
42
+
43
+ Motion nesse nível não sai de molde: cada cena é UMA ideia desenhada para a
44
+ fala — a interface do produto montando, o número que cresce, o objeto em 3D,
45
+ o gráfico que mostra a ideia, a palavra gigante que se transforma. Quem
46
+ desenha é você, em React.
47
+
48
+ 1. `estudio_projeto` (criar). A marca tem site? `estudio_site` mede cores e fontes.
49
+ 2. `estudio_midia acao:"estilos"`: monte a identidade pelo assunto em
50
+ `roteiro.estiloVisual` (humor, paleta, display, texto…). Um vídeo, uma
51
+ identidade: 2 chãos, 1 acento, 2 fontes.
52
+ 3. `estudio_narrar` com o roteiro: os tempos MEDIDOS de cada fala.
53
+ 4. Para cada parte, `write_file` com `path:"componentes/<nome>.jsx"`:
54
+ `export default function Cena({ el, t, fps })`, `t` em segundos desde o
55
+ começo da parte. Em `el.dados`: `titulo`, `fala`, `palavras` (cada uma com
56
+ o seu `t`, para entrar junto com a voz), `duracao`, `cores` {fundo, texto,
57
+ acento}, `fontes`, `largura`, `altura`. Imports só de react, remotion,
58
+ @remotion/*, three. Cor e fonte vêm de `el.dados`, nunca inventadas na cena.
59
+ 5. `estudio_plano` com o roteiro: `estiloVisual` e, em cada parte,
60
+ `componente:"componentes/<nome>.jsx"`. Voltou `cenasQuebradas`? Conserte o
61
+ arquivo e mande o roteiro de novo.
62
+ 6. `estudio_render`. A voz, a trilha, os sons e o fundo são do Estúdio.
63
+
64
+ Parte sem `componente` cai no gerativo (a tipografia anima sozinha): é a
65
+ reserva, não o objetivo. Sem imagem é o padrão (foto só se pedirem).
63
66
 
64
67
  ## Cor e tipo
65
68
 
66
- Escolha a paleta a partir do ASSUNTO, e escreva os valores. Três a cinco
67
- cores: uma dominante, uma de destaque, neutros com viés da dominante. Se a
68
- marca existe, leia o site dela com `estudio_site` e use as cores medidas —
69
- não as que você imagina que ela usa.
69
+ A paleta sai do ASSUNTO, no `estiloVisual` (um nome do cardápio ou 5 cores
70
+ suas). Se a marca existe, as cores medidas pelo `estudio_site` mandam — não
71
+ as que você imagina que ela usa.
70
72
 
71
- Duas fontes, no máximo: uma de display para o que grita, uma neutra para o
72
- que se lê. `estudio_midia acao:"fontes"` mostra um leque grande — sair do
73
- Inter/Montserrat é metade do trabalho de não parecer genérico.
73
+ Duas fontes, no máximo: display para o que grita, neutra para o que se lê.
74
74
 
75
75
  ## Antes de renderizar
76
76
 
77
- - Todo `src` aponta para arquivo que existe na pasta (baixe antes).
78
77
  - Os tempos das cenas batem com os tempos MEDIDOS da narração.
79
78
  - Nenhuma cena tem a mesma composição da anterior.
80
79
  - Leia o seu próprio plano e pergunte: em que este vídeo é diferente do