primocode 11.1.0 → 11.3.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 +16 -2
- package/bin/primocode-rpc.js +100 -0
- package/lib/agentes-pessoais.js +120 -0
- package/lib/app-server/cli.js +64 -0
- package/lib/app-server/enderecos.js +43 -0
- package/lib/app-server/index.js +3 -0
- package/lib/app-server/pareamento.js +61 -0
- package/lib/app-server/protocolo.js +86 -0
- package/lib/app-server/servidor.js +805 -0
- package/lib/app-server/websocket.js +87 -0
- package/lib/avisos.js +65 -0
- package/lib/cli/sessao.js +202 -17
- package/lib/cli-nova.js +237 -0
- package/lib/config/config.js +22 -2
- package/lib/diff.js +12 -2
- package/lib/motor/aprovacao.js +114 -0
- package/lib/motor/eventos.js +77 -0
- package/lib/motor/index.js +8 -0
- package/lib/motor/tela-terminal.js +64 -0
- package/lib/motor/transcricao.js +57 -0
- package/lib/motor/turno.js +43 -0
- package/lib/motor/voz.js +65 -0
- package/lib/piloto.js +39 -0
- package/lib/plugin/entregaveis/nucleo.js +2 -2
- package/lib/plugin/estudio/criar.js +113 -0
- package/lib/plugin/estudio/gosto.js +3 -0
- package/lib/plugin/estudio/historico.js +60 -0
- package/lib/plugin/estudio/index.js +14 -1
- package/lib/plugin/estudio/kits/tela.html +3 -1
- package/lib/plugin/estudio/miniatura.js +56 -0
- package/lib/plugin/video/auto.js +1 -1
- package/lib/plugin/video/catalogo.js +20 -0
- package/lib/plugin/video/mutar.js +28 -0
- package/lib/plugin/video/ops-fx.js +6 -6
- package/lib/plugin/video/ops.js +2 -13
- package/lib/plugin/video/pedido.js +1 -1
- package/lib/plugin/video/zoom.js +1 -1
- package/lib/projetos-meta.js +107 -0
- package/lib/qr.js +209 -0
- package/lib/session/runner/act.js +127 -68
- package/lib/skill/catalogo.js +3 -1
- package/lib/tool/tools.js +7 -0
- package/lib/tui/entrada.js +143 -18
- package/lib/tui/tela.js +312 -49
- package/lib/tui/ui.js +78 -4
- package/package.json +10 -2
- package/voz/primo_voz/transcrever.py +12 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Um turno sem tela: chama o runAct e emite o ciclo do turno.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { EVENTOS, novoId } = require('./eventos');
|
|
5
|
+
|
|
6
|
+
/* `iniciarTurno({ texto, ...opts, emitir })`. Sem `emitir`, usa a tela de terminal.
|
|
7
|
+
`opts` são os mesmos do runAct (server, model, token, history, projectDir…). */
|
|
8
|
+
async function iniciarTurno({ texto, emitir, portao, ...opts }) {
|
|
9
|
+
const act = require('../session/runner/act');
|
|
10
|
+
const saida = emitir || require('./tela-terminal').criar();
|
|
11
|
+
const id = novoId();
|
|
12
|
+
const t0 = Date.now();
|
|
13
|
+
saida({ evento: EVENTOS.TURNO_COMECOU, id, tipo: 'turno', estado: 'rodando', texto });
|
|
14
|
+
let r;
|
|
15
|
+
try {
|
|
16
|
+
// Com `portao` (lib/motor/aprovacao.js), aprovações e perguntas viram eventos.
|
|
17
|
+
const viaPortao = portao ? { autorizar: portao.autorizar, mudarPasta: opts.mudarPasta || portao.mudarPasta } : {};
|
|
18
|
+
r = await act.runAct(texto, { ...opts, ...viaPortao, emitir: saida });
|
|
19
|
+
} catch (erro) {
|
|
20
|
+
if (portao) portao.cancelar();
|
|
21
|
+
saida({ evento: EVENTOS.TURNO_INTERROMPIDO, id, tipo: 'turno', estado: 'erro', duracao: Date.now() - t0, erro });
|
|
22
|
+
throw erro;
|
|
23
|
+
}
|
|
24
|
+
const duracao = Date.now() - t0;
|
|
25
|
+
if (portao) portao.cancelar();
|
|
26
|
+
// Contexto da conversa para a status line (régua do /compact, lib/memoria-conversa.js).
|
|
27
|
+
try {
|
|
28
|
+
const memoria = require('../session/memoria-conversa');
|
|
29
|
+
const hist = (opts.history || []).concat(r && r.userMessage ? [r.userMessage] : [], r && r.assistantMessage ? [r.assistantMessage] : []);
|
|
30
|
+
saida({ evento: EVENTOS.USO, id: novoId(), tipo: 'uso', estado: 'ok', contexto: memoria.usoDoContexto(memoria.nova(), hist) });
|
|
31
|
+
} catch { /* sem medida, a status line fica sem o número */ }
|
|
32
|
+
if (act.interrompido()) {
|
|
33
|
+
saida({ evento: EVENTOS.TURNO_INTERROMPIDO, id, tipo: 'turno', estado: 'interrompido', duracao, resultado: r });
|
|
34
|
+
} else {
|
|
35
|
+
saida({ evento: EVENTOS.TURNO_TERMINOU, id, tipo: 'turno', estado: 'ok', duracao, resultado: r });
|
|
36
|
+
}
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const interromper = (...a) => require('../session/runner/act').interromper(...a);
|
|
41
|
+
const mandarDuranteATarefa = (...a) => require('../session/runner/act').mandarDuranteATarefa(...a);
|
|
42
|
+
|
|
43
|
+
module.exports = { iniciarTurno, interromper, mandarDuranteATarefa };
|
package/lib/motor/voz.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Voz do app (docs/estrutura.md 6.9): interface com provedor trocável.
|
|
2
|
+
// DECISÃO FINAL DO PROVEDOR PENDENTE com o dono. Aqui só o mínimo que não trava a escolha:
|
|
3
|
+
// - fala (TTS): provedor "kokoro" (voz local do Estúdio, grátis, gera arquivo de áudio; este módulo NUNCA toca som);
|
|
4
|
+
// - escuta (STT): fica no renderer (Web Speech API do navegador); o motor só informa que é do navegador.
|
|
5
|
+
// Provedor = { nome, disponivel() -> {ok, motivo?}, falar(texto, arquivo) -> Promise<{ok, error?}> }.
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const TETO_TEXTO = 600;
|
|
13
|
+
|
|
14
|
+
function provedorKokoro({ uv, voz, executar } = {}) {
|
|
15
|
+
const K = require('../plugin/estudio/kokoro');
|
|
16
|
+
const achar = () => uv || (() => { try { return require('child_process').execFileSync('which', ['uv'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() || null; } catch { return null; } })();
|
|
17
|
+
return {
|
|
18
|
+
nome: 'kokoro',
|
|
19
|
+
disponivel() {
|
|
20
|
+
if (!achar()) return { ok: false, motivo: 'falta o uv (voz local)' };
|
|
21
|
+
if (!K.acharEspeak()) return { ok: false, motivo: 'falta o espeak-ng (brew install espeak-ng)' };
|
|
22
|
+
return { ok: true };
|
|
23
|
+
},
|
|
24
|
+
async falar(texto, arquivo) {
|
|
25
|
+
const r = await K.narrar([{ texto }], arquivo, { voz, uv: achar(), ...(executar ? { executar } : {}) });
|
|
26
|
+
return r && r.ok ? { ok: true } : { ok: false, error: (r && r.error) || 'a voz não respondeu' };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PROVEDORES = { kokoro: (o) => provedorKokoro(o) };
|
|
32
|
+
|
|
33
|
+
function criarVoz({ provedor, pasta, escuta = 'navegador' } = {}) {
|
|
34
|
+
const prov = provedor || PROVEDORES.kokoro();
|
|
35
|
+
const saida = pasta || path.join(os.tmpdir(), 'primocode-voz');
|
|
36
|
+
let ativa = false;
|
|
37
|
+
let n = 0;
|
|
38
|
+
|
|
39
|
+
const estado = () => {
|
|
40
|
+
const d = prov.disponivel();
|
|
41
|
+
return {
|
|
42
|
+
ativa, provedor: prov.nome, fala: { disponivel: !!d.ok, motivo: d.ok ? undefined : d.motivo },
|
|
43
|
+
escuta, decisaoPendente: true,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
estado,
|
|
49
|
+
/** Liga o modo voz; com `texto`, gera o arquivo de áudio da fala (não toca). */
|
|
50
|
+
async iniciar({ texto } = {}) {
|
|
51
|
+
ativa = true;
|
|
52
|
+
const e = estado();
|
|
53
|
+
const t = typeof texto === 'string' ? texto.trim().slice(0, TETO_TEXTO) : '';
|
|
54
|
+
if (!t) return e;
|
|
55
|
+
if (!e.fala.disponivel) return { ...e, fala: { ...e.fala, gerou: false } };
|
|
56
|
+
fs.mkdirSync(saida, { recursive: true });
|
|
57
|
+
const arquivo = path.join(saida, `fala-${Date.now()}-${++n}.mp3`);
|
|
58
|
+
const r = await prov.falar(t, arquivo);
|
|
59
|
+
return r.ok ? { ...e, arquivo } : { ...e, fala: { ...e.fala, gerou: false, motivo: r.error } };
|
|
60
|
+
},
|
|
61
|
+
parar() { ativa = false; return estado(); },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { criarVoz, provedorKokoro, PROVEDORES, TETO_TEXTO };
|
package/lib/piloto.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Piloto automático (docs/estrutura.md, seção 9): os agentes ligados procuram o que fazer e fazem.
|
|
2
|
+
// Este módulo só decide QUEM roda e QUANDO parar; quem executa é injetado (o app-server passa o motor).
|
|
3
|
+
// Node puro. Um ciclo roda os agentes ligados um de cada vez e para no freio da cota.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
/* O que o agente recebe quando ninguém pediu nada: procurar trabalho no seu terreno. */
|
|
7
|
+
function pedidoDoPiloto(agente, { grupo = false } = {}) {
|
|
8
|
+
return `[piloto automático] Você é @${agente.nome}. Procure UMA coisa útil para fazer no seu terreno `
|
|
9
|
+
+ `(${agente.onde && agente.onde !== 'fora' ? `projeto ${agente.onde}` : 'fora de projetos'}) e faça. `
|
|
10
|
+
+ 'Tudo que escrever arquivo vai para a pessoa aprovar antes de valer. Se não houver nada a fazer, diga isso em uma frase.'
|
|
11
|
+
+ (grupo ? ' Se precisar de outro agente ligado, chame com spawn_agent (profundidade 1).' : '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Freio padrão: a cota do plano (lib/limites.js). Sem rede vale o modo local generoso. */
|
|
15
|
+
async function freioDaCota() {
|
|
16
|
+
try {
|
|
17
|
+
const c = await require('./config/limites').situacao();
|
|
18
|
+
if (c && (c.ok === false || c.liberado === false || c.restam === 0)) return { parar: true, motivo: 'a cota do plano acabou' };
|
|
19
|
+
} catch { /* sem leitura, não barra */ }
|
|
20
|
+
return { parar: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* `ciclo({ agentes, executar, freio, ocupado, grupo })` → { executados:[{agente,ok,resumo}], parou? }.
|
|
24
|
+
`executar(agente, pedido)` devolve o resumo (texto) ou lança. */
|
|
25
|
+
async function ciclo({ agentes = [], executar, freio = freioDaCota, ocupado = () => false, grupo = false } = {}) {
|
|
26
|
+
const ligados = agentes.filter((a) => a.ligado);
|
|
27
|
+
const r = { executados: [], parou: null };
|
|
28
|
+
if (!ligados.length) { r.parou = 'nenhum agente ligado'; return r; }
|
|
29
|
+
for (const a of ligados) {
|
|
30
|
+
if (ocupado()) { r.parou = 'a pessoa está trabalhando'; break; }
|
|
31
|
+
const f = await freio();
|
|
32
|
+
if (f && f.parar) { r.parou = f.motivo || 'freio'; break; }
|
|
33
|
+
try { r.executados.push({ agente: a.nome, ok: true, resumo: String(await executar(a, pedidoDoPiloto(a, { grupo })) || '').slice(0, 400) }); }
|
|
34
|
+
catch (e) { r.executados.push({ agente: a.nome, ok: false, resumo: (e && e.message) || 'falhou' }); }
|
|
35
|
+
}
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { ciclo, pedidoDoPiloto, freioDaCota };
|
|
@@ -103,7 +103,7 @@ function acharNavegador(env = process.env, existe = require('fs').existsSync) {
|
|
|
103
103
|
function htmlParaPdf(htmlPath, pdfPath, { navegador = acharNavegador() } = {}) {
|
|
104
104
|
if (!navegador) return { ok: false, erro: 'Chrome/Chromium não encontrado (defina PRIMO_CHROME)' };
|
|
105
105
|
const { spawnSync } = require('child_process');
|
|
106
|
-
const r = spawnSync(navegador, ['--headless=new', '--disable-gpu', '--no-pdf-header-footer',
|
|
106
|
+
const r = spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--no-pdf-header-footer',
|
|
107
107
|
`--print-to-pdf=${pdfPath}`, 'file://' + htmlPath], { encoding: 'utf8', timeout: 90000 });
|
|
108
108
|
return require('fs').existsSync(pdfPath) ? { ok: true } : { ok: false, erro: (r.stderr || '').slice(-300) || 'PDF não saiu' };
|
|
109
109
|
}
|
|
@@ -180,7 +180,7 @@ function htmlParaPngs(htmlPath, baseSemExt, n, w, h, { escala = 1, navegador = a
|
|
|
180
180
|
const tmp = `${baseSemExt}.tmp-${i}.html`;
|
|
181
181
|
const alvo = `${baseSemExt}-${String(i).padStart(2, '0')}.png`;
|
|
182
182
|
fs.writeFileSync(tmp, html.replace('</head>', `<style>section:not(:nth-of-type(${i})){display:none!important}html,body{height:${h}px;overflow:hidden}</style></head>`));
|
|
183
|
-
spawnSync(navegador, ['--headless=new', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
|
|
183
|
+
spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
|
|
184
184
|
`--window-size=${w},${h}`, `--screenshot=${alvo}`, 'file://' + tmp], { encoding: 'utf8', timeout: 90000 });
|
|
185
185
|
try { fs.unlinkSync(tmp); } catch {}
|
|
186
186
|
if (fs.existsSync(alvo)) arquivos.push(alvo); else erro = `PNG ${i} não saiu`;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// estudio_criar: a porta de entrada simples do Estúdio (docs/estrutura.md, seção 8).
|
|
2
|
+
// Quatro parâmetros fixos (tipo, briefing, duracao, formato); por dentro escolhe a receita,
|
|
3
|
+
// abre o projeto e chama as ferramentas que já existem (estudio_plano, estudio_motion,
|
|
4
|
+
// estudio_render). Não reescreve nada do Estúdio. As dependências entram por parâmetro,
|
|
5
|
+
// para o teste rodar sem rede e sem render.
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/* tipo → receita. `via`: 'roteiro' (plano pelo roteirista + render) ou 'motion' (a máquina de motion). */
|
|
11
|
+
const TIPOS = Object.freeze({
|
|
12
|
+
slides: { rotulo: 'Slides', via: 'roteiro', formato: '16:9', projeto: 'slides' },
|
|
13
|
+
video: { rotulo: 'Vídeo curto', via: 'roteiro', formato: '9:16', projeto: 'video', duracao: [15, 60, 30] },
|
|
14
|
+
motion: { rotulo: 'Motion', via: 'motion', formato: '9:16', duracao: [6, 60, 15] },
|
|
15
|
+
anuncio: { rotulo: 'Anúncio', via: 'motion', formato: '9:16', duracao: [10, 30, 15], prefixo: 'Anúncio para chamar pra comprar: ' },
|
|
16
|
+
abertura: { rotulo: 'Abertura de canal', via: 'motion', formato: '16:9', duracao: [5, 5, 5], prefixo: 'Abertura de canal com a marca: ' },
|
|
17
|
+
});
|
|
18
|
+
const FORMATOS = ['9:16', '16:9', '1:1'];
|
|
19
|
+
|
|
20
|
+
const limitar = (n, [min, max, padrao]) => { n = Number(n); return Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : padrao; };
|
|
21
|
+
|
|
22
|
+
/** Confere os parâmetros. Devolve { ok, tipo, briefing, duracao, formato } ou { ok:false, error }. */
|
|
23
|
+
function normalizar(args = {}) {
|
|
24
|
+
const tipo = String(args.tipo || '').toLowerCase().trim();
|
|
25
|
+
const receita = TIPOS[tipo];
|
|
26
|
+
if (!receita) return { ok: false, error: `tipo "${args.tipo || ''}" não existe. Use um de: ${Object.keys(TIPOS).join(', ')}.` };
|
|
27
|
+
const briefing = String(args.briefing || '').replace(/\s+/g, ' ').trim();
|
|
28
|
+
if (briefing.length < 3) return { ok: false, error: 'falta o briefing: diga em uma frase o que criar.' };
|
|
29
|
+
const formato = FORMATOS.includes(String(args.formato)) ? String(args.formato) : receita.formato;
|
|
30
|
+
const duracao = receita.duracao ? limitar(args.duracao, receita.duracao) : undefined;
|
|
31
|
+
return { ok: true, tipo, receita, briefing: briefing.slice(0, 600), formato, duracao };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Os arquivos entregues, com o tipo que a tela sabe abrir. */
|
|
35
|
+
function arquivosDe(r) {
|
|
36
|
+
const lista = [];
|
|
37
|
+
const por = (caminho, extra) => { if (caminho) lista.push({ nome: path.basename(caminho), caminho, tipo: tipoDoArquivo(caminho), ...extra }); };
|
|
38
|
+
por(r.arquivo || r.video);
|
|
39
|
+
if (r.entrega && r.entrega.pasta && Array.isArray(r.entrega.arquivos)) {
|
|
40
|
+
for (const n of r.entrega.arquivos) { const c = path.join(r.entrega.pasta, n); if (!lista.some((x) => x.caminho === c)) por(c); }
|
|
41
|
+
}
|
|
42
|
+
return lista;
|
|
43
|
+
}
|
|
44
|
+
function tipoDoArquivo(c) {
|
|
45
|
+
const e = path.extname(c).toLowerCase();
|
|
46
|
+
if (e === '.mp4' || e === '.webm' || e === '.mov') return 'video';
|
|
47
|
+
if (e === '.pdf') return 'pdf';
|
|
48
|
+
if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(e)) return 'imagem';
|
|
49
|
+
if (e === '.html') return 'pagina';
|
|
50
|
+
return 'arquivo';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {object} args { tipo, briefing, duracao?, formato? }
|
|
55
|
+
* @param {object} deps { projeto, plano(args), motion(args), render(args), progresso(pct, etapa) }
|
|
56
|
+
*/
|
|
57
|
+
async function criar(args, deps) {
|
|
58
|
+
const n = normalizar(args);
|
|
59
|
+
if (!n.ok) return n;
|
|
60
|
+
const { tipo, receita, briefing, formato, duracao } = n;
|
|
61
|
+
const progresso = deps.progresso || (() => {});
|
|
62
|
+
const falha = (error, extra) => ({ ok: false, error, ...extra });
|
|
63
|
+
|
|
64
|
+
progresso(5, 'Abrindo o projeto');
|
|
65
|
+
const pedido = `${receita.prefixo || ''}${briefing}`;
|
|
66
|
+
const titulo = briefing.slice(0, 60);
|
|
67
|
+
const proj = deps.projeto.criar({ titulo, pedido: `${pedido} (${receita.rotulo}, ${formato}${duracao ? `, ${duracao} s` : ''})`, formato: receita.projeto || 'video' });
|
|
68
|
+
if (!proj || proj.ok === false) return falha((proj && proj.error) || 'não consegui abrir o projeto.');
|
|
69
|
+
const id = proj.id;
|
|
70
|
+
const base = { tipo, id, pasta: proj.pasta, formato, duracao, titulo };
|
|
71
|
+
|
|
72
|
+
if (receita.via === 'motion') {
|
|
73
|
+
progresso(15, 'Montando as cenas de motion');
|
|
74
|
+
const m = await deps.motion({ id, assunto: pedido, formato, duracao });
|
|
75
|
+
if (!m || m.ok === false) return falha((m && m.error) || 'o motion não saiu.', base);
|
|
76
|
+
progresso(100, 'Pronto');
|
|
77
|
+
return { ok: true, ...base, duracao: m.duracao || duracao, arquivo: m.video, arquivos: arquivosDe(m), resumo: `${receita.rotulo} de ${m.duracao || duracao} s.` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
progresso(15, 'Escrevendo o roteiro');
|
|
81
|
+
const pl = await deps.plano({ id, pedido: `${pedido}. Formato ${formato}.`, segundos: duracao });
|
|
82
|
+
if (!pl || pl.ok === false) return falha((pl && pl.error) || 'não consegui escrever o plano.', base);
|
|
83
|
+
progresso(40, tipo === 'slides' ? 'Desenhando os slides' : 'Montando o vídeo');
|
|
84
|
+
const r = await deps.render({ id });
|
|
85
|
+
if (!r || r.ok === false) return falha((r && r.error) || 'o render não saiu.', { ...base, problemas: r && r.problemas });
|
|
86
|
+
progresso(100, 'Pronto');
|
|
87
|
+
return {
|
|
88
|
+
ok: true, ...base, arquivo: r.arquivo, arquivos: arquivosDe(r), paginas: r.paginas,
|
|
89
|
+
duracao: r.segundos || duracao,
|
|
90
|
+
resumo: r.paginas ? `${r.paginas} slides em PDF.` : `${receita.rotulo} de ${r.segundos || duracao} s.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { criar, normalizar, TIPOS, FORMATOS, tipoDoArquivo, arquivosDe };
|
|
95
|
+
|
|
96
|
+
/* Os "quadrados" do Studio no app viram `tipo` no turn/start. O motor traduz o tipo em instrução:
|
|
97
|
+
o modelo escolhe o caminho (a IA decide); aqui só o cardápio do tipo. Carrossel não tem receita no Estúdio
|
|
98
|
+
(conferido na fase 7), por isso não há quadrado para ele. */
|
|
99
|
+
const QUADRADOS = Object.freeze({
|
|
100
|
+
slides: 'Tipo escolhido na tela: Apresentação de slides. Use estudio_criar com tipo "slides" e o briefing do pedido (a skill slides-que-prendem ajuda no conteúdo).',
|
|
101
|
+
video: 'Tipo escolhido na tela: Vídeo curto vertical. Use estudio_criar com tipo "video" e o briefing do pedido.',
|
|
102
|
+
motion: 'Tipo escolhido na tela: Motion. Use estudio_criar com tipo "motion" e o briefing do pedido.',
|
|
103
|
+
anuncio: 'Tipo escolhido na tela: Anúncio de 15 s. Use estudio_criar com tipo "anuncio" e o briefing do pedido.',
|
|
104
|
+
abertura: 'Tipo escolhido na tela: Abertura de canal de 5 s. Use estudio_criar com tipo "abertura" e o briefing do pedido.',
|
|
105
|
+
pagina: 'Tipo escolhido na tela: Página ou app. Construa com write_file na pasta do projeto, confira abrindo no navegador e entregue o arquivo principal. Não use o Estúdio.',
|
|
106
|
+
bug: 'Tipo escolhido na tela: Corrigir um bug. Ache a causa lendo o código do projeto (read_file, grep), corrija com edit_file e crie um teste que pegue o caso; rode os testes e conte o que mudou. Não use o Estúdio.',
|
|
107
|
+
pr: 'Tipo escolhido na tela: Revisar um PR. Leia o diff do PR (o número ou o link que a pessoa der; gh ou git diff), aponte os riscos do mais grave ao menos grave e sugira a correção. Não use o Estúdio.',
|
|
108
|
+
app: 'Tipo escolhido na tela: Página ou app. Construa com write_file na pasta do projeto, confira abrindo no navegador e entregue o arquivo principal. Não use o Estúdio.',
|
|
109
|
+
});
|
|
110
|
+
const instrucaoDoTipo = (t) => QUADRADOS[String(t || '').toLowerCase()] || (t ? `Tipo de trabalho escolhido na tela: ${String(t).slice(0, 40)}` : '');
|
|
111
|
+
|
|
112
|
+
module.exports.QUADRADOS = QUADRADOS;
|
|
113
|
+
module.exports.instrucaoDoTipo = instrucaoDoTipo;
|
|
@@ -519,6 +519,9 @@ function abrirTela({ home, abrir, timeoutMs = 10 * 60 * 1000, porta = 0, inicio
|
|
|
519
519
|
const token = crypto.randomBytes(16).toString('hex');
|
|
520
520
|
let html;
|
|
521
521
|
try { html = fs.readFileSync(path.join(__dirname, 'gosto', 'tela.html'), 'utf8'); } catch { return resolve({ ok: false, motivo: 'sem_tela' }); }
|
|
522
|
+
// No app do PrimoCode a tela abre num quadro (iframe) e o "Finalizar" (window.close) não faz nada ali: avisa a janela de fora
|
|
523
|
+
// para fechar o quadro. Entra na hora de servir, porque o gosto/tela.html é guardado sem mudança (test/kits-tela.test.js confere o sha1).
|
|
524
|
+
html = html.replace('</body>', "<script>document.addEventListener('click',function(e){var b=e.target.closest&&e.target.closest('#finalizar');if(b&&window.parent!==window)window.parent.postMessage('primocode:tela-fim','*');},true);</script></body>");
|
|
522
525
|
// os vídeos que já estão no gosto: quem abre o /referencias de novo vê o que já trouxe (só o nome)
|
|
523
526
|
const jaTem = ((ler(home) || {}).referencias || []).map((r) => String(r.nome || 'vídeo').slice(0, 80));
|
|
524
527
|
const dados = JSON.stringify({ ESTILOS, GOSTEI, NAO_QUERO, INICIO: inicio === 'referencias' ? 'referencias' : 'carrossel', JA_TEM: jaTem }).replace(/</g, '\\u003c');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Histórico do Studio no app (studio/history): o que o Estúdio já gerou, com miniatura.
|
|
2
|
+
// Lê as pastas de projeto (lib/estudio/projeto.js) sem mudar nada. Node puro.
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { tipoDoArquivo } = require('./criar');
|
|
8
|
+
const Miniatura = require('./miniatura');
|
|
9
|
+
|
|
10
|
+
const PULAR = new Set(['node_modules', 'remotion', 'midia', 'entrada', 'versoes', '.git']);
|
|
11
|
+
const SAIDAS = new Set(['video', 'pdf', 'imagem']);
|
|
12
|
+
|
|
13
|
+
/* Procura as saídas em até 2 níveis; imagem solta (quadro de conferência) serve de miniatura, não de entrega. */
|
|
14
|
+
function varrer(pasta, nivel = 0, achados = []) {
|
|
15
|
+
let itens = [];
|
|
16
|
+
try { itens = fs.readdirSync(pasta, { withFileTypes: true }); } catch { return achados; }
|
|
17
|
+
for (const d of itens) {
|
|
18
|
+
const c = path.join(pasta, d.name);
|
|
19
|
+
if (d.isDirectory()) { if (nivel < 2 && !PULAR.has(d.name) && !d.name.startsWith('.')) varrer(c, nivel + 1, achados); continue; }
|
|
20
|
+
const tipo = tipoDoArquivo(c);
|
|
21
|
+
if (!SAIDAS.has(tipo)) continue;
|
|
22
|
+
let st; try { st = fs.statSync(c); } catch { continue; }
|
|
23
|
+
achados.push({ nome: d.name, caminho: c, tipo, bytes: st.size, quando: st.mtimeMs, dentro: path.relative(pasta, c) });
|
|
24
|
+
}
|
|
25
|
+
return achados;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** { itens, total }. `busca` filtra por título e pedido (sem acento); `limite` padrão 60. */
|
|
29
|
+
function historico({ busca = '', limite = 60, projeto, miniaturaDeVideo } = {}) {
|
|
30
|
+
// Vídeo na grade vira um JPG (cache ao lado do vídeo). Até 4 saem na hora; o resto sai em segundo plano e aparece na próxima abertura.
|
|
31
|
+
const quadro = miniaturaDeVideo || Miniatura.gerar;
|
|
32
|
+
let gerados = 0;
|
|
33
|
+
const P = projeto || require('./projeto');
|
|
34
|
+
const sem = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
35
|
+
const q = sem(busca).trim();
|
|
36
|
+
const itens = [];
|
|
37
|
+
for (const p of P.listar({ limite: 400 })) {
|
|
38
|
+
if (q && !sem(`${p.titulo} ${p.pedido}`).includes(q)) continue;
|
|
39
|
+
const todos = varrer(p.pasta).sort((a, b) => b.quando - a.quando);
|
|
40
|
+
const entregas = todos.filter((a) => a.tipo !== 'imagem' || /^saida\b/.test(a.dentro) || !todos.some((x) => x.tipo !== 'imagem'));
|
|
41
|
+
const principal = entregas.find((a) => a.tipo === 'video') || entregas.find((a) => a.tipo === 'pdf') || entregas[0] || null;
|
|
42
|
+
const imagem = todos.find((a) => a.tipo === 'imagem');
|
|
43
|
+
let miniatura = null;
|
|
44
|
+
if (principal && principal.tipo === 'imagem') miniatura = { tipo: 'imagem', caminho: principal.caminho };
|
|
45
|
+
else if (principal && principal.tipo === 'video') {
|
|
46
|
+
let jpg = Miniatura.emCache(principal.caminho);
|
|
47
|
+
if (!jpg && gerados < 4) { gerados++; jpg = quadro(principal.caminho); }
|
|
48
|
+
else if (!jpg && !miniaturaDeVideo) Miniatura.gerarDepois(principal.caminho);
|
|
49
|
+
miniatura = jpg ? { tipo: 'imagem', caminho: jpg, deVideo: true } : (imagem ? { tipo: 'imagem', caminho: imagem.caminho } : { tipo: 'video-sem-quadro', caminho: principal.caminho });
|
|
50
|
+
} else if (imagem) miniatura = { tipo: 'imagem', caminho: imagem.caminho };
|
|
51
|
+
itens.push({
|
|
52
|
+
id: p.id, titulo: p.titulo, formato: p.formato || 'video', criado: p.criado, mudado: p.mudado || p.criado,
|
|
53
|
+
pasta: p.pasta, arquivos: entregas.map(({ dentro, ...a }) => a), principal: principal ? principal.caminho : null, miniatura,
|
|
54
|
+
pronto: entregas.length > 0,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return { itens: itens.slice(0, Math.max(1, Number(limite) || 60)), total: itens.length };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { historico, varrer };
|
|
@@ -748,6 +748,19 @@ async function renderMotion(args, p, receita) {
|
|
|
748
748
|
return { ...r, ...(gostoResumo ? { gosto: gostoResumo } : {}), nota: 'Abra os quadros (png em ae/) para conferir. Se algo ficou torto, mude o JSON e rode de novo.' };
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
+
// ── estudio_criar ────────────────────────────────────────────────────────
|
|
752
|
+
|
|
753
|
+
/** A porta simples (criar.js): escolhe a receita e chama as ferramentas de cima. `ctx.progresso(pct, etapa)` alimenta o item da tela. */
|
|
754
|
+
async function ferramentaCriar(args, ctx = {}) {
|
|
755
|
+
return require('./criar.js').criar(args, {
|
|
756
|
+
projeto,
|
|
757
|
+
plano: (a) => ferramentaPlano({ ...a, __pasta: ctx.projectDir }),
|
|
758
|
+
motion: (a) => ferramentaMotion(a),
|
|
759
|
+
render: (a) => ferramentaRender({ ...a, __pasta: ctx.projectDir }, ctx),
|
|
760
|
+
progresso: ctx.progresso,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
751
764
|
// ── estudio_editar ───────────────────────────────────────────────────────
|
|
752
765
|
|
|
753
766
|
/** O editor por comando: reabre o plano da máquina de motion, aplica as mudanças e guarda a versão (editor.js). */
|
|
@@ -2706,7 +2719,7 @@ async function ferramentaSite(args) {
|
|
|
2706
2719
|
|
|
2707
2720
|
module.exports = { proporcaoDoPedido,
|
|
2708
2721
|
vazamentoNoComeco, variarIcones, objetosDoRoteiro,
|
|
2709
|
-
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaAE, ferramentaMotion, ferramentaEditar, ferramentaNarrar,
|
|
2722
|
+
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaAE, ferramentaMotion, ferramentaEditar, ferramentaNarrar, ferramentaCriar,
|
|
2710
2723
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
2711
2724
|
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|
|
2712
2725
|
};
|
|
@@ -380,9 +380,11 @@ function mandar(rota, corpo) {
|
|
|
380
380
|
}
|
|
381
381
|
function erro(msg) { const e = $('#erro'); e.hidden = !msg; e.textContent = msg || ''; }
|
|
382
382
|
|
|
383
|
+
/* No app do PrimoCode a tela abre dentro de um quadro (iframe): avisa a janela de fora que acabou, pra ela fechar o quadro. */
|
|
384
|
+
function avisarApp() { try { if (window.parent !== window) setTimeout(() => window.parent.postMessage('primocode:tela-fim', '*'), 1200); } catch { /* aberta solta no navegador */ } }
|
|
383
385
|
function mostrar(n) {
|
|
384
386
|
[1, 4].forEach((i) => { $('#passo' + i).hidden = i !== n; });
|
|
385
|
-
if (n === 4) { $('#titulo').textContent = 'Pronto. O PrimoCode já sabe o estilo.'; $('#sub').hidden = true; }
|
|
387
|
+
if (n === 4) { $('#titulo').textContent = 'Pronto. O PrimoCode já sabe o estilo.'; $('#sub').hidden = true; avisarApp(); }
|
|
386
388
|
window.scrollTo({ top: 0, behavior: parar ? 'auto' : 'smooth' });
|
|
387
389
|
}
|
|
388
390
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Miniatura de vídeo para a grade do Studio: um quadro em JPG, gerado pelo ffmpeg e guardado em cache ao lado
|
|
2
|
+
// do vídeo (<pasta do vídeo>/.miniaturas/<nome>.jpg). A tela mostra <img>, nunca <video> na grade: vídeo em formato
|
|
3
|
+
// que o Chromium não decodifica loga "Unsupported pixel format" só de abrir. Node puro, só `ffmpeg` do sistema.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const PASTA = '.miniaturas';
|
|
11
|
+
const LARGURA = 480;
|
|
12
|
+
const emAndamento = new Set();
|
|
13
|
+
|
|
14
|
+
const destinoDe = (video) => path.join(path.dirname(video), PASTA, `${path.basename(video)}.jpg`);
|
|
15
|
+
const argsDe = (video, saida) => ['-v', 'error', '-ss', '0.5', '-i', video, '-frames:v', '1', '-vf', `scale=${LARGURA}:-2`, '-q:v', '4', '-y', saida];
|
|
16
|
+
|
|
17
|
+
/* O cache vale enquanto o JPG existe, tem conteúdo e não é mais velho que o vídeo. */
|
|
18
|
+
function emCache(video) {
|
|
19
|
+
const jpg = destinoDe(video);
|
|
20
|
+
try {
|
|
21
|
+
const j = fs.statSync(jpg); const v = fs.statSync(video);
|
|
22
|
+
return j.size > 0 && j.mtimeMs >= v.mtimeMs ? jpg : null;
|
|
23
|
+
} catch { return null; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ffmpegExiste(ffmpeg = 'ffmpeg') {
|
|
27
|
+
try { return spawnSync(ffmpeg, ['-version'], { stdio: 'ignore', timeout: 4000 }).status === 0; } catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Gera agora (bloqueia até uns segundos). Devolve o caminho do JPG, ou null se não deu. */
|
|
31
|
+
function gerar(video, { ffmpeg = 'ffmpeg', espera = 8000 } = {}) {
|
|
32
|
+
const hit = emCache(video);
|
|
33
|
+
if (hit) return hit;
|
|
34
|
+
const jpg = destinoDe(video);
|
|
35
|
+
try { fs.mkdirSync(path.dirname(jpg), { recursive: true }); } catch { return null; }
|
|
36
|
+
try { spawnSync(ffmpeg, argsDe(video, jpg), { stdio: 'ignore', timeout: espera }); } catch { /* sem ffmpeg */ }
|
|
37
|
+
const ok = emCache(video);
|
|
38
|
+
if (!ok) { try { fs.rmSync(jpg, { force: true }); } catch { /* nada */ } }
|
|
39
|
+
return ok;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Gera sem bloquear (a próxima abertura do histórico já acha o JPG). Um por vídeo ao mesmo tempo. */
|
|
43
|
+
function gerarDepois(video, { ffmpeg = 'ffmpeg' } = {}) {
|
|
44
|
+
if (emAndamento.has(video) || emCache(video)) return;
|
|
45
|
+
const jpg = destinoDe(video);
|
|
46
|
+
try { fs.mkdirSync(path.dirname(jpg), { recursive: true }); } catch { return; }
|
|
47
|
+
emAndamento.add(video);
|
|
48
|
+
let p;
|
|
49
|
+
try { p = spawn(ffmpeg, argsDe(video, jpg), { stdio: 'ignore' }); } catch { emAndamento.delete(video); return; }
|
|
50
|
+
const fim = () => { emAndamento.delete(video); if (!emCache(video)) { try { fs.rmSync(jpg, { force: true }); } catch { /* nada */ } } };
|
|
51
|
+
p.on('error', fim); p.on('close', fim);
|
|
52
|
+
const t = setTimeout(() => { try { p.kill('SIGKILL'); } catch { /* já saiu */ } }, 20000);
|
|
53
|
+
if (t.unref) t.unref();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { gerar, gerarDepois, emCache, destinoDe, ffmpegExiste, PASTA };
|
package/lib/plugin/video/auto.js
CHANGED
|
@@ -301,7 +301,7 @@ const OPS = {
|
|
|
301
301
|
const ver = plano.passos.map((p) => ({ op: p.op, motivo: p.motivo, args: p.args }));
|
|
302
302
|
if (a.dryRun) return { ok: true, plano: ver, legenda: plano.legenda, segundos: 'da gravação' };
|
|
303
303
|
|
|
304
|
-
const
|
|
304
|
+
const TODAS = require('./catalogo.js').catalogo();
|
|
305
305
|
const feitos = [];
|
|
306
306
|
const roda = async (op, args, motivo) => {
|
|
307
307
|
const res = await TODAS[op].run(ctx, args);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalogo.js — o catálogo completo das operações. ops.js registra aqui ao
|
|
3
|
+
* carregar; auto.js e pedido.js leem daqui para encadear operações sem
|
|
4
|
+
* importar ops.js (que os importa: isso fechava um ciclo).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
let todas = null;
|
|
10
|
+
|
|
11
|
+
function registrar(ops) {
|
|
12
|
+
todas = ops;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function catalogo() {
|
|
16
|
+
if (!todas) throw new Error('catálogo de operações vazio: carregue lib/video/ops.js antes');
|
|
17
|
+
return todas;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { registrar, catalogo };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mutar.js — o caminho de toda operação que MUDA o projeto: foto de antes,
|
|
3
|
+
* edição, validação, gravação, delta. Se qualquer passo falha, nada é gravado.
|
|
4
|
+
*
|
|
5
|
+
* Mora fora de ops.js para os módulos de operação (ops-fx, zoom) usarem sem
|
|
6
|
+
* importar o catálogo que os importa.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const T = require('./timeline.js');
|
|
12
|
+
const { diff } = require('./delta.js');
|
|
13
|
+
const P = require('./projeto.js');
|
|
14
|
+
|
|
15
|
+
async function mutar(ctx, op, fn) {
|
|
16
|
+
const antes = T.clone(ctx.tl);
|
|
17
|
+
const extra = (await fn(ctx.tl)) || {};
|
|
18
|
+
T.ordenar(ctx.tl);
|
|
19
|
+
const probs = T.problemas(ctx.tl);
|
|
20
|
+
if (probs.length) throw T.erro(`edição recusada (nada mudou): ${probs.slice(0, 4).join('; ')}`);
|
|
21
|
+
P.fotografar(ctx.p, antes, op);
|
|
22
|
+
P.salvar(ctx.p, ctx.tl);
|
|
23
|
+
const d = diff(antes, ctx.tl, extra);
|
|
24
|
+
P.anotar(ctx.p, op, { dur: d.dur, added: (d.added || []).length, removed: (d.removed || []).length });
|
|
25
|
+
return d;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { mutar };
|
|
@@ -215,7 +215,7 @@ const OPS = {
|
|
|
215
215
|
ate = Math.min(asset.seconds, ate);
|
|
216
216
|
if (!(ate > de + 0.5)) throw T.erro('o momento 3D precisa de pelo menos meio segundo');
|
|
217
217
|
}
|
|
218
|
-
return require('./
|
|
218
|
+
return require('./mutar.js').mutar(ctx, 'separate_layers', (tl) => {
|
|
219
219
|
const pessoa = camadaDaPessoa(tl, asset, rs);
|
|
220
220
|
const out = { layers: { pessoa: pessoa.length, fundo: 'a gravação embaixo' } };
|
|
221
221
|
if (a.show3d !== undefined) { C.limparPapel(tl, 'camadas3d', asset.id); C.limparGerados(tl); }
|
|
@@ -261,7 +261,7 @@ const OPS = {
|
|
|
261
261
|
const seg = a.at === undefined ? await momento(ctx, asset, 'snap').catch(() => 0) : await momento(ctx, asset, a.at);
|
|
262
262
|
const medido = await r().medirAsset(ctx.tl, arquivo);
|
|
263
263
|
const ganho = ganhoDeCor(await corMedia(rs.arquivos.fundo), await corMedia(arquivo));
|
|
264
|
-
return require('./
|
|
264
|
+
return require('./mutar.js').mutar(ctx, 'replace_background', (tl) => {
|
|
265
265
|
if (!temPessoa(tl, asset.id)) camadaDaPessoa(tl, asset, rs);
|
|
266
266
|
const F = quadro({ tl }, asset, seg) ?? 0;
|
|
267
267
|
const fim = a.until !== undefined ? Math.round(Number(a.until)) : T.duracao(tl);
|
|
@@ -334,7 +334,7 @@ const OPS = {
|
|
|
334
334
|
if (de === null) de = t[0];
|
|
335
335
|
const ate = a.to !== undefined ? await momento(ctx, asset, a.to) : Math.min(asset.seconds, Math.max(t[1] + 0.3, de + 1.5));
|
|
336
336
|
if (!(ate > de)) throw T.erro('"to" antes de "from"');
|
|
337
|
-
return require('./
|
|
337
|
+
return require('./mutar.js').mutar(ctx, 'attach_3d', (tl) => {
|
|
338
338
|
const f = fxDe(tl, asset.id);
|
|
339
339
|
const o = { id: `o${f.objetos.length + 1}`, modelo, mao, de: r2(de * 1000) / 1000, ate: r2(ate * 1000) / 1000 };
|
|
340
340
|
if (a.color) o.cor = C.cor(a.color);
|
|
@@ -365,7 +365,7 @@ const OPS = {
|
|
|
365
365
|
}
|
|
366
366
|
const ateFusao = a.until !== undefined ? await momento(ctx, asset, a.until) : null;
|
|
367
367
|
const depois = a.then === 'none' || a.then === false ? null : C.modeloDe(a.then || 'brain') || 'cerebro';
|
|
368
|
-
return require('./
|
|
368
|
+
return require('./mutar.js').mutar(ctx, 'merge_on_join', (tl) => {
|
|
369
369
|
const f = fxDe(tl, asset.id);
|
|
370
370
|
// Os objetos vivos na junção ficam até ela (e se fundem).
|
|
371
371
|
for (const o of f.objetos) if (o.de < junta && o.ate < junta + 0.5) o.ate = r2((junta + 0.6) * 1000) / 1000;
|
|
@@ -400,7 +400,7 @@ const OPS = {
|
|
|
400
400
|
if (i1 > i0) ranges.push([T.origemParaQuadro(ctx.tl, c, i0), T.origemParaQuadro(ctx.tl, c, i1)]);
|
|
401
401
|
}
|
|
402
402
|
if (!ranges.length) return { ok: false, error: 'esse silêncio já foi cortado' };
|
|
403
|
-
return require('./
|
|
403
|
+
return require('./mutar.js').mutar(ctx, 'cut_silence_at', (tl) => ({ silence: { de: r2(de), ate: r2(ate) }, ...E.apagarTrechos(tl, ranges) }));
|
|
404
404
|
},
|
|
405
405
|
},
|
|
406
406
|
style_captions: {
|
|
@@ -423,7 +423,7 @@ const OPS = {
|
|
|
423
423
|
const CY = { bottom: 0.78, baixo: 0.78, top: 0.14, topo: 0.14, center: 0.5, centro: 0.5, middle: 0.5 };
|
|
424
424
|
if (pos && !seguir && CY[pos] === undefined) throw T.erro(`position "${a.position}" (use above_head, bottom, top ou center)`);
|
|
425
425
|
const corPedida = a.color !== undefined ? C.cor(a.color) : undefined;
|
|
426
|
-
return require('./
|
|
426
|
+
return require('./mutar.js').mutar(ctx, 'style_captions', (t) => {
|
|
427
427
|
for (const ref of alvo) {
|
|
428
428
|
const c = T.achar(t, ref.id).clip;
|
|
429
429
|
const st = { ...(c.text.style || {}) };
|
package/lib/plugin/video/ops.js
CHANGED
|
@@ -17,6 +17,7 @@ const T = require('./timeline.js');
|
|
|
17
17
|
const E = require('./edits.js');
|
|
18
18
|
const F = require('./filters.js');
|
|
19
19
|
const { diff } = require('./delta.js');
|
|
20
|
+
const { mutar } = require('./mutar.js');
|
|
20
21
|
const P = require('./projeto.js');
|
|
21
22
|
const TR = require('./transcript.js');
|
|
22
23
|
const A = require('./analise.js');
|
|
@@ -35,19 +36,6 @@ function alvos(tl, args, { tipos = ['video', 'image'] } = {}) {
|
|
|
35
36
|
return refs.map((ref) => T.achar(tl, ref).clip.id);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
async function mutar(ctx, op, fn) {
|
|
39
|
-
const antes = T.clone(ctx.tl);
|
|
40
|
-
const extra = (await fn(ctx.tl)) || {};
|
|
41
|
-
T.ordenar(ctx.tl);
|
|
42
|
-
const probs = T.problemas(ctx.tl);
|
|
43
|
-
if (probs.length) throw T.erro(`edição recusada (nada mudou): ${probs.slice(0, 4).join('; ')}`);
|
|
44
|
-
P.fotografar(ctx.p, antes, op);
|
|
45
|
-
P.salvar(ctx.p, ctx.tl);
|
|
46
|
-
const d = diff(antes, ctx.tl, extra);
|
|
47
|
-
P.anotar(ctx.p, op, { dur: d.dur, added: (d.added || []).length, removed: (d.removed || []).length });
|
|
48
|
-
return d;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
39
|
function precisaProjeto(ctx) {
|
|
52
40
|
if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."} (cria o projeto) ou new');
|
|
53
41
|
}
|
|
@@ -311,6 +299,7 @@ const PED = require('./pedido.js');
|
|
|
311
299
|
const AUTO = require('./auto.js');
|
|
312
300
|
const ZOOM = require('./zoom.js'); // o zoom que segue o cursor (gravação de tela): fica na ajuda principal
|
|
313
301
|
Object.assign(OPS, FXO.OPS, PED.OPS, AUTO.OPS, ZOOM.OPS);
|
|
302
|
+
require('./catalogo.js').registrar(OPS); // auto e pedido encadeiam operações por aqui
|
|
314
303
|
|
|
315
304
|
const ALIAS = { timeline: 'get_timeline', transcript: 'get_transcript', captions: 'add_captions', texts: 'add_texts', render: 'export', pedido: 'do', analyze: 'track' };
|
|
316
305
|
|
|
@@ -152,7 +152,7 @@ const OPS = {
|
|
|
152
152
|
const resumo = plano.passos.map((p) => ({ op: p.op, args: p.args }));
|
|
153
153
|
if (a.dryRun) return { ok: true, plano: resumo, ...(plano.avisos.length ? { avisos: plano.avisos } : {}) };
|
|
154
154
|
if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
|
|
155
|
-
const
|
|
155
|
+
const TODAS = require('./catalogo.js').catalogo();
|
|
156
156
|
// O que é lento e não depende de nada roda JUNTO antes do primeiro passo.
|
|
157
157
|
const C2 = require('./camadas.js');
|
|
158
158
|
const asset = C2.base(tl);
|
package/lib/plugin/video/zoom.js
CHANGED
|
@@ -231,7 +231,7 @@ const OPS = {
|
|
|
231
231
|
async run(ctx, a) {
|
|
232
232
|
const T = require('./timeline.js');
|
|
233
233
|
const E = require('./edits.js');
|
|
234
|
-
const { mutar } = require('./
|
|
234
|
+
const { mutar } = require('./mutar.js');
|
|
235
235
|
if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."}');
|
|
236
236
|
const primeiro = T.todosOsClipes(ctx.tl).find((x) => x.clip.type === 'video');
|
|
237
237
|
if (!a.id && !primeiro) throw T.erro('não há clipe de vídeo');
|