primocode 10.9.1 → 10.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.9.1
1
+ # PrimoCode v10.10.0
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.9.1
12
+ ▐▛███▜▌ Primo Code v10.10.0
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/lib/act.js CHANGED
@@ -939,6 +939,8 @@ async function runAct(userPrompt, opts) {
939
939
  + 'Senão: '
940
940
  + 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
941
941
  + 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.\n'
942
+ /* "deixar os vídeos cinematográficos… fazer um color grading sozinha" (13/10). */
943
+ + 'CINEMATOGRÁFICO: `primocode video grade --json \'{}\'` mede a imagem e faz o color grading de cinema sozinho (look:cinema|quente|noite|natural|pb, intensidade); use antes do export quando a pessoa quer o vídeo bonito, e OLHE o inspect.\n'
942
944
  + 'EFEITOS que seguem a pessoa: separate_layers (recorte + momento 3D), replace_background (fundo novo no estalo), '
943
945
  + 'attach_3d (logo 3D na mão, segue a mão), merge_on_join (mãos juntas → fusão → cérebro), cut_silence_at (silêncio depois do estalo), '
944
946
  + 'style_captions (cor, legenda em cima da cabeça) — detalhe em `primocode video help efeitos`. Atalho: `primocode video do --json \'{"pedido":"<o pedido em português>"}\'`.' });
package/lib/catalogo.js CHANGED
@@ -93,7 +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
- + 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega — 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 (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) | '
97
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). '
98
99
  + 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
99
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. '
@@ -323,7 +323,7 @@ const MOLDURAS = ['sem', 'cantos', 'completa'];
323
323
  'bokeh' (as manchas de luz desfocadas na frente). O acento: 'traco' (cada
324
324
  traço se desenha e some) ou 'viagem' (o ponto que nasce no meio, viaja
325
325
  entre as batidas e se recolhe — o de antes). */
326
- const AMBIENTES = ['limpo', 'luz', 'bokeh'];
326
+ const AMBIENTES = ['limpo', 'luz', 'bokeh', 'brilho'];
327
327
  const ACENTOS_JEITO = ['traco', 'viagem'];
328
328
  const RITMOS_ICONE = ['seguidos', 'alternados', 'esparsos'];
329
329
  const MATERIAIS_ICONE = ['argila', 'verniz', 'metal', 'perola', 'acetinado'];
@@ -815,7 +815,25 @@ function desenharSom(P, { trilha = null, sons = {} } = {}) {
815
815
  const eco = () => `aecho=0.8:${f(sh.entre(0.4, 0.75))}:${Math.round(sh.entre(30, 90))}|${Math.round(sh.entre(100, 220))}:${f(sh.entre(0.15, 0.35))}|${f(sh.entre(0.08, 0.2))}`;
816
816
  const ss = () => f(sh.entre(4, Math.max(6, DUR + 20))); // de onde da trilha sai o pedaço
817
817
  const sint = sonsDoVideo({ partes: P.batidas.map((b) => ({ titulo: b.comp, fala: String(b.de) })) }).receitas;
818
+ /* OS SONS PEQUENOS (13/10: "90% do motion é efeito sonoro: barulhinho
819
+ bonitinho, gostoso, satisfatório"). Sintetizados, limpos e cada um com
820
+ a sua altura: o tique da palavra que entra, o pop do ícone e do item, o
821
+ brilho da marca. Não vêm da trilha: são o "clique" do motion. */
822
+ const MICRO = new Set(['tique', 'pop', 'brilho']);
823
+ const receitaMicro = (gesto) => {
824
+ if (gesto === 'tique') {
825
+ const fq = Math.round(sh.entre(1800, 3200)), d = f(sh.entre(0.05, 0.08));
826
+ return ['-f', 'lavfi', '-i', `aevalsrc='0.55*sin(2*PI*${fq}*t)*exp(-${Math.round(sh.entre(70, 110))}*t)+0.12*(random(0)*2-1)*exp(-200*t)':s=44100:d=${d}`, '-af', `highpass=f=900,${panF()}`];
827
+ }
828
+ if (gesto === 'pop') {
829
+ const a0 = Math.round(sh.entre(700, 1200)), a1 = Math.round(sh.entre(380, 640));
830
+ return ['-f', 'lavfi', '-i', `aevalsrc='0.8*sin(2*PI*(${a0}-${a0 - a1}*t*5)*t)*exp(-${Math.round(sh.entre(22, 34))}*t)*(1-exp(-800*t))':s=44100:d=0.2`, '-af', panF()];
831
+ }
832
+ const b0 = Math.round(sh.entre(780, 1050));
833
+ return ['-f', 'lavfi', '-i', `aevalsrc='0.3*sin(2*PI*${b0}*t)*exp(-3*t)+0.2*sin(2*PI*${Math.round(b0 * 1.5)}*t)*exp(-3.8*t)+0.12*sin(2*PI*${b0 * 2}*t)*exp(-4.6*t)':s=44100:d=1.3`, '-af', `${eco()},${panF()}`];
834
+ };
818
835
  const receita = (gesto) => {
836
+ if (MICRO.has(gesto)) return receitaMicro(gesto);
819
837
  if (!musica) { // sem trilha: a síntese, um arquivo por evento (a semente muda a cada um)
820
838
  const base = { revela: 'revela0', golpe: sh.um(['golpe0', 'golpe1']), fatia: sh.um(['nasce0', 'nasce1', 'nasce2']), gaguejo: sh.um(['passagem0', 'passagem1']), ar: sh.um(['passagem1', 'passagem2']) }[gesto];
821
839
  const args = [...sint[base]];
@@ -854,7 +872,9 @@ function desenharSom(P, { trilha = null, sons = {} } = {}) {
854
872
  };
855
873
  const evento = (t, gesto, volume) => {
856
874
  if (t < 0 || t > DUR - 0.1) return;
857
- if (efeitos.some((e) => Math.abs(e.t - t) < 0.25)) return;
875
+ // Som grande não encosta em som grande (0,25 s); o pequeno só não encosta em nada a menos de 0,09 s.
876
+ const micro = MICRO.has(gesto);
877
+ if (efeitos.some((e) => Math.abs(e.t - t) < (micro || MICRO.has(e.gesto) ? 0.09 : 0.25))) return;
858
878
  const nome = 'e' + String(efeitos.length).padStart(2, '0') + '-' + gesto;
859
879
  receitas[nome] = receita(gesto);
860
880
  efeitos.push({ t: f(Math.max(0, t + (sh.prox() - 0.5) * 0.02)), nome, gesto, volume: f(volume * (0.85 + sh.prox() * 0.3)) });
@@ -882,6 +902,18 @@ function desenharSom(P, { trilha = null, sons = {} } = {}) {
882
902
  if (tr === 'atravessa' || tr === 'inunda') return evento(b.de - 0.4, 'ar', 0.75);
883
903
  return evento(b.de - 0.08, 'fatia', 0.75);
884
904
  });
905
+ // O estilo da casa ganha os sons pequenos: cada palavra da tese, cada ícone e item, e o brilho da marca.
906
+ if (P.somDenso) {
907
+ bs.forEach((b) => {
908
+ for (const l of b.linhas || []) {
909
+ if (l.papel === 'tese' || l.papel === 'numero') for (const p of l.palavras || []) if (p.t != null) evento(p.t, 'tique', 0.32);
910
+ if (l.papel === 'item' && l.t != null) evento(l.t, 'pop', 0.42);
911
+ }
912
+ if (b.icone3d && b.icone3d.t != null) evento(b.icone3d.t, 'pop', 0.5);
913
+ if (b.comp === 'marca') evento(b.de + 0.35, 'brilho', 0.55);
914
+ if (b.livre) evento(b.de + 0.08, 'pop', 0.45); // a cena da IA entra com um pop
915
+ });
916
+ }
885
917
  efeitos.sort((a, b) => a.t - b.t);
886
918
  // O src é o nome que sons-leves.prepararReceitas dá (semente + nome): um arquivo por evento.
887
919
  const sufixo = (P.semente >>> 0).toString(36);
@@ -905,6 +937,27 @@ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], n
905
937
  const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos, icones });
