primocode 9.8.0-beta.14 → 9.8.0-beta.15
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/edicao.js +44 -8
- package/lib/estudio/index.js +58 -3
- 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.15
|
|
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.15
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -32,7 +32,10 @@ const VAZIAS = new Set((
|
|
|
32
32
|
+ 'ou mas como mais menos muito muita muitos muitas isso isto esse essa este esta aquele aquela ele ela '
|
|
33
33
|
+ 'eles elas eu voce voces nos meu minha seu sua nosso nossa tem ter ser foi era sao esta estao vai vou '
|
|
34
34
|
+ 'ja ai aqui la entao tambem so quando onde porque pois ate sobre depois antes cada todo toda todos '
|
|
35
|
-
+ 'todas qual quais quem nao sim ne ta tipo coisa coisas gente fazer faz feito pode podem vamos hoje'
|
|
35
|
+
+ 'todas qual quais quem nao sim ne ta tipo coisa coisas gente fazer faz feito pode podem vamos hoje '
|
|
36
|
+
// Verbos de ligação e de fala: não são a ideia da frase.
|
|
37
|
+
+ 'mostra mostrar ajuda ajudar deixa deixar usar usa usando sabe saber quer querer precisa precisar '
|
|
38
|
+
+ 'acho olha vem fica ficar falar fala dizer diz ver vai ir estou estava sendo tendo existe chamado'
|
|
36
39
|
).split(' '));
|
|
37
40
|
|
|
38
41
|
/* A palavra (sem acento, minúscula, começo dela) → o emoji colorido. Casa
|
|
@@ -208,8 +211,8 @@ function camera(i, dur) {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
const ESTILOS = {
|
|
211
|
-
viral: { icones: true, letreiro: true, sons: true, barra: true, apoio: true, destaque: '#FFD400', forte: '#00E0A4' },
|
|
212
|
-
limpo: { icones: false, letreiro: false, sons: false, barra: false, apoio: true, destaque: '#FFFFFF', forte: '#FFD400' },
|
|
214
|
+
viral: { icones: true, letreiro: true, sons: true, barra: true, apoio: true, recorte: true, fundoRecorte: ['#1B1F3B', '#3A1C71'], destaque: '#FFD400', forte: '#00E0A4' },
|
|
215
|
+
limpo: { icones: false, letreiro: false, sons: false, barra: false, apoio: true, recorte: false, fundoRecorte: ['#111111', '#333333'], destaque: '#FFFFFF', forte: '#FFD400' },
|
|
213
216
|
};
|
|
214
217
|
|
|
215
218
|
/**
|
|
@@ -225,7 +228,10 @@ const ESTILOS = {
|
|
|
225
228
|
* @param {object} op.sons { pop, whoosh } — caminhos na pasta do projeto, opcionais
|
|
226
229
|
* @param {Set} op.iconesOk os nomes de ícone que existem de verdade (sem isso, todos valem)
|
|
227
230
|
*/
|
|
228
|
-
|
|
231
|
+
// Em quais cenas a pessoa é recortada, com a palavra gigante atrás dela.
|
|
232
|
+
const temRecorte = (i) => i % 4 === 2;
|
|
233
|
+
|
|
234
|
+
function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {} }) {
|
|
229
235
|
const e = ESTILOS[estilo] || ESTILOS.viral;
|
|
230
236
|
const lista = (palavras || []).filter((p) => p && p.texto && p.ate > p.de).sort((a, b) => a.de - b.de);
|
|
231
237
|
if (!lista.length) return { ok: false, error: 'a gravação não tem fala transcrita para editar.' };
|
|
@@ -236,6 +242,7 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
236
242
|
const total = duracao || lista[lista.length - 1].ate + 0.3;
|
|
237
243
|
const cenas = [];
|
|
238
244
|
const efeitos = [];
|
|
245
|
+
const infos = []; // onde cada cena começa e a palavra forte dela — para o recorte
|
|
239
246
|
let lado = 0;
|
|
240
247
|
|
|
241
248
|
grupos.forEach((g, i) => {
|
|
@@ -253,10 +260,37 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
253
260
|
deOrigem: r2(ini), ateOrigem: r2(ini + dur), volume: 1,
|
|
254
261
|
}];
|
|
255
262
|
|
|
263
|
+
infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null });
|
|
264
|
+
|
|
265
|
+
/* A PALAVRA ATRÁS DA PESSOA: o fundo do quarto some, entra a cor da
|
|
266
|
+
marca, a palavra forte aparece enorme e a pessoa, recortada, fica
|
|
267
|
+
na frente dela. É o efeito que mais denuncia edição de videomaker. */
|
|
268
|
+
const rec = e.recorte && temRecorte(i) ? recortes[i] : null;
|
|
269
|
+
if (rec && forte) {
|
|
270
|
+
const vida = r2(rec.ate - rec.de);
|
|
271
|
+
const some = [{ t: 0, v: 0 }, { t: 0.1, v: 1 }, { t: vida - 0.12, v: 1 }, { t: vida, v: 0 }];
|
|
272
|
+
elementos.push({
|
|
273
|
+
tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', de: rec.de, ate: rec.ate,
|
|
274
|
+
fundo: { tipo: 'gradiente', cores: [e.fundoRecorte[0], e.fundoRecorte[1]], angulo: 160 }, anima: { opacidade: some },
|
|
275
|
+
});
|
|
276
|
+
const palavra = limpaPontuacao(forte.texto).toUpperCase();
|
|
277
|
+
const fit = Math.round(Math.min(altura * 0.42, (largura * 0.92) / Math.max(3, palavra.length * 0.46)));
|
|
278
|
+
elementos.push({
|
|
279
|
+
tipo: 'texto', texto: palavra, fonte: 'Anton', peso: 400, tamanho: fit, cor: e.forte,
|
|
280
|
+
x: '50%', y: deitado ? '38%' : '34%', ancora: 'centro', de: rec.de, ate: rec.ate,
|
|
281
|
+
anima: { escala: [{ t: 0, v: 0.6 }, { t: 0.22, v: 1.06, e: 'saida' }, { t: 0.4, v: 1, e: 'suave' }, { t: vida, v: 1.05 }], opacidade: some },
|
|
282
|
+
});
|
|
283
|
+
elementos.push({
|
|
284
|
+
tipo: 'video', src: rec.src, volume: 0, x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
|
|
285
|
+
de: rec.de, ate: rec.ate, anima: { opacidade: some },
|
|
286
|
+
});
|
|
287
|
+
if (e.sons && sons.whoosh) efeitos.push({ t: r2(Math.max(0, ini + rec.de - 0.1)), src: sons.whoosh, volume: 0.55 });
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
/* A IMAGEM DE APOIO: na hora da palavra forte, a tela vira a imagem do
|
|
257
291
|
que está sendo dito, com um zoom lento, e a legenda continua por
|
|
258
292
|
cima. É o "b-roll" das edições de videomaker. */
|
|
259
|
-
const apoio = forte && e.apoio && temApoio(i) ? imagens[termoDeApoio(forte.texto)] : null;
|
|
293
|
+
const apoio = forte && e.apoio && temApoio(i) && !rec ? imagens[termoDeApoio(forte.texto)] : null;
|
|
260
294
|
if (apoio) {
|
|
261
295
|
const de = local(forte.de - 0.1);
|
|
262
296
|
const vida = Math.min(2.2, dur - de);
|
|
@@ -278,7 +312,7 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
278
312
|
}
|
|
279
313
|
|
|
280
314
|
// O ícone salta na hora da palavra; sem ícone, o letreiro da ideia.
|
|
281
|
-
const icone = forte && e.icones && !apoio ? iconeDe(forte.texto) : null;
|
|
315
|
+
const icone = forte && e.icones && !apoio && !rec ? iconeDe(forte.texto) : null;
|
|
282
316
|
const temIcone = icone && (!iconesOk || iconesOk.has(icone));
|
|
283
317
|
const x = lado++ % 2 === 0 ? '78%' : '22%';
|
|
284
318
|
if (temIcone) {
|
|
@@ -298,7 +332,7 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
298
332
|
});
|
|
299
333
|
if (e.sons && sons.pop) efeitos.push({ t: r2(ini + de), src: sons.pop, volume: 0.8 });
|
|
300
334
|
}
|
|
301
|
-
} else if (forte && e.letreiro && i % 2 === 0 && peso(forte.texto) >= 7) {
|
|
335
|
+
} else if (forte && e.letreiro && !rec && i % 2 === 0 && peso(forte.texto) >= 7) {
|
|
302
336
|
const de = local(forte.de);
|
|
303
337
|
const vida = Math.min(1.6, dur - de);
|
|
304
338
|
if (vida > 0.5) {
|
|
@@ -362,8 +396,10 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
362
396
|
...(trilha ? { trilha: { src: trilha, volume: 0.07, loop: true } } : {}),
|
|
363
397
|
} } : {}),
|
|
364
398
|
},
|
|
399
|
+
infos,
|
|
365
400
|
resumo: {
|
|
366
401
|
cenas: cenas.length,
|
|
402
|
+
recortes: Object.keys(recortes).length,
|
|
367
403
|
icones: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'icone').length, 0),
|
|
368
404
|
apoios: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'imagem' || (x.tipo === 'video' && x.volume === 0)).length, 0),
|
|
369
405
|
musica: Boolean(trilha),
|
|
@@ -402,4 +438,4 @@ async function prepararSons(pasta, video) {
|
|
|
402
438
|
return sons;
|
|
403
439
|
}
|
|
404
440
|
|
|
405
|
-
module.exports = { montarEdicao, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, ESTILOS, ICONES, FOTO };
|
|
441
|
+
module.exports = { montarEdicao, temRecorte, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, ESTILOS, ICONES, FOTO };
|
package/lib/estudio/index.js
CHANGED
|
@@ -81,6 +81,37 @@ function rodarPython(script, args, { timeout = 20 * 60000 } = {}) {
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/* O RECORTE DA PESSOA precisa do MediaPipe, e o MediaPipe só existe até o
|
|
85
|
+
Python 3.12 — no Python 3.14 do Homebrew ele nem instala (medido em
|
|
86
|
+
25/09: o recorte nunca tinha rodado nesta máquina). Com o `uv`, o script
|
|
87
|
+
roda num Python 3.12 isolado, baixado uma vez; sem ele, tenta o Python da
|
|
88
|
+
máquina, como antes. */
|
|
89
|
+
function acharUv() {
|
|
90
|
+
const { execFileSync } = require('child_process');
|
|
91
|
+
for (const c of ['uv', path.join(os.homedir(), '.local', 'bin', 'uv'), '/opt/homebrew/bin/uv', '/usr/local/bin/uv']) {
|
|
92
|
+
try { execFileSync(c, ['--version'], { stdio: 'ignore', timeout: 5000 }); return c; } catch { /* próximo */ }
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function rodarPessoa(args, { timeout = 40 * 60000 } = {}) {
|
|
97
|
+
const uv = acharUv();
|
|
98
|
+
if (!uv) return rodarPython('pessoa.py', args, { timeout });
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const p = spawn(uv, ['run', '--quiet', '--python', '3.12', '--with', 'mediapipe<0.10.22',
|
|
101
|
+
'--with', 'opencv-python', '--with', 'numpy', 'python', path.join(PY, 'pessoa.py'), ...args]);
|
|
102
|
+
let saida = '', erro = '';
|
|
103
|
+
p.stdout.on('data', (d) => { saida += d.toString(); });
|
|
104
|
+
p.stderr.on('data', (d) => { erro += d.toString(); });
|
|
105
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} }, timeout);
|
|
106
|
+
p.on('close', () => {
|
|
107
|
+
clearTimeout(relogio);
|
|
108
|
+
try { resolve(JSON.parse(saida.trim().split('\n').pop())); }
|
|
109
|
+
catch { resolve({ ok: false, error: (erro || saida).slice(-400).trim() || 'o recorte não respondeu' }); }
|
|
110
|
+
});
|
|
111
|
+
p.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, error: e.message }); });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
// ── estudio_projeto ──────────────────────────────────────────────────────
|
|
85
116
|
|
|
86
117
|
/**
|
|
@@ -481,12 +512,36 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
481
512
|
} catch { /* segue sem música */ }
|
|
482
513
|
}
|
|
483
514
|
|
|
484
|
-
const
|
|
515
|
+
const base = {
|
|
485
516
|
src: path.relative(p.pasta, fonte).replace(/\\/g, '/'),
|
|
486
517
|
duracao: m.segundos, palavras, largura: m.largura, altura: m.altura,
|
|
487
518
|
estilo: args.estilo || 'viral', sons, iconesOk, imagens, trilha,
|
|
488
|
-
}
|
|
519
|
+
};
|
|
520
|
+
let montado = edicao.montarEdicao(base);
|
|
489
521
|
if (!montado.ok) return montado;
|
|
522
|
+
|
|
523
|
+
/* A segunda passada: nas frases escolhidas, a pessoa recortada na
|
|
524
|
+
frente da palavra gigante. Só os trechos que entram são recortados
|
|
525
|
+
(2 s cada), não o vídeo inteiro — recortar 52 s levaria minutos. */
|
|
526
|
+
if ((args.estilo || 'viral') === 'viral' && args.recorte !== false) {
|
|
527
|
+
const recortes = {};
|
|
528
|
+
for (const [i, info] of montado.infos.entries()) {
|
|
529
|
+
if (!edicao.temRecorte(i) || !info.forte || info.forte.peso < 9 || Object.keys(recortes).length >= 3) continue;
|
|
530
|
+
const de = Math.max(0, info.forte.de - 0.15);
|
|
531
|
+
const ate = Math.min(info.dur, de + 2.2);
|
|
532
|
+
if (ate - de < 1) continue;
|
|
533
|
+
const seg = path.join(p.pasta, 'midia', `seg-${i + 1}-${Date.now().toString(36)}.mp4`);
|
|
534
|
+
const corte = await video.rodar(video.ffmpeg(), ['-y', '-ss', String(info.ini + de), '-t', String(ate - de), '-i', fonte,
|
|
535
|
+
'-vf', `scale=${Math.min(1280, m.largura)}:-2`, '-an', '-c:v', 'libx264', '-preset', 'veryfast', seg], { timeout: 120000 });
|
|
536
|
+
if (!corte.ok) continue;
|
|
537
|
+
const saida = path.join(p.pasta, 'midia', `pessoa-${i + 1}-${Date.now().toString(36)}.webm`);
|
|
538
|
+
const r = await rodarPessoa(['recortar', '--entrada', seg, '--saida', saida], { timeout: 10 * 60000 });
|
|
539
|
+
try { fs.unlinkSync(seg); } catch { /* fica */ }
|
|
540
|
+
if (!r.ok) break; // sem MediaPipe, nenhum recorte vai dar: não tenta os outros
|
|
541
|
+
recortes[i] = { src: path.relative(p.pasta, saida).replace(/\\/g, '/'), de: Math.round(de * 100) / 100, ate: Math.round(ate * 100) / 100 };
|
|
542
|
+
}
|
|
543
|
+
if (Object.keys(recortes).length) montado = edicao.montarEdicao({ ...base, recortes });
|
|
544
|
+
}
|
|
490
545
|
const salvo = await ferramentaPlano({ id: args.id, plano: montado.plano, motivo: `edição automática (${args.estilo || 'viral'})` });
|
|
491
546
|
if (!salvo.ok) return salvo;
|
|
492
547
|
projeto.anotar(p.id, 'edicao', `${montado.resumo.cenas} cenas, ${montado.resumo.icones} ícones, ${cortes} cortes de silêncio`);
|
|
@@ -499,7 +554,7 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
499
554
|
|
|
500
555
|
if (acao === 'recortar-pessoa' || acao === 'pessoa') {
|
|
501
556
|
const saida = path.join(p.pasta, 'midia', `pessoa-${Date.now().toString(36)}.webm`);
|
|
502
|
-
const r = await
|
|
557
|
+
const r = await rodarPessoa([
|
|
503
558
|
'recortar', '--entrada', alvo, '--saida', saida,
|
|
504
559
|
...(args.suavizar != null ? ['--suavizar', String(args.suavizar)] : []),
|
|
505
560
|
...(args.encolher != null ? ['--encolher', String(args.encolher)] : []),
|
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.15",
|
|
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": {
|