primocode 9.8.0-beta.13 → 9.8.0-beta.14

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.13
1
+ # PrimoCode v9.8.0-beta.14
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.13
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.14
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 };
@@ -213,7 +213,28 @@ function extensaoPorConteudo(bytes) {
213
213
  *
214
214
  * @returns {{ok, imagens: Array<{url, titulo, autor, licenca, creditos, fonte}>}}
215
215
  */
216
- async function procurarImagens(termo, { quantas = 8, transparente = false } = {}) {
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 src = imagens[i] || p.src;
328
+ const m = imagens[i] || p.src;
329
+ const src = m && typeof m === 'object' ? m.src : m;
329
330
  if (src) {
330
- els.push({ tipo: 'imagem', src, x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
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;
@@ -133,8 +133,8 @@ function termoDeApoio(palavra) {
133
133
  return ic ? FOTO[ic.split(':')[1]] || null : null;
134
134
  }
135
135
 
136
- // Em quais cenas entra imagem de apoio: uma a cada três, a partir da segunda.
137
- const temApoio = (i) => i % 3 === 2;
136
+ // Em quais cenas entra a mídia de apoio: uma a cada duas, a partir da segunda.
137
+ const temApoio = (i) => i % 2 === 1;
138
138
 
139
139
  function forteDe(g) {
140
140
  let forte = null;
@@ -261,8 +261,12 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
261
261
  const de = local(forte.de - 0.1);
262
262
  const vida = Math.min(2.2, dur - de);
263
263
  if (vida > 0.8) {
264
+ // O apoio é foto ou vídeo (o Pexels dá os dois; vídeo é melhor).
265
+ const ehVideo = typeof apoio === 'object' && apoio.tipo === 'video';
264
266
  elementos.push({
265
- tipo: 'imagem', src: apoio, x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
267
+ tipo: ehVideo ? 'video' : 'imagem', src: typeof apoio === 'object' ? apoio.src : apoio,
268
+ ...(ehVideo ? { volume: 0 } : {}),
269
+ x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
266
270
  de, ate: r2(de + vida),
267
271
  anima: {
268
272
  escala: [{ t: 0, v: 1.12 }, { t: vida, v: 1.0, e: 'suave' }],
@@ -361,7 +365,7 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
361
365
  resumo: {
362
366
  cenas: cenas.length,
363
367
  icones: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'icone').length, 0),
364
- imagens: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'imagem').length, 0),
368
+ apoios: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'imagem' || (x.tipo === 'video' && x.volume === 0)).length, 0),
365
369
  musica: Boolean(trilha),
366
370
  letreiros: cenas.reduce((n, c) => n + c.elementos.filter((x) => x.tipo === 'texto').length, 0),
367
371
  efeitos: efeitos.length,
@@ -462,14 +462,10 @@ async function ferramentaVideo(args, ctx = {}) {
462
462
  // As imagens de apoio: uma por termo, a primeira que baixar.
463
463
  const imagens = {};
464
464
  if ((args.estilo || 'viral') !== 'sem-apoio') {
465
+ const orientacao = m.largura >= m.altura ? 'landscape' : 'portrait';
465
466
  for (const termo of edicao.termosDeApoio(palavras).slice(0, 8)) {
466
- try {
467
- const b = await assets.procurarImagens(termo, { quantas: 4 });
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 */ }
467
+ imagens[termo] = await baixarApoio(p, termo, { orientacao, video: true });
468
+ if (!imagens[termo]) delete imagens[termo];
473
469
  }
474
470
  }
475
471
 
@@ -520,6 +516,28 @@ async function ferramentaVideo(args, ctx = {}) {
520
516
  return { ok: false, error: `não conheço a ação "${acao}".` };
521
517
  }
522
518
 
519
+ /* A mídia de apoio de um termo: vídeo do Pexels quando pedido e houver,
520
+ senão a primeira foto que baixar. Devolve {src, tipo} ou null. */
521
+ async function baixarApoio(p, termo, { orientacao = null, video: querVideo = false } = {}) {
522
+ if (querVideo) {
523
+ try {
524
+ const b = await assets.procurarVideos(termo, { quantas: 4, orientacao });
525
+ for (const item of (b.ok ? b.videos : []).filter((v) => !v.duracao || v.duracao >= 3)) {
526
+ const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
527
+ if (d.ok) return { src: d.relativo, tipo: 'video' };
528
+ }
529
+ } catch { /* cai para a foto */ }
530
+ }
531
+ try {
532
+ const b = await assets.procurarImagens(termo, { quantas: 4, orientacao });
533
+ for (const item of (b.ok ? b.imagens : [])) {
534
+ const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
535
+ if (d.ok) return { src: d.relativo, tipo: 'imagem' };
536
+ }
537
+ } catch { /* sem apoio */ }
538
+ return null;
539
+ }
540
+
523
541
  const PROPORCOES = { '16:9': [1920, 1080], '9:16': [1080, 1920], '1:1': [1080, 1080], '4:5': [1080, 1350] };
524
542
 
525
543
  /** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
@@ -537,13 +555,8 @@ async function prepararRoteiro(p, args) {
537
555
  const imagens = {};
538
556
  for (const [i, parte] of partes.entries()) {
539
557
  if (parte.tipo !== 'imagem' || parte.src || !parte.busca) continue;
540
- try {
541
- const b = await assets.procurarImagens(parte.busca, { quantas: 4 });
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 */ }
558
+ const m = await baixarApoio(p, parte.busca, { orientacao: largura >= altura ? 'landscape' : 'portrait', video: modo === 'video' });
559
+ if (m) imagens[i] = m;
547
560
  }
548
561
 
549
562
  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.13",
3
+ "version": "9.8.0-beta.14",
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": {