primocode 9.8.0-beta.19 → 9.8.0-beta.20

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.19
1
+ # PrimoCode v9.8.0-beta.20
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.19
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.20
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -91,7 +91,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
91
91
  fundo: { tipo: 'gradiente', cores: [`rgba(7,7,10,${veu * 0.7})`, `rgba(7,7,10,${veu})`, `rgba(7,7,10,${Math.min(0.95, veu + 0.25)})`], angulo: 180 } });
92
92
  };
93
93
  // O texto: entra palavra por palavra no tempo da voz e fica; as palavras marcadas no gradiente.
94
- const pTexto = (els, texto, de, ate, { y = null, max = deitado ? H * 0.11 : W * 0.1, linhas = 3, peso = 700, caixa = true, marcar = [], cor = BRANCO, pal = [], alinhar = 'center', x = M, larg = W0 } = {}) => {
94
+ const pTexto = (els, texto, de, ate, { y = null, max = deitado ? H * 0.11 : W * 0.13, linhas = 3, peso = 700, caixa = true, marcar = [], cor = BRANCO, pal = [], alinhar = 'center', x = M, larg = W0 } = {}) => {
95
95
  const txt = caixa ? String(texto).toUpperCase() : String(texto);
96
96
  const fit = caber(txt, { largura: larg * 0.86, linhas, max, fonte: F.titulo });
97
97
  const h = fit.tamanho * 1.08 * fit.linhas;
@@ -118,12 +118,18 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
118
118
  const narrada = p.tipo !== 'depoimento';
119
119
  const tp = narrada ? (tempos[narrI++] || null) : null;
120
120
  const off = 0.35; // a voz entra um instante depois do corte
121
- const falaDur = tp ? tp.ate - tp.de : 0;
121
+ const falaDur = tp ? Math.min(tp.ate + 0.6, tempos[narrI] ? tempos[narrI].de : tp.ate + 0.6) - tp.de : 0;
122
122
  const rel = (w) => r2(off + w - (tp ? tp.de : 0)); // tempo da voz → tempo da cena
123
123
  const pals = tp ? tp.palavras || [] : [];
124
124
  let dur;
125
125
 
126
- if (tp && narracao) trechos.push({ src: narracao, t: r2(t + off), de: r2(tp.de), ate: r2(tp.ate + 0.05), volume: 1 });
126
+ /* O fim da fala: a transcrição marca o fim da última palavra um pouco
127
+ antes do fim de verdade, e cortar ali comia a última sílaba ("parece
128
+ que a narração não terminou", 25/09). O trecho vai até perto do
129
+ começo da próxima fala, com folga. */
130
+ const proxFala = tempos[narrI] ? tempos[narrI].de : null;
131
+ const fimFala = tp ? r2(Math.min(proxFala != null ? proxFala - 0.02 : tp.ate + 0.6, tp.ate + 0.6)) : 0;
132
+ if (tp && narracao) trechos.push({ src: narracao, t: r2(t + off), de: r2(tp.de), ate: fimFala, volume: 1 });
127
133
 
128
134
  const textosDaParte = p.tipo === 'escada' ? [...(p.itens || []), p.final].filter(Boolean)
129
135
  : p.tipo === 'numeros' ? [p.antes, ...(p.itens || [])].filter(Boolean)
@@ -144,7 +150,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
144
150
  if (p.tipo === 'frase') {
145
151
  if (i === 0) { som(t + 0.1, 'teclado', 0.5); vol(t, 0.12); }
146
152
  linhaFina(els, H * 0.5 - base * 0.12, 0.2);
147
- textosDaParte.forEach((x, k) => pTexto(els, x, inicios[k], fimDe(k), { caixa: false, peso: 600, max: deitado ? H * 0.1 : W * 0.095, marcar: p.marcar || [], pal: palDo(x) }));
153
+ textosDaParte.forEach((x, k) => pTexto(els, x, inicios[k], fimDe(k), { caixa: false, peso: 600, max: deitado ? H * 0.1 : W * 0.12, linhas: deitado ? 3 : 4, marcar: p.marcar || [], pal: palDo(x) }));
148
154
  } else if (p.tipo === 'montagem' || p.tipo === 'climax') {
149
155
  const rap = p.tipo === 'climax' ? 0.38 : (p.ritmo || 0.75);
150
156
  const fimFotos = p.tipo === 'climax' && textosDaParte.length ? Math.max(1.8, inicios[0] - 0.4) : dur;
@@ -171,7 +177,8 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
171
177
  vol(t, 0.22);
172
178
  if (sons.sala) trechos.push({ src: sons.sala, t: r2(t), de: 0, ate: dur, volume: 0.25 });
173
179
  // As fotos passam devagar numa janela, à direita (ou em cima, no vertical).
174
- const jan = deitado ? { x: W * 0.56, y: M, l: W * 0.44 - M, a: H - 2 * M } : { x: M, y: M, l: W0, a: H * 0.36 };
180
+ // No vertical a janela desce da zona do topo do Instagram e fica maior.
181
+ const jan = deitado ? { x: W * 0.56, y: M, l: W * 0.44 - M, a: H - 2 * M } : { x: M, y: H * 0.1, l: W0, a: H * 0.42 };
175
182
  const itens = p.itens || [];
176
183
  const hItem = Math.round(deitado ? H * 0.1 : W * 0.12);
177
184
  const fimItens = p.final ? inicios[itens.length] : dur;
@@ -181,7 +188,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
181
188
  anima: { ...movs[mov++ % movs.length](1.3), opacidade: [{ t: 0, v: 0 }, { t: 0.3, v: 1 }] } });
182
189
  }