906
938
  const DUR = partitura.duracao;
907
939
 
940
+ /* A PARTE QUE A IA DESENHA (13/10: "a IA vai criar os estilos, os ícones,
941
+ as coisas"). A parte com `componente` aponta um React escrito pela IA na
942
+ pasta do projeto; ele ocupa o quadro no tempo da batida e recebe em
943
+ el.dados o texto, cada palavra com o seu tempo (t relativo ao início da
944
+ batida), as cores e as fontes. O Estúdio segue cuidando da voz, da
945
+ trilha, dos sons e do fundo; a batida deixa de desenhar o texto dela. */
946
+ const livres = [];
947
+ partitura.batidas.forEach((b) => {
948
+ const pt = partes[b.parte];
949
+ const src = pt && typeof pt.componente === 'string' ? pt.componente.trim() : '';
950
+ if (!src) return;
951
+ const palavras = (b.linhas || []).flatMap((l) => (l.palavras || []).map((p) => ({ texto: p.texto, t: p.t != null ? r3(p.t - b.de) : null })));
952
+ livres.push({ tipo: 'componente', src, de: b.de, ate: b.ate, dados: {
953
+ fala: pt.fala || '', titulo: pt.titulo || '', palavras, duracao: r3(b.ate - b.de),
954
+ cores: b.cores, fontes: partitura.fontes, largura, altura, ...(pt.dados && typeof pt.dados === 'object' ? pt.dados : {}) } });
955
+ // As linhas ficam (o leiaute do kit conta com elas), mas ocultas: quem desenha é a cena da IA.
956
+ for (const l of b.linhas || []) l.oculta = true;
957
+ b.acento = []; b.livre = true;
958
+ delete b.heroi; delete b.icone3d; delete b.textura; delete b.contador;
959
+ });
960
+
908
961
  // ── o som: efeitos nas transformações, a música abaixa sob a voz ─────
909
962
  /* O SOM (desenharSom): um evento por batida, no máximo — e cada um é um
910
963
  arquivo PRÓPRIO, feito da trilha do vídeo (ou sintetizado, sem trilha). */
@@ -928,7 +981,7 @@ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], n
928
981
  // As chaves "fonte*" são o que o fontes.css procura (remotion.js).
929
982
  fonte: partitura.fontes.display, fonteTexto: partitura.fontes.texto, fonteMono: partitura.fontes.mono,
930
983
  fonteCinetica: partitura.fontes.cinetica, fonteSerifa: partitura.fontes.serifa,
931
- }],
984
+ }, ...livres],
932
985
  }],
933
986
  audio: {
934
987
  ...(narracao ? { narracao } : {}),
@@ -1498,6 +1551,34 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1498
1551
  }
1499
1552
  });
1500
1553
 
