primocode 8.40.0 → 8.42.0
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 +34 -5
- package/bin/primocode.js +90 -0
- package/lib/acervo.js +391 -0
- package/lib/act.js +96 -0
- package/lib/audio-livre.js +6 -1
- package/lib/boas-vindas.js +80 -21
- package/lib/catalogo.js +24 -0
- package/lib/conta.js +24 -3
- package/lib/diretor.js +309 -0
- package/lib/tools.js +43 -0
- package/lib/ui.js +3 -2
- package/package.json +2 -2
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +57 -0
- package/studio/ferramentas/corte.md +42 -0
- package/studio/studio.log +39 -10
- package/studio/web/core.css +178 -0
- package/studio/web/home.html +91 -24
- package/studio/web/js/editor.js +17 -0
- package/studio/web/js/render.js +155 -15
- package/studio/web/render.css +18 -0
- package/studio/arquivos/corte-estatico-corte-a5bab.json +0 -150
- package/studio/arquivos/corte-founders-ai-teste-do-player-2de1d.json +0 -65
- package/studio/arquivos/corte-legado-teste.json +0 -1
- package/studio/arquivos/corte-legado2.json +0 -1
- package/studio/arquivos/corte-minimo-2a829.json +0 -24
- package/studio/arquivos/corte-ok-00a3b.json +0 -18
- package/studio/arquivos/corte-ok-1270e.json +0 -22
- package/studio/arquivos/corte-sweep-corte-cfb29.json +0 -147
- package/studio/arquivos/corte-visual-completo-78f50.json +0 -35
- package/studio/arquivos/grade-sweep-grade-0c2f1.json +0 -119
- package/studio/arquivos/prisma-estatico-prisma-d8740.json +0 -182
- package/studio/arquivos/prisma-sweep-prisma-4c58c.json +0 -166
- package/studio/arquivos/prisma-y-b0785.json +0 -52
- package/studio/arquivos/prosa-sweep-prosa-f29cf.json +0 -80
- package/studio/arquivos/tela-estatico-tela-c7daf.json +0 -90
- package/studio/arquivos/tela-motion-3ecaa.json +0 -135
- package/studio/arquivos/tela-motion-d6150.json +0 -135
- package/studio/arquivos/tela-sweep-tela-0d2bf.json +0 -84
- package/studio/arquivos/traco-estatico-traco-25872.json +0 -113
- package/studio/arquivos/traco-sweep-traco-0bcf4.json +0 -102
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
# PrimoCode v8.
|
|
1
|
+
# PrimoCode v8.42.0
|
|
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** —
|
|
5
5
|
e lembra do que fez entre uma sessão e outra.
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
**Sem chave de API sua.** O modelo vem hospedado: você entra com a conta do
|
|
8
|
+
Conecta Primo AI (plano **Premium** ou **Super**) e usa — nada de console de
|
|
9
|
+
provedor, nada de teto de gasto para acompanhar. Quem preferir a própria chave
|
|
10
|
+
do Claude ou do GPT escolhe isso na abertura, com as setinhas, e passa a usar a
|
|
11
|
+
assinatura dele.
|
|
10
12
|
|
|
11
13
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v8.
|
|
14
|
+
▐▛███▜▌ Primo Code v8.42.0
|
|
13
15
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
16
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
17
|
```
|
|
@@ -31,6 +33,33 @@ veja `/mascot` e `/name`.
|
|
|
31
33
|
|
|
32
34
|
Em uma frase: **é o poder de um agente de código no terminal, em português, controlando o seu computador de verdade — sem você precisar configurar chave de API nenhuma.**
|
|
33
35
|
|
|
36
|
+
## Vídeo e apresentação: o diretor entra antes da produção
|
|
37
|
+
|
|
38
|
+
Quando o pedido é de vídeo, reels, teaser ou apresentação narrada, o PrimoCode
|
|
39
|
+
não sai montando cena: ele chama primeiro o **diretor** — uma IA que só
|
|
40
|
+
estrutura. Ela devolve o roteiro cena a cena com a **narração escrita para ser
|
|
41
|
+
falada**, a duração, a transição, o movimento de cada cena, a voz (perfil e
|
|
42
|
+
instrução de leitura) e a lista de imagens, logos e música a buscar. Só depois
|
|
43
|
+
disso a produção começa, e ela segue a pauta em vez de inventar.
|
|
44
|
+
|
|
45
|
+
A mídia é de verdade: foto com licença aberta (Openverse), logo em SVG da marca
|
|
46
|
+
(Wikimedia Commons) e, com chave própria, a busca de imagens do Google pela API
|
|
47
|
+
oficial. Música e efeito sonoro vêm do `buscar_audio`, que baixa e já traz o
|
|
48
|
+
crédito; a identidade de uma empresa — logo, cor, paleta, fontes e frase — o
|
|
49
|
+
`marca_da_empresa` lê do site dela. Tudo o que desce guarda a licença ao lado do
|
|
50
|
+
arquivo, e os créditos saem prontos para a cena final. Recorte de fundo com
|
|
51
|
+
`rembg`, quando instalado.
|
|
52
|
+
|
|
53
|
+
O quadro também se move: cada cena aceita uma **câmera** (`aproximar`,
|
|
54
|
+
`empurrar`, `pan-direita`, `balanco`…) e cada elemento uma **profundidade**, e
|
|
55
|
+
aí as camadas andam em velocidades diferentes — é o que separa motion de slide
|
|
56
|
+
animado. Junto com as chaves em 3D, as curvas com nome e o som por elemento,
|
|
57
|
+
dá para montar no navegador o que antes pedia After Effects.
|
|
58
|
+
|
|
59
|
+
O motor não é gosto: vídeo curto sai pelo estúdio, em segundos, sem instalar
|
|
60
|
+
nada; vídeo longo, canal alfa, ProRes ou fps alto viram projeto **Remotion** na
|
|
61
|
+
sua pasta — e a resposta sempre diz por que escolheu.
|
|
62
|
+
|
|
34
63
|
## Instalação
|
|
35
64
|
|
|
36
65
|
**macOS / Linux / WSL** — um comando, e ele resolve tudo (inclusive instalar o Node, se faltar):
|
package/bin/primocode.js
CHANGED
|
@@ -228,6 +228,69 @@ function sliderEffort(atual) {
|
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/* ── A LISTA COM AS SETINHAS ─────────────────────────────────────────────
|
|
232
|
+
*
|
|
233
|
+
* "O usuário seleciona usando as setinhas, igual você."
|
|
234
|
+
*
|
|
235
|
+
* ↑↓ anda, Enter confirma, Esc fica com o primeiro item. O número também
|
|
236
|
+
* funciona — quem já sabe qual quer não precisa navegar até lá.
|
|
237
|
+
*
|
|
238
|
+
* Devolve `undefined` quando não há terminal de verdade: aí quem chamou cai
|
|
239
|
+
* na lista numerada, que funciona em pipe e em CI. `null` é outra coisa —
|
|
240
|
+
* é Esc, que É uma resposta ("tanto faz, a primeira").
|
|
241
|
+
*/
|
|
242
|
+
function menuSetas({ titulo, ajuda, lista }) {
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(undefined);
|
|
245
|
+
let idx = 0;
|
|
246
|
+
const linhas = () => {
|
|
247
|
+
const fora = [''];
|
|
248
|
+
fora.push(c.bold(c.brand(' ' + titulo)));
|
|
249
|
+
if (ajuda) fora.push(c.muted(' ' + ajuda));
|
|
250
|
+
fora.push('');
|
|
251
|
+
lista.forEach((e, i) => {
|
|
252
|
+
const aqui = i === idx;
|
|
253
|
+
fora.push((aqui ? c.brand(' ❯ ') : ' ') + (aqui ? c.bold(c.white(e.titulo)) : c.muted(e.titulo)));
|
|
254
|
+
fora.push(' ' + c.dim(e.detalhe || ''));
|
|
255
|
+
});
|
|
256
|
+
fora.push('');
|
|
257
|
+
return fora;
|
|
258
|
+
};
|
|
259
|
+
const ALTURA = linhas().length;
|
|
260
|
+
|
|
261
|
+
const pintar = (primeira) => {
|
|
262
|
+
if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
|
|
263
|
+
process.stdout.write(linhas().map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const encerrar = (valor) => {
|
|
267
|
+
process.stdin.removeListener('keypress', aoTeclar);
|
|
268
|
+
sliderAtivo = false;
|
|
269
|
+
try { process.stdout.write('\x1b[?25h'); } catch {}
|
|
270
|
+
resolve(valor);
|
|
271
|
+
};
|
|
272
|
+
const aoTeclar = (ch, key) => {
|
|
273
|
+
if (!key) return;
|
|
274
|
+
if (key.name === 'down' || key.name === 'j' || (key.name === 'tab' && !key.shift)) {
|
|
275
|
+
idx = (idx + 1) % lista.length; return pintar(false);
|
|
276
|
+
}
|
|
277
|
+
if (key.name === 'up' || key.name === 'k' || (key.name === 'tab' && key.shift)) {
|
|
278
|
+
idx = (idx - 1 + lista.length) % lista.length; return pintar(false);
|
|
279
|
+
}
|
|
280
|
+
if (/^[1-9]$/.test(ch || '') && Number(ch) <= lista.length) {
|
|
281
|
+
idx = Number(ch) - 1; return pintar(false);
|
|
282
|
+
}
|
|
283
|
+
if (key.name === 'return' || key.name === 'enter') return encerrar(lista[idx].id);
|
|
284
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) return encerrar(null);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
sliderAtivo = true; // enquanto está no ar, o resto do teclado se cala
|
|
288
|
+
try { process.stdout.write('\x1b[?25l'); } catch {}
|
|
289
|
+
pintar(true);
|
|
290
|
+
process.stdin.on('keypress', aoTeclar);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
231
294
|
function aplicarEffort(nome) {
|
|
232
295
|
const nivel = effort.nivelDe(nome);
|
|
233
296
|
state.config.effort = nivel.nome;
|
|
@@ -1652,6 +1715,10 @@ async function main() {
|
|
|
1652
1715
|
}
|
|
1653
1716
|
|
|
1654
1717
|
rl.on('line', (line) => {
|
|
1718
|
+
// A lista de setinhas (e o slider do /effort) são donos do teclado
|
|
1719
|
+
// enquanto estão no ar: o Enter que confirma a escolha não é uma
|
|
1720
|
+
// linha para a fila, e sem isto ele vira um pedido em branco.
|
|
1721
|
+
if (sliderAtivo) return;
|
|
1655
1722
|
if (pendingAsk) { const resolve = pendingAsk; pendingAsk = null; resolve(line); return; }
|
|
1656
1723
|
// Abertura correndo: guarda em vez de mandar ao agente.
|
|
1657
1724
|
if (segurando) { adiantadas.push(line); return; }
|
|
@@ -1687,6 +1754,29 @@ async function main() {
|
|
|
1687
1754
|
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
1688
1755
|
conectado: () => conta.atual(),
|
|
1689
1756
|
conectar: () => handleSlash('/connect').then(() => conta.atual()),
|
|
1757
|
+
/* A ESCOLHA, COM AS SETINHAS — E O QUE FOI DIGITADO ANTES DELA.
|
|
1758
|
+
*
|
|
1759
|
+
* A lista só aparece depois de o plano voltar do servidor, e quem
|
|
1760
|
+
* colou "3⏎1⏎/version" já respondeu antes de ver. Esse texto está em
|
|
1761
|
+
* `adiantadas`; se a lista ignorasse, a resposta se perderia e o
|
|
1762
|
+
* menu ficaria esperando um teclado que já falou. Então: primeiro o
|
|
1763
|
+
* que foi adiantado (se casar com algum item), depois as setinhas. O
|
|
1764
|
+
* que não casa fica na fila — é comando, não resposta. */
|
|
1765
|
+
selecionar: ({ titulo, ajuda, lista }) => {
|
|
1766
|
+
while (adiantadas.length) {
|
|
1767
|
+
const dito = String(adiantadas[0] || '').trim();
|
|
1768
|
+
if (!dito) { adiantadas.shift(); continue; }
|
|
1769
|
+
const qual = boasVindas.escolher(dito, lista);
|
|
1770
|
+
if (!qual) break; // isso é comando, não escolha
|
|
1771
|
+
adiantadas.shift();
|
|
1772
|
+
const item = lista.find((e) => e.id === qual);
|
|
1773
|
+
console.log('');
|
|
1774
|
+
console.log(c.bold(c.brand(' ' + titulo)));
|
|
1775
|
+
console.log(c.brand(' ❯ ') + c.white(item ? item.titulo : qual));
|
|
1776
|
+
return Promise.resolve(qual);
|
|
1777
|
+
}
|
|
1778
|
+
return menuSetas({ titulo, ajuda, lista });
|
|
1779
|
+
},
|
|
1690
1780
|
sessao: () => conta.sessao(),
|
|
1691
1781
|
aplicar: (cmd) => handleSlash(cmd),
|
|
1692
1782
|
}).then((r) => {
|
package/lib/acervo.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acervo.js — de onde vêm as imagens, os logos e a música.
|
|
3
|
+
*
|
|
4
|
+
* "Pesquise e use imagens e logos do Google, baixe imagens, corte fundo, use
|
|
5
|
+
* músicas da internet."
|
|
6
|
+
*
|
|
7
|
+
* ── POR QUE ISTO NÃO É "BAIXAR DO GOOGLE" E PRONTO ───────────────────────
|
|
8
|
+
* Porque o Google Imagens não é uma API: raspar a página dele é contra os
|
|
9
|
+
* termos, quebra sem aviso e devolve a MINIATURA, que num vídeo 1920×1080
|
|
10
|
+
* aparece como um borrão. O que existe de verdade são três caminhos, e este
|
|
11
|
+
* módulo usa os três, nesta ordem:
|
|
12
|
+
*
|
|
13
|
+
* 1. OPENVERSE — busca de imagem E de áudio com licença aberta, sem chave.
|
|
14
|
+
* É o padrão: funciona na máquina de todo mundo, no primeiro minuto.
|
|
15
|
+
* 2. WIKIMEDIA COMMONS — é onde MORAM os logos das marcas, em SVG e PNG
|
|
16
|
+
* grandes. Uma busca de logo que não passa por aqui devolve fan art.
|
|
17
|
+
* 3. GOOGLE CUSTOM SEARCH — a busca do Google DE VERDADE, pela API oficial,
|
|
18
|
+
* quando a pessoa tem chave (`PRIMOCODE_GOOGLE_KEY` + `PRIMOCODE_GOOGLE_CX`).
|
|
19
|
+
* Cobre o que os dois primeiros não têm.
|
|
20
|
+
*
|
|
21
|
+
* ── A LICENÇA ANDA JUNTO DO ARQUIVO ──────────────────────────────────────
|
|
22
|
+
* Todo download grava um `.json` ao lado com fonte, autor e licença. Não é
|
|
23
|
+
* burocracia: um vídeo de cliente com uma foto "CC BY" sem crédito é um
|
|
24
|
+
* problema jurídico do cliente, e descobrir isso depois de publicar é tarde.
|
|
25
|
+
* `creditos()` monta a lista pronta para a cena final ou a descrição.
|
|
26
|
+
*
|
|
27
|
+
* ── O QUE NÃO ENTRA AQUI ─────────────────────────────────────────────────
|
|
28
|
+
* Nada é baixado para dentro do pacote npm nem fica de dependência. O acervo
|
|
29
|
+
* mora em `~/.primocode/acervo`, com cache por URL: a mesma foto pedida duas
|
|
30
|
+
* vezes desce uma vez só.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const https = require('https');
|
|
39
|
+
const http = require('http');
|
|
40
|
+
const { spawn, spawnSync } = require('child_process');
|
|
41
|
+
|
|
42
|
+
const PASTA = () => path.join(os.homedir(), '.primocode', 'acervo');
|
|
43
|
+
const TETO_BYTES = 25 * 1024 * 1024; // 25 MB: foto grande cabe, filme não
|
|
44
|
+
const TIPOS_OK = /^(image\/(jpeg|png|webp|gif|svg\+xml)|audio\/(mpeg|mp4|ogg|wav|flac)|video\/mp4)/;
|
|
45
|
+
|
|
46
|
+
// ── a rede, sem dependência ──────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function buscarJSON(url, { timeout = 15000, cabecalhos = {} } = {}) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const req = https.get(url, {
|
|
51
|
+
timeout,
|
|
52
|
+
headers: { 'User-Agent': agente(), Accept: 'application/json', ...cabecalhos },
|
|
53
|
+
}, (res) => {
|
|
54
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
55
|
+
res.resume();
|
|
56
|
+
return resolve(buscarJSON(new URL(res.headers.location, url).href, { timeout, cabecalhos }));
|
|
57
|
+
}
|
|
58
|
+
let txt = '';
|
|
59
|
+
res.setEncoding('utf8');
|
|
60
|
+
res.on('data', (d) => { txt += d; });
|
|
61
|
+
res.on('end', () => {
|
|
62
|
+
if (res.statusCode >= 400) return reject(new Error(`${res.statusCode} em ${new URL(url).host}`));
|
|
63
|
+
try { resolve(JSON.parse(txt)); } catch { reject(new Error('resposta que não é JSON')); }
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
req.on('timeout', () => req.destroy(new Error('o servidor demorou demais')));
|
|
67
|
+
req.on('error', reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* O User-Agent é EXIGIDO pela Wikimedia — sem ele a API responde 403, e o
|
|
72
|
+
erro não diz isso. Vai com o nome do programa e a versão, que é o que a
|
|
73
|
+
política deles pede. */
|
|
74
|
+
function agente() {
|
|
75
|
+
let v = '?';
|
|
76
|
+
try { v = require('../package.json').version; } catch { /* sem package: tanto faz */ }
|
|
77
|
+
return `PrimoCode/${v} (https://github.com/parisgroup-ai/primocode)`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── as buscas ────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Imagens com licença aberta, do Openverse. Sem chave, sem cadastro.
|
|
84
|
+
* `licenca: 'comercial'` (padrão) só traz o que pode ir em vídeo de cliente.
|
|
85
|
+
*/
|
|
86
|
+
async function openverseImagens(consulta, { quantos = 8, licenca = 'comercial', orientacao } = {}) {
|
|
87
|
+
const q = new URLSearchParams({ q: consulta, page_size: String(Math.min(20, quantos * 2)) });
|
|
88
|
+
if (licenca === 'comercial') q.set('license_type', 'commercial');
|
|
89
|
+
if (orientacao) q.set('aspect_ratio', orientacao === 'retrato' ? 'tall'
|
|
90
|
+
: orientacao === 'quadrado' ? 'square' : 'wide');
|
|
91
|
+
const r = await buscarJSON('https://api.openverse.org/v1/images/?' + q.toString());
|
|
92
|
+
return (r.results || []).slice(0, quantos).map((i) => ({
|
|
93
|
+
fonte: 'openverse', titulo: i.title || consulta, url: i.url, pagina: i.foreign_landing_url,
|
|
94
|
+
autor: i.creator || null, licenca: [i.license, i.license_version].filter(Boolean).join(' ').toUpperCase(),
|
|
95
|
+
largura: i.width || null, altura: i.height || null, tipo: i.filetype || null,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Música e efeitos, do mesmo Openverse — é ele quem indexa ccMixter e Jamendo. */
|
|
100
|
+
async function openverseAudio(consulta, { quantos = 6, licenca = 'comercial', segundosMin = 20 } = {}) {
|
|
101
|
+
const q = new URLSearchParams({ q: consulta, page_size: String(Math.min(20, quantos * 3)) });
|
|
102
|
+
if (licenca === 'comercial') q.set('license_type', 'commercial');
|
|
103
|
+
const r = await buscarJSON('https://api.openverse.org/v1/audio/?' + q.toString());
|
|
104
|
+
return (r.results || [])
|
|
105
|
+
.filter((a) => !segundosMin || !a.duration || a.duration / 1000 >= segundosMin)
|
|
106
|
+
.slice(0, quantos)
|
|
107
|
+
.map((a) => ({
|
|
108
|
+
fonte: 'openverse', titulo: a.title || consulta,
|
|
109
|
+
url: a.url || (a.alt_files && a.alt_files[0] && a.alt_files[0].url),
|
|
110
|
+
pagina: a.foreign_landing_url, autor: a.creator || null,
|
|
111
|
+
licenca: [a.license, a.license_version].filter(Boolean).join(' ').toUpperCase(),
|
|
112
|
+
segundos: a.duration ? Math.round(a.duration / 1000) : null,
|
|
113
|
+
genero: (a.genres || []).join(', ') || null,
|
|
114
|
+
}))
|
|
115
|
+
.filter((a) => a.url);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Logos, no lugar onde eles moram de verdade: o Wikimedia Commons.
|
|
120
|
+
*
|
|
121
|
+
* A busca vai atrás de "<marca> logo" no espaço de arquivos e devolve a URL
|
|
122
|
+
* do ORIGINAL — normalmente SVG, que é o que se quer num vídeo: escala sem
|
|
123
|
+
* borrar. Procurar logo numa busca de fotos devolve fan art e camiseta.
|
|
124
|
+
*/
|
|
125
|
+
async function wikimediaLogos(marca, { quantos = 6 } = {}) {
|
|
126
|
+
const q = new URLSearchParams({
|
|
127
|
+
action: 'query', format: 'json', origin: '*', generator: 'search',
|
|
128
|
+
gsrsearch: `${marca} logo`, gsrnamespace: '6', gsrlimit: String(Math.min(20, quantos * 2)),
|
|
129
|
+
prop: 'imageinfo', iiprop: 'url|size|extmetadata', iiurlwidth: '1024',
|
|
130
|
+
});
|
|
131
|
+
const r = await buscarJSON('https://commons.wikimedia.org/w/api.php?' + q.toString());
|
|
132
|
+
const paginas = r && r.query && r.query.pages ? Object.values(r.query.pages) : [];
|
|
133
|
+
return paginas
|
|
134
|
+
.map((p) => {
|
|
135
|
+
const info = (p.imageinfo || [])[0] || {};
|
|
136
|
+
const meta = info.extmetadata || {};
|
|
137
|
+
return {
|
|
138
|
+
fonte: 'wikimedia', titulo: p.title || marca,
|
|
139
|
+
url: info.url, pagina: info.descriptionurl,
|
|
140
|
+
autor: limpar(meta.Artist && meta.Artist.value) || null,
|
|
141
|
+
licenca: (meta.LicenseShortName && meta.LicenseShortName.value) || 'ver a página',
|
|
142
|
+
largura: info.width || null, altura: info.height || null,
|
|
143
|
+
vetor: /\.svg$/i.test(info.url || ''),
|
|
144
|
+
};
|
|
145
|
+
})
|
|
146
|
+
.filter((i) => i.url && /\.(svg|png|jpe?g|webp)$/i.test(i.url))
|
|
147
|
+
.sort((a, b) => Number(b.vetor) - Number(a.vetor)) // SVG primeiro
|
|
148
|
+
.slice(0, quantos);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function limpar(html) {
|
|
152
|
+
return html ? String(html).replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim() : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A busca do Google DE VERDADE — pela API oficial, com a chave da pessoa.
|
|
157
|
+
* Sem chave, devolve lista vazia e quem chamou segue com os outros caminhos:
|
|
158
|
+
* não é erro, é um caminho que esta máquina não tem.
|
|
159
|
+
*/
|
|
160
|
+
async function googleImagens(consulta, { quantos = 8, chave, cx, tipo } = {}) {
|
|
161
|
+
const key = chave || process.env.PRIMOCODE_GOOGLE_KEY;
|
|
162
|
+
const eng = cx || process.env.PRIMOCODE_GOOGLE_CX;
|
|
163
|
+
if (!key || !eng) return [];
|
|
164
|
+
const q = new URLSearchParams({
|
|
165
|
+
key, cx: eng, q: consulta, searchType: 'image', num: String(Math.min(10, quantos)),
|
|
166
|
+
safe: 'active', imgSize: 'xlarge',
|
|
167
|
+
});
|
|
168
|
+
if (tipo === 'logo') { q.set('imgType', 'clipart'); q.set('imgSize', 'large'); }
|
|
169
|
+
const r = await buscarJSON('https://www.googleapis.com/customsearch/v1?' + q.toString());
|
|
170
|
+
return (r.items || []).map((i) => ({
|
|
171
|
+
fonte: 'google', titulo: i.title, url: i.link,
|
|
172
|
+
pagina: (i.image && i.image.contextLink) || i.link,
|
|
173
|
+
autor: i.displayLink || null, licenca: 'confira na página de origem',
|
|
174
|
+
largura: i.image && i.image.width, altura: i.image && i.image.height,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A busca que o agente chama: tenta os caminhos na ordem certa para o que
|
|
180
|
+
* está sendo procurado, e junta.
|
|
181
|
+
*
|
|
182
|
+
* Logo pede Wikimedia primeiro; foto pede Openverse. O Google entra sempre
|
|
183
|
+
* que houver chave — é ele que cobre o que falta.
|
|
184
|
+
*/
|
|
185
|
+
async function procurar(consulta, { tipo = 'imagem', quantos = 8, licenca = 'comercial', orientacao } = {}) {
|
|
186
|
+
const achados = [];
|
|
187
|
+
const tentar = async (fn) => {
|
|
188
|
+
try { achados.push(...(await fn())); } catch (e) { falhas.push(e.message); }
|
|
189
|
+
};
|
|
190
|
+
const falhas = [];
|
|
191
|
+
|
|
192
|
+
if (tipo === 'musica' || tipo === 'audio') {
|
|
193
|
+
await tentar(() => openverseAudio(consulta, { quantos, licenca }));
|
|
194
|
+
} else if (tipo === 'logo') {
|
|
195
|
+
await tentar(() => wikimediaLogos(consulta, { quantos }));
|
|
196
|
+
await tentar(() => googleImagens(consulta + ' logo', { quantos, tipo: 'logo' }));
|
|
197
|
+
if (!achados.length) await tentar(() => openverseImagens(consulta + ' logo', { quantos, licenca }));
|
|
198
|
+
} else {
|
|
199
|
+
await tentar(() => openverseImagens(consulta, { quantos, licenca, orientacao }));
|
|
200
|
+
await tentar(() => googleImagens(consulta, { quantos }));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const vistos = new Set();
|
|
204
|
+
const lista = achados.filter((a) => a.url && !vistos.has(a.url) && vistos.add(a.url));
|
|
205
|
+
return { ok: lista.length > 0, tipo, consulta, achados: lista.slice(0, quantos),
|
|
206
|
+
falhas: lista.length ? [] : falhas };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── baixar ───────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
function nomeDe(url, titulo) {
|
|
212
|
+
let ext = '';
|
|
213
|
+
try { ext = path.extname(new URL(url).pathname).slice(0, 6); } catch { /* url estranha */ }
|
|
214
|
+
const base = String(titulo || 'arquivo').toLowerCase()
|
|
215
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
216
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'arquivo';
|
|
217
|
+
const marca = hash(url);
|
|
218
|
+
return `${base}-${marca}${ext || ''}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hash(texto) {
|
|
222
|
+
// Um identificador curto e estável por URL. Não é segurança — é cache.
|
|
223
|
+
let h = 0;
|
|
224
|
+
for (let i = 0; i < texto.length; i++) { h = (h * 31 + texto.charCodeAt(i)) | 0; }
|
|
225
|
+
return Math.abs(h).toString(36).slice(0, 7);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Baixa e guarda, com a licença ao lado. Mesma URL duas vezes: baixa uma.
|
|
230
|
+
*
|
|
231
|
+
* Recusa o que não é mídia (uma página de erro em HTML chega com 200 e
|
|
232
|
+
* viraria um "jpg" que não abre) e o que passa do teto.
|
|
233
|
+
*/
|
|
234
|
+
function baixar(url, { titulo, meta = {}, pasta = PASTA(), teto = TETO_BYTES } = {}) {
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
let destino;
|
|
237
|
+
try { destino = path.join(pasta, nomeDe(url, titulo)); }
|
|
238
|
+
catch { return resolve({ ok: false, error: 'url inválida' }); }
|
|
239
|
+
|
|
240
|
+
if (fs.existsSync(destino) && fs.statSync(destino).size > 0) {
|
|
241
|
+
return resolve({ ok: true, arquivo: destino, cache: true, ...lerFicha(destino) });
|
|
242
|
+
}
|
|
243
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
244
|
+
|
|
245
|
+
const puxar = (alvo, saltos = 0) => {
|
|
246
|
+
let u;
|
|
247
|
+
try { u = new URL(alvo); } catch { return resolve({ ok: false, error: 'url inválida' }); }
|
|
248
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
249
|
+
const req = lib.get(u, { timeout: 30000, headers: { 'User-Agent': agente() } }, (res) => {
|
|
250
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
251
|
+
res.resume();
|
|
252
|
+
if (saltos > 4) return resolve({ ok: false, error: 'redirecionamento sem fim' });
|
|
253
|
+
return puxar(new URL(res.headers.location, alvo).href, saltos + 1);
|
|
254
|
+
}
|
|
255
|
+
if (res.statusCode !== 200) {
|
|
256
|
+
res.resume();
|
|
257
|
+
return resolve({ ok: false, error: `o servidor respondeu ${res.statusCode}` });
|
|
258
|
+
}
|
|
259
|
+
const tipo = String(res.headers['content-type'] || '').split(';')[0];
|
|
260
|
+
if (tipo && !TIPOS_OK.test(tipo)) {
|
|
261
|
+
res.resume();
|
|
262
|
+
return resolve({ ok: false, error: `isso não é mídia (${tipo})` });
|
|
263
|
+
}
|
|
264
|
+
const tamanho = Number(res.headers['content-length'] || 0);
|
|
265
|
+
if (tamanho && tamanho > teto) {
|
|
266
|
+
res.resume();
|
|
267
|
+
return resolve({ ok: false, error: `arquivo grande demais (${Math.round(tamanho / 1e6)} MB)` });
|
|
268
|
+
}
|
|
269
|
+
let escritos = 0;
|
|
270
|
+
const saida = fs.createWriteStream(destino);
|
|
271
|
+
res.on('data', (p) => {
|
|
272
|
+
escritos += p.length;
|
|
273
|
+
if (escritos > teto) {
|
|
274
|
+
req.destroy();
|
|
275
|
+
saida.destroy();
|
|
276
|
+
try { fs.rmSync(destino, { force: true }); } catch {}
|
|
277
|
+
resolve({ ok: false, error: 'arquivo grande demais' });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
res.pipe(saida);
|
|
281
|
+
saida.on('finish', () => {
|
|
282
|
+
const ficha = { url, titulo: titulo || null, baixado: new Date().toISOString(), ...meta };
|
|
283
|
+
try { fs.writeFileSync(destino + '.json', JSON.stringify(ficha, null, 2), 'utf8'); } catch {}
|
|
284
|
+
resolve({ ok: true, arquivo: destino, bytes: escritos, cache: false, ficha });
|
|
285
|
+
});
|
|
286
|
+
saida.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
287
|
+
});
|
|
288
|
+
req.on('timeout', () => req.destroy(new Error('demorou demais')));
|
|
289
|
+
req.on('error', (e) => resolve({ ok: false, error: e.message }));
|
|
290
|
+
};
|
|
291
|
+
puxar(url);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function lerFicha(arquivo) {
|
|
296
|
+
try { return { ficha: JSON.parse(fs.readFileSync(arquivo + '.json', 'utf8')) }; }
|
|
297
|
+
catch { return { ficha: null }; }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** A lista de créditos do que foi usado — pronta para a cena final. */
|
|
301
|
+
function creditos(arquivos = null, pasta = PASTA()) {
|
|
302
|
+
let fichas = [];
|
|
303
|
+
try {
|
|
304
|
+
const alvos = arquivos || fs.readdirSync(pasta)
|
|
305
|
+
.filter((f) => f.endsWith('.json'))
|
|
306
|
+
.map((f) => path.join(pasta, f.replace(/\.json$/, '')));
|
|
307
|
+
fichas = alvos.map((a) => lerFicha(a).ficha).filter(Boolean);
|
|
308
|
+
} catch { /* sem acervo ainda */ }
|
|
309
|
+
return fichas.map((f) => ({
|
|
310
|
+
titulo: f.titulo, autor: f.autor || null, licenca: f.licenca || null,
|
|
311
|
+
fonte: f.fonte || null, pagina: f.pagina || f.url,
|
|
312
|
+
linha: [f.titulo, f.autor && `por ${f.autor}`, f.licenca, f.fonte]
|
|
313
|
+
.filter(Boolean).join(' · '),
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── tirar o fundo ────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Qual ferramenta desta máquina sabe recortar. A decisão fica separada do
|
|
321
|
+
* processo para poder ser testada sem recortar nada.
|
|
322
|
+
*/
|
|
323
|
+
function comoTirarFundo({ temRembg, temBackgroundRemover, temPython, temFfmpeg } = {}) {
|
|
324
|
+
if (temRembg) return 'rembg';
|
|
325
|
+
if (temBackgroundRemover) return 'backgroundremover';
|
|
326
|
+
if (temPython) return 'python-rembg';
|
|
327
|
+
if (temFfmpeg) return 'ffmpeg-chroma'; // só resolve fundo de cor chapada
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function existe(cmd) {
|
|
332
|
+
try { return spawnSync(cmd, ['--help'], { stdio: 'ignore', timeout: 8000 }).status != null; }
|
|
333
|
+
catch { return false; }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Recorta o fundo, com o que houver instalado.
|
|
338
|
+
*
|
|
339
|
+
* O caminho bom é o `rembg` (modelo U²-Net), que resolve pessoa, produto e
|
|
340
|
+
* logo com sombra. O `ffmpeg -colorkey` é o último recurso e só serve para
|
|
341
|
+
* fundo de UMA cor chapada — dizer que ele "tira o fundo" de uma foto
|
|
342
|
+
* qualquer seria mentira, e por isso o resultado vem com `metodo` na
|
|
343
|
+
* resposta: quem chamou mostra o que foi feito.
|
|
344
|
+
*/
|
|
345
|
+
async function tirarFundo(arquivo, { destino, cor = '0x00FF00', tolerancia = 0.3 } = {}) {
|
|
346
|
+
if (!fs.existsSync(arquivo)) return { ok: false, error: 'esse arquivo não existe' };
|
|
347
|
+
const saida = destino || arquivo.replace(/\.[^.]+$/, '') + '-sem-fundo.png';
|
|
348
|
+
const metodo = comoTirarFundo({
|
|
349
|
+
temRembg: existe('rembg'),
|
|
350
|
+
temBackgroundRemover: existe('backgroundremover'),
|
|
351
|
+
// `import rembg` só sai 0 quando o pacote ESTÁ lá — é a pergunta
|
|
352
|
+
// certa: ter python não é ter recortador.
|
|
353
|
+
temPython: (() => {
|
|
354
|
+
try { return spawnSync('python3', ['-c', 'import rembg'], { stdio: 'ignore', timeout: 20000 }).status === 0; }
|
|
355
|
+
catch { return false; }
|
|
356
|
+
})(),
|
|
357
|
+
temFfmpeg: existe('ffmpeg'),
|
|
358
|
+
});
|
|
359
|
+
if (!metodo) {
|
|
360
|
+
return { ok: false, error: 'nada nesta máquina sabe recortar fundo',
|
|
361
|
+
comoResolver: 'instale com: pip install rembg (ou pipx install rembg)' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const rodar = (cmd, args) => new Promise((ok) => {
|
|
365
|
+
const p = spawn(cmd, args, { stdio: 'ignore' });
|
|
366
|
+
p.on('error', () => ok(false));
|
|
367
|
+
p.on('close', (cod) => ok(cod === 0));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
let deu = false;
|
|
371
|
+
if (metodo === 'rembg') deu = await rodar('rembg', ['i', arquivo, saida]);
|
|
372
|
+
else if (metodo === 'backgroundremover') deu = await rodar('backgroundremover', ['-i', arquivo, '-o', saida]);
|
|
373
|
+
else if (metodo === 'python-rembg') {
|
|
374
|
+
deu = await rodar('python3', ['-c',
|
|
375
|
+
'import sys;from rembg import remove;open(sys.argv[2],"wb").write(remove(open(sys.argv[1],"rb").read()))',
|
|
376
|
+
arquivo, saida]);
|
|
377
|
+
} else {
|
|
378
|
+
deu = await rodar('ffmpeg', ['-y', '-i', arquivo, '-vf',
|
|
379
|
+
`colorkey=${cor}:${tolerancia}:0.1`, '-c:a', 'copy', saida]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!deu || !fs.existsSync(saida)) return { ok: false, error: `o recorte falhou (${metodo})`, metodo };
|
|
383
|
+
return { ok: true, arquivo: saida, metodo,
|
|
384
|
+
aviso: metodo === 'ffmpeg-chroma'
|
|
385
|
+
? 'recorte por cor: só funciona bem em fundo de uma cor só' : null };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
procurar, openverseImagens, openverseAudio, wikimediaLogos, googleImagens,
|
|
390
|
+
baixar, creditos, tirarFundo, comoTirarFundo, nomeDe, PASTA,
|
|
391
|
+
};
|
package/lib/act.js
CHANGED
|
@@ -460,6 +460,31 @@ async function runAct(userPrompt, opts) {
|
|
|
460
460
|
if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
+
/* ── O GATILHO DO DIRETOR ────────────────────────────────────────────
|
|
464
|
+
*
|
|
465
|
+
* "A IA identifica que precisa criar vídeo, ativa o agente, esse agente
|
|
466
|
+
* faz a estrutura e manda pra IA, e depois ela só faz o vídeo."
|
|
467
|
+
*
|
|
468
|
+
* Deixar isso só escrito no catálogo não basta: com um pedido curto
|
|
469
|
+
* ("faz um vídeo do produto") o modelo pula direto para o studio_criar,
|
|
470
|
+
* porque criar é o que ele sabe fazer. Quem reconhece o pedido é uma
|
|
471
|
+
* regra (diretor.precisaDeProducao), e ela entra como ORDEM na conversa —
|
|
472
|
+
* não como conselho no meio de cinquenta ferramentas.
|
|
473
|
+
*
|
|
474
|
+
* Num subagente não entra: ele já está fazendo uma parte de um plano, e
|
|
475
|
+
* um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
|
|
476
|
+
if (!subagent && profundidade === 0) {
|
|
477
|
+
const producao = require('./diretor.js').precisaDeProducao(userPrompt);
|
|
478
|
+
if (producao.producao) {
|
|
479
|
+
messages.push({ role: 'system', content:
|
|
480
|
+
'ESTE PEDIDO É DE PRODUÇÃO AUDIOVISUAL. Antes de criar qualquer peça, chame '
|
|
481
|
+
+ `planejar_producao com o pedido do usuário (formato "${producao.formato}"). `
|
|
482
|
+
+ 'Ele devolve a pauta — roteiro, narração falada, voz, câmera e a mídia a buscar — '
|
|
483
|
+
+ 'e é ela que você produz, cena a cena. Não comece pelo studio_criar: o roteiro '
|
|
484
|
+
+ 'escrito no meio do JSON da cena vira legenda de slide, que é o que se quer evitar.' });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
463
488
|
messages.push({ role: 'user', content: userPrompt });
|
|
464
489
|
|
|
465
490
|
let finalText = '';
|
|
@@ -760,6 +785,22 @@ async function runAct(userPrompt, opts) {
|
|
|
760
785
|
continue;
|
|
761
786
|
}
|
|
762
787
|
|
|
788
|
+
/* O DIRETOR mora aqui pelo mesmo motivo do pipeline: ele roda
|
|
789
|
+
um AGENTE, e quem sabe rodar agente é este arquivo. É a peça
|
|
790
|
+
que o dono pediu — "a IA identifica que precisa criar vídeo,
|
|
791
|
+
ativa o agente, esse agente faz a estrutura e manda pra IA, e
|
|
792
|
+
depois ela só faz o vídeo". */
|
|
793
|
+
if (tc.name === 'planejar_producao') {
|
|
794
|
+
algoRodou = true;
|
|
795
|
+
fala.acao(tc.name, tc.arguments, profundidade);
|
|
796
|
+
const r = await rodarDiretor(tc.arguments || {}, {
|
|
797
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
798
|
+
});
|
|
799
|
+
logResult(tc.name, r, profundidade);
|
|
800
|
+
messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
763
804
|
// Repetiu a mesma chamada? Não executa de novo. Rodar outra vez
|
|
764
805
|
// gastaria a cota para devolver exatamente o mesmo texto, e é
|
|
765
806
|
// esse texto que convence o modelo a repetir mais uma vez. Sem
|
|
@@ -994,5 +1035,60 @@ async function rodarPipeline(args, ctx) {
|
|
|
994
1035
|
});
|
|
995
1036
|
}
|
|
996
1037
|
|
|
1038
|
+
/* ══ O DIRETOR ═════════════════════════════════════════════════════════════
|
|
1039
|
+
*
|
|
1040
|
+
* "Tenha uma IA específica pra roteiro que monta tudo — narração, qual voz,
|
|
1041
|
+
* roteiro. Ela estrutura tudo antes de mandar pra IA."
|
|
1042
|
+
*
|
|
1043
|
+
* As regras, o prompt e a conferência do plano moram em `diretor.js`. Aqui é
|
|
1044
|
+
* a costura: uma conversa NOVA para o diretor (ele não vê a do usuário nem a
|
|
1045
|
+
* do produtor — é isso que faz o roteiro parar de virar legenda do slide) e a
|
|
1046
|
+
* pauta de volta, que é o que o produtor recebe no lugar do pedido cru.
|
|
1047
|
+
*/
|
|
1048
|
+
async function rodarDiretor(args, ctx) {
|
|
1049
|
+
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
1050
|
+
const diretor = require('./diretor.js');
|
|
1051
|
+
|
|
1052
|
+
const correr = async (prompt, rotulo) => {
|
|
1053
|
+
if (PARAR) return '';
|
|
1054
|
+
fala.agente('Diretor', rotulo, profundidade);
|
|
1055
|
+
const r = await runAct(prompt, {
|
|
1056
|
+
server, model, token, projectDir, projectName,
|
|
1057
|
+
history: [], profundidade: profundidade + 1, subagent: true, motor,
|
|
1058
|
+
});
|
|
1059
|
+
return (r && r.assistantMessage && r.assistantMessage.content) || '';
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const r = await diretor.planejar({
|
|
1063
|
+
pedido: args.pedido || args.prompt || '',
|
|
1064
|
+
segundos: Number(args.segundos) || 30,
|
|
1065
|
+
formato: args.formato === 'apresentacao' ? 'apresentacao' : 'video',
|
|
1066
|
+
contexto: args.contexto || '',
|
|
1067
|
+
correr,
|
|
1068
|
+
mostrar: (linha) => console.log(fala.resultado(profundidade) + c.muted(linha)),
|
|
1069
|
+
});
|
|
1070
|
+
if (!r.ok) return r;
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
ok: true,
|
|
1074
|
+
motor: r.plano.motor,
|
|
1075
|
+
voz: r.plano.voz,
|
|
1076
|
+
midia: r.plano.midia,
|
|
1077
|
+
trilha: r.plano.trilha,
|
|
1078
|
+
pauta: diretor.pauta(r.plano),
|
|
1079
|
+
plano: r.plano,
|
|
1080
|
+
ressalvas: r.defeitos,
|
|
1081
|
+
aviso: 'A ESTRUTURA ESTÁ PRONTA. Agora só PRODUZA: busque a mídia listada com '
|
|
1082
|
+
+ 'midia_procurar + midia_baixar (use o "src" que volta em fundo.imagem e em src); '
|
|
1083
|
+
+ 'a TRILHA vem do buscar_audio (já baixa e credita) e a identidade de uma empresa, do marca_da_empresa. '
|
|
1084
|
+
+ `e monte com studio_criar (${r.plano.formato === 'apresentacao' ? 'prisma' : 'corte'}) `
|
|
1085
|
+
+ 'seguindo a pauta cena a cena. Não invente cena, não troque a ordem, não reescreva a narração. '
|
|
1086
|
+
+ (r.plano.motor.motor === 'remotion'
|
|
1087
|
+
? 'Depois de criar, gere o projeto Remotion com remotion_projeto: '
|
|
1088
|
+
+ r.plano.motor.motivo + '.'
|
|
1089
|
+
: 'O motor é o estúdio: ' + r.plano.motor.motivo + '.'),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
997
1093
|
module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
998
1094
|
anotacaoDoQueJaFoi, encolherParaContinuar, pareceParadaNoMeio, MAX_ITERATIONS, MAX_EMPURROES };
|