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 +2 -2
- package/lib/estudio/cinema.js +24 -14
- package/lib/estudio/index.js +26 -2
- 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.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.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.20
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/cinema.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
219
|
-
|
|
220
|
-
|
|
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.
|
|
233
|
-
els.push({ tipo: 'imagem', src: marca.logo, x: Math.round(W / 2 - lh * 1.75), y: Math.round(H * (deitado ? 0.2 : 0.
|
|
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;
|
package/lib/estudio/index.js
CHANGED
|
@@ -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
|
-
|
|
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 : [])) {
|
|
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.
|
|
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": {
|