1554
+ /* O ESTILO DA CASA (13/10, as referências do Joel — o "Vantage"): o vídeo
1555
+ inteiro no chão escuro da marca, com o brilho do acento derivando no
1556
+ fundo; a frase média (título 7–11% da altura, motionmaxxing SKILL:134),
1557
+ não a palavra gigante; cada palavra entra inteira, desfocando (type.md
1558
+ R2.6), na hora em que a voz a diz; sem a parede de texto repetido. */
1559
+ if (roteiro.casa) {
1560
+ fundo.ambiente = 'brilho';
1561
+ const escura = batidas.reduce((m, b) => (b.cores && (!m || luz(b.cores.fundo) < luz(m.fundo)) ? b.cores : m), null);
1562
+ const teto = (deitado ? 0.11 : 0.075) * H;
1563
+ for (const b of batidas) {
1564
+ if (escura) b.cores = { ...escura };
1565
+ b.troca = 'corte';
1566
+ // A saída é da LINHA inteira (desliza) ou corte seco: o "evapora" e o "comprime" espalhavam as letras ("a ad e").
1567
+ if (b.saida && b.saida !== 'fim') b.saida = batidas.indexOf(b) % 2 ? 'corte' : 'desliza';
1568
+ delete b.textura;
1569
+ // O enfeite abstrato (colunas 3D, truchet, a textura de curvas) sai; o ícone do site (b.icone3d) fica.
1570
+ delete b.heroi;
1571
+ for (const l of b.linhas || []) {
1572
+ // O teto vale também para o "encher" do kit (cresce até l.max): sem isso a tese gigante passava de 30% da altura.
1573
+ if ((l.papel === 'tese' || l.papel === 'numero' || l.papel === 'fraco') && !l.marca) {
1574
+ if (l.tamanho > teto) l.tamanho = Math.round(teto);
1575
+ if (l.encher) l.max = Math.round(Math.min(l.max || teto, teto));
1576
+ }
1577
+ if (l.papel === 'tese' && !(b.contador && b.contador.linha === l.id)) l.comportamento = 'desfoque';
1578
+ }
1579
+ }
1580
+ }
1581
+
1501
1582
  return {
1502
1583
  versao: 1, semente, fps: FPS, humor,
1503
1584
  musica: { genero: ou(E.genero, s.um(H0.generos)), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
@@ -1505,6 +1586,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
1505
1586
  margem: M, fundo, batidas, camera, produtos,
1506
1587
  identidade: { chaos: ident.chaos, acento: ident.acento, comeca: campos[comeca], fontes: [display, texto] }, secoes: secoes.nomes,
1507
1588
  hud: molduraDe(roteiro, E, { W, H, deitado, marca, secoes: secoes.nomes }),
1589
+ somDenso: !!roteiro.casa,
1508
1590
  };
1509
1591
  }
1510
1592
 
@@ -898,6 +898,12 @@ async function variarIcones(plano, roteiro) {
898
898
  const explicitos = [];
899
899
  try { JSON.stringify(roteiro || {}, (k, v) => { if (k === 'icone' && typeof v === 'string') explicitos.push(v); return v; }); } catch { /* sem */ }
900
900
  const troca = {};
901
+ /* VÁRIAS BIBLIOTECAS NO MESMO VÍDEO (13/10: "não uma biblioteca de ícone,
902
+ mas várias, que vai misturar tudo"). Antes o vídeo sorteava UMA coleção
903
+ e trocava todos os ícones para ela. Agora cada ícone tenta uma coleção,
904
+ em rodízio a partir da sorteada; sem o desenho nela, a próxima. */
905
+ const colecoes = [colecao, ...assets.COLECOES.map((c) => c[0]).filter((c) => c !== colecao)];
906
+ let vez = 0;
901
907
  for (const nome of variedade.iconesTrocaveis(plano, explicitos)) {
902
908
  /* Primeiro O ÍCONE: o conceito escolhe entre as formas dele
903
909
  (edicao.ALTERNATIVAS) pelos pontos da variedade — o que os últimos
@@ -914,9 +920,12 @@ async function variarIcones(plano, roteiro) {
914
920
  variedade.marcar('icone-nome', escolhido);
915
921
  }
916
922
  let novo = 'fluent-3d:' + escolhido;
917
- if (colecao !== 'fluent-3d') {
918
- try { const r = await assets.icone(colecao + ':' + escolhido); if (r && r.ok) novo = colecao + ':' + escolhido; } catch { /* fica o 3D */ }
923
+ for (let k = 0; k < colecoes.length; k++) {
924
+ const c = colecoes[(vez + k) % colecoes.length];
925
+ if (c === 'fluent-3d') break;
926
+ try { const r = await assets.icone(c + ':' + escolhido); if (r && r.ok) { novo = c + ':' + escolhido; break; } } catch { /* a próxima */ }
919
927
  }
928
+ vez++;
920
929
  if (novo !== nome) troca[nome] = novo;
921
930
  }
922
931
  const n = variedade.trocarIcones(plano, troca);
