primocode 9.8.0-beta.22 → 9.8.0-beta.23

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.22
1
+ # PrimoCode v9.8.0-beta.23
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.22
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.23
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -35,11 +35,46 @@ const tokens = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').t
35
35
  const leitura = (t) => 0.55 + 0.22 * String(t || '').split(/\s+/).filter(Boolean).length;
36
36
 
37
37
  /** A fala desta parte (o que a voz diz): a "fala", ou os textos da tela. */
38
+ /* A voz lê EXATAMENTE o que está na tela, na ordem da tela ("a narração tem
39
+ que ser a mesma dos textos", 25/09). Caixa alta vira frase normal (senão a
40
+ voz soletra), "IA" fica "IA", o separador vira pausa e "/SC" sai da fala. */
41
+ function paraFala(x) {
42
+ let t = String(x || '').replace(/\b0(\d)\b/g, '$1').replace(/[·→]/g, ',').replace(/\/(SC|SP|RJ|PR|RS|MG)\b/g, '').replace(/\s+,/g, ',').trim();
43
+ if (t && t === t.toUpperCase()) {
44
+ // Sigla de até 3 letras fica sigla ("AI" minúsculo a voz lê como "ai").
45
+ const SIGLAS = new Set(['IA', 'AI', 'TI', 'RH', 'CEO', 'KPI', 'BI', 'CRM', 'ERP', 'SC', 'SP', 'PR', 'RS', 'RJ', 'MG']);
46
+ t = t.split(/(\s+)/).map((w) => (SIGLAS.has(w.replace(/[:.,!?]$/, '')) ? w : w.toLowerCase())).join('')
47
+ .replace(/(^|[.!?:]\s+)(\p{L})/gu, (m, a, b) => a + b.toUpperCase());
48
+ }
49
+ t = t.replace(/[,;:\s]+$/, '');
50
+ if (!/[.!?]$/.test(t)) t += '.';
51
+ return t;
52
+ }
38
53
  function falaDe(p) {
39
54
  if (p.fala) return p.fala;
40
55
  if (p.tipo === 'depoimento') return '';
41
- const partes = [p.antes, ...(p.linhas || []), ...(p.itens || []), p.num ? `Dia ${p.num}.` : '', p.titulo, p.final];
42
- return partes.filter(Boolean).map((x) => String(x).replace(/[·→]/g, ',')).join(' ');
56
+ const tela = p.tipo === 'dia' ? [`Dia ${p.num}`, p.titulo, ...(p.linhas || [])]
57
+ : p.tipo === 'escada' ? [...(p.itens || []), p.final]
58
+ : p.tipo === 'numeros' ? [p.antes, ...(p.itens || [])]
59
+ : (p.linhas || []);
60
+ /* A linha só emenda na seguinte quando não termina em pontuação E a
61
+ seguinte começa em minúscula ("E se 3 dias fossem suficientes" / "para
62
+ mudar…"); o resto é frase inteira. A frase repetida em seguida
63
+ ("CONSTRUIR" / "Construir.") a voz diz uma vez só. */
64
+ const linhas = tela.filter(Boolean).map((x) => String(x).trim());
65
+ const frases = [];
66
+ let atual = '';
67
+ linhas.forEach((bruto, k) => {
68
+ const prox = linhas[k + 1];
69
+ const emenda = !/[.!?…·→:]$/.test(bruto) && prox && /^[a-zà-ú]/.test(prox);
70
+ let pedaco = paraFala(bruto);
71
+ if (emenda) pedaco = pedaco.replace(/\.$/, '');
72
+ atual = atual ? `${atual} ${pedaco.charAt(0).toLowerCase()}${pedaco.slice(1)}` : pedaco;
73
+ if (!emenda) { frases.push(atual); atual = ''; }
74
+ });
75
+ if (atual) frases.push(atual);
76
+ const chave = (f) => f.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
77
+ return frases.filter((f, k) => k === 0 || chave(f) !== chave(frases[k - 1])).join(' ');
43
78
  }
44
79
 
