primocode 10.12.0 → 10.14.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 +74 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +48 -1
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/assinaturas/gerativo.json +1 -1
- package/lib/estudio/camadas.js +4 -0
- package/lib/estudio/cota.js +150 -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/diretor.js +2 -1
- package/lib/estudio/editor.js +285 -0
- package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/gerativo.js +44 -15
- package/lib/estudio/index.js +126 -5
- package/lib/estudio/kit/gerativo.jsx +15 -0
- 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/kokoro.js +3 -1
- 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 +60 -0
- package/lib/estudio/remotion.js +6 -4
- package/lib/estudio/revisor.js +61 -0
- package/lib/estudio/voicestudio.js +81 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +309 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +47 -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/lib/video/auto.js +18 -3
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
- package/skills/motion-de-primeira/SKILL.md +33 -51
|
@@ -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/gerativo.js
CHANGED
|
@@ -510,7 +510,9 @@ function tituloDaFala(fala) {
|
|
|
510
510
|
|
|
511
511
|
function conteudoDe(p, marca = {}) {
|
|
512
512
|
const titulo = p.titulo || p.texto || tituloDaFala(p.fala);
|
|
513
|
-
|
|
513
|
+
// O item que repete o título ("Sem troco" / "sem troco", 15/10) sai: o quadro dizia a mesma coisa duas vezes.
|
|
514
|
+
const igual = (a, b) => tokens(a).join(' ') === tokens(b).join(' ');
|
|
515
|
+
const itens = itensDe(p).filter((it) => !igual(it, titulo));
|
|
514
516
|
if (p.tipo === 'numero' && numeroDe(p.valor)) return { tipo: 'numero', valor: String(p.valor), rotulo: p.rotulo || titulo, sub: p.sub || '' };
|
|
515
517
|
if (itens.length >= 2) return { tipo: 'lista', titulo: p.tipo === 'cores' ? titulo : titulo, itens: itens.slice(0, 5), numerada: p.tipo === 'passos' };
|
|
516
518
|
if (p.tipo === 'contraste' && (p.nao || p.sim)) return { tipo: 'contraste', nao: p.nao || '', sim: p.sim || titulo };
|
|
@@ -528,7 +530,7 @@ const textoDoConteudo = (c) => [c.titulo, c.sub, c.valor, c.rotulo, c.nao, c.sim
|
|
|
528
530
|
const COMPOSICOES = ['gigante', 'pilha', 'divisao', 'contador', 'cascata', 'parede', 'citacao', 'marca', 'palavras', 'produto'];
|
|
529
531
|
const COMPORTAMENTOS = ['carimbo', 'mascara', 'onda', 'decodifica', 'peso', 'largura', 'tracking', 'desfoque', 'flap', 'pontos', 'contorno', 'monta'];
|
|
530
532
|
const SAIDAS = ['pontos', 'evapora', 'atravessa', 'desliza', 'comprime', 'explode', 'chicote', 'inunda', 'corte'];
|
|
531
|
-
const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta'];
|
|
533
|
+
const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta', 'visto'];
|
|
532
534
|
|
|
533
535
|
function pesosDe(c, { curto, palavras, primeira, ultima, tem3d, pergunta, exclama, numeroNoTitulo }) {
|
|
534
536
|
switch (c.tipo) {
|
|
@@ -1055,13 +1057,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1055
1057
|
const M = Math.round(W * (deitado ? 0.085 : 0.095));
|
|
1056
1058
|
const larguraUtil = W - 2 * M;
|
|
1057
1059
|
const base = Math.min(W, H) / 1080;
|
|
1058
|
-
|
|
1060
|
+
/* (15/10, as referências) O rótulo de 24–30 px (1,6% da altura no 9:16)
|
|
1061
|
+
não se lia no celular ("NO COADOR", "pra você, de graça"). O rótulo
|
|
1062
|
+
fica em ~2% da altura, e a segunda voz (o sub, a frase de apoio) em
|
|
1063
|
+
3–4%, como a anotação das referências. */
|
|
1064
|
+
const tamRotulo = Math.round(Math.max((deitado ? 24 : 30) * base, H * (deitado ? 0.026 : 0.02)));
|
|
1059
1065
|
const tem3d = !!roteiro.modelo3d;
|
|
1060
1066
|
const ws = temposDasPartes(partes, tempos);
|
|
1061
1067
|
|
|
1062
1068
|
// ── os limites de cada parte na linha do tempo ────────────────────────
|
|
1063
1069
|
const inicio = [];
|
|
1064
|
-
|
|
1070
|
+
// 0,9 s (eram 1,4): nas referências a cena dura 1 a 2 s; com fala rápida a parte segue a voz.
|
|
1071
|
+
partes.forEach((_, i) => inicio.push(i === 0 ? 0 : r3(Math.max(inicio[i - 1] + 0.9, ws[i].de - 0.12))));
|
|
1065
1072
|
const ultimaFala = ws[ws.length - 1];
|
|
1066
1073
|
const duracao = r1(Math.max(inicio[inicio.length - 1] + 2.0, (ultimaFala.ate || 0) + 1.15));
|
|
1067
1074
|
const fimParte = partes.map((_, i) => (i < partes.length - 1 ? inicio[i + 1] : duracao));
|
|
@@ -1544,14 +1551,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1544
1551
|
resto); no fim, cada gesto vira o seu par limpo. */
|
|
1545
1552
|
/* O substituto é um limpo que não aparece nas 3 batidas de cada lado: a
|
|
1546
1553
|
passagem e o gesto da tese não voltam antes de 3 batidas. */
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1554
|
+
/* (15/10, as referências) Os pontos, o evapora e o comprime também
|
|
1555
|
+
espalhavam as letras ("dad|o", "· p o n d e", parecia glitch). Nas
|
|
1556
|
+
referências a frase sai inteira: desliza, atravessa ou corta seco. */
|
|
1557
|
+
const TORTOS = ['decodifica', 'flap', 'onda', 'monta', 'pontos'];
|
|
1558
|
+
const GESTOS_LIMPOS = ['mascara', 'desfoque', 'tracking', 'contorno', 'carimbo'];
|
|
1559
|
+
const SAIDAS_LIMPAS = ['desliza', 'atravessa', 'corte'];
|
|
1560
|
+
const SAIDAS_SUJAS = ['explode', 'chicote', 'pontos', 'evapora', 'comprime'];
|
|
1550
1561
|
const vizinhas = (i) => batidas.filter((_, j) => j !== i && Math.abs(j - i) <= 3);
|
|
1551
1562
|
const principal = (b) => (b.linhas || []).find((l) => (l.papel === 'tese' || l.papel === 'numero') && !l.marca);
|
|
1552
1563
|
const comGesto = batidas.filter(principal);
|
|
1553
1564
|
batidas.forEach((b, i) => {
|
|
1554
|
-
if (
|
|
1565
|
+
if (SAIDAS_SUJAS.includes(b.saida)) {
|
|
1555
1566
|
const perto = vizinhas(i).map((x) => x.saida);
|
|
1556
1567
|
b.saida = SAIDAS_LIMPAS.find((x) => !perto.includes(x)) || 'corte';
|
|
1557
1568
|
if (batidas[i + 1]) batidas[i + 1].entrada = b.saida;
|
|
@@ -1626,6 +1637,11 @@ function molduraDe(roteiro, E, { W, H, deitado, marca, secoes }) {
|
|
|
1626
1637
|
const pesoDe = (fonte) => (VARIAVEIS[fonte] && VARIAVEIS[fonte].eixos.wght) || [400, 400];
|
|
1627
1638
|
|
|
1628
1639
|
// ── as composições: de conteúdo + contexto, uma batida ─────────────────
|
|
1640
|
+
/** A segunda voz (sub, a frase de apoio): 3–4% da altura, como a anotação das referências. */
|
|
1641
|
+
function tamSegunda({ H, deitado, tamRotulo }) {
|
|
1642
|
+
return Math.round(Math.max(tamRotulo, H * (deitado ? 0.042 : 0.03)));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1629
1645
|
function compor(comp, br, ctx) {
|
|
1630
1646
|
const { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes } = ctx;
|
|
1631
1647
|
const c = br.conteudo;
|
|
@@ -1739,6 +1755,8 @@ function compor(comp, br, ctx) {
|
|
|
1739
1755
|
else {
|
|
1740
1756
|
const lt = comTese || teses[teses.length - 1];
|
|
1741
1757
|
b.acento.push({ forma, alvo: { linha: lt.id, palavra: comTese ? lt.palavras.findIndex((p) => p.papel === 'tese') : null } });
|
|
1758
|
+
// O círculo de caneta na tese ganha o ✓ (15/10, as referências), sem gastar sorteio.
|
|
1759
|
+
if (forma === 'anel' && comTese) b.acento.push({ forma: 'visto', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') } });
|
|
1742
1760
|
}
|
|
1743
1761
|
}
|
|
1744
1762
|
// O ThreeCanvas só aceita pixel inteiro; ele ocupa a altura toda, para o chão não cortar.
|
|
@@ -1759,7 +1777,13 @@ function compor(comp, br, ctx) {
|
|
|
1759
1777
|
}
|
|
1760
1778
|
for (const t of fit.linhas) { const l = linha(t, 'apoio', { tamanho: fit.tamanho, eixos: { wght: 560 } }); misturarPesos(l, { maiorTese: 1.14 }); }
|
|
1761
1779
|
const lt = b.linhas.find((l) => l.palavras.some((p) => p.papel === 'tese'));
|
|
1762
|
-
if (lt) {
|
|
1780
|
+
if (lt) {
|
|
1781
|
+
tese(lt);
|
|
1782
|
+
const alvo = { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') };
|
|
1783
|
+
const forma = s.chance(0.6) ? 'sublinha' : 'anel';
|
|
1784
|
+
b.acento.push({ forma, alvo });
|
|
1785
|
+
if (forma === 'anel') b.acento.push({ forma: 'visto', alvo: { ...alvo } });
|
|
1786
|
+
}
|
|
1763
1787
|
else pontoFinal(b.linhas[b.linhas.length - 1]);
|
|
1764
1788
|
break;
|
|
1765
1789
|
}
|
|
@@ -1774,7 +1798,7 @@ function compor(comp, br, ctx) {
|
|
|
1774
1798
|
const l = linha(c.valor, 'numero', { fonte, tamanho: tam, eixos });
|
|
1775
1799
|
const tRot = Math.max(30 * base, Math.min(tam * 0.22, 64 * base));
|
|
1776
1800
|
if (c.rotulo) linha(c.rotulo, 'apoio', { tamanho: tRot, eixos: { wght: 600 }, fonte: fontes.display });
|
|
1777
|
-
if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho:
|
|
1801
|
+
if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho: tamSegunda(ctx), tracking: 0.02, eixos: { wght: 400 } });
|
|
1778
1802
|
b.contador = { linha: l.id, ...n, dur: 0.9 };
|
|
1779
1803
|
b.acento.push({ forma: c.valor.length <= 4 ? 'anel' : 'sublinha', alvo: { linha: l.id } });
|
|
1780
1804
|
break;
|
|
@@ -1844,7 +1868,7 @@ function compor(comp, br, ctx) {
|
|
|
1844
1868
|
const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.3 : 0.16) * H, { fonte, wght: 800 });
|
|
1845
1869
|
b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
|
|
1846
1870
|
linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, pesoDe(fonte)[1]) }, comportamento: 'contorno' }).marca = true;
|
|
1847
|
-
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho:
|
|
1871
|
+
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamSegunda(ctx), tracking: 0.06, eixos: { wght: 400 } });
|
|
1848
1872
|
b.acento.push({ forma: 'ponto', alvo: { linha: b.linhas[0].id, antes: true } });
|
|
1849
1873
|
break;
|
|
1850
1874
|
}
|
|
@@ -1952,9 +1976,10 @@ function tempoDaBatida(b, palavras, ctx) {
|
|
|
1952
1976
|
l.t = r3(Math.max(i === 0 ? b.de + 0.1 : t + REGRAS.stagger[0] * Q, falado != null ? falado : (i === 0 ? b.de + 0.12 : t + passo)));
|
|
1953
1977
|
t = l.t;
|
|
1954
1978
|
});
|
|
1955
|
-
/* Quadro morto não: a primeira coisa da batida entra em até 0,
|
|
1956
|
-
que a voz ainda não a tenha dito (o resto segue a voz).
|
|
1957
|
-
|
|
1979
|
+
/* Quadro morto não: a primeira coisa da batida entra em até 0,12 s, mesmo
|
|
1980
|
+
que a voz ainda não a tenha dito (o resto segue a voz). Eram 0,35 s, e
|
|
1981
|
+
com a saída da anterior o teste de 15/10 tinha 23% de quadro vazio. */
|
|
1982
|
+
if (ordem[0] && ordem[0].t > b.de + 0.12) ordem[0].t = r3(b.de + 0.12);
|
|
1958
1983
|
for (let i = 1; i < ordem.length; i++) if (ordem[i].t < ordem[i - 1].t + REGRAS.stagger[0] * Q) ordem[i].t = r3(ordem[i - 1].t + REGRAS.stagger[0] * Q);
|
|
1959
1984
|
// Nada entra tarde demais para ser lido: de trás para frente, com o stagger mínimo.
|
|
1960
1985
|
for (let i = ordem.length - 1; i >= 0; i--) {
|
|
@@ -1985,7 +2010,11 @@ function tempoDaBatida(b, palavras, ctx) {
|
|
|
1985
2010
|
let tw = l.t;
|
|
1986
2011
|
l.palavras.forEach((p, j) => {
|
|
1987
2012
|
const alvo = qs[j] != null ? qs[j] : (j === 0 ? l.t : tw + 2 * Q);
|
|
1988
|
-
|
|
2013
|
+
/* Quadro morto não, também na palavra (15/10): a voz pode dizer
|
|
2014
|
+
outra coisa antes ("Aponta a câmera, paga, e o pão…"); a primeira
|
|
2015
|
+
palavra da batida entra com a linha, as outras seguem a voz. */
|
|
2016
|
+
const abre = j === 0 && l === ordem[0];
|
|
2017
|
+
p.t = r3(abre ? l.t : Math.min(Math.max(j === 0 ? l.t : tw + 2 * Q, alvo), Math.max(l.t, fim)));
|
|
1989
2018
|
tw = p.t;
|
|
1990
2019
|
});
|
|
1991
2020
|
}
|