@@ -1300,6 +1309,24 @@ async function prepararRoteiro(p, args) {
1300
1309
  for (const pt of roteiro.partes || []) {
1301
1310
  if (pt.src && !existe(pt.src)) { if (!pt.busca && /^https?:/i.test(pt.src)) pt.busca = pt.titulo || ''; delete pt.src; }
1302
1311
  }
1312
+ /* A CENA QUE A IA ESCREVEU (parte.componente): ela escreve na pasta de
1313
+ trabalho, e o Remotion só lê o que está no projeto. O arquivo é copiado
1314
+ para componentes/ do projeto; se não existe, a parte fica no estilo da
1315
+ casa (e o caderno diz por quê), em vez de o render quebrar. */
1316
+ for (const pt of roteiro.partes || []) {
1317
+ if (!pt || typeof pt.componente !== 'string' || !pt.componente.trim()) continue;
1318
+ const pedido = pt.componente.trim();
1319
+ const achado = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
1320
+ .find((f) => f && fs.existsSync(f) && /\.(jsx|tsx|js)$/i.test(f));
1321
+ if (!achado) { projeto.anotar(p.id, 'plano', `componente ${pedido} não existe: a parte ficou no estilo da casa`); delete pt.componente; continue; }
1322
+ const dentro = path.resolve(p.pasta) + path.sep;
1323
+ if (!achado.startsWith(dentro)) {
1324
+ const destino = path.join(p.pasta, 'componentes', path.basename(achado));
1325
+ fs.mkdirSync(path.dirname(destino), { recursive: true });
1326
+ fs.copyFileSync(achado, destino);
1327
+ pt.componente = path.relative(p.pasta, destino).replace(/\\/g, '/');
1328
+ } else pt.componente = path.relative(p.pasta, achado).replace(/\\/g, '/');
1329
+ }
1303
1330
  // As imagens do pedido (o terminal guardou em referencias.imagens) entram sozinhas, uma vez por projeto.
1304
1331
  const jaTem = new Set((p.referencias || []).map((x) => x.de));
1305
1332
  const novas = referencias.imagens().filter((f) => !jaTem.has(f));
@@ -1333,6 +1360,8 @@ async function prepararRoteiro(p, args) {
1333
1360
  if (estilo === 'gerativo' && !/\bfontes?\b|tipografia|\bserifa/i.test(pedidoTxt)) {
1334
1361
  roteiro.marca = { ...(roteiro.marca || {}), fontes: { titulo: 'Inter', texto: 'JetBrains Mono' } };
1335
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;
1336
1365
  /* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
1337
1366
  com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
1338
1367
  o agente tenha escolhido outro estilo pelo nome. */
@@ -2061,10 +2090,63 @@ async function transcrever(p, alvo, ctx) {
2061
2090
 
2062
2091
  // ── estudio_render ───────────────────────────────────────────────────────
2063
2092
 
2093
+ /* O SEGUNDO MOTOR: HYPERFRAMES (heygen-com/hyperframes, Apache-2.0, 13/10:
2094
+ "trabalhar com o Remotion, mas também com o Hyperframes"). A IA escreve a
2095
+ composição em HTML (index.html com data-start/data-duration e GSAP, CSS ou
2096
+ three.js) numa pasta dela; aqui a pasta vai para o projeto, ganha a midia/
2097
+ do projeto (voz, trilha, sons do Estúdio), e o CLI renderiza num Chrome
2098
+ escondido — sem telemetria (HYPERFRAMES_NO_TELEMETRY). Depois, o mesmo do
2099
+ Remotion: volume de Reels, a conferência e o vídeo aberto. */
2100
+ const HYPERFRAMES = 'hyperframes@0.8.154';
2101
+ async function renderHyperframes(p, args, ctx = {}) {
2102
+ const pedido = String(args.html || args.composicao || 'hyperframes').trim();
2103
+ const origem = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
2104
+ .find((d) => d && fs.existsSync(path.join(d, 'index.html')));
2105
+ if (!origem) return { ok: false, error: `não achei ${pedido}/index.html — escreva a composição do Hyperframes numa pasta com index.html e passe html:"<pasta>".` };
2106
+ const destino = path.join(p.pasta, 'hyperframes');
2107
+ if (path.resolve(origem) !== path.resolve(destino)) {
2108
+ fs.rmSync(destino, { recursive: true, force: true });
2109
+ fs.cpSync(origem, destino, { recursive: true });
2110
+ }
2111
+ const midia = path.join(destino, 'midia');
2112
+ if (!fs.existsSync(midia) && fs.existsSync(path.join(p.pasta, 'midia'))) fs.symlinkSync(path.join(p.pasta, 'midia'), midia);
2113
+ const saida = path.join(p.pasta, 'saida', `${projeto.apelidar(p.titulo)}.mp4`);
2114
+ fs.mkdirSync(path.dirname(saida), { recursive: true });
2115
+ (ctx.aoFalar || (() => {}))('renderizando no Hyperframes');
2116
+ const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
2117
+ const r = await new Promise((resolve) => {
2118
+ const { spawn } = require('child_process');
2119
+ const proc = spawn(npx, ['--yes', HYPERFRAMES, 'render', destino, '-o', saida, '--quiet'], { env: { ...process.env, HYPERFRAMES_NO_TELEMETRY: '1', DO_NOT_TRACK: '1' } });
2120
+ let erro = '';
2121
+ proc.stderr.on('data', (d) => { erro += d; });
2122
+ proc.stdout.on('data', (d) => { erro += d; });
2123
+ const relogio = setTimeout(() => { try { proc.kill(); } catch { /* já saiu */ } }, (Number(args.minutos) || 20) * 60000);
2124
+ proc.on('close', (code) => { clearTimeout(relogio); resolve({ ok: code === 0 && fs.existsSync(saida), erro }); });
2125
+ proc.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, erro: e.message }); });
2126
+ });
2127
+ if (!r.ok) return { ok: false, error: `o Hyperframes não renderizou: ${r.erro.slice(-400)}`, comoResolver: 'precisa de Node 22+ e do ffmpeg; rode `npx hyperframes doctor` na pasta.' };
2128
+ const med = await video.medir(saida);
2129
+ if (med.ok && med.temAudio) {
2130
+ const tmp = saida.replace(/\.mp4$/, '.norm.mp4');
2131
+ const ln = await video.rodar(video.ffmpeg(), ['-y', '-i', saida, '-c:v', 'copy', '-af', 'loudnorm=I=-14:TP=-1.2:LRA=10', '-c:a', 'aac', '-b:a', '256k', tmp], { timeout: 15 * 60000 });
2132
+ if (ln.ok && fs.existsSync(tmp)) fs.renameSync(tmp, saida); else { try { fs.unlinkSync(tmp); } catch { /* não havia */ } }
2133
+ }
2134
+ const bytes = fs.statSync(saida).size;
2135
+ projeto.salvar(p.id, { renders: [...(p.renders || []), { quando: new Date().toISOString(), arquivo: saida, bytes, motor: 'hyperframes' }] });
2136
+ projeto.anotar(p.id, 'render', `hyperframes: ${(bytes / 1048576).toFixed(1)} MB, ${(med.segundos || 0).toFixed(1)}s`);
2137
+ const conf = await conferencia.conferir(saida, { duracao: med.segundos || 0 });
2138
+ if (conf.ok && conf.avisos.length) projeto.anotar(p.id, 'conferencia', conf.avisos.join('; '));
2139
+ if (ctx.abrir) { try { ctx.abrir(saida); } catch { /* o caminho está na resposta */ } }
2140
+ return { ok: true, motor: 'hyperframes', arquivo: saida, bytes, segundos: med.segundos,
2141
+ conferencia: conf.ok ? { som: conf.som, parados: conf.parados, avisos: conf.avisos } : undefined,
2142
+ aviso: 'ABERTO para o usuário assistir. Chame finish dizendo o que foi feito' + (conf.ok && conf.avisos.length ? ' — e cite os avisos da conferência.' : '.') };
2143
+ }
2144
+
2064
2145
  async function ferramentaRender(args, ctx = {}) {
2065
2146
  const p = projetoOuRecente(args.id);
2066
2147
  if (!p) return semProjeto(args.id);
2067
2148
  if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
2149
+ if (String(args.motor || '').toLowerCase() === 'hyperframes' || args.html) return renderHyperframes(p, args, ctx);
2068
2150
  const pl = projeto.lerPlano(args.id);
2069
2151
  if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
2070
2152
 
@@ -592,7 +592,8 @@ function pontoDaLetra({ t, fps, l, p, idx, n, b, cx, cores, alvoSaida }) {
592
592
 
593
593
  // ── uma linha ──────────────────────────────────────────────────────────
594
594
  function Linha({ l, b, cx, t, fps, P, frame, alvoSaida, centroBloco }) {
595
- if (!cx) return null;
595
+ // A linha da parte que a IA desenha (componente): o texto está na cena dela.
596
+ if (!cx || l.oculta) return null;
596
597
  const folga = Math.max(0, (P.largura * 0.94) / Math.max(1, cx.w) - 1);
597
598
  // O GOLPE: a tese soca (escala que bate e assenta).
598
599
  let soco = 1;
@@ -725,7 +726,26 @@ function Fundo({ P, b, prevB, t, cam, frame, origem }) {
725
726
  return (
726
727
  <div style={{ position: 'absolute', inset: 0, background: corBase, overflow: 'hidden' }}>
727
728
  {circulo}
728
- {(f.ambiente || 'bokeh') !== 'limpo' && <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />}
729
+ {(f.ambiente || 'bokeh') !== 'limpo' && f.ambiente !== 'brilho' && <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />}
730
+ {/* O BRILHO (13/10, a referência "Vantage" do Joel): dois halos grandes
731
+ da cor de acento derivando devagar no fundo escuro, e um arco de luz
732
+ no horizonte na batida da marca e no fim. É o ambiente, não um efeito. */}
733
+ {f.ambiente === 'brilho' && (() => {
734
+ const ac = b.cores.acento;
735
+ const h1 = { x: 30 + 14 * Math.sin(t * 0.21), y: 38 + 10 * Math.cos(t * 0.17) }, h2 = { x: 72 + 12 * Math.cos(t * 0.15), y: 66 + 9 * Math.sin(t * 0.23) };
736
+ const pulso = 0.9 + 0.1 * Math.sin(t * 0.8);
737
+ const arco = b.comp === 'marca' || b.arco;
738
+ const ka = arco ? ed(c01((t - b.de) / 0.9)) : 0;
739
+ return (
740
+ <>
741
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 55% 60% at ' + h1.x + '% ' + h1.y + '%, ' + alfa(ac, (claro ? 0.22 : 0.42) * pulso) + ' 0%, transparent 70%)' }} />
742
+ <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 45% 50% at ' + h2.x + '% ' + h2.y + '%, ' + alfa(ac, (claro ? 0.14 : 0.28) * pulso) + ' 0%, transparent 70%)' }} />
743
+ {ka > 0 && <div style={{ position: 'absolute', left: -W * 0.25, width: W * 1.5, height: W * 1.5, top: H * (1.02 - 0.3 * ka), borderRadius: '50%',
744
+ boxShadow: '0 0 ' + (W * 0.05) + 'px ' + (W * 0.012) + 'px ' + alfa(ac, 0.75 * ka) + ', inset 0 ' + (W * 0.02) + 'px ' + (W * 0.05) + 'px ' + alfa(ac, 0.45 * ka),
745
+ borderTop: Math.max(2, W * 0.002) + 'px solid ' + alfa('#ffffff', 0.55 * ka) }} />}
746
+ </>
747
+ );
748
+ })()}
729
749
  {(f.particulas || []).map((q, i) => {
730
750
  const x = ((q.x - cam.x * q.z * 1.6 + t * q.v * (i % 2 ? 1 : -0.6)) % W + W) % W;
731
751
  const y = ((q.y - cam.y * q.z * 1.6 - t * q.v * 0.7) % H + H) % H;
@@ -80,8 +80,15 @@ function notasDaAssinatura() {
80
80
  function instrucao(atual = ATUAL) { const i = instrucaoDoModo(atual); return i ? i + notasDaAssinatura() : ''; }
81
81
  function instrucaoDoModo({ modo, escopo, explicito } = ATUAL) {
82
82
  // O vídeo sem imagem é motion: o agente escolhe o estilo de motion que serve ao roteiro, não um fixo.
83
- const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som, no ESTILO DA CASA (o gerativo, com as cores da '
84
- + 'marca): não ponha roteiro.estilo, a menos que a pessoa tenha pedido outro estilo pelo nome. '
83
+ const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som. O estilo da casa (o gerativo, com as cores da marca) '
84
+ + 'é o PONTO DE PARTIDA, não o limite: não ponha roteiro.estilo, a menos que a pessoa peça outro pelo nome. '
85
+ /* A IA CRIA (13/10: "a IA vai criar os estilos, os ícones, as coisas"). */
86
+ + 'Quando a referência ou o pedido pede um visual que o estilo não faz (uma janela de app, um gráfico, uma colagem, um objeto), '
87
+ + 'CRIE a cena: escreva um React (write_file, ex.: componentes/cena-x.jsx, export default ({el, t}) => …, t em segundos desde o '
88
+ + 'começo da parte; el.dados traz palavras [{texto, t}], cores {fundo, texto, acento}, fontes, largura, altura) e ponha '
89
+ + 'componente:"componentes/cena-x.jsx" naquela parte. A voz, a trilha, os sons e o fundo continuam do Estúdio. '
90
+ + 'Vídeo inteiro desenhado por você: escreva uma composição Hyperframes (pasta com index.html, data-start/data-duration, GSAP), '
91
+ + 'use a voz do estudio_narrar e os sons de midia/, e estudio_render motor:"hyperframes" html:"<pasta>". '
85
92
  /* O ROTEIRO (13/10: "o roteiro que monta é horrível" — eram os títulos da landing em fila). */
86
93
  + 'O ROTEIRO é uma história de 15 a 25 s: gancho (a situação de quem assiste) → o que o produto FAZ → UMA prova concreta '
87
94
  + 'dos fatos do site → a ação (nome, comando, link). Nunca os títulos das seções do site em sequência. Cada fala tem de 5 a '
package/lib/tools.js CHANGED
@@ -815,7 +815,8 @@ async function despachar(name, args, ctx) {
815
815
  case 'estudio_projeto':
816
816
  return estudio.ferramentaProjeto(args, ctx);
817
817
  case 'estudio_plano':
818
- return estudio.ferramentaPlano(args);
818
+ // A pasta de trabalho vai junto: o componente que a IA escreveu mora nela (index.js copia para o projeto).
819
+ return estudio.ferramentaPlano({ ...args, __pasta: projectDir });
819
820
  case 'estudio_midia':
820
821
  return estudio.ferramentaMidia(args);
821
822
  case 'estudio_narrar':
@@ -825,7 +826,8 @@ async function despachar(name, args, ctx) {
825
826
  // contexto do pedido atravessa até aqui.
826
827
  return estudio.ferramentaVideo(args, ctx);
827
828
  case 'estudio_render':
828
- return estudio.ferramentaRender(args, {
829
+ // A pasta de trabalho vai junto: a composição do Hyperframes que a IA escreveu mora nela.
830
+ return estudio.ferramentaRender({ ...args, __pasta: projectDir }, {
829
831
  ...ctx,
830
832
  // O vídeo pronto abre para a pessoa assistir. É a única
831
833
  // coisa que abre: foi o vídeo que ela pediu.
package/lib/ui.js CHANGED
@@ -567,13 +567,26 @@ const ROTULOS = {
567
567
  seguidas eram uma coluna de ganchos sem dizer o que era ação e o que era
568
568
  saída. O argumento vem numa linha só e cortado: um comando de três linhas
569
569
  no cabeçalho empurra o resultado para fora da vista. */
570
+ /* O CAMINHO LEGÍVEL (13/10: "deixar o layout mais organizado"). O cabeçalho
571
+ mostrava o caminho cru — "/Users/joel/.primocode/studio/projetos/2026-10/
572
+ primocode-bastidor-vox/midia/logo-prim…" — e o que importa (o arquivo)
573
+ era o que o corte comia. Agora: a pasta pessoal vira ~, o projeto do
574
+ Estúdio vira "projeto <id> ›", e caminho longo fica com o fim. */
575
+ function encurtarCaminhos(texto) {
576
+ const home = require('os').homedir();
577
+ return String(texto || '')
578
+ .replace(/(?:~|\/[^\s'"]*?)\/\.primocode\/studio\/projetos\/(?:\d{4}-\d{2}\/)?([^\/\s'"]+)\/?/g, (m, id) => `projeto ${id} › `)
579
+ .split(home).join('~')
580
+ .replace(/(?:~|\/)[^\s'"]{50,}/g, (cam) => { const ps = cam.split('/').filter(Boolean); return ps.length > 3 ? '…/' + ps.slice(-2).join('/') : cam; });
581
+ }
582
+
570
583
  function actionHeader(name, args) {
571
584
  const label = ROTULOS[name] || name;
572
585
  let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
573
586
  // Campo que veio como objeto (o modelo às vezes manda {"window": {}})
574
587
  // virava "[object Object]" na tela.
575
588
  if (argStr && typeof argStr === 'object') argStr = JSON.stringify(argStr).slice(0, 60);
576
- argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
589
+ argStr = encurtarCaminhos(String(argStr || '').replace(/\s+/g, ' ').trim());
577
590
  // O que ele está fazendo AGORA — a barra da direita da tela cheia mostra.
578
591
  agora.texto = label + (argStr ? ' · ' + argStr.slice(0, 60) : '');
579
592
  agora.desde = Date.now();
@@ -0,0 +1,104 @@
1
+ /**
2
+ * cor.js — o color grading do vídeo da pessoa, decidido pela própria imagem.
3
+ *
4
+ * "deixar os vídeos cinematográficos, não só recortar o fundo… ela conseguir
5
+ * sozinha fazer um color grading. No meu vídeo." (Joel, 13/10/2026)
6
+ *
7
+ * O `apply_color` já existia, mas só com números passados à mão — e quem
8
+ * passava era o agente, chutando. Aqui a imagem decide:
9
+ *
10
+ * 1. MEDE o ffmpeg (signalstats) lê, a 1 quadro/s nos primeiros 60 s: o
11
+ * brilho médio, a sombra e a luz mais forte, a saturação e a
12
+ * dominante de cor (U e V longe de 128 = vídeo azulado ou amarelado).
13
+ * 2. CORRIGE a exposição vai para o meio (gama, não brilho chapado) e a
14
+ * dominante sai — o look é posto em cima de uma imagem neutra.
15
+ * 3. LOOK o de cinema separa sombra e luz: sombra para o azul-petróleo, luz
16
+ * (e a pele) para o laranja, curva de filme com o preto levemente
17
+ * levantado, saturação contida, vinheta e grão leves.
18
+ *
19
+ * O look sai pela imagem quando ninguém pediu: escuro vira "noite", quase sem
20
+ * cor vira "natural", o resto "cinema". Medir é I/O; corrigir e montar os
21
+ * filtros são puros — o teste confere sem vídeo nenhum.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const LOOKS = ['cinema', 'quente', 'noite', 'natural', 'pb'];
27
+
28
+ /** Média das métricas do signalstats impressas pelo `metadata=print`. Pura. */
29
+ function lerMedidas(texto) {
30
+ const soma = {}, n = {};
31
+ for (const m of String(texto || '').matchAll(/lavfi\.signalstats\.(YAVG|YLOW|YHIGH|SATAVG|UAVG|VAVG)=([\d.]+)/g)) {
32
+ soma[m[1]] = (soma[m[1]] || 0) + Number(m[2]);
33
+ n[m[1]] = (n[m[1]] || 0) + 1;
34
+ }
35
+ if (!n.YAVG) return null;
36
+ const media = (k, padrao) => (n[k] ? soma[k] / n[k] : padrao);
37
+ return { y: media('YAVG'), ylow: media('YLOW', 16), yhigh: media('YHIGH', 235), sat: media('SATAVG', 30), u: media('UAVG', 128), v: media('VAVG', 128) };
38
+ }
39
+
40
+ async function medir(arquivo, { video = require('../estudio/video.js') } = {}) {
41
+ const r = await video.rodar(video.ffmpeg(), ['-hide_banner', '-t', '60', '-i', arquivo, '-an',
42
+ '-vf', 'fps=1,scale=320:-2,signalstats,metadata=print:file=-', '-f', 'null', '-'], { timeout: 5 * 60000 });
43
+ const m = lerMedidas(r.saida + '\n' + r.erro);
44
+ return m ? { ok: true, ...m } : { ok: false, error: 'não consegui medir a cor do vídeo' };
45
+ }
46
+
47
+ const lim = (v, a, b) => Math.max(a, Math.min(b, v));
48
+ const n3 = (v) => Number(v.toFixed(3));
49
+
50
+ /** O look pela imagem, quando ninguém pediu. Pura. */
51
+ function escolherLook(m) {
52
+ if (m.y < 72) return 'noite';
53
+ if (m.sat < 10) return 'natural';
54
+ return 'cinema';
55
+ }
56
+
57
+ /** A correção que deixa a imagem neutra antes do look. Pura. */
58
+ function correcao(m) {
59
+ // Gama que leva o brilho médio para ~44% (o meio de um vídeo bem exposto).
60
+ // No eq do ffmpeg a saída é entrada^(1/gama): gama = ln(brilho)/ln(0,44) (>1 clareia).
61
+ const gama = lim(Math.log(lim(m.y / 255, 0.05, 0.95)) / Math.log(0.44), 0.72, 1.4);
62
+ // Dominante: U acima de 128 é azul demais, V acima é vermelho demais. Tira ~70% dela.
63
+ const azul = lim(-((m.u - 128) / 128) * 0.7, -0.15, 0.15);
64
+ const vermelho = lim(-((m.v - 128) / 128) * 0.7, -0.15, 0.15);
65
+ // Faixa curta entre a sombra e a luz = imagem lavada: um pouco de contraste.
66
+ const contraste = m.yhigh - m.ylow < 170 ? 1.08 : 1;
67
+ return { gama: n3(gama), azul: n3(azul), vermelho: n3(vermelho), contraste };
68
+ }
69
+
70
+ const CURVA_FILME = "0/0.035 0.25/0.21 0.5/0.5 0.78/0.82 1/0.97";
71
+
72
+ /** Os filtros do ffmpeg para o grade. Puro: { look, intensidade 0..1, ...correção }. */
73
+ function filtrosDoGrade(p = {}) {
74
+ const k = lim(p.intensidade != null ? Number(p.intensidade) : 0.8, 0, 1);
75
+ const look = LOOKS.includes(p.look) ? p.look : 'cinema';
76
+ const f = [];
77
+ // 1. a imagem neutra
78
+ const eq = [`gamma=${n3(p.gama || 1)}`];
79
+ if (p.contraste && p.contraste !== 1) eq.push(`contrast=${n3(p.contraste)}`);
80
+ f.push('eq=' + eq.join(':'));
81
+ if (p.azul || p.vermelho) f.push(`colorbalance=rm=${n3(p.vermelho || 0)}:bm=${n3(p.azul || 0)}`);
82
+ // 2. o look, na intensidade pedida
83
+ const s = (v) => n3(v * k);
84
+ if (look === 'cinema') {
85
+ f.push(`colorbalance=rs=${s(-0.06)}:bs=${s(0.09)}:rh=${s(0.07)}:gh=${s(0.02)}:bh=${s(-0.07)}`);
86
+ f.push(`curves=all='${CURVA_FILME}'`, `eq=saturation=${n3(1 - 0.1 * k)}`);
87
+ } else if (look === 'quente') {
88
+ f.push(`colorbalance=rm=${s(0.05)}:bm=${s(-0.05)}:rh=${s(0.04)}:bh=${s(-0.05)}`);
89
+ f.push(`curves=all='0/0.03 0.5/0.52 1/0.97'`, `eq=saturation=${n3(1 + 0.05 * k)}`);
90
+ } else if (look === 'noite') {
91
+ f.push(`colorbalance=rs=${s(-0.04)}:bs=${s(0.1)}:rm=${s(-0.02)}:bm=${s(0.06)}`);
92
+ f.push(`curves=all='0/0.02 0.3/0.24 0.6/0.6 1/0.94'`, `eq=saturation=${n3(1 - 0.22 * k)}`);
93
+ } else if (look === 'pb') {
94
+ f.push('hue=s=0', `curves=all='0/0.04 0.3/0.24 0.7/0.78 1/0.97'`);
95
+ } else {
96
+ f.push(`curves=all='0/0.02 0.25/0.23 0.75/0.78 1/0.98'`);
97
+ }
98
+ // 3. o acabamento de filme: vinheta e grão leves
99
+ f.push(`vignette=angle=${n3(Math.PI / (5.8 - 1.2 * k))}`);
100
+ if (k > 0.2) f.push(`noise=alls=${Math.round(3 + 4 * k)}:allf=t`);
101
+ return f;
102
+ }
103
+
104
+ module.exports = { LOOKS, lerMedidas, medir, escolherLook, correcao, filtrosDoGrade };
@@ -92,6 +92,9 @@ const EFEITOS = {
92
92
  return f;
93
93
  },
94
94
  },
95
+ // O grade de cinema (cor.js): correção medida + look. Quem preenche os números é a op `grade`.
96
+ 'color.cine': { params: 'look (cinema|quente|noite|natural|pb), intensidade (0..1, 0.8), gama, contraste, azul, vermelho',
97
+ filtro: (p) => require('./cor.js').filtrosDoGrade(p) },
95
98
  'blur.gaussian': { params: 'sigma (px, 8)', filtro: (p) => [`gblur=sigma=${num(lim(p.sigma != null ? p.sigma : 8, 0.1, 200))}`] },
96
99
  sharpen: { params: 'amount (0..3, 1)', filtro: (p) => [`unsharp=5:5:${num(lim(p.amount != null ? p.amount : 1, 0, 3))}`] },
97
100
  'stylize.vignette': { params: 'strength (0..1, 0.5)', filtro: (p) => [`vignette=angle=${num(0.25 + 0.9 * lim(p.strength !== undefined ? p.strength : 0.5, 0, 1))}`] },
package/lib/video/ops.js CHANGED
@@ -209,6 +209,8 @@ const OPS = {
209
209
  },
210
210
  },
211
211
  apply_color: {
212
+ // Fora da ajuda (13/10): o `grade` mede e escolhe sozinho; este, de número à mão, segue funcionando.
213
+ oculto: true,
212
214
  uso: `apply_color {ids|"all",preset?(${Object.keys(F.PRESETS_COR).join('|')}),exposure,contrast,saturation,temperature,tint(-1..1),gamma}`,
213
215
  async run(ctx, a) {
214
216
  precisaProjeto(ctx);
@@ -218,6 +220,35 @@ const OPS = {
218
220
  });
219
221
  },
220
222
  },
223
+ /* O COLOR GRADING SOZINHO (cor.js, 13/10): mede cada mídia, corrige a
224
+ exposição e a dominante e põe o look de cinema — sem número chutado.
225
+ Troca o apply_color/grade que o clipe tinha. */
226
+ grade: {
227
+ uso: `grade {ids?|"all",look?(${require('./cor.js').LOOKS.join('|')}; sem: a imagem escolhe),intensidade?:0.8} — color grading de cinema medido do vídeo; depois inspect e OLHE`,
228
+ async run(ctx, a) {
229
+ precisaProjeto(ctx);
230
+ const C = require('./cor.js');
231
+ const ids = alvos(ctx.tl, { ...a, ids: a.ids || 'all' }).filter((id) => { const { clip } = T.achar(ctx.tl, id); return clip.asset && ctx.tl.assets[clip.asset] && clip.type !== 'audio'; });
232
+ const porAsset = {};
233
+ for (const id of ids) {
234
+ const { clip } = T.achar(ctx.tl, id);
235
+ if (porAsset[clip.asset]) continue;
236
+ const asset = ctx.tl.assets[clip.asset];
237
+ const m = await C.medir(asset.path);
238
+ if (!m.ok) return m;
239
+ const look = C.LOOKS.includes(a.look) ? a.look : C.escolherLook(m);
240
+ porAsset[clip.asset] = { look, ...C.correcao(m), intensidade: a.intensidade != null ? Number(a.intensidade) : 0.8 };
241
+ }
242
+ const r = await mutar(ctx, 'grade', (tl) => {
243
+ for (const id of ids) {
244
+ const { clip } = T.achar(tl, id);
245
+ const params = porAsset[clip.asset];
246
+ if (params) clip.effects = (clip.effects || []).filter((e) => e.type !== 'color.adjust' && e.type !== 'color.cine').concat({ type: 'color.cine', params });
247
+ }
248
+ });
249
+ return { ...r, grade: porAsset };
250
+ },
251
+ },
221
252
  apply_effect: {
222
253
  uso: `apply_effect {ids|"all",type(${Object.keys(F.EFEITOS).filter((k) => k !== 'color.adjust').join('|')}),params?,remove?:true}`,
223
254
  async run(ctx, a) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.9.1",
3
+ "version": "10.10.0",
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/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/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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
package/studio/edge.py CHANGED
@@ -204,6 +204,11 @@ def sintetizar(texto, voz_cfg=None, timeout=90):
204
204
  if r.returncode != 0 or not os.path.exists(mp3) or os.path.getsize(mp3) < 800:
205
205
  msg = (r.stderr or b"").decode("utf-8", "replace")[-200:]
206
206
  sys.stderr.write("[edge] síntese falhou: %s\n" % msg)
207
+ # 403 do Bing = edge-tts velho (13/10): atualiza em segundo plano, e a próxima fala já sai.
208
+ if "403" in msg:
209
+ threading.Thread(target=lambda: subprocess.run(
210
+ [sys.executable, "-m", "pip", "install", "--user", "--quiet", "--upgrade", "--break-system-packages", "edge-tts"],
211
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=240), daemon=True).start()
207
212
  return None, None
208
213
  with open(mp3, "rb") as fh:
209
214
  dados = fh.read()
package/studio/narrar.py CHANGED
@@ -317,6 +317,22 @@ def comando_roteiro(args):
317
317
  try:
318
318
  ps = _parte_edge(texto, v["voz"], reg["ritmo"], reg["tom"], mp3)
319
319
  except Exception as e:
320
+ # 403 do Bing: a Microsoft trocou a autenticação e o edge-tts
321
+ # desta máquina ficou velho (13/10: o 7.2.8 levava 403, o
322
+ # 7.3.0 não). Sem isto, toda narração caía na voz robótica de
323
+ # reserva quando a cota do Gemini acabava. Atualiza uma vez e
324
+ # roda de novo num processo novo (o módulo velho já carregou).
325
+ if "403" in str(e) and not os.environ.get("PRIMO_EDGE_ATUALIZADO"):
326
+ for estrategia in ESTRATEGIAS:
327
+ try:
328
+ r = subprocess.run([sys.executable] + estrategia[:3] + ["--upgrade"] + estrategia[3:],
329
+ capture_output=True, timeout=240)
330
+ if r.returncode == 0:
331
+ break
332
+ except Exception:
333
+ pass
334
+ os.environ["PRIMO_EDGE_ATUALIZADO"] = "1"
335
+ os.execv(sys.executable, [sys.executable] + sys.argv)
320
336
  return {"ok": False, "error": f"a voz falhou na parte {i + 1}: {str(e)[:200]}"}
321
337
  dur = _duracao(mp3)
322
338
  wav = Path(tmp) / f"p{i:02d}.wav"