primocode 9.8.0-beta.13 → 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/api.js +8 -1
- package/lib/estudio/assets.js +23 -2
- package/lib/estudio/composicao.js +4 -2
- package/lib/estudio/edicao.js +52 -12
- package/lib/estudio/index.js +85 -17
- 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/api.js
CHANGED
|
@@ -268,6 +268,13 @@ function getConectores(serverUrl, token) {
|
|
|
268
268
|
return pedirGet(serverUrl, '/api/conectores', token);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/* Foto e vídeo de apoio do Pexels, pela chave do servidor (ninguém precisa
|
|
272
|
+
ter uma). tipo: "imagens" | "videos". */
|
|
273
|
+
function getMidia(serverUrl, token, tipo, { q, n, orientacao } = {}) {
|
|
274
|
+
const qs = new URLSearchParams({ q: q || '', n: String(n || 6), ...(orientacao ? { orientacao } : {}) });
|
|
275
|
+
return pedirGet(serverUrl, `/api/midia/${tipo}?${qs}`, token);
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
function getHealth(serverUrl, token) {
|
|
272
279
|
return pedirGet(serverUrl, '/health', token);
|
|
273
280
|
}
|
|
@@ -300,4 +307,4 @@ function pedirGet(serverUrl, rota, token) {
|
|
|
300
307
|
});
|
|
301
308
|
}
|
|
302
309
|
|
|
303
|
-
module.exports = { request, requestStream, requestAct, requestChat, requestResumo, requestRefino, requestPlano, requestTranscricao, getHealth, getConectores };
|
|
310
|
+
module.exports = { request, requestStream, requestAct, requestChat, requestResumo, requestRefino, requestPlano, requestTranscricao, getHealth, getConectores, getMidia };
|
package/lib/estudio/assets.js
CHANGED
|
@@ -213,7 +213,28 @@ function extensaoPorConteudo(bytes) {
|
|
|
213
213
|
*
|
|
214
214
|
* @returns {{ok, imagens: Array<{url, titulo, autor, licenca, creditos, fonte}>}}
|
|
215
215
|
*/
|
|
216
|
-
|
|
216
|
+
/* O Pexels pelo SERVIDOR: a chave é dele, e ninguém precisa ter uma. Sem
|
|
217
|
+
servidor (ou sem a chave lá), devolve null e a busca segue no Openverse. */
|
|
218
|
+
async function pexelsDoServidor(tipo, termo, { quantas, orientacao } = {}) {
|
|
219
|
+
try {
|
|
220
|
+
const cfg = require('../config').loadConfig();
|
|
221
|
+
const r = await require('../api').getMidia(cfg.server, cfg.token, tipo, { q: termo, n: quantas, orientacao });
|
|
222
|
+
const lista = r && r.ok ? r[tipo] : null;
|
|
223
|
+
return lista && lista.length ? lista : null;
|
|
224
|
+
} catch { return null; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Procura vídeo de apoio (b-roll). Só Pexels: o Openverse não tem vídeo. */
|
|
228
|
+
async function procurarVideos(termo, { quantas = 5, orientacao = null } = {}) {
|
|
229
|
+
const videos = await pexelsDoServidor('videos', termo, { quantas, orientacao });
|
|
230
|
+
return videos ? { ok: true, videos } : { ok: false, error: 'sem acervo de vídeo agora (o servidor está sem a chave do Pexels).', videos: [] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function procurarImagens(termo, { quantas = 8, transparente = false, orientacao = null } = {}) {
|
|
234
|
+
if (!transparente && !process.env.PEXELS_API_KEY) {
|
|
235
|
+
const imagens = await pexelsDoServidor('imagens', termo, { quantas, orientacao });
|
|
236
|
+
if (imagens) return { ok: true, imagens };
|
|
237
|
+
}
|
|
217
238
|
const chave = process.env.PEXELS_API_KEY;
|
|
218
239
|
if (chave && !transparente) {
|
|
219
240
|
try {
|
|
@@ -337,7 +358,7 @@ async function conferirFonte(familia) {
|
|
|
337
358
|
module.exports = {
|
|
338
359
|
extensaoPorConteudo,
|
|
339
360
|
pegar, guardar,
|
|
340
|
-
icone, procurarIcones, iconesDoPlano, COLECOES,
|
|
361
|
+
icone, procurarIcones, iconesDoPlano, COLECOES, procurarVideos,
|
|
341
362
|
procurarImagens, procurarSom, baixar,
|
|
342
363
|
FONTES, conferirFonte,
|
|
343
364
|
};
|
|
@@ -325,9 +325,11 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
325
325
|
break;
|
|
326
326
|
}
|
|
327
327
|
case 'imagem': {
|
|
328
|
-
const
|
|
328
|
+
const m = imagens[i] || p.src;
|
|
329
|
+
const src = m && typeof m === 'object' ? m.src : m;
|
|
329
330
|
if (src) {
|
|
330
|
-
|
|
331
|
+
const ehVideo = m && typeof m === 'object' && m.tipo === 'video';
|
|
332
|
+
els.push({ tipo: ehVideo ? 'video' : 'imagem', src, ...(ehVideo ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
|
|
331
333
|
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.25)', 'rgba(0,0,0,0.85)'], angulo: 180 } });
|
|
332
334
|
}
|
|
333
335
|
const larg = W0 * 0.8;
|
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
|
|
@@ -133,8 +136,8 @@ function termoDeApoio(palavra) {
|
|
|
133
136
|
return ic ? FOTO[ic.split(':')[1]] || null : null;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
// Em quais cenas entra
|
|
137
|
-
const temApoio = (i) => i %
|
|
139
|
+
// Em quais cenas entra a mídia de apoio: uma a cada duas, a partir da segunda.
|
|
140
|
+
const temApoio = (i) => i % 2 === 1;
|
|
138
141
|
|
|
139
142
|
function forteDe(g) {
|
|
140
143
|
let forte = null;
|
|
@@ -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,16 +260,47 @@ 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);
|
|
263
297
|
if (vida > 0.8) {
|
|
298
|
+
// O apoio é foto ou vídeo (o Pexels dá os dois; vídeo é melhor).
|
|
299
|
+
const ehVideo = typeof apoio === 'object' && apoio.tipo === 'video';
|
|
264
300
|
elementos.push({
|
|
265
|
-
tipo: '
|
|
301
|
+
tipo: ehVideo ? 'video' : 'imagem', src: typeof apoio === 'object' ? apoio.src : apoio,
|
|
302
|
+
...(ehVideo ? { volume: 0 } : {}),
|
|
303
|
+
x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
|
|
266
304
|
de, ate: r2(de + vida),
|
|
267
305
|
anima: {
|
|
268
306
|
escala: [{ t: 0, v: 1.12 }, { t: vida, v: 1.0, e: 'suave' }],
|
|
@@ -274,7 +312,7 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
274
312
|
}
|
|
275
313
|
|
|
276
314
|
// O ícone salta na hora da palavra; sem ícone, o letreiro da ideia.
|
|
277
|
-
const icone = forte && e.icones && !apoio ? iconeDe(forte.texto) : null;
|
|
315
|
+
const icone = forte && e.icones && !apoio && !rec ? iconeDe(forte.texto) : null;
|
|
278
316
|
const temIcone = icone && (!iconesOk || iconesOk.has(icone));
|
|
279
317
|
const x = lado++ % 2 === 0 ? '78%' : '22%';
|
|
280
318
|
if (temIcone) {
|
|
@@ -294,7 +332,7 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
294
332
|
});
|
|
295
333
|
if (e.sons && sons.pop) efeitos.push({ t: r2(ini + de), src: sons.pop, volume: 0.8 });
|
|
296
334
|
}
|
|
297
|
-
} 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) {
|
|
298
336
|
const de = local(forte.de);
|
|
299
337
|
const vida = Math.min(1.6, dur - de);
|
|
300
338
|
if (vida > 0.5) {
|
|
@@ -358,10 +396,12 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
358
396
|
...(trilha ? { trilha: { src: trilha, volume: 0.07, loop: true } } : {}),
|
|
359
397
|
} } : {}),
|
|
360
398
|
},
|
|
399
|
+
infos,
|
|
361
400
|
resumo: {
|
|
362
401
|
cenas: cenas.length,
|
|
402
|
+
recortes: Object.keys(recortes).length,
|
|
363
403
|
icones: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'icone').length, 0),
|
|
364
|
-
|
|
404
|
+
apoios: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'imagem' || (x.tipo === 'video' && x.volume === 0)).length, 0),
|
|
365
405
|
musica: Boolean(trilha),
|
|
366
406
|
letreiros: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'texto').length, 0),
|
|
367
407
|
efeitos: efeitos.length,
|
|
@@ -398,4 +438,4 @@ async function prepararSons(pasta, video) {
|
|
|
398
438
|
return sons;
|
|
399
439
|
}
|
|
400
440
|
|
|
401
|
-
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
|
/**
|
|
@@ -462,14 +493,10 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
462
493
|
// As imagens de apoio: uma por termo, a primeira que baixar.
|
|
463
494
|
const imagens = {};
|
|
464
495
|
if ((args.estilo || 'viral') !== 'sem-apoio') {
|
|
496
|
+
const orientacao = m.largura >= m.altura ? 'landscape' : 'portrait';
|
|
465
497
|
for (const termo of edicao.termosDeApoio(palavras).slice(0, 8)) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
for (const item of (b.ok ? b.imagens : [])) {
|
|
469
|
-
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
|
|
470
|
-
if (d.ok) { imagens[termo] = d.relativo; break; }
|
|
471
|
-
}
|
|
472
|
-
} catch { /* segue sem esta */ }
|
|
498
|
+
imagens[termo] = await baixarApoio(p, termo, { orientacao, video: true });
|
|
499
|
+
if (!imagens[termo]) delete imagens[termo];
|
|
473
500
|
}
|
|
474
501
|
}
|
|
475
502
|
|
|
@@ -485,12 +512,36 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
485
512
|
} catch { /* segue sem música */ }
|
|
486
513
|
}
|
|
487
514
|
|
|
488
|
-
const
|
|
515
|
+
const base = {
|
|
489
516
|
src: path.relative(p.pasta, fonte).replace(/\\/g, '/'),
|
|
490
517
|
duracao: m.segundos, palavras, largura: m.largura, altura: m.altura,
|
|
491
518
|
estilo: args.estilo || 'viral', sons, iconesOk, imagens, trilha,
|
|
492
|
-
}
|
|
519
|
+
};
|
|
520
|
+
let montado = edicao.montarEdicao(base);
|
|
493
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
|
+
}
|
|
494
545
|
const salvo = await ferramentaPlano({ id: args.id, plano: montado.plano, motivo: `edição automática (${args.estilo || 'viral'})` });
|
|
495
546
|
if (!salvo.ok) return salvo;
|
|
496
547
|
projeto.anotar(p.id, 'edicao', `${montado.resumo.cenas} cenas, ${montado.resumo.icones} ícones, ${cortes} cortes de silêncio`);
|
|
@@ -503,7 +554,7 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
503
554
|
|
|
504
555
|
if (acao === 'recortar-pessoa' || acao === 'pessoa') {
|
|
505
556
|
const saida = path.join(p.pasta, 'midia', `pessoa-${Date.now().toString(36)}.webm`);
|
|
506
|
-
const r = await
|
|
557
|
+
const r = await rodarPessoa([
|
|
507
558
|
'recortar', '--entrada', alvo, '--saida', saida,
|
|
508
559
|
...(args.suavizar != null ? ['--suavizar', String(args.suavizar)] : []),
|
|
509
560
|
...(args.encolher != null ? ['--encolher', String(args.encolher)] : []),
|
|
@@ -520,6 +571,28 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
520
571
|
return { ok: false, error: `não conheço a ação "${acao}".` };
|
|
521
572
|
}
|
|
522
573
|
|
|
574
|
+
/* A mídia de apoio de um termo: vídeo do Pexels quando pedido e houver,
|
|
575
|
+
senão a primeira foto que baixar. Devolve {src, tipo} ou null. */
|
|
576
|
+
async function baixarApoio(p, termo, { orientacao = null, video: querVideo = false } = {}) {
|
|
577
|
+
if (querVideo) {
|
|
578
|
+
try {
|
|
579
|
+
const b = await assets.procurarVideos(termo, { quantas: 4, orientacao });
|
|
580
|
+
for (const item of (b.ok ? b.videos : []).filter((v) => !v.duracao || v.duracao >= 3)) {
|
|
581
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
|
|
582
|
+
if (d.ok) return { src: d.relativo, tipo: 'video' };
|
|
583
|
+
}
|
|
584
|
+
} catch { /* cai para a foto */ }
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const b = await assets.procurarImagens(termo, { quantas: 4, orientacao });
|
|
588
|
+
for (const item of (b.ok ? b.imagens : [])) {
|
|
589
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
|
|
590
|
+
if (d.ok) return { src: d.relativo, tipo: 'imagem' };
|
|
591
|
+
}
|
|
592
|
+
} catch { /* sem apoio */ }
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
523
596
|
const PROPORCOES = { '16:9': [1920, 1080], '9:16': [1080, 1920], '1:1': [1080, 1080], '4:5': [1080, 1350] };
|
|
524
597
|
|
|
525
598
|
/** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
|
|
@@ -537,13 +610,8 @@ async function prepararRoteiro(p, args) {
|
|
|
537
610
|
const imagens = {};
|
|
538
611
|
for (const [i, parte] of partes.entries()) {
|
|
539
612
|
if (parte.tipo !== 'imagem' || parte.src || !parte.busca) continue;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
for (const item of (b.ok ? b.imagens : [])) {
|
|
543
|
-
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'cena-' + (i + 1) });
|
|
544
|
-
if (d.ok) { imagens[i] = d.relativo; break; }
|
|
545
|
-
}
|
|
546
|
-
} catch { /* a cena sai sem a foto, com o título */ }
|
|
613
|
+
const m = await baixarApoio(p, parte.busca, { orientacao: largura >= altura ? 'landscape' : 'portrait', video: modo === 'video' });
|
|
614
|
+
if (m) imagens[i] = m;
|
|
547
615
|
}
|
|
548
616
|
|
|
549
617
|
let tempos = [], narracao = null, sons = {}, trilha = null;
|
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": {
|