primocode 10.13.0 → 10.14.1
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 +66 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +56 -3
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/cota.js +157 -0
- package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
- package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
- package/lib/estudio/dados/projetos/clube-agent.json +120 -0
- package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
- package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
- package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
- package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
- package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
- package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
- package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
- package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
- package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
- package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
- package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
- package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
- package/lib/estudio/dados/projetos/clube-clube.json +108 -0
- package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
- package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
- package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
- package/lib/estudio/dados/projetos/clube-dub.json +119 -0
- package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
- package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
- package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
- package/lib/estudio/dados/projetos/clube-frame.json +108 -0
- package/lib/estudio/dados/projetos/clube-glass.json +118 -0
- package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
- package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
- package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
- package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
- package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
- package/lib/estudio/dados/projetos/clube-morph.json +119 -0
- package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
- package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
- package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
- package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
- package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
- package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
- package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
- package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
- package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
- package/lib/estudio/dados/projetos/clube-promo.json +114 -0
- package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
- package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
- package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
- package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
- package/lib/estudio/dados/projetos/clube-robo.json +125 -0
- package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
- package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
- package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
- package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
- package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
- package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
- package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
- package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
- package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
- package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
- package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
- package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
- package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
- package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
- package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
- package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
- package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
- package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
- package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
- package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
- package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
- package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
- package/lib/estudio/editor.js +290 -0
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/index.js +97 -2
- package/lib/estudio/kit-ae/base.jsx +1146 -0
- package/lib/estudio/kit-ae/cenas.jsx +395 -0
- package/lib/estudio/kit-ae/primitivas.jsx +911 -0
- package/lib/estudio/maquina-diretor.js +393 -0
- package/lib/estudio/maquina-elementos.js +134 -0
- package/lib/estudio/maquina-layout.js +198 -0
- package/lib/estudio/maquina-som.js +478 -0
- package/lib/estudio/maquina-trilha.js +160 -0
- package/lib/estudio/maquina-voz.js +429 -0
- package/lib/estudio/maquina.js +516 -0
- package/lib/estudio/projetos.js +72 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/limites.js +100 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +312 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +49 -19
- package/lib/preparar.js +111 -0
- package/lib/retomar.js +55 -0
- package/lib/sandbox.js +197 -0
- package/lib/skills.js +3 -1
- package/lib/tools.js +13 -4
- package/lib/ui.js +1 -1
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effectcraft.js — o motor After Effects do Estúdio.
|
|
3
|
+
*
|
|
4
|
+
* O EffectCraft (github.com/storytold/effectcraft, MIT/Apache-2.0) roda JSX
|
|
5
|
+
* (ExtendScript) no modelo de objetos do After Effects, sem o After Effects:
|
|
6
|
+
* `script` monta o projeto (.ecproj), `render` gera o mp4 e `render-frame` um
|
|
7
|
+
* quadro para a IA conferir. O PrimoCode baixa o binário uma vez, confere o
|
|
8
|
+
* sha256 e guarda em ~/.primocode/bin/effectcraft-<versão>/.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const VERSAO = '0.11.1';
|
|
20
|
+
const BASE = 'https://github.com/storytold/effectcraft/releases/download/v' + VERSAO + '/';
|
|
21
|
+
const SHA_MAC = '8eae2e1e9c13e03d1bc2460501a90d43fc570b37e9b386294c0720eb12aad0f0';
|
|
22
|
+
const PASTA_BIN = () => path.join(os.homedir(), '.primocode', 'bin', 'effectcraft-' + VERSAO);
|
|
23
|
+
|
|
24
|
+
/** O asset da release para esta máquina: { arquivo, sha (null = ler do SHA256SUMS.txt) }. */
|
|
25
|
+
function asset(plataforma = process.platform, arq = process.arch) {
|
|
26
|
+
if (plataforma === 'darwin') return { arquivo: `effectcraft-cli-${VERSAO}-macos-universal.zip`, sha: SHA_MAC };
|
|
27
|
+
if (plataforma === 'linux') return { arquivo: `effectcraft-${VERSAO}-linux-${arq === 'arm64' ? 'aarch64' : 'x86_64'}.tar.gz`, sha: null };
|
|
28
|
+
if (plataforma === 'win32') return { arquivo: `effectcraft-${VERSAO}-windows-${arq === 'arm64' ? 'arm64' : 'x64'}-portable.zip`, sha: null };
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function achar(dir, nome) {
|
|
33
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const p = path.join(dir, e.name);
|
|
35
|
+
if (e.isDirectory()) { const r = achar(p, nome); if (r) return r; }
|
|
36
|
+
else if (e.name === nome || e.name === nome + '.exe') return p;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function baixar(url) {
|
|
42
|
+
const r = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(180000) });
|
|
43
|
+
if (!r.ok) throw new Error('HTTP ' + r.status + ' em ' + url);
|
|
44
|
+
return Buffer.from(await r.arrayBuffer());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** O caminho do executável (baixa se faltar). `{ok, bin}` ou `{ok:false, error}`. */
|
|
48
|
+
async function garantir({ bin } = {}) {
|
|
49
|
+
const dado = bin || process.env.PRIMOCODE_EFFECTCRAFT_BIN;
|
|
50
|
+
if (dado) return fs.existsSync(dado) ? { ok: true, bin: dado } : { ok: false, error: 'binário do EffectCraft não existe: ' + dado };
|
|
51
|
+
const dir = PASTA_BIN();
|
|
52
|
+
const ja = fs.existsSync(dir) ? achar(dir, 'effectcraft-cli') : null;
|
|
53
|
+
if (ja) return { ok: true, bin: ja };
|
|
54
|
+
const a = asset();
|
|
55
|
+
if (!a) return { ok: false, error: 'o EffectCraft não tem binário para ' + process.platform };
|
|
56
|
+
try {
|
|
57
|
+
let sha = a.sha;
|
|
58
|
+
if (!sha) {
|
|
59
|
+
const sums = (await baixar(BASE + 'SHA256SUMS.txt')).toString('utf8');
|
|
60
|
+
const linha = sums.split('\n').find((l) => l.trim().endsWith(a.arquivo));
|
|
61
|
+
sha = linha && linha.trim().split(/\s+/)[0];
|
|
62
|
+
if (!sha) return { ok: false, error: 'SHA256SUMS.txt da release não lista ' + a.arquivo };
|
|
63
|
+
}
|
|
64
|
+
const buf = await baixar(BASE + a.arquivo);
|
|
65
|
+
const got = crypto.createHash('sha256').update(buf).digest('hex');
|
|
66
|
+
if (got !== sha) return { ok: false, error: `sha256 do EffectCraft não confere (esperado ${sha}, veio ${got})` };
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
const tmp = path.join(dir, a.arquivo);
|
|
69
|
+
fs.writeFileSync(tmp, buf);
|
|
70
|
+
const x = a.arquivo.endsWith('.zip')
|
|
71
|
+
? (process.platform === 'win32' ? spawnSync('tar', ['-xf', tmp, '-C', dir]) : spawnSync('unzip', ['-oq', tmp, '-d', dir]))
|
|
72
|
+
: spawnSync('tar', ['-xzf', tmp, '-C', dir]);
|
|
73
|
+
fs.rmSync(tmp, { force: true });
|
|
74
|
+
if (x.status !== 0) return { ok: false, error: 'não consegui extrair o EffectCraft: ' + String(x.stderr || x.error || '').slice(0, 300) };
|
|
75
|
+
const exe = achar(dir, 'effectcraft-cli');
|
|
76
|
+
if (!exe) return { ok: false, error: 'o executável effectcraft-cli não estava no pacote' };
|
|
77
|
+
if (process.platform !== 'win32') fs.chmodSync(exe, 0o755);
|
|
78
|
+
return { ok: true, bin: exe };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { ok: false, error: 'não consegui baixar o EffectCraft (sem rede?): ' + (e && e.message || e) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rodar(bin, args, env, timeout) {
|
|
85
|
+
const r = spawnSync(bin, args, { encoding: 'utf8', env, timeout, maxBuffer: 32 * 1024 * 1024 });
|
|
86
|
+
const saida = ((r.stdout || '') + (r.stderr || '')).trim();
|
|
87
|
+
if (r.error) return { ok: false, saida: (r.error.code === 'ETIMEDOUT' ? 'estourou o tempo' : r.error.message) + '\n' + saida };
|
|
88
|
+
return { ok: r.status === 0, saida };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Roda o JSX, renderiza o mp4 e os quadros de conferência.
|
|
93
|
+
* `fontes`: [{familia, peso}] baixadas do Google Fonts e passadas em EFFECTCRAFT_FONT_DIRS.
|
|
94
|
+
* `previa`: rascunho em meia resolução e qualidade draft (segundos, não minutos), para aprovar antes do final.
|
|
95
|
+
*/
|
|
96
|
+
async function renderizar({ pasta, script, comp, fontes = [], audio, conferir = [], bin, timeouts = {}, semVideo = false, previa = false } = {}) {
|
|
97
|
+
if (!pasta || !script || !comp) return { ok: false, error: 'faltou pasta, script ou comp' };
|
|
98
|
+
const g = await garantir({ bin });
|
|
99
|
+
if (!g.ok) return g;
|
|
100
|
+
const ae = path.join(pasta, 'ae');
|
|
101
|
+
fs.mkdirSync(ae, { recursive: true });
|
|
102
|
+
const jsx = path.join(ae, 'cena.jsx');
|
|
103
|
+
const projeto = path.join(ae, 'projeto.ecproj');
|
|
104
|
+
fs.writeFileSync(jsx, script);
|
|
105
|
+
|
|
106
|
+
const env = { ...process.env };
|
|
107
|
+
const dirs = [];
|
|
108
|
+
if (fontes && fontes.length) {
|
|
109
|
+
const glifos = require('./glifos.js');
|
|
110
|
+
for (const f of fontes) {
|
|
111
|
+
try { if (await glifos.baixarFonte(f.familia, f.peso || 400)) dirs.push(glifos.CACHE); } catch { /* sem rede: segue com a fonte do sistema */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (dirs.length) env.EFFECTCRAFT_FONT_DIRS = [...new Set(dirs)].join(path.delimiter);
|
|
115
|
+
|
|
116
|
+
const s = rodar(g.bin, ['script', jsx, '--empty', '--save-as', projeto], env, timeouts.script || 120000);
|
|
117
|
+
if (!s.ok || !fs.existsSync(projeto)) return { ok: false, error: 'o script JSX falhou:\n' + s.saida.slice(-1500) };
|
|
118
|
+
|
|
119
|
+
const bruto = path.join(pasta, audio ? 'video-ae-mudo.mp4' : 'video-ae.mp4');
|
|
120
|
+
if (semVideo) { // só a conferência: o script rodou (avisos, auditoria) e os quadros saem; o vídeo fica para a passada final
|
|
121
|
+
const quadros0 = [];
|
|
122
|
+
for (const t of conferir || []) {
|
|
123
|
+
const png = path.join(ae, `quadro-${t}.png`);
|
|
124
|
+
const q = rodar(g.bin, ['render-frame', '--project', projeto, '--comp', comp, '--time', String(t), '--out', png, '--max-side', '640'], env, timeouts.quadro || 60000);
|
|
125
|
+
if (q.ok && fs.existsSync(png)) quadros0.push(png);
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, video: null, quadros: quadros0, projeto, saida: s.saida, semVideo: true };
|
|
128
|
+
}
|
|
129
|
+
const r = rodar(g.bin, ['render', '--project', projeto, '--comp', comp, '--out', bruto, '--format', 'h264', ...(previa ? ['--resolution', 'half', '--quality', 'draft'] : [])], env, timeouts.render || (previa ? 180000 : 600000));
|
|
130
|
+
if (!r.ok || !fs.existsSync(bruto)) return { ok: false, error: 'o render falhou:\n' + r.saida.slice(-1500) };
|
|
131
|
+
|
|
132
|
+
const quadros = [];
|
|
133
|
+
for (const t of conferir || []) {
|
|
134
|
+
const png = path.join(ae, `quadro-${t}.png`);
|
|
135
|
+
const q = rodar(g.bin, ['render-frame', '--project', projeto, '--comp', comp, '--time', String(t), '--out', png, '--max-side', '640'], env, timeouts.quadro || 60000);
|
|
136
|
+
if (q.ok && fs.existsSync(png)) quadros.push(png);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let video = bruto;
|
|
140
|
+
const aud = audio ? path.resolve(pasta, audio) : null;
|
|
141
|
+
if (aud && fs.existsSync(aud)) {
|
|
142
|
+
const final = path.join(pasta, 'video-ae.mp4');
|
|
143
|
+
const m = rodar('ffmpeg', ['-y', '-i', bruto, '-i', aud, '-c:v', 'copy', '-c:a', 'aac', '-shortest', final], process.env, timeouts.audio || 120000);
|
|
144
|
+
if (m.ok && fs.existsSync(final)) { video = final; fs.rmSync(bruto, { force: true }); }
|
|
145
|
+
} else if (audio) {
|
|
146
|
+
fs.renameSync(bruto, path.join(pasta, 'video-ae.mp4'));
|
|
147
|
+
video = path.join(pasta, 'video-ae.mp4');
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, video, quadros, projeto, saida: s.saida };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { VERSAO, asset, garantir, renderizar };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* exportar.js — o PACOTE PRA POSTAR do estudio_motion, depois do render.
|
|
4
|
+
* exportar({video, pasta, assunto, formatos, cenas, palavras, trilha, sfx, assinatura}) → {ok, dir, arquivos}
|
|
5
|
+
* srt(palavras) → texto .srt (usa os mesmos pedaços da legenda do vídeo)
|
|
6
|
+
* postTxt({...}) → gancho + 3 linhas + CTA + 5 hashtags + créditos CC BY
|
|
7
|
+
* creditosDe(trilha, sfx) → linhas de atribuição (só licenças que exigem)
|
|
8
|
+
* galeria: registrar / listar / abrir / main(['abrir'|'pasta', n])
|
|
9
|
+
* Nunca estica: 9:16, 1:1 e 16:9 saem com o fundo desfocado e o vídeo centralizado.
|
|
10
|
+
*/
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const DIMS = { '9:16': [1080, 1920], '1:1': [1080, 1080], '16:9': [1920, 1080] };
|
|
17
|
+
const ARQ_GALERIA = () => process.env.PRIMOCODE_VIDEOS || path.join(os.homedir(), '.primocode', 'studio', 'videos.json');
|
|
18
|
+
const PARADAS = new Set('para como uma umas uns com que dos das nos nas por mais sobre entre seu sua isso esse essa este esta pelo pela muito tudo cada sem'.split(' '));
|
|
19
|
+
|
|
20
|
+
const ff = (args) => spawnSync('ffmpeg', ['-y', '-v', 'error', ...args], { encoding: 'utf8' });
|
|
21
|
+
function sondar(arq) {
|
|
22
|
+
const r = spawnSync('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height:format=duration', '-of', 'json', arq], { encoding: 'utf8' });
|
|
23
|
+
try { const j = JSON.parse(r.stdout); const s = j.streams[0]; return { w: s.width, h: s.height, dur: parseFloat(j.format.duration) }; } catch { return null; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── legenda .srt ─────────────────────────────────────────────────────────
|
|
27
|
+
const tempoSrt = (s) => {
|
|
28
|
+
const ms = Math.max(0, Math.round(s * 1000));
|
|
29
|
+
const p = (n, l = 2) => String(n).padStart(l, '0');
|
|
30
|
+
return `${p(Math.floor(ms / 3600000))}:${p(Math.floor(ms / 60000) % 60)}:${p(Math.floor(ms / 1000) % 60)},${p(ms % 1000, 3)}`;
|
|
31
|
+
};
|
|
32
|
+
function srt(palavras, { max = 3 } = {}) {
|
|
33
|
+
if (!Array.isArray(palavras) || !palavras.length) return '';
|
|
34
|
+
// agrupamento próprio (a legenda do vídeo junta frases inteiras; o .srt das redes quebra em trechos curtos de até `max` palavras)
|
|
35
|
+
const chunks = [];
|
|
36
|
+
palavras.forEach((p, i) => {
|
|
37
|
+
const prev = palavras[i - 1], atual = chunks[chunks.length - 1];
|
|
38
|
+
if (!atual || !prev || atual.palavras.length >= max || /[.!?;:]$/.test(prev.w) || p.t - prev.t2 > 0.25) chunks.push({ t: p.t, t2: p.t2, palavras: [p] });
|
|
39
|
+
else { atual.palavras.push(p); atual.t2 = p.t2; }
|
|
40
|
+
});
|
|
41
|
+
return chunks.map((c, i) => {
|
|
42
|
+
const fim = Math.max(c.t2, c.t + 0.4);
|
|
43
|
+
return `${i + 1}\n${tempoSrt(c.t)} --> ${tempoSrt(fim)}\n${c.palavras.map((p) => String(p.w).replace(/\*/g, '')).join(' ')}\n`;
|
|
44
|
+
}).join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── os créditos que exigem atribuição (CC BY, BY-SA...) ──────────────────
|
|
48
|
+
function fichaDe(arq) {
|
|
49
|
+
if (!arq) return null;
|
|
50
|
+
for (const f of [arq.replace(/\.[a-z0-9]+$/i, '') + '.licenca.json', arq + '.licenca.json']) {
|
|
51
|
+
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { /* próxima */ }
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const exigeCredito = (lic) => /^(cc[- ]?by|by)/i.test(String(lic || '').trim()) && !/^(cc0|zero)/i.test(String(lic || ''));
|
|
56
|
+
function creditosDe(trilha, sfx = []) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const fichaT = trilha && fichaDe(trilha.arquivo);
|
|
59
|
+
if (trilha && trilha.creditos && !(fichaT && !exigeCredito(fichaT.licenca) && !/by/i.test(String(trilha.creditos)))) out.push(`Trilha: ${typeof trilha.creditos === 'string' ? trilha.creditos : (trilha.titulo || '') + (trilha.autor ? ' — ' + trilha.autor : '')}`);
|
|
60
|
+
else if (fichaT && exigeCredito(fichaT.licenca)) out.push(`Trilha: "${fichaT.titulo}"${fichaT.autor ? ' — ' + fichaT.autor : ''} (${String(fichaT.licenca).toUpperCase()}${fichaT.origem ? ', ' + fichaT.origem : ''})`);
|
|
61
|
+
const vistos = new Set();
|
|
62
|
+
for (const arq of sfx) {
|
|
63
|
+
const f = fichaDe(arq);
|
|
64
|
+
if (!f || !exigeCredito(f.licenca)) continue;
|
|
65
|
+
const l = `SFX: "${f.titulo}"${f.autor ? ' — ' + f.autor : ''} (${String(f.licenca).toUpperCase()}${f.origem ? ', ' + f.origem : ''})`;
|
|
66
|
+
if (!vistos.has(l)) { vistos.add(l); out.push(l); }
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── o texto do post ──────────────────────────────────────────────────────
|
|
72
|
+
const limpo = (s) => String(s || '').replace(/\*/g, '').replace(/\s+/g, ' ').trim();
|
|
73
|
+
function hashtags(assunto, n = 5) {
|
|
74
|
+
const norm = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
75
|
+
const tags = [];
|
|
76
|
+
for (const w of limpo(assunto).split(/\s+/)) { const t = norm(w); if (t.length >= 4 && !PARADAS.has(t) && !tags.includes(t)) tags.push(t); }
|
|
77
|
+
for (const t of ['motion', 'video', 'reels', 'design', 'criatividade']) if (!tags.includes(t)) tags.push(t);
|
|
78
|
+
return tags.slice(0, n).map((t) => '#' + t);
|
|
79
|
+
}
|
|
80
|
+
function postTxt({ assunto, cenas = [], creditos = [] } = {}) {
|
|
81
|
+
const txt = (c) => limpo(c.titulo || c.texto || c.fala || '');
|
|
82
|
+
const gancho = cenas.find((c) => c.papel === 'gancho') || cenas[0] || {};
|
|
83
|
+
const cta = [...cenas].reverse().find((c) => c.papel === 'cta') || null;
|
|
84
|
+
const meio = cenas.filter((c) => c !== gancho && c !== cta).map(txt).filter(Boolean);
|
|
85
|
+
const linhas = meio.slice(0, 3);
|
|
86
|
+
while (linhas.length < 3) linhas.push(['Feito em motion, do roteiro ao corte.', 'Cada cena no tempo da batida.', 'Salve pra rever depois.'][linhas.length]);
|
|
87
|
+
const chamada = cta ? limpo(cta.botao || cta.titulo || cta.texto) : '';
|
|
88
|
+
const partes = [txt(gancho) || limpo(assunto), '', ...linhas.map((l) => '• ' + l), '', chamada ? `${chamada} 👇` : 'Curtiu? Salva e manda pra quem precisa.', '', hashtags(assunto).join(' ')];
|
|
89
|
+
if (creditos.length) partes.push('', 'Créditos:', ...creditos);
|
|
90
|
+
return partes.join('\n') + '\n';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── as versões de formato (fundo desfocado + vídeo centralizado) ─────────
|
|
94
|
+
function recompor(video, saida, [W, H]) {
|
|
95
|
+
const vf = `split[a][b];[a]scale=${W}:${H}:force_original_aspect_ratio=increase,crop=${W}:${H},boxblur=30:5[bg];[b]scale=${W}:${H}:force_original_aspect_ratio=decrease[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2,setsar=1,format=yuv420p`;
|
|
96
|
+
const r = ff(['-i', video, '-vf', vf, '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-c:a', 'copy', '-movflags', '+faststart', saida]);
|
|
97
|
+
return r.status === 0 && fs.existsSync(saida);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── a capa: o quadro de maior contraste, com título (meio de cada cena) ──
|
|
101
|
+
function capa(video, saida, { cenas = [], dur } = {}) {
|
|
102
|
+
const layout = require('./maquina-layout.js');
|
|
103
|
+
const total = dur || (sondar(video) || {}).dur || 0;
|
|
104
|
+
let t = 0;
|
|
105
|
+
const tempos = cenas.length ? cenas.map((c) => { const x = t + (c.dur || 1) * 0.6; t += c.dur || 1; return x; }) : [0.2, 0.4, 0.6, 0.8].map((k) => k * total);
|
|
106
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'capa-'));
|
|
107
|
+
let melhor = null;
|
|
108
|
+
try {
|
|
109
|
+
tempos.filter((x) => x < total).forEach((x, i) => {
|
|
110
|
+
const png = path.join(tmp, `q${i}.png`);
|
|
111
|
+
if (ff(['-ss', x.toFixed(2), '-i', video, '-frames:v', '1', png]).status !== 0 || !fs.existsSync(png)) return;
|
|
112
|
+
let m; try { m = layout.medirQuadro(png); } catch { m = null; }
|
|
113
|
+
if (!m) return;
|
|
114
|
+
const pontos = m.contraste + Math.min(m.variancia, 60) / 30 - (i === 0 ? 0.3 : 0); // título legível + algo na tela
|
|
115
|
+
if (!melhor || pontos > melhor.pontos) melhor = { png, pontos, t: x };
|
|
116
|
+
});
|
|
117
|
+
if (!melhor) return null;
|
|
118
|
+
const r = ff(['-i', melhor.png, '-vf', 'scale=1080:-2', '-q:v', '2', saida]);
|
|
119
|
+
return r.status === 0 && fs.existsSync(saida) ? { arquivo: saida, t: melhor.t } : null;
|
|
120
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── o pacote ─────────────────────────────────────────────────────────────
|
|
124
|
+
function exportar(o = {}) {
|
|
125
|
+
const { video, pasta, assunto = '', formatos = [], cenas = [], palavras = [], trilha = null, sfx = [] } = o;
|
|
126
|
+
if (!video || !fs.existsSync(video)) return { ok: false, error: 'sem vídeo para exportar.' };
|
|
127
|
+
const dir = path.join(pasta || path.dirname(video), 'entrega');
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
const arquivos = [], avisos = [];
|
|
130
|
+
const final = path.join(dir, 'video.mp4');
|
|
131
|
+
fs.copyFileSync(video, final); arquivos.push(final);
|
|
132
|
+
const info = sondar(video);
|
|
133
|
+
for (const f of [...new Set(formatos || [])]) {
|
|
134
|
+
if (!DIMS[f]) { avisos.push(`formato desconhecido: ${f}`); continue; }
|
|
135
|
+
const s = path.join(dir, `video-${f.replace(':', 'x')}.mp4`);
|
|
136
|
+
if (recompor(video, s, DIMS[f])) arquivos.push(s); else avisos.push(`não consegui gerar o ${f}`);
|
|
137
|
+
}
|
|
138
|
+
const legenda = srt(palavras);
|
|
139
|
+
if (legenda) { const s = path.join(dir, 'legenda.srt'); fs.writeFileSync(s, legenda); arquivos.push(s); }
|
|
140
|
+
const c = capa(video, path.join(dir, 'capa.jpg'), { cenas, dur: info && info.dur });
|
|
141
|
+
if (c) arquivos.push(c.arquivo); else avisos.push('a capa não saiu');
|
|
142
|
+
const creditos = creditosDe(trilha, sfx);
|
|
143
|
+
const post = path.join(dir, 'post.txt');
|
|
144
|
+
fs.writeFileSync(post, postTxt({ assunto, cenas, creditos })); arquivos.push(post);
|
|
145
|
+
return { ok: true, dir, arquivos, creditos, ...(avisos.length ? { avisos } : {}) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── a galeria: ~/.primocode/studio/videos.json ───────────────────────────
|
|
149
|
+
function ler(arq = ARQ_GALERIA()) { try { const j = JSON.parse(fs.readFileSync(arq, 'utf8')); return Array.isArray(j) ? j : []; } catch { return []; } }
|
|
150
|
+
function registrar({ id, assunto, caminho, formato, duracao, assinatura, entrega }, arq = ARQ_GALERIA()) {
|
|
151
|
+
const l = ler(arq).filter((v) => v.caminho !== caminho);
|
|
152
|
+
const item = { id: id || path.basename(path.dirname(caminho)), assunto: limpo(assunto), data: new Date().toISOString(), caminho, formato, duracao, assinatura: assinatura || null, entrega: entrega || null };
|
|
153
|
+
l.push(item);
|
|
154
|
+
try { fs.mkdirSync(path.dirname(arq), { recursive: true }); fs.writeFileSync(arq, JSON.stringify(l.slice(-200), null, 2)); } catch { /* sem disco: segue */ }
|
|
155
|
+
return item;
|
|
156
|
+
}
|
|
157
|
+
const listar = (arq) => ler(arq).filter((v) => v.caminho && fs.existsSync(v.caminho)).sort((a, b) => String(b.data).localeCompare(String(a.data)));
|
|
158
|
+
function abrirNoSistema(alvo) {
|
|
159
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
160
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '""', alvo] : [alvo];
|
|
161
|
+
return spawnSync(cmd, args, { stdio: 'ignore' }).status === 0;
|
|
162
|
+
}
|
|
163
|
+
const mmss = (s) => `${Math.floor(s / 60)}:${String(Math.round(s % 60)).padStart(2, '0')}`;
|
|
164
|
+
function formatarLista(l) {
|
|
165
|
+
if (!l.length) return ' Nenhum vídeo ainda. Peça um ao estúdio de motion.';
|
|
166
|
+
return ['', ...l.map((v, i) => ` ${String(i + 1).padStart(2)}. ${limpo(v.assunto).slice(0, 44).padEnd(44)} ${String(v.formato || '').padEnd(5)} ${mmss(v.duracao || 0).padStart(5)} ${String(v.data).slice(0, 10)}`), '', ' primocode videos abrir <n> · primocode videos pasta <n>'].join('\n');
|
|
167
|
+
}
|
|
168
|
+
/** `primocode videos [abrir|pasta <n>]` e /videos. Devolve o texto; `abrir` é injetável nos testes. */
|
|
169
|
+
function main(args = [], { arq, abrir = abrirNoSistema } = {}) {
|
|
170
|
+
const l = listar(arq);
|
|
171
|
+
const [acao, n] = args;
|
|
172
|
+
if (acao === 'abrir' || acao === 'pasta') {
|
|
173
|
+
const v = l[parseInt(n, 10) - 1];
|
|
174
|
+
if (!v) return { ok: false, texto: ` Não achei o vídeo ${n || ''}. Veja a lista com: primocode videos` };
|
|
175
|
+
const alvo = acao === 'abrir' ? v.caminho : (v.entrega || path.dirname(v.caminho));
|
|
176
|
+
return { ok: abrir(alvo), alvo, texto: ` Abrindo ${alvo}` };
|
|
177
|
+
}
|
|
178
|
+
return { ok: true, texto: formatarLista(l) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { exportar, srt, postTxt, creditosDe, hashtags, capa, recompor, registrar, listar, ler, main, formatarLista, DIMS };
|
package/lib/estudio/index.js
CHANGED
|
@@ -48,6 +48,7 @@ const video = require('./video.js');
|
|
|
48
48
|
const batida = require('./batida.js');
|
|
49
49
|
const revisor = require('./revisor.js');
|
|
50
50
|
const marcaLib = require('./marca.js');
|
|
51
|
+
const marcaKit = require('../marca-kit.js');
|
|
51
52
|
const conferencia = require('./conferencia.js');
|
|
52
53
|
const margem = require('./margem.js');
|
|
53
54
|
const assinatura = require('./assinatura.js');
|
|
@@ -362,7 +363,7 @@ async function roteiroDoServidor(p, args) {
|
|
|
362
363
|
estilo: args.estilo || (escolherEstilo({ pedido }) === 'impacto' ? 'impacto' : undefined),
|
|
363
364
|
}, cfg.token);
|
|
364
365
|
if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'o roteirista não respondeu.' };
|
|
365
|
-
return { ok: true, roteiro: { ...r.roteiro, marca: (args.roteiro && args.roteiro.marca) || p.marca || undefined } };
|
|
366
|
+
return { ok: true, roteiro: { ...r.roteiro, marca: marcaKit.preencher((args.roteiro && args.roteiro.marca) || p.marca || undefined, { pastas: [p.pasta, process.cwd()] }) } };
|
|
366
367
|
}
|
|
367
368
|
|
|
368
369
|
async function ferramentaPlano(args) {
|
|
@@ -506,6 +507,9 @@ async function ferramentaMidia(args) {
|
|
|
506
507
|
const p = args.id ? projeto.ler(args.id) : null;
|
|
507
508
|
if (args.id && !p) return semProjeto(args.id);
|
|
508
509
|
|
|
510
|
+
if (acao === 'projetos') return require('./projetos.js').procurar({ termo: args.termo, categoria: args.categoria, limite: args.limite });
|
|
511
|
+
if (acao === 'projeto') return require('./projetos.js').ver(args.nome || args.id_item || args.termo, { c: await require('../limites.js').situacao() });
|
|
512
|
+
|
|
509
513
|
if (acao === 'fontes') {
|
|
510
514
|
if (args.conferir) return assets.conferirFonte(args.conferir);
|
|
511
515
|
return { ok: true, fontes: assets.FONTES, biblioteca: biblioteca.cardapio(),
|
|
@@ -663,6 +667,88 @@ async function baixarEscolhido(lista, args, p) {
|
|
|
663
667
|
comoUsar: r.ok ? `no plano: "src": "${r.relativo}"` : undefined };
|
|
664
668
|
}
|
|
665
669
|
|
|
670
|
+
// ── estudio_ae ───────────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
/** O motor After Effects: roda o JSX do agente na pasta do projeto e devolve o mp4 e os quadros. */
|
|
673
|
+
async function ferramentaAE(args) {
|
|
674
|
+
const p = args.id ? projeto.ler(args.id) : null;
|
|
675
|
+
if (!args.id || !p) return semProjeto(args.id);
|
|
676
|
+
if (!args.script || !args.comp) return { ok: false, error: 'estudio_ae pede script (o JSX) e comp (o nome da composição).' };
|
|
677
|
+
return require('./cota.js').comCota(() => renderAE(args, p));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function renderAE(args, p) {
|
|
681
|
+
const kitM = marcaKit.preencher(null, { pastas: [p.pasta, process.cwd()] });
|
|
682
|
+
const fontesKit = kitM && kitM.kit ? [kitM.fontes && kitM.fontes.titulo, kitM.fontes && kitM.fontes.texto].filter(Boolean) : [];
|
|
683
|
+
const r = await require('./effectcraft.js').renderizar({
|
|
684
|
+
pasta: p.pasta, script: String(args.script), comp: String(args.comp),
|
|
685
|
+
fontes: [...new Set([...(Array.isArray(args.fontes) ? args.fontes : []), ...fontesKit])], audio: args.audio || undefined,
|
|
686
|
+
conferir: Array.isArray(args.conferir) ? args.conferir : [],
|
|
687
|
+
});
|
|
688
|
+
if (!r.ok) return r;
|
|
689
|
+
const daMarca = kitM && kitM.kit ? { marca: { nome: kitM.nome, cores: kitM.cores, fontes: kitM.fontes, logo: kitM.logo } } : {};
|
|
690
|
+
return { ...r, ...daMarca, nota: 'Olhe os quadros (arquivos em ae/) antes de dar por pronto; ajuste o JSX e rode de novo se algo estiver torto.' };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ── estudio_motion ───────────────────────────────────────────────────────
|
|
694
|
+
|
|
695
|
+
/** O pacote pra postar (exportar.js) + a galeria; nunca derruba o vídeo. `entrega:false` desliga. */
|
|
696
|
+
function entregar(args, p, r) {
|
|
697
|
+
const x = r._exportar || {}; delete r._exportar;
|
|
698
|
+
if (args.entrega === false) return;
|
|
699
|
+
try {
|
|
700
|
+
const E = require('./exportar.js');
|
|
701
|
+
const e = E.exportar({ video: r.video, pasta: p.pasta, assunto: args.assunto, formatos: args.formatos, cenas: x.cenas, palavras: x.palavras, trilha: r.trilha && { ...r.trilha, arquivo: x.trilhaArquivo }, sfx: x.sfx });
|
|
702
|
+
if (e.ok) {
|
|
703
|
+
r.entrega = { pasta: e.dir, arquivos: e.arquivos.map((f) => path.basename(f)), creditos: e.creditos, ...(e.avisos ? { avisos: e.avisos } : {}) };
|
|
704
|
+
E.registrar({ id: p.id, assunto: args.assunto, caminho: r.video, formato: r.formato, duracao: r.duracao, assinatura: r.assinatura, entrega: e.dir });
|
|
705
|
+
} else r.entrega = { erro: e.error };
|
|
706
|
+
} catch (err) { r.entrega = { erro: err.message }; }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** A máquina de motion: assunto + cenas em JSON viram o mp4 (maquina.js), com sons, trilha e voz opcionais. */
|
|
710
|
+
async function ferramentaMotion(args) {
|
|
711
|
+
const p = args.id ? projeto.ler(args.id) : null;
|
|
712
|
+
if (!args.id || !p) return semProjeto(args.id);
|
|
713
|
+
let receita = null;
|
|
714
|
+
if (args.receita) {
|
|
715
|
+
const r = require('./projetos.js').ver(args.receita, { c: await require('../limites.js').situacao() });
|
|
716
|
+
if (!r.ok) return r;
|
|
717
|
+
// Grátis vê o resumo da receita, não monta a partir dela: diz o porquê em vez de renderizar pela metade.
|
|
718
|
+
if (r.bloqueado) return { ok: false, error: r.aviso, projeto: r.projeto };
|
|
719
|
+
receita = r.projeto;
|
|
720
|
+
}
|
|
721
|
+
return require('./cota.js').comCota(() => renderMotion(args, p, receita));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function renderMotion(args, p, receita) {
|
|
725
|
+
const narrar = async (texto, mp3, voz) => rodarPython('narrar.py', ['falar', '--texto', texto, '--saida', mp3, '--voz', String(voz || 'francisca')], { timeout: 5 * 60000 });
|
|
726
|
+
// Kit de marca ativo: cores, fontes e logo entram onde o JSON não disse; o logo vai no CTA (última cena).
|
|
727
|
+
const kitM = marcaKit.preencher(args.marca, { pastas: [p.pasta, process.cwd()] });
|
|
728
|
+
if (kitM && kitM.kit) {
|
|
729
|
+
args = { ...args, marca: kitM };
|
|
730
|
+
if (kitM.logo && Array.isArray(args.cenas) && args.cenas.length) {
|
|
731
|
+
args.cenas = args.cenas.map((c, i) => (i === args.cenas.length - 1 && c && !c.logo ? { ...c, logo: kitM.logo } : c));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const r = await require('./maquina.js').motion(args, { pasta: p.pasta, receita, deps: { narrar } });
|
|
735
|
+
if (r.ok) projeto.anotar(p.id, 'motion', `${r.duracao}s, ${r.cenas.length} cenas, ${r.lufs} LUFS`);
|
|
736
|
+
if (r.ok) entregar(args, p, r);
|
|
737
|
+
return r.ok ? { ...r, nota: 'Abra os quadros (png em ae/) para conferir. Se algo ficou torto, mude o JSON e rode de novo.' } : r;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── estudio_editar ───────────────────────────────────────────────────────
|
|
741
|
+
|
|
742
|
+
/** O editor por comando: reabre o plano da máquina de motion, aplica as mudanças e guarda a versão (editor.js). */
|
|
743
|
+
async function ferramentaEditar(args) {
|
|
744
|
+
const id = args.id_video || args.id;
|
|
745
|
+
const p = id ? projeto.ler(id) : null;
|
|
746
|
+
if (!p) return semProjeto(id);
|
|
747
|
+
const r = await require('./editor.js').editar(args, { pasta: p.pasta });
|
|
748
|
+
if (r.ok && r.versao && !args.listar) projeto.anotar(p.id, 'editar', `v${r.versao}: ${(r.feitas || [r.nota || '']).join('; ').slice(0, 120)}`);
|
|
749
|
+
return r.ok && r.video ? { ...r, nota: r.nota || 'Mostre o vídeo. Se não era isso, mude de novo ou use voltar_para.' } : r;
|
|
750
|
+
}
|
|
751
|
+
|
|
666
752
|
// ── estudio_narrar ───────────────────────────────────────────────────────
|
|
667
753
|
|
|
668
754
|
async function ferramentaNarrar(args) {
|
|
@@ -1120,6 +1206,11 @@ function vazamentoNoComeco(lista, palavras, direcao = '') {
|
|
|
1120
1206
|
}
|
|
1121
1207
|
|
|
1122
1208
|
async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
1209
|
+
/* A narração premium é do Pro em diante (lib/limites.js). Sem ela a corrente
|
|
1210
|
+
segue para a voz local (Kokoro/Edge) e o aviso explica o porquê. */
|
|
1211
|
+
const lim = require('../limites.js');
|
|
1212
|
+
const prem = await lim.exigir('narracaoPremium');
|
|
1213
|
+
if (!prem.ok) { lim.avisar(prem.error + ' A narração saiu com a voz local.'); return { ok: false, error: prem.error }; }
|
|
1123
1214
|
try {
|
|
1124
1215
|
const cfg = require('../config').loadConfig();
|
|
1125
1216
|
const api = require('../api');
|
|
@@ -1490,6 +1581,10 @@ async function prepararRoteiro(p, args) {
|
|
|
1490
1581
|
const novas = referencias.imagens().filter((f) => !jaTem.has(f));
|
|
1491
1582
|
for (const f of novas) { try { importarReferencia(p, f); } catch (e) { projeto.anotar(p.id, 'referencia', `não importou ${path.basename(f)}: ${e.message}`); } }
|
|
1492
1583
|
if (novas.length) p = projeto.ler(p.id) || p;
|
|
1584
|
+
/* O KIT DE MARCA ativo (lib/marca-kit.js) vale mais que a marca lida do
|
|
1585
|
+
site: a pessoa criou uma vez e quer em todo vídeo. O roteiro ainda ganha. */
|
|
1586
|
+
const kitM = marcaKit.preencher({}, { pastas: [p.pasta, process.cwd()] });
|
|
1587
|
+
if (kitM.kit) { p = { ...p, marca: kitM }; }
|
|
1493
1588
|
if (roteiro.marca) {
|
|
1494
1589
|
/* A do roteiro por cima da do site, CAMPO A CAMPO: antes o "cores" em
|
|
1495
1590
|
lista do agente apagava o {fundo, destaque} lido do site (13/10). */
|
|
@@ -2600,7 +2695,7 @@ async function ferramentaSite(args) {
|
|
|
2600
2695
|
|
|
2601
2696
|
module.exports = { proporcaoDoPedido,
|
|
2602
2697
|
vazamentoNoComeco, variarIcones, objetosDoRoteiro,
|
|
2603
|
-
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
2698
|
+
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaAE, ferramentaMotion, ferramentaEditar, ferramentaNarrar,
|
|
2604
2699
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
2605
2700
|
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|
|
2606
2701
|
};
|