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/lib/audio-livre.js
CHANGED
|
@@ -64,7 +64,12 @@ async function procurar({ consulta, tipo = 'musica', dur = 0, comercial = true,
|
|
|
64
64
|
if (!bruto) return { ok: false, error: 'o Openverse não respondeu (rede?)' };
|
|
65
65
|
let j;
|
|
66
66
|
try { j = JSON.parse(bruto); } catch { return { ok: false, error: 'resposta ilegível do Openverse' }; }
|
|
67
|
-
|
|
67
|
+
// `f.url &&` PRECISA valer sempre: sem o parêntese, `&&` liga mais forte
|
|
68
|
+
// que `||` e `comercial: false` deixava passar item sem url (o download
|
|
69
|
+
// falhava três vezes e a tool dizia "não consegui baixar") E, pior, faixa
|
|
70
|
+
// NC dentro de peça comercial — o que a lista LIVRES existe para impedir.
|
|
71
|
+
const itens = (j.results || []).filter((f) => f.url
|
|
72
|
+
&& (LIVRES.has(String(f.license || '').toLowerCase()) || !comercial));
|
|
68
73
|
const alvoMs = tipo === 'efeito' ? 2500 : (dur ? dur * 1000 : 90000);
|
|
69
74
|
const notas = itens.map((f) => {
|
|
70
75
|
let n = 0;
|
package/lib/boas-vindas.js
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* pra próxima tela, e aí você seleciona Claude ou GPT — e quando seleciona,
|
|
17
17
|
* ele dá aquele /gpt ou /claude, e vai funcionar como já funciona hoje."
|
|
18
18
|
*
|
|
19
|
+
* ── O FLUXO, DO NPM AO PRIMEIRO PEDIDO ───────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* npm → `primocode` → login (a página /token do conecta-primo-ai, que é o
|
|
22
|
+
* login normal do app com o token do PrimoCode; o site fala com o terminal
|
|
23
|
+
* sozinho) → "✅ Autenticado, redirecionando…" → "Como quer prosseguir?",
|
|
24
|
+
* com as SETINHAS: API padrão, API própria de outra IA, ou uma IA já
|
|
25
|
+
* configurada. Escolheu, está pronto para usar.
|
|
26
|
+
*
|
|
19
27
|
* ── A ORDEM É O PRODUTO ──────────────────────────────────────────────────
|
|
20
28
|
* Conectar → conferir o plano → escolher o motor. Nesta ordem, e não em
|
|
21
29
|
* outra. A primeira versão disto oferecia o login como UMA OPÇÃO de menu, ao
|
|
@@ -43,6 +51,8 @@ const fs = require('fs');
|
|
|
43
51
|
const os = require('os');
|
|
44
52
|
const path = require('path');
|
|
45
53
|
|
|
54
|
+
const chavesModulo = require('./chaves.js');
|
|
55
|
+
|
|
46
56
|
const MARCA = () => path.join(os.homedir(), '.primocode', 'ja-abriu.json');
|
|
47
57
|
|
|
48
58
|
/** Já escolheu o motor alguma vez? A marca guarda a escolha, não o login. */
|
|
@@ -67,15 +77,17 @@ function esquecer() {
|
|
|
67
77
|
|
|
68
78
|
const MOTORES = [
|
|
69
79
|
{ id: 'padrao', titulo: 'Continuar com a API padrão',
|
|
70
|
-
detalhe: 'A que já vem pronta, incluída no seu plano.' },
|
|
71
|
-
{ id: 'propria', titulo: 'Usar a
|
|
72
|
-
detalhe: '
|
|
80
|
+
detalhe: 'A que já vem pronta, incluída no seu plano. É só começar.' },
|
|
81
|
+
{ id: 'propria', titulo: 'Usar a API própria de outra IA',
|
|
82
|
+
detalhe: 'Você cola a sua chave do Claude ou do GPT, e ela é usada nas chamadas.' },
|
|
83
|
+
{ id: 'configurada', titulo: 'Usar uma IA já configurada',
|
|
84
|
+
detalhe: 'O Claude Code ou o Codex que já está instalado e logado nesta máquina.' },
|
|
73
85
|
];
|
|
74
86
|
|
|
75
87
|
const PROVEDORES = [
|
|
76
|
-
{ id: 'claude', titulo: 'Claude', comando: '/claude',
|
|
88
|
+
{ id: 'claude', titulo: 'Claude', comando: '/claude', chave: 'claude',
|
|
77
89
|
detalhe: 'Usa o Claude Code da sua conta, no Opus.' },
|
|
78
|
-
{ id: 'gpt', titulo: 'GPT', comando: '/gpt',
|
|
90
|
+
{ id: 'gpt', titulo: 'GPT', comando: '/gpt', chave: 'codex',
|
|
79
91
|
detalhe: 'Usa o Codex da sua conta, num modelo barato.' },
|
|
80
92
|
];
|
|
81
93
|
|
|
@@ -90,14 +102,15 @@ function escolher(texto, lista) {
|
|
|
90
102
|
if (!t) return lista[0].id;
|
|
91
103
|
const n = Number(t);
|
|
92
104
|
if (Number.isInteger(n) && n >= 1 && n <= lista.length) return lista[n - 1].id;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
// Primeiro o que bate INTEIRO. O prefixo vem depois, senão "continuar com
|
|
106
|
+
// a API padrão" cairia em "configurada" só por começar com "con".
|
|
107
|
+
for (const e of lista) if (t === e.id || t === e.titulo.toLowerCase()) return e.id;
|
|
108
|
+
for (const e of lista) if (e.id.length >= 3 && t.startsWith(e.id.slice(0, 3))) return e.id;
|
|
97
109
|
// O jeito como as pessoas falam, e não como a lista se chama.
|
|
98
110
|
if (lista === MOTORES) {
|
|
111
|
+
if (/j[aá] config|j[aá] instal|j[aá] logad|configurad|instalad|logad/.test(t)) return 'configurada';
|
|
99
112
|
if (/padr|inclu|mesmo|pronta|voc[eê]s|de voc/.test(t)) return 'padrao';
|
|
100
|
-
if (/minha|meu|pr[oó]pri|conta|chave|assinatura/.test(t)) return 'propria';
|
|
113
|
+
if (/minha|meu|pr[oó]pri|conta|chave|assinatura|api de outra/.test(t)) return 'propria';
|
|
101
114
|
} else {
|
|
102
115
|
if (/claude|anthropic|opus|sonnet/.test(t)) return 'claude';
|
|
103
116
|
if (/gpt|openai|codex|chatgpt/.test(t)) return 'gpt';
|
|
@@ -105,7 +118,19 @@ function escolher(texto, lista) {
|
|
|
105
118
|
return null;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
/**
|
|
122
|
+
* A escolha na tela. Com terminal de verdade é a LISTA COM AS SETINHAS — ↑↓
|
|
123
|
+
* anda, Enter confirma, como no Claude Code: ninguém precisa saber que o item
|
|
124
|
+
* tem número. `selecionar` vem de quem chamou (o bin, que é dono do teclado);
|
|
125
|
+
* onde ele não existe — teste, pipe, terminal sem raw mode — cai na lista
|
|
126
|
+
* numerada de sempre, que responde ao número, ao nome e ao Enter.
|
|
127
|
+
*/
|
|
128
|
+
async function perguntarDaLista({ perguntar, mostrar, cores: c, titulo, ajuda, lista, selecionar }) {
|
|
129
|
+
if (selecionar) {
|
|
130
|
+
const escolhido = await selecionar({ titulo, ajuda, lista });
|
|
131
|
+
if (escolhido && lista.some((e) => e.id === escolhido)) return escolhido;
|
|
132
|
+
if (escolhido === null) return lista[0].id; // Esc: fica com a primeira
|
|
133
|
+
}
|
|
109
134
|
mostrar('');
|
|
110
135
|
mostrar(c.bold(c.brand(' ' + titulo)));
|
|
111
136
|
if (ajuda) mostrar(c.muted(' ' + ajuda));
|
|
@@ -142,7 +167,7 @@ async function perguntarDaLista({ perguntar, mostrar, cores: c, titulo, ajuda, l
|
|
|
142
167
|
* @param {(cmd:string)=>Promise<any>} op.aplicar executa /claude ou /gpt
|
|
143
168
|
*/
|
|
144
169
|
async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
145
|
-
conectado, conectar, sessao, aplicar } = {}) {
|
|
170
|
+
conectado, conectar, sessao, aplicar, selecionar, chaves = chavesModulo } = {}) {
|
|
146
171
|
|
|
147
172
|
/* Sem terminal de verdade (pipe, script, CI) não se pergunta nada — uma
|
|
148
173
|
pergunta onde ninguém pode responder trava o processo para sempre, e
|
|
@@ -166,6 +191,12 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
166
191
|
mostrar(c.muted(' Tente de novo com ') + c.brand('/connect') + c.muted('.'));
|
|
167
192
|
return { liberado: false, motivo: 'sem-login' };
|
|
168
193
|
}
|
|
194
|
+
/* A MESMA FRASE DOS DOIS LADOS. A página mostra "✅ Autenticado,
|
|
195
|
+
redirecionando…" e some; quem estava olhando o navegador volta ao
|
|
196
|
+
terminal e precisa ver que o terminal soube — senão parece que a
|
|
197
|
+
aprovação ficou no site. */
|
|
198
|
+
mostrar('');
|
|
199
|
+
mostrar(c.ok(' ✅ Autenticado, redirecionando…'));
|
|
169
200
|
credencial = conectado ? conectado() : { plano: r.plano };
|
|
170
201
|
}
|
|
171
202
|
|
|
@@ -209,8 +240,9 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
209
240
|
if (jaEscolheu()) return { liberado: true, motivo: 'ja-escolheu', plano, perguntou: false };
|
|
210
241
|
|
|
211
242
|
const motor = await perguntarDaLista({
|
|
212
|
-
perguntar, mostrar, cores: c,
|
|
213
|
-
titulo: '
|
|
243
|
+
perguntar, mostrar, cores: c, selecionar,
|
|
244
|
+
titulo: 'Como quer prosseguir?',
|
|
245
|
+
ajuda: 'Use as setinhas ↑↓ e Enter para confirmar — ou digite o número.',
|
|
214
246
|
lista: MOTORES,
|
|
215
247
|
});
|
|
216
248
|
|
|
@@ -222,21 +254,48 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
222
254
|
return { liberado: true, motor: 'padrao', plano, perguntou: true };
|
|
223
255
|
}
|
|
224
256
|
|
|
225
|
-
// ── 4. qual
|
|
257
|
+
// ── 4. qual IA ───────────────────────────────────────────────────────
|
|
258
|
+
const propria = motor === 'propria';
|
|
226
259
|
const qual = await perguntarDaLista({
|
|
227
|
-
perguntar, mostrar, cores: c,
|
|
228
|
-
titulo: 'Qual
|
|
229
|
-
ajuda:
|
|
260
|
+
perguntar, mostrar, cores: c, selecionar,
|
|
261
|
+
titulo: propria ? 'De qual IA é a sua chave?' : 'Qual IA já configurada?',
|
|
262
|
+
ajuda: propria
|
|
263
|
+
? 'A chave fica só nesta máquina, em ~/.primocode/chaves.json.'
|
|
264
|
+
: 'Ele usa o programa que você já tem instalado e logado.',
|
|
230
265
|
lista: PROVEDORES,
|
|
231
266
|
});
|
|
232
267
|
const escolhido = PROVEDORES.find((p) => p.id === qual) || PROVEDORES[0];
|
|
233
268
|
|
|
234
|
-
|
|
269
|
+
/* ── A CHAVE PRÓPRIA ─────────────────────────────────────────────────
|
|
270
|
+
* Só neste caminho se pede chave. Quem escolheu "já configurada" não tem
|
|
271
|
+
* chave para colar — a dele está no programa em que já entrou, e pedir
|
|
272
|
+
* uma ali seria inventar um passo que não existe.
|
|
273
|
+
*
|
|
274
|
+
* Chave recusada (colou a do outro provedor, veio truncada) NÃO trava a
|
|
275
|
+
* abertura: diz o que houve, guarda a escolha do motor e segue. O motor
|
|
276
|
+
* roda com o login que a pessoa já tem, e `/chave` conserta depois. */
|
|
277
|
+
if (propria && perguntar) {
|
|
278
|
+
const p = chaves.PROVEDORES[escolhido.chave] || {};
|
|
279
|
+
mostrar('');
|
|
280
|
+
mostrar(c.muted(` Cole a chave do ${escolhido.titulo}`)
|
|
281
|
+
+ c.dim(p.onde ? ` (pegue em ${p.onde})` : '') + c.muted('.'));
|
|
282
|
+
mostrar(c.dim(' Enter em branco pula: dá para configurar depois com ')
|
|
283
|
+
+ c.brand('/chave ' + escolhido.chave) + c.dim('.'));
|
|
284
|
+
const colada = String(await perguntar(c.brand(' ❯ ')) || '').trim();
|
|
285
|
+
if (colada) {
|
|
286
|
+
const g = chaves.guardar(escolhido.chave, colada);
|
|
287
|
+
mostrar(g.ok
|
|
288
|
+
? c.ok(` Chave do ${g.nome} guardada.`) + c.muted(` ${g.disfarce}`)
|
|
289
|
+
: c.err(` ${g.error}`));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
marcar({ motor: escolhido.id, origem: motor, plano });
|
|
235
294
|
mostrar('');
|
|
236
295
|
if (aplicar) await aplicar(escolhido.comando);
|
|
237
296
|
mostrar('');
|
|
238
|
-
return { liberado: true, motor: escolhido.id,
|
|
239
|
-
plano, perguntou: true };
|
|
297
|
+
return { liberado: true, motor: escolhido.id, origem: motor,
|
|
298
|
+
comando: escolhido.comando, plano, perguntou: true };
|
|
240
299
|
}
|
|
241
300
|
|
|
242
301
|
module.exports = {
|
package/lib/catalogo.js
CHANGED
|
@@ -89,6 +89,22 @@ const GRUPOS = [
|
|
|
89
89
|
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
|
|
90
90
|
{ tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
|
|
91
91
|
args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
|
|
92
|
+
{ tool: 'midia_procurar', escreve: false, faz: 'procura imagem, logo ou música na internet, com licença e crédito',
|
|
93
|
+
args: 'consulta: o que procurar, específico ("painel solar em telhado residencial", não "imagem legal"). '
|
|
94
|
+
+ 'tipo (opcional): imagem | logo | musica (padrão imagem). quantos (opcional, padrão 6). '
|
|
95
|
+
+ 'Para MÚSICA e EFEITO prefira buscar_audio, que já baixa, manda para o estúdio e devolve o crédito; '
|
|
96
|
+
+ 'para a logo de uma EMPRESA prefira marca_da_empresa, que traz também cor, paleta e fontes do site dela. '
|
|
97
|
+
+ 'orientacao (opcional): paisagem | retrato | quadrado. '
|
|
98
|
+
+ 'Logo vem do Wikimedia Commons (SVG de verdade da marca); foto e música do Openverse (licença aberta); '
|
|
99
|
+
+ 'com PRIMOCODE_GOOGLE_KEY e PRIMOCODE_GOOGLE_CX configurados, entra também a busca de imagens do Google. '
|
|
100
|
+
+ 'Devolve a lista com url, autor e licença — escolha UMA e chame midia_baixar.' },
|
|
101
|
+
{ tool: 'midia_baixar', escreve: false, faz: 'baixa o que foi encontrado e já deixa disponível para as peças',
|
|
102
|
+
args: 'url: a url do resultado de midia_procurar. titulo (opcional). semFundo (opcional): true recorta o fundo da imagem. '
|
|
103
|
+
+ 'paraEstudio (opcional, padrão true): também manda para a galeria do estúdio e devolve o "/midia/..." para usar em src e fundo. '
|
|
104
|
+
+ 'A licença fica gravada junto do arquivo; midia_creditos monta a lista para a cena final.' },
|
|
105
|
+
{ tool: 'midia_sem_fundo', escreve: false, faz: 'recorta o fundo de uma imagem que já está na máquina',
|
|
106
|
+
args: 'arquivo: caminho da imagem. destino (opcional). Usa rembg quando existe; sem ele, só fundo de cor chapada (avisa na resposta).' },
|
|
107
|
+
{ tool: 'midia_creditos', escreve: false, faz: 'lista os créditos do que foi baixado, prontos para a cena final ou a descrição', args: null },
|
|
92
108
|
{ tool: 'remotion_projeto', escreve: true, faz: 'transforma uma peça do estúdio em projeto Remotion (React) na pasta do usuário',
|
|
93
109
|
args: 'id da peça. pasta (opcional): nome da pasta. fps (opcional, padrão 30). renderizar: true só se o npm install já foi feito. '
|
|
94
110
|
+ 'Use SÓ quando o usuário pedir Remotion pelo nome, ou quando precisar de render determinístico, vídeo longo, codec/fps específico ou o projeto em React. '
|
|
@@ -164,6 +180,14 @@ const GRUPOS = [
|
|
|
164
180
|
args: 'pedido: o que o usuário quer, com as palavras dele. pasta (opcional): onde construir — sem ela, uma pasta nova com o nome do pedido. rodadas (opcional, 1 a 4, padrão 2): quantas voltas de correção. '
|
|
165
181
|
+ 'USE quando pedirem um app, um site, uma página ou uma ferramenta INTEIRA, do zero. NÃO use para mexer no que já existe, para um arquivo só, nem para conserto: são seis a oito chamadas de agente, '
|
|
166
182
|
+ 'e gastá-las para trocar a cor de um botão come o dia do usuário. O revisor ABRE a página num navegador invisível e mede — página em branco e erro de JavaScript reprovam sozinhos, o que ele achar volta para o construtor.' },
|
|
183
|
+
{ tool: 'planejar_producao', escreve: false, noLoop: true,
|
|
184
|
+
faz: 'ativa o DIRETOR: roteiro, narração cena a cena, voz, movimento, mídia a buscar e trilha — a estrutura ANTES de produzir',
|
|
185
|
+
args: 'pedido: o que o usuário quer, com as palavras dele. segundos (opcional, padrão 30): duração alvo. '
|
|
186
|
+
+ 'formato (opcional): video | apresentacao. contexto (opcional): o que já se sabe (marca, público, dados).\n'
|
|
187
|
+
+ 'USE SEMPRE antes de criar vídeo, reels, teaser, animação ou apresentação com narração — inclusive quando o pedido parecer simples. '
|
|
188
|
+
+ 'Devolve uma PAUTA: produza exatamente o que ela diz (não invente cena, não troque a ordem, não reescreva a narração), '
|
|
189
|
+
+ 'buscando a mídia com midia_procurar + midia_baixar e montando com studio_criar (corte/prisma). '
|
|
190
|
+
+ 'NÃO use para ajustar peça existente nem quando o usuário pediu só o roteiro em texto.' },
|
|
167
191
|
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true,
|
|
168
192
|
args: 'summary (opcional): uma frase do que foi feito.' },
|
|
169
193
|
],
|
package/lib/conta.js
CHANGED
|
@@ -38,6 +38,26 @@ const CONECTA = process.env.PRIMOCODE_CONTA_SERVER || 'https://conecta-primo-api
|
|
|
38
38
|
|
|
39
39
|
const ARQUIVO = path.join(os.homedir(), '.primocode', 'conta.json');
|
|
40
40
|
|
|
41
|
+
/* ── ONDE O LOGIN ABRE ───────────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* "Abre conecta-primo-ai.vercel.app/token do PrimoCode: abre o login normal
|
|
44
|
+
* do app, mas com o token do PrimoCode. Aí o site se comunica com o
|
|
45
|
+
* terminal."
|
|
46
|
+
*
|
|
47
|
+
* A página do token é do SITE, não da API. O servidor devolve a URL dele —
|
|
48
|
+
* que num ambiente antigo pode apontar para outra origem ou para outro
|
|
49
|
+
* caminho — e aqui ela é normalizada para o endereço acima, preservando o que
|
|
50
|
+
* vier na query (é ali que vai o código do pareamento, quando vai).
|
|
51
|
+
* `PRIMOCODE_SITE` troca a origem para rodar contra uma prévia local. */
|
|
52
|
+
const SITE = (process.env.PRIMOCODE_SITE || 'https://conecta-primo-ai.vercel.app').replace(/\/+$/, '');
|
|
53
|
+
const PAGINA_TOKEN = '/token';
|
|
54
|
+
|
|
55
|
+
function paginaDeLogin(urlDoServidor) {
|
|
56
|
+
let query = '';
|
|
57
|
+
try { query = new URL(urlDoServidor).search; } catch { /* sem url utilizável: só a página */ }
|
|
58
|
+
return SITE + PAGINA_TOKEN + query;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
// Os planos que liberam tudo. A lista é a MESMA do servidor
|
|
42
62
|
// (`PLANOS_COM_PRO`), e quem decide de verdade é ele: isto aqui só evita uma
|
|
43
63
|
// ida à rede para mostrar o selo na abertura.
|
|
@@ -201,11 +221,11 @@ async function entrar({ abrir, contar = () => {}, minutos = 5 } = {}) {
|
|
|
201
221
|
}
|
|
202
222
|
|
|
203
223
|
let origemDoSite = null;
|
|
204
|
-
try { origemDoSite = new URL(pedido.url).origin; } catch { /* url estranha: sem CORS para ela */ }
|
|
224
|
+
try { origemDoSite = new URL(paginaDeLogin(pedido.url)).origin; } catch { /* url estranha: sem CORS para ela */ }
|
|
205
225
|
const local = await servirPareamento(pedido.codigo, origemDoSite);
|
|
206
226
|
const copiado = await area.copiar(pedido.codigo);
|
|
207
227
|
|
|
208
|
-
let url = pedido.url;
|
|
228
|
+
let url = paginaDeLogin(pedido.url);
|
|
209
229
|
if (local.porta) url += (url.includes('?') ? '&' : '?') + 'porta=' + local.porta;
|
|
210
230
|
|
|
211
231
|
contar({ url, codigo: pedido.codigo, copiado: copiado.ok, porta: local.porta });
|
|
@@ -287,4 +307,5 @@ function versao() {
|
|
|
287
307
|
try { return require('../package.json').version; } catch { return '?'; }
|
|
288
308
|
}
|
|
289
309
|
|
|
290
|
-
module.exports = { entrar, sair, atual, sessao, dispositivos, servirPareamento,
|
|
310
|
+
module.exports = { entrar, sair, atual, sessao, dispositivos, servirPareamento, paginaDeLogin,
|
|
311
|
+
ARQUIVO, CONECTA, SITE, PAGINA_TOKEN, PLANOS_PRO, PORTA_PAREAMENTO };
|
package/lib/diretor.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diretor.js — o agente que ESTRUTURA antes de alguém produzir.
|
|
3
|
+
*
|
|
4
|
+
* "Tenha uma IA específica pra roteiro que monta tudo: narração, qual voz,
|
|
5
|
+
* roteiro, etc. Ela estrutura tudo antes de mandar pra IA. Funciona assim:
|
|
6
|
+
* a IA identifica que precisa criar vídeo, ativa o agente, esse agente faz
|
|
7
|
+
* a estrutura e manda pra IA, e depois ela só faz o vídeo."
|
|
8
|
+
*
|
|
9
|
+
* ── POR QUE UM PAPEL SEPARADO, E NÃO UM PARÁGRAFO NO PROMPT ──────────────
|
|
10
|
+
* É o mesmo motivo do pipeline.js: o modelo que está montando as cenas já
|
|
11
|
+
* gastou a atenção dele em JSON — fonte, cor, duração, layout — e é ali que o
|
|
12
|
+
* roteiro morre. Vira legenda do que aparece na tela ("Vantagens", "Nossos
|
|
13
|
+
* serviços"), sem arco, sem narração escrita, sem decisão de voz. O diretor
|
|
14
|
+
* roda numa conversa PRÓPRIA, sem ver JSON nenhum, e só decide: o que se
|
|
15
|
+
* conta, em que ordem, em quantos segundos, com que voz, com quais imagens e
|
|
16
|
+
* qual música. Depois disso o produtor tem pouco a inventar — e inventar é
|
|
17
|
+
* justamente onde ele erra.
|
|
18
|
+
*
|
|
19
|
+
* ── O PLANO É UM CONTRATO, NÃO UM TEXTO BONITO ───────────────────────────
|
|
20
|
+
* A saída do diretor é JSON com forma conhecida, e este módulo NORMALIZA e
|
|
21
|
+
* CONFERE o que voltou: duração que fecha, narração em toda cena, keyframes
|
|
22
|
+
* onde faz sentido, lista de mídia a buscar. Um plano que não fecha volta ao
|
|
23
|
+
* diretor com o defeito apontado — uma vez. Aceitar "quase certo" aqui custa
|
|
24
|
+
* um vídeo de 40 segundos que dura 12.
|
|
25
|
+
*
|
|
26
|
+
* ── O MOTOR NÃO É GOSTO ──────────────────────────────────────────────────
|
|
27
|
+
* Estúdio ou Remotion: a escolha tem regra (`motorPara`), porque a resposta
|
|
28
|
+
* errada aparece tarde — no vídeo de três minutos que engasgou na gravação do
|
|
29
|
+
* navegador, ou nos cinco minutos de `npm install` para fazer um story de
|
|
30
|
+
* 15 segundos.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
// ── 1. reconhecer que o pedido é de vídeo ────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const PEDE_VIDEO = /\b(v[ií]deo|reels?|tiktok|short|shorts|motion|anima[cç][aã]o|anima\b|teaser|trailer|comercial|propaganda|v[ií]nheta|vinheta|abertura|capcut|premiere|after\s*effects|aftereffects)\b/i;
|
|
38
|
+
const PEDE_SLIDE = /\b(slide|slides|deck|apresenta[cç][aã]o|pitch|powerpoint|keynote|prisma)\b/i;
|
|
39
|
+
const SO_TEXTO = /\b(roteiro apenas|s[oó] o roteiro|s[oó] o texto|sem v[ií]deo)\b/i;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* O pedido precisa de produção audiovisual? E de que tipo?
|
|
43
|
+
*
|
|
44
|
+
* Isto é o gatilho do agente: o CLI pergunta aqui antes de deixar o produtor
|
|
45
|
+
* começar. Pedir "só o roteiro" NÃO liga a produção — quem pediu texto quer
|
|
46
|
+
* texto, e devolver um mp4 é ignorar a pessoa.
|
|
47
|
+
*/
|
|
48
|
+
function precisaDeProducao(texto) {
|
|
49
|
+
const t = String(texto || '');
|
|
50
|
+
if (!t.trim()) return { producao: false };
|
|
51
|
+
if (SO_TEXTO.test(t)) return { producao: false, motivo: 'pediu só o texto' };
|
|
52
|
+
if (PEDE_VIDEO.test(t)) return { producao: true, formato: 'video', motivo: 'o pedido é de vídeo' };
|
|
53
|
+
if (PEDE_SLIDE.test(t)) return { producao: true, formato: 'apresentacao', motivo: 'o pedido é de apresentação' };
|
|
54
|
+
return { producao: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 2. as decisões que não são do modelo ─────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Estúdio ou Remotion.
|
|
61
|
+
*
|
|
62
|
+
* O estúdio exporta em segundos e não instala nada: é o padrão, e para story,
|
|
63
|
+
* reels e teaser é a resposta certa. Remotion custa um `npm install` de
|
|
64
|
+
* minutos e meio giga, e devolve o que o estúdio não tem: quadro a quadro
|
|
65
|
+
* determinístico, vídeo longo sem risco de engasgo, canal alfa e ProRes.
|
|
66
|
+
*/
|
|
67
|
+
function motorPara({ segundos = 0, pedido = '', alfa = false, fps = 30, entrega } = {}) {
|
|
68
|
+
const t = String(pedido).toLowerCase();
|
|
69
|
+
if (/remotion/.test(t)) return { motor: 'remotion', motivo: 'foi pedido pelo nome' };
|
|
70
|
+
if (/(est[uú]dio|studio|r[aá]pido|agora|j[aá])\b/.test(t) && segundos <= 60) {
|
|
71
|
+
return { motor: 'estudio', motivo: 'pedido rápido e curto' };
|
|
72
|
+
}
|
|
73
|
+
if (alfa || /alfa|transpar[eê]nci|prores|canal alfa/.test(t)) {
|
|
74
|
+
return { motor: 'remotion', motivo: 'canal alfa e ProRes só existem no Remotion' };
|
|
75
|
+
}
|
|
76
|
+
if (entrega === 'cinema' || fps > 30) {
|
|
77
|
+
return { motor: 'remotion', motivo: 'fps alto pede render determinístico' };
|
|
78
|
+
}
|
|
79
|
+
if (segundos > 90) {
|
|
80
|
+
return { motor: 'remotion', motivo: 'vídeo longo: a gravação do navegador engasga' };
|
|
81
|
+
}
|
|
82
|
+
return { motor: 'estudio', motivo: 'curto: sai em segundos, sem instalar nada' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A voz. Tom e idioma decidem; o nome exato quem resolve é o estúdio, que
|
|
87
|
+
* sabe o que está instalado — aqui sai a INTENÇÃO, que é o que o diretor
|
|
88
|
+
* precisa escrever no plano.
|
|
89
|
+
*/
|
|
90
|
+
function vozPara({ tom = 'neutro', idioma = 'pt-BR', genero } = {}) {
|
|
91
|
+
const t = String(tom).toLowerCase();
|
|
92
|
+
const perfil = /urgente|energ|hype|an[uú]ncio|venda/.test(t) ? 'energica'
|
|
93
|
+
: /calm|institucional|corporativ|s[eé]ri/.test(t) ? 'firme'
|
|
94
|
+
: /doc|narrativ|hist[oó]ria|documental/.test(t) ? 'grave'
|
|
95
|
+
: /leve|simp[aá]tic|amig|casual/.test(t) ? 'proxima'
|
|
96
|
+
: 'neutra';
|
|
97
|
+
const ritmo = perfil === 'energica' ? 1.08 : perfil === 'grave' ? 0.94 : 1;
|
|
98
|
+
return { perfil, idioma, genero: genero || 'indiferente', ritmo,
|
|
99
|
+
instrucao: {
|
|
100
|
+
energica: 'projeção alta, frases curtas, corte seco entre elas',
|
|
101
|
+
firme: 'ritmo constante, sem exagero, pausa depois do número',
|
|
102
|
+
grave: 'grave e pausada, respira entre as frases',
|
|
103
|
+
proxima: 'como quem conversa, sorriso na voz',
|
|
104
|
+
neutra: 'clara e neutra, sem cor demais',
|
|
105
|
+
}[perfil] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Quantas cenas cabem, e de que tamanho, num vídeo desta duração. */
|
|
109
|
+
function ritmoPara(segundos) {
|
|
110
|
+
const s = Math.max(5, Number(segundos) || 30);
|
|
111
|
+
const media = s <= 20 ? 2.6 : s <= 45 ? 3.4 : s <= 90 ? 4.2 : 5;
|
|
112
|
+
return { cenas: Math.max(3, Math.round(s / media)), duracaoMedia: media,
|
|
113
|
+
palavrasPorCena: Math.round(media * 2.6) }; // ~155 palavras/minuto de narração
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── 3. o plano ───────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const CAMPOS_CENA = ['titulo', 'narracao', 'duracao', 'layout', 'fundo', 'transicao',
|
|
119
|
+
'elementos', 'midia', 'camera', 'chaves', 'notas'];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normaliza o que o diretor devolveu e aponta o que ficou faltando.
|
|
123
|
+
*
|
|
124
|
+
* Não conserta conteúdo — conteúdo é dele. Conserta FORMA: duração que virou
|
|
125
|
+
* string, cena sem transição, o total que não fecha com o pedido. E lista os
|
|
126
|
+
* defeitos que só o diretor resolve (cena muda, narração vazia), para voltar
|
|
127
|
+
* com o apontamento em vez de seguir com o plano torto.
|
|
128
|
+
*/
|
|
129
|
+
function normalizarPlano(bruto, { segundos } = {}) {
|
|
130
|
+
const p = bruto && typeof bruto === 'object' ? bruto : {};
|
|
131
|
+
const defeitos = [];
|
|
132
|
+
const cenas = Array.isArray(p.cenas) ? p.cenas : [];
|
|
133
|
+
if (!cenas.length) defeitos.push('o plano veio sem cenas');
|
|
134
|
+
|
|
135
|
+
const TRANSICOES = ['corte', 'fade', 'deslizar', 'subir', 'zoom', 'limpar', 'persiana', 'flash'];
|
|
136
|
+
const limpas = cenas.map((c, i) => {
|
|
137
|
+
const cena = {};
|
|
138
|
+
for (const k of CAMPOS_CENA) if (c && c[k] !== undefined) cena[k] = c[k];
|
|
139
|
+
cena.duracao = Math.max(1.2, Math.min(15, Number(cena.duracao) || 3.5));
|
|
140
|
+
cena.narracao = typeof cena.narracao === 'string' ? cena.narracao.trim() : '';
|
|
141
|
+
if (!cena.narracao) defeitos.push(`cena ${i + 1} sem narração`);
|
|
142
|
+
if (!cena.titulo && !cena.elementos) defeitos.push(`cena ${i + 1} sem nada na tela`);
|
|
143
|
+
// Tudo em fade parece slide, não vídeo — e o diretor esquece disso.
|
|
144
|
+
if (!TRANSICOES.includes(cena.transicao)) cena.transicao = i === 0 ? 'fade' : TRANSICOES[(i % 6) + 1];
|
|
145
|
+
cena.midia = Array.isArray(cena.midia) ? cena.midia.filter((m) => m && m.busca) : [];
|
|
146
|
+
return cena;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const total = Math.round(limpas.reduce((s, c) => s + c.duracao, 0) * 10) / 10;
|
|
150
|
+
const alvo = Number(segundos) || Number(p.segundos) || total;
|
|
151
|
+
// 20% de folga: o alvo é intenção, não cronômetro. Fora disso o vídeo
|
|
152
|
+
// entrega outra coisa — 12 segundos onde pediram 40.
|
|
153
|
+
const fecha = !alvo || Math.abs(total - alvo) <= Math.max(3, alvo * 0.2);
|
|
154
|
+
if (!fecha) defeitos.push(`a soma das cenas dá ${total}s e o pedido é ${alvo}s`);
|
|
155
|
+
|
|
156
|
+
const plano = {
|
|
157
|
+
titulo: String(p.titulo || '').trim() || 'Sem título',
|
|
158
|
+
formato: p.formato === 'apresentacao' ? 'apresentacao' : 'video',
|
|
159
|
+
proporcao: ['16:9', '9:16', '1:1', '4:5'].includes(p.proporcao) ? p.proporcao : '16:9',
|
|
160
|
+
tema: p.tema || 'meia-noite',
|
|
161
|
+
tom: p.tom || 'neutro',
|
|
162
|
+
publico: p.publico || null,
|
|
163
|
+
promessa: p.promessa || null, // o que a pessoa leva do vídeo
|
|
164
|
+
segundos: total,
|
|
165
|
+
voz: vozPara({ tom: p.tom, idioma: p.idioma || 'pt-BR',
|
|
166
|
+
genero: p.voz && p.voz.genero }),
|
|
167
|
+
trilha: p.trilha && (p.trilha.busca || p.trilha.arquivo)
|
|
168
|
+
? { busca: p.trilha.busca || null, arquivo: p.trilha.arquivo || null,
|
|
169
|
+
volume: Number(p.trilha.volume) || 0.18, humor: p.trilha.humor || p.tom || null }
|
|
170
|
+
: null,
|
|
171
|
+
cenas: limpas,
|
|
172
|
+
midia: limpas.flatMap((c, i) => c.midia.map((m) => ({ cena: i + 1, ...m }))),
|
|
173
|
+
creditos: !!p.creditos,
|
|
174
|
+
};
|
|
175
|
+
plano.motor = motorPara({ segundos: total, pedido: p.pedido || '', alfa: !!p.alfa,
|
|
176
|
+
fps: Number(p.fps) || 30 });
|
|
177
|
+
return { plano, defeitos, ok: defeitos.length === 0 };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 4. o prompt do diretor ───────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
const DIRETOR = (pedido, { segundos = 30, formato = 'video', contexto = '' } = {}) => {
|
|
183
|
+
const r = ritmoPara(segundos);
|
|
184
|
+
return `Você é o DIRETOR de conteúdo. Você NÃO produz nada: você ESTRUTURA, e outra IA produz a partir do que você escrever. Não escreva código, não escreva JSON de cena de ferramenta, não chame ferramenta nenhuma.
|
|
185
|
+
|
|
186
|
+
PEDIDO: ${pedido}
|
|
187
|
+
${contexto ? `CONTEXTO: ${contexto}\n` : ''}FORMATO: ${formato} · DURAÇÃO ALVO: ${segundos}s · RITMO: ~${r.cenas} cenas de ~${r.duracaoMedia}s (~${r.palavrasPorCena} palavras de narração por cena)
|
|
188
|
+
|
|
189
|
+
Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
"titulo": "…",
|
|
193
|
+
"formato": "${formato}",
|
|
194
|
+
"proporcao": "16:9 | 9:16 | 1:1 | 4:5",
|
|
195
|
+
"tema": "meia-noite | claro | papel | neon | brasa | floresta",
|
|
196
|
+
"tom": "como deve soar (institucional, urgente, documental, leve…)",
|
|
197
|
+
"publico": "para quem é",
|
|
198
|
+
"promessa": "o que a pessoa leva ao terminar de ver",
|
|
199
|
+
"trilha": { "busca": "termo de busca da música", "humor": "…", "volume": 0.18 },
|
|
200
|
+
"cenas": [
|
|
201
|
+
{
|
|
202
|
+
"titulo": "o que aparece grande na tela",
|
|
203
|
+
"narracao": "a frase FALADA, escrita para ser lida em voz alta",
|
|
204
|
+
"duracao": 3.5,
|
|
205
|
+
"layout": "capa | titulo | secao | duas-colunas | numeros | citacao | imagem-direita | centro | livre",
|
|
206
|
+
"fundo": "malha | aurora | raios | ondas | nevoa | vinheta, uma cor, ou {\\"imagem\\":\\"<da lista midia>\\"}",
|
|
207
|
+
"transicao": "corte | fade | deslizar | subir | zoom | limpar | persiana | flash",
|
|
208
|
+
"midia": [ { "busca": "o que procurar", "tipo": "imagem | logo", "papel": "fundo | destaque | selo", "semFundo": false } ],
|
|
209
|
+
"camera": "aproximar | afastar | pan-direita | pan-esquerda | subir | descer | empurrar | balanco | nenhuma",
|
|
210
|
+
"chaves": "descreva em uma frase o movimento principal desta cena (o que entra, o que atravessa, o que cresce)",
|
|
211
|
+
"notas": "o que o produtor precisa saber e não está acima"
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
REGRAS QUE SEPARAM UM VÍDEO BOM DE UM GENÉRICO:
|
|
217
|
+
1. NARRAÇÃO É FALA, NÃO LEGENDA. "Vantagens" não é narração. Escreva a frase que a pessoa ouve.
|
|
218
|
+
2. O QUE SE OUVE E O QUE SE VÊ NÃO SE REPETEM. A tela mostra a prova (número, imagem, logo); a voz conta o que ela significa.
|
|
219
|
+
3. ARCO. Primeira cena prende em 2 segundos, o miolo entrega, a última pede uma ação. Sem "Introdução / Desenvolvimento / Conclusão".
|
|
220
|
+
4. NÚMERO É CONCRETO. Nada de "muitos clientes": ou tem o número, ou a frase muda.
|
|
221
|
+
5. RITMO VARIADO. Cenas de durações diferentes e transições diferentes. Tudo em fade parece slide.
|
|
222
|
+
6. MÍDIA COM PROPÓSITO. Só peça imagem ou logo quando ela PROVA algo. Diga o termo de busca de verdade ("painel solar em telhado residencial", não "imagem legal").
|
|
223
|
+
7. MOVIMENTO. Em cada cena diga em "chaves" o movimento principal — é ele que tira a cara de apresentação parada.
|
|
224
|
+
8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar".`;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const REVISAO = (defeitos) => `O plano voltou com defeitos de FORMA. Corrija SÓ o que está listado e devolva o JSON inteiro de novo, no mesmo formato:
|
|
228
|
+
|
|
229
|
+
${defeitos.map((d) => '- ' + d).join('\n')}
|
|
230
|
+
|
|
231
|
+
Nada de explicação: só o bloco \`\`\`json.`;
|
|
232
|
+
|
|
233
|
+
/** Tira o JSON do que o modelo escreveu, com ou sem cerca. */
|
|
234
|
+
function extrairJSON(texto) {
|
|
235
|
+
const t = String(texto || '');
|
|
236
|
+
const cerca = t.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
237
|
+
const cru = cerca ? cerca[1] : t.slice(t.indexOf('{'), t.lastIndexOf('}') + 1);
|
|
238
|
+
try { return JSON.parse(cru); } catch { /* tenta limpar vírgula sobrando */ }
|
|
239
|
+
try { return JSON.parse(String(cru).replace(/,\s*([}\]])/g, '$1')); } catch { return null; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Roda o diretor: uma conversa própria, o plano de volta, conferido, e uma
|
|
244
|
+
* segunda chance quando a forma não fecha.
|
|
245
|
+
*
|
|
246
|
+
* @param {object} op
|
|
247
|
+
* @param {string} op.pedido
|
|
248
|
+
* @param {(prompt:string, papel:string)=>Promise<string>} op.correr uma conversa NOVA
|
|
249
|
+
* @param {number} [op.segundos]
|
|
250
|
+
* @param {(s:string)=>void} [op.mostrar]
|
|
251
|
+
*/
|
|
252
|
+
async function planejar({ pedido, correr, segundos = 30, formato = 'video',
|
|
253
|
+
contexto = '', mostrar = () => {} } = {}) {
|
|
254
|
+
if (!pedido) return { ok: false, error: 'Diga o que produzir.' };
|
|
255
|
+
if (typeof correr !== 'function') return { ok: false, error: 'sem quem rode o diretor' };
|
|
256
|
+
|
|
257
|
+
mostrar('diretor — roteiro, narração e voz');
|
|
258
|
+
let bruto = extrairJSON(await correr(DIRETOR(pedido, { segundos, formato, contexto }), 'diretor'));
|
|
259
|
+
if (!bruto) return { ok: false, error: 'o diretor não devolveu um plano legível' };
|
|
260
|
+
|
|
261
|
+
let { plano, defeitos, ok } = normalizarPlano({ ...bruto, pedido }, { segundos });
|
|
262
|
+
if (!ok) {
|
|
263
|
+
mostrar('diretor — corrigindo o plano');
|
|
264
|
+
const segunda = extrairJSON(await correr(REVISAO(defeitos), 'diretor (revisão)'));
|
|
265
|
+
if (segunda) {
|
|
266
|
+
const r2 = normalizarPlano({ ...segunda, pedido }, { segundos });
|
|
267
|
+
// Fica com o melhor dos dois: a segunda volta só vale se corrigiu.
|
|
268
|
+
if (r2.defeitos.length <= defeitos.length) ({ plano, defeitos, ok } = r2);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { ok: true, plano, defeitos, aprovado: defeitos.length === 0 };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* O plano vira a PAUTA do produtor — o texto que a IA de produção recebe no
|
|
276
|
+
* lugar do pedido cru. Ela não precisa mais decidir o que contar: só montar.
|
|
277
|
+
*/
|
|
278
|
+
function pauta(plano) {
|
|
279
|
+
const p = plano || {};
|
|
280
|
+
const linhas = [];
|
|
281
|
+
linhas.push(`PLANO DE PRODUÇÃO — ${p.titulo}`);
|
|
282
|
+
linhas.push(`formato ${p.formato} · ${p.proporcao} · ${p.segundos}s · tema ${p.tema} · tom ${p.tom}`);
|
|
283
|
+
linhas.push(`motor: ${p.motor && p.motor.motor} (${p.motor && p.motor.motivo})`);
|
|
284
|
+
if (p.promessa) linhas.push(`promessa: ${p.promessa}`);
|
|
285
|
+
if (p.publico) linhas.push(`público: ${p.publico}`);
|
|
286
|
+
linhas.push(`voz: ${p.voz && p.voz.perfil} · ${p.voz && p.voz.instrucao} · ${p.voz && p.voz.idioma}`);
|
|
287
|
+
if (p.trilha) linhas.push(`trilha: ${p.trilha.busca || p.trilha.arquivo} (volume ${p.trilha.volume})`);
|
|
288
|
+
linhas.push('');
|
|
289
|
+
(p.cenas || []).forEach((c, i) => {
|
|
290
|
+
linhas.push(`CENA ${i + 1} — ${c.duracao}s · layout ${c.layout || 'titulo'} · entra em ${c.transicao}`);
|
|
291
|
+
if (c.titulo) linhas.push(` na tela: ${c.titulo}`);
|
|
292
|
+
linhas.push(` narração: ${c.narracao}`);
|
|
293
|
+
if (c.camera && c.camera !== 'nenhuma') linhas.push(` câmera: ${c.camera}`);
|
|
294
|
+
if (c.chaves) linhas.push(` movimento: ${c.chaves}`);
|
|
295
|
+
(c.midia || []).forEach((m) => linhas.push(
|
|
296
|
+
` mídia: ${m.tipo || 'imagem'} "${m.busca}" como ${m.papel || 'destaque'}${m.semFundo ? ' (sem fundo)' : ''}`));
|
|
297
|
+
if (c.notas) linhas.push(` nota: ${c.notas}`);
|
|
298
|
+
linhas.push('');
|
|
299
|
+
});
|
|
300
|
+
linhas.push('Produza EXATAMENTE este plano. As decisões de conteúdo já foram tomadas:');
|
|
301
|
+
linhas.push('não invente cena, não troque a ordem, não reescreva a narração.');
|
|
302
|
+
return linhas.join('\n');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
precisaDeProducao, motorPara, vozPara, ritmoPara,
|
|
307
|
+
normalizarPlano, extrairJSON, planejar, pauta,
|
|
308
|
+
DIRETOR, REVISAO,
|
|
309
|
+
};
|
package/lib/tools.js
CHANGED
|
@@ -15,6 +15,7 @@ const browser = require('./browser');
|
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
16
|
const referencia = require('./referencia');
|
|
17
17
|
const studio = require('./studio');
|
|
18
|
+
const acervo = require('./acervo');
|
|
18
19
|
const cfg = require('./config');
|
|
19
20
|
|
|
20
21
|
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
@@ -580,6 +581,48 @@ async function despachar(name, args, ctx) {
|
|
|
580
581
|
return args.arquivo
|
|
581
582
|
? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
|
|
582
583
|
: studio.midiaListar();
|
|
584
|
+
/* ── MÍDIA DE VERDADE, VINDA DA INTERNET ──────────────────
|
|
585
|
+
*
|
|
586
|
+
* Buscar, baixar, recortar o fundo e já deixar na galeria do
|
|
587
|
+
* estúdio. O caminho inteiro é uma tool só do ponto de vista do
|
|
588
|
+
* agente (procurar → baixar), e o `paraEstudio` é o que evita a
|
|
589
|
+
* volta clássica: baixar num canto do disco e depois descobrir
|
|
590
|
+
* que a cena precisa de uma URL "/midia/...".
|
|
591
|
+
*
|
|
592
|
+
* A licença desce junto e fica gravada ao lado do arquivo. Um
|
|
593
|
+
* vídeo de cliente com foto CC BY sem crédito é problema do
|
|
594
|
+
* cliente, e descobrir isso depois de publicar é tarde. */
|
|
595
|
+
case 'midia_procurar':
|
|
596
|
+
return acervo.procurar(args.consulta || args.q || '', {
|
|
597
|
+
tipo: args.tipo, quantos: args.quantos, orientacao: args.orientacao,
|
|
598
|
+
licenca: args.licenca,
|
|
599
|
+
});
|
|
600
|
+
case 'midia_baixar': {
|
|
601
|
+
const r = await acervo.baixar(args.url, { titulo: args.titulo, meta: {
|
|
602
|
+
autor: args.autor || null, licenca: args.licenca || null,
|
|
603
|
+
fonte: args.fonte || null, pagina: args.pagina || null,
|
|
604
|
+
} });
|
|
605
|
+
if (!r.ok) return r;
|
|
606
|
+
let arquivo = r.arquivo;
|
|
607
|
+
if (args.semFundo) {
|
|
608
|
+
const corte = await acervo.tirarFundo(arquivo);
|
|
609
|
+
if (corte.ok) { arquivo = corte.arquivo; r.recorte = corte.metodo; r.avisoRecorte = corte.aviso; }
|
|
610
|
+
else r.avisoRecorte = corte.error;
|
|
611
|
+
}
|
|
612
|
+
r.arquivo = arquivo;
|
|
613
|
+
if (args.paraEstudio !== false) {
|
|
614
|
+
const g = await studio.midiaEnviar({ arquivo, nome: args.nome });
|
|
615
|
+
// O "/midia/..." é o que vai em `src` e em `fundo.imagem`.
|
|
616
|
+
if (g && g.ok !== false) r.src = g.url || g.src || null;
|
|
617
|
+
else r.avisoGaleria = (g && g.error) || 'não deu para mandar para a galeria';
|
|
618
|
+
}
|
|
619
|
+
return r;
|
|
620
|
+
}
|
|
621
|
+
case 'midia_sem_fundo':
|
|
622
|
+
return acervo.tirarFundo(resolveInProject(args.arquivo, projectDir), { destino: args.destino });
|
|
623
|
+
case 'midia_creditos':
|
|
624
|
+
return { ok: true, creditos: acervo.creditos() };
|
|
625
|
+
|
|
583
626
|
case 'studio_analisar': {
|
|
584
627
|
// Ouvir o vídeo para achar as pausas. A conta acontece num
|
|
585
628
|
// navegador (é quem sabe decodificar MP4 sem instalar nada) —
|