183
190
  itens.forEach((x, k) => {
184
- const yy = deitado ? H * 0.5 - (itens.length * hItem * 1.25) / 2 + k * hItem * 1.25 : H * 0.48 + k * hItem * 1.3;
191
+ const yy = deitado ? H * 0.5 - (itens.length * hItem * 1.25) / 2 + k * hItem * 1.25 : H * 0.56 + k * hItem * 1.25;
185
192
  pTexto(els, x, inicios[k], fimItens, { y: yy, max: hItem, linhas: 1, alinhar: 'left', larg: deitado ? W * 0.48 : W0, peso: 800, pal: palDo(x) });
186
193
  });
187
194
  if (p.final) pTexto(els, p.final, fimItens, dur, { marcar: p.marcar || [], pal: palDo(p.final) });
@@ -193,9 +200,9 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
193
200
  els.push({ tipo: 'texto', texto: String(p.num), fonte: F.titulo, peso: 800, tamanho: tamN, cor: 'rgba(149,103,219,0.12)', contorno: `${Math.max(3, Math.round(base * 0.004))}px ${DEST}`,
194
201
  x: M, y: Math.round(deitado ? H * 0.12 : H * 0.14), largura: W0, alinhamento: deitado ? 'left' : 'center',
195
202
  anima: { opacidade: [{ t: 0, v: 0 }, { t: 0.6, v: 1 }], x: [{ t: 0, v: -20 }, { t: dur, v: 20, e: 'linear' }] } });
196
- const yT = deitado ? H * 0.5 : H * 0.44;
203
+ const yT = deitado ? H * 0.5 : H * 0.42;
197
204
  const hT = pTexto(els, p.titulo, inicios[1] || 0.4, dur, { y: yT, max: deitado ? H * 0.11 : W * 0.13, linhas: 1, alinhar: deitado ? 'left' : 'center', larg: W0, peso: 800, pal: palDo(p.titulo) });
