primocode 10.3.0-beta.0 → 10.4.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 +21 -2
- package/bin/primocode.js +63 -39
- package/lib/act.js +19 -5
- package/lib/catalogo.js +10 -4
- package/lib/entrada.js +113 -61
- package/lib/estudio/acervo.js +21 -1
- package/lib/estudio/estilo.js +12 -5
- package/lib/estudio/gerativo.js +18 -0
- package/lib/estudio/index.js +35 -3
- package/lib/estudio/kit/gerativo.jsx +62 -14
- package/lib/estudio/kit/index.jsx +2 -1
- package/lib/estudio/kit/personagem.jsx +582 -0
- package/lib/estudio/kit.js +3 -1
- package/lib/estudio/lancamento.js +5 -2
- package/lib/estudio/margem.js +235 -0
- package/lib/estudio/personagem.js +232 -0
- package/lib/estudio/print-site.js +190 -1
- package/lib/ui.js +70 -88
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.
|
|
1
|
+
# PrimoCode v10.4.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.4.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -17,6 +17,25 @@ está no limite de pedidos, o próximo atende. Você não vê o erro, e não pag
|
|
|
17
17
|
Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
|
|
18
18
|
veja `/mascot` e `/name`.
|
|
19
19
|
|
|
20
|
+
## O que mudou na 10.4
|
|
21
|
+
|
|
22
|
+
- **Vídeo gravado em 3D (`estilo: personagem`).** Personagens modelados — gente
|
|
23
|
+
ou robô — conversando num set (podcast, estúdio, escritório). A boca segue a
|
|
24
|
+
voz letra por letra, eles piscam, gesticulam e viram a cabeça para quem fala.
|
|
25
|
+
A câmera corta como num programa gravado: aberto, ombro, close e detalhe,
|
|
26
|
+
com o balanço de câmera na mão. Por cima: legenda grossa palavra por
|
|
27
|
+
palavra, o nome de quem fala, o cartão de destaque, o REC e o grão de filme.
|
|
28
|
+
Peça "um podcast", "uma entrevista" ou "o Claude falando em 3D".
|
|
29
|
+
- **A peça do site, tirada pelo código.** `estudio_midia acao:elementos` lê o
|
|
30
|
+
site e lista logo, menu, título, botões, cartões, preço; `acao:elemento` tira
|
|
31
|
+
uma delas sozinha, em PNG transparente no dobro da resolução, com o código
|
|
32
|
+
(HTML, fonte, cor, raio) para redesenhar se quiser.
|
|
33
|
+
- **Recorte de fundo com IA sem instalar nada.** Com o `uv` na máquina, o
|
|
34
|
+
`acao:recortar` usa o rembg num ambiente à parte, em vez do corte por cor.
|
|
35
|
+
- **O logo do Claude saiu do cardápio dos vídeos.**
|
|
36
|
+
- **Com Claude, Codex ou chave própria, o agente não esquece o arquivo que
|
|
37
|
+
acabou de ler** (antes relia o mesmo arquivo várias vezes).
|
|
38
|
+
|
|
20
39
|
## O que mudou na 10.0
|
|
21
40
|
|
|
22
41
|
- **A potência vem do sistema, não do modelo.** O mesmo pedido de landing
|
package/bin/primocode.js
CHANGED
|
@@ -427,30 +427,21 @@ function ramoDoGit(dir) {
|
|
|
427
427
|
return ramo;
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/* O que a caixa mostra além do texto, no desenho do OpenCode: DENTRO do
|
|
431
|
+
painel, o modo e o esforço ("Auto · alto" — o "Build · Nemotron" de lá);
|
|
432
|
+
FORA, uma linha só, com a pasta e o ramo à esquerda e o contexto e os
|
|
433
|
+
atalhos à direita. A cor da barra do painel é a do modo. */
|
|
434
|
+
const COR_MODO = { auto: '\x1b[38;5;75m', primocode: '\x1b[38;5;216m', chat: '\x1b[38;5;45m' };
|
|
435
|
+
const NOME_MODO = { auto: 'Auto', primocode: 'Primo Code', chat: 'Chat' };
|
|
436
|
+
|
|
430
437
|
function barraDeStatus() {
|
|
431
438
|
const partes = [];
|
|
432
439
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
vermelho a partir de 100%, que é quando `/compact` deixa de ser
|
|
437
|
-
conselho e vira o que fazer. */
|
|
438
|
-
const uso = memoria.usoDoContexto(state.memoria, state.history);
|
|
439
|
-
if (uso.pct >= 5) {
|
|
440
|
-
partes.push(uso.pct >= 100 ? c.err(`${uso.pct}% ctx`)
|
|
441
|
-
: uso.pct >= 70 ? c.warn(`${uso.pct}% ctx`)
|
|
442
|
-
: c.dim(`${uso.pct}% ctx`));
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// O esforço, que é o que escolhe o modelo. Não há provedor para mostrar:
|
|
446
|
-
// a API vem com o produto.
|
|
447
|
-
partes.push(c.muted(effort.normalizar(state.config.effort)));
|
|
448
|
-
|
|
449
|
-
// A PASTA. O nome dela, e o caminho curto quando não é um projeto — quem
|
|
450
|
-
// apontou com /dir para fora do ~/primocode precisa ver onde está.
|
|
440
|
+
// A PASTA, e o ramo do git colado nela ("~/primocode/loja:main"), como
|
|
441
|
+
// o rodapé do OpenCode. Quem apontou com /dir para fora do ~/primocode
|
|
442
|
+
// precisa ver onde está.
|
|
451
443
|
const dir = state.workDir || process.cwd();
|
|
452
444
|
partes.push(c.brandDim(shortPath(dir)));
|
|
453
|
-
|
|
454
445
|
const ramo = ramoDoGit(dir);
|
|
455
446
|
if (ramo) partes.push(c.dim('⎇ ' + ramo));
|
|
456
447
|
|
|
@@ -463,25 +454,43 @@ function barraDeStatus() {
|
|
|
463
454
|
esquece que deu — e ela vale até o PrimoCode fechar. */
|
|
464
455
|
if (state.controle && state.controle.liberado) partes.push(c.warn('controle do pc'));
|
|
465
456
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
457
|
+
/* O CONTEXTO. Só aparece depois que há conversa: um "ctx 0%" na abertura
|
|
458
|
+
é ruído, e ruído que aparece sempre é o que a pessoa aprende a não
|
|
459
|
+
enxergar — inclusive quando marcar 90%. Âmbar a partir de 70% e
|
|
460
|
+
vermelho a partir de 100%, que é quando `/compact` deixa de ser
|
|
461
|
+
conselho e vira o que fazer. */
|
|
462
|
+
const direita = [];
|
|
463
|
+
const uso = memoria.usoDoContexto(state.memoria, state.history);
|
|
464
|
+
if (uso.pct >= 5) {
|
|
465
|
+
direita.push(uso.pct >= 100 ? c.err(`${uso.pct}% ctx`)
|
|
466
|
+
: uso.pct >= 70 ? c.warn(`${uso.pct}% ctx`)
|
|
467
|
+
: c.dim(`${uso.pct}% ctx`));
|
|
475
468
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
469
|
+
direita.push(state.trabalhando
|
|
470
|
+
? c.warn('trabalhando') + c.dim(' · ') + c.white('esc esc') + c.dim(' para')
|
|
471
|
+
: c.white('tab') + c.dim(' modos ') + c.white('ctrl+p') + c.dim(' comandos'));
|
|
472
|
+
|
|
473
|
+
/* O modo DENTRO do painel. No auto, depois do primeiro pedido, ele diz
|
|
474
|
+
qual modo acabou respondendo — senão você nunca sabe quem respondeu. */
|
|
475
|
+
const cor = COR_MODO[state.mode] || COR_MODO.auto;
|
|
476
|
+
let modo = `${cor}${NOME_MODO[state.mode] || 'Auto'}${c.reset}`;
|
|
477
|
+
if (state.mode === 'auto' && state.modoEfetivo) {
|
|
478
|
+
modo += c.dim(' → ') + c.muted(NOME_MODO[state.modoEfetivo] || state.modoEfetivo);
|
|
479
|
+
}
|
|
480
|
+
modo += c.dim(' · ') + c.white(effort.normalizar(state.config.effort)) + ' ' + c.dim('PrimoCode');
|
|
479
481
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
: c.dim('/ comandos · ↑ histórico · \\ ou alt+enter quebra linha · Esc Esc interrompe');
|
|
482
|
+
return { modo, cor, esquerda: partes.join(c.dim(' · ')), direita: direita.join(c.dim(' ')) };
|
|
483
|
+
}
|
|
483
484
|
|
|
484
|
-
|
|
485
|
+
/* O Tab com a caixa vazia gira o modo, como o "tab agents" do OpenCode:
|
|
486
|
+
auto → primo code → chat → auto. A escolha fica salva, como a do /mode. */
|
|
487
|
+
function trocarModo() {
|
|
488
|
+
const ordem = ['auto', 'primocode', 'chat'];
|
|
489
|
+
const m = ordem[(ordem.indexOf(state.mode) + 1) % ordem.length];
|
|
490
|
+
state.mode = m;
|
|
491
|
+
state.modoEfetivo = null;
|
|
492
|
+
cfg.saveConfig({ mode: m });
|
|
493
|
+
refreshPrompt();
|
|
485
494
|
}
|
|
486
495
|
|
|
487
496
|
// A barra se redesenha sozinha a cada quadro; isto só pede um quadro novo.
|
|
@@ -1043,10 +1052,18 @@ async function sendPrompt(userText) {
|
|
|
1043
1052
|
|
|
1044
1053
|
// PRIMOCODE_TEMPOS=1: uma linha no fim com o custo de cada fase.
|
|
1045
1054
|
tempos.comecar();
|
|
1055
|
+
const inicio = Date.now();
|
|
1046
1056
|
try { await enviarPedido(userText); }
|
|
1047
1057
|
finally {
|
|
1048
1058
|
const linha = tempos.relatorio();
|
|
1049
1059
|
if (linha) console.log(c.dim(` ⏱ ${linha}`));
|
|
1060
|
+
/* O fecho de cada resposta, como no OpenCode ("▣ Build · Nemotron ·
|
|
1061
|
+
4.0s"): quem respondeu, com que esforço, e quanto levou. */
|
|
1062
|
+
const m = state.mode === 'auto' ? (state.modoEfetivo || 'auto') : state.mode;
|
|
1063
|
+
if (!state.semTerminal) {
|
|
1064
|
+
console.log('\n ' + `${COR_MODO[m] || COR_MODO.auto}▣${c.reset} ` + c.white(NOME_MODO[m] || m)
|
|
1065
|
+
+ c.dim(` · ${effort.normalizar(state.config.effort)} · ${((Date.now() - inicio) / 1000).toFixed(1)}s`) + '\n');
|
|
1066
|
+
}
|
|
1050
1067
|
}
|
|
1051
1068
|
}
|
|
1052
1069
|
|
|
@@ -2690,16 +2707,22 @@ async function main() {
|
|
|
2690
2707
|
process.exit(0);
|
|
2691
2708
|
}
|
|
2692
2709
|
|
|
2710
|
+
/* A abertura do OpenCode: o nome e a caixa no MEIO da tela, não no pé de
|
|
2711
|
+
um terminal cheio. Quebras de linha empurram o que havia para o
|
|
2712
|
+
histórico do terminal (nada se perde, dá para rolar), o cursor volta ao
|
|
2713
|
+
topo e desce até o ponto em que o logo e a caixa ficam centrados. */
|
|
2714
|
+
if (process.stdout.isTTY && process.stdout.rows >= 20) {
|
|
2715
|
+
const rows = process.stdout.rows;
|
|
2716
|
+
const alto = 8 + 6; // o logo com a versão + a caixa
|
|
2717
|
+
process.stdout.write('\n'.repeat(rows) + '\x1b[H' + '\n'.repeat(Math.max(0, Math.floor((rows - alto) / 2) - 1)));
|
|
2718
|
+
}
|
|
2693
2719
|
console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null, {
|
|
2694
|
-
modo: state.mode,
|
|
2695
|
-
mascot: state.config.mascot,
|
|
2696
|
-
userName: state.config.userName,
|
|
2697
2720
|
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
2698
2721
|
}));
|
|
2699
2722
|
rl = entrada.criar({
|
|
2700
2723
|
status: barraDeStatus,
|
|
2701
2724
|
comandos: comandosDoMenu,
|
|
2702
|
-
vazio:
|
|
2725
|
+
vazio: ui.convite(),
|
|
2703
2726
|
});
|
|
2704
2727
|
// O spinner desenha dentro da caixa (lib/entrada.js mostrarSpinner).
|
|
2705
2728
|
if (rl.tty) Spinner.caixa = rl;
|
|
@@ -2739,6 +2762,7 @@ async function main() {
|
|
|
2739
2762
|
// Ctrl+C com a caixa vazia: sair é sair, mas sem deixar o último turno
|
|
2740
2763
|
// pela metade — o sayBye espera a gravação subir.
|
|
2741
2764
|
rl.on('sigint', () => rl.close());
|
|
2765
|
+
rl.on('trocar-modo', trocarModo);
|
|
2742
2766
|
|
|
2743
2767
|
function sayBye() {
|
|
2744
2768
|
if (saidBye) return;
|
package/lib/act.js
CHANGED
|
@@ -333,6 +333,16 @@ function summarizeForModel(result) {
|
|
|
333
333
|
// que o modelo precisa para decidir o próximo passo; os velhos viram peso.
|
|
334
334
|
const RESULTADOS_INTEIROS = 3;
|
|
335
335
|
const RESTO_DO_RESULTADO = 300;
|
|
336
|
+
/* ── COM MOTOR PRÓPRIO, A MEMÓRIA NÃO É AMPUTADA ────────────────────────
|
|
337
|
+
* Os 3 e os 300 de cima são para o teto de ~6 mil tokens por minuto do
|
|
338
|
+
* servidor grátis. Com Claude Code, Codex ou chave própria (`provedor.atual()`)
|
|
339
|
+
* eles valiam igual, e o modelo perdia o arquivo que tinha acabado de ler:
|
|
340
|
+
* no jogo de carro de 09/10 o Claude leu o world.js (458 linhas) sete vezes,
|
|
341
|
+
* em fatias, porque a cada três ferramentas a leitura anterior virava 300
|
|
342
|
+
* caracteres. A pre-leitura já folgava com motor próprio; o compactar agora
|
|
343
|
+
* também. */
|
|
344
|
+
const RESULTADOS_INTEIROS_FOLGADO = 20;
|
|
345
|
+
const RESTO_DO_RESULTADO_FOLGADO = 1500;
|
|
336
346
|
// A pesquisa é MATÉRIA-PRIMA, não passo intermediário: é dela que saem os
|
|
337
347
|
// fatos do vídeo, do post, do slide. Ela sempre fica velha (pesquisa-se no
|
|
338
348
|
// começo, escreve-se no fim), e a 300 caracteres não sobrava fato nenhum — o
|
|
@@ -404,15 +414,17 @@ function encolherEscritas(m) {
|
|
|
404
414
|
* Nenhuma mensagem é REMOVIDA: a API exige que cada role:'tool' continue
|
|
405
415
|
* pareada com o tool_call que a pediu. Só o texto encolhe.
|
|
406
416
|
*/
|
|
407
|
-
function compactar(messages) {
|
|
417
|
+
function compactar(messages, { folgado = false } = {}) {
|
|
408
418
|
const { inteiras, duplicadas } = repeticao.referenciasNaConversa(messages);
|
|
419
|
+
const quantosInteiros = folgado ? RESULTADOS_INTEIROS_FOLGADO : RESULTADOS_INTEIROS;
|
|
420
|
+
const resto = folgado ? RESTO_DO_RESULTADO_FOLGADO : RESTO_DO_RESULTADO;
|
|
409
421
|
|
|
410
422
|
const indices = [];
|
|
411
423
|
for (let i = 0; i < messages.length; i++) if (messages[i].role === 'tool') indices.push(i);
|
|
412
|
-
if (indices.length <=
|
|
424
|
+
if (indices.length <= quantosInteiros && !duplicadas.size && !assistentesAntigas(messages).size) return messages;
|
|
413
425
|
|
|
414
426
|
const chamadas = repeticao.chamadasPorId(messages);
|
|
415
|
-
const encolher = new Set(indices.slice(0, -
|
|
427
|
+
const encolher = new Set(indices.slice(0, -quantosInteiros));
|
|
416
428
|
const antigas = assistentesAntigas(messages);
|
|
417
429
|
return messages.map((m, i) => {
|
|
418
430
|
if (antigas.has(i)) return encolherEscritas(m);
|
|
@@ -421,7 +433,7 @@ function compactar(messages) {
|
|
|
421
433
|
}
|
|
422
434
|
if (inteiras.has(i) || !encolher.has(i)) return m;
|
|
423
435
|
const chamada = chamadas.get(m.tool_call_id);
|
|
424
|
-
const limite = chamada && MATERIA_PRIMA.has(chamada.nome) ? RESTO_DA_PESQUISA :
|
|
436
|
+
const limite = chamada && MATERIA_PRIMA.has(chamada.nome) ? Math.max(RESTO_DA_PESQUISA, resto) : resto;
|
|
425
437
|
const texto = String(m.content || '');
|
|
426
438
|
if (texto.length <= limite) return m;
|
|
427
439
|
return { ...m, content: texto.slice(0, limite) + `…(resultado antigo encurtado; eram ${texto.length} caracteres)` };
|
|
@@ -984,7 +996,9 @@ async function runAct(userPrompt, opts) {
|
|
|
984
996
|
emVoo = new AbortController();
|
|
985
997
|
const tRodada = Date.now();
|
|
986
998
|
try {
|
|
987
|
-
|
|
999
|
+
let folgado = false;
|
|
1000
|
+
try { folgado = !!require('./local/provedor.js').atual(); } catch { /* sem provedor próprio */ }
|
|
1001
|
+
const enviadas = compactar(messages, { folgado });
|
|
988
1002
|
traco(`volta ${voltas}${subagent ? ' (agente)' : ''} · ${enviadas.length} mensagens · ${JSON.stringify(enviadas).length} caracteres`);
|
|
989
1003
|
await motor.requestAct(server, { messages: enviadas, model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
|
|
990
1004
|
if (evt.erro) { streamError = evt.erro; return; }
|
package/lib/catalogo.js
CHANGED
|
@@ -94,8 +94,8 @@ const GRUPOS = [
|
|
|
94
94
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
|
|
95
95
|
+ 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
|
|
96
96
|
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa|false: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — ponha só se o vídeo pede cara de peça de design; vazio, o PrimoCode alterna com memória e às vezes não põe; heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; procure com estudio_midia acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
97
|
-
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema. '
|
|
98
|
-
+ 'marca e lancamento aceitam visual:orbe|vetor|
|
|
97
|
+
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (o vídeo GRAVADO em 3D — cada parte é UMA fala {falante, texto, emocao?, destaque?}, sem tipo: personagens modelados — gente ou robô — conversando num set de podcast/estúdio/escritório, a boca segue a voz, gestos, e a câmera corta entre aberto, ombro e close; use quando o pedido fala em personagem, boneco, avatar, podcast, entrevista, ou alguém FALANDO em 3D. Com dois falantes (falante em cada parte) eles conversam; personagens:{"nome":{tipo:robo|pessoa, pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba, rotulo}} — nome de IA vira robô sozinho; emocao feliz|serio|surpreso|pensativo|animado|curioso e destaque:"texto curto" (o cartão na tela) em cada parte; cenario podcast|estudio|escritorio|noite). '
|
|
98
|
+
+ 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
99
99
|
+ 'voz (thalita|antonio|francisca). DUAS VOZES (o pedido fala em dois narradores, diálogo, conversa, entrevista): roteiro.vozes {"nome":"antonio","outro":"thalita"} e falante:"nome" em cada parte — cada um sai na sua voz. Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
100
100
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|
|
101
101
|
+ 'motivo (opcional): fica no caderno. voltarPara (opcional): nome de uma versão. '
|
|
@@ -104,13 +104,19 @@ const GRUPOS = [
|
|
|
104
104
|
+ '"anima": {propriedade: [{t,v,e}]} — quadros-chave em segundos. A cena tem "camera": {zoom,x,y,rotacao} e '
|
|
105
105
|
+ '"perspectiva" para 3D. Cena.transicao {tipo,duracao}: corte|esmaecer|deslizar|zoom|giro|mascara|morph|chicote|soco|flash|liquido, '
|
|
106
106
|
+ 'e as 3D virar|cubo|profundidade|inclinar (mexem nas duas cenas) — use 3D se o vídeo pede volume (produto, site, tecnologia); em editorial plano, prefira as planas. '
|
|
107
|
-
+ 'NÃO existe layout pronto nem tema — cada propriedade do vídeo é escolha sua, a cada vídeo.'
|
|
107
|
+
+ 'NÃO existe layout pronto nem tema — cada propriedade do vídeo é escolha sua, a cada vídeo. '
|
|
108
|
+
+ 'ÁREA SEGURA: nada a menos de 6% da largura e 7% da altura da borda, e 4% do lado curto entre dois elementos no ar juntos — '
|
|
109
|
+
+ 'o render empurra para dentro, encolhe o texto largo e afasta o colado; enfeite de fundo marque decorativo: true.' },
|
|
108
110
|
|
|
109
111
|
{ tool: 'estudio_midia', escreve: true, faz: 'procura e baixa ícone, imagem, música e efeito sonoro da internet, e lista as fontes',
|
|
110
|
-
args: 'acao: imagem | icones | som | trilha | efeito | fontes | print. termo. id (para baixar). '
|
|
112
|
+
args: 'acao: imagem | icones | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
|
|
111
113
|
+ 'print: url — tira o print REAL do site e devolve o src e um elemento {tipo:"kit", peca:"printsite"} pronto '
|
|
112
114
|
+ '(o site de verdade numa janela 3D que rola a página; celular: true para a tela do celular; inteira: false só a primeira dobra). '
|
|
113
115
|
+ 'Use SE o pedido trouxer um site e o vídeo for SOBRE ele (lançamento, tour, case); vídeo que só cita a marca não precisa. '
|
|
116
|
+
+ 'elementos: url — lê o CÓDIGO do site e lista as peças que dá para tirar (logo, menu, titulo, hero, botao, cartao, imagem, formulario, preco, rodape), cada uma com o seletor. '
|
|
117
|
+
+ 'elemento: url + seletor (ou texto: o que está escrito nele) — tira SÓ aquela peça, em PNG com fundo transparente e no dobro da resolução, e devolve o código dela (html e estilo: fonte, cor, raio, sombra) e um elemento {tipo:"imagem"} pronto; '
|
|
118
|
+
+ 'use para pôr o botão, o cartão de preço ou a logo do site animando no vídeo (ou redesenhe a peça como componente a partir do código). '
|
|
119
|
+
+ 'recortar: arquivo — tira o fundo de qualquer foto com IA (rembg), PNG transparente. '
|
|
114
120
|
+ 'efeitos: lista os componentes guardados (os menos usados primeiro). usar-efeito {nome}: copia para o projeto e devolve o elemento — use se um deles servir a ESTE roteiro. '
|
|
115
121
|
+ 'guardar-efeito {arquivo, nome, descricao}: guarda um componente que você escreveu e ficou bom, para os próximos vídeos '
|
|
116
122
|
+ '(o componente: export default de uma função React que recebe {el, t, fps}, num arquivo só, importando só react, remotion, @remotion/*, three). '
|
package/lib/entrada.js
CHANGED
|
@@ -47,7 +47,26 @@ const { c, visibleWidth, truncate } = require('./ui.js');
|
|
|
47
47
|
const RE_ETIQUETA = /\[Colado #\d+ · [^\]]+\]/g;
|
|
48
48
|
function pintarEtiquetas(s) { return String(s).replace(RE_ETIQUETA, (m) => c.brand(m) + '\x1b[38;5;254m'); }
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/* As cores do painel, no cubo de 256 cores. O fundo é um degrau acima do
|
|
51
|
+
preto do terminal (235); a barra é o azul do OpenCode; o escolhido do menu,
|
|
52
|
+
o pêssego dele com texto escuro. */
|
|
53
|
+
const COR = {
|
|
54
|
+
fundo: '\x1b[48;5;235m',
|
|
55
|
+
barra: '\x1b[38;5;75m',
|
|
56
|
+
warn: '\x1b[38;5;214m',
|
|
57
|
+
selFundo: '\x1b[48;5;216m',
|
|
58
|
+
selTexto: '\x1b[38;5;234m',
|
|
59
|
+
reset: '\x1b[0m',
|
|
60
|
+
};
|
|
61
|
+
/* Onde o texto começa: a barra, o espaço, e as duas colunas da marca ("$ "
|
|
62
|
+
no modo bash, vazias no normal). O cursor conta a partir daqui. */
|
|
63
|
+
const PREFIXO = 4;
|
|
64
|
+
|
|
65
|
+
/** Uma linha do painel: fundo até `w` colunas, religado depois de cada reset. */
|
|
66
|
+
function painel(s, w) {
|
|
67
|
+
const corpo = String(s).split(COR.reset).join(COR.reset + COR.fundo);
|
|
68
|
+
return COR.fundo + corpo + ' '.repeat(Math.max(0, w - visibleWidth(s))) + COR.reset;
|
|
69
|
+
}
|
|
51
70
|
|
|
52
71
|
/** Preenche até `w` colunas VISÍVEIS (códigos de cor não ocupam coluna). */
|
|
53
72
|
function encher(s, w) {
|
|
@@ -95,9 +114,13 @@ function quebrar(texto, w) {
|
|
|
95
114
|
*/
|
|
96
115
|
function montar(e) {
|
|
97
116
|
const colunas = Math.max(24, Math.min(e.colunas || 80, 200));
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
/* Na ABERTURA (nada mandado ainda) a caixa é estreita e centrada embaixo
|
|
118
|
+
do logo, como a home do OpenCode; depois do primeiro pedido ela ocupa a
|
|
119
|
+
largura toda. `recuo` é a margem da esquerda, que entra em toda linha. */
|
|
120
|
+
const largura = e.centro ? Math.min(colunas - 1, 80) : colunas - 1; // a última coluna
|
|
121
|
+
// fica livre: escrever nela faz o terminal quebrar a linha
|
|
122
|
+
const recuo = e.centro ? Math.max(0, Math.floor((colunas - largura) / 2)) : 0;
|
|
123
|
+
const w = Math.max(8, largura - PREFIXO);
|
|
101
124
|
|
|
102
125
|
const cursor = Math.max(0, Math.min(e.cursor | 0, String(e.texto).length));
|
|
103
126
|
const linhas = quebrar(e.texto, w);
|
|
@@ -112,41 +135,39 @@ function montar(e) {
|
|
|
112
135
|
if (cursor >= l.de && cursor <= fim) { cursorLinha = i; cursorColuna = cursor - l.de; }
|
|
113
136
|
}
|
|
114
137
|
|
|
115
|
-
/*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
/* O DESENHO DO OPENCODE, que é a referência pedida: um PAINEL — fundo um
|
|
139
|
+
tom mais claro que o terminal, uma barra grossa na esquerda na cor do
|
|
140
|
+
modo — e não réguas nem caixa de cantos. Dentro dele, o texto; uma
|
|
141
|
+
linha em branco; e embaixo, ainda dentro, o modo e o esforço ("Auto ·
|
|
142
|
+
alto"), que é o que muda o que acontece no Enter. Fora do painel, uma
|
|
143
|
+
linha só: a pasta à esquerda, os atalhos à direita.
|
|
144
|
+
|
|
145
|
+
O fundo vai até a última coluna útil em toda linha do painel. É ele que
|
|
146
|
+
desenha a caixa — sem borda nenhuma — e por isso cada `reset` de cor no
|
|
147
|
+
meio do texto tem de religar o fundo (ver `painel`). */
|
|
148
|
+
/* MODO BASH: a linha que começa com "!" roda no shell, e o painel muda de
|
|
149
|
+
cor para avisar. Um comando de shell faz coisa irreversível, e a
|
|
126
150
|
diferença entre "pedir ao agente" e "rodar direto" não pode depender de
|
|
127
|
-
a pessoa lembrar do que digitou
|
|
128
|
-
|
|
129
|
-
mesma cor que o resto do programa usa para "atenção, não é o caminho
|
|
130
|
-
comum". */
|
|
151
|
+
a pessoa lembrar do que digitou: ela tem de VER, enquanto digita, que
|
|
152
|
+
aquele Enter vai ao shell. Âmbar é o "atenção" do resto do programa. */
|
|
131
153
|
const bash = /^\s*!/.test(String(e.texto));
|
|
154
|
+
const st = Array.isArray(e.status) ? { linhas: e.status } : (e.status || {});
|
|
155
|
+
const corBarra = bash ? COR.warn : (st.cor || COR.barra);
|
|
156
|
+
const barra = `${corBarra}┃${COR.reset}`;
|
|
157
|
+
const linhaDoPainel = (conteudo) => painel(barra + conteudo, largura);
|
|
132
158
|
|
|
133
159
|
const out = [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
que espera a linha fechar para se redesenhar, ficava apagada enquanto
|
|
138
|
-
ele girasse: o que se digitava não aparecia, e a barra de status
|
|
139
|
-
sumia ("não consigo digitar enquanto ele trabalha"). */
|
|
160
|
+
/* O SPINNER MORA AQUI, na linha logo acima do painel. Ele escrevia direto
|
|
161
|
+
no terminal com "\r" a cada 80 ms — e a caixa, que espera a linha fechar
|
|
162
|
+
para se redesenhar, ficava apagada enquanto ele girasse. */
|
|
140
163
|
if (e.spinner) out.push(' ' + truncate(e.spinner, largura - 2));
|
|
141
|
-
|
|
164
|
+
|
|
165
|
+
out.push(linhaDoPainel('')); // o respiro de cima
|
|
142
166
|
|
|
143
167
|
const vazio = !String(e.texto).length;
|
|
144
168
|
linhas.forEach((l, i) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: c.dim('┆ ');
|
|
148
|
-
// O "!" some do texto desenhado: quem vê "$ " já sabe que é shell, e
|
|
149
|
-
// mostrar os dois é mostrar a mesma informação duas vezes.
|
|
169
|
+
// O "!" some do texto desenhado: quem vê "$ " já sabe que é shell.
|
|
170
|
+
const marca = i === 0 && bash ? c.warn('$ ') : ' ';
|
|
150
171
|
let conteudo = pintarEtiquetas(c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto));
|
|
151
172
|
// O fantasma só existe na última linha e só com o cursor no fim dela:
|
|
152
173
|
// desenhado no meio do texto, ele empurraria o que vem depois.
|
|
@@ -155,56 +176,71 @@ function montar(e) {
|
|
|
155
176
|
}
|
|
156
177
|
if (i === 0 && vazio && e.ocupado) conteudo = c.dim('mande um recado — ele entra na tarefa depois do passo atual');
|
|
157
178
|
else if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
|
|
158
|
-
|
|
159
|
-
// espaço sobrando só apagaria o que estivesse embaixo.
|
|
160
|
-
out.push(' ' + marca + conteudo);
|
|
179
|
+
out.push(linhaDoPainel(' ' + marca + truncate(conteudo, w)));
|
|
161
180
|
});
|
|
162
181
|
|
|
163
|
-
out.push(
|
|
182
|
+
out.push(linhaDoPainel(''));
|
|
183
|
+
// O modo e o esforço, dentro do painel — "Build · Nemotron" no OpenCode.
|
|
184
|
+
out.push(linhaDoPainel(' ' + truncate(bash
|
|
185
|
+
? c.warn('roda no shell') + c.dim(`, em ${e.pasta || 'a pasta atual'} · apague o ! para voltar ao normal`)
|
|
186
|
+
: (st.modo || c.dim('PrimoCode')), largura - 4)));
|
|
187
|
+
|
|
188
|
+
// ── a linha de baixo: pasta à esquerda, atalhos à direita ────────────
|
|
189
|
+
if (st.linhas) {
|
|
190
|
+
for (const linha of st.linhas) out.push(' ' + truncate(linha, largura - 2));
|
|
191
|
+
} else {
|
|
192
|
+
const esq = st.esquerda || '';
|
|
193
|
+
const dir = st.direita || '';
|
|
194
|
+
const sobra = largura - 2 - visibleWidth(esq) - visibleWidth(dir);
|
|
195
|
+
out.push(' ' + (sobra >= 2 ? esq + ' '.repeat(sobra) + dir : truncate(esq, largura - 2)));
|
|
196
|
+
}
|
|
164
197
|
|
|
165
198
|
// ── o menu dos comandos ──────────────────────────────────────────────
|
|
166
|
-
// Fica ABAIXO
|
|
167
|
-
// digitar para
|
|
168
|
-
// de lugar enquanto ela escreve.
|
|
199
|
+
// Fica ABAIXO de tudo, não em cima do painel: em cima, ele empurraria o
|
|
200
|
+
// campo de digitar para baixo a cada tecla, e o texto que a pessoa
|
|
201
|
+
// escreve saltaria de lugar enquanto ela escreve. O desenho é o do
|
|
202
|
+
// OpenCode: fundo de painel, e o escolhido numa faixa pêssego.
|
|
169
203
|
if (e.menu && e.menu.itens.length) {
|
|
170
204
|
const cabem = Math.max(1, Math.min(e.menu.itens.length, 8));
|
|
171
205
|
// A janela acompanha a seleção: descer na lista rola, em vez de o
|
|
172
206
|
// item escolhido sumir embaixo.
|
|
173
207
|
let topo = Math.max(0, Math.min(e.menu.sel - Math.floor(cabem / 2),
|
|
174
208
|
e.menu.itens.length - cabem));
|
|
209
|
+
const nomeW = 16;
|
|
175
210
|
for (let i = topo; i < topo + cabem; i++) {
|
|
176
211
|
const [nome, desc] = e.menu.itens[i];
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
+ c.dim(desc || '');
|
|
182
|
-
|
|
212
|
+
if (i === e.menu.sel) {
|
|
213
|
+
const corpo = ' ' + encher(nome, nomeW) + (desc || '');
|
|
214
|
+
out.push(`${COR.selFundo}${COR.selTexto}${encher(truncate(corpo, largura), largura)}${COR.reset}`);
|
|
215
|
+
} else {
|
|
216
|
+
out.push(painel(' ' + c.white(encher(nome, nomeW)) + c.dim(desc || ''), largura));
|
|
217
|
+
}
|
|
183
218
|
}
|
|
184
219
|
if (e.menu.itens.length > cabem) {
|
|
185
|
-
out.push('
|
|
220
|
+
out.push(painel(' ' + c.dim(`… e mais ${e.menu.itens.length - cabem}`), largura));
|
|
186
221
|
}
|
|
187
222
|
}
|
|
188
223
|
|
|
189
|
-
|
|
190
|
-
normal de propósito: no instante em que a pessoa vai rodar um comando,
|
|
191
|
-
o que importa não é lembrar os atalhos, é saber onde ele vai rodar. */
|
|
192
|
-
if (bash) {
|
|
193
|
-
out.push(' ' + c.warn('roda no shell') + c.dim(`, em ${e.pasta || 'a pasta atual'}`)
|
|
194
|
-
+ c.dim(' · apague o ! para voltar ao normal'));
|
|
195
|
-
}
|
|
196
|
-
for (const linha of (e.status || [])) out.push(' ' + linha);
|
|
197
|
-
|
|
224
|
+
const margem = ' '.repeat(recuo);
|
|
198
225
|
return {
|
|
199
|
-
linhas: out,
|
|
200
|
-
cursorLinha: 1 + cursorLinha,
|
|
226
|
+
linhas: recuo ? out.map((l) => margem + l) : out,
|
|
227
|
+
cursorLinha: (e.spinner ? 1 : 0) + 1 + cursorLinha, // +1 pelo respiro de cima
|
|
201
228
|
/* No modo bash o "!" não é desenhado, então tudo depois dele anda uma
|
|
202
229
|
coluna para a esquerda — e o cursor tem de andar junto, ou ele
|
|
203
230
|
aparece um caractere à frente de onde o texto realmente está. */
|
|
204
|
-
cursorColuna:
|
|
231
|
+
cursorColuna: recuo + PREFIXO + cursorColuna - (bash && cursorLinha === 0 ? 1 : 0),
|
|
205
232
|
};
|
|
206
233
|
}
|
|
207
234
|
|
|
235
|
+
/** O pedido que foi mandado, como fica no histórico da tela: um painel. */
|
|
236
|
+
function pedidoNaTela(texto, colunas) {
|
|
237
|
+
const largura = Math.max(24, Math.min(colunas || 80, 200)) - 1;
|
|
238
|
+
const barra = `${COR.barra}┃${COR.reset}`;
|
|
239
|
+
const linhas = quebrar(texto, Math.max(8, largura - PREFIXO))
|
|
240
|
+
.map((l) => painel(barra + ' ' + pintarEtiquetas(c.white(l.texto)), largura));
|
|
241
|
+
return [painel(barra, largura), ...linhas, painel(barra, largura)].join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
208
244
|
// ── A caixa viva ─────────────────────────────────────────────────────────
|
|
209
245
|
|
|
210
246
|
class Entrada extends EventEmitter {
|
|
@@ -416,11 +452,16 @@ class Entrada extends EventEmitter {
|
|
|
416
452
|
menu: this.menu,
|
|
417
453
|
ocupado: this.ocupado,
|
|
418
454
|
spinner: this.spinnerTexto || '',
|
|
455
|
+
centro: !this.mandou,
|
|
419
456
|
});
|
|
420
457
|
}
|
|
421
458
|
|
|
422
459
|
_desenhar() {
|
|
423
460
|
if (!this.tty || this.suspenso || this.fechado) return;
|
|
461
|
+
/* Já desenhada (um `prompt()` de quem não sabe que ela está no ar):
|
|
462
|
+
apaga antes. Sem isso o quadro novo começava na linha do cursor —
|
|
463
|
+
a do texto — e o respiro de cima do painel ficava dobrado. */
|
|
464
|
+
if (this.desenhado && !this._meiaLinha) this._apagar();
|
|
424
465
|
const q = this._quadro();
|
|
425
466
|
this._pintando = true;
|
|
426
467
|
/* Cursor no meio de uma linha (fluxo que ainda não fechou): desce uma
|
|
@@ -525,6 +566,9 @@ class Entrada extends EventEmitter {
|
|
|
525
566
|
|
|
526
567
|
case 'tab': {
|
|
527
568
|
if (this.menu) return this._escolherDoMenu();
|
|
569
|
+
/* Tab com a caixa vazia troca o modo — o "tab agents" do
|
|
570
|
+
OpenCode. Com texto, continua completando o comando. */
|
|
571
|
+
if (!this.line && !this.ocupado) { this.emit('trocar-modo'); return; }
|
|
528
572
|
const f = this._fantasma();
|
|
529
573
|
if (f) return this.write(f);
|
|
530
574
|
if (this.line.startsWith('/')) { this._filtrarMenu(); this._redesenhar(); }
|
|
@@ -605,6 +649,12 @@ class Entrada extends EventEmitter {
|
|
|
605
649
|
}
|
|
606
650
|
case 'l': this._apagar(); this.escreverCru('\x1b[2J\x1b[H'); return this._desenhar();
|
|
607
651
|
case 'j': return this._novaLinha();
|
|
652
|
+
// Ctrl+P abre os comandos, como no OpenCode: o mesmo menu do "/".
|
|
653
|
+
case 'p':
|
|
654
|
+
if (this.line && !this.line.startsWith('/')) return;
|
|
655
|
+
if (!this.line) { this.line = '/'; this.cursor = 1; }
|
|
656
|
+
this._filtrarMenu();
|
|
657
|
+
return this._redesenhar();
|
|
608
658
|
default: return;
|
|
609
659
|
}
|
|
610
660
|
}
|
|
@@ -660,10 +710,12 @@ class Entrada extends EventEmitter {
|
|
|
660
710
|
senão o pedido some no instante em que a resposta começa, e quem
|
|
661
711
|
rola para cima não acha o que perguntou. */
|
|
662
712
|
this._apagar();
|
|
713
|
+
if (texto.trim()) this.mandou = true; // a caixa sai do centro e ocupa a largura
|
|
663
714
|
if (texto.trim() && !oculto) {
|
|
664
|
-
|
|
665
|
-
//
|
|
666
|
-
|
|
715
|
+
// Na tela fica a etiqueta (o que se viu ao mandar); quem vai ao agente
|
|
716
|
+
// é o texto inteiro. O desenho é o do pedido no OpenCode: o mesmo
|
|
717
|
+
// painel da caixa, com a barra, e um respiro em cima e embaixo.
|
|
718
|
+
this.escreverCru(pedidoNaTela(visto, process.stdout.columns || 80) + '\n');
|
|
667
719
|
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
|
668
720
|
}
|
|
669
721
|
this._desenhar();
|
|
@@ -673,4 +725,4 @@ class Entrada extends EventEmitter {
|
|
|
673
725
|
|
|
674
726
|
function criar(op) { return new Entrada(op).iniciar(); }
|
|
675
727
|
|
|
676
|
-
module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas };
|
|
728
|
+
module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas, painel, pedidoNaTela, PREFIXO, COR };
|
package/lib/estudio/acervo.js
CHANGED
|
@@ -320,14 +320,28 @@ function creditos(arquivos = null, pasta = PASTA()) {
|
|
|
320
320
|
* Qual ferramenta desta máquina sabe recortar. A decisão fica separada do
|
|
321
321
|
* processo para poder ser testada sem recortar nada.
|
|
322
322
|
*/
|
|
323
|
-
function comoTirarFundo({ temRembg, temBackgroundRemover, temPython, temFfmpeg } = {}) {
|
|
323
|
+
function comoTirarFundo({ temRembg, temBackgroundRemover, temPython, temUv, temFfmpeg } = {}) {
|
|
324
324
|
if (temRembg) return 'rembg';
|
|
325
325
|
if (temBackgroundRemover) return 'backgroundremover';
|
|
326
326
|
if (temPython) return 'python-rembg';
|
|
327
|
+
/* Com o uv (o mesmo do recorte de pessoa), o rembg roda sem instalar nada:
|
|
328
|
+
o uv baixa o pacote num ambiente à parte. Antes, sem `pip install rembg`,
|
|
329
|
+
a foto caía no corte por cor e saía com o fundo inteiro (10/10). */
|
|
330
|
+
if (temUv) return 'uv-rembg';
|
|
327
331
|
if (temFfmpeg) return 'ffmpeg-chroma'; // só resolve fundo de cor chapada
|
|
328
332
|
return null;
|
|
329
333
|
}
|
|
330
334
|
|
|
335
|
+
let _uv;
|
|
336
|
+
function acharUv() {
|
|
337
|
+
if (_uv !== undefined) return _uv;
|
|
338
|
+
const os = require('os');
|
|
339
|
+
for (const c of ['uv', path.join(os.homedir(), '.local', 'bin', 'uv'), '/opt/homebrew/bin/uv', '/usr/local/bin/uv']) {
|
|
340
|
+
try { if (spawnSync(c, ['--version'], { stdio: 'ignore', timeout: 5000 }).status === 0) return (_uv = c); } catch { /* próximo */ }
|
|
341
|
+
}
|
|
342
|
+
return (_uv = null);
|
|
343
|
+
}
|
|
344
|
+
|
|
331
345
|
function existe(cmd) {
|
|
332
346
|
try { return spawnSync(cmd, ['--help'], { stdio: 'ignore', timeout: 8000 }).status != null; }
|
|
333
347
|
catch { return false; }
|
|
@@ -354,6 +368,7 @@ async function tirarFundo(arquivo, { destino, cor = '0x00FF00', tolerancia = 0.3
|
|
|
354
368
|
try { return spawnSync('python3', ['-c', 'import rembg'], { stdio: 'ignore', timeout: 20000 }).status === 0; }
|
|
355
369
|
catch { return false; }
|
|
356
370
|
})(),
|
|
371
|
+
temUv: !!acharUv(),
|
|
357
372
|
temFfmpeg: existe('ffmpeg'),
|
|
358
373
|
});
|
|
359
374
|
if (!metodo) {
|
|
@@ -381,6 +396,11 @@ async function tirarFundo(arquivo, { destino, cor = '0x00FF00', tolerancia = 0.3
|
|
|
381
396
|
deu = await rodar('python3', ['-c',
|
|
382
397
|
'import sys;from rembg import remove;open(sys.argv[2],"wb").write(remove(open(sys.argv[1],"rb").read()))',
|
|
383
398
|
arquivo, saida]);
|
|
399
|
+
} else if (metodo === 'uv-rembg') {
|
|
400
|
+
// A primeira vez baixa o modelo (~170 MB, fica em ~/.u2net); depois é segundos por imagem.
|
|
401
|
+
deu = await rodar(acharUv(), ['run', '--quiet', '--python', '3.12', '--with', 'rembg[cpu]', '--with', 'pillow', 'python', '-c',
|
|
402
|
+
'import sys;from rembg import remove, new_session;open(sys.argv[2],"wb").write(remove(open(sys.argv[1],"rb").read(), session=new_session("isnet-general-use")))',
|
|
403
|
+
arquivo, saida]);
|
|
384
404
|
} else {
|
|
385
405
|
deu = await rodar('ffmpeg', ['-y', '-i', arquivo, '-vf',
|
|
386
406
|
`colorkey=${cor}:${tolerancia}:0.1`, '-c:a', 'copy', saida]);
|