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.
- package/README.md +2 -2
- package/lib/act.js +2 -0
- package/lib/catalogo.js +6 -2
- package/lib/comandos-extra.js +2 -1
- package/lib/entrada.js +18 -0
- package/lib/estudio/cena.js +47 -0
- package/lib/estudio/estilos.js +147 -0
- package/lib/estudio/gerativo.js +104 -6
- package/lib/estudio/index.js +139 -11
- package/lib/estudio/kit/gerativo.jsx +22 -2
- package/lib/estudio/referencias.js +9 -2
- package/lib/tools.js +15 -2
- package/lib/ui.js +14 -1
- package/lib/video/cor.js +104 -0
- package/lib/video/filters.js +3 -0
- package/lib/video/ops.js +31 -0
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +35 -36
- package/studio/edge.py +5 -0
- package/studio/narrar.py +16 -0
package/lib/estudio/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
|
84
|
-
+ '
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/lib/video/cor.js
ADDED
|
@@ -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 };
|
package/lib/video/filters.js
CHANGED
|
@@ -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.
|
|
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"
|