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 +2 -2
- package/lib/estudio/cinema.js +38 -3
- package/lib/estudio/index.js +24 -7
- 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.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.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.23
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/cinema.js
CHANGED
|
@@ -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
|
|
42
|
-
|
|
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 };
|
package/lib/estudio/index.js
CHANGED
|
@@ -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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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": {
|