198
- (p.linhas || []).forEach((x, k) => pTexto(els, x, inicios[2 + k] || 0.8 + k * 0.6, dur, { y: yT + hT + H * 0.04 + k * base * 0.085, max: base * 0.06, linhas: 1, caixa: false, peso: 500, cor: 'rgba(245,245,247,0.8)',
205
+ (p.linhas || []).forEach((x, k) => pTexto(els, x, inicios[2 + k] || 0.8 + k * 0.6, dur, { y: yT + hT + H * 0.03 + k * base * (deitado ? 0.085 : 0.1), max: base * (deitado ? 0.06 : 0.07), linhas: 1, caixa: false, peso: 500, cor: 'rgba(245,245,247,0.8)',
199
206
  alinhar: deitado ? 'left' : 'center', larg: W0, pal: palDo(x) }));
200
207
  } else if (p.tipo === 'depoimento') {
201
208
  /* O aluno falando, com a voz dele; a música quase some. CADA depoimento
@@ -215,9 +222,10 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
215
222
  if (c.nome) el2.push({ tipo: 'texto', texto: c.nome.toUpperCase(), fonte: F.texto, peso: 700, tamanho: Math.round(H * 0.03), cor: DEST, espacamento: 3, x: xT, y: Math.round(H * 0.8), de: 0.5 });
216
223
  } else {
217
224
  el2.push({ tipo: 'video', src: c.src, volume: 1, deOrigem: c.de, ateOrigem: c.ate + 0.3, x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 30%' });
218
- el2.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(7,7,10,0)', 'rgba(7,7,10,0.1)', 'rgba(7,7,10,0.85)'], angulo: 180 } });
219
- pTexto(el2, `“${c.frase}”`, 0.3, d, { y: H * 0.68, caixa: false, peso: 600, max: W * 0.075, linhas: 4 });
220
- if (c.nome) el2.push({ tipo: 'texto', texto: c.nome.toUpperCase(), fonte: F.texto, peso: 700, tamanho: Math.round(W * 0.035), cor: DEST, espacamento: 3, x: M, y: Math.round(H * 0.9), largura: W0, alinhamento: 'center', de: 0.5 });
225
+ el2.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(7,7,10,0)', 'rgba(7,7,10,0.35)', 'rgba(7,7,10,0.9)'], angulo: 180 } });
226
+ // Tudo acima de 75% da altura: embaixo disso o Instagram põe a legenda e os botões.
227
+ pTexto(el2, `“${c.frase}”`, 0.3, d, { y: H * 0.54, caixa: false, peso: 600, max: W * 0.08, linhas: 4 });
228
+ if (c.nome) el2.push({ tipo: 'texto', texto: c.nome.toUpperCase(), fonte: F.texto, peso: 700, tamanho: Math.round(W * 0.035), cor: DEST, espacamento: 3, x: M, y: Math.round(H * 0.72), largura: W0, alinhamento: 'center', de: 0.5 });
221
229
  }
222
230
  if (n < clipes.length - 1) { cenas.push(ce); t += d; } else { Object.assign(cena, ce); dur = d; }
223
231
  });
@@ -229,17 +237,19 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
229
237
  const R = Math.round(Math.max(W, H) * 0.7);
230
238
  els.push({ tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(W / 2 - R / 2), y: Math.round(H * 0.42 - R / 2), cor: grad[1], opacidade: 0.18, anima: { desfoque: [{ t: 0, v: Math.round(R * 0.25) }], escala: [{ t: 0, v: 0.8 }, { t: dur, v: 1.15, e: 'suave' }] } });
231
239
  if (marca.logo) {
232
- const lh = Math.round(base * (deitado ? 0.13 : 0.1));
233
- els.push({ tipo: 'imagem', src: marca.logo, x: Math.round(W / 2 - lh * 1.75), y: Math.round(H * (deitado ? 0.2 : 0.26)), largura: Math.round(lh * 3.5), altura: lh, ajuste: 'contain',
240
+ const lh = Math.round(base * (deitado ? 0.13 : 0.16));
241
+ els.push({ tipo: 'imagem', src: marca.logo, x: Math.round(W / 2 - lh * 1.75), y: Math.round(H * (deitado ? 0.2 : 0.3)), largura: Math.round(lh * 3.5), altura: lh, ajuste: 'contain',
234
242
  anima: { opacidade: [{ t: 0, v: 0 }, { t: 1.0, v: 1 }, { t: dur - 1.2, v: 1 }, { t: dur, v: 0 }], escala: [{ t: 0, v: 0.94 }, { t: dur, v: 1.02, e: 'linear' }] } });
235
243
  }
236
244
  const y0 = H * (deitado ? 0.44 : 0.42);
237
- textosDaParte.forEach((x, k) => pTexto(els, x, inicios[k], k < textosDaParte.length - 1 ? fimDe(k) : dur, { y: y0 + (k % 2) * 0, max: k === textosDaParte.length - 1 ? base * 0.1 : base * 0.08, peso: k === textosDaParte.length - 1 ? 800 : 600,
245
+ textosDaParte.forEach((x, k) => pTexto(els, x, inicios[k], k < textosDaParte.length - 1 ? fimDe(k) : dur, { y: y0 + (k % 2) * 0, max: k === textosDaParte.length - 1 ? base * (deitado ? 0.1 : 0.12) : base * (deitado ? 0.08 : 0.1), peso: k === textosDaParte.length - 1 ? 800 : 600,
238
246
  marcar: p.marcar || [], cor: k === textosDaParte.length - 1 ? DEST : BRANCO, pal: palDo(x) }));
239
247
  pontos(els, 0.3);
240
248
  cena.transicao = { tipo: 'esmaecer', duracao: 0.6 };
241
249
  }
242
250
 
251
+ // Entre as partes, um esmaecer curto: corte seco em tudo parecia fala interrompida.
252
+ if (i > 0 && cena.transicao.tipo === 'corte' && !['montagem', 'climax'].includes(p.tipo)) cena.transicao = { tipo: 'esmaecer', duracao: 0.35 };
243
253
  cena.duracao = dur;
244
254
  cenas.push(cena);
245
255
  t += dur;
@@ -751,6 +751,24 @@ async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
751
751
  }
752
752
  }
753
753
 
754
+ /* A MÚSICA TEM LETRA? O acervo não diz se a faixa é instrumental, então ela
755
+ é ouvida: 25 s do meio vão para a transcrição, e mais de 6 palavras é voz
756
+ cantada. Sem servidor para ouvir, a faixa passa (melhor música que nenhuma). */
757
+ async function temLetra(arquivo) {
758
+ try {
759
+ const m = await video.medir(arquivo);
760
+ const ini = Math.max(0, (m.segundos || 60) * 0.35);
761
+ const amostra = arquivo.replace(/\.[a-z0-9]+$/i, '') + '.amostra.mp3';
762
+ const c = await video.rodar(video.ffmpeg(), ['-y', '-ss', String(ini), '-t', '25', '-i', arquivo, '-ac', '1', '-ar', '16000', '-b:a', '32k', amostra], { timeout: 60000 });
763
+ if (!c.ok) return false;
764
+ const cfg = require('../config').loadConfig();
765
+ const r = await require('../api').requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(amostra).toString('base64'), nome: 'musica.mp3', idioma: 'en' });
766
+ try { fs.unlinkSync(amostra); } catch { /* fica */ }
767
+ const palavras = (r && r.palavras ? r.palavras.length : String((r && r.texto) || '').split(/\s+/).filter(Boolean).length);
768
+ return palavras > 6;
769
+ } catch { return false; }
770
+ }
771
+
754
772
  /* OS SONS DE VERDADE: gravação real de acervo aberto (teclado, whoosh
755
773
  cinematográfico, impacto grave, som de sala), e o sintetizado só quando a
756
774
  busca falha. "Esses efeitos sonoros estão genéricos e sem impacto."
@@ -899,11 +917,17 @@ async function prepararRoteiro(p, args) {
899
917
  const sonsC = await sonsReais(p, video);
900
918
  let trilhaC = null;
901
919
  // Vários termos e durações: vídeo sem música foi o que saiu na primeira vez (25/09).
902
- for (const [termo, min] of [[roteiro.musica, 60], ['cinematic inspiring', 70], ['inspiring piano cinematic', 60], ['epic cinematic', 60], ['inspiring', 45]]) {
920
+ // E só instrumental: "essa música daria pra deixar só o instrumental?" — a faixa é OUVIDA.
921
+ for (const [termo, min] of [[roteiro.musica, 60], ['instrumental cinematic inspiring', 70], ['instrumental piano cinematic', 60], ['instrumental epic cinematic', 60], ['instrumental ambient inspiring', 45], ['instrumental', 45]]) {
903
922
  if (!termo || trilhaC) continue;
904
923
  try {
905
924
  const b = await assets.procurarMusica(termo, { quantas: 6, duracaoMin: min });
906
- for (const item of (b.ok ? b.sons : [])) { const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' }); if (d.ok) { trilhaC = d.relativo; break; } }
925
+ for (const item of (b.ok ? b.sons : [])) {
926
+ const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
927
+ if (!d.ok) continue;
928
+ if (await temLetra(path.join(p.pasta, d.relativo))) { projeto.anotar(p.id, 'midia', `trilha com voz descartada: ${item.titulo}`); continue; }
929
+ trilhaC = d.relativo; break;
930
+ }
907
931
  } catch { /* o próximo termo */ }
908
932
  }
909
933
  const fotosC = acervo.fotos.map((f) => ({ src: f.src, tipo: 'imagem' }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.19",
3
+ "version": "9.8.0-beta.20",
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": {