primocode 9.8.0-beta.21 → 9.8.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/estudio/index.js +50 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
1
|
+
# PrimoCode v9.8.0-beta.22
|
|
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 v9.8.0-beta.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.22
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/index.js
CHANGED
|
@@ -701,11 +701,21 @@ function alinharPartes(lista, palavras, total) {
|
|
|
701
701
|
for (let i = 1; i < lista.length; i++) {
|
|
702
702
|
acum += chars[i - 1];
|
|
703
703
|
const esperado = Math.round(ws.length * acum / soma);
|
|
704
|
-
|
|
704
|
+
/* As DUAS primeiras palavras da parte, seguidas: achar só pela primeira
|
|
705
|
+
palavra longa deixava o "E" de "E sair com algo" no fim da fala
|
|
706
|
+
anterior, e ele soava duas vezes (medido em 25/09, aos 38 s). */
|
|
707
|
+
const tk = tokens(lista[i].texto);
|
|
705
708
|
let achado = -1;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
+
for (let j = Math.max(cursor + 1, esperado - 14); j < Math.min(ws.length, esperado + 14); j++) {
|
|
710
|
+
if (ws[j].k === tk[0] && (!tk[1] || (ws[j + 1] && ws[j + 1].k === tk[1]))) { achado = j; break; }
|
|
711
|
+
}
|
|
712
|
+
if (achado < 0) {
|
|
713
|
+
const alvo = tk.find((w) => w.length >= 3) || tk[0];
|
|
714
|
+
for (let j = Math.max(cursor + 1, esperado - 12); j < Math.min(ws.length, esperado + 12); j++) {
|
|
715
|
+
if (ws[j].k === alvo) { achado = j; break; }
|
|
716
|
+
}
|
|
717
|
+
// A palavra curta que vem antes da achada ("e", "a", "o") é desta parte, não da anterior.
|
|
718
|
+
while (achado > cursor + 1 && ws[achado - 1].k.length <= 2 && tk.slice(0, 3).includes(ws[achado - 1].k)) achado--;
|
|
709
719
|
}
|
|
710
720
|
const idx = achado > cursor ? achado : Math.max(cursor + 1, Math.min(ws.length - 1, esperado));
|
|
711
721
|
inicios.push(idx);
|
|
@@ -791,20 +801,26 @@ async function prepararDepoimento(p, c) {
|
|
|
791
801
|
try { fs.unlinkSync(amostra); } catch { /* fica */ }
|
|
792
802
|
const base0 = Math.max(0, c.de - 1.5);
|
|
793
803
|
const ws = ((tr && tr.palavras) || []).map((w) => ({ ...w, de: w.de + base0, ate: w.ate + base0 }));
|
|
804
|
+
/* Só a FRASE de efeito: começa na primeira palavra dela e termina na
|
|
805
|
+
última. "Fala a frase que tem que falar e continua antes de trocar"
|
|
806
|
+
(25/09): o resto da conversa não tem nada a ver com o vídeo. */
|
|
794
807
|
const alvo = tokens(c.frase || '');
|
|
795
|
-
|
|
796
|
-
const iniW = ws.find((w) => w.de >= c.de -
|
|
797
|
-
|
|
808
|
+
const k0 = (w) => tokens(w.texto)[0] || '';
|
|
809
|
+
const iniW = ws.find((w) => w.de >= c.de - 1.2 && alvo.length && k0(w) === alvo[0])
|
|
810
|
+
|| ws.find((w) => w.de >= c.de - 0.6 && alvo.includes(k0(w)));
|
|
811
|
+
const ultimo = alvo[alvo.length - 1];
|
|
812
|
+
const fimW = [...ws].reverse().find((w) => w.ate <= c.ate + 1.5 && (!iniW || w.de > iniW.de) && k0(w) === ultimo);
|
|
798
813
|
if (iniW) de = Math.max(0, iniW.de - 0.08);
|
|
799
|
-
if (
|
|
814
|
+
if (fimW) ate = fimW.ate;
|
|
815
|
+
else { const fimWs = ws.filter((w) => w.ate <= c.ate + 0.3); if (fimWs.length) ate = fimWs[fimWs.length - 1].ate; }
|
|
800
816
|
} catch { /* fica o trecho pedido */ }
|
|
801
|
-
const dur = ate - de + 0.
|
|
817
|
+
const dur = ate - de + 0.45; // um respiro curto depois da última palavra
|
|
802
818
|
const saida = path.join(p.pasta, 'midia', `depo-${path.basename(c.src, path.extname(c.src))}-${Math.round(de)}.mp4`);
|
|
803
819
|
const r = await video.rodar(video.ffmpeg(), ['-y', '-ss', String(de), '-t', String(dur), '-i', origem,
|
|
804
|
-
'-af', `loudnorm=I=-14:TP=-1.5:LRA=9,afade=t=in:d=0.
|
|
820
|
+
'-af', `loudnorm=I=-14:TP=-1.5:LRA=9,afade=t=in:d=0.1,afade=t=out:st=${Math.max(0, dur - 0.4).toFixed(2)}:d=0.4`,
|
|
805
821
|
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '18', '-c:a', 'aac', '-b:a', '192k', saida], { timeout: 5 * 60000 });
|
|
806
822
|
if (!r.ok) return {};
|
|
807
|
-
return { src: path.relative(p.pasta, saida).replace(/\\/g, '/'), de: 0, ate: Math.round((dur - 0.3) * 100) / 100, pronto: true };
|
|
823
|
+
return { src: path.relative(p.pasta, saida).replace(/\\/g, '/'), de: 0, ate: Math.round((dur - 0.3) * 100) / 100, pronto: true, origem: { src: c.src, de: c.de, ate: c.ate } };
|
|
808
824
|
}
|
|
809
825
|
|
|
810
826
|
/* A narração com cache (a mesma fala, na mesma direção, não é gerada duas vezes). */
|
|
@@ -946,6 +962,17 @@ async function prepararRoteiro(p, args) {
|
|
|
946
962
|
if (fs.existsSync(cacheMp3) && fs.existsSync(cacheJson)) {
|
|
947
963
|
fs.copyFileSync(cacheMp3, mp3);
|
|
948
964
|
v = { ok: true, ...JSON.parse(fs.readFileSync(cacheJson, 'utf8')), doCache: true };
|
|
965
|
+
// Cache sem as palavras (vindo de um plano antigo): transcreve de novo e realinha — sem gastar voz.
|
|
966
|
+
if (!(v.partes || []).some((pt) => (pt.palavras || []).length)) {
|
|
967
|
+
try {
|
|
968
|
+
const cfg = require('../config').loadConfig();
|
|
969
|
+
const tr = await require('../api').requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
970
|
+
if (tr && (tr.palavras || []).length) {
|
|
971
|
+
v.partes = alinharPartes(lista, tr.palavras, v.segundos);
|
|
972
|
+
fs.writeFileSync(cacheJson, JSON.stringify({ voz: v.voz, segundos: v.segundos, partes: v.partes }));
|
|
973
|
+
}
|
|
974
|
+
} catch { /* fica o alinhamento guardado */ }
|
|
975
|
+
}
|
|
949
976
|
} else {
|
|
950
977
|
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz,
|
|
951
978
|
direcao: 'Leia em português do Brasil como narrador de trailer de marca premium: voz grave, confiante e humana, '
|
|
@@ -997,6 +1024,15 @@ async function prepararRoteiro(p, args) {
|
|
|
997
1024
|
}
|
|
998
1025
|
} catch { /* o próximo termo */ }
|
|
999
1026
|
}
|
|
1027
|
+
/* A MÚSICA mais baixa na origem: a faixa vem masterizada alta e comprimida,
|
|
1028
|
+
e mesmo a 30% ela cobria a voz e o depoimento ("a música tá muito
|
|
1029
|
+
alta, não dá pra ouvir as pessoas", 25/09). Ela vai a -28 LUFS antes
|
|
1030
|
+
da mistura; os volumes do plano partem daí. */
|
|
1031
|
+
if (trilhaC) {
|
|
1032
|
+
const norm = path.join(p.pasta, 'midia', 'trilha-baixa-' + path.basename(trilhaC).replace(/\.[a-z0-9]+$/i, '.mp3'));
|
|
1033
|
+
const c = await video.rodar(video.ffmpeg(), ['-y', '-i', path.join(p.pasta, trilhaC), '-af', 'loudnorm=I=-28:TP=-6:LRA=11', '-ar', '44100', norm], { timeout: 5 * 60000 });
|
|
1034
|
+
if (c.ok) trilhaC = path.relative(p.pasta, norm).replace(/\\/g, '/');
|
|
1035
|
+
}
|
|
1000
1036
|
/* AS FOTOS, separadas e enquadradas. "Uma sala, 20 cadeiras" com foto de
|
|
1001
1037
|
dashboard "não tem nada a ver" (25/09): foto de gente e print de tela
|
|
1002
1038
|
são coisas diferentes, e a miniatura de depoimento fica de fora. E
|
|
@@ -1020,8 +1056,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1020
1056
|
if (pt.falaForte) {
|
|
1021
1057
|
const arqF = path.join(p.pasta, 'midia', `voz-forte-${i + 1}-${Date.now().toString(36)}.mp3`);
|
|
1022
1058
|
const vf = await narrarComCache([{ texto: pt.falaForte }], arqF, { voz: args.voz || roteiro.voz,
|
|
1023
|
-
|
|
1024
|
-
|
|
1059
|
+
// A MESMA voz do narrador, só com um pouco mais de ênfase: "não é mudar a voz, é só um pouco a entonação" (25/09).
|
|
1060
|
+
direcao: pt.entonacao || 'Leia em português do Brasil com a mesma voz e o mesmo ritmo de um narrador de trailer de marca: '
|
|
1061
|
+
+ 'confiante e natural, só um pouco mais de ênfase nas palavras principais. Sem dramatizar.' });
|
|
1025
1062
|
if (vf.ok) fortes[iNarr] = { src: path.relative(p.pasta, arqF).replace(/\\/g, '/'), segundos: vf.segundos, palavras: (vf.partes[0] || {}).palavras || [] };
|
|
1026
1063
|
}
|
|
1027
1064
|
iNarr++;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.22",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|