primocode 10.8.0 → 10.9.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/bin/primocode.js +13 -4
- package/lib/act.js +36 -7
- package/lib/entrada.js +87 -9
- package/lib/estudio/estilo.js +23 -1
- package/lib/estudio/gerativo.js +74 -16
- package/lib/estudio/index.js +68 -5
- package/lib/estudio/kit/gerativo.jsx +9 -4
- package/lib/estudio/marca.js +116 -1
- package/lib/estudio/personagem.js +31 -5
- 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 +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.
|
|
1
|
+
# PrimoCode v10.9.1
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.9.1
|
|
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
|
@@ -684,8 +684,20 @@ const RITMO_AGENTE = '[ritmo]\n'
|
|
|
684
684
|
|
|
685
685
|
/* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
|
|
686
686
|
entrar já lido (ver lib/pre-leitura.js). */
|
|
687
|
+
/** O agente quer o editor de gravação (`primocode video`) num pedido que não é editar gravação? Puro. */
|
|
688
|
+
function videoForaDeEdicao(nome, args, ehEdicao) {
|
|
689
|
+
return nome === 'run_shell' && !ehEdicao && /^\s*(?:cd\s+\S+\s*&&\s*)?(?:npx\s+)?primocode\s+video\b/.test(String((args || {}).command || ''));
|
|
690
|
+
}
|
|
691
|
+
|
|
687
692
|
function buildMemoryContext(projectDir, projectName, pedido = '', { memoriaDaPasta = true } = {}) {
|
|
688
693
|
const parts = [];
|
|
694
|
+
/* NO SERVIDOR O CONTEXTO É CURTO (13/10: "continua lendo e listando
|
|
695
|
+
coisas desnecessárias"). O plano grátis guarda ~1.900 tokens de
|
|
696
|
+
conversa; 3.400 de contexto fixo empurravam para fora o que o modelo
|
|
697
|
+
acabou de ler, e ele lia de novo. Com provedor próprio (/api) nada muda. */
|
|
698
|
+
let proprio = false;
|
|
699
|
+
try { proprio = !!require('./local/provedor.js').atual(); } catch { /* sem provedor próprio */ }
|
|
700
|
+
const curto = (t) => (proprio || t.length <= 1500 ? t : '…' + t.slice(-1500));
|
|
689
701
|
// As instruções vêm primeiro: são regra de quem manda no projeto, e a
|
|
690
702
|
// memória é o que o agente anotou. Entram aqui (e não no bin) para valer
|
|
691
703
|
// igual no terminal, no modo voz e em cada passo de um plano.
|
|
@@ -697,14 +709,14 @@ function buildMemoryContext(projectDir, projectName, pedido = '', { memoriaDaPas
|
|
|
697
709
|
bin), ela não vem de novo. */
|
|
698
710
|
if (memoriaDaPasta) {
|
|
699
711
|
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}`);
|
|
712
|
+
if (daPasta) parts.push(`[AGENTS.md — memória desta pasta: de onde a conversa parou]\n${curto(daPasta)}`);
|
|
701
713
|
}
|
|
702
714
|
const geral = cfg.readGeneralMemory();
|
|
703
|
-
if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
|
|
715
|
+
if (geral.trim()) parts.push(`[memory-geral.md]\n${curto(geral)}`);
|
|
704
716
|
const projeto = cfg.readProjectMemory(projectDir);
|
|
705
|
-
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
|
|
717
|
+
if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${curto(projeto)}`);
|
|
706
718
|
const arquivos = arquivosDoProjeto(projectDir);
|
|
707
|
-
const mapa = mapaDoProjeto(projectDir, 80, arquivos);
|
|
719
|
+
const mapa = mapaDoProjeto(projectDir, proprio ? 80 : 30, arquivos);
|
|
708
720
|
if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
|
|
709
721
|
const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
|
|
710
722
|
if (lidos) parts.push(lidos);
|
|
@@ -860,6 +872,10 @@ async function runAct(userPrompt, opts) {
|
|
|
860
872
|
* lib/skills.js, que também explica por que mandar todas em todo pedido
|
|
861
873
|
* não é uma opção num plano limitado por token. */
|
|
862
874
|
let pedido = userPrompt;
|
|
875
|
+
/* Editar a GRAVAÇÃO da pessoa? Decide pelo que ELA escreveu (userPrompt),
|
|
876
|
+
não pelo `pedido` já com skills e instruções na frente: uma skill com
|
|
877
|
+
"editar… o vídeo" no corpo bastava para virar edição. */
|
|
878
|
+
const ehEdicaoDaGravacao = require('./estudio/diretor.js').precisaDeEdicao(userPrompt);
|
|
863
879
|
if (!subagent) {
|
|
864
880
|
const comSkills = skills.cardapio(userPrompt);
|
|
865
881
|
pedido = comSkills.texto;
|
|
@@ -914,8 +930,8 @@ async function runAct(userPrompt, opts) {
|
|
|
914
930
|
// Vídeo que o Estúdio fez se edita no Remotion Studio, não no editor por linha do tempo.
|
|
915
931
|
const EDITAR_DO_ESTUDIO = 'SE ELA QUER EDITAR UM VÍDEO QUE O ESTÚDIO FEZ (não uma gravação dela): '
|
|
916
932
|
+ 'estudio_projeto acao:"editar" com o id do projeto abre o Remotion Studio no navegador; diga a pasta a ela e pare.\n';
|
|
917
|
-
const producao = diretor.precisaDeProducao(
|
|
918
|
-
if (
|
|
933
|
+
const producao = diretor.precisaDeProducao(userPrompt);
|
|
934
|
+
if (ehEdicaoDaGravacao) {
|
|
919
935
|
// A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
|
|
920
936
|
messages.push({ role: 'system', content: EDITAR_DO_ESTUDIO + 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
921
937
|
+ 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
|
|
@@ -1474,6 +1490,19 @@ async function runAct(userPrompt, opts) {
|
|
|
1474
1490
|
/* Só conta o que passou do freio de repetição: a chamada idêntica
|
|
1475
1491
|
já é barrada lá (e a rodada parada desiste sozinha). Aqui entra
|
|
1476
1492
|
o que escapa dele — a fatia sobreposta, a volta sem nada novo. */
|
|
1493
|
+
/* VÍDEO SE FAZ NO ESTÚDIO (13/10). Pedida uma animação 3D, o modelo
|
|
1494
|
+
do servidor rodou `primocode video do` e `primocode video help`
|
|
1495
|
+
pelo shell cinco vezes até o freio parar — o editor por linha do
|
|
1496
|
+
tempo é para a GRAVAÇÃO da pessoa, e as ferramentas do Estúdio
|
|
1497
|
+
estavam ali. Fora de edição, a chamada volta com o caminho certo. */
|
|
1498
|
+
if (videoForaDeEdicao(tc.name, tc.arguments, ehEdicaoDaGravacao)) {
|
|
1499
|
+
anotar(tc, 'recusada', 'primocode video fora de edição de gravação');
|
|
1500
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false,
|
|
1501
|
+
error: '`primocode video` é só para editar uma GRAVAÇÃO da pessoa. Para criar vídeo, animação ou motion: '
|
|
1502
|
+
+ 'estudio_projeto acao:"criar" → estudio_plano com o roteiro → estudio_render.' }) });
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1477
1506
|
motivoTravou = motivoTravou || travado.observar(circulo, tc.name, tc.arguments, pastaAtual);
|
|
1478
1507
|
|
|
1479
1508
|
if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
|
|
@@ -1955,5 +1984,5 @@ async function rodarPipeline(args, ctx) {
|
|
|
1955
1984
|
});
|
|
1956
1985
|
}
|
|
1957
1986
|
|
|
1958
|
-
module.exports = { runAct, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1987
|
+
module.exports = { runAct, videoForaDeEdicao, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
1959
1988
|
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
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 };
|
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,7 @@ 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');
|
|
50
51
|
const conferencia = require('./conferencia.js');
|
|
51
52
|
const margem = require('./margem.js');
|
|
52
53
|
const assinatura = require('./assinatura.js');
|
|
@@ -510,6 +511,15 @@ async function ferramentaMidia(args) {
|
|
|
510
511
|
|
|
511
512
|
/* O ELEMENTO do site, tirado pelo código (print-site.js): a lista das peças
|
|
512
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
|
+
|
|
513
523
|
if (acao === 'elementos') return printSite.elementosDoSite(args.url || args.site || args.termo, { celular: !!args.celular });
|
|
514
524
|
if (acao === 'elemento') {
|
|
515
525
|
if (!p) return { ok: false, error: 'para tirar o elemento é preciso dizer em qual projeto (id).' };
|
|
@@ -975,13 +985,19 @@ function vazamentoNoComeco(lista, palavras, direcao = '') {
|
|
|
975
985
|
const roteiro = norm(lista.slice(0, 2).map((x) => x.texto).join(' ')).split(' ').filter(Boolean);
|
|
976
986
|
if (roteiro.length < 2 || !palavras.length) return 0;
|
|
977
987
|
const pares = new Set(roteiro.slice(0, -1).map((w, i) => w + ' ' + roteiro[i + 1]));
|
|
978
|
-
|
|
988
|
+
/* 40 palavras, não 14 (13/10): a direção do personagem tem ~20 palavras, e
|
|
989
|
+
o roteiro só começava depois da janela — o corte desistia e o vídeo saía
|
|
990
|
+
com "Natural and close. Never like an announcer." na legenda. */
|
|
991
|
+
const ditas = palavras.slice(0, 40).map((w) => norm(w.texto));
|
|
979
992
|
let i = 0;
|
|
980
993
|
while (i < ditas.length - 1 && !pares.has(ditas[i] + ' ' + ditas[i + 1])) i++;
|
|
981
994
|
if (i === 0 || i >= ditas.length - 1) return 0;
|
|
982
995
|
const daDirecao = new Set(norm(direcao).split(' ').filter(Boolean));
|
|
983
996
|
const antes = ditas.slice(0, i).join(' ').split(' ').filter(Boolean);
|
|
984
|
-
|
|
997
|
+
/* A transcrição entorta a direção ("Read aloud in Brazilian Portuguese" vira
|
|
998
|
+
"Lê a alta em Brasília e Português"): basta 35% das palavras, ou 3, serem dela. */
|
|
999
|
+
const daDir = antes.filter((w) => daDirecao.has(w)).length;
|
|
1000
|
+
const vazou = !direcao || daDir / antes.length >= 0.35 || daDir >= 3;
|
|
985
1001
|
return vazou ? Math.max(0, palavras[i].de - 0.08) : 0;
|
|
986
1002
|
}
|
|
987
1003
|
|
|
@@ -1131,7 +1147,7 @@ async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
|
1131
1147
|
/* 'v3' (11/10): as narrações guardadas com a direção antiga (a que tinha
|
|
1132
1148
|
"RÁPIDO" e "contínuas") ficam para trás — reaproveitar uma delas trazia o
|
|
1133
1149
|
"Rápido e contínuo." de volta mesmo com a direção nova. */
|
|
1134
|
-
const chave = require('crypto').createHash('sha1').update(JSON.stringify(['
|
|
1150
|
+
const chave = require('crypto').createHash('sha1').update(JSON.stringify(['v4', lista.map((x) => x.texto), voz || '', direcao || ''])).digest('hex').slice(0, 16);
|
|
1135
1151
|
const dir = path.join(os.homedir(), '.primocode', 'studio', 'cache-voz');
|
|
1136
1152
|
const cm = path.join(dir, chave + '.mp3'), cj = path.join(dir, chave + '.json');
|
|
1137
1153
|
if (fs.existsSync(cm) && fs.existsSync(cj)) { fs.copyFileSync(cm, mp3); return { ok: true, ...JSON.parse(fs.readFileSync(cj, 'utf8')) }; }
|
|
@@ -1248,6 +1264,32 @@ function conferirAssinatura(p, r, nome) {
|
|
|
1248
1264
|
projeto.anotar(p.id, 'assinatura', fora.length ? `fora da assinatura ${nome}: ${fora.join('; ')}` : `seguiu a assinatura ${nome}`);
|
|
1249
1265
|
}
|
|
1250
1266
|
|
|
1267
|
+
/* A REFERÊNCIA VISUAL que a pessoa manda (13/10: "não usa as referências
|
|
1268
|
+
que mandei" — era o print da landing, arrastado no pedido). O agente não
|
|
1269
|
+
enxerga imagem; aqui ela entra no projeto, as cores saem pelo ffmpeg e, se o
|
|
1270
|
+
projeto ainda não tem marca, viram a marca. Com site, o site manda. */
|
|
1271
|
+
function importarReferencia(p, arquivo) {
|
|
1272
|
+
const orig = String(arquivo || '').trim().replace(/^file:\/\//, '');
|
|
1273
|
+
if (!orig || !fs.existsSync(orig)) return { ok: false, error: 'mande em "arquivo" o caminho da imagem de referência.' };
|
|
1274
|
+
const nome = path.basename(orig).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^\w.-]+/g, '-');
|
|
1275
|
+
const destino = path.join(p.pasta, 'midia', 'referencias', nome);
|
|
1276
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
1277
|
+
fs.copyFileSync(orig, destino);
|
|
1278
|
+
const rel = path.relative(p.pasta, destino).replace(/\\/g, '/');
|
|
1279
|
+
const cores = marcaLib.coresDaImagem(destino);
|
|
1280
|
+
const temMarca = !!(p.marca && Object.keys(marcaLib.marcaEmObjeto(p.marca).cores || {}).length);
|
|
1281
|
+
const salvar = { referencias: [...(p.referencias || []).filter((x) => x.src !== rel), { src: rel, cores, de: orig }] };
|
|
1282
|
+
if (!temMarca && cores.length) salvar.marca = { ...(p.marca || {}), ...marcaLib.marcaEmObjeto({ cores }) };
|
|
1283
|
+
projeto.salvar(p.id, salvar);
|
|
1284
|
+
projeto.anotar(p.id, 'referencia', `imagem ${rel}: ${cores.join(' ') || 'sem cor'}${temMarca ? '' : ' (virou a marca)'}`);
|
|
1285
|
+
return {
|
|
1286
|
+
ok: true, src: rel, cores, viraMarca: !temMarca && cores.length > 0,
|
|
1287
|
+
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). '
|
|
1288
|
+
: 'Estas cores viraram a marca do projeto. ')
|
|
1289
|
+
+ '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).',
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1251
1293
|
async function prepararRoteiro(p, args) {
|
|
1252
1294
|
const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
|
|
1253
1295
|
if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
|
|
@@ -1258,8 +1300,16 @@ async function prepararRoteiro(p, args) {
|
|
|
1258
1300
|
for (const pt of roteiro.partes || []) {
|
|
1259
1301
|
if (pt.src && !existe(pt.src)) { if (!pt.busca && /^https?:/i.test(pt.src)) pt.busca = pt.titulo || ''; delete pt.src; }
|
|
1260
1302
|
}
|
|
1303
|
+
// As imagens do pedido (o terminal guardou em referencias.imagens) entram sozinhas, uma vez por projeto.
|
|
1304
|
+
const jaTem = new Set((p.referencias || []).map((x) => x.de));
|
|
1305
|
+
const novas = referencias.imagens().filter((f) => !jaTem.has(f));
|
|
1306
|
+
for (const f of novas) { try { importarReferencia(p, f); } catch (e) { projeto.anotar(p.id, 'referencia', `não importou ${path.basename(f)}: ${e.message}`); } }
|
|
1307
|
+
if (novas.length) p = projeto.ler(p.id) || p;
|
|
1261
1308
|
if (roteiro.marca) {
|
|
1262
|
-
roteiro
|
|
1309
|
+
/* A do roteiro por cima da do site, CAMPO A CAMPO: antes o "cores" em
|
|
1310
|
+
lista do agente apagava o {fundo, destaque} lido do site (13/10). */
|
|
1311
|
+
const site = marcaLib.marcaEmObjeto(p.marca), dele = marcaLib.marcaEmObjeto(roteiro.marca);
|
|
1312
|
+
roteiro.marca = { ...site, ...dele, cores: { ...(site.cores || {}), ...(dele.cores || {}) }, fontes: { ...(site.fontes || {}), ...(dele.fontes || {}) } };
|
|
1263
1313
|
if (roteiro.marca.logo && !existe(roteiro.marca.logo)) roteiro.marca.logo = (p.marca && existe(p.marca.logo)) ? p.marca.logo : undefined;
|
|
1264
1314
|
} else if (p.marca) roteiro.marca = p.marca;
|
|
1265
1315
|
const partes = roteiro.partes || [];
|
|
@@ -1277,6 +1327,12 @@ async function prepararRoteiro(p, args) {
|
|
|
1277
1327
|
const semImagem = referencias.semImagem(ref);
|
|
1278
1328
|
let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
|
|
1279
1329
|
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
|
+
}
|
|
1280
1336
|
/* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
|
|
1281
1337
|
com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
|
|
1282
1338
|
o agente tenha escolhido outro estilo pelo nome. */
|
|
@@ -1848,6 +1904,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1848
1904
|
} catch { /* fica sem */ }
|
|
1849
1905
|
continue;
|
|
1850
1906
|
}
|
|
1907
|
+
// O ícone do próprio site (estudio_site guardou os <svg> na marca).
|
|
1908
|
+
const doSite = h.match(/^site:(\d+)$/i);
|
|
1909
|
+
if (doSite) { const x = ((roteiro.marca || {}).iconesSite || [])[Number(doSite[1]) - 1]; if (x && x.svg) icones[h] = x.svg; continue; }
|
|
1851
1910
|
if (!/^[a-z0-9-]+:[a-z0-9-]+$/i.test(h) || icones[h]) continue;
|
|
1852
1911
|
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 */ }
|
|
1853
1912
|
}
|
|
@@ -2235,6 +2294,9 @@ async function ferramentaSite(args) {
|
|
|
2235
2294
|
}
|
|
2236
2295
|
}
|
|
2237
2296
|
} catch { /* sem gradiente */ }
|
|
2297
|
+
/* Os ícones do próprio site (os <svg> dos cartões): viram "site:N" no roteiro. */
|
|
2298
|
+
const icSite = marcaLib.iconesDoSite(html);
|
|
2299
|
+
if (icSite.length) { marca.iconesSite = icSite; r.icones = icSite.map((x, i) => `site:${i + 1}${x.nome ? ' (' + x.nome + ')' : ''}`); }
|
|
2238
2300
|
projeto.salvar(args.id, { acervo: { fotos: a.fotos, videos: a.videos, fatos: a.fatos, site: r.url }, marca });
|
|
2239
2301
|
projeto.anotar(args.id, 'midia', `acervo do site: ${a.fotos.length} fotos, ${a.videos.length} vídeos, ${a.paginas.length} páginas lidas`);
|
|
2240
2302
|
let orcamento = 4200;
|
|
@@ -2248,7 +2310,8 @@ async function ferramentaSite(args) {
|
|
|
2248
2310
|
r.comoUsar = 'Escreva o roteiro com os FATOS acima (datas, local, números, frases da marca) — nada '
|
|
2249
2311
|
+ 'inventado. As fotos e os vídeos reais da marca entram SOZINHOS no vídeo: não precisa pôr '
|
|
2250
2312
|
+ '"busca" nas partes de imagem (use tipo "imagem" com só o título). Use as cores, a fonte e '
|
|
2251
|
-
+ 'a logo em roteiro.marca.'
|
|
2313
|
+
+ 'a logo em roteiro.marca (objeto: {cores: {fundo, destaque, tinta}, fontes: {titulo, texto}}).'
|
|
2314
|
+
+ (r.icones ? ' Os ícones do site estão em "icones": ponha icone:"site:N" na parte em que o assunto dele aparece.' : '');
|
|
2252
2315
|
} catch (e) {
|
|
2253
2316
|
r.acervo = { erro: e.message };
|
|
2254
2317
|
}
|
|
@@ -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'));
|
package/lib/estudio/marca.js
CHANGED
|
@@ -280,4 +280,119 @@ 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
|
+
/* Objeto com OUTROS nomes ({primaria, neutras: […]}, visto em 13/10): sem
|
|
297
|
+
fundo/destaque/tinta, todas as cores dele viram a lista e são lidas por
|
|
298
|
+
luz e saturação, como a lista do agente. */
|
|
299
|
+
const papeis = ['fundo', 'destaque', 'tinta', 'apoio'];
|
|
300
|
+
if (m.cores && typeof m.cores === 'object' && !Array.isArray(m.cores) && !papeis.some((k) => m.cores[k])) {
|
|
301
|
+
const todas = [];
|
|
302
|
+
const juntar = (v) => { if (Array.isArray(v)) v.forEach(juntar); else if (v && typeof v === 'object') Object.values(v).forEach(juntar); else if (typeof v === 'string') todas.push(v); };
|
|
303
|
+
juntar(m.cores);
|
|
304
|
+
m = { ...m, cores: todas };
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(m.cores)) {
|
|
307
|
+
const rgb = (h) => { const n = parseInt(h.slice(1), 16); return [n >> 16, (n >> 8) & 255, n & 255].map((v) => v / 255); };
|
|
308
|
+
const luz = (h) => { const [r, g, b] = rgb(h); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
|
|
309
|
+
const sat = (h) => { const v = rgb(h); return Math.max(...v) - Math.min(...v); };
|
|
310
|
+
const cs = m.cores.map((x) => String((x && x.cor) || x || '').trim()).filter((x) => /^#[0-9a-f]{6}$/i.test(x));
|
|
311
|
+
const vivas = cs.filter((x) => sat(x) > 0.25);
|
|
312
|
+
const fundo = cs.find((x) => luz(x) < 0.12 && sat(x) <= 0.25);
|
|
313
|
+
const tinta = cs.find((x) => luz(x) > 0.8 && sat(x) <= 0.25);
|
|
314
|
+
out.cores = { ...(fundo ? { fundo } : {}), ...(vivas[0] ? { destaque: vivas[0] } : {}), ...(vivas[1] ? { apoio: vivas[1] } : {}), ...(tinta ? { tinta } : {}) };
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(m.fontes)) {
|
|
317
|
+
const fs = m.fontes.map((f) => String((f && f.nome) || f || '').trim()).filter(Boolean);
|
|
318
|
+
const mono = fs.find((f) => /mono|code|consol/i.test(f));
|
|
319
|
+
const resto = fs.filter((f) => f !== mono);
|
|
320
|
+
out.fontes = { ...(resto[0] ? { titulo: resto[0] } : {}), ...(resto[1] ? { texto: resto[1] } : {}), ...(mono ? { mono } : {}) };
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* OS ÍCONES DO SITE (13/10: "não usa os ícones do site"). Os ícones de uma
|
|
326
|
+
landing quase sempre são <svg> inline (o desenho de traço ao lado de cada
|
|
327
|
+
cartão), e não arquivos. Aqui saem os pequenos — com viewBox, sem imagem
|
|
328
|
+
nem script dentro, até 4 KB — com um nome: o aria-label, o <title>, ou o
|
|
329
|
+
título (h2/h3/h4) logo depois dele no HTML. No roteiro viram "site:N". */
|
|
330
|
+
function iconesDoSite(html, max = 12) {
|
|
331
|
+
const out = [], vistos = new Set();
|
|
332
|
+
const re = /<svg\b[^>]*>[\s\S]*?<\/svg>/gi;
|
|
333
|
+
let m;
|
|
334
|
+
while ((m = re.exec(String(html || ''))) && out.length < max) {
|
|
335
|
+
const svg = m[0];
|
|
336
|
+
if (svg.length > 4000 || /<(image|script|foreignObject)\b/i.test(svg) || !/viewBox=/i.test(svg)) continue;
|
|
337
|
+
const abre = svg.slice(0, svg.indexOf('>') + 1);
|
|
338
|
+
const lado = Number((abre.match(/\b(?:width|height)=["']?(\d+)/i) || [])[1] || 0);
|
|
339
|
+
if (lado > 120) continue;
|
|
340
|
+
const corpo = svg.replace(/\s+/g, ' ');
|
|
341
|
+
if (vistos.has(corpo)) continue;
|
|
342
|
+
vistos.add(corpo);
|
|
343
|
+
const depois = String(html).slice(re.lastIndex, re.lastIndex + 500);
|
|
344
|
+
const nome = (abre.match(/aria-label=["']([^"']+)/i) || [])[1]
|
|
345
|
+
|| (svg.match(/<title>([^<]+)<\/title>/i) || [])[1]
|
|
346
|
+
|| ((depois.match(/<h[2-4][^>]*>([\s\S]*?)<\/h[2-4]>/i) || [])[1] || '').replace(/<[^>]+>/g, '').trim()
|
|
347
|
+
|| '';
|
|
348
|
+
out.push({ nome: nome.slice(0, 40), svg });
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* AS CORES DE UMA IMAGEM (a referência que a pessoa manda: o print do site,
|
|
354
|
+
um frame, um anúncio de que gostou). O ffmpeg reduz a imagem a 160 px e
|
|
355
|
+
devolve os pixels; as cores parecidas viram uma, e saem as 6 que mais
|
|
356
|
+
aparecem, da mais comum à menos. Sem ffmpeg, lista vazia. */
|
|
357
|
+
function coresDaImagem(arquivo, max = 6) {
|
|
358
|
+
const { spawnSync } = require('child_process');
|
|
359
|
+
const r = spawnSync('ffmpeg', ['-v', 'error', '-i', arquivo, '-vf', 'scale=160:-1', '-frames:v', '1', '-f', 'rawvideo', '-pix_fmt', 'rgb24', '-'], { maxBuffer: 1 << 22 });
|
|
360
|
+
if (r.status !== 0 || !r.stdout || !r.stdout.length) return [];
|
|
361
|
+
const conta = new Map();
|
|
362
|
+
for (let i = 0; i + 2 < r.stdout.length; i += 3) {
|
|
363
|
+
const k = ((r.stdout[i] >> 3) << 10) | ((r.stdout[i + 1] >> 3) << 5) | (r.stdout[i + 2] >> 3);
|
|
364
|
+
conta.set(k, (conta.get(k) || 0) + 1);
|
|
365
|
+
}
|
|
366
|
+
const hex = (k) => '#' + [(k >> 10) & 31, (k >> 5) & 31, k & 31].map((v) => (v * 8 + 4).toString(16).padStart(2, '0')).join('');
|
|
367
|
+
const rgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
368
|
+
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]); };
|
|
369
|
+
// As 4 mais comuns, e as 2 mais VIVAS que aparecem de verdade: a cor da
|
|
370
|
+
// marca costuma ocupar pouca área (o ciano de um título, o botão), e sem
|
|
371
|
+
// isto o print dava só fundo e cinza.
|
|
372
|
+
const total = r.stdout.length / 3;
|
|
373
|
+
const sat = (h) => { const v = rgb(h); return (Math.max(...v) - Math.min(...v)) / 255; };
|
|
374
|
+
const lista = [...conta].sort((a, b) => b[1] - a[1]).map(([k, n]) => ({ h: hex(k), n }));
|
|
375
|
+
const out = [];
|
|
376
|
+
const por = (x) => { if (out.length < max && out.every((o) => longe(o, x.h) > 48)) out.push(x.h); };
|
|
377
|
+
for (const x of lista) { if (out.length >= Math.max(1, max - 2)) break; por(x); }
|
|
378
|
+
/* As vivas se espalham em dezenas de tons de 1 ou 2 pixels (a borda da
|
|
379
|
+
letra mistura com o fundo): contam por MATIZ, em faixas de 30°, e de
|
|
380
|
+
cada faixa vale o pixel mais vivo — o miolo da letra, não a borda. */
|
|
381
|
+
const faixas = new Map();
|
|
382
|
+
for (let i = 0; i + 2 < r.stdout.length; i += 3) {
|
|
383
|
+
const v = [r.stdout[i], r.stdout[i + 1], r.stdout[i + 2]], mx = Math.max(...v), mn = Math.min(...v), d = mx - mn;
|
|
384
|
+
if (d / 255 < 0.35 || mx < 60) continue;
|
|
385
|
+
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;
|
|
386
|
+
const f = Math.floor(((hh * 60 + 360) % 360) / 30);
|
|
387
|
+
const x = faixas.get(f) || { n: 0, melhor: null, nota: -1 };
|
|
388
|
+
x.n++;
|
|
389
|
+
const nota = d * mx;
|
|
390
|
+
if (nota > x.nota) { x.nota = nota; x.melhor = '#' + v.map((c) => c.toString(16).padStart(2, '0')).join(''); }
|
|
391
|
+
faixas.set(f, x);
|
|
392
|
+
}
|
|
393
|
+
[...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 }));
|
|
394
|
+
void sat; void lista;
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = { marcaDaEmpresa, candidatasALogo, coresDe, fontesDe, metas, atributo, marcaEmObjeto, iconesDoSite, coresDaImagem };
|
|
@@ -128,28 +128,54 @@ function generoDoApresentador(roteiro = {}) {
|
|
|
128
128
|
const CABELOS_F = ['longo']; // o coque e o cacheado, de frente, liam como homem (12/10); só quando pedidos
|
|
129
129
|
const CABELOS_M = ['curto', 'topete', 'careca', 'cacheado'];
|
|
130
130
|
|
|
131
|
+
/* A COR PELO NOME (13/10). O agente escreveu pele: "morena", corCabelo:
|
|
132
|
+
"castanho escuro", roupa: "moletom cinza" — e o three.js, sem entender o
|
|
133
|
+
nome, pinta de BRANCO: o humano saiu todo branco, pele, cabelo e roupa.
|
|
134
|
+
Código de cor passa direto; nome em português vira o código pela tabela
|
|
135
|
+
(o nome mais longo primeiro: "castanho escuro" antes de "castanho"); o que
|
|
136
|
+
não se reconhece cai no sorteio de sempre. */
|
|
137
|
+
const CORES_POR_NOME = {
|
|
138
|
+
pele: [['muito clara', '#f6d7bd'], ['clara', '#f2c9a6'], ['branca', '#f2c9a6'], ['bronzeada', '#c08660'], ['parda', '#c98d66'],
|
|
139
|
+
['morena clara', '#c98d66'], ['morena escura', '#8a5a3c'], ['morena', '#a8704c'], ['oliva', '#c4a07a'], ['negra', '#6b4229'],
|
|
140
|
+
['preta', '#6b4229'], ['escura', '#6b4229'], ['media', '#c98d66']],
|
|
141
|
+
cabelo: [['castanho escuro', '#3b2618'], ['castanho claro', '#8a5a34'], ['castanho', '#6b4423'], ['preto', '#1d1612'], ['loiro', '#d8c39a'],
|
|
142
|
+
['ruivo', '#a4471f'], ['grisalho', '#8d8d93'], ['branco', '#e8e6e1'], ['cinza', '#8d8d93']],
|
|
143
|
+
cor: [['azul marinho', '#1f2a5c'], ['azul claro', '#6fa8ff'], ['cinza escuro', '#3a3d45'], ['cinza claro', '#c4c6cc'], ['cinza', '#8a8d96'],
|
|
144
|
+
['preto', '#24262d'], ['preta', '#24262d'], ['branco', '#e8e2d6'], ['branca', '#e8e2d6'], ['azul', '#2f4cdd'], ['verde', '#1f8a5b'],
|
|
145
|
+
['vermelho', '#c2452f'], ['vermelha', '#c2452f'], ['amarelo', '#f1b434'], ['amarela', '#f1b434'], ['laranja', '#d9532b'],
|
|
146
|
+
['roxo', '#7b5cff'], ['roxa', '#7b5cff'], ['rosa', '#e07aa0'], ['bege', '#d8c4a4'], ['marrom', '#6b4423'], ['vinho', '#7a1f2b'], ['dourado', '#c9a24a']],
|
|
147
|
+
};
|
|
148
|
+
function corEmHex(valor, tabela, sorteada) {
|
|
149
|
+
const v = String(valor == null ? '' : valor).trim();
|
|
150
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v)) return v;
|
|
151
|
+
const t = ' ' + v.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z ]+/g, ' ') + ' ';
|
|
152
|
+
for (const [nomeCor, hex] of CORES_POR_NOME[tabela] || []) if (t.includes(' ' + nomeCor + ' ')) return hex;
|
|
153
|
+
return sorteada;
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
function aparencia(nome, pedido = {}) {
|
|
132
157
|
const k = (s) => hashDe(nome + ':' + s);
|
|
133
158
|
const tipo = tipoDe(nome, pedido);
|
|
134
159
|
const rotulo = String(pedido.rotulo || pedido.nome || nome || '').trim().slice(0, 24);
|
|
135
160
|
if (tipo === 'animal') {
|
|
136
161
|
const especie = especieDe(pedido.especie || '') || especieDe(nome) || 'cachorro';
|
|
137
|
-
|
|
162
|
+
const cor = corEmHex(pedido.cor, 'cor', null), cor2 = corEmHex(pedido.cor2, 'cor', null);
|
|
163
|
+
return { nome, rotulo, tipo, especie, ...(cor ? { cor } : {}), ...(cor2 ? { cor2 } : {}) };
|
|
138
164
|
}
|
|
139
165
|
if (tipo === 'coisa') {
|
|
140
166
|
const termo = String(pedido.icone || pedido.coisa || nome);
|
|
141
167
|
return { nome, rotulo, tipo, icone: termo, ...(pedido.src ? { src: pedido.src } : {}) };
|
|
142
168
|
}
|
|
143
169
|
if (tipo === 'robo') {
|
|
144
|
-
return { nome, rotulo, tipo, cor: pedido.cor
|
|
170
|
+
return { nome, rotulo, tipo, cor: corEmHex(pedido.cor, 'cor', pegar(ROBOS, k('cor'))), olhos: pedido.olhos || null, ...(pedido.bigode ? { bigode: true } : {}) };
|
|
145
171
|
}
|
|
146
172
|
return {
|
|
147
173
|
nome, rotulo, tipo,
|
|
148
|
-
pele: pedido.pele
|
|
174
|
+
pele: corEmHex(pedido.pele, 'pele', pegar(PELES, k('pele'))),
|
|
149
175
|
cabelo: CABELOS.includes(pedido.cabelo) ? pedido.cabelo
|
|
150
176
|
: pegar(pedido.genero === 'f' ? CABELOS_F : pedido.genero === 'm' ? CABELOS_M : CABELOS, k('cabelo')),
|
|
151
|
-
corCabelo: pedido.corCabelo
|
|
152
|
-
roupa: pedido.roupa
|
|
177
|
+
corCabelo: corEmHex(pedido.corCabelo, 'cabelo', pegar(CORES_CABELO, k('corCabelo'))),
|
|
178
|
+
roupa: corEmHex(pedido.roupa, 'cor', pegar(ROUPAS, k('roupa'))),
|
|
153
179
|
oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
|
|
154
180
|
barba: !!pedido.barba && pedido.genero !== 'f', // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
|
|
155
181
|
bigode: !!pedido.bigode && pedido.genero !== 'f',
|
|
@@ -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.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": {
|