primocode 10.9.1 → 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.
@@ -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);
@@ -898,6 +908,12 @@ async function variarIcones(plano, roteiro) {
898
908
  const explicitos = [];
899
909
  try { JSON.stringify(roteiro || {}, (k, v) => { if (k === 'icone' && typeof v === 'string') explicitos.push(v); return v; }); } catch { /* sem */ }
900
910
  const troca = {};
911
+ /* VÁRIAS BIBLIOTECAS NO MESMO VÍDEO (13/10: "não uma biblioteca de ícone,
912
+ mas várias, que vai misturar tudo"). Antes o vídeo sorteava UMA coleção
913
+ e trocava todos os ícones para ela. Agora cada ícone tenta uma coleção,
914
+ em rodízio a partir da sorteada; sem o desenho nela, a próxima. */
915
+ const colecoes = [colecao, ...assets.COLECOES.map((c) => c[0]).filter((c) => c !== colecao)];
916
+ let vez = 0;
901
917
  for (const nome of variedade.iconesTrocaveis(plano, explicitos)) {
902
918
  /* Primeiro O ÍCONE: o conceito escolhe entre as formas dele
903
919
  (edicao.ALTERNATIVAS) pelos pontos da variedade — o que os últimos
@@ -914,9 +930,12 @@ async function variarIcones(plano, roteiro) {
914
930
  variedade.marcar('icone-nome', escolhido);
915
931
  }
916
932
  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 */ }
933
+ for (let k = 0; k < colecoes.length; k++) {
934
+ const c = colecoes[(vez + k) % colecoes.length];
935
+ if (c === 'fluent-3d') break;
936
+ try { const r = await assets.icone(c + ':' + escolhido); if (r && r.ok) { novo = c + ':' + escolhido; break; } } catch { /* a próxima */ }
919
937
  }
938
+ vez++;
920
939
  if (novo !== nome) troca[nome] = novo;
921
940
  }
922
941
  const n = variedade.trocarIcones(plano, troca);
@@ -1254,6 +1273,15 @@ async function sonsReais(p, video) {
1254
1273
  }
1255
1274
 
1256
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
+ }
1257
1285
 
1258
1286
  /** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
1259
1287
  /* A ASSINATURA (assinatura.js): o vídeo é medido e conferido contra as
@@ -1300,6 +1328,31 @@ async function prepararRoteiro(p, args) {
1300
1328
  for (const pt of roteiro.partes || []) {
1301
1329
  if (pt.src && !existe(pt.src)) { if (!pt.busca && /^https?:/i.test(pt.src)) pt.busca = pt.titulo || ''; delete pt.src; }
1302
1330
  }
1331
+ /* A CENA QUE A IA ESCREVEU (parte.componente): ela escreve na pasta de
1332
+ trabalho, e o Remotion só lê o que está no projeto. O arquivo é copiado
1333
+ para componentes/ do projeto; se não existe, a parte fica no estilo da
1334
+ casa (e o caderno diz por quê), em vez de o render quebrar. */
1335
+ for (const pt of roteiro.partes || []) {
1336
+ if (!pt || typeof pt.componente !== 'string' || !pt.componente.trim()) continue;
1337
+ const pedido = pt.componente.trim();
1338
+ const achado = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
1339
+ .find((f) => f && fs.existsSync(f) && /\.(jsx|tsx|js)$/i.test(f));
1340
+ if (!achado) { projeto.anotar(p.id, 'plano', `componente ${pedido} não existe: a parte ficou no estilo da casa`); delete pt.componente; continue; }
1341
+ const dentro = path.resolve(p.pasta) + path.sep;
1342
+ if (!achado.startsWith(dentro)) {
1343
+ const destino = path.join(p.pasta, 'componentes', path.basename(achado));
1344
+ fs.mkdirSync(path.dirname(destino), { recursive: true });
1345
+ fs.copyFileSync(achado, destino);
1346
+ pt.componente = path.relative(p.pasta, destino).replace(/\\/g, '/');
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
+ }
1355
+ }
1303
1356
  // As imagens do pedido (o terminal guardou em referencias.imagens) entram sozinhas, uma vez por projeto.
1304
1357
  const jaTem = new Set((p.referencias || []).map((x) => x.de));
1305
1358
  const novas = referencias.imagens().filter((f) => !jaTem.has(f));
