primocode 10.0.0 → 10.1.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 +4 -4
- package/bin/primocode.js +88 -3
- package/lib/act.js +122 -20
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +6 -3
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -2
- package/lib/entrada.js +73 -9
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +63 -25
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +41 -10
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +1471 -0
- package/lib/estudio/hibrido.js +169 -0
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +366 -47
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +990 -0
- package/lib/estudio/kit/index.jsx +4 -1
- package/lib/estudio/kit/tres.jsx +375 -0
- package/lib/estudio/kit.js +9 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +27 -0
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +103 -14
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +183 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +51 -1
- package/lib/tools.js +53 -8
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +5 -4
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/voz_kokoro.py +98 -0
package/lib/tools.js
CHANGED
|
@@ -209,9 +209,35 @@ function grepFiles({ pattern, glob, ignoreCase, contexto = 0, maxResultados = 10
|
|
|
209
209
|
return { ok: true, arquivosVistos, total: resultados.length, resultados, truncado: resultados.length >= maxResultados };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
/* ── O SHELL DO AGENTE NASCE SEM SEGREDO ──────────────────────────────────
|
|
213
|
+
Antes, o run_shell herdava o process.env inteiro: o agente rodava `env` e
|
|
214
|
+
lia o NPM_TOKEN, a chave da Anthropic, o GH_TOKEN de quem usa — e o que o
|
|
215
|
+
agente lê pode ir parar no modelo, num arquivo ou numa página (prompt
|
|
216
|
+
injection). Peça 3 do OpenBot (comum/desmontados/copilotkit-openbot.md):
|
|
217
|
+
o shell do agente não herda o ambiente do processo pai.
|
|
218
|
+
|
|
219
|
+
Filtro por NOME, não lista do que passa: PATH, HOME, LANG, TERM, SHELL,
|
|
220
|
+
USER, NODE_*, nvm etc. continuam — sem eles `npm`, `node` e `git` quebram.
|
|
221
|
+
Na dúvida o nome sai: um token a mais escondido custa pouco, um a menos
|
|
222
|
+
vaza. Só o caminho do agente (execute → run_shell) é filtrado; os usos
|
|
223
|
+
internos (abrir terminal, desinstalar) seguem com o ambiente inteiro. */
|
|
224
|
+
const NOME_SECRETO = /(TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|AUTH|COOKIE|SESSION)/i;
|
|
225
|
+
const PREFIXO_SECRETO = /^(AWS_|OPENAI_|ANTHROPIC_|GEMINI_|GOOGLE_API|GROQ_|OPENROUTER_|AZURE_|HF_|HUGGING)/i;
|
|
226
|
+
const SEMPRE_PASSA = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'PWD', 'SSH_AUTH_SOCK']);
|
|
227
|
+
|
|
228
|
+
function ambienteLimpo(env = process.env) {
|
|
229
|
+
const limpo = {};
|
|
230
|
+
for (const [nome, valor] of Object.entries(env)) {
|
|
231
|
+
if (valor === undefined) continue;
|
|
232
|
+
if (!SEMPRE_PASSA.has(nome) && (NOME_SECRETO.test(nome) || PREFIXO_SECRETO.test(nome))) continue;
|
|
233
|
+
limpo[nome] = valor;
|
|
234
|
+
}
|
|
235
|
+
return limpo;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function runShell(command, timeout = 60000, cwd, env) {
|
|
213
239
|
return new Promise((resolve) => {
|
|
214
|
-
exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
240
|
+
exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024, env: env || process.env }, (error, stdout, stderr) => {
|
|
215
241
|
if (error) resolve({ ok: false, code: error.code ?? 1, stdout, stderr, killed: error.killed });
|
|
216
242
|
else resolve({ ok: true, code: 0, stdout, stderr });
|
|
217
243
|
});
|
|
@@ -402,6 +428,13 @@ function readFile({ path: rawPath, offset, limit }, projectDir) {
|
|
|
402
428
|
};
|
|
403
429
|
}
|
|
404
430
|
|
|
431
|
+
/* Arquivo pequeno vem INTEIRO, mesmo pedido aos pedaços: o modelo lia um
|
|
432
|
+
app.js de 393 linhas de 50 em 50, cada pedaço uma ida ao servidor
|
|
433
|
+
(teste da loja, 01/10). Inteiro, uma vez, custa menos. */
|
|
434
|
+
if (todas.length <= 400 && bruto.length <= 40000) {
|
|
435
|
+
return { ok: true, content: todas.map((l, i) => `${i + 1}\t${l}`).join('\n'), linhas: todas.length, bytes: bruto.length,
|
|
436
|
+
inteiro: true, dica: 'arquivo pequeno: veio inteiro, com o número das linhas — não precisa ler de novo' };
|
|
437
|
+
}
|
|
405
438
|
const inicio = Math.max(0, (offset || 1) - 1);
|
|
406
439
|
const fim = Math.min(todas.length, inicio + (limit || 200));
|
|
407
440
|
const recorte = todas.slice(inicio, fim).map((l, i) => `${inicio + i + 1}\t${l}`).join('\n');
|
|
@@ -553,13 +586,21 @@ async function despachar(name, args, ctx) {
|
|
|
553
586
|
case 'grep':
|
|
554
587
|
return grepFiles(args, projectDir);
|
|
555
588
|
case 'run_shell':
|
|
556
|
-
return runShell(args.command, args.timeout_ms || 60000, projectDir);
|
|
589
|
+
return runShell(args.command, args.timeout_ms || 60000, projectDir, ambienteLimpo());
|
|
557
590
|
case 'run_in_new_terminal':
|
|
558
591
|
return runInNewTerminal(args.command, args.cwd || projectDir);
|
|
559
592
|
case 'open_in_browser': {
|
|
560
593
|
const alvo = /^https?:\/\//i.test(args.target || '')
|
|
561
594
|
? args.target
|
|
562
595
|
: resolveInProject(args.target, projectDir);
|
|
596
|
+
/* Site da web abre À VISTA no Chrome controlado (o cursor vai na
|
|
597
|
+
barra e digita). Arquivo do projeto continua no navegador padrão. */
|
|
598
|
+
if (/^https?:/i.test(alvo) && process.env.PRIMOCODE_BROWSER_HEADLESS !== '1' && require('./cursor').disponivel()) {
|
|
599
|
+
try {
|
|
600
|
+
const r = await browser.abrirVisivel(alvo);
|
|
601
|
+
if (r && r.ok !== false) return { ok: true, opener: 'chrome à vista', url: r.url, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR };
|
|
602
|
+
} catch { /* cai no navegador padrão */ }
|
|
603
|
+
}
|
|
563
604
|
return openInBrowser(alvo);
|
|
564
605
|
}
|
|
565
606
|
case 'multi_edit':
|
|
@@ -570,11 +611,14 @@ async function despachar(name, args, ctx) {
|
|
|
570
611
|
// Pesquisa de referência: abre documentação e exemplos sobre o
|
|
571
612
|
// pedido e devolve o TEXTO para o modelo aprender antes de
|
|
572
613
|
// escrever. Aparece no terminal, com as fontes — nada oculto.
|
|
573
|
-
case 'pesquisar_referencia':
|
|
574
|
-
|
|
614
|
+
case 'pesquisar_referencia': {
|
|
615
|
+
const r = await referencia.pesquisar({
|
|
575
616
|
consulta: args.consulta || args.query || '',
|
|
576
617
|
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
577
618
|
});
|
|
619
|
+
// Escopo fechado ("só do site x.com"): o buscador devolve o mundo; aqui só passa o site.
|
|
620
|
+
return require('./estudio/referencias.js').filtrar(r);
|
|
621
|
+
}
|
|
578
622
|
|
|
579
623
|
// memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
|
|
580
624
|
// não alcançam por design (resolveInProject bloqueia ../ de propósito).
|
|
@@ -595,7 +639,8 @@ async function despachar(name, args, ctx) {
|
|
|
595
639
|
const alvo = /^https?:\/\//i.test(args.url || '')
|
|
596
640
|
? args.url
|
|
597
641
|
: 'file://' + resolveInProject(args.url, projectDir);
|
|
598
|
-
|
|
642
|
+
// Endereço da web abre À VISTA: o cursor vai na barra e digita (browser.abrirVisivel).
|
|
643
|
+
const r = /^https?:/i.test(alvo) ? await browser.abrirVisivel(alvo) : await browser.open(alvo);
|
|
599
644
|
return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR } : r;
|
|
600
645
|
}
|
|
601
646
|
case 'browser_click': {
|
|
@@ -692,7 +737,7 @@ async function despachar(name, args, ctx) {
|
|
|
692
737
|
* lib/estudio/ para o porquê de cada peça.
|
|
693
738
|
*/
|
|
694
739
|
case 'estudio_projeto':
|
|
695
|
-
return estudio.ferramentaProjeto(args);
|
|
740
|
+
return estudio.ferramentaProjeto(args, ctx);
|
|
696
741
|
case 'estudio_plano':
|
|
697
742
|
return estudio.ferramentaPlano(args);
|
|
698
743
|
case 'estudio_midia':
|
|
@@ -834,7 +879,7 @@ async function uninstall() {
|
|
|
834
879
|
}
|
|
835
880
|
|
|
836
881
|
module.exports = {
|
|
837
|
-
fmtBytes, treeDir, runShell, openInBrowser, runInNewTerminal,
|
|
882
|
+
fmtBytes, treeDir, runShell, ambienteLimpo, openInBrowser, runInNewTerminal,
|
|
838
883
|
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
839
884
|
multiEdit, writeTodos, getTodos,
|
|
840
885
|
};
|
package/lib/ui.js
CHANGED
|
@@ -310,11 +310,11 @@ const TIPS = [
|
|
|
310
310
|
];
|
|
311
311
|
|
|
312
312
|
const NEWS = [
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'primocode video
|
|
316
|
-
'
|
|
317
|
-
'
|
|
313
|
+
'A IA decide o caminho: skills e equipe num cardápio, ela escolhe o que usa',
|
|
314
|
+
'Até 8 agentes em paralelo, cada um volta com uma entrega — e passa a vez',
|
|
315
|
+
'Edite falando no vídeo: primocode video auto aplica o que você diz, na hora',
|
|
316
|
+
'Colou texto grande? Vira etiqueta [Colado #1 · N linhas] na caixa',
|
|
317
|
+
'Escopo fechado de verdade: "só do site x.com" filtra a pesquisa',
|
|
318
318
|
];
|
|
319
319
|
|
|
320
320
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
|
@@ -419,6 +419,21 @@ class Spinner {
|
|
|
419
419
|
// Fora de um terminal (pipe, CI) a animação só sujaria a saída.
|
|
420
420
|
if (!process.stdout.isTTY) return this;
|
|
421
421
|
this.desde = Date.now();
|
|
422
|
+
/* Com a caixa de digitar no ar, o spinner desenha DENTRO dela (ver
|
|
423
|
+
mostrarSpinner em lib/entrada.js): escrever "\r" por cima apagava
|
|
424
|
+
a caixa e a barra de status enquanto ele girava. */
|
|
425
|
+
const caixa = Spinner.caixa;
|
|
426
|
+
if (caixa && caixa.mostrarSpinner && caixa.desenhado !== undefined) {
|
|
427
|
+
this.naCaixa = caixa;
|
|
428
|
+
this.timer = setInterval(() => {
|
|
429
|
+
if (this.textos.length > 1) this.text = this.textos[Math.floor((Date.now() - this.desde) / 4000) % this.textos.length];
|
|
430
|
+
const seg = Math.floor((Date.now() - this.desde) / 1000);
|
|
431
|
+
const extra = (seg >= 3 ? ` · ${seg}s` : '') + (this.detalhe ? ` · ${this.detalhe}` : '');
|
|
432
|
+
caixa.mostrarSpinner(`${c.muted(this.frames[this.i])} ${c.muted(this.text)}${c.dim(extra)}`);
|
|
433
|
+
this.i = (this.i + 1) % this.frames.length;
|
|
434
|
+
}, 120);
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
422
437
|
this.timer = setInterval(() => {
|
|
423
438
|
// Com mais de uma frase, troca a cada 4s. A frase nova pode ser mais
|
|
424
439
|
// curta que a anterior: o resto da linha velha ficaria na tela, então
|
|
@@ -439,10 +454,29 @@ class Spinner {
|
|
|
439
454
|
return this;
|
|
440
455
|
}
|
|
441
456
|
stop() {
|
|
457
|
+
if (this.naCaixa) {
|
|
458
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
459
|
+
this.naCaixa.mostrarSpinner('');
|
|
460
|
+
this.naCaixa = null;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
442
463
|
if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(Math.max(80, this.largura + 4))}\r`); }
|
|
443
464
|
}
|
|
444
465
|
}
|
|
445
466
|
|
|
467
|
+
/* O TÍTULO DO TERMINAL: é por ele que o Orca (e qualquer terminal com abas)
|
|
468
|
+
sabe que este painel é um agente e se está trabalhando. O Orca lê o glifo
|
|
469
|
+
de spinner em braile como "trabalhando" e o nome como identidade — então
|
|
470
|
+
"⠋ PrimoCode — tarefa" enquanto trabalha, "PrimoCode" parado. Vai pelo
|
|
471
|
+
canal cru da caixa: pelo stdout embrulhado, um escape sem quebra de linha
|
|
472
|
+
deixaria a caixa apagada (o mesmo defeito do spinner). */
|
|
473
|
+
function titulo(texto) {
|
|
474
|
+
if (!process.stdout.isTTY || process.env.PRIMOCODE_SEM_TITULO === '1') return;
|
|
475
|
+
const limpo = String(texto || '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 80);
|
|
476
|
+
const escrever = Spinner.caixa && Spinner.caixa.escreverCru ? Spinner.caixa.escreverCru : (s) => process.stdout.write(s);
|
|
477
|
+
try { escrever(`\x1b]0;${limpo}\x07`); } catch { /* terminal fechado */ }
|
|
478
|
+
}
|
|
479
|
+
|
|
446
480
|
// O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
|
|
447
481
|
const ROTULOS = {
|
|
448
482
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
@@ -616,4 +650,5 @@ function friendlyError(raw) {
|
|
|
616
650
|
}
|
|
617
651
|
|
|
618
652
|
|
|
619
|
-
module.exports = {
|
|
653
|
+
module.exports = {
|
|
654
|
+
titulo, c, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto.js — `primocode video auto`: o vídeo é o pedido.
|
|
3
|
+
*
|
|
4
|
+
* "Quero que ele leia o que eu falo no vídeo e aplique cada pedido no
|
|
5
|
+
* momento em que eu digo." (01/10)
|
|
6
|
+
*
|
|
7
|
+
* A pessoa grava falando o que quer ("separe em camadas", "coloca o logo do
|
|
8
|
+
* Claude aqui", "com o estalar de dedos me coloca numa praia") e o editor
|
|
9
|
+
* acha cada pedido na transcrição e o ancora no que o rastro viu: a palavra,
|
|
10
|
+
* a mão que aparece logo depois, o estalo que vem em seguida, o juntar das
|
|
11
|
+
* mãos. Nada de quadro escrito à mão.
|
|
12
|
+
*
|
|
13
|
+
* planejar(dados) → [{op, args(segundos da origem), motivo}] pura (o teste roda)
|
|
14
|
+
* OPS.auto.run → junta os dados, executa o plano, devolve o que fez
|
|
15
|
+
*
|
|
16
|
+
* Tudo em SEGUNDOS DA ORIGEM até a hora de executar: os cortes (silêncio,
|
|
17
|
+
* fim) vão por último e a legenda depois deles, já nos cortes.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const PED = require('./pedido.js');
|
|
23
|
+
|
|
24
|
+
const tira = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
25
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
26
|
+
|
|
27
|
+
/* O que o Whisper ouve no lugar do que a pessoa disse — visto nos dois
|
|
28
|
+
primeiros vídeos de verdade: "cloud", "Claudio", "xgbt", "estravar",
|
|
29
|
+
"instruar de dedos". Normaliza antes de qualquer regra. */
|
|
30
|
+
const OUVIDO = [
|
|
31
|
+
[/\b(claudio|claudia|cloud|clode|clod|claud|cloude)\b/g, 'claude'],
|
|
32
|
+
[/\b(x ?gbt|x ?gpt|chat ?gbt|chat ?gpt|chat ?jpt|tchat ?gpt|chatgbt|gbt)\b/g, 'chatgpt'],
|
|
33
|
+
[/\bgoogle\s+(chrome|drive)\b/g, '$1'],
|
|
34
|
+
[/\b(estr[ae]l\w*|estrav\w*|estal\w*|instru\w*|estral\w*)\s+(?:os|nos|dos|de|o|do)?\s*dedos?\b/g, 'estalar o dedo'],
|
|
35
|
+
[/\b(estal\w*|estral\w*)\b/g, 'estalar'],
|
|
36
|
+
];
|
|
37
|
+
function normal(t) {
|
|
38
|
+
let s = tira(t).replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
39
|
+
for (const [re, por] of OUVIDO) s = s.replace(re, por);
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* "colocar um foguete aqui na palma da minha mão": qualquer coisa na mão, não
|
|
44
|
+
só as logos. Vira o modelo, se houver, ou a placa com o nome. */
|
|
45
|
+
const NA_MAO = /\b(?:coloc\w*|po[er]\w*|bot\w*|aparec\w*)\s+(?:um|uma|o|a)\s+([a-z]{3,20})(?:\s+[a-z]{3,20})?\s+(?:aqui\s+)?(?:na|em|sobre|nas)\s+(?:a\s+)?(?:palma|mao|maos)\b/g;
|
|
46
|
+
const NAO_OBJETO = /^(logo|imagem|foto|fundo|legenda|praia|cerebro|video|mao|coisa)$/;
|
|
47
|
+
function objetoNaMao(t) {
|
|
48
|
+
const out = [];
|
|
49
|
+
let m;
|
|
50
|
+
NA_MAO.lastIndex = 0;
|
|
51
|
+
while ((m = NA_MAO.exec(t))) if (!NAO_OBJETO.test(m[1])) out.push({ object: m[1], pos: m.index, mao: null });
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** As palavras viram frases: ponto final, ou pausa de mais de 0,7 s. */
|
|
56
|
+
function frases(palavras) {
|
|
57
|
+
const out = [];
|
|
58
|
+
let atual = [];
|
|
59
|
+
const fecha = () => { if (atual.length) out.push({ ws: atual, de: atual[0].de, ate: atual[atual.length - 1].ate }); atual = []; };
|
|
60
|
+
palavras.forEach((p, i) => {
|
|
61
|
+
const ant = palavras[i - 1];
|
|
62
|
+
if (ant && p.de - ant.ate > 0.7) fecha();
|
|
63
|
+
atual.push(p);
|
|
64
|
+
if (/[.!?]$/.test(String(p.texto).trim())) fecha();
|
|
65
|
+
});
|
|
66
|
+
fecha();
|
|
67
|
+
for (const f of out) f.t = normal(f.ws.map((w) => w.texto).join(' '));
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** O segundo em que a frase diz `re` (a primeira palavra que casa), ou o começo dela. */
|
|
72
|
+
function quando(f, re) {
|
|
73
|
+
for (let i = 0; i < f.ws.length; i++) {
|
|
74
|
+
const um = normal(f.ws[i].texto);
|
|
75
|
+
const dois = normal(f.ws.slice(i, i + 3).map((w) => w.texto).join(' '));
|
|
76
|
+
if (re.test(um) || (re.test(dois) && !re.test(normal(f.ws.slice(i + 1, i + 3).map((w) => w.texto).join(' '))))) return f.ws[i].de;
|
|
77
|
+
}
|
|
78
|
+
return f.de;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const RE = {
|
|
82
|
+
camadas: /\bcamadas?\b|\btres dimens|\b(tir\w*|remov\w*|recort\w*|separ\w*)\s+(?:me\s+)?(?:o\s+|do\s+|meu\s+)?fundo\b|\bme\s+(tir\w*|recort\w*)\s+do\s+fundo\b/,
|
|
83
|
+
volta: /\bvolt\w*\b|\bnormal\b/,
|
|
84
|
+
estalo: /\bestalar\b/,
|
|
85
|
+
juntar: /\bjunt\w*\b|\buni\w*\b/,
|
|
86
|
+
cerebro: /\bcerebro\b/,
|
|
87
|
+
silencio: /\bsilencios?\b/,
|
|
88
|
+
fim: /\b(encerr|acab|termin|finaliz)\w*\b/,
|
|
89
|
+
legenda: /\blegendas?\b/,
|
|
90
|
+
acima: /\b(em cima|acima|encima|sobre|por cima)\s+da\s+(?:minha\s+)?cabeca\b|\baqui em cima\b|\bla em cima\b/,
|
|
91
|
+
imagem: /\b(?:imagem|foto|fundo)\s+(?:de|do|da)\s+(?:(?:um|uma)\s+)?([a-z]{3,20})\b/,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** O fundo pedido na frase: um lugar conhecido, ou "imagem de um tubarão". */
|
|
95
|
+
function fundoPedido(t) {
|
|
96
|
+
const l = PED.lugar(t);
|
|
97
|
+
if (l && !/^(tras|atras|aqui)$/.test(l)) return l;
|
|
98
|
+
const m = t.match(RE.imagem);
|
|
99
|
+
return m && !/^(tras|atras|aqui|novo|nova)$/.test(m[1]) ? m[1] : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* dados: {palavras:[{texto,de,ate}], estalos:[{t,score,mao}], juntaAbre:[{junta,abre}],
|
|
104
|
+
* maos:{left:[[de,ate]],right:[[de,ate]]}, silencios:[{de,ate}], dur} — tudo em segundos.
|
|
105
|
+
* → {passos:[{op,args,motivo}], legenda:{color?,position?,desde?}}
|
|
106
|
+
*/
|
|
107
|
+
function planejar(d) {
|
|
108
|
+
const fs = frases(d.palavras || []);
|
|
109
|
+
const passos = [];
|
|
110
|
+
const bons = (d.estalos || []).filter((e) => e.mao || e.score >= 20);
|
|
111
|
+
const usados = new Set();
|
|
112
|
+
/* O estalo da janela é o MAIS FORTE dela, não o primeiro: o dedo que se
|
|
113
|
+
prepara faz um "tec" fraco antes do estalo de verdade (visto: 10,3 s
|
|
114
|
+
com 17 antes do 11,8 s com 21). */
|
|
115
|
+
const estaloDepois = (t, janela) => {
|
|
116
|
+
const c = bons.filter((x) => x.t >= t - 0.2 && x.t <= t + janela && !usados.has(x.t)).sort((a, b) => b.score - a.score)[0];
|
|
117
|
+
if (!c) return undefined;
|
|
118
|
+
usados.add(c.t);
|
|
119
|
+
return c.t;
|
|
120
|
+
};
|
|
121
|
+
const ehPedido = (f) => RE.camadas.test(f.t) || !!fundoPedido(f.t) || PED.objetosCitados(f.t).length || RE.juntar.test(f.t) || RE.silencio.test(f.t) || RE.fim.test(f.t);
|
|
122
|
+
|
|
123
|
+
// 1. As camadas em 3D: da palavra até o "volta" (ou o próximo pedido), 2,5–5 s.
|
|
124
|
+
const iCam = fs.findIndex((f) => RE.camadas.test(f.t) && !fundoPedido(f.t));
|
|
125
|
+
if (iCam >= 0) {
|
|
126
|
+
const de = Math.max(0, quando(fs[iCam], /camada|fundo|tir|recort|separ/) - 0.2);
|
|
127
|
+
const prox = fs.slice(iCam + 1).find((f) => f.de > de + 1 && (RE.volta.test(f.t) || ehPedido(f)));
|
|
128
|
+
const ate = Math.min(d.dur, de + 5, Math.max(de + 2.5, prox ? prox.de : de + 4));
|
|
129
|
+
passos.push({ op: 'separate_layers', args: { show3d: true, from: r2(de), to: r2(ate) }, motivo: `"${fs[iCam].t}"` });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 2. Os objetos: na palavra, na mão que aparece logo ali (a outra, se a primeira já tem um).
|
|
133
|
+
const objetos = [];
|
|
134
|
+
const maoEm = (t, ocupadas) => {
|
|
135
|
+
const cand = [];
|
|
136
|
+
for (const lado of ['left', 'right']) {
|
|
137
|
+
for (const [a, b] of (d.maos && d.maos[lado]) || []) {
|
|
138
|
+
if (b < t - 0.2 || a > t + 3) continue;
|
|
139
|
+
cand.push({ lado, a: Math.max(a, t), b });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
cand.sort((x, y) => (ocupadas.has(x.lado) - ocupadas.has(y.lado)) || x.a - y.a);
|
|
143
|
+
return cand[0] || null;
|
|
144
|
+
};
|
|
145
|
+
for (const f of fs) {
|
|
146
|
+
for (const o of [...PED.objetosCitados(f.t), ...objetoNaMao(f.t)]) {
|
|
147
|
+
const nome = o.object === 'chatgpt' ? /chatgpt|gpt/ : new RegExp(o.object.slice(0, 5));
|
|
148
|
+
const t = quando(f, nome);
|
|
149
|
+
if (objetos.some((x) => x.object === o.object && Math.abs(x.t - t) < 4)) continue;
|
|
150
|
+
const ocupadas = new Set(objetos.filter((x) => x.ate === null || x.ate > t).map((x) => x.mao));
|
|
151
|
+
const m = maoEm(t, ocupadas);
|
|
152
|
+
const mao = o.mao === 'left' || o.mao === 'right' ? o.mao : m ? m.lado : (ocupadas.has('left') ? 'right' : 'auto');
|
|
153
|
+
objetos.push({ object: o.object, t, de: m && m.lado === mao ? m.a : t, mao, ate: null, motivo: `"${f.t}"` });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Juntar as mãos (com objeto na mão): a junção que vem depois da frase; o cérebro se for dito.
|
|
158
|
+
let fusao = null;
|
|
159
|
+
if (objetos.length) {
|
|
160
|
+
const iJ = fs.findIndex((f) => (RE.juntar.test(f.t) || RE.cerebro.test(f.t)) && f.ate > objetos[0].t);
|
|
161
|
+
if (iJ >= 0) {
|
|
162
|
+
const f = fs[iJ];
|
|
163
|
+
const ev = (d.juntaAbre || []).find((e) => e.junta >= f.de - 1.5 && e.junta > objetos[0].de + 0.5);
|
|
164
|
+
if (ev) {
|
|
165
|
+
const cerebro = [f, fs[iJ + 1]].some((x) => x && RE.cerebro.test(x.t));
|
|
166
|
+
fusao = { junta: ev.junta, abre: ev.abre, then: cerebro ? 'brain' : 'none', motivo: `"${f.t}"` };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const o of objetos) {
|
|
171
|
+
o.ate = fusao && fusao.junta > o.de ? fusao.junta : Math.min(d.dur, o.de + 4);
|
|
172
|
+
if (o.ate > o.de + 0.3) passos.push({ op: 'attach_3d', args: { object: o.object, hand: o.mao, from: r2(o.de), to: r2(o.ate) }, motivo: o.motivo });
|
|
173
|
+
}
|
|
174
|
+
if (fusao) passos.push({ op: 'merge_on_join', args: { junta: r2(fusao.junta), ...(fusao.abre !== null ? { abre: r2(fusao.abre) } : {}), then: fusao.then }, motivo: fusao.motivo });
|
|
175
|
+
|
|
176
|
+
// 4. Fundos novos: no estalo, se a frase (ou a anterior) fala dele, ou se um estalo vem logo depois; senão na palavra.
|
|
177
|
+
const fundos = [];
|
|
178
|
+
fs.forEach((f, i) => {
|
|
179
|
+
const onde = fundoPedido(f.t);
|
|
180
|
+
if (!onde || !/\b(coloc\w*|poe|pon\w*|bot\w*|lev\w*|troc\w*|mud\w*|fundo|imagem|foto|me\s+em)\b/.test(f.t)) return;
|
|
181
|
+
const t = quando(f, new RegExp(onde.split(' ')[0].slice(0, 5)));
|
|
182
|
+
/* Um estalo logo depois do lugar é o gatilho, dito ou não. Se o estalo
|
|
183
|
+
foi dito ANTES do lugar ("se estalar os dedos ela coloca um
|
|
184
|
+
tubarão"), ele pode vir uns segundos depois da frase. */
|
|
185
|
+
const antes = f.t.slice(0, f.t.indexOf(onde.split(' ')[0]));
|
|
186
|
+
const falaEstalo = RE.estalo.test(antes) || (fs[i - 1] && RE.estalo.test(fs[i - 1].t) && !fundoPedido(fs[i - 1].t));
|
|
187
|
+
let e = estaloDepois(t, 3);
|
|
188
|
+
if (e === undefined && falaEstalo) e = estaloDepois(f.de, 10);
|
|
189
|
+
// "só aqui", "somente aqui": o fundo vale para a frase, depois volta o original.
|
|
190
|
+
const so = /\b(somente|so|apenas)\s+(aqui|agora|nessa parte|nesse momento)\b/.test(f.t) ? Math.min(d.dur, f.ate + 0.6) : null;
|
|
191
|
+
fundos.push({ with: onde, at: e !== undefined ? e : t, ...(so ? { ate: so } : {}), motivo: `"${f.t}"` + (e !== undefined ? ' (no estalo)' : '') });
|
|
192
|
+
});
|
|
193
|
+
fundos.forEach((b, i) => {
|
|
194
|
+
const until = b.ate !== undefined ? b.ate : fundos[i + 1] ? fundos[i + 1].at : null;
|
|
195
|
+
passos.push({ op: 'replace_background', args: { with: b.with, at: r2(b.at), ...(until !== null ? { until: r2(until) } : {}) }, motivo: b.motivo });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 5. Legenda: sempre (é reels); a cor e a posição se forem pedidas, a partir do estalo se ele for citado.
|
|
199
|
+
const legenda = {};
|
|
200
|
+
const iL = fs.findIndex((f) => RE.legenda.test(f.t));
|
|
201
|
+
if (iL >= 0) {
|
|
202
|
+
const janela = fs.slice(iL, iL + 3);
|
|
203
|
+
const cor = janela.map((f) => PED.corPedida(f.t)).find(Boolean);
|
|
204
|
+
if (cor) legenda.color = cor;
|
|
205
|
+
const fp = janela.find((f) => RE.acima.test(f.t));
|
|
206
|
+
if (fp) {
|
|
207
|
+
legenda.position = 'above_head';
|
|
208
|
+
if (RE.estalo.test(fp.t)) { const e = estaloDepois(fp.de, 8); if (e !== undefined) legenda.desde = e; }
|
|
209
|
+
// "a legenda que tá aqui… aqui em cima": sobe quando ela diz onde.
|
|
210
|
+
else if (/\b(aqui|la) em cima\b/.test(fp.t)) legenda.desde = quando(fp, /cima/);
|
|
211
|
+
} else {
|
|
212
|
+
/* "a legenda daqui ficar aqui", apontando: o pedido é mudar de lugar, e
|
|
213
|
+
o lugar que se aponta em quem fala para a câmera é perto do rosto —
|
|
214
|
+
a legenda sobe para a cabeça no segundo "aqui". (01/10) */
|
|
215
|
+
const fd = janela.find((f) => /\bdaqui\b[\s\S]*\b(ficar|ir|pra|para)\b[\s\S]*\baqui\b|\b(mud|muda|mover|move|pass)\w*\b[\s\S]*\baqui\b/.test(f.t));
|
|
216
|
+
if (fd) {
|
|
217
|
+
legenda.position = 'above_head';
|
|
218
|
+
const aquis = fd.ws.filter((w) => /^aqui/.test(normal(w.texto)));
|
|
219
|
+
legenda.desde = (aquis[aquis.length - 1] || fd.ws[0]).de;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 6. Cortes, por último: o silêncio que a pessoa cita, e o fim que ela anuncia.
|
|
225
|
+
const cortes = [];
|
|
226
|
+
const iS = fs.findIndex((f) => RE.silencio.test(f.t) && /\b(cort\w*|tir\w*|remov\w*|apag\w*|retir\w*)\b/.test(f.t));
|
|
227
|
+
if (iS >= 0) {
|
|
228
|
+
const t0 = fs[iS].de;
|
|
229
|
+
const sil = juntarSilencios(d.silencios || []).filter((s) => s.de >= t0 && s.de <= t0 + 15 && s.ate - s.de >= 1.5)
|
|
230
|
+
.sort((a, b) => (b.ate - b.de) - (a.ate - a.de))[0];
|
|
231
|
+
if (sil) cortes.push({ de: r2(sil.de + 0.1), ate: r2(sil.ate - 0.1), motivo: `"${fs[iS].t}" (${r2(sil.ate - sil.de)} s parado)` });
|
|
232
|
+
}
|
|
233
|
+
const iF = fs.findIndex((f) => RE.fim.test(f.t) && /\bvideo\b/.test(f.t));
|
|
234
|
+
if (iF >= 0) {
|
|
235
|
+
const f = fs[iF];
|
|
236
|
+
const ultima = fs[fs.length - 1].ate;
|
|
237
|
+
const e = RE.estalo.test(f.t) ? estaloDepois(f.de, 6) : undefined;
|
|
238
|
+
const fim = Math.max(ultima, e !== undefined ? e : 0) + 0.4;
|
|
239
|
+
if (d.dur - fim > 0.8) cortes.push({ de: r2(fim), ate: r2(d.dur), motivo: `"${f.t}"` });
|
|
240
|
+
}
|
|
241
|
+
if (cortes.length) passos.push({ op: 'cortes', args: { trechos: cortes.map(({ de, ate }) => [de, ate]) }, motivo: cortes.map((c) => c.motivo).join(' · ') });
|
|
242
|
+
|
|
243
|
+
return { passos, legenda };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Silêncios separados por um respiro (< 0,4 s) são um só. */
|
|
247
|
+
function juntarSilencios(sil) {
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const s of [...sil].sort((a, b) => a.de - b.de)) {
|
|
250
|
+
const u = out[out.length - 1];
|
|
251
|
+
if (u && s.de - u.ate < 0.4) u.ate = Math.max(u.ate, s.ate);
|
|
252
|
+
else out.push({ de: s.de, ate: s.ate });
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const OPS = {
|
|
258
|
+
auto: {
|
|
259
|
+
uso: 'auto {asset?,dryRun?} — O VÍDEO É O PEDIDO: lê o que a pessoa FALA na gravação ("separe em camadas", "coloca o logo do Claude aqui", "com o estalar de dedos me coloca numa praia", "junta e vira um cérebro", "cortou o silêncio", "o vídeo acaba") e aplica cada efeito no momento em que ela diz, ancorado na palavra, na mão, no estalo e no juntar das mãos; legenda sempre. Use logo depois do import quando pedirem "edite esse vídeo" sem dizer o quê',
|
|
260
|
+
async run(ctx, a) {
|
|
261
|
+
const T = require('./timeline.js');
|
|
262
|
+
const C = require('./camadas.js');
|
|
263
|
+
const R = require('./rastro.js');
|
|
264
|
+
const RT = require('./rastrear.js');
|
|
265
|
+
const TR = require('./transcript.js');
|
|
266
|
+
const FXO = require('./ops-fx.js');
|
|
267
|
+
const tl = ctx.tl;
|
|
268
|
+
if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
|
|
269
|
+
const asset = C.base(tl, a.asset);
|
|
270
|
+
const inicio = Date.now();
|
|
271
|
+
// O lento e independente junto: rastreio (MediaPipe em paralelo) e transcrição.
|
|
272
|
+
const [rs, tr] = await Promise.all([RT.garantir(ctx.p.dir, asset), TR.obter(ctx.p.dir, asset)]);
|
|
273
|
+
if (!rs.ok) return rs;
|
|
274
|
+
if (!tr.ok) return { ok: false, error: 'sem a fala não sei o que fazer: ' + tr.error };
|
|
275
|
+
const fps = rs.rastro.fps;
|
|
276
|
+
const trilhas = R.trilhasDasMaos(rs.rastro);
|
|
277
|
+
const trechos = (t) => R.trechosVisiveis(t, Math.round(fps * 0.3)).map(([x, y]) => [x / fps, y / fps]);
|
|
278
|
+
const dados = {
|
|
279
|
+
palavras: tr.palavras,
|
|
280
|
+
estalos: (await FXO.estalosDe(ctx, asset)).map((e) => ({ t: e.t, score: e.score, mao: e.mao })),
|
|
281
|
+
juntaAbre: R.juntaEAbre(trilhas, { rastro: rs.rastro }).map((e) => ({ junta: e.junta / fps, abre: e.abre === null ? null : e.abre / fps })),
|
|
282
|
+
maos: { left: trechos(trilhas.left), right: trechos(trilhas.right) },
|
|
283
|
+
silencios: await require('./render.js').silenciosDe(ctx.p.dir, asset, { threshold: -35, minSilence: 0.8 }),
|
|
284
|
+
dur: asset.seconds,
|
|
285
|
+
};
|
|
286
|
+
const plano = planejar(dados);
|
|
287
|
+
const q = (s) => C.origemParaQuadro(ctx.tl, asset.id, s);
|
|
288
|
+
const ver = plano.passos.map((p) => ({ op: p.op, motivo: p.motivo, args: p.args }));
|
|
289
|
+
if (a.dryRun) return { ok: true, plano: ver, legenda: plano.legenda, segundos: 'da gravação' };
|
|
290
|
+
|
|
291
|
+
const { OPS: TODAS } = require('./ops.js');
|
|
292
|
+
const feitos = [];
|
|
293
|
+
const roda = async (op, args, motivo) => {
|
|
294
|
+
const res = await TODAS[op].run(ctx, args);
|
|
295
|
+
feitos.push({ op, motivo, ok: !!res && res.ok !== false, ...(res && res.ok === false ? { error: res.error } : {}) });
|
|
296
|
+
return res;
|
|
297
|
+
};
|
|
298
|
+
for (const p of plano.passos) {
|
|
299
|
+
const g = p.args;
|
|
300
|
+
if (p.op === 'separate_layers') await roda(p.op, { show3d: true, from: q(g.from), to: q(g.to) }, p.motivo);
|
|
301
|
+
else if (p.op === 'attach_3d') await roda(p.op, { object: g.object, hand: g.hand, from: q(g.from), to: q(g.to) }, p.motivo);
|
|
302
|
+
else if (p.op === 'merge_on_join') await roda(p.op, { junta: q(g.junta), ...(g.abre !== undefined ? { abre: q(g.abre) } : {}), then: g.then }, p.motivo);
|
|
303
|
+
else if (p.op === 'replace_background') await roda(p.op, { with: g.with, at: q(g.at), ...(g.until !== undefined ? { until: q(g.until) } : {}) }, p.motivo);
|
|
304
|
+
else if (p.op === 'cortes') {
|
|
305
|
+
const ranges = g.trechos.map(([de, ate]) => [q(de), Math.min(T.duracao(ctx.tl), q(Math.min(ate, asset.seconds - 0.001)) + 1)]).filter(([x, y]) => x !== null && y > x);
|
|
306
|
+
if (ranges.length) await roda('ripple_delete_ranges', { ranges }, p.motivo);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// A legenda, já nos cortes; a posição a partir do estalo, se ele foi citado.
|
|
310
|
+
await roda('add_captions', { maxWords: 3, animation: 'highlightPop' }, 'legenda (reels)');
|
|
311
|
+
const L = plano.legenda;
|
|
312
|
+
if (L.color || L.position) {
|
|
313
|
+
const leg = T.todosOsClipes(ctx.tl).filter((x) => x.clip.caption).map((x) => x.clip);
|
|
314
|
+
const desde = L.desde !== undefined ? q(L.desde) : null;
|
|
315
|
+
if (L.color) await roda('style_captions', { color: L.color }, 'cor pedida');
|
|
316
|
+
const ids = leg.filter((c) => desde === null || c.startFrame >= desde).map((c) => c.id);
|
|
317
|
+
if (L.position && ids.length) await roda('style_captions', { ids, position: L.position }, desde !== null ? 'em cima da cabeça, desde o momento dito' : 'em cima da cabeça');
|
|
318
|
+
}
|
|
319
|
+
const falhou = feitos.filter((f) => !f.ok);
|
|
320
|
+
return { ok: !falhou.length, feitos, tookS: Math.round((Date.now() - inicio) / 100) / 10,
|
|
321
|
+
...(falhou.length ? { error: falhou.map((f) => `${f.op}: ${f.error}`).join('; ') } : {}),
|
|
322
|
+
depois: 'inspect para OLHAR os momentos; export renderiza (o 3D sai no export)' };
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const EX = { auto: '{}' };
|
|
328
|
+
|
|
329
|
+
module.exports = { planejar, frases, normal, fundoPedido, juntarSilencios, OPS, EX };
|