primocode 9.10.0 → 10.0.9
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 +30 -6
- package/bin/primocode.js +169 -342
- package/lib/act.js +360 -39
- package/lib/browser.js +105 -4
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +77 -11
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +21 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +66 -0
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +284 -53
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +13 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +78 -0
- package/lib/estudio/lancamento.js +441 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +139 -51
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +414 -0
- package/lib/fala.js +1 -94
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +44 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +100 -22
- package/lib/ui.js +50 -8
- package/lib/video/analise.js +89 -0
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +280 -0
- 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 +347 -0
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +274 -0
- package/lib/video/texto.js +144 -0
- package/lib/video/timeline.js +318 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +5 -5
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/tools.js
CHANGED
|
@@ -61,6 +61,8 @@ async function clicarPeloCursor(x, y, { botao = 'left', duplo = false, rotulo }
|
|
|
61
61
|
() => (duplo ? desktop.doubleClick(nx, ny) : desktop.click(nx, ny, botao)));
|
|
62
62
|
}
|
|
63
63
|
const referencia = require('./referencia');
|
|
64
|
+
const snapshot = require('./snapshot');
|
|
65
|
+
const casar = require('./casar');
|
|
64
66
|
const estudio = require('./estudio');
|
|
65
67
|
const cfg = require('./config');
|
|
66
68
|
|
|
@@ -85,7 +87,8 @@ function resolveInProject(rawPath, projectDir) {
|
|
|
85
87
|
}
|
|
86
88
|
const target = path.resolve(base, rawPath || '.');
|
|
87
89
|
const rel = path.relative(base, target);
|
|
88
|
-
|
|
90
|
+
// `..` como SEGMENTO, não como começo de nome: "..rascunho" é legítimo.
|
|
91
|
+
if (rel === '..' || rel.startsWith('..' + path.sep) || rel.startsWith('../') || path.isAbsolute(rel)) {
|
|
89
92
|
throw new Error(`Caminho fora da pasta de trabalho: ${rawPath}`);
|
|
90
93
|
}
|
|
91
94
|
return target;
|
|
@@ -100,7 +103,10 @@ function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
|
100
103
|
for (const item of items) {
|
|
101
104
|
if (item.startsWith('.') && item !== '.env') continue;
|
|
102
105
|
if (IGNORAR.has(item)) continue;
|
|
103
|
-
|
|
106
|
+
// Link quebrado não derruba a listagem inteira: entra como arquivo.
|
|
107
|
+
let ehPasta = false;
|
|
108
|
+
try { ehPasta = fs.statSync(path.join(dir, item)).isDirectory(); } catch { ehPasta = false; }
|
|
109
|
+
(ehPasta ? dirs : files).push(item);
|
|
104
110
|
}
|
|
105
111
|
dirs.sort(); files.sort();
|
|
106
112
|
const all = [...dirs, ...files];
|
|
@@ -109,7 +115,7 @@ function treeDir(dir, maxDepth = 2, currentDepth = 0, prefix = '') {
|
|
|
109
115
|
const isLast = i === all.length - 1;
|
|
110
116
|
result += prefix + (isLast ? '└── ' : '├── ') + item + '\n';
|
|
111
117
|
const full = path.join(dir, item);
|
|
112
|
-
if (
|
|
118
|
+
if (dirs.includes(item) && currentDepth < maxDepth) {
|
|
113
119
|
result += treeDir(full, maxDepth, currentDepth + 1, prefix + (isLast ? ' ' : '│ '));
|
|
114
120
|
}
|
|
115
121
|
}
|
|
@@ -262,7 +268,19 @@ async function runInNewTerminal(command, cwd) {
|
|
|
262
268
|
como ~/Documents/Meus Projetos virava dois argumentos e o cd falhava
|
|
263
269
|
calado: o terminal abria na pasta errada e o comando rodava no lugar
|
|
264
270
|
errado, que é pior que não abrir. */
|
|
265
|
-
|
|
271
|
+
/* Sem shell no meio: o script vai como UM argumento do osascript
|
|
272
|
+
(execFile), e dentro dele a string do AppleScript é escapada (\\ e
|
|
273
|
+
\"). Antes o comando entrava cru entre aspas duplas dentro de aspas
|
|
274
|
+
simples — `node server.js --name="x"` quebrava o AppleScript. A
|
|
275
|
+
pasta vai entre aspas simples para o shell do Terminal (a aspa
|
|
276
|
+
simples está barrada acima). */
|
|
277
|
+
const doTerminal = `cd '${pastaAlvo}' && ${command}`;
|
|
278
|
+
const script = `tell application "Terminal" to do script "${doTerminal.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
execFile('osascript', ['-e', script], { timeout: 60000 }, (erro, _o, stderr) => {
|
|
281
|
+
resolve(erro ? { ok: false, error: String(stderr || erro.message).trim() || 'falhou' } : { ok: true, opener: 'osascript' });
|
|
282
|
+
});
|
|
283
|
+
});
|
|
266
284
|
} else if (p === 'win32') {
|
|
267
285
|
// Aspas em volta do caminho: "C:\Users\Joel Silva\..." é comum, e sem
|
|
268
286
|
// elas o cmd corta no espaço. A aspa dupla já está barrada acima, então
|
|
@@ -273,7 +291,8 @@ async function runInNewTerminal(command, cwd) {
|
|
|
273
291
|
o caminho não precisa entrar na string de shell nenhuma. É a solução
|
|
274
292
|
mais segura das três: o que não é interpolado não pode ser mal
|
|
275
293
|
escapado. */
|
|
276
|
-
|
|
294
|
+
// A aspa simples do comando fecharia o `bash -c '…'`: vira '\''.
|
|
295
|
+
cmd = `x-terminal-emulator -e "bash -c '${String(command).replace(/'/g, "'\\''")}; exec bash'"`;
|
|
277
296
|
pastaDoProcesso = pastaAlvo;
|
|
278
297
|
}
|
|
279
298
|
const r = await runShell(cmd, 60000, pastaDoProcesso);
|
|
@@ -315,7 +334,19 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
315
334
|
const original = fs.readFileSync(filePath, 'utf-8');
|
|
316
335
|
const ocorrencias = original.split(old_string).length - 1;
|
|
317
336
|
|
|
337
|
+
// Não bateu letra por letra: o casador frouxo (lib/casar.js) acha o
|
|
338
|
+
// trecho com o espaço, a indentação ou o número de linha errados — desde
|
|
339
|
+
// que seja UM lugar só. Poupa a volta "não achei → relê → edita de novo".
|
|
340
|
+
let casado = null;
|
|
318
341
|
if (ocorrencias === 0) {
|
|
342
|
+
const r = casar.achar(original, old_string);
|
|
343
|
+
if (r.erro === 'varios') {
|
|
344
|
+
return { ok: false, error: `Trecho aparece ${r.vezes} vezes (${r.como}). Inclua mais linhas em volta para ficar único.` };
|
|
345
|
+
}
|
|
346
|
+
if (!r.erro) casado = r;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (ocorrencias === 0 && !casado) {
|
|
319
350
|
// Erro seco faz o modelo chutar de novo. Devolvendo o que EXISTE no
|
|
320
351
|
// arquivo, ele acerta na segunda tentativa.
|
|
321
352
|
return {
|
|
@@ -329,15 +360,18 @@ function editFile({ path: rawPath, old_string, new_string, replace_all }, projec
|
|
|
329
360
|
return { ok: false, error: `Trecho aparece ${ocorrencias} vezes. Inclua mais linhas em volta para ficar único, ou use replace_all.` };
|
|
330
361
|
}
|
|
331
362
|
|
|
332
|
-
const atualizado =
|
|
333
|
-
? original.
|
|
334
|
-
:
|
|
363
|
+
const atualizado = casado
|
|
364
|
+
? original.slice(0, casado.inicio) + (new_string ?? '') + original.slice(casado.fim)
|
|
365
|
+
: replace_all
|
|
366
|
+
? original.split(old_string).join(new_string ?? '')
|
|
367
|
+
: original.replace(old_string, () => new_string ?? '');
|
|
335
368
|
|
|
336
369
|
fs.writeFileSync(filePath, atualizado, 'utf-8');
|
|
337
370
|
return {
|
|
338
371
|
ok: true,
|
|
339
372
|
path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
|
|
340
|
-
substituicoes: replace_all ? ocorrencias : 1,
|
|
373
|
+
substituicoes: replace_all && !casado ? ocorrencias : 1,
|
|
374
|
+
casouPor: casado ? casado.como : undefined,
|
|
341
375
|
linhasAntes: original.split('\n').length,
|
|
342
376
|
linhasDepois: atualizado.split('\n').length,
|
|
343
377
|
// As duas versões, para a TELA desenhar o diff. O `_` na frente marca
|
|
@@ -368,6 +402,13 @@ function readFile({ path: rawPath, offset, limit }, projectDir) {
|
|
|
368
402
|
};
|
|
369
403
|
}
|
|
370
404
|
|
|
405
|
+
/* Arquivo pequeno vem INTEIRO, mesmo pedido aos pedaços: o modelo lia um
|
|
406
|
+
app.js de 393 linhas de 50 em 50, cada pedaço uma ida ao servidor
|
|
407
|
+
(teste da loja, 01/10). Inteiro, uma vez, custa menos. */
|
|
408
|
+
if (todas.length <= 400 && bruto.length <= 40000) {
|
|
409
|
+
return { ok: true, content: todas.map((l, i) => `${i + 1}\t${l}`).join('\n'), linhas: todas.length, bytes: bruto.length,
|
|
410
|
+
inteiro: true, dica: 'arquivo pequeno: veio inteiro, com o número das linhas — não precisa ler de novo' };
|
|
411
|
+
}
|
|
371
412
|
const inicio = Math.max(0, (offset || 1) - 1);
|
|
372
413
|
const fim = Math.min(todas.length, inicio + (limit || 200));
|
|
373
414
|
const recorte = todas.slice(inicio, fim).map((l, i) => `${inicio + i + 1}\t${l}`).join('\n');
|
|
@@ -391,12 +432,21 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
391
432
|
if (old_string === new_string) return { ok: false, error: `edit ${i + 1}: old_string e new_string são iguais` };
|
|
392
433
|
const ocorrencias = conteudo.split(old_string).length - 1;
|
|
393
434
|
if (ocorrencias === 0) {
|
|
394
|
-
|
|
435
|
+
const r = casar.achar(conteudo, old_string);
|
|
436
|
+
if (r.erro === 'varios') {
|
|
437
|
+
return { ok: false, error: `edit ${i + 1}: trecho aparece ${r.vezes}x (${r.como}) — inclua mais contexto para torná-lo único` };
|
|
438
|
+
}
|
|
439
|
+
if (r.erro) {
|
|
440
|
+
return { ok: false, error: `edit ${i + 1}: trecho não encontrado`, trechosParecidos: trechosParecidos(conteudo, old_string) };
|
|
441
|
+
}
|
|
442
|
+
conteudo = conteudo.slice(0, r.inicio) + (new_string ?? '') + conteudo.slice(r.fim);
|
|
443
|
+
substituicoes++;
|
|
444
|
+
continue;
|
|
395
445
|
}
|
|
396
446
|
if (ocorrencias > 1 && !replace_all) {
|
|
397
447
|
return { ok: false, error: `edit ${i + 1}: trecho aparece ${ocorrencias}x — use replace_all ou inclua mais contexto para torná-lo único` };
|
|
398
448
|
}
|
|
399
|
-
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, new_string ?? '');
|
|
449
|
+
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, () => new_string ?? '');
|
|
400
450
|
substituicoes++;
|
|
401
451
|
}
|
|
402
452
|
|
|
@@ -517,6 +567,14 @@ async function despachar(name, args, ctx) {
|
|
|
517
567
|
const alvo = /^https?:\/\//i.test(args.target || '')
|
|
518
568
|
? args.target
|
|
519
569
|
: resolveInProject(args.target, projectDir);
|
|
570
|
+
/* Site da web abre À VISTA no Chrome controlado (o cursor vai na
|
|
571
|
+
barra e digita). Arquivo do projeto continua no navegador padrão. */
|
|
572
|
+
if (/^https?:/i.test(alvo) && process.env.PRIMOCODE_BROWSER_HEADLESS !== '1' && require('./cursor').disponivel()) {
|
|
573
|
+
try {
|
|
574
|
+
const r = await browser.abrirVisivel(alvo);
|
|
575
|
+
if (r && r.ok !== false) return { ok: true, opener: 'chrome à vista', url: r.url, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR };
|
|
576
|
+
} catch { /* cai no navegador padrão */ }
|
|
577
|
+
}
|
|
520
578
|
return openInBrowser(alvo);
|
|
521
579
|
}
|
|
522
580
|
case 'multi_edit':
|
|
@@ -527,11 +585,14 @@ async function despachar(name, args, ctx) {
|
|
|
527
585
|
// Pesquisa de referência: abre documentação e exemplos sobre o
|
|
528
586
|
// pedido e devolve o TEXTO para o modelo aprender antes de
|
|
529
587
|
// escrever. Aparece no terminal, com as fontes — nada oculto.
|
|
530
|
-
case 'pesquisar_referencia':
|
|
531
|
-
|
|
588
|
+
case 'pesquisar_referencia': {
|
|
589
|
+
const r = await referencia.pesquisar({
|
|
532
590
|
consulta: args.consulta || args.query || '',
|
|
533
591
|
paginas: Math.max(1, Math.min(5, Number(args.paginas) || 3)),
|
|
534
592
|
});
|
|
593
|
+
// Escopo fechado ("só do site x.com"): o buscador devolve o mundo; aqui só passa o site.
|
|
594
|
+
return require('./estudio/referencias.js').filtrar(r);
|
|
595
|
+
}
|
|
535
596
|
|
|
536
597
|
// memory-geral.md fica FORA da pasta do projeto — write_file/edit_file
|
|
537
598
|
// não alcançam por design (resolveInProject bloqueia ../ de propósito).
|
|
@@ -544,24 +605,41 @@ async function despachar(name, args, ctx) {
|
|
|
544
605
|
}
|
|
545
606
|
|
|
546
607
|
// ── Navegador controlado (CDP) — o agente age numa página real ──
|
|
608
|
+
/* O navegador fala em REFS (lib/snapshot.js): abrir e clicar já
|
|
609
|
+
devolvem o retrato da página com "@e3 [button] Entrar", e o
|
|
610
|
+
selector aceita "@e3". Uma leitura a menos por passo, e nenhum
|
|
611
|
+
seletor CSS chutado. */
|
|
547
612
|
case 'browser_open': {
|
|
548
613
|
const alvo = /^https?:\/\//i.test(args.url || '')
|
|
549
614
|
? args.url
|
|
550
615
|
: 'file://' + resolveInProject(args.url, projectDir);
|
|
551
|
-
|
|
616
|
+
// Endereço da web abre À VISTA: o cursor vai na barra e digita (browser.abrirVisivel).
|
|
617
|
+
const r = /^https?:/i.test(alvo) ? await browser.abrirVisivel(alvo) : await browser.open(alvo);
|
|
618
|
+
return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40), comoUsar: snapshot.COMO_USAR } : r;
|
|
619
|
+
}
|
|
620
|
+
case 'browser_click': {
|
|
621
|
+
let r;
|
|
622
|
+
if (args.selector || snapshot.ehRef(args.text)) r = await browser.clickSelector(snapshot.seletorDaRef(args.selector || args.text));
|
|
623
|
+
else if (args.text) r = await browser.clickText(args.text);
|
|
624
|
+
else if (args.x != null && args.y != null) r = await browser.click(args.x, args.y);
|
|
625
|
+
else return { ok: false, error: 'browser_click precisa de selector (ou a ref "@e3"), text ou x/y' };
|
|
626
|
+
// O clique quase sempre muda a página: o retrato novo vai junto.
|
|
627
|
+
await new Promise((ok) => setTimeout(ok, 500));
|
|
628
|
+
return r && r.ok !== false ? { ...r, pagina: await browser.retrato(40) } : r;
|
|
552
629
|
}
|
|
553
|
-
case 'browser_click':
|
|
554
|
-
if (args.selector) return browser.clickSelector(args.selector);
|
|
555
|
-
if (args.text) return browser.clickText(args.text);
|
|
556
|
-
if (args.x != null && args.y != null) return browser.click(args.x, args.y);
|
|
557
|
-
return { ok: false, error: 'browser_click precisa de selector, text ou x/y' };
|
|
558
630
|
case 'browser_type':
|
|
559
|
-
if (args.selector) return browser.typeInSelector(args.selector, args.text || '', args.clear !== false);
|
|
631
|
+
if (args.selector) return browser.typeInSelector(snapshot.seletorDaRef(args.selector), args.text || '', args.clear !== false);
|
|
560
632
|
return browser.type(args.text || '');
|
|
561
633
|
case 'browser_key':
|
|
562
634
|
return browser.key(args.key);
|
|
563
635
|
case 'browser_read': {
|
|
564
|
-
|
|
636
|
+
if (!args.selector) {
|
|
637
|
+
const pagina = await browser.retrato(80);
|
|
638
|
+
const texto = await browser.evaluate('(function(){return document.body?document.body.innerText.slice(0,2500):null;})()');
|
|
639
|
+
if (!pagina && texto == null) return { ok: false, error: 'nada na página — ela pode não ter carregado' };
|
|
640
|
+
return { ok: true, pagina, text: texto || '', comoUsar: snapshot.COMO_USAR };
|
|
641
|
+
}
|
|
642
|
+
const sel = `document.querySelector(${JSON.stringify(snapshot.seletorDaRef(args.selector))})`;
|
|
565
643
|
const texto = await browser.evaluate(`(function(){const e=${sel};return e?e.innerText.slice(0,6000):null;})()`);
|
|
566
644
|
return texto == null ? { ok: false, error: 'nada na página (ou seletor não encontrado)' } : { ok: true, text: texto };
|
|
567
645
|
}
|
|
@@ -573,7 +651,7 @@ async function despachar(name, args, ctx) {
|
|
|
573
651
|
case 'browser_scroll':
|
|
574
652
|
return browser.scroll(args.dy != null ? args.dy : 600);
|
|
575
653
|
case 'browser_wait':
|
|
576
|
-
return browser.waitFor(args.selector, args.timeout_ms || 15000);
|
|
654
|
+
return browser.waitFor(snapshot.seletorDaRef(args.selector), args.timeout_ms || 15000);
|
|
577
655
|
case 'browser_eval':
|
|
578
656
|
return { ok: true, value: await browser.evaluate(args.js || 'null') };
|
|
579
657
|
|
package/lib/ui.js
CHANGED
|
@@ -310,11 +310,11 @@ const TIPS = [
|
|
|
310
310
|
];
|
|
311
311
|
|
|
312
312
|
const NEWS = [
|
|
313
|
-
'
|
|
314
|
-
'
|
|
315
|
-
'
|
|
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 ────────────────────────────────────────────────────────────
|
|
@@ -413,29 +413,70 @@ class Spinner {
|
|
|
413
413
|
this.text = this.textos[0];
|
|
414
414
|
this.frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
415
415
|
this.i = 0; this.timer = null; this.desde = 0; this.largura = 0;
|
|
416
|
+
this.detalhe = ''; // o que está chegando agora (act.js atualiza)
|
|
416
417
|
}
|
|
417
418
|
start() {
|
|
418
419
|
// Fora de um terminal (pipe, CI) a animação só sujaria a saída.
|
|
419
420
|
if (!process.stdout.isTTY) return this;
|
|
420
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
|
+
}
|
|
421
437
|
this.timer = setInterval(() => {
|
|
422
438
|
// Com mais de uma frase, troca a cada 4s. A frase nova pode ser mais
|
|
423
439
|
// curta que a anterior: o resto da linha velha ficaria na tela, então
|
|
424
440
|
// a linha é sempre preenchida até a maior largura já escrita.
|
|
425
441
|
if (this.textos.length > 1) this.text = this.textos[Math.floor((Date.now() - this.desde) / 4000) % this.textos.length];
|
|
426
|
-
|
|
442
|
+
/* O relógio e o que está chegando. "Parece travado" era isto: a
|
|
443
|
+
mesma frase girando por um minuto inteiro enquanto o modelo
|
|
444
|
+
escrevia um arquivo grande. Com os segundos contando (e o que
|
|
445
|
+
já chegou, quando há), quem olha sabe que está andando. */
|
|
446
|
+
const seg = Math.floor((Date.now() - this.desde) / 1000);
|
|
447
|
+
const extra = (seg >= 3 ? ` · ${seg}s` : '') + (this.detalhe ? ` · ${this.detalhe}` : '');
|
|
448
|
+
const cru = ` ${this.frames[this.i]} ${this.text}${extra}`;
|
|
427
449
|
const falta = Math.max(0, this.largura - cru.length);
|
|
428
450
|
this.largura = Math.max(this.largura, cru.length);
|
|
429
|
-
process.stdout.write(`\r ${c.muted(this.frames[this.i])} ${c.muted(this.text)}${' '.repeat(falta)}`);
|
|
451
|
+
process.stdout.write(`\r ${c.muted(this.frames[this.i])} ${c.muted(this.text)}${c.dim(extra)}${' '.repeat(falta)}`);
|
|
430
452
|
this.i = (this.i+1) % this.frames.length;
|
|
431
453
|
}, 80);
|
|
432
454
|
return this;
|
|
433
455
|
}
|
|
434
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
|
+
}
|
|
435
463
|
if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(Math.max(80, this.largura + 4))}\r`); }
|
|
436
464
|
}
|
|
437
465
|
}
|
|
438
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
|
+
|
|
439
480
|
// O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
|
|
440
481
|
const ROTULOS = {
|
|
441
482
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
@@ -609,4 +650,5 @@ function friendlyError(raw) {
|
|
|
609
650
|
}
|
|
610
651
|
|
|
611
652
|
|
|
612
|
-
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,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analise.js — silêncio e batida, como conta pura sobre números medidos.
|
|
3
|
+
*
|
|
4
|
+
* O ffmpeg mede (onde o áudio fica abaixo do limiar; as amostras cruas), e
|
|
5
|
+
* as decisões ficam aqui, testáveis sem áudio nenhum:
|
|
6
|
+
*
|
|
7
|
+
* · o SILÊNCIO vira trecho da linha do tempo com `padding` de respiro em
|
|
8
|
+
* cada ponta — cortar no exato instante em que a voz volta é o que faz
|
|
9
|
+
* corte automático soar como máquina (lib/estudio/video.js conta a mesma
|
|
10
|
+
* história). O silêncio no começo e no fim do arquivo sai inteiro;
|
|
11
|
+
* · a BATIDA é o pico de subida de energia (onset), com limiar que se
|
|
12
|
+
* adapta ao volume da música em volta. Não é um detector de tempo de
|
|
13
|
+
* estúdio; é o bastante para cortar no ritmo.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const T = require('./timeline.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Os trechos (quadros da linha do tempo) a apagar, dados os silêncios de
|
|
22
|
+
* cada mídia em segundos da origem: {assetId: [{de, ate}]}.
|
|
23
|
+
*/
|
|
24
|
+
function trechosDeSilencio(tl, clipes, silencios, { padding = 0.15, minimo = 2 } = {}) {
|
|
25
|
+
const ranges = [];
|
|
26
|
+
for (const c of clipes) {
|
|
27
|
+
const lista = silencios[c.asset] || [];
|
|
28
|
+
const dur = (tl.assets[c.asset] || {}).seconds || Infinity;
|
|
29
|
+
const { de, ate } = T.janelaOrigem(tl, c);
|
|
30
|
+
for (const s of lista) {
|
|
31
|
+
const a = s.de <= 0.05 ? s.de : s.de + padding;
|
|
32
|
+
const b = s.ate >= dur - 0.05 ? s.ate : s.ate - padding;
|
|
33
|
+
const ia = Math.max(a, de);
|
|
34
|
+
const ib = Math.min(b, ate);
|
|
35
|
+
if (ib <= ia) continue;
|
|
36
|
+
const fa = T.origemParaQuadro(tl, c, ia);
|
|
37
|
+
const fb = T.origemParaQuadro(tl, c, ib);
|
|
38
|
+
if (fb - fa >= minimo) ranges.push([fa, fb]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ranges;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Amostras (Float32, mono) → {bpm, beats: [segundos]}. */
|
|
45
|
+
function batidas(amostras, taxa, { sensibilidade = 1.4, minimoEntre = 0.22 } = {}) {
|
|
46
|
+
const hop = Math.max(64, Math.round(taxa * 0.0116)); // ~11,6 ms
|
|
47
|
+
const janela = hop * 2;
|
|
48
|
+
const energia = [];
|
|
49
|
+
for (let i = 0; i + janela <= amostras.length; i += hop) {
|
|
50
|
+
let s = 0;
|
|
51
|
+
for (let j = i; j < i + janela; j++) s += amostras[j] * amostras[j];
|
|
52
|
+
energia.push(Math.log(1e-9 + s / janela));
|
|
53
|
+
}
|
|
54
|
+
const onset = energia.map((e, i) => (i ? Math.max(0, e - energia[i - 1]) : 0));
|
|
55
|
+
const porSeg = taxa / hop;
|
|
56
|
+
const raio = Math.round(porSeg * 0.5);
|
|
57
|
+
const beats = [];
|
|
58
|
+
let ultimo = -Infinity;
|
|
59
|
+
for (let i = 1; i < onset.length - 1; i++) {
|
|
60
|
+
const a = Math.max(0, i - raio);
|
|
61
|
+
const b = Math.min(onset.length, i + raio);
|
|
62
|
+
let soma = 0;
|
|
63
|
+
let soma2 = 0;
|
|
64
|
+
for (let j = a; j < b; j++) { soma += onset[j]; soma2 += onset[j] * onset[j]; }
|
|
65
|
+
const n = b - a;
|
|
66
|
+
const media = soma / n;
|
|
67
|
+
const dp = Math.sqrt(Math.max(0, soma2 / n - media * media));
|
|
68
|
+
const limiar = media + sensibilidade * dp;
|
|
69
|
+
let pico = onset[i] > limiar && onset[i] > 0.3;
|
|
70
|
+
for (let j = Math.max(0, i - 3); pico && j <= Math.min(onset.length - 1, i + 3); j++) if (onset[j] > onset[i]) pico = false;
|
|
71
|
+
const t = (i * hop) / taxa;
|
|
72
|
+
if (pico && t - ultimo >= minimoEntre) { beats.push(Math.round(t * 1000) / 1000); ultimo = t; }
|
|
73
|
+
}
|
|
74
|
+
return { bpm: bpmDe(beats), beats };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bpmDe(beats) {
|
|
78
|
+
if (beats.length < 4) return null;
|
|
79
|
+
const iv = [];
|
|
80
|
+
for (let i = 1; i < beats.length; i++) iv.push(beats[i] - beats[i - 1]);
|
|
81
|
+
iv.sort((a, b) => a - b);
|
|
82
|
+
let med = iv[Math.floor(iv.length / 2)];
|
|
83
|
+
let bpm = 60 / med;
|
|
84
|
+
while (bpm < 70) bpm *= 2;
|
|
85
|
+
while (bpm > 180) bpm /= 2;
|
|
86
|
+
return Math.round(bpm);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { trechosDeSilencio, batidas, bpmDe };
|