45
80
  /* Quando cada texto começa, pelo tempo das palavras da voz: procura a
@@ -351,4 +386,4 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
351
386
  };
352
387
  }
353
388
 
354
- module.exports = { montarCinema, falaDe, quando, leitura };
389
+ module.exports = { montarCinema, falaDe, paraFala, quando, leitura };
@@ -733,11 +733,22 @@ async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
733
733
  try {
734
734
  const cfg = require('../config').loadConfig();
735
735
  const api = require('../api');
736
- const r = await api.request(cfg.server, '/api/voz', {
737
- texto: lista.map((x) => x.texto).join('\n\n'),
738
- voz: VOZ_GEMINI[String(voz || '').toLowerCase()] || voz || 'Charon',
739
- ...(direcao ? { direcao } : {}),
740
- }, cfg.token);
736
+ /* A cota grátis da voz é por MINUTO e pequena: "quota" é esperar e
737
+ tentar de novo, não desistir para a voz de reserva (medido em 25/09:
738
+ a mesma chamada passava meio minuto depois). */
739
+ let r = null;
740
+ for (let tentativa = 0; tentativa < 4; tentativa++) {
741
+ try {
742
+ r = await api.request(cfg.server, '/api/voz', {
743
+ texto: lista.map((x) => x.texto).join('\n\n'),
744
+ voz: VOZ_GEMINI[String(voz || '').toLowerCase()] || voz || 'Charon',
745
+ ...(direcao ? { direcao } : {}),
746
+ }, cfg.token);
747
+ } catch (e) { r = { ok: false, erro: e.message }; }
748
+ if (r && r.ok) break;
749
+ if (!/quota|rate|429|exhaust/i.test(String(r && r.erro))) break;
750
+ await new Promise((ok) => setTimeout(ok, 30000));
751
+ }
741
752
  if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'sem voz no servidor' };
742
753
  const pcm = mp3.replace(/\.mp3$/, '.pcm');
743
754
  fs.writeFileSync(pcm, Buffer.from(r.audio, 'base64'));
@@ -813,6 +824,9 @@ async function prepararDepoimento(p, c) {
813
824
  if (iniW) de = Math.max(0, iniW.de - 0.08);
814
825
  if (fimW) ate = fimW.ate;
815
826
  else { const fimWs = ws.filter((w) => w.ate <= c.ate + 0.3); if (fimWs.length) ate = fimWs[fimWs.length - 1].ate; }
827
+ // A frase na tela é o que a pessoa FALA no trecho — palavra por palavra.
828
+ const ditas = ws.filter((w) => w.de >= de - 0.05 && w.ate <= ate + 0.05).map((w) => w.texto).join(' ').trim();
829
+ if (ditas.split(/\s+/).length >= 3) c.fraseDita = ditas.replace(/^./, (x) => x.toUpperCase()).replace(/[,;]$/, '.');
816
830
  } catch { /* fica o trecho pedido */ }
817
831
  const dur = ate - de + 0.45; // um respiro curto depois da última palavra
818
832
  const saida = path.join(p.pasta, 'midia', `depo-${path.basename(c.src, path.extname(c.src))}-${Math.round(de)}.mp4`);
@@ -820,7 +834,8 @@ async function prepararDepoimento(p, c) {
820
834
  '-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`,
821
835
  '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '18', '-c:a', 'aac', '-b:a', '192k', saida], { timeout: 5 * 60000 });
822
836
  if (!r.ok) return {};
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 } };
837
+ return { src: path.relative(p.pasta, saida).replace(/\\/g, '/'), de: 0, ate: Math.round((dur - 0.3) * 100) / 100, pronto: true,
838
+ origem: { src: c.src, de: c.de, ate: c.ate, frase: c.frase }, ...(c.fraseDita ? { frase: c.fraseDita } : {}) };
824
839
  }
825
840
 
826
841
  /* A narração com cache (a mesma fala, na mesma direção, não é gerada duas vezes). */
@@ -1055,7 +1070,9 @@ async function prepararRoteiro(p, args) {
1055
1070
  if (pt.tipo === 'depoimento') continue;
1056
1071
  if (pt.falaForte) {
1057
1072
  const arqF = path.join(p.pasta, 'midia', `voz-forte-${i + 1}-${Date.now().toString(36)}.mp3`);
1058
- const vf = await narrarComCache([{ texto: pt.falaForte }], arqF, { voz: args.voz || roteiro.voz,
1073
+ // falaForte: true → a voz forte lê o texto da tela.
1074
+ const textoForte = pt.falaForte === true ? cinema.falaDe({ ...pt, fala: undefined }) : pt.falaForte;
1075
+ const vf = await narrarComCache([{ texto: textoForte }], arqF, { voz: args.voz || roteiro.voz,
1059
1076
  // 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
1077
  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
1078
  + 'confiante e natural, só um pouco mais de ênfase nas palavras principais. Sem dramatizar.' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.22",
3
+ "version": "9.8.0-beta.23",
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": {