@@ -1327,12 +1380,6 @@ async function prepararRoteiro(p, args) {
1327
1380
  const semImagem = referencias.semImagem(ref);
1328
1381
  let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
1329
1382
  if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
1330
- /* AS FONTES DA CASA (13/10, o primocode-ele-constroi.mp4 que o Joel aprovou):
1331
- no gerativo, título em Inter e texto em JetBrains Mono, com as CORES da
1332
- marca. Só muda se o pedido falar de fonte. */
1333
- if (estilo === 'gerativo' && !/\bfontes?\b|tipografia|\bserifa/i.test(pedidoTxt)) {
1334
- roteiro.marca = { ...(roteiro.marca || {}), fontes: { titulo: 'Inter', texto: 'JetBrains Mono' } };
1335
- }
1336
1383
  /* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
1337
1384
  com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
1338
1385
  o agente tenha escolhido outro estilo pelo nome. */
@@ -1364,10 +1411,27 @@ async function prepararRoteiro(p, args) {
1364
1411
  vídeos (09/10, "vício em cores, músicas, fontes"): por PROJETO, como a semente. */
1365
1412
  // O assunto do vídeo é o rótulo da moldura (quando ela sai): o título do projeto.
1366
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
+ }
1367
1421
  if (estilo === 'gerativo' && !roteiro.escolhasGerativo) {
1368
1422
  roteiro.escolhasGerativo = p.escolhasGerativo || gerativo.escolherVisual(roteiro);
1369
1423
  if (!p.escolhasGerativo) projeto.salvar(p.id, { escolhasGerativo: roteiro.escolhasGerativo });
1370
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;
1371
1435
  if (ref.modo === 'fechado' && !roteiro.busca && referencias.termos().length) roteiro.busca = referencias.termos();
1372
1436
  /* A BIBLIOTECA (biblioteca.js): sem fonte e cor da marca, o híbrido, o
1373
1437
  cinema e o impacto caíam sempre no Inter e no azul e roxo — "os vídeos
@@ -1380,7 +1444,7 @@ async function prepararRoteiro(p, args) {
1380
1444
  // Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
1381
1445
  const ehMotion = LEVES.has(estilo);
1382
1446
  const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
1383
- 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'];
1384
1448
 
1385
1449
  const iconesOk = new Set();
1386
1450
  for (const nome of composicao.iconesDoRoteiro(roteiro)) {
@@ -1890,6 +1954,17 @@ async function prepararRoteiro(p, args) {
1890
1954
  '-af', 'afade=t=in:st=0:d=0.4,loudnorm=I=-18:TP=-2:LRA=11', '-ar', '44100', norm], { timeout: 5 * 60000 });
1891
1955
  if (c.ok && fs.existsSync(norm)) trilha = path.relative(p.pasta, norm).replace(/\\/g, '/');
1892
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(', ')}`);
1893
1968
  /* O herói que o roteiro pediu do Iconify ("mdi:bread-slice"): o desenho é
1894
1969
  baixado aqui (fica em midia/icones); o que não baixar, a batida fica sem. */
1895
1970
  const icones = {};
@@ -2061,10 +2136,63 @@ async function transcrever(p, alvo, ctx) {
2061
2136
 
2062
2137
  // ── estudio_render ───────────────────────────────────────────────────────
2063
2138
 
2139
+ /* O SEGUNDO MOTOR: HYPERFRAMES (heygen-com/hyperframes, Apache-2.0, 13/10:
2140
+ "trabalhar com o Remotion, mas também com o Hyperframes"). A IA escreve a
2141
+ composição em HTML (index.html com data-start/data-duration e GSAP, CSS ou
2142
+ three.js) numa pasta dela; aqui a pasta vai para o projeto, ganha a midia/
2143
+ do projeto (voz, trilha, sons do Estúdio), e o CLI renderiza num Chrome
2144
+ escondido — sem telemetria (HYPERFRAMES_NO_TELEMETRY). Depois, o mesmo do
2145
+ Remotion: volume de Reels, a conferência e o vídeo aberto. */
2146
+ const HYPERFRAMES = 'hyperframes@0.8.154';
2147
+ async function renderHyperframes(p, args, ctx = {}) {
2148
+ const pedido = String(args.html || args.composicao || 'hyperframes').trim();
2149
+ const origem = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
2150
+ .find((d) => d && fs.existsSync(path.join(d, 'index.html')));
2151
+ 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>".` };
2152
+ const destino = path.join(p.pasta, 'hyperframes');
2153
+ if (path.resolve(origem) !== path.resolve(destino)) {
2154
+ fs.rmSync(destino, { recursive: true, force: true });
2155
+ fs.cpSync(origem, destino, { recursive: true });
2156
+ }
2157
+ const midia = path.join(destino, 'midia');
2158
+ if (!fs.existsSync(midia) && fs.existsSync(path.join(p.pasta, 'midia'))) fs.symlinkSync(path.join(p.pasta, 'midia'), midia);
2159
+ const saida = path.join(p.pasta, 'saida', `${projeto.apelidar(p.titulo)}.mp4`);
2160
+ fs.mkdirSync(path.dirname(saida), { recursive: true });
2161
+ (ctx.aoFalar || (() => {}))('renderizando no Hyperframes');
2162
+ const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
2163
+ const r = await new Promise((resolve) => {
2164
+ const { spawn } = require('child_process');
2165
+ const proc = spawn(npx, ['--yes', HYPERFRAMES, 'render', destino, '-o', saida, '--quiet'], { env: { ...process.env, HYPERFRAMES_NO_TELEMETRY: '1', DO_NOT_TRACK: '1' } });
2166
+ let erro = '';
2167
+ proc.stderr.on('data', (d) => { erro += d; });
2168
+ proc.stdout.on('data', (d) => { erro += d; });
2169
+ const relogio = setTimeout(() => { try { proc.kill(); } catch { /* já saiu */ } }, (Number(args.minutos) || 20) * 60000);
2170
+ proc.on('close', (code) => { clearTimeout(relogio); resolve({ ok: code === 0 && fs.existsSync(saida), erro }); });
2171
+ proc.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, erro: e.message }); });
2172
+ });
2173
+ 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.' };
2174
+ const med = await video.medir(saida);
2175
+ if (med.ok && med.temAudio) {
2176
+ const tmp = saida.replace(/\.mp4$/, '.norm.mp4');
2177
+ 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 });
2178
+ if (ln.ok && fs.existsSync(tmp)) fs.renameSync(tmp, saida); else { try { fs.unlinkSync(tmp); } catch { /* não havia */ } }
2179
+ }
2180
+ const bytes = fs.statSync(saida).size;
2181
+ projeto.salvar(p.id, { renders: [...(p.renders || []), { quando: new Date().toISOString(), arquivo: saida, bytes, motor: 'hyperframes' }] });
2182
+ projeto.anotar(p.id, 'render', `hyperframes: ${(bytes / 1048576).toFixed(1)} MB, ${(med.segundos || 0).toFixed(1)}s`);
2183
+ const conf = await conferencia.conferir(saida, { duracao: med.segundos || 0 });
2184
+ if (conf.ok && conf.avisos.length) projeto.anotar(p.id, 'conferencia', conf.avisos.join('; '));
2185
+ if (ctx.abrir) { try { ctx.abrir(saida); } catch { /* o caminho está na resposta */ } }
2186
+ return { ok: true, motor: 'hyperframes', arquivo: saida, bytes, segundos: med.segundos,
2187
+ conferencia: conf.ok ? { som: conf.som, parados: conf.parados, avisos: conf.avisos } : undefined,
2188
+ 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.' : '.') };
2189
+ }
2190
+
2064
2191
  async function ferramentaRender(args, ctx = {}) {
2065
2192
  const p = projetoOuRecente(args.id);
2066
2193
  if (!p) return semProjeto(args.id);
2067
2194
  if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
2195
+ if (String(args.motor || '').toLowerCase() === 'hyperframes' || args.html) return renderHyperframes(p, args, ctx);
2068
2196
  const pl = projeto.lerPlano(args.id);
2069
2197
  if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
2070
2198
 
@@ -2320,7 +2448,7 @@ async function ferramentaSite(args) {
2320
2448
  return r;
2321
2449
  }
2322
2450
 
2323
- module.exports = {
2451
+ module.exports = { proporcaoDoPedido,
2324
2452
  vazamentoNoComeco, variarIcones, objetosDoRoteiro,
2325
2453
  ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
2326
2454
  ferramentaVideo, ferramentaRender, ferramentaSite,
@@ -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
@@ -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
@@ -815,7 +826,8 @@ async function despachar(name, args, ctx) {
815
826
  case 'estudio_projeto':
816
827
  return estudio.ferramentaProjeto(args, ctx);
817
828
  case 'estudio_plano':
818
- return estudio.ferramentaPlano(args);
829
+ // A pasta de trabalho vai junto: o componente que a IA escreveu mora nela (index.js copia para o projeto).
830
+ return estudio.ferramentaPlano({ ...args, __pasta: projectDir });
819
831
  case 'estudio_midia':
820
832
  return estudio.ferramentaMidia(args);
821
833
  case 'estudio_narrar':
@@ -825,7 +837,8 @@ async function despachar(name, args, ctx) {
825
837
  // contexto do pedido atravessa até aqui.
826
838
  return estudio.ferramentaVideo(args, ctx);
827
839
  case 'estudio_render':
828
- return estudio.ferramentaRender(args, {
840
+ // A pasta de trabalho vai junto: a composição do Hyperframes que a IA escreveu mora nela.
841
+ return estudio.ferramentaRender({ ...args, __pasta: projectDir }, {
829
842
  ...ctx,
830
843
  // O vídeo pronto abre para a pessoa assistir. É a única
831
844
  // 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.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/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"