primocode 10.7.1 → 10.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +13 -4
- package/lib/act.js +11 -4
- package/lib/entrada.js +87 -9
- package/lib/estudio/conferencia.js +98 -0
- package/lib/estudio/estilo.js +23 -1
- package/lib/estudio/gerativo.js +74 -16
- package/lib/estudio/index.js +68 -3
- package/lib/estudio/kit/fluido.jsx +5 -0
- package/lib/estudio/kit/gerativo.jsx +9 -4
- package/lib/estudio/kit/mundo.jsx +9 -0
- package/lib/estudio/kit/voxel.jsx +35 -8
- package/lib/estudio/marca.js +106 -1
- package/lib/estudio/personagem.js +4 -2
- package/lib/estudio/referencias.js +45 -3
- package/lib/pre-leitura.js +2 -1
- package/lib/tools.js +9 -2
- package/lib/travado.js +23 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.
|
|
1
|
+
# PrimoCode v10.9.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.9.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -253,16 +253,20 @@ async function comandoApi(rest) {
|
|
|
253
253
|
|
|
254
254
|
const apelido = { claude: 'claude-code', gpt: 'chatgpt', openai: 'chatgpt' }[arg.toLowerCase()];
|
|
255
255
|
let id = apelido || ids.find((k) => k === arg.toLowerCase() || P[k].nome.toLowerCase().startsWith(arg.toLowerCase()) && arg);
|
|
256
|
+
// A lista numerada, a pergunta e a resposta somem depois da escolha: fica só o resultado (lib/entrada.js, marcar).
|
|
257
|
+
let marca = null;
|
|
258
|
+
const tirarLista = () => { if (marca && rl && rl.voltarPara) rl.voltarPara(marca); marca = null; };
|
|
256
259
|
if (!id) {
|
|
260
|
+
if (rl && rl.marcar) marca = rl.marcar();
|
|
257
261
|
console.log('');
|
|
258
262
|
console.log(' ' + c.bold('Com qual IA você quer rodar?') + c.muted(' a sua chave ou a sua assinatura, sem o limite do servidor'));
|
|
259
263
|
if (atual) console.log(' ' + c.muted('agora: ') + c.white(atual.cli ? atual.nome : `${atual.nome} · ${atual.modelo} · chave ${provedorLocal.mascarar(atual.chave)}`));
|
|
260
264
|
ids.forEach((k, i) => console.log(` ${c.brand(String(i + 1))} ${c.white(P[k].nome.padEnd(24))}${c.dim(P[k].dica)}`));
|
|
261
265
|
console.log(` ${c.brand('0')} ${c.white('Servidor do PrimoCode'.padEnd(24))}${c.dim('sem chave, com o limite do plano')}`);
|
|
262
266
|
const escolha = String(await ask(` ${c.brand('?')} Número: `)).trim();
|
|
263
|
-
if (escolha === '0') return comandoApi('off');
|
|
267
|
+
if (escolha === '0') { tirarLista(); return comandoApi('off'); }
|
|
264
268
|
id = ids[Number(escolha) - 1];
|
|
265
|
-
if (!id) { console.log(c.muted(' Nada mudou.')); return; }
|
|
269
|
+
if (!id) { tirarLista(); console.log(c.muted(' Nada mudou.')); return; }
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
const p = P[id];
|
|
@@ -270,22 +274,25 @@ async function comandoApi(rest) {
|
|
|
270
274
|
// e logada. Quem pensa é ela; quem executa é o PrimoCode.
|
|
271
275
|
if (p.cli) {
|
|
272
276
|
if (!require('../lib/local/motor-cli').instalado(id)) {
|
|
277
|
+
tirarLista();
|
|
273
278
|
console.log(c.warn(` O ${p.nome} não está instalado nesta máquina.`) + c.muted(` Veja ${p.onde}`));
|
|
274
279
|
return;
|
|
275
280
|
}
|
|
276
281
|
const salvo = provedorLocal.salvar({ provedor: id, modelo: p.modelos[0] });
|
|
282
|
+
tirarLista();
|
|
277
283
|
console.log(c.ok(` Pronto: ${salvo.nome}`) + c.muted(' · usa a sua assinatura, sem chave'));
|
|
278
284
|
console.log(c.dim(` O ${p.nome} decide, o PrimoCode executa. /api off volta ao servidor`));
|
|
279
285
|
return;
|
|
280
286
|
}
|
|
281
287
|
console.log(c.muted(` Pegue a chave em `) + c.white(p.onde));
|
|
282
288
|
const chave = String(await askSecret(` ${c.brand('?')} Cole a chave do ${p.nome} (não aparece na tela): `)).trim();
|
|
283
|
-
if (!chave) { console.log(c.muted(' Nada mudou.')); return; }
|
|
289
|
+
if (!chave) { tirarLista(); console.log(c.muted(' Nada mudou.')); return; }
|
|
284
290
|
const sp = new Spinner('conferindo a chave').start();
|
|
285
291
|
const r = await provedorLocal.conferir(id, chave);
|
|
286
292
|
sp.stop();
|
|
287
|
-
if (!r.ok) { console.log(c.warn(` ${r.error}`)); return; }
|
|
293
|
+
if (!r.ok) { tirarLista(); console.log(c.warn(` ${r.error}`)); return; }
|
|
288
294
|
const salvo = provedorLocal.salvar({ provedor: id, chave, modelo: r.modelo });
|
|
295
|
+
tirarLista();
|
|
289
296
|
console.log(c.ok(` Pronto: ${salvo.nome} · ${salvo.modelo}`) + c.muted(` · chave ${provedorLocal.mascarar(chave)}`));
|
|
290
297
|
console.log(c.dim(' Agora os pedidos vão direto daqui, com a sua cota. /api modelo <nome> troca o modelo · /api off volta ao servidor'));
|
|
291
298
|
}
|
|
@@ -1279,6 +1286,8 @@ async function enviarPedido(userText) {
|
|
|
1279
1286
|
// ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
|
|
1280
1287
|
instrucao: [i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
|
|
1281
1288
|
i === 0 ? require('../lib/estudio/referencias.js').instrucao() : '',
|
|
1289
|
+
// As imagens do pedido (arrastadas ou coladas): guardadas para o Estúdio importar, e ditas ao agente.
|
|
1290
|
+
i === 0 ? (() => { const R = require('../lib/estudio/referencias.js'); return R.instrucaoDasImagens(R.guardarImagens(R.imagensNoTexto(userText))); })() : '',
|
|
1282
1291
|
i === 0 ? plan.notaOutraPasta || '' : ''].filter(Boolean).join('\n\n'),
|
|
1283
1292
|
});
|
|
1284
1293
|
|
package/lib/act.js
CHANGED
|
@@ -686,6 +686,13 @@ const RITMO_AGENTE = '[ritmo]\n'
|
|
|
686
686
|
entrar já lido (ver lib/pre-leitura.js). */
|
|
687
687
|
function buildMemoryContext(projectDir, projectName, pedido = '', { memoriaDaPasta = true } = {}) {
|
|
688
688
|
const parts = [];
|
|
689
|
+
/* NO SERVIDOR O CONTEXTO É CURTO (13/10: "continua lendo e listando
|
|
690
|
+
coisas desnecessárias"). O plano grátis guarda ~1.900 tokens de
|
|
691
|
+
conversa; 3.400 de contexto fixo empurravam para fora o que o modelo
|
|
692
|
+
acabou de ler, e ele lia de novo. Com provedor próprio (/api) nada muda. */
|
|
693
|
+
let proprio = false;
|
|
694
|
+
try { proprio = !!require('./local/provedor.js').atual(); } catch { /* sem provedor próprio */ }
|
|
695
|
+
const curto = (t) => (proprio || t.length <= 1500 ? t : '…' + t.slice(-1500));
|
|
689
696
|
// As instruções vêm primeiro: são regra de quem manda no projeto, e a
|
|
690
697
|
// memória é o que o agente anotou. Entram aqui (e não no bin) para valer
|
|
691
698
|
// igual no terminal, no modo voz e em cada passo de um plano.
|
|
@@ -697,14 +704,14 @@ function buildMemoryContext(projectDir, projectName, pedido = '', { memoriaDaPas
|
|
|
697
704
|
bin), ela não vem de novo. */
|
|
698
705
|
if (memoriaDaPasta) {
|
|
699
706
|
const daPasta = require('./agents-md.js').ler(projectDir);
|
|
700
|
-
if (daPasta) parts.push(`[AGENTS.md — memória desta pasta: de onde a conversa parou]\n${daPasta}`);
|
|
707
|
+
if (daPasta) parts.push(`[AGENTS.md — memória desta pasta: de onde a conversa parou]\n${curto(daPasta)}`);
|
|
701
708
|
}
|
|
702
709
|
const geral = cfg.readGeneralMemory();
|
|
703
|
-
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
710
|
+
if (geral.trim()) parts.push(`[memory-geral.md]\n${curto(geral)}`);
|
|
704
711
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
705
|
-
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
|
|
712
|
+
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${curto(projeto)}`);
|
|
706
713
|
const arquivos = arquivosDoProjeto(projectDir);
|
|
707
|
-
const mapa = mapaDoProjeto(projectDir, 80, arquivos);
|
|
714
|
+
const mapa = mapaDoProjeto(projectDir, proprio ? 80 : 30, arquivos);
|
|
708
715
|
if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
|
|
709
716
|
const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
|
|
710
717
|
if (lidos) parts.push(lidos);
|
package/lib/entrada.js
CHANGED
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
|
|
38
38
|
'use strict';
|
|
39
39
|
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const os = require('os');
|
|
42
|
+
const path = require('path');
|
|
40
43
|
const readline = require('readline');
|
|
41
44
|
const { EventEmitter } = require('events');
|
|
42
45
|
const { c, visibleWidth, truncate } = require('./ui.js');
|
|
@@ -46,7 +49,51 @@ const stripAnsiLocal = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '');
|
|
|
46
49
|
// ── O desenho ────────────────────────────────────────────────────────────
|
|
47
50
|
|
|
48
51
|
/** A etiqueta de colagem em destaque (fininha, na cor da marca), sem mudar a largura. */
|
|
49
|
-
const RE_ETIQUETA = /\[Colado #\d+
|
|
52
|
+
const RE_ETIQUETA = /\[(?:Colado|Imagem) #\d+[^\]]*\]/g;
|
|
53
|
+
|
|
54
|
+
/* A IMAGEM ARRASTADA (Joel, 13/10: "#imagem1 #imagem2, igual fica aqui").
|
|
55
|
+
Arrastar arquivo para o terminal é uma colagem do CAMINHO, e cada terminal
|
|
56
|
+
escreve de um jeito: o do macOS escapa o espaço e põe um no fim
|
|
57
|
+
("Captura\ de\ Tela.png "), o iTerm põe entre aspas, outros mandam
|
|
58
|
+
file://. Devolve os caminhos quando TODOS são imagens que existem; senão
|
|
59
|
+
null, e a colagem segue como texto. */
|
|
60
|
+
const RE_IMAGEM = /\.(png|jpe?g|gif|webp|heic|bmp|tiff?|svg)$/i;
|
|
61
|
+
function imagensArrastadas(texto) {
|
|
62
|
+
const t = String(texto || '').trim();
|
|
63
|
+
if (!t || t.includes('\n')) return null;
|
|
64
|
+
const pedacos = [];
|
|
65
|
+
let cur = '', aspas = null;
|
|
66
|
+
for (let i = 0; i < t.length; i++) {
|
|
67
|
+
const ch = t[i];
|
|
68
|
+
if (aspas) { if (ch === aspas) aspas = null; else cur += ch; continue; }
|
|
69
|
+
if (ch === "'" || ch === '"') { aspas = ch; continue; }
|
|
70
|
+
if (ch === '\\' && i + 1 < t.length) { cur += t[++i]; continue; }
|
|
71
|
+
if (/\s/.test(ch)) { if (cur) pedacos.push(cur); cur = ''; continue; }
|
|
72
|
+
cur += ch;
|
|
73
|
+
}
|
|
74
|
+
if (aspas) return null;
|
|
75
|
+
if (cur) pedacos.push(cur);
|
|
76
|
+
const arquivos = pedacos.map((p) => (p.startsWith('file://') ? decodeURI(p.slice(7)) : p.replace(/^~(?=\/)/, os.homedir())));
|
|
77
|
+
const existe = (f) => { try { return fs.statSync(f).isFile(); } catch { return false; } };
|
|
78
|
+
if (!arquivos.length || !arquivos.every((f) => path.isAbsolute(f) && RE_IMAGEM.test(f) && existe(f))) return null;
|
|
79
|
+
return arquivos;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* O QUE A ETIQUETA GUARDAVA, embaixo do pedido já enviado: o nome da imagem,
|
|
83
|
+
ou as primeiras linhas do texto colado e quanto falta. Na caixa a etiqueta
|
|
84
|
+
poupa espaço; depois de mandar, quem rola a conversa quer ver o que foi. */
|
|
85
|
+
function guardadosNaTela(guardados, largura) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const [et, conteudo] of guardados || []) {
|
|
88
|
+
if (et.startsWith('[Imagem')) { out.push(`⎿ ${et} ${path.basename(conteudo)}`); continue; }
|
|
89
|
+
const linhas = String(conteudo).split('\n');
|
|
90
|
+
const mostra = linhas.slice(0, 3).map((l) => truncate(l, Math.max(10, largura - et.length - 6)));
|
|
91
|
+
out.push(`⎿ ${et} ${mostra[0]}`);
|
|
92
|
+
for (const l of mostra.slice(1)) out.push(` ${' '.repeat(et.length)} ${l}`);
|
|
93
|
+
if (linhas.length > 3) out.push(` ${' '.repeat(et.length)} … +${linhas.length - 3} linhas`);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
50
97
|
function pintarEtiquetas(s) { return String(s).replace(RE_ETIQUETA, (m) => c.brand(m) + '\x1b[38;5;254m'); }
|
|
51
98
|
|
|
52
99
|
/* As cores do painel, no cubo de 256 cores. O fundo é um degrau acima do
|
|
@@ -248,12 +295,14 @@ function montar(e) {
|
|
|
248
295
|
}
|
|
249
296
|
|
|
250
297
|
/** O pedido que foi mandado, como fica no histórico da tela: um painel. */
|
|
251
|
-
function pedidoNaTela(texto, colunas) {
|
|
298
|
+
function pedidoNaTela(texto, colunas, guardados = []) {
|
|
252
299
|
const largura = Math.max(24, Math.min(colunas || 80, 200)) - 1;
|
|
253
300
|
const barra = `${COR.barra}┃${COR.reset}`;
|
|
254
301
|
const linhas = quebrar(texto, Math.max(8, largura - PREFIXO))
|
|
255
302
|
.map((l) => painel(barra + ' ' + pintarEtiquetas(c.white(l.texto)), largura));
|
|
256
|
-
|
|
303
|
+
const extras = guardadosNaTela(guardados, largura - PREFIXO)
|
|
304
|
+
.map((l) => painel(barra + ' ' + c.dim(truncate(l, largura - PREFIXO)), largura));
|
|
305
|
+
return [painel(barra, largura), ...linhas, ...extras, painel(barra, largura)].join('\n');
|
|
257
306
|
}
|
|
258
307
|
|
|
259
308
|
// ── A caixa viva ─────────────────────────────────────────────────────────
|
|
@@ -403,6 +452,21 @@ class Entrada extends EventEmitter {
|
|
|
403
452
|
if (this.ativo && !this.fechado) this._desenhar();
|
|
404
453
|
}
|
|
405
454
|
|
|
455
|
+
/* UMA ESCOLHA QUE SOME depois de respondida (o /api, 13/10: "só apareça
|
|
456
|
+
pra qual modelo trocou, aquela lista enorme os números somem").
|
|
457
|
+
marcar() antes de listar, voltarPara(marca) depois: a lista, a pergunta
|
|
458
|
+
e a resposta saem da conversa, e fica só o que vier depois. Só na tela
|
|
459
|
+
cheia; no modo linha o terminal já rolou e o texto fica. */
|
|
460
|
+
marcar() { return this.cheia ? { n: this.conversa.length, ultima: this.conversa[this.conversa.length - 1] } : null; }
|
|
461
|
+
voltarPara(marca) {
|
|
462
|
+
if (!marca || !this.cheia || this.conversa.length < marca.n) return;
|
|
463
|
+
this.conversa.length = marca.n;
|
|
464
|
+
if (marca.n) this.conversa[marca.n - 1] = marca.ultima;
|
|
465
|
+
this.rolagem = 0;
|
|
466
|
+
this._quadroAnterior = null;
|
|
467
|
+
this._pintar();
|
|
468
|
+
}
|
|
469
|
+
|
|
406
470
|
/** Um comando com tela própria desenha aqui (na tela cheia). null fecha. */
|
|
407
471
|
painel(texto) {
|
|
408
472
|
this.painelTexto = texto == null ? null : String(texto).replace(/\n+$/, '');
|
|
@@ -566,7 +630,7 @@ class Entrada extends EventEmitter {
|
|
|
566
630
|
this._mudou();
|
|
567
631
|
}
|
|
568
632
|
|
|
569
|
-
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this.colados = new Map(); this._mudou(); }
|
|
633
|
+
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this.colados = new Map(); this.nImagem = 0; this._mudou(); }
|
|
570
634
|
|
|
571
635
|
/* COLAR VIRA ETIQUETA, como no Claude Code: texto grande colado (várias
|
|
572
636
|
linhas, ou mais de 300 caracteres) entra na caixa como
|
|
@@ -576,10 +640,22 @@ class Entrada extends EventEmitter {
|
|
|
576
640
|
colar(texto) {
|
|
577
641
|
const t = String(texto || '');
|
|
578
642
|
if (!t) return;
|
|
643
|
+
if (!this.colados) this.colados = new Map();
|
|
644
|
+
/* Imagem arrastada: uma etiqueta por arquivo, e no Enter volta o caminho.
|
|
645
|
+
O contador não reaproveita número: apagar a #1 não faz a próxima repetir a #2. */
|
|
646
|
+
const imagens = imagensArrastadas(t);
|
|
647
|
+
if (imagens) {
|
|
648
|
+
const ets = imagens.map((f) => {
|
|
649
|
+
this.nImagem = (this.nImagem || 0) + 1;
|
|
650
|
+
const et = `[Imagem #${this.nImagem}]`;
|
|
651
|
+
this.colados.set(et, f);
|
|
652
|
+
return et;
|
|
653
|
+
});
|
|
654
|
+
return this.write(ets.join(' ') + (/\s$/.test(t) ? ' ' : ''));
|
|
655
|
+
}
|
|
579
656
|
const linhas = t.split('\n').length;
|
|
580
657
|
if (linhas < 3 && t.length <= 300) return this.write(t);
|
|
581
|
-
|
|
582
|
-
const n = this.colados.size + 1;
|
|
658
|
+
const n = [...this.colados.keys()].filter((k) => k.startsWith('[Colado')).length + 1;
|
|
583
659
|
const etiqueta = `[Colado #${n} · ${linhas > 1 ? `${linhas} linhas` : `${t.length} caracteres`}]`;
|
|
584
660
|
this.colados.set(etiqueta, t);
|
|
585
661
|
this.write(etiqueta);
|
|
@@ -890,7 +966,9 @@ class Entrada extends EventEmitter {
|
|
|
890
966
|
if (this.menu) return this._escolherDoMenu();
|
|
891
967
|
const visto = this.line;
|
|
892
968
|
const texto = this.expandir(visto);
|
|
969
|
+
const guardados = [...(this.colados || [])].filter(([et]) => visto.includes(et));
|
|
893
970
|
this.colados = new Map();
|
|
971
|
+
this.nImagem = 0;
|
|
894
972
|
const oculto = this.oculto;
|
|
895
973
|
this.oculto = false;
|
|
896
974
|
this.line = '';
|
|
@@ -921,8 +999,8 @@ class Entrada extends EventEmitter {
|
|
|
921
999
|
const temAlgo = this.conversa.some((l) => /\S/.test(l));
|
|
922
1000
|
const ultima = this.conversa[this.conversa.length - 1] || '';
|
|
923
1001
|
const antes = !temAlgo ? '' : /\S/.test(ultima) ? '\n\n' : '\n';
|
|
924
|
-
tela.acrescentar(this.conversa, antes + pedidoNaTela(visto, mainW - 1) + '\n\n');
|
|
925
|
-
} else this.escreverCru(pedidoNaTela(visto, process.stdout.columns || 80) + '\n');
|
|
1002
|
+
tela.acrescentar(this.conversa, antes + pedidoNaTela(visto, mainW - 1, guardados) + '\n\n');
|
|
1003
|
+
} else this.escreverCru(pedidoNaTela(visto, process.stdout.columns || 80, guardados) + '\n');
|
|
926
1004
|
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
|
927
1005
|
}
|
|
928
1006
|
this._desenhar();
|
|
@@ -932,4 +1010,4 @@ class Entrada extends EventEmitter {
|
|
|
932
1010
|
|
|
933
1011
|
function criar(op) { return new Entrada(op).iniciar(); }
|
|
934
1012
|
|
|
935
|
-
module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas, painel, pedidoNaTela, PREFIXO, COR };
|
|
1013
|
+
module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas, painel, pedidoNaTela, imagensArrastadas, PREFIXO, COR };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conferencia.js — a conferência do vídeo PRONTO: o arquivo, medido, e não
|
|
3
|
+
* o plano.
|
|
4
|
+
*
|
|
5
|
+
* "gostei, ficou bom. mas ainda não. tá quase lá" (Joel, 11/10)
|
|
6
|
+
*
|
|
7
|
+
* O revisor.js olha o plano antes do render. Este olha o .mp4 que saiu, e
|
|
8
|
+
* troca o "quase lá" no olho por número que se confere sem abrir o vídeo
|
|
9
|
+
* (a ideia veio do review.mjs do cth9191/animate, MIT):
|
|
10
|
+
*
|
|
11
|
+
* som volume integrado (LUFS), variação (LRA) e pico verdadeiro, pelo
|
|
12
|
+
* ebur128 do ffmpeg. Alvo: -14 LUFS (±2) e pico até -1 dBTP.
|
|
13
|
+
* parado trecho em que a imagem não muda por 2 s ou mais, pelo
|
|
14
|
+
* freezedetect, com o quadro reduzido a 320 px. Calibrado em 13/10
|
|
15
|
+
* em 5 vídeos: na resolução cheia o ruído do fundo esconde a tela
|
|
16
|
+
* parada; a 160 px o texto fica pequeno demais e a palavra que
|
|
17
|
+
* entra não conta como mudança. A 320 px o teste-10-5-motion-com-
|
|
18
|
+
* imagem dá 6,5 s a 8,3 s ("torrado" parado, a pílula vazia, e
|
|
19
|
+
* para quando "café" entra), e os vídeos 3D não dão nada.
|
|
20
|
+
*
|
|
21
|
+
* Só mede e avisa. Não conserta nada: quem decide se refaz é o agente.
|
|
22
|
+
* As duas leituras (lerSom, lerParados) são puras e testáveis sem vídeo.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const video = require('./video.js');
|
|
28
|
+
|
|
29
|
+
const ALVO = { lufs: -14, folgaLufs: 2, picoMax: -1, paradoMin: 2 };
|
|
30
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
31
|
+
|
|
32
|
+
/* O ebur128 escreve uma linha a cada 100 ms e um resumo no fim. Só o resumo
|
|
33
|
+
vale: a última linha "I:" de antes dele é a média até ali, não do vídeo. */
|
|
34
|
+
function lerSom(texto) {
|
|
35
|
+
const t = String(texto || '');
|
|
36
|
+
const i = t.lastIndexOf('Summary:');
|
|
37
|
+
if (i < 0) return null;
|
|
38
|
+
const resumo = t.slice(i);
|
|
39
|
+
const num = (re) => { const m = resumo.match(re); return m ? Number(m[1]) : null; };
|
|
40
|
+
const lufs = num(/I:\s*(-?[\d.]+|-inf)\s*LUFS/);
|
|
41
|
+
if (lufs == null) return null;
|
|
42
|
+
return { lufs, lra: num(/LRA:\s*(-?[\d.]+)\s*LU\b/), pico: num(/Peak:\s*(-?[\d.]+)\s*dBFS/) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* O freezedetect marca o começo e o fim de cada trecho parado. Um começo
|
|
46
|
+
sem fim é tela parada até o último quadro. Dois trechos colados (o texto
|
|
47
|
+
some e o fundo fica) são, na tela, uma parada só. */
|
|
48
|
+
function lerParados(texto, duracao) {
|
|
49
|
+
const trechos = [];
|
|
50
|
+
const juntar = (de, ate) => {
|
|
51
|
+
const ultimo = trechos[trechos.length - 1];
|
|
52
|
+
if (ultimo && de - ultimo.ate < 0.2) ultimo.ate = r1(ate);
|
|
53
|
+
else trechos.push({ de: r1(de), ate: r1(ate) });
|
|
54
|
+
};
|
|
55
|
+
let de = null;
|
|
56
|
+
for (const linha of String(texto || '').split('\n')) {
|
|
57
|
+
const i = linha.match(/freeze_start:\s*([\d.]+)/);
|
|
58
|
+
if (i) { de = Number(i[1]); continue; }
|
|
59
|
+
const f = linha.match(/freeze_end:\s*([\d.]+)/);
|
|
60
|
+
if (f && de != null) { juntar(de, Number(f[1])); de = null; }
|
|
61
|
+
}
|
|
62
|
+
if (de != null && duracao > de) juntar(de, duracao);
|
|
63
|
+
return trechos;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** As medidas viram avisos. Puro. */
|
|
67
|
+
function julgar({ som, parados = [] }, alvo = ALVO) {
|
|
68
|
+
const avisos = [];
|
|
69
|
+
if (som && Number.isFinite(som.lufs)) {
|
|
70
|
+
if (Math.abs(som.lufs - alvo.lufs) > alvo.folgaLufs) {
|
|
71
|
+
avisos.push(`volume em ${som.lufs} LUFS, longe do alvo de ${alvo.lufs} (${som.lufs < alvo.lufs ? 'baixo demais' : 'alto demais'})`);
|
|
72
|
+
}
|
|
73
|
+
if (som.pico != null && som.pico > alvo.picoMax) avisos.push(`pico de ${som.pico} dBTP: estoura no celular (o máximo é ${alvo.picoMax})`);
|
|
74
|
+
}
|
|
75
|
+
for (const p of parados) {
|
|
76
|
+
const dura = r1(p.ate - p.de);
|
|
77
|
+
if (dura >= alvo.paradoMin) avisos.push(`imagem parada por ${dura} s (de ${p.de} s a ${p.ate} s)`);
|
|
78
|
+
}
|
|
79
|
+
return avisos;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Mede o arquivo numa passada só do ffmpeg e devolve medidas e avisos. */
|
|
83
|
+
async function conferir(arquivo, { duracao = 0 } = {}) {
|
|
84
|
+
const c = video.conferir();
|
|
85
|
+
if (!c.ok) return c;
|
|
86
|
+
const r = await video.rodar(video.ffmpeg(), [
|
|
87
|
+
'-hide_banner', '-nostats', '-i', arquivo,
|
|
88
|
+
'-vf', 'scale=320:-2,freezedetect=n=0.002:d=1.5',
|
|
89
|
+
'-af', 'ebur128=peak=true',
|
|
90
|
+
'-f', 'null', '-',
|
|
91
|
+
], { timeout: 10 * 60000 });
|
|
92
|
+
if (!r.ok) return { ok: false, error: `a conferência não leu o vídeo: ${r.erro.slice(-200)}` };
|
|
93
|
+
const som = lerSom(r.erro);
|
|
94
|
+
const parados = lerParados(r.erro, duracao);
|
|
95
|
+
return { ok: true, som, parados, avisos: julgar({ som, parados }) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { ALVO, lerSom, lerParados, julgar, conferir };
|
package/lib/estudio/estilo.js
CHANGED
|
@@ -76,7 +76,29 @@ function estiloDaVez(aleatorio = Math.random, semImagem = false) {
|
|
|
76
76
|
return variedade.sortear(lista, 'estilo', { aleatorio }) || lista[0];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/* O ESTILO DA CASA (13/10, sobre o primocode-ele-constroi.mp4: "esse vídeo tá
|
|
80
|
+
top! quero os vídeos nesse estilo, todos os vídeos que ele gerar"). Todo
|
|
81
|
+
vídeo é o GERATIVO — tipografia grande e reta, as cores da marca, Inter e
|
|
82
|
+
JetBrains Mono. O estilo que o agente ou o roteiro escolhem não vale mais:
|
|
83
|
+
o rodízio dava um vídeo de cada jeito. Muda só quando a PESSOA pede pelo
|
|
84
|
+
nome, ou pede a animação 3D com personagens (o personagem). */
|
|
85
|
+
// Nome de estilo inequívoco, ou "estilo X". "Vídeo de lançamento" e "cinema" contam o assunto, não o estilo.
|
|
86
|
+
const PEDE_PELO_NOME = /\b(fluid[oa]|vox|explainer|h[ií]brid[oa]|cl[aá]ssico|brand reveal|estilo (?:de )?(?:marca|impacto|cinema|lan[çc]amento|minimalista|fluido|vox))\b/i;
|
|
87
|
+
const CASA = true;
|
|
88
|
+
|
|
79
89
|
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, semImagem = false, aleatorio } = {}) {
|
|
90
|
+
if (CASA) {
|
|
91
|
+
// O personagem 3D é outro produto (bonecos conversando), não um jeito do motion: esse o agente e o roteiro ainda pedem.
|
|
92
|
+
if (PEDE_PERSONAGEM.test(pedido) || normal(agente) === 'personagem' || normal(roteiro) === 'personagem') return 'personagem';
|
|
93
|
+
const nomeado = (String(pedido).match(PEDE_PELO_NOME) || [])[1];
|
|
94
|
+
if (nomeado) {
|
|
95
|
+
const x = nomeado.toLowerCase().replace(/^estilo (?:de )?/, '').replace(/^fluida$/, 'fluido').replace(/^h[ií]brid[oa]$/, 'hibrido')
|
|
96
|
+
.replace(/^brand reveal$/, 'marca').replace(/^minimalista$/, 'fluido').replace(/^explainer$/, 'vox').replace(/^cl[aá]ssico$/, 'classico').replace(/^lan[çc]amento$/, 'lancamento');
|
|
97
|
+
const n = normal(x);
|
|
98
|
+
if (n) return n;
|
|
99
|
+
}
|
|
100
|
+
return 'gerativo';
|
|
101
|
+
}
|
|
80
102
|
const pedidoAgente = normal(agente);
|
|
81
103
|
if (pedidoAgente) return pedidoAgente;
|
|
82
104
|
const doRoteiro = normal(roteiro);
|
|
@@ -101,4 +123,4 @@ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = fa
|
|
|
101
123
|
/** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
|
|
102
124
|
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido', 'personagem']);
|
|
103
125
|
|
|
104
|
-
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM, PEDE_PERSONAGEM };
|
|
126
|
+
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM, PEDE_PERSONAGEM, PEDE_PELO_NOME };
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -238,7 +238,7 @@ function acertosDeHumor(roteiro) {
|
|
|
238
238
|
lê roteiro.escolhasGerativo, e a marca continua mandando por cima. */
|
|
239
239
|
function escolherVisual(roteiro = {}, { aleatorio = Math.random } = {}) {
|
|
240
240
|
const variedade = require('./variedade.js');
|
|
241
|
-
const marca = roteiro.marca
|
|
241
|
+
const marca = require('./marca.js').marcaEmObjeto(roteiro.marca);
|
|
242
242
|
const n = acertosDeHumor(roteiro);
|
|
243
243
|
const max = Math.max(...Object.values(n));
|
|
244
244
|
const humor = variedade.sortear(Object.keys(HUMORES), 'humor-gerativo', { aleatorio, adequacao: (h) => (n[h] + 1) / (max + 1) });
|
|
@@ -637,7 +637,11 @@ function camposDe(marca, humor, s, dita = null, fixa = null) {
|
|
|
637
637
|
if (dita && !ehCor(c.destaque)) { const j = k.indexOf(dita); if (j > 0) k[j] = k[1]; k[1] = dita; }
|
|
638
638
|
// A marca: o fundo dela, o destaque dela e a tinta dela tomam os lugares de mesmo papel.
|
|
639
639
|
const escuro = (x) => luz(x) < 0.35, claro = (x) => luz(x) > 0.7;
|
|
640
|
-
|
|
640
|
+
/* Marca de fundo escuro sem tinta declarada: o claro é o branco frio, não
|
|
641
|
+
o creme do sorteio (o motion do PrimoCode, preto e ciano, ganhava fundo
|
|
642
|
+
#F1EADB na lista e no fim — 13/10). */
|
|
643
|
+
const tinta = ehCor(c.tinta) ? c.tinta : (ehCor(c.fundo) && luz(c.fundo) < 0.2 ? '#F5F5F7' : null);
|
|
644
|
+
for (const cor of [c.fundo, c.destaque, tinta].filter(ehCor)) {
|
|
641
645
|
const lugar = escuro(cor) ? 0 : claro(cor) ? 2 : 1;
|
|
642
646
|
if (!k.includes(cor)) k[lugar] = cor;
|
|
643
647
|
}
|
|
@@ -941,7 +945,7 @@ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], n
|
|
|
941
945
|
/** A partitura, sem áudio nem plano — é o que os testes olham. */
|
|
942
946
|
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icones = {} }) {
|
|
943
947
|
const partes = roteiro.partes || [];
|
|
944
|
-
const marca = roteiro.marca
|
|
948
|
+
const marca = require('./marca.js').marcaEmObjeto(roteiro.marca);
|
|
945
949
|
const W = largura, H = altura, deitado = W >= H;
|
|
946
950
|
const semente = roteiro.semente != null ? hash('semente:' + roteiro.semente)
|
|
947
951
|
: hash(JSON.stringify([partes.map((p) => [p.tipo, p.titulo, p.texto, p.fala, p.valor, p.itens, p.nao, p.sim, p.destaque]), marca.nome || '']));
|
|
@@ -959,8 +963,14 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
959
963
|
eixos para animar); a genérica da marca vira texto, e o título ganha
|
|
960
964
|
uma face da biblioteca do humor. ─────────────────────────────────── */
|
|
961
965
|
const pedida = (marca.fontes && marca.fontes.titulo) || '';
|
|
962
|
-
|
|
963
|
-
|
|
966
|
+
/* O texto da marca; sem ele, a mono da marca (o site do PrimoCode é a
|
|
967
|
+
Instrument Serif nos títulos e a JetBrains Mono no resto, 13/10). */
|
|
968
|
+
const monoMarca = (marca.fontes && marca.fontes.mono) || '';
|
|
969
|
+
const pedidaTexto = (marca.fontes && marca.fontes.texto) || (VARIAVEIS[monoMarca] ? monoMarca : '');
|
|
970
|
+
/* A da marca vale se a biblioteca carrega (e não é cara de IA), mesmo sem
|
|
971
|
+
eixo de peso: sem eixo ela só não anima o peso. Exigir o wght trocava a
|
|
972
|
+
Instrument Serif do site por uma Bodoni sorteada (13/10). */
|
|
973
|
+
const boa = (f) => f && VARIAVEIS[f] && !BANIDAS.includes(f);
|
|
964
974
|
const ou = (salva, sorteada) => salva || sorteada;
|
|
965
975
|
const display = boa(pedida) ? pedida : ou(E.display, s.um(H0.displays));
|
|
966
976
|
const cinetica = eixoDe(display, 'wdth') && eixoDe(display, 'wdth')[0] <= 75 ? display : s.um(H0.cineticas);
|
|
@@ -1129,7 +1139,12 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1129
1139
|
const mx = Math.max(r, g, bb), mn = Math.min(r, g, bb), d = mx - mn; if (!d) return 0;
|
|
1130
1140
|
const hh = mx === r ? ((g - bb) / d) % 6 : mx === g ? (bb - r) / d + 2 : (r - g) / d + 4; return (hh * 60 + 360) % 360; };
|
|
1131
1141
|
const longe = (x) => { const d = Math.abs(matiz(x) - matiz(ident.acento)); return Math.min(d, 360 - d); };
|
|
1132
|
-
|
|
1142
|
+
/* Com marca, a ousada é a de APOIO da marca, ou nenhuma: o amarelo do
|
|
1143
|
+
sistema sorteado entrava de fundo no motion preto e ciano do PrimoCode (13/10). */
|
|
1144
|
+
const mc = marca.cores || {};
|
|
1145
|
+
const daMarca = ehCor(mc.fundo) || ehCor(mc.destaque);
|
|
1146
|
+
const ousada = daMarca ? (ehCor(mc.apoio) && mc.apoio !== ident.acento ? mc.apoio : null)
|
|
1147
|
+
: [campos[3], campos[4]].filter(ehCor).sort((x, y) => longe(y) - longe(x))[0];
|
|
1133
1148
|
const fortes = [ident.acento, ousada].filter((x, i, a) => ehCor(x) && a.indexOf(x) === i);
|
|
1134
1149
|
const nCor = batidas.length >= 6 ? Math.max(2, Math.round(batidas.length / 3.5)) : batidas.length >= 4 ? 1 : 0;
|
|
1135
1150
|
const candCor = batidas.map((b, i) => i).filter((i) => i > 0 && !['marca', 'produto'].includes(batidas[i].comp));
|
|
@@ -1453,6 +1468,36 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1453
1468
|
fundo.ambiente = AMBIENTES.includes(roteiro.ambiente) ? roteiro.ambiente : E.ambiente || 'limpo';
|
|
1454
1469
|
fundo.acento = ACENTOS_JEITO.includes(roteiro.acento) ? roteiro.acento : E.acento || 'traco';
|
|
1455
1470
|
|
|
1471
|
+
/* SEM LETRA TORTA (13/10: "os motions saem muito tortos"). A letra que
|
|
1472
|
+
vira outra antes de assentar (decodifica, flap), a que chega voando e
|
|
1473
|
+
girando (monta), a que entra inclinada (onda) e a palavra que explode em
|
|
1474
|
+
letras rodando deixavam o "SGERE", o "a o" solto e o quadro vazio de
|
|
1475
|
+
letras fantasmas. Os sorteios acontecem igual (a semente não muda o
|
|
1476
|
+
resto); no fim, cada gesto vira o seu par limpo. */
|
|
1477
|
+
/* O substituto é um limpo que não aparece nas 3 batidas de cada lado: a
|
|
1478
|
+
passagem e o gesto da tese não voltam antes de 3 batidas. */
|
|
1479
|
+
const TORTOS = ['decodifica', 'flap', 'onda', 'monta'];
|
|
1480
|
+
const GESTOS_LIMPOS = ['mascara', 'desfoque', 'tracking', 'pontos', 'contorno', 'carimbo'];
|
|
1481
|
+
const SAIDAS_LIMPAS = ['evapora', 'desliza', 'comprime', 'pontos', 'atravessa'];
|
|
1482
|
+
const vizinhas = (i) => batidas.filter((_, j) => j !== i && Math.abs(j - i) <= 3);
|
|
1483
|
+
const principal = (b) => (b.linhas || []).find((l) => (l.papel === 'tese' || l.papel === 'numero') && !l.marca);
|
|
1484
|
+
const comGesto = batidas.filter(principal);
|
|
1485
|
+
batidas.forEach((b, i) => {
|
|
1486
|
+
if (b.saida === 'explode' || b.saida === 'chicote') {
|
|
1487
|
+
const perto = vizinhas(i).map((x) => x.saida);
|
|
1488
|
+
b.saida = SAIDAS_LIMPAS.find((x) => !perto.includes(x)) || 'corte';
|
|
1489
|
+
if (batidas[i + 1]) batidas[i + 1].entrada = b.saida;
|
|
1490
|
+
}
|
|
1491
|
+
const tortas = (b.linhas || []).filter((l) => TORTOS.includes(l.comportamento));
|
|
1492
|
+
if (tortas.length) {
|
|
1493
|
+
// A janela conta só as batidas com linha principal (tese ou número, fora a marca), como quem assiste vê.
|
|
1494
|
+
const k = comGesto.indexOf(b);
|
|
1495
|
+
const perto = k < 0 ? [] : comGesto.filter((_, j) => j !== k && Math.abs(j - k) <= 3).map((x) => principal(x).comportamento);
|
|
1496
|
+
const limpo = GESTOS_LIMPOS.find((x) => !perto.includes(x)) || 'mascara';
|
|
1497
|
+
for (const l of tortas) l.comportamento = limpo;
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1456
1501
|
return {
|
|
1457
1502
|
versao: 1, semente, fps: FPS, humor,
|
|
1458
1503
|
musica: { genero: ou(E.genero, s.um(H0.generos)), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
|
|
@@ -1480,14 +1525,16 @@ function molduraDe(roteiro, E, { W, H, deitado, marca, secoes }) {
|
|
|
1480
1525
|
};
|
|
1481
1526
|
}
|
|
1482
1527
|
|
|
1528
|
+
/* O eixo de peso da fonte; a estática (a Instrument Serif da marca) é só 400. */
|
|
1529
|
+
const pesoDe = (fonte) => (VARIAVEIS[fonte] && VARIAVEIS[fonte].eixos.wght) || [400, 400];
|
|
1530
|
+
|
|
1483
1531
|
// ── as composições: de conteúdo + contexto, uma batida ─────────────────
|
|
1484
1532
|
function compor(comp, br, ctx) {
|
|
1485
1533
|
const { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes } = ctx;
|
|
1486
1534
|
const c = br.conteudo;
|
|
1487
1535
|
const palavras = ctx.palavras || [];
|
|
1488
1536
|
const b = { id: 'b' + Math.round(br.de * 1000), parte: br.parte, de: r3(br.de), ate: r3(br.ate), comp, origem: c.tipo === 'falado' ? 'falado' : 'conteudo', linhas: [], acento: [] };
|
|
1489
|
-
const
|
|
1490
|
-
const wMax = eixosDisplay.wght[1];
|
|
1537
|
+
const wMax = pesoDe(fontes.display)[1];
|
|
1491
1538
|
const wTese = Math.min(wMax, s.um([800, 900, 900, 1000]));
|
|
1492
1539
|
const destaqueTk = tokens(c.destaque || '');
|
|
1493
1540
|
const ehTese = (w) => destaqueTk.length && tokens(w).some((k) => destaqueTk.includes(k));
|
|
@@ -1516,13 +1563,21 @@ function compor(comp, br, ctx) {
|
|
|
1516
1563
|
};
|
|
1517
1564
|
const vert = (opcoes) => s.um(opcoes);
|
|
1518
1565
|
const tese = (l) => { l.papel = 'tese'; return l; };
|
|
1566
|
+
/* O ponto do acento ENTRA NO LUGAR do ponto final: com os dois, saía
|
|
1567
|
+
"Uma ideia.." (13/10). */
|
|
1568
|
+
const pontoFinal = (l) => {
|
|
1569
|
+
l.texto = l.texto.replace(/\.$/, '');
|
|
1570
|
+
const u = l.palavras[l.palavras.length - 1];
|
|
1571
|
+
if (u) u.texto = u.texto.replace(/\.$/, '');
|
|
1572
|
+
b.acento.push({ forma: 'ponto', alvo: { linha: l.id, fim: true } });
|
|
1573
|
+
};
|
|
1519
1574
|
|
|
1520
1575
|
switch (comp) {
|
|
1521
1576
|
case 'gigante': {
|
|
1522
1577
|
const txt = c.titulo || c.nome || c.sim || c.valor || c.texto || '';
|
|
1523
1578
|
const maiusc = txt.length <= 16 && s.chance(0.65);
|
|
1524
1579
|
const fonte = s.chance(0.5) ? fontes.cinetica : fontes.display;
|
|
1525
|
-
const eixos = { wght: Math.min(
|
|
1580
|
+
const eixos = { wght: Math.min(pesoDe(fonte)[1], wTese) };
|
|
1526
1581
|
const op = { fonte, wght: eixos.wght, maiusc };
|
|
1527
1582
|
// A frase de 3+ palavras pode ir em 3 linhas no 16:9: cresce na altura em vez de encolher na largura.
|
|
1528
1583
|
const nMax = deitado ? Math.min(3, Math.max(2, txt.split(/\s+/).length)) : Math.min(4, txt.split(/\s+/).length);
|
|
@@ -1543,7 +1598,7 @@ function compor(comp, br, ctx) {
|
|
|
1543
1598
|
plano inteiro (12/10, o estilo padrão quebrava). Sem linha, sem acento. */
|
|
1544
1599
|
if (!ultima) break;
|
|
1545
1600
|
if (c.riscado) b.acento.push({ forma: 'risco', alvo: { linha: ultima.id } });
|
|
1546
|
-
else if (/[.!]$/.test(txt) || s.chance(0.4))
|
|
1601
|
+
else if (/[.!]$/.test(txt) || s.chance(0.4)) pontoFinal(ultima);
|
|
1547
1602
|
else {
|
|
1548
1603
|
const lt = b.linhas.find((l) => l.papel === 'tese' && l.palavras.some((p) => p.papel === 'tese')) || ultima;
|
|
1549
1604
|
const pi = Math.max(0, lt.palavras.findIndex((p) => p.papel === 'tese'));
|
|
@@ -1608,13 +1663,13 @@ function compor(comp, br, ctx) {
|
|
|
1608
1663
|
for (const t of fit.linhas) { const l = linha(t, 'apoio', { tamanho: fit.tamanho, eixos: { wght: 560 } }); misturarPesos(l, { maiorTese: 1.14 }); }
|
|
1609
1664
|
const lt = b.linhas.find((l) => l.palavras.some((p) => p.papel === 'tese'));
|
|
1610
1665
|
if (lt) { tese(lt); b.acento.push({ forma: s.chance(0.6) ? 'sublinha' : 'anel', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') } }); }
|
|
1611
|
-
else
|
|
1666
|
+
else pontoFinal(b.linhas[b.linhas.length - 1]);
|
|
1612
1667
|
break;
|
|
1613
1668
|
}
|
|
1614
1669
|
case 'contador': {
|
|
1615
1670
|
const n = numeroDe(c.valor);
|
|
1616
1671
|
const fonte = s.chance(0.6) ? fontes.cinetica : fontes.display;
|
|
1617
|
-
const eixos = { wght: Math.min(
|
|
1672
|
+
const eixos = { wght: Math.min(pesoDe(fonte)[1], wTese) };
|
|
1618
1673
|
const alinha = 'centro';
|
|
1619
1674
|
const lg = deitado ? W * 0.62 : larguraUtil;
|
|
1620
1675
|
const tam = tamanhoQueCabe(c.valor, lg * 0.82, (deitado ? 0.36 : 0.17) * H, { fonte, wght: eixos.wght });
|
|
@@ -1653,8 +1708,11 @@ function compor(comp, br, ctx) {
|
|
|
1653
1708
|
case 'divisao': {
|
|
1654
1709
|
const lado = deitado ? (larguraUtil - M * 0.6) / 2 : larguraUtil;
|
|
1655
1710
|
const op = { fonte: fontes.display, wght: wTese };
|
|
1656
|
-
|
|
1657
|
-
|
|
1711
|
+
/* Teto mais alto (13/10): com 13% e 17% da altura, "sugere / constrói"
|
|
1712
|
+
ficava miúdo no canto, metade do tamanho do gigante ao lado. A
|
|
1713
|
+
largura da metade é que segura a frase longa. */
|
|
1714
|
+
const fN = caberEmLinhas(c.nao, lado, deitado ? 3 : 2, (deitado ? 0.2 : 0.11) * H, { ...op, wght: 400 });
|
|
1715
|
+
const fS = caberEmLinhas(c.sim, lado, deitado ? 3 : 3, (deitado ? 0.26 : 0.14) * H, op);
|
|
1658
1716
|
if (deitado) {
|
|
1659
1717
|
b.bloco = { x: M, y: H * 0.5, alinha: 'esq', vert: 'meio', largura: lado, gap: 0.1, colunas: [M, M + lado + M * 0.6] };
|
|
1660
1718
|
} else {
|
|
@@ -1688,7 +1746,7 @@ function compor(comp, br, ctx) {
|
|
|
1688
1746
|
const lg = deitado ? W * 0.7 : larguraUtil * 0.9;
|
|
1689
1747
|
const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.3 : 0.16) * H, { fonte, wght: 800 });
|
|
1690
1748
|
b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
|
|
1691
|
-
linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800,
|
|
1749
|
+
linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, pesoDe(fonte)[1]) }, comportamento: 'contorno' }).marca = true;
|
|
1692
1750
|
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamRotulo, tracking: 0.2, maiusc: true, eixos: { wght: 400 } });
|
|
1693
1751
|
b.acento.push({ forma: 'ponto', alvo: { linha: b.linhas[0].id, antes: true } });
|
|
1694
1752
|
break;
|
|
@@ -1699,7 +1757,7 @@ function compor(comp, br, ctx) {
|
|
|
1699
1757
|
const maiusc = true;
|
|
1700
1758
|
const tam = tamanhoQueCabe(txt, larguraUtil, (deitado ? 0.26 : 0.13) * H, { fonte, wght: 900, maiusc });
|
|
1701
1759
|
b.bloco = { x: W / 2, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: larguraUtil, gap: 0.1 };
|
|
1702
|
-
linha(txt, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(900,
|
|
1760
|
+
linha(txt, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(900, pesoDe(fonte)[1]) }, maiusc });
|
|
1703
1761
|
b.textura = { texto: (maiusc ? txt.toUpperCase() : txt), tamanho: tam, fonte, linhas: Math.ceil(H / (tam * 1.02)) + 3, velocidade: Math.round(s.entre(40, 90) * base) };
|
|
1704
1762
|
// Moldura em volta da palavra é vício de "cara de IA" (a caixa na palavra-chave); sai em 08/10.
|
|
1705
1763
|
b.acento.push({ forma: s.chance(0.5) ? 'ponto' : 'sublinha', alvo: { linha: b.linhas[0].id } });
|
package/lib/estudio/index.js
CHANGED
|
@@ -47,6 +47,8 @@ const assets = require('./assets.js');
|
|
|
47
47
|
const video = require('./video.js');
|
|
48
48
|
const batida = require('./batida.js');
|
|
49
49
|
const revisor = require('./revisor.js');
|
|
50
|
+
const marcaLib = require('./marca.js');
|
|
51
|
+
const conferencia = require('./conferencia.js');
|
|
50
52
|
const margem = require('./margem.js');
|
|
51
53
|
const assinatura = require('./assinatura.js');
|
|
52
54
|
const identidade = require('./identidade.js');
|
|
@@ -509,6 +511,15 @@ async function ferramentaMidia(args) {
|
|
|
509
511
|
|
|
510
512
|
/* O ELEMENTO do site, tirado pelo código (print-site.js): a lista das peças
|
|
511
513
|
(logo, botão, cartão, título…) e uma peça sozinha, com o fundo transparente. */
|
|
514
|
+
/* A REFERÊNCIA VISUAL que a pessoa manda (13/10: "não usa as referências
|
|
515
|
+
que mandei" — era o print da landing, arrastado no pedido). O agente não
|
|
516
|
+
enxerga imagem; aqui ela entra no projeto, as cores saem pelo ffmpeg e,
|
|
517
|
+
se o projeto ainda não tem marca, viram a marca. Com site, o site manda. */
|
|
518
|
+
if (acao === 'referencia' || acao === 'referência') {
|
|
519
|
+
if (!p) return semProjeto(args.id);
|
|
520
|
+
return importarReferencia(p, String(args.arquivo || args.caminho || args.src || ''));
|
|
521
|
+
}
|
|
522
|
+
|
|
512
523
|
if (acao === 'elementos') return printSite.elementosDoSite(args.url || args.site || args.termo, { celular: !!args.celular });
|
|
513
524
|
if (acao === 'elemento') {
|
|
514
525
|
if (!p) return { ok: false, error: 'para tirar o elemento é preciso dizer em qual projeto (id).' };
|
|
@@ -1247,6 +1258,32 @@ function conferirAssinatura(p, r, nome) {
|
|
|
1247
1258
|
projeto.anotar(p.id, 'assinatura', fora.length ? `fora da assinatura ${nome}: ${fora.join('; ')}` : `seguiu a assinatura ${nome}`);
|
|
1248
1259
|
}
|
|
1249
1260
|
|
|
1261
|
+
/* A REFERÊNCIA VISUAL que a pessoa manda (13/10: "não usa as referências
|
|
1262
|
+
que mandei" — era o print da landing, arrastado no pedido). O agente não
|
|
1263
|
+
enxerga imagem; aqui ela entra no projeto, as cores saem pelo ffmpeg e, se o
|
|
1264
|
+
projeto ainda não tem marca, viram a marca. Com site, o site manda. */
|
|
1265
|
+
function importarReferencia(p, arquivo) {
|
|
1266
|
+
const orig = String(arquivo || '').trim().replace(/^file:\/\//, '');
|
|
1267
|
+
if (!orig || !fs.existsSync(orig)) return { ok: false, error: 'mande em "arquivo" o caminho da imagem de referência.' };
|
|
1268
|
+
const nome = path.basename(orig).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^\w.-]+/g, '-');
|
|
1269
|
+
const destino = path.join(p.pasta, 'midia', 'referencias', nome);
|
|
1270
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
1271
|
+
fs.copyFileSync(orig, destino);
|
|
1272
|
+
const rel = path.relative(p.pasta, destino).replace(/\\/g, '/');
|
|
1273
|
+
const cores = marcaLib.coresDaImagem(destino);
|
|
1274
|
+
const temMarca = !!(p.marca && Object.keys(marcaLib.marcaEmObjeto(p.marca).cores || {}).length);
|
|
1275
|
+
const salvar = { referencias: [...(p.referencias || []).filter((x) => x.src !== rel), { src: rel, cores, de: orig }] };
|
|
1276
|
+
if (!temMarca && cores.length) salvar.marca = { ...(p.marca || {}), ...marcaLib.marcaEmObjeto({ cores }) };
|
|
1277
|
+
projeto.salvar(p.id, salvar);
|
|
1278
|
+
projeto.anotar(p.id, 'referencia', `imagem ${rel}: ${cores.join(' ') || 'sem cor'}${temMarca ? '' : ' (virou a marca)'}`);
|
|
1279
|
+
return {
|
|
1280
|
+
ok: true, src: rel, cores, viraMarca: !temMarca && cores.length > 0,
|
|
1281
|
+
comoUsar: (temMarca ? 'O projeto já tem a marca do site: ela manda nas cores; a referência guia o JEITO (composição, peso, o que aparece). '
|
|
1282
|
+
: 'Estas cores viraram a marca do projeto. ')
|
|
1283
|
+
+ 'Se a imagem é print do site ou do produto, o vídeo mostra a tela de verdade (estudio_midia acao:"print" e a peça printsite).',
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1250
1287
|
async function prepararRoteiro(p, args) {
|
|
1251
1288
|
const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
|
|
1252
1289
|
if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
|
|
@@ -1257,8 +1294,16 @@ async function prepararRoteiro(p, args) {
|
|
|
1257
1294
|
for (const pt of roteiro.partes || []) {
|
|
1258
1295
|
if (pt.src && !existe(pt.src)) { if (!pt.busca && /^https?:/i.test(pt.src)) pt.busca = pt.titulo || ''; delete pt.src; }
|
|
1259
1296
|
}
|
|
1297
|
+
// As imagens do pedido (o terminal guardou em referencias.imagens) entram sozinhas, uma vez por projeto.
|
|
1298
|
+
const jaTem = new Set((p.referencias || []).map((x) => x.de));
|
|
1299
|
+
const novas = referencias.imagens().filter((f) => !jaTem.has(f));
|
|
1300
|
+
for (const f of novas) { try { importarReferencia(p, f); } catch (e) { projeto.anotar(p.id, 'referencia', `não importou ${path.basename(f)}: ${e.message}`); } }
|
|
1301
|
+
if (novas.length) p = projeto.ler(p.id) || p;
|
|
1260
1302
|
if (roteiro.marca) {
|
|
1261
|
-
roteiro
|
|
1303
|
+
/* A do roteiro por cima da do site, CAMPO A CAMPO: antes o "cores" em
|
|
1304
|
+
lista do agente apagava o {fundo, destaque} lido do site (13/10). */
|
|
1305
|
+
const site = marcaLib.marcaEmObjeto(p.marca), dele = marcaLib.marcaEmObjeto(roteiro.marca);
|
|
1306
|
+
roteiro.marca = { ...site, ...dele, cores: { ...(site.cores || {}), ...(dele.cores || {}) }, fontes: { ...(site.fontes || {}), ...(dele.fontes || {}) } };
|
|
1262
1307
|
if (roteiro.marca.logo && !existe(roteiro.marca.logo)) roteiro.marca.logo = (p.marca && existe(p.marca.logo)) ? p.marca.logo : undefined;
|
|
1263
1308
|
} else if (p.marca) roteiro.marca = p.marca;
|
|
1264
1309
|
const partes = roteiro.partes || [];
|
|
@@ -1276,6 +1321,12 @@ async function prepararRoteiro(p, args) {
|
|
|
1276
1321
|
const semImagem = referencias.semImagem(ref);
|
|
1277
1322
|
let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
|
|
1278
1323
|
if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
|
|
1324
|
+
/* AS FONTES DA CASA (13/10, o primocode-ele-constroi.mp4 que o Joel aprovou):
|
|
1325
|
+
no gerativo, título em Inter e texto em JetBrains Mono, com as CORES da
|
|
1326
|
+
marca. Só muda se o pedido falar de fonte. */
|
|
1327
|
+
if (estilo === 'gerativo' && !/\bfontes?\b|tipografia|\bserifa/i.test(pedidoTxt)) {
|
|
1328
|
+
roteiro.marca = { ...(roteiro.marca || {}), fontes: { titulo: 'Inter', texto: 'JetBrains Mono' } };
|
|
1329
|
+
}
|
|
1279
1330
|
/* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
|
|
1280
1331
|
com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
|
|
1281
1332
|
o agente tenha escolhido outro estilo pelo nome. */
|
|
@@ -1847,6 +1898,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1847
1898
|
} catch { /* fica sem */ }
|
|
1848
1899
|
continue;
|
|
1849
1900
|
}
|
|
1901
|
+
// O ícone do próprio site (estudio_site guardou os <svg> na marca).
|
|
1902
|
+
const doSite = h.match(/^site:(\d+)$/i);
|
|
1903
|
+
if (doSite) { const x = ((roteiro.marca || {}).iconesSite || [])[Number(doSite[1]) - 1]; if (x && x.svg) icones[h] = x.svg; continue; }
|
|
1850
1904
|
if (!/^[a-z0-9-]+:[a-z0-9-]+$/i.test(h) || icones[h]) continue;
|
|
1851
1905
|
try { const ic = await assets.icone(h, { pasta: p.pasta }); if (ic && ic.ok) icones[h] = ic.svg; else projeto.anotar(p.id, 'icones', `herói ${h} não baixou: ${(ic && ic.error) || '?'}`); } catch { /* fica sem */ }
|
|
1852
1906
|
}
|
|
@@ -2106,6 +2160,12 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
2106
2160
|
projeto.salvar(args.id, { renders: [...(p.renders || []), { quando: new Date().toISOString(), arquivo: saida, bytes }] });
|
|
2107
2161
|
projeto.anotar(args.id, 'render', `${(bytes / 1048576).toFixed(1)} MB, ${conferida.duracao}s`);
|
|
2108
2162
|
|
|
2163
|
+
/* A CONFERÊNCIA (conferencia.js): o arquivo que saiu, medido. Som fora do
|
|
2164
|
+
alvo e imagem parada viram aviso; quem decide se refaz é o agente. */
|
|
2165
|
+
const conf = fs.existsSync(saida) ? await conferencia.conferir(saida, { duracao: conferida.duracao }) : { ok: false };
|
|
2166
|
+
const avisosDoArquivo = conf.ok ? conf.avisos : [];
|
|
2167
|
+
if (avisosDoArquivo.length) projeto.anotar(args.id, 'conferencia', avisosDoArquivo.join('; '));
|
|
2168
|
+
|
|
2109
2169
|
// O vídeo pronto abre. É a única coisa que abre — a interface morreu, e
|
|
2110
2170
|
// o que a pessoa pediu foi o vídeo.
|
|
2111
2171
|
if (ctx.abrir) { try { ctx.abrir(saida); } catch { /* o caminho está na resposta */ } }
|
|
@@ -2114,8 +2174,9 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
2114
2174
|
ok: true, arquivo: saida, bytes, segundos: conferida.duracao,
|
|
2115
2175
|
faltaram: faltaram.length ? faltaram : undefined,
|
|
2116
2176
|
revisao: revisao.consertos.length || revisao.avisos.length ? { consertado: revisao.consertos, avisos: revisao.avisos } : undefined,
|
|
2177
|
+
conferencia: conf.ok ? { som: conf.som, parados: conf.parados, avisos: conf.avisos } : undefined,
|
|
2117
2178
|
aviso: 'ABERTO para o usuário assistir. Chame finish dizendo o que foi feito'
|
|
2118
|
-
+ (revisao.avisos.length ? ' — e cite os avisos da revisão que valem um novo plano.' : '.'),
|
|
2179
|
+
+ (revisao.avisos.length || avisosDoArquivo.length ? ' — e cite os avisos da revisão e da conferência que valem um novo plano.' : '.'),
|
|
2119
2180
|
};
|
|
2120
2181
|
}
|
|
2121
2182
|
|
|
@@ -2227,6 +2288,9 @@ async function ferramentaSite(args) {
|
|
|
2227
2288
|
}
|
|
2228
2289
|
}
|
|
2229
2290
|
} catch { /* sem gradiente */ }
|
|
2291
|
+
/* Os ícones do próprio site (os <svg> dos cartões): viram "site:N" no roteiro. */
|
|
2292
|
+
const icSite = marcaLib.iconesDoSite(html);
|
|
2293
|
+
if (icSite.length) { marca.iconesSite = icSite; r.icones = icSite.map((x, i) => `site:${i + 1}${x.nome ? ' (' + x.nome + ')' : ''}`); }
|
|
2230
2294
|
projeto.salvar(args.id, { acervo: { fotos: a.fotos, videos: a.videos, fatos: a.fatos, site: r.url }, marca });
|
|
2231
2295
|
projeto.anotar(args.id, 'midia', `acervo do site: ${a.fotos.length} fotos, ${a.videos.length} vídeos, ${a.paginas.length} páginas lidas`);
|
|
2232
2296
|
let orcamento = 4200;
|
|
@@ -2240,7 +2304,8 @@ async function ferramentaSite(args) {
|
|
|
2240
2304
|
r.comoUsar = 'Escreva o roteiro com os FATOS acima (datas, local, números, frases da marca) — nada '
|
|
2241
2305
|
+ 'inventado. As fotos e os vídeos reais da marca entram SOZINHOS no vídeo: não precisa pôr '
|
|
2242
2306
|
+ '"busca" nas partes de imagem (use tipo "imagem" com só o título). Use as cores, a fonte e '
|
|
2243
|
-
+ 'a logo em roteiro.marca.'
|
|
2307
|
+
+ 'a logo em roteiro.marca (objeto: {cores: {fundo, destaque, tinta}, fontes: {titulo, texto}}).'
|
|
2308
|
+
+ (r.icones ? ' Os ícones do site estão em "icones": ponha icone:"site:N" na parte em que o assunto dele aparece.' : '');
|
|
2244
2309
|
} catch (e) {
|
|
2245
2310
|
r.acervo = { erro: e.message };
|
|
2246
2311
|
}
|
|
@@ -281,6 +281,11 @@ function leiaute(P) {
|
|
|
281
281
|
ela ficava VAZIA até a voz chegar na frase (teste de 08/10). */
|
|
282
282
|
const p0 = pr ? pr.palavras[0].em : b.de;
|
|
283
283
|
const entT = [Math.max(ent[0], p0 - 0.4), Math.max(ent[1], p0 + 0.1)];
|
|
284
|
+
/* Mas a pílula de ANTES ficava: o texto dela saía e a forma esperava
|
|
285
|
+
vazia até a voz (0,6 s no café de 11/10, de 7,2 s a 7,9 s). Quando
|
|
286
|
+
a voz demora, a forma encolhe num ponto e cresce já com a frase. */
|
|
287
|
+
// Começa em b.de, quando o texto de antes já saiu (antes disso a forma laranja ficava atrás dele).
|
|
288
|
+
if (entT[0] >= b.de + 0.4) vai(b.de, b.de + 0.4, forma(tx, ty, 16 * u, 16 * u, { solid: 1, ac: 1 }));
|
|
284
289
|
if (umaLinha) vai(entT[0], entT[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
|
|
285
290
|
else vai(entT[0], entT[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
|
|
286
291
|
}
|
|
@@ -430,8 +430,8 @@ function letra({ t, fps, l, p, j, ch, i, idx, n, b, cx, cores, eixosF, alvoSaida
|
|
|
430
430
|
const centroLinha = cx ? cx.x + cx.w / 2 : 0;
|
|
431
431
|
switch (c) {
|
|
432
432
|
case 'carimbo': {
|
|
433
|
-
|
|
434
|
-
tr.push('
|
|
433
|
+
// Sem inclinação: a letra carimbada torta lia como defeito (13/10). Fica o impacto da escala.
|
|
434
|
+
tr.push('scale(' + (1 + 0.32 * (1 - m)).toFixed(4) + ')');
|
|
435
435
|
const f = tl * fps;
|
|
436
436
|
blur = f < 2 ? 1.4 : Math.max(0, 0.7 * (1 - (f - 2) / 7));
|
|
437
437
|
break;
|
|
@@ -917,7 +917,11 @@ export function Gerativo({ el, t, fps }) {
|
|
|
917
917
|
leve a cada meio segundo, como um bumbo. */
|
|
918
918
|
// A respiração lenta do mundo (sem bumbo: o pulso batendo lia como tranco).
|
|
919
919
|
sz += 0.008 * Math.sin((t * Math.PI * 2) / 3.4);
|
|
920
|
-
|
|
920
|
+
/* A câmera NÃO GIRA (13/10, "os motions saem muito tortos"): o giro das
|
|
921
|
+
chaves (cam0.rot) e o da deriva (sr) inclinavam o mundo inteiro, e todo
|
|
922
|
+
texto saía com a linha de base caída. Ficam a deriva, o zoom e o tranco. */
|
|
923
|
+
void sr;
|
|
924
|
+
const cam = { x: cam0.x + sx, y: cam0.y + sy, zoom: cam0.zoom * (1 + sz), rot: 0 };
|
|
921
925
|
const ac0 = acentoEm(t, estados);
|
|
922
926
|
// O acento também vive: treme de leve e respira na espessura.
|
|
923
927
|
const ac = { ...ac0, pts: ac0.pts.map((q, i) => [q[0] + Math.sin(t * 3.1 + i * 0.5) * 1.6 * base, q[1] + Math.cos(t * 2.7 + i * 0.37) * 1.6 * base]), larg: ac0.larg * (1 + 0.08 * Math.sin(t * 4.2)) };
|
|
@@ -929,7 +933,8 @@ export function Gerativo({ el, t, fps }) {
|
|
|
929
933
|
const origem = tela(ac.cx, ac.cy);
|
|
930
934
|
/* O ESPAÇO 3D: o mundo tem perspectiva, a câmera também inclina (rotateX/Y)
|
|
931
935
|
e as camadas estão em profundidades diferentes — o texto não é adesivo. */
|
|
932
|
-
|
|
936
|
+
// A perspectiva fica, mas quase parada: 1,4° e 2° deixavam a frase em trapézio.
|
|
937
|
+
const rx = Math.cos(t * 0.27) * 0.35, ry = Math.sin(t * 0.31) * 0.5;
|
|
933
938
|
const mundo = 'translate(' + W / 2 + 'px,' + H / 2 + 'px) rotate(' + cam.rot.toFixed(3) + 'deg) rotateX(' + rx.toFixed(3) + 'deg) rotateY(' + ry.toFixed(3) + 'deg) scale(' + cam.zoom.toFixed(4) + ') translate(' + (-W / 2 - cam.x).toFixed(1) + 'px,' + (-H / 2 - cam.y).toFixed(1) + 'px)';
|
|
934
939
|
// No ar: a batida atual, a anterior enquanto sai.
|
|
935
940
|
const noAr = bs.filter((x, i) => i === bi || (i === bi - 1 && t < x.ate + 0.14 && x.saida !== 'corte'));
|
|
@@ -831,6 +831,15 @@ const ESP = {
|
|
|
831
831
|
macaco: { forma: 4, corpo: [0.36, 0.42, 0.42], perna: 0.36, cab: 0.36, foc: [0.22, 0.14, 0.08], orelha: 'redonda', rabo: 'longo', cor: '#7a5232', cor2: '#e6c9a0', focCor: '#e6c9a0' },
|
|
832
832
|
};
|
|
833
833
|
|
|
834
|
+
/** Onde a cabeça do quatro-patas fica no chão, a partir do centro do bicho:
|
|
835
|
+
* o corpo vira 0,55 de lado e a cabeça vai na frente dele (o Quadrupede).
|
|
836
|
+
* A câmera mirava o meio do corpo e o rosto saía cortado na borda (13/10). */
|
|
837
|
+
export function frenteDoBicho(especie) {
|
|
838
|
+
const e = ESP[especie] || ESP.cachorro;
|
|
839
|
+
if (e.forma === 'ave' || e.forma === 'pinguim' || e.forma === 'peixe' || e.forma === 'dino') return { giro: 0, dist: 0 };
|
|
840
|
+
return { giro: 0.55, dist: e.corpo[2] / 2 + e.cab * 0.25 + (e.pescoco || 0) * 0.25 };
|
|
841
|
+
}
|
|
842
|
+
|
|
834
843
|
/** A altura da cabeça (para a câmera mirar no rosto do bicho). */
|
|
835
844
|
export function alturaDoBicho(especie) {
|
|
836
845
|
const e = ESP[especie] || ESP.cachorro;
|
|
@@ -30,7 +30,7 @@ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPa
|
|
|
30
30
|
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
|
|
31
31
|
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
|
|
32
32
|
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
|
33
|
-
import { Mundo, Animal, Coisa, alturaDoBicho, useIcones } from './mundo.jsx';
|
|
33
|
+
import { Mundo, Animal, Coisa, alturaDoBicho, frenteDoBicho, useIcones } from './mundo.jsx';
|
|
34
34
|
|
|
35
35
|
const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
|
|
36
36
|
const lerp = (a, b, k) => a + (b - a) * k;
|
|
@@ -267,7 +267,8 @@ function RostoPixel({ p, est }) {
|
|
|
267
267
|
"olha como quero o 3D": o rosto da referência é LOW-POLY — uma esfera de
|
|
268
268
|
poucas faces com sombreado chapado, nariz de pirâmide, sobrancelha grossa,
|
|
269
269
|
o cabelo cacheado feito de dezenas de pedrinhas facetadas. O corpo segue
|
|
270
|
-
de blocos. `cabeca: '
|
|
270
|
+
de blocos. Desde 13/10 só sai com `cabeca: 'facetada'`: o padrão voltou a
|
|
271
|
+
ser o cubo com rosto de pixel da 10.5 (partitura sem o campo também). */
|
|
271
272
|
const GEO_FACETA = new Map();
|
|
272
273
|
function faceta(det) {
|
|
273
274
|
if (!GEO_FACETA.has(det)) GEO_FACETA.set(det, new THREE.IcosahedronGeometry(1, det));
|
|
@@ -432,7 +433,7 @@ function PessoaBloco({ p, est }) {
|
|
|
432
433
|
<BracoBloco lado={-1} g={est.gesto[0]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
|
|
433
434
|
<BracoBloco lado={1} g={est.gesto[1]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
|
|
434
435
|
<group position={[0, 1.8 + resp, 0]} rotation={[cp, cy, cr]}>
|
|
435
|
-
{p.cabeca
|
|
436
|
+
{p.cabeca !== 'facetada' ? (
|
|
436
437
|
<>
|
|
437
438
|
<Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
|
|
438
439
|
<RostoPixel p={p} est={est} />
|
|
@@ -623,13 +624,34 @@ function lugarDoFigurante(el, fig, j, vert = false) {
|
|
|
623
624
|
return [lado * (borda + Math.floor(j / 2) * 0.9), 0, baixo ? 0.45 : -0.5];
|
|
624
625
|
}
|
|
625
626
|
|
|
627
|
+
/* A CÂMERA NÃO ATRAVESSA NINGUÉM (13/10): no close de quem fala, outro do
|
|
628
|
+
elenco ficava entre a lente e o rosto, ou com a lente dentro dele (o close
|
|
629
|
+
do cachorro com o braço do outro cortando o quadro). `bate` diz se alguém
|
|
630
|
+
do elenco, um cilindro do chão até acima da cabeça, corta o caminho. */
|
|
631
|
+
function bate(el, quem, pos, alvo) {
|
|
632
|
+
const dx = alvo[0] - pos[0], dz = alvo[2] - pos[2], L2 = dx * dx + dz * dz || 1;
|
|
633
|
+
for (let j = 0; j < el.length; j++) {
|
|
634
|
+
if (j === quem) continue;
|
|
635
|
+
const [x, , z] = lugarDe(el, j);
|
|
636
|
+
const u = clamp(((x - pos[0]) * dx + (z - pos[2]) * dz) / L2);
|
|
637
|
+
const py = pos[1] + (alvo[1] - pos[1]) * u;
|
|
638
|
+
if (Math.hypot(pos[0] + dx * u - x, pos[2] + dz * u - z) < 0.55 && py < alturaDaCabeca(el[j]) + 0.3) return true;
|
|
639
|
+
}
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
|
|
626
643
|
function cameraDe(pl, t, part, aspecto) {
|
|
627
644
|
const vert = aspecto < 1;
|
|
628
645
|
const el = part.elenco;
|
|
629
646
|
const dois = el.length > 1;
|
|
630
647
|
const quem = el[pl.alvo] ? pl.alvo : 0;
|
|
631
|
-
|
|
632
|
-
const
|
|
648
|
+
// O bicho de quatro patas tem a cabeça NA FRENTE do corpo, e o rosto virado para onde o corpo vira.
|
|
649
|
+
const doBicho = (i) => (el[i] && el[i].tipo === 'animal' ? frenteDoBicho(el[i].especie) : { giro: 0, dist: 0 });
|
|
650
|
+
const cabeca = (i) => {
|
|
651
|
+
const p = lugarDe(el, i), b = doBicho(i), g = giroDe(el, i) + b.giro;
|
|
652
|
+
return [p[0] + Math.sin(g) * b.dist, alturaDaCabeca(el[i]), p[2] + Math.cos(g) * b.dist];
|
|
653
|
+
};
|
|
654
|
+
const frente = (i) => { const g = giroDe(el, i) + doBicho(i).giro * 0.7; return [Math.sin(g), 0, Math.cos(g)]; };
|
|
633
655
|
const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
|
|
634
656
|
const fovK = vert ? 1.45 : 1;
|
|
635
657
|
let pos, alvo, fov;
|
|
@@ -658,11 +680,16 @@ function cameraDe(pl, t, part, aspecto) {
|
|
|
658
680
|
const dist = (pl.tipo === 'detalhe' ? 1.25 : pl.tipo === 'close' ? 1.7 : 2.7) * (vert ? 1.25 : 1);
|
|
659
681
|
fov = (pl.tipo === 'detalhe' ? 30 : pl.tipo === 'close' ? 32 : 34) * fovK;
|
|
660
682
|
const h = cabeca(quem), f = frente(quem);
|
|
661
|
-
const lado = pl.lado * 0.42;
|
|
662
|
-
const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
|
|
663
683
|
const baixo = pl.tipo === 'medio' ? 0.05 : -0.32;
|
|
664
|
-
pos = [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
|
|
665
684
|
alvo = [h[0], h[1] + (pl.tipo === 'medio' ? -0.35 : -0.02), h[2]];
|
|
685
|
+
const de = (ld) => {
|
|
686
|
+
const lado = ld * 0.42;
|
|
687
|
+
const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
|
|
688
|
+
return [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
|
|
689
|
+
};
|
|
690
|
+
// O lado pedido; se alguém corta, o outro lado, de frente, mais aberto; senão, a câmera sobe por cima.
|
|
691
|
+
pos = [pl.lado, -pl.lado, 0, 1.6, -1.6].map(de).find((q) => !bate(el, quem, q, alvo));
|
|
692
|
+
if (!pos) { const q = de(pl.lado); pos = [q[0], q[1] + 0.9, q[2]]; }
|
|
666
693
|
}
|
|
667
694
|
// No vertical a legenda está no alto: a câmera mira acima, e o rosto desce no quadro.
|
|
668
695
|
if (vert) alvo = [alvo[0], alvo[1] + (pl.tipo === 'aberto' || pl.tipo === 'largo' ? 0.45 : pl.tipo === 'medio' ? 0.3 : pl.tipo === 'ombro' ? 0.22 : 0.17), alvo[2]];
|
package/lib/estudio/marca.js
CHANGED
|
@@ -280,4 +280,109 @@ async function marcaDaEmpresa(args, { projectDir, studio }) {
|
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
/* A MARCA EM LISTA VIRA OBJETO (13/10). O agente escreve a marca do jeito
|
|
284
|
+
que leu: "cores": ["#22d3ee", "#0a0b0b", …], "fontes": ["Instrument
|
|
285
|
+
Serif", "JetBrains Mono"]. O Estúdio lê {fundo, destaque, tinta} e
|
|
286
|
+
{titulo, texto, mono}. Sem a conversão, `cores.fundo` não existia, o
|
|
287
|
+
gerativo achava que não havia marca e sorteava o humor: o motion do
|
|
288
|
+
PrimoCode saiu vermelho e creme em Bodoni, com o site preto e ciano.
|
|
289
|
+
cores a primeira escura é o fundo, a primeira viva o destaque (a
|
|
290
|
+
segunda, o apoio), a primeira clara a tinta;
|
|
291
|
+
fontes a mono vai para mono; das outras, a 1ª é título e a 2ª texto.
|
|
292
|
+
O que já é objeto passa intacto. */
|
|
293
|
+
function marcaEmObjeto(m) {
|
|
294
|
+
if (!m || typeof m !== 'object') return {};
|
|
295
|
+
const out = { ...m };
|
|
296
|
+
if (Array.isArray(m.cores)) {
|
|
297
|
+
const rgb = (h) => { const n = parseInt(h.slice(1), 16); return [n >> 16, (n >> 8) & 255, n & 255].map((v) => v / 255); };
|
|
298
|
+
const luz = (h) => { const [r, g, b] = rgb(h); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
|
|
299
|
+
const sat = (h) => { const v = rgb(h); return Math.max(...v) - Math.min(...v); };
|
|
300
|
+
const cs = m.cores.map((x) => String((x && x.cor) || x || '').trim()).filter((x) => /^#[0-9a-f]{6}$/i.test(x));
|
|
301
|
+
const vivas = cs.filter((x) => sat(x) > 0.25);
|
|
302
|
+
const fundo = cs.find((x) => luz(x) < 0.12 && sat(x) <= 0.25);
|
|
303
|
+
const tinta = cs.find((x) => luz(x) > 0.8 && sat(x) <= 0.25);
|
|
304
|
+
out.cores = { ...(fundo ? { fundo } : {}), ...(vivas[0] ? { destaque: vivas[0] } : {}), ...(vivas[1] ? { apoio: vivas[1] } : {}), ...(tinta ? { tinta } : {}) };
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(m.fontes)) {
|
|
307
|
+
const fs = m.fontes.map((f) => String((f && f.nome) || f || '').trim()).filter(Boolean);
|
|
308
|
+
const mono = fs.find((f) => /mono|code|consol/i.test(f));
|
|
309
|
+
const resto = fs.filter((f) => f !== mono);
|
|
310
|
+
out.fontes = { ...(resto[0] ? { titulo: resto[0] } : {}), ...(resto[1] ? { texto: resto[1] } : {}), ...(mono ? { mono } : {}) };
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* OS ÍCONES DO SITE (13/10: "não usa os ícones do site"). Os ícones de uma
|
|
316
|
+
landing quase sempre são <svg> inline (o desenho de traço ao lado de cada
|
|
317
|
+
cartão), e não arquivos. Aqui saem os pequenos — com viewBox, sem imagem
|
|
318
|
+
nem script dentro, até 4 KB — com um nome: o aria-label, o <title>, ou o
|
|
319
|
+
título (h2/h3/h4) logo depois dele no HTML. No roteiro viram "site:N". */
|
|
320
|
+
function iconesDoSite(html, max = 12) {
|
|
321
|
+
const out = [], vistos = new Set();
|
|
322
|
+
const re = /<svg\b[^>]*>[\s\S]*?<\/svg>/gi;
|
|
323
|
+
let m;
|
|
324
|
+
while ((m = re.exec(String(html || ''))) && out.length < max) {
|
|
325
|
+
const svg = m[0];
|
|
326
|
+
if (svg.length > 4000 || /<(image|script|foreignObject)\b/i.test(svg) || !/viewBox=/i.test(svg)) continue;
|
|
327
|
+
const abre = svg.slice(0, svg.indexOf('>') + 1);
|
|
328
|
+
const lado = Number((abre.match(/\b(?:width|height)=["']?(\d+)/i) || [])[1] || 0);
|
|
329
|
+
if (lado > 120) continue;
|
|
330
|
+
const corpo = svg.replace(/\s+/g, ' ');
|
|
331
|
+
if (vistos.has(corpo)) continue;
|
|
332
|
+
vistos.add(corpo);
|
|
333
|
+
const depois = String(html).slice(re.lastIndex, re.lastIndex + 500);
|
|
334
|
+
const nome = (abre.match(/aria-label=["']([^"']+)/i) || [])[1]
|
|
335
|
+
|| (svg.match(/<title>([^<]+)<\/title>/i) || [])[1]
|
|
336
|
+
|| ((depois.match(/<h[2-4][^>]*>([\s\S]*?)<\/h[2-4]>/i) || [])[1] || '').replace(/<[^>]+>/g, '').trim()
|
|
337
|
+
|| '';
|
|
338
|
+
out.push({ nome: nome.slice(0, 40), svg });
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* AS CORES DE UMA IMAGEM (a referência que a pessoa manda: o print do site,
|
|
344
|
+
um frame, um anúncio de que gostou). O ffmpeg reduz a imagem a 160 px e
|
|
345
|
+
devolve os pixels; as cores parecidas viram uma, e saem as 6 que mais
|
|
346
|
+
aparecem, da mais comum à menos. Sem ffmpeg, lista vazia. */
|
|
347
|
+
function coresDaImagem(arquivo, max = 6) {
|
|
348
|
+
const { spawnSync } = require('child_process');
|
|
349
|
+
const r = spawnSync('ffmpeg', ['-v', 'error', '-i', arquivo, '-vf', 'scale=160:-1', '-frames:v', '1', '-f', 'rawvideo', '-pix_fmt', 'rgb24', '-'], { maxBuffer: 1 << 22 });
|
|
350
|
+
if (r.status !== 0 || !r.stdout || !r.stdout.length) return [];
|
|
351
|
+
const conta = new Map();
|
|
352
|
+
for (let i = 0; i + 2 < r.stdout.length; i += 3) {
|
|
353
|
+
const k = ((r.stdout[i] >> 3) << 10) | ((r.stdout[i + 1] >> 3) << 5) | (r.stdout[i + 2] >> 3);
|
|
354
|
+
conta.set(k, (conta.get(k) || 0) + 1);
|
|
355
|
+
}
|
|
356
|
+
const hex = (k) => '#' + [(k >> 10) & 31, (k >> 5) & 31, k & 31].map((v) => (v * 8 + 4).toString(16).padStart(2, '0')).join('');
|
|
357
|
+
const rgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
358
|
+
const longe = (a, b) => { const x = rgb(a), y = rgb(b); return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]); };
|
|
359
|
+
// As 4 mais comuns, e as 2 mais VIVAS que aparecem de verdade: a cor da
|
|
360
|
+
// marca costuma ocupar pouca área (o ciano de um título, o botão), e sem
|
|
361
|
+
// isto o print dava só fundo e cinza.
|
|
362
|
+
const total = r.stdout.length / 3;
|
|
363
|
+
const sat = (h) => { const v = rgb(h); return (Math.max(...v) - Math.min(...v)) / 255; };
|
|
364
|
+
const lista = [...conta].sort((a, b) => b[1] - a[1]).map(([k, n]) => ({ h: hex(k), n }));
|
|
365
|
+
const out = [];
|
|
366
|
+
const por = (x) => { if (out.length < max && out.every((o) => longe(o, x.h) > 48)) out.push(x.h); };
|
|
367
|
+
for (const x of lista) { if (out.length >= Math.max(1, max - 2)) break; por(x); }
|
|
368
|
+
/* As vivas se espalham em dezenas de tons de 1 ou 2 pixels (a borda da
|
|
369
|
+
letra mistura com o fundo): contam por MATIZ, em faixas de 30°, e de
|
|
370
|
+
cada faixa vale o pixel mais vivo — o miolo da letra, não a borda. */
|
|
371
|
+
const faixas = new Map();
|
|
372
|
+
for (let i = 0; i + 2 < r.stdout.length; i += 3) {
|
|
373
|
+
const v = [r.stdout[i], r.stdout[i + 1], r.stdout[i + 2]], mx = Math.max(...v), mn = Math.min(...v), d = mx - mn;
|
|
374
|
+
if (d / 255 < 0.35 || mx < 60) continue;
|
|
375
|
+
const hh = mx === v[0] ? ((v[1] - v[2]) / d) % 6 : mx === v[1] ? (v[2] - v[0]) / d + 2 : (v[0] - v[1]) / d + 4;
|
|
376
|
+
const f = Math.floor(((hh * 60 + 360) % 360) / 30);
|
|
377
|
+
const x = faixas.get(f) || { n: 0, melhor: null, nota: -1 };
|
|
378
|
+
x.n++;
|
|
379
|
+
const nota = d * mx;
|
|
380
|
+
if (nota > x.nota) { x.nota = nota; x.melhor = '#' + v.map((c) => c.toString(16).padStart(2, '0')).join(''); }
|
|
381
|
+
faixas.set(f, x);
|
|
382
|
+
}
|
|
383
|
+
[...faixas.values()].filter((x) => x.n >= Math.max(8, total * 0.0005)).sort((a, b) => b.n - a.n).slice(0, 2).forEach((x) => por({ h: x.melhor }));
|
|
384
|
+
void sat; void lista;
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = { marcaDaEmpresa, candidatasALogo, coresDe, fontesDe, metas, atributo, marcaEmObjeto, iconesDoSite, coresDaImagem };
|
|
@@ -154,8 +154,10 @@ function aparencia(nome, pedido = {}) {
|
|
|
154
154
|
barba: !!pedido.barba && pedido.genero !== 'f', // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
|
|
155
155
|
bigode: !!pedido.bigode && pedido.genero !== 'f',
|
|
156
156
|
...(pedido.genero ? { genero: pedido.genero } : {}),
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
/* O cubo com rosto de pixel (o da 10.5) é o padrão de novo: o low-poly
|
|
158
|
+
da 10.6 ficou "feito com polígonos, bugado e horrível" (Joel, 13/10).
|
|
159
|
+
A facetada só sai pedida. */
|
|
160
|
+
cabeca: pedido.cabeca === 'facetada' ? 'facetada' : 'cubo',
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -80,8 +80,12 @@ 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, 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. '
|
|
85
|
+
/* O ROTEIRO (13/10: "o roteiro que monta é horrível" — eram os títulos da landing em fila). */
|
|
86
|
+
+ '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
|
+
+ '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 '
|
|
88
|
+
+ '12 palavras e puxa a próxima; o titulo da parte é a palavra-chave (1 a 4 palavras), não a fala repetida.';
|
|
85
89
|
if (modo === 'sem' && explicito) {
|
|
86
90
|
return '[REFERÊNCIAS: NENHUMA] A pessoa quer SÓ MOTION: não pesquise (nada de pesquisar_referencia nem estudio_site), '
|
|
87
91
|
+ 'não busque foto nem vídeo, não use estudio_midia de banco. Escreva o roteiro com o que o pedido já diz. ' + motion;
|
|
@@ -124,4 +128,42 @@ function filtrar(r, { modo, escopo } = ATUAL) {
|
|
|
124
128
|
return { ...r, fontes: refs.length, referencias: refs, ...(fora ? { foraDoEscopo: fora } : {}) };
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
/* AS IMAGENS DO PEDIDO (13/10: "não usa as referências que mandei" — era o
|
|
132
|
+
print da landing, arrastado no pedido). O modelo do servidor não enxerga
|
|
133
|
+
imagem e nem pode chamar a importação (a lista de ações dele é fechada),
|
|
134
|
+
então o terminal guarda aqui os caminhos de imagem que EXISTEM no texto do
|
|
135
|
+
pedido, e o Estúdio importa sozinho no próximo roteiro (index.js). */
|
|
136
|
+
let IMAGENS = [];
|
|
137
|
+
const RE_EXT_IMG = /\.(png|jpe?g|gif|webp|heic|bmp)\b/gi;
|
|
138
|
+
function imagensNoTexto(texto) {
|
|
139
|
+
const fs = require('fs'), os = require('os');
|
|
140
|
+
const t = String(texto || '');
|
|
141
|
+
const achadas = [];
|
|
142
|
+
let m;
|
|
143
|
+
while ((m = RE_EXT_IMG.exec(t))) {
|
|
144
|
+
const fim = m.index + m[0].length;
|
|
145
|
+
// Do fim da extensão para trás: o começo é uma "/" (ou "~/", "file://") no início ou depois de espaço/aspas.
|
|
146
|
+
let melhor = null;
|
|
147
|
+
for (let i = m.index; i >= 0; i--) {
|
|
148
|
+
if (t[i] !== '/' && !(t[i] === '~' && t[i + 1] === '/')) continue;
|
|
149
|
+
const antes = i === 0 ? ' ' : t[i - 1];
|
|
150
|
+
if (!/[\s'"(:]/.test(antes) && !(t.slice(Math.max(0, i - 7), i) === 'file://')) continue;
|
|
151
|
+
const bruto = t.slice(i, fim).replace(/\\(.)/g, '$1').replace(/^~(?=\/)/, os.homedir());
|
|
152
|
+
try { if (fs.statSync(bruto).isFile()) melhor = bruto; } catch { /* não é este começo */ }
|
|
153
|
+
}
|
|
154
|
+
if (melhor && !achadas.includes(melhor)) achadas.push(melhor);
|
|
155
|
+
}
|
|
156
|
+
return achadas.slice(0, 6);
|
|
157
|
+
}
|
|
158
|
+
function guardarImagens(lista) { IMAGENS = Array.isArray(lista) ? lista.slice(0, 6) : []; return IMAGENS; }
|
|
159
|
+
function imagens() { return IMAGENS; }
|
|
160
|
+
function instrucaoDasImagens(lista = IMAGENS) {
|
|
161
|
+
if (!lista.length) return '';
|
|
162
|
+
return `[REFERÊNCIA VISUAL] A pessoa mandou ${lista.length} imagem(ns): ${lista.map((f) => require('path').basename(f)).join(', ')}. `
|
|
163
|
+
+ 'Elas SÃO a referência do pedido, mesmo em "só motion". Se o pedido é vídeo, o Estúdio já guarda cada uma no projeto e tira as '
|
|
164
|
+
+ 'cores dela (com site, as cores do site mandam). Escreva o roteiro seguindo o que a referência mostra e diga no fim, '
|
|
165
|
+
+ 'em uma linha, o que dela o vídeo usou.';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { definir, atual, limpar, semImagem, termos, entender, inferir, instrucao, dominios, filtrar,
|
|
169
|
+
imagensNoTexto, guardarImagens, imagens, instrucaoDasImagens };
|
package/lib/pre-leitura.js
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
const fs = require('fs');
|
|
33
33
|
const path = require('path');
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// 2,5 KB no servidor (13/10): 6 KB de arquivo pré-lido não cabiam no orçamento do plano grátis e empurravam a conversa para fora.
|
|
36
|
+
const ORCAMENTO_SERVIDOR = 2.5 * 1024;
|
|
36
37
|
const ORCAMENTO_LOCAL = 40 * 1024;
|
|
37
38
|
const MAIOR_ARQUIVO = 20 * 1024;
|
|
38
39
|
|
package/lib/tools.js
CHANGED
|
@@ -630,8 +630,15 @@ async function despachar(name, args, ctx) {
|
|
|
630
630
|
return editFile(args, projectDir);
|
|
631
631
|
case 'read_file':
|
|
632
632
|
return readFile(args, projectDir);
|
|
633
|
-
case 'list_dir':
|
|
634
|
-
|
|
633
|
+
case 'list_dir': {
|
|
634
|
+
/* No máximo 60 linhas (13/10: "continua listando coisas desnecessárias").
|
|
635
|
+
Pasta grande listada inteira enche a conversa e não diz onde está o que importa. */
|
|
636
|
+
const arvore = String(treeDir(resolverParaLer(args.path, projectDir), args.depth || 2));
|
|
637
|
+
const ls = arvore.split('\n');
|
|
638
|
+
if (ls.length <= 60) return { ok: true, tree: arvore };
|
|
639
|
+
return { ok: true, tree: ls.slice(0, 60).join('\n'), cortado: ls.length - 60,
|
|
640
|
+
dica: 'Lista grande, cortei. Não liste de novo: use glob com o padrão do que procura (ex.: "src/**/*.css") ou grep pelo nome.' };
|
|
641
|
+
}
|
|
635
642
|
/* `pasta` (opcional): procurar numa pasta que o usuário citou, sem
|
|
636
643
|
trocar a de trabalho. Os caminhos voltam relativos a ELA, e a
|
|
637
644
|
resposta diz qual é. */
|
package/lib/travado.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* vezes sem escrita no meio? → travado
|
|
16
16
|
* 3. VOLTAS_SEM_NOVIDADE voltas seguidas sem escrever nada e sem nenhuma
|
|
17
17
|
* chamada que ainda não tinha sido feita? → travado
|
|
18
|
+
* 4. LIMITE_OLHAR leituras, listagens e buscas DIFERENTES seguidas, sem
|
|
19
|
+
* nenhuma ação no meio (escrever, rodar, usar o Estúdio)? → travado
|
|
20
|
+
* (13/10: "continua lendo e listando coisas desnecessárias". As regras
|
|
21
|
+
* 1 a 3 só pegam repetição: ler 20 arquivos diferentes nunca travava.)
|
|
18
22
|
* Na primeira vez, um empurrão curto (o que está repetindo, o que fazer). Se
|
|
19
23
|
* travar de novo depois do empurrão, PARA e diz por quê.
|
|
20
24
|
*
|
|
@@ -28,10 +32,14 @@ const path = require('path');
|
|
|
28
32
|
const LIMITE_RELEITURA = 3;
|
|
29
33
|
const LIMITE_MESMA_CHAMADA = 4;
|
|
30
34
|
const VOLTAS_SEM_NOVIDADE = 6;
|
|
35
|
+
const LIMITE_OLHAR = 8;
|
|
36
|
+
const OLHAR = new Set(['read_file', 'list_dir', 'glob', 'grep']);
|
|
37
|
+
// O shell que só olha: listar, achar, procurar, mostrar arquivo.
|
|
38
|
+
const RE_SHELL_OLHA = /^(?:cd\s+\S+\s*&&\s*)?(?:ls|find|grep|rg|cat|head|tail|sed\s+-n|tree|wc|nl|less|more|stat|file|du)\b/;
|
|
31
39
|
const ESCRITAS = new Set(['write_file', 'edit_file', 'multi_edit']);
|
|
32
40
|
|
|
33
41
|
function novo() {
|
|
34
|
-
return { leituras: new Map(), chamadas: new Map(), vistas: new Set(), novidade: false, voltasSemNovidade: 0, avisos: 0 };
|
|
42
|
+
return { leituras: new Map(), chamadas: new Map(), vistas: new Set(), novidade: false, voltasSemNovidade: 0, avisos: 0, olhou: [], limiteOlhar: LIMITE_OLHAR };
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/* A faixa que a leitura cobre. Sem offset nem limit, o read_file devolve o
|
|
@@ -89,6 +97,19 @@ function observar(estado, nome, args, pasta) {
|
|
|
89
97
|
estado.novidade = true;
|
|
90
98
|
}
|
|
91
99
|
if (!estado.vistas.has(chave)) { estado.vistas.add(chave); estado.novidade = true; }
|
|
100
|
+
// Regra 4: o que só olha soma; qualquer ação zera.
|
|
101
|
+
const soOlha = OLHAR.has(nome) || (nome === 'run_shell' && RE_SHELL_OLHA.test(String(a.command || '').trim()));
|
|
102
|
+
if (!soOlha) { estado.olhou = []; estado.limiteOlhar = LIMITE_OLHAR; }
|
|
103
|
+
else {
|
|
104
|
+
const alvo = a.path || a.pattern || a.query || a.command || '.';
|
|
105
|
+
const curto = pasta && a.path ? path.relative(pasta, caminhoDe(a.path, pasta)) || '.' : String(alvo).slice(0, 40);
|
|
106
|
+
estado.olhou.push(`${nome === 'run_shell' ? '$ ' : ''}${curto}`);
|
|
107
|
+
if (estado.olhou.length >= estado.limiteOlhar) {
|
|
108
|
+
// A segunda vez vem 4 olhadas depois: o empurrão dá espaço para o último passo, não para mais um passeio.
|
|
109
|
+
estado.limiteOlhar = estado.olhou.length + 4;
|
|
110
|
+
return `olhou ${estado.olhou.length} coisas seguidas sem fazer nada (${estado.olhou.slice(-6).join(', ')})`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
92
113
|
const leitura = nome === 'read_file' ? a : nome === 'run_shell' ? leituraDoShell(a.command) : null;
|
|
93
114
|
if (leitura && leitura.path) {
|
|
94
115
|
const caminho = caminhoDe(leitura.path, pasta);
|
|
@@ -138,4 +159,4 @@ function decidir(estado, motivo) {
|
|
|
138
159
|
};
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
module.exports = { LIMITE_RELEITURA, LIMITE_MESMA_CHAMADA, VOLTAS_SEM_NOVIDADE, LEITURA_PADRAO, novo, faixa, leituraDoShell, observar, fimDaVolta, decidir };
|
|
162
|
+
module.exports = { LIMITE_RELEITURA, LIMITE_MESMA_CHAMADA, VOLTAS_SEM_NOVIDADE, LIMITE_OLHAR, LEITURA_PADRAO, novo, faixa, leituraDoShell, observar, fimDaVolta, decidir };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.9.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/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/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|