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 CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v9.8.0-beta.21
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.21
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.22
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -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
- const alvo = tokens(lista[i].texto).find((w) => w.length >= 3) || tokens(lista[i].texto)[0];
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
- // Procura perto de onde a proporção diz que a parte começa.
707
- for (let j = Math.max(cursor + 1, esperado - 12); j < Math.min(ws.length, esperado + 12); j++) {
708
- if (ws[j].k === alvo) { achado = j; break; }
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
- // A primeira palavra da frase depois do começo pedido, e a última antes do fim pedido.
796
- const iniW = ws.find((w) => w.de >= c.de - 0.6 && (!alvo.length || alvo.includes(tokens(w.texto)[0])));
797
- const fimWs = ws.filter((w) => w.ate <= c.ate + 0.8);
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 (fimWs.length) ate = fimWs[fimWs.length - 1].ate;
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.7; // o respiro depois da última palavra
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.12,afade=t=out:st=${Math.max(0, dur - 0.55).toFixed(2)}:d=0.55`,
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
- direcao: pt.entonacao || 'Leia em português do Brasil como a VIRADA de um trailer: voz grave, intensa e muito firme, '
1024
- + 'com peso em cada palavra, pausa dramática no meio, sem gritar.' });
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.21",
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": {