primocode 10.13.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 +66 -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/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/editor.js +285 -0
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/index.js +90 -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 +60 -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/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maquina.js — a máquina de motion: o assunto entra, o vídeo de motion sai, com assinatura própria.
|
|
3
|
+
*
|
|
4
|
+
* O modelo (mesmo o grátis) preenche um JSON declarativo; o DIRETOR (maquina-diretor.js) completa o que faltar:
|
|
5
|
+
* metáfora do assunto, primitivas de cena (38), transições (15), câmera (8), tipografia (8), ritmo, com semente por
|
|
6
|
+
* vídeo e conferência contra as últimas assinaturas. O kit (kit-ae/*.jsx) faz o trabalho fino no EffectCraft.
|
|
7
|
+
*
|
|
8
|
+
* estudio_motion → elementos reais (3D/GIF/logo) → direção → [voz + whisper: o tempo de cada palavra]
|
|
9
|
+
* → [trilha + batidas: cortes na batida] → JSX → render → conferência (folha de contato, quadro vazio,
|
|
10
|
+
* texto fora, contraste; falhou → corrige e refaz até 2×) → SFX reais CC0 por ação → mix −14 LUFS.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawnSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
const KIT = path.join(__dirname, 'kit-ae');
|
|
21
|
+
const FORMATOS = { '9:16': [1080, 1920], '1:1': [1080, 1080], '16:9': [1920, 1080] };
|
|
22
|
+
const diretor = require('./maquina-diretor.js');
|
|
23
|
+
const TIPOS = diretor.TIPOS; // as 38 primitivas de cena
|
|
24
|
+
const TROCAS = diretor.TRANSICOES; // as 15 transições
|
|
25
|
+
const LUFS_ALVO = -14;
|
|
26
|
+
|
|
27
|
+
// ── cor ──────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function rgb(h) {
|
|
30
|
+
let s = String(h || '').replace('#', '');
|
|
31
|
+
if (s.length === 3) s = s.split('').map((c) => c + c).join('');
|
|
32
|
+
if (!/^[0-9a-f]{6}$/i.test(s)) return null;
|
|
33
|
+
return [0, 2, 4].map((i) => parseInt(s.substr(i, 2), 16));
|
|
34
|
+
}
|
|
35
|
+
const hex = (c) => '#' + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('');
|
|
36
|
+
function luminancia(h) {
|
|
37
|
+
const c = (rgb(h) || [0, 0, 0]).map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); });
|
|
38
|
+
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
39
|
+
}
|
|
40
|
+
function contraste(a, b) {
|
|
41
|
+
const x = luminancia(a), y = luminancia(b);
|
|
42
|
+
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
43
|
+
}
|
|
44
|
+
function misturar(a, b, p) {
|
|
45
|
+
const x = rgb(a) || [0, 0, 0], y = rgb(b) || [255, 255, 255];
|
|
46
|
+
return hex(x.map((v, i) => v + (y[i] - v) * p));
|
|
47
|
+
}
|
|
48
|
+
/** A cor da lista que mais contrasta com `fundo`. */
|
|
49
|
+
const melhorCor = (fundo, lista) => lista.slice().sort((p, q) => contraste(q, fundo) - contraste(p, fundo))[0];
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
// ── paletas: nada de roxo-azul genérico ──────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const CHAVES = {
|
|
56
|
+
forno: /p[aã]o|padaria|caf[eé]|comida|restaurante|doce|bolo|lanche|confeit|pizza|hamb|sorvete|cozinha|a[cç]a[ií]/i,
|
|
57
|
+
rose: /cl[ií]nica|est[eé]tica|beleza|sal[aã]o|cabelo|unha|spa|pele|sa[uú]de|m[eé]dic|dent|cliente|sumid/i,
|
|
58
|
+
grafite: /app|saas|software|c[oó]digo|program|tecnolog|ia\b|intelig|primocode|sistema|plataforma|dados|site|digital|startup/i,
|
|
59
|
+
floresta: /finan|dinheiro|invest|agro|natur|sustent|verde|jardim|planta|energia|banco|cr[eé]dito|imobili/i,
|
|
60
|
+
petroleo: /curso|aula|educa|escola|ensino|consult|advoc|direito|mentoria|treinamento|palestra|livro/i,
|
|
61
|
+
coral: /moda|roupa|fitness|academia|esporte|treino|loja|vestid|sapato|brech[oó]|corrida|dan[cç]a/i,
|
|
62
|
+
};
|
|
63
|
+
const PALETAS = Object.keys(diretor.PALETAS).map((nome) => ({ nome, chaves: CHAVES[nome] || null, ...diretor.PALETAS[nome] }));
|
|
64
|
+
const hashTexto = diretor.hashTexto;
|
|
65
|
+
function paletaDoAssunto(assunto) {
|
|
66
|
+
const a = String(assunto || '');
|
|
67
|
+
const achou = PALETAS.find((p) => p.chaves && p.chaves.test(a));
|
|
68
|
+
return achou || PALETAS[hashTexto(a) % PALETAS.length];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** As 3 variações da paleta: normal, fundo profundo e fundo de destaque, sempre com contraste de leitura. */
|
|
72
|
+
function variacoes(base) {
|
|
73
|
+
const fundo = base.fundo, tinta = base.tinta, dest = base.destaque;
|
|
74
|
+
const claroBg = luminancia(fundo) >= luminancia(tinta) ? fundo : tinta;
|
|
75
|
+
const escuroBg = claroBg === fundo ? tinta : fundo;
|
|
76
|
+
const fix = (bg, fg, ac) => {
|
|
77
|
+
if (contraste(fg, bg) < 4.5) fg = melhorCor(bg, ['#FFFFFF', '#111111']);
|
|
78
|
+
if (contraste(ac, bg) < 3) ac = melhorCor(bg, [dest, fg, tinta, fundo]);
|
|
79
|
+
const mudo = misturar(bg, fg, 0.66);
|
|
80
|
+
return { bg, fg, ac, mudo: contraste(mudo, bg) >= 3 ? mudo : fg, card: misturar(bg, fg, 0.1), brilho: misturar(bg, ac, 0.32) };
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
normal: fix(fundo, tinta, dest),
|
|
84
|
+
fundo: fix(escuroBg, claroBg, dest),
|
|
85
|
+
destaque: (() => { const fg = melhorCor(dest, [claroBg, escuroBg]), ac = melhorCor(dest, [escuroBg, claroBg]); return fix(dest, contraste(fg, dest) >= 3 ? fg : melhorCor(dest, ['#FFFFFF', '#111111']), contraste(ac, dest) >= 3 ? ac : melhorCor(dest, ['#FFFFFF', '#111111'])); })(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── entrada → plano ──────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const limpa = (s, max) => String(s == null ? '' : s).replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
92
|
+
const marcarChave = diretor.marcarChave;
|
|
93
|
+
|
|
94
|
+
function formatoDe(arg, ficha) {
|
|
95
|
+
const bruto = String(arg || (ficha && ficha.formato) || '9:16');
|
|
96
|
+
if (FORMATOS[bruto]) return bruto;
|
|
97
|
+
if (/16\s*[:x×]\s*9|1920\s*[x×]\s*1080/.test(bruto)) return '16:9';
|
|
98
|
+
if (/9\s*[:x×]\s*16|1080\s*[x×]\s*1920/.test(bruto)) return '9:16';
|
|
99
|
+
if (/1\s*:\s*1|quadrad|1080\s*[x×]\s*1080/.test(bruto)) return '1:1';
|
|
100
|
+
return '9:16';
|
|
101
|
+
}
|
|
102
|
+
function duracaoDaFicha(ficha) {
|
|
103
|
+
const m = ficha && String(ficha.duracao || '').match(/(\d+(?:[.,]\d+)?)\s*s/);
|
|
104
|
+
return m ? Number(m[1].replace(',', '.')) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Os papéis padrão (compatível com o roteiro antigo): quando o modelo não manda cenas, o diretor sorteia os papéis. */
|
|
108
|
+
function roteiroPadrao(assunto, { duracao = 15 } = {}) {
|
|
109
|
+
return diretor.dirigir({ assunto, duracao }, { historico: [], semente: hashTexto(assunto) }).cenas;
|
|
110
|
+
}
|
|
111
|
+
function normalizarCena(c, i) {
|
|
112
|
+
c = c && typeof c === 'object' ? c : { titulo: String(c || '') };
|
|
113
|
+
const tipo = diretor.PRIMITIVAS[diretor.normTipo(c.tipo)] ? diretor.normTipo(c.tipo) : (i === 0 ? 'gancho' : 'citacao');
|
|
114
|
+
return diretor.completar(c, tipo, { assunto: c.titulo || '', meta: diretor.METAFORAS.geral, proxima: null });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const maxDur = (tipo) => (tipo === 'celular' || tipo === 'prompt' ? 5 : 4.2);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Do pedido ao PLANO que o kit entende (sem rede nem disco; o histórico de assinaturas vem por `opc.historico`).
|
|
121
|
+
* `args`: {assunto, formato, marca:{cores,fontes}, cenas:[{papel, tipo, fala, titulo, itens…}], transicoes, camera, tipografia,
|
|
122
|
+
* metafora, ritmo, semente, receita, duracao}. `opc`: {receita, historico, semente, elementos}.
|
|
123
|
+
*/
|
|
124
|
+
function plano(args = {}, opc = {}) {
|
|
125
|
+
const receita = opc.receita || null;
|
|
126
|
+
const ficha = receita && receita.ficha;
|
|
127
|
+
const formato = formatoDe(args.formato, ficha);
|
|
128
|
+
const [w, h] = FORMATOS[formato];
|
|
129
|
+
const alvo = Math.max(6, Math.min(60, Number(args.duracao) || duracaoDaFicha(ficha) || 15));
|
|
130
|
+
const elementos = opc.elementos || [];
|
|
131
|
+
const dir = diretor.dirigir({ ...args, duracao: alvo }, { historico: opc.historico || [], semente: opc.semente != null ? opc.semente : null, temElemento: opc.elementos ? opc.elementos.length > 0 : true });
|
|
132
|
+
const cenas = dir.cenas;
|
|
133
|
+
|
|
134
|
+
// o ritmo: sem pedido de duração por cena, ajusta ao alvo (cada cena entre 1,4 e 4,2 s)
|
|
135
|
+
const soma = cenas.reduce((s, c) => s + c.dur, 0);
|
|
136
|
+
if (!args.cenas || args.duracao) {
|
|
137
|
+
const k = alvo / soma;
|
|
138
|
+
cenas.forEach((c) => { c.dur = Math.round(Math.max(1.4, Math.min(maxDur(c.tipo), c.dur * k)) * 10) / 10; });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// paleta: a da marca ou a sorteada pela metáfora; 3 variações em ciclo, sem repetir o fundo em fila
|
|
142
|
+
const m = args.marca || {};
|
|
143
|
+
const cores = m.cores || {};
|
|
144
|
+
const base = dir.paleta && diretor.PALETAS[dir.paleta] ? diretor.PALETAS[dir.paleta] : paletaDoAssunto(args.assunto);
|
|
145
|
+
const paleta = {
|
|
146
|
+
fundo: rgb(cores.fundo) ? cores.fundo : base.fundo,
|
|
147
|
+
tinta: rgb(cores.tinta) ? cores.tinta : base.tinta,
|
|
148
|
+
destaque: rgb(cores.destaque) ? cores.destaque : base.destaque,
|
|
149
|
+
};
|
|
150
|
+
const v = variacoes(paleta);
|
|
151
|
+
// o ciclo de fundos: o fundo da marca domina; o de destaque e o invertido entram de contraponto (nunca iguais em fila)
|
|
152
|
+
const N = v.normal, D = v.destaque, F = v.fundo;
|
|
153
|
+
const ciclos = luminancia(paleta.fundo) > 0.5 ? [[N, D, N, F], [N, F, N, D], [N, D, F, N]] : [[N, D, F, N], [N, F, D, N], [N, N, D, F]];
|
|
154
|
+
const ordem = ciclos[dir.ordemPal % 3];
|
|
155
|
+
cenas.forEach((c, i) => {
|
|
156
|
+
let p = ordem[i % ordem.length];
|
|
157
|
+
if (i > 0 && p.bg === cenas[i - 1].pal.bg) p = p === N ? D : N;
|
|
158
|
+
if (i === cenas.length - 1 && cenas.length > 3) p = i > 0 && cenas[i - 1].pal.bg === F.bg ? (cenas[i - 1].pal.bg !== N.bg ? N : D) : F; // o CTA fecha no fundo invertido, a não ser que a cena anterior já o use
|
|
159
|
+
c.pal = { ...p };
|
|
160
|
+
});
|
|
161
|
+
cenas.forEach((c, i) => { if (cenas[i + 1]) c.proxBg = cenas[i + 1].pal.bg; });
|
|
162
|
+
|
|
163
|
+
const tip = diretor.TIPOGRAFIAS[dir.tipografia];
|
|
164
|
+
const f = m.fontes || {};
|
|
165
|
+
const fontes = { titulo: limpa(f.titulo, 40) || tip.fontes.titulo, texto: limpa(f.texto, 40) || tip.fontes.texto, destaque: limpa(f.destaque, 40) || tip.fontes.destaque };
|
|
166
|
+
const tipo = { nome: dir.tipografia, entrada: tip.entrada, realce: tip.realce, caixa: tip.caixa || null, passo: tip.passo, tracking: tip.tracking == null ? 0 : tip.tracking, entreLinhas: tip.entreLinhas || null };
|
|
167
|
+
// tudo entra nos primeiros ~35% da cena: a cena é montada em tempo nominal e tocada f vezes mais rápido (f ≥ 1, até 2,2)
|
|
168
|
+
cenas.forEach((c) => { const e = (diretor.PRIMITIVAS[c.tipo] || {}).entra || 2; c.f = Math.round(Math.max(1, Math.min(2.2, e / (0.36 * c.dur))) * 100) / 100; c.elemento = !!(diretor.PRIMITIVAS[c.tipo] || {}).elemento; });
|
|
169
|
+
const duracao = Math.round(cenas.reduce((s, c) => s + c.dur, 0) * 100) / 100;
|
|
170
|
+
cenas.forEach((c, i) => { c.inicio = Math.round(cenas.slice(0, i).reduce((s, x) => s + x.dur, 0) * 1000) / 1000; });
|
|
171
|
+
return { w, h, fps: 30, formato, fontes, tipo, paleta, trocas: dir.trocas, cenas, ajuste: 1, receita: receita ? receita.id : null, duracao,
|
|
172
|
+
camera: dir.camera, elementos, bpm: 0, voz: null, legenda: null, direcao: dir.assinatura, semente: dir.semente, variacoes: v };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A voz manda: cada cena começa na 1ª palavra da fala dela e dura até a próxima. `voz` = resultado de falarEMedir. */
|
|
176
|
+
function aplicarVoz(pl, voz, { legenda = true } = {}) {
|
|
177
|
+
const vozCenas = voz.cenas;
|
|
178
|
+
pl.cenas.forEach((c, i) => {
|
|
179
|
+
const vc = vozCenas[i];
|
|
180
|
+
c.inicio = Math.round(vc.inicio * 1000) / 1000;
|
|
181
|
+
const fim = i < pl.cenas.length - 1 ? vozCenas[i + 1].inicio : voz.dur + 0.7;
|
|
182
|
+
c.dur = Math.round(Math.max(1.5, Math.min(7.5, fim - vc.inicio)) * 100) / 100;
|
|
183
|
+
c.f = 1; // com voz, o elemento entra no início da palavra que o cita: o tempo é o da fala, sem acelerar
|
|
184
|
+
});
|
|
185
|
+
// contíguas: a cena seguinte começa onde a anterior termina
|
|
186
|
+
for (let i = 1; i < pl.cenas.length; i++) pl.cenas[i].inicio = Math.round((pl.cenas[i - 1].inicio + pl.cenas[i - 1].dur) * 1000) / 1000;
|
|
187
|
+
pl.duracao = Math.round((pl.cenas[pl.cenas.length - 1].inicio + pl.cenas[pl.cenas.length - 1].dur) * 100) / 100;
|
|
188
|
+
pl.voz = { palavras: voz.palavras.map((p) => ({ w: p.w, n: p.n, t: p.t, t2: p.t2, cena: p.cena })), dur: voz.dur, motor: voz.motor };
|
|
189
|
+
if (legenda) {
|
|
190
|
+
let chunks = require('./maquina-voz.js').legendaChunks(voz.palavras, { max: 12, maxCar: 64 });
|
|
191
|
+
// a legenda some quando o título da cena já diz a mesma coisa (≥ 70% das palavras do trecho estão no título)
|
|
192
|
+
const norm = (x) => String(x || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]/g, '');
|
|
193
|
+
chunks = chunks.filter((ch) => {
|
|
194
|
+
const tit = new Set(String(((pl.cenas[ch.cena] && pl.cenas[ch.cena].titulo) || '') + ' ' + ((pl.cenas[ch.cena] && pl.cenas[ch.cena].botao) || '')).replace(/\*/g, '').split(/\s+/).map(norm).filter(Boolean));
|
|
195
|
+
if (!tit.size) return true;
|
|
196
|
+
const ws = ch.palavras.map((x) => norm(x.w)).filter(Boolean);
|
|
197
|
+
return ws.filter((x) => tit.has(x)).length / Math.max(1, ws.length) < 0.6;
|
|
198
|
+
});
|
|
199
|
+
const v = pl.variacoes || {};
|
|
200
|
+
pl.legenda = { chunks, fonte: pl.fontes.texto, fonteDestaque: pl.fontes.destaque, tamanho: pl.h > pl.w ? 50 : 44, corDestaque: legendaCor(pl), y: pl.h > pl.w ? 0.78 : 0.9 };
|
|
201
|
+
}
|
|
202
|
+
return pl;
|
|
203
|
+
}
|
|
204
|
+
/** A cor da palavra-chave na legenda (sobre a faixa preta): o destaque se tem contraste, senão amarelo. */
|
|
205
|
+
function legendaCor(pl) {
|
|
206
|
+
const ac = pl.paleta.destaque;
|
|
207
|
+
return contraste(ac, '#000000') >= 4.5 ? ac : (contraste(pl.paleta.tinta, '#000000') >= 4.5 ? pl.paleta.tinta : '#FFD54A');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Puxa os cortes para a batida mais próxima (até 0,28 s), sem deixar cena com menos de 1,4 s nem mexer no total. */
|
|
211
|
+
function ajustarCortes(pl, batidas, { tolerancia = 0.28 } = {}) {
|
|
212
|
+
const bs = (batidas || []).filter((b) => isFinite(b));
|
|
213
|
+
if (!bs.length || pl.voz) return 0;
|
|
214
|
+
const n = pl.cenas.length, ini = pl.cenas.map((c) => c.inicio), total = pl.duracao;
|
|
215
|
+
let mexeu = 0;
|
|
216
|
+
for (let k = 1; k < n; k++) {
|
|
217
|
+
let melhor = null;
|
|
218
|
+
for (const b of bs) if (melhor == null || Math.abs(b - ini[k]) < Math.abs(melhor - ini[k])) melhor = b;
|
|
219
|
+
if (melhor == null || Math.abs(melhor - ini[k]) > tolerancia) continue;
|
|
220
|
+
const min = ini[k - 1] + 1.4, max = total - (n - k) * 1.4;
|
|
221
|
+
const novo = Math.max(min, Math.min(max, melhor));
|
|
222
|
+
if (Math.abs(novo - melhor) < 0.001) { ini[k] = novo; mexeu++; }
|
|
223
|
+
}
|
|
224
|
+
pl.cenas.forEach((c, i) => { c.inicio = Math.round(ini[i] * 1000) / 1000; c.dur = Math.round(((i < n - 1 ? ini[i + 1] : total) - ini[i]) * 1000) / 1000; });
|
|
225
|
+
return mexeu;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const lerKit = (n) => fs.readFileSync(path.join(KIT, n), 'utf8');
|
|
229
|
+
/** O JSX completo: o kit, as primitivas, as cenas e a chamada `montar(PLANO)`. */
|
|
230
|
+
function script(pl) {
|
|
231
|
+
return lerKit('base.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
|
|
232
|
+
}
|
|
233
|
+
/** Os tempos medidos de cada cena (início de cada uma e o instante de conferir). */
|
|
234
|
+
function tempos(pl) {
|
|
235
|
+
return pl.cenas.map((c) => {
|
|
236
|
+
const t = c.inicio != null ? c.inicio : 0;
|
|
237
|
+
return { inicio: t, fim: t + c.dur, conferir: Math.round((t + Math.min(c.dur - 0.1, c.dur * 0.78 + 0.05)) * 100) / 100 };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── áudio (a mixagem e as medidas moram em maquina-som.js) ────────────────
|
|
242
|
+
|
|
243
|
+
function sondar(arq) {
|
|
244
|
+
const r = spawnSync('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'default=nw=1:nk=1', arq], { encoding: 'utf8' });
|
|
245
|
+
const d = parseFloat(String(r.stdout || '').trim());
|
|
246
|
+
return isNaN(d) ? null : d;
|
|
247
|
+
}
|
|
248
|
+
const medirLufs = (arq) => require('./maquina-som.js').medirLufs(arq);
|
|
249
|
+
const mixar = (o) => require('./maquina-som.js').mixar(o);
|
|
250
|
+
|
|
251
|
+
/** Os eventos "SOM {json}" que o kit escreveu no stdout do script. */
|
|
252
|
+
function lerSons(saida) {
|
|
253
|
+
const r = [];
|
|
254
|
+
for (const l of String(saida || '').split('\n')) {
|
|
255
|
+
const m = l.match(/^SOM (\{.*\})\s*$/);
|
|
256
|
+
if (m) { try { const e = JSON.parse(m[1]); if (e && e.tipo && isFinite(e.t)) r.push(e); } catch { /* linha ruim */ } }
|
|
257
|
+
}
|
|
258
|
+
return r.sort((a, b) => a.t - b.t);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Só os eventos-chave soam (Joel, 18/10: "muito efeito sonoro, sujo" e "muito explosivo"): troca (whoosh/swish curto), pouso do
|
|
263
|
+
* herói (1 pop por cena), clique, número/caixa/sino e a digitação (até 3 por cena). No máximo 1 evento a cada 0,6 s. Um único impacto
|
|
264
|
+
* SUAVE no vídeo inteiro, na virada principal (o corte do meio). Sem sub-drop, sem riser, sem glitch.
|
|
265
|
+
*/
|
|
266
|
+
function filtrarSons(eventos, pl) {
|
|
267
|
+
const cortes = pl.cenas.slice(1).map((c) => c.inicio);
|
|
268
|
+
const meio = cortes.length ? cortes[Math.floor(cortes.length / 2)] : null;
|
|
269
|
+
const daTroca = (t) => cortes.findIndex((c) => t >= c - 0.75 && t <= c + 0.3);
|
|
270
|
+
const cenaDe = (t) => { let k = 0; pl.cenas.forEach((c, i) => { if (t >= c.inicio - 0.01) k = i; }); return k; };
|
|
271
|
+
const cand = [];
|
|
272
|
+
const porCena = {};
|
|
273
|
+
const conta = (cena, tipo) => { const k = cena + ':' + tipo; porCena[k] = (porCena[k] || 0) + 1; return porCena[k]; };
|
|
274
|
+
for (const e of eventos) {
|
|
275
|
+
const g = e.ganho == null ? 1 : e.ganho;
|
|
276
|
+
const k = daTroca(e.t), cena = cenaDe(e.t);
|
|
277
|
+
let prio = 0, tipo = e.tipo, gan = g;
|
|
278
|
+
if (['troca', 'sobe', 'impacto', 'sub', 'glitch'].includes(e.tipo) && k >= 0) {
|
|
279
|
+
if (e.tipo === 'troca') { prio = 90; gan = Math.min(g, 0.8); }
|
|
280
|
+
else if (e.tipo === 'impacto' && meio != null && Math.abs(cortes[k] - meio) < 0.01) { prio = 95; tipo = 'impacto'; gan = 0.7; }
|
|
281
|
+
} else if (e.tipo === 'clique') prio = 80;
|
|
282
|
+
else if (e.tipo === 'impacto' && cena === pl.cenas.length - 1 && e.t > pl.cenas[cena].inicio + 0.5 && !eventos.some((x) => x.tipo === 'impacto' && meio != null && Math.abs(daTroca(x.t) >= 0 ? cortes[daTroca(x.t)] - meio : 9) < 0.01)) prio = 70;
|
|
283
|
+
else if (['caixa', 'sino', 'notif'].includes(e.tipo)) prio = conta(cena, e.tipo) <= 2 ? 70 : 0;
|
|
284
|
+
else if (e.tipo === 'brilho') prio = g >= 0.6 && conta(cena, 'brilho') <= 1 ? 60 : 0;
|
|
285
|
+
else if (e.tipo === 'pop') prio = conta(cena, 'pop') === 1 ? 55 : 0; // o pouso do herói
|
|
286
|
+
else if (e.tipo === 'digita') prio = conta(cena, 'digita') <= 3 ? 45 : 0;
|
|
287
|
+
else if (['papel', 'tap', 'toque'].includes(e.tipo)) prio = conta(cena, e.tipo) <= 1 ? 40 : 0;
|
|
288
|
+
if (prio > 0) cand.push({ ...e, tipo, ganho: gan, prio, troca: k >= 0 && ['troca', 'impacto'].includes(e.tipo) ? k : -1 });
|
|
289
|
+
}
|
|
290
|
+
// no máximo 1 impacto suave no vídeo
|
|
291
|
+
let impactos = 0;
|
|
292
|
+
const lista = cand.filter((e) => (e.tipo === 'impacto' ? (++impactos <= 1) : true));
|
|
293
|
+
// espaçamento mínimo de 0,6 s entre eventos de grupos diferentes (a troca e o seu impacto contam como um grupo só)
|
|
294
|
+
lista.sort((a, b) => b.prio - a.prio || a.t - b.t);
|
|
295
|
+
const aceitos = [];
|
|
296
|
+
for (const e of lista) {
|
|
297
|
+
const choca = aceitos.some((x) => Math.abs(x.t - e.t) < 0.6 && !(e.troca >= 0 && x.troca === e.troca));
|
|
298
|
+
if (!choca) aceitos.push(e);
|
|
299
|
+
}
|
|
300
|
+
return aceitos.sort((a, b) => a.t - b.t).map(({ prio, troca, ...r }) => r);
|
|
301
|
+
}
|
|
302
|
+
const lerAvisos = (saida) => String(saida || '').split('\n').filter((l) => /^AVISO /.test(l)).map((l) => l.slice(6));
|
|
303
|
+
const lerEntradas = (saida) => String(saida || '').split('\n').filter((l) => /^ENTRADA \{/.test(l)).map((l) => { try { return JSON.parse(l.slice(8)); } catch { return null; } }).filter(Boolean);
|
|
304
|
+
const lerElementos = (saida) => { const m = String(saida || '').match(/^ELEMENTOS (\d+)/m); return m ? Number(m[1]) : null; };
|
|
305
|
+
|
|
306
|
+
/** A variância de luminância do quadro (0 = tela lisa). */
|
|
307
|
+
const variancia = (png) => { const m = require('./maquina-layout.js').medirQuadro(png); return m ? m.variancia : null; };
|
|
308
|
+
const LIMIAR_VAZIO = 12;
|
|
309
|
+
|
|
310
|
+
// ── a máquina ────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/** Procura e baixa uma trilha; null se a busca não funcionar. */
|
|
313
|
+
async function acharTrilha(pasta, busca) {
|
|
314
|
+
try {
|
|
315
|
+
const assets = require('./assets.js');
|
|
316
|
+
const termos = [busca, 'upbeat instrumental', 'corporate background'].filter(Boolean);
|
|
317
|
+
for (const t of termos) {
|
|
318
|
+
const r = await assets.procurarMusica(t, { quantas: 6, duracaoMin: 25 });
|
|
319
|
+
const lista = (r && r.sons) || [];
|
|
320
|
+
for (const item of lista.slice(0, 3)) {
|
|
321
|
+
const b = await assets.baixar(item, { pasta, nome: 'trilha-motion', sub: 'midia' });
|
|
322
|
+
if (b.ok) return { arquivo: b.arquivo, creditos: b.creditos || item.creditos || null, titulo: item.titulo };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch { /* sem rede */ }
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** As fontes para o render: o peso que a família tem (as de peso único vão em 400). */
|
|
330
|
+
const SO_400 = new Set(['Anton', 'Archivo Black', 'Instrument Serif', 'Bebas Neue', 'Caveat']);
|
|
331
|
+
function fontesDoPlano(pl) {
|
|
332
|
+
const f = pl.fontes, it = pl.tipo && pl.tipo.realce === 'italico';
|
|
333
|
+
const so = (fam) => SO_400.has(fam);
|
|
334
|
+
return [
|
|
335
|
+
{ familia: f.titulo, peso: so(f.titulo) ? 400 : 800 },
|
|
336
|
+
{ familia: f.texto, peso: so(f.texto) ? 400 : 700 },
|
|
337
|
+
{ familia: f.destaque, peso: it ? (so(f.destaque) ? '400i' : '700i') : (so(f.destaque) ? 400 : 700) },
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Reparte uma narração única entre as cenas que não trazem `fala` (frases em ordem, em grupos contíguos). */
|
|
342
|
+
function repartirFala(texto, cenas) {
|
|
343
|
+
const frases = String(texto || '').replace(/\s+/g, ' ').match(/[^.!?]+[.!?]*/g) || [];
|
|
344
|
+
const limpas = frases.map((s) => s.trim()).filter(Boolean);
|
|
345
|
+
if (!limpas.length) return;
|
|
346
|
+
const n = cenas.length;
|
|
347
|
+
cenas.forEach((c, i) => {
|
|
348
|
+
const de = Math.floor(i * limpas.length / n), ate = Math.floor((i + 1) * limpas.length / n);
|
|
349
|
+
c.fala = limpas.slice(de, Math.max(ate, de + (i === n - 1 && de >= limpas.length ? 0 : 0))).join(' ') || c.fala || '';
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* estudio_motion: do pedido ao mp4 com som.
|
|
355
|
+
* `deps` (injetáveis; testes e servidor): renderizar, narrar(texto, mp3, voz), whisper(mp3), elementos(opts), trilha(pasta, busca),
|
|
356
|
+
* batidas(arquivo), sfxIndice, semRede, historico (array) e arquivoHistorico (caminho).
|
|
357
|
+
* Devolve {ok, video, quadros, folha, duracao, lufs, cenas, conferencia, avisos, trilha, assinatura, elementos}.
|
|
358
|
+
*/
|
|
359
|
+
async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
360
|
+
if (!pasta) return { ok: false, error: 'faltou a pasta do projeto.' };
|
|
361
|
+
if (!args.assunto && !(Array.isArray(args.cenas) && args.cenas.length)) return { ok: false, error: 'estudio_motion pede o assunto (ou as cenas).' };
|
|
362
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
363
|
+
const renderizar = deps.renderizar || require('./effectcraft.js').renderizar;
|
|
364
|
+
const layout = require('./maquina-layout.js');
|
|
365
|
+
const semRede = !!(deps.semRede || args.semRede);
|
|
366
|
+
const arqHist = deps.arquivoHistorico || diretor.arquivoHistorico();
|
|
367
|
+
const historico = deps.historico || diretor.lerHistorico(arqHist);
|
|
368
|
+
const assunto = limpa(args.assunto || (args.cenas[0] && (args.cenas[0].titulo || args.cenas[0].texto)) || '', 160);
|
|
369
|
+
|
|
370
|
+
// 1. os elementos reais do assunto
|
|
371
|
+
const meta = diretor.achar(assunto + ' ' + (args.metafora || ''));
|
|
372
|
+
const pedidos = Array.isArray(args.elementos) ? args.elementos : (args.elementos ? [args.elementos] : []);
|
|
373
|
+
const reunir = deps.elementos || require('./maquina-elementos.js').reunir;
|
|
374
|
+
const el = await reunir({ assunto, pedidos, meta, pasta, rede: !semRede, logos: args.logos, site: args.site, minimo: 3, max: 6 });
|
|
375
|
+
const elementos = (el && el.elementos) || [];
|
|
376
|
+
|
|
377
|
+
// 2. a direção (primitivas, transições, câmera, tipografia, paleta) com semente e conferência de assinatura
|
|
378
|
+
let pl = plano({ ...args, assunto }, { receita, historico, elementos });
|
|
379
|
+
const avisosDeMotor = [];
|
|
380
|
+
if (elementos.length < 3) avisosDeMotor.push(`só ${elementos.length} elemento(s) real(is) do assunto (o pedido é ≥ 3)`);
|
|
381
|
+
|
|
382
|
+
// 3. a narração (opcional) manda no tempo
|
|
383
|
+
let voz = null;
|
|
384
|
+
const quer = args.voz || pl.cenas.some((c) => c.fala);
|
|
385
|
+
if (args.voz && quer) {
|
|
386
|
+
const textoVoz = typeof args.voz === 'string' ? args.voz : (args.voz.texto || '');
|
|
387
|
+
if (textoVoz && !pl.cenas.some((c) => c.fala)) repartirFala(textoVoz, pl.cenas);
|
|
388
|
+
pl.cenas.forEach((c) => { if (!c.fala) c.fala = String(c.titulo || c.texto || '').replace(/\*/g, ''); });
|
|
389
|
+
const mv = require('./maquina-voz.js');
|
|
390
|
+
const r = await mv.falarEMedir({ falas: pl.cenas.map((c) => c.fala), pasta, voz: typeof args.voz === 'object' ? args.voz.voz : undefined, narrar: deps.narrar, whisper: deps.whisper });
|
|
391
|
+
if (r.ok) { voz = r; aplicarVoz(pl, r, { legenda: args.legenda !== false }); }
|
|
392
|
+
else avisosDeMotor.push('a narração não saiu: ' + r.error);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 4. a trilha: energia alta (BPM ≥ 110, batida firme), fora da lista negra, com o drop no corte mais pesado; sem faixa boa, sem música
|
|
396
|
+
let trilha = null, analise = null, semMusica = null, trilhaInicio = 0;
|
|
397
|
+
if (args.trilha !== false && args.trilha !== 0) {
|
|
398
|
+
const T = require('./maquina-trilha.js');
|
|
399
|
+
const arquivoPessoa = typeof args.trilha === 'string' && fs.existsSync(args.trilha) ? args.trilha : null;
|
|
400
|
+
const busca = typeof args.trilha === 'string' && !arquivoPessoa && args.trilha !== 'true' ? args.trilha : null;
|
|
401
|
+
let r;
|
|
402
|
+
if (deps.trilha) { // injetada (testes): o contrato antigo {arquivo,...} + deps.batidas
|
|
403
|
+
const t0 = await deps.trilha(pasta, busca);
|
|
404
|
+
r = t0 && t0.arquivo ? { ...t0, ...(deps.batidas ? await deps.batidas(t0.arquivo) : {}) } : { semMusica: true, motivo: 'sem trilha' };
|
|
405
|
+
} else r = await T.escolher({ chave: meta.chave, busca, arquivo: arquivoPessoa, pasta, rede: !semRede });
|
|
406
|
+
if (r && r.arquivo) {
|
|
407
|
+
trilha = r;
|
|
408
|
+
const cortes = pl.cenas.slice(1).map((c, i) => ({ t: c.inicio, peso: ['tremor', 'corte', 'chicote', 'zoom'].includes(pl.trocas[i]) ? 3 : 1 }));
|
|
409
|
+
const meio = cortes[Math.floor(cortes.length / 2)]; if (meio) meio.peso = 5; // o drop cai no corte do meio (a virada)
|
|
410
|
+
if (r.batidas && r.batidas.length > 3 && r.confianca >= 0.7) {
|
|
411
|
+
trilhaInicio = T.inicioNoDrop(cortes, r, pl.duracao).inicio || 0;
|
|
412
|
+
const vb = r.batidas.map((b) => b - trilhaInicio).filter((b) => b >= 0);
|
|
413
|
+
analise = { bpm: r.bpm, batidas: vb, confianca: r.confianca, acentos: (r.acentos || []).map((a) => a - trilhaInicio).filter((a) => a >= 0) };
|
|
414
|
+
pl.bpm = r.bpm; pl.batidaFase = vb.length ? vb[0] % (60 / r.bpm) : 0;
|
|
415
|
+
if (!voz) analise.cortesNaBatida = ajustarCortes(pl, vb);
|
|
416
|
+
}
|
|
417
|
+
} else if (r && r.semMusica) { semMusica = r.motivo; avisosDeMotor.push('sem música: ' + r.motivo); }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 5. conferência → corrige → confere de novo (o script e os quadros, sem renderizar o vídeo); só no fim vem o render de verdade.
|
|
421
|
+
// O relatório conta as violações de cada tipo na 1ª passada ("antes") e na última ("depois").
|
|
422
|
+
let r = null, conferencia = null;
|
|
423
|
+
const historicoDeTentativas = [], passadas = [];
|
|
424
|
+
const classificar = (avisos, auditorias, entradas, vazios, baixo, tardias) => {
|
|
425
|
+
const cnt = { foraDaArea: 0, colisao: 0, poucoEspaco: 0, textoPequeno: 0, heroiPequeno: 0, entradaTardia: tardias.length, quadroVazio: vazios.length, contrasteBaixo: baixo.filter((b) => b.contraste < 3).length, quadrosForaDaArea: 0, quadrosComColisao: 0 };
|
|
426
|
+
for (const a of avisos) {
|
|
427
|
+
if (/fora da área segura/.test(a)) cnt.foraDaArea++;
|
|
428
|
+
else if (/colisão|se sobrepõem/.test(a)) cnt.colisao++;
|
|
429
|
+
else if (/pouco espaço/.test(a)) cnt.poucoEspaco++;
|
|
430
|
+
else if (/texto principal pequeno/.test(a)) cnt.textoPequeno++;
|
|
431
|
+
else if (/herói pequeno/.test(a)) cnt.heroiPequeno++;
|
|
432
|
+
}
|
|
433
|
+
for (const x of auditorias) { cnt.quadrosForaDaArea += x.foraDaArea || 0; cnt.quadrosComColisao += x.colisoes || 0; }
|
|
434
|
+
return cnt;
|
|
435
|
+
};
|
|
436
|
+
const lerAudit = (saida) => String(saida || '').split('\n').filter((l) => /^AUDIT \{/.test(l)).map((l) => { try { return JSON.parse(l.slice(6)); } catch { return null; } }).filter(Boolean);
|
|
437
|
+
const tm0 = tempos(pl);
|
|
438
|
+
for (let tentativa = 1; tentativa <= 5; tentativa++) {
|
|
439
|
+
const tm = tempos(pl);
|
|
440
|
+
// quadros: 1 por cena (no instante de ler) + 2 em cada troca (+0,08 s e +0,22 s) para pegar borda de comp aparecendo e borrão
|
|
441
|
+
const trTempos = []; pl.cenas.slice(1).forEach((c) => trTempos.push(Math.round((c.inicio + 0.08) * 100) / 100, Math.round((c.inicio + 0.22) * 100) / 100));
|
|
442
|
+
r = await renderizar({ pasta, script: script(pl), comp: 'MAIN', fontes: fontesDoPlano(pl), conferir: tm.map((x) => x.conferir).concat(trTempos), semVideo: true, previa: !!args.previa });
|
|
443
|
+
if (!r.ok) return r;
|
|
444
|
+
const avisos = lerAvisos(r.saida);
|
|
445
|
+
const nReais = lerElementos(r.saida);
|
|
446
|
+
const entradas = lerEntradas(r.saida);
|
|
447
|
+
const auditorias = lerAudit(r.saida);
|
|
448
|
+
// a regra dos 30–40%: nada novo entra depois de 40% da cena (com voz, vale a palavra: até 60%)
|
|
449
|
+
const lim = voz ? 0.6 : 0.4;
|
|
450
|
+
const tardias = entradas.filter((x) => x.max / (x.f || 1) > lim * x.dur + 0.02).map((x) => ({ cena: x.cena, entrou: Math.round(x.max / (x.f || 1) * 100) / 100, dur: x.dur, f: x.f }));
|
|
451
|
+
const nCenas = pl.cenas.length, quadrosCena = (r.quadros || []).slice(0, nCenas), quadrosTroca = (r.quadros || []).slice(nCenas);
|
|
452
|
+
const c = layout.conferir(quadrosCena);
|
|
453
|
+
const vazios = c.vazios;
|
|
454
|
+
// as checagens de pixel: borda da comp aparecendo, retângulo sólido preto/branco fora da paleta, borrão ilegível nas trocas
|
|
455
|
+
const pxChecks = { bordaDaComp: [], solidoForaDaPaleta: [], borrao: [] };
|
|
456
|
+
const nitidezCena = [];
|
|
457
|
+
quadrosCena.forEach((q, i) => { const px = layout.lerPixels(q); if (!px) return; nitidezCena.push(layout.nitidez(px)); const fb = layout.faixaDeBorda(px); if (fb) pxChecks.bordaDaComp.push({ cena: i, ...fb }); const pc = pl.cenas[i].pal; const so = layout.solidosForaDaPaleta(px, [pc.bg, pc.fg, pc.ac, pc.mudo, pc.card]); if (so >= 0.025) pxChecks.solidoForaDaPaleta.push({ cena: i, fracao: Math.round(so * 1000) / 1000 }); });
|
|
458
|
+
const nitMed = nitidezCena.slice().sort((a, b) => a - b)[Math.floor(nitidezCena.length / 2)] || 0;
|
|
459
|
+
quadrosTroca.forEach((q, k) => { const px = layout.lerPixels(q); if (!px) return; const cutIdx = Math.floor(k / 2) + 1; const nt = layout.nitidez(px); if (nitMed > 0 && layout.varLum(px) > 400 && nt < nitMed * 0.01) pxChecks.borrao.push({ cena: cutIdx, nitidez: nt, referencia: nitMed }); });
|
|
460
|
+
const graves = c.baixoContraste.filter((b) => b.contraste < 3);
|
|
461
|
+
const contagem = classificar(avisos, auditorias, entradas, vazios, c.baixoContraste, tardias);
|
|
462
|
+
contagem.bordaDaComp = pxChecks.bordaDaComp.length; contagem.solidoForaDaPaleta = pxChecks.solidoForaDaPaleta.length; contagem.borrao = pxChecks.borrao.length;
|
|
463
|
+
passadas.push({ passada: tentativa, ...contagem });
|
|
464
|
+
conferencia = { tentativas: tentativa, quadros: (r.quadros || []).length, pixels: pxChecks, vazios, baixoContraste: c.baixoContraste, bordas: c.bordas, avisos, auditoria: auditorias, elementosNaCena: nReais, entradas: entradas.map((x) => ({ cena: x.cena, entrou: Math.round(x.max / (x.f || 1) * 100) / 100, dur: x.dur, f: x.f })), entradaTardia: tardias, ok: !vazios.length && !graves.length && !avisos.length && !tardias.length && !pxChecks.bordaDaComp.length && !pxChecks.solidoForaDaPaleta.length && !pxChecks.borrao.length };
|
|
465
|
+
if (conferencia.ok || tentativa === 5) break;
|
|
466
|
+
const mudou = layout.corrigir(pl, { avisos, vazios, baixoContraste: c.baixoContraste, bordas: [], tardias, pixels: pxChecks }, { tentativa, paletaFundo: pl.variacoes && pl.variacoes.fundo });
|
|
467
|
+
historicoDeTentativas.push({ tentativa, mudou });
|
|
468
|
+
if (!mudou.length) break;
|
|
469
|
+
}
|
|
470
|
+
conferencia.correcoes = historicoDeTentativas;
|
|
471
|
+
conferencia.relatorio = { antes: passadas[0], depois: passadas[passadas.length - 1], passadas };
|
|
472
|
+
try { fs.writeFileSync(path.join(pasta, 'ae', 'conferencia.json'), JSON.stringify(conferencia.relatorio, null, 2)); } catch { /* sem relatório em disco */ }
|
|
473
|
+
// a passada de verdade: o render do vídeo com o plano já corrigido
|
|
474
|
+
r = await renderizar({ pasta, script: script(pl), comp: 'MAIN', fontes: fontesDoPlano(pl), conferir: tm0.map((x) => x.conferir), previa: !!args.previa });
|
|
475
|
+
if (!r.ok) return r;
|
|
476
|
+
|
|
477
|
+
// 6. o som: SFX reais CC0 escolhidos por ação e assunto, nos tempos, com a batida e a voz
|
|
478
|
+
const eventosBrutos = lerSons(r.saida);
|
|
479
|
+
const eventos = filtrarSons(eventosBrutos, pl);
|
|
480
|
+
const som = require('./maquina-som.js');
|
|
481
|
+
const final = path.join(pasta, 'video-motion.mp4');
|
|
482
|
+
let mix = { ok: false, semAudio: true, video: r.video }, sons = [], sfxReais = 0;
|
|
483
|
+
if (!args.previa) try { // prévia: sem mix final pesado
|
|
484
|
+
let indice = deps.sfxIndice || null;
|
|
485
|
+
if (!indice) indice = await som.prepararBiblioteca({ assunto, acoes: [...new Set(eventos.map((e) => e.tipo))], rede: !semRede });
|
|
486
|
+
const base = eventos.map((e) => (voz ? { ...e, ganho: (e.ganho == null ? 1 : e.ganho) * 0.8 } : e));
|
|
487
|
+
sons = som.escolherSons(base, { assunto, indice, semente: pl.semente || 1 });
|
|
488
|
+
if (analise && analise.confianca >= 0.7) sons = som.encaixarNaBatida(sons, analise.batidas);
|
|
489
|
+
sfxReais = sons.filter((s) => s.arquivo).length;
|
|
490
|
+
mix = som.mixar({ pasta, mudo: r.video, saida: final, sons, trilha: trilha && trilha.arquivo, trilhaInicio, voz: voz && voz.mp3, duracao: pl.duracao });
|
|
491
|
+
} catch (e) { mix = { ok: false, error: e.message, video: r.video }; }
|
|
492
|
+
const video = mix.ok ? mix.video : r.video;
|
|
493
|
+
if (args.saida && fs.existsSync(video)) { try { fs.mkdirSync(path.dirname(args.saida), { recursive: true }); fs.copyFileSync(video, args.saida); } catch { /* sem cópia */ } }
|
|
494
|
+
|
|
495
|
+
// 7. a folha de contato e a assinatura (gravada no histórico: o próximo vídeo foge dela)
|
|
496
|
+
const dur = sondar(video) || pl.duracao;
|
|
497
|
+
const folha = layout.folhaDeContato(video, path.join(pasta, 'ae', 'folha-de-contato.png'), dur);
|
|
498
|
+
const assinatura = { ...pl.direcao, arquivo: path.basename(pasta), elementos: elementos.map((e) => e.nome), voz: !!voz };
|
|
499
|
+
if (!deps.historico && args.registrar !== false && !args.previa) diretor.registrar(assinatura, arqHist);
|
|
500
|
+
// o editor (editor.js) guarda o plano e a versão: é o que `estudio_editar` reabre
|
|
501
|
+
let sv = {};
|
|
502
|
+
try { sv = require('./editor.js').salvarSpec(pasta, { pl, args, trilha: trilha && { arquivo: trilha.arquivo, inicio: trilhaInicio }, voz: voz && { mp3: voz.mp3 }, video, previa: !!args.previa }) || {}; } catch { /* sem versão */ }
|
|
503
|
+
return {
|
|
504
|
+
ok: true, video: sv.video || video, versao: sv.versao, previa: !!args.previa, quadros: r.quadros || [], folha, duracao: dur,
|
|
505
|
+
lufs: mix.ok ? mix.lufs : null, formato: pl.formato, cenas: pl.cenas.map((c) => ({ tipo: c.tipo, dur: c.dur, papel: c.papel, camera: c.camera })),
|
|
506
|
+
trocas: pl.trocas, tipografia: pl.tipo.nome, camera: pl.camera, paleta: pl.direcao.paleta,
|
|
507
|
+
sons: eventos.length, sonsBrutos: eventosBrutos.length, sfxReais, mix: mix.mix || null, trilha: trilha ? { titulo: trilha.titulo, autor: trilha.autor, creditos: trilha.creditos, bpm: trilha.bpm, nota: trilha.nota, lufs: trilha.lufs, inicio: trilhaInicio, cortesNaBatida: analise && analise.cortesNaBatida, pessoa: !!trilha.pessoa } : null, semMusica,
|
|
508
|
+
voz: !!voz, vozMotor: voz && voz.motor, vozMedida: voz && voz.medido, conferencia, receita: pl.receita, projeto: r.projeto, assinatura,
|
|
509
|
+
elementos: elementos.map((e) => ({ nome: e.nome, tipo: e.tipo, licenca: e.licenca })), creditos: (el && el.creditos) || [],
|
|
510
|
+
avisos: avisosDeMotor,
|
|
511
|
+
_exportar: { palavras: voz ? voz.palavras : [], sfx: sons.filter((x) => x.arquivo).map((x) => x.arquivo), cenas: pl.cenas.map((c) => ({ papel: c.papel, titulo: c.titulo, texto: c.texto, fala: c.fala, botao: c.botao, dur: c.dur })), trilhaArquivo: trilha && trilha.arquivo },
|
|
512
|
+
...(mix.ok || args.previa ? {} : { aviso: 'o vídeo saiu sem a mixagem de áudio: ' + (mix.error || 'sem sons') }),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
module.exports = { filtrarSons, FORMATOS, TIPOS, TROCAS, PALETAS, plano, aplicarVoz, ajustarCortes, roteiroPadrao, normalizarCena, paletaDoAssunto, variacoes, contraste, script, tempos, lerSons, lerAvisos, lerElementos, lerEntradas, mixar, variancia, medirLufs, motion, marcarChave, fontesDoPlano, repartirFala, LIMIAR_VAZIO };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projetos.js — receitas de projetos completos que a IA consulta antes de montar.
|
|
3
|
+
*
|
|
4
|
+
* Cada receita (dados/projetos/*.json) diz para quem serve, a ficha técnica,
|
|
5
|
+
* as etapas e um prompt com {{CAMPOS}}. O conteúdo é original do PrimoCode e
|
|
6
|
+
* descreve o que as ferramentas do Estúdio já fazem.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const PASTA = path.join(__dirname, 'dados', 'projetos');
|
|
15
|
+
const CATEGORIAS = ['narrado', 'produto', 'saas', 'sound', 'elementos', 'ae'];
|
|
16
|
+
|
|
17
|
+
const sem = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
|
|
18
|
+
|
|
19
|
+
let cache = null;
|
|
20
|
+
function todos() {
|
|
21
|
+
if (!cache) {
|
|
22
|
+
cache = fs.readdirSync(PASTA).filter((f) => f.endsWith('.json')).sort()
|
|
23
|
+
.map((f) => JSON.parse(fs.readFileSync(path.join(PASTA, f), 'utf8')));
|
|
24
|
+
}
|
|
25
|
+
return cache;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Busca sem acento em título, resumo, estilo e indicado; sem termo, lista tudo. */
|
|
29
|
+
function procurar({ termo, categoria, limite = 10 } = {}) {
|
|
30
|
+
const cat = categoria ? sem(categoria) : '';
|
|
31
|
+
const palavras = sem(termo).split(/\s+/).filter(Boolean);
|
|
32
|
+
const achados = todos().filter((p) => {
|
|
33
|
+
if (cat && p.categoria !== cat) return false;
|
|
34
|
+
const palheiro = sem([p.titulo, p.resumo, (p.estilo || []).join(' '), p.indicado].join(' '));
|
|
35
|
+
return palavras.every((w) => palheiro.includes(w));
|
|
36
|
+
})
|
|
37
|
+
// Com 68 receitas, a ordem importa: a palavra no título vale mais que no resumo.
|
|
38
|
+
.map((p) => ({ p, nota: palavras.filter((w) => sem(p.titulo).includes(w)).length }))
|
|
39
|
+
.sort((a, b) => b.nota - a.nota)
|
|
40
|
+
.map((x) => x.p)
|
|
41
|
+
.slice(0, Math.max(1, Number(limite) || 10));
|
|
42
|
+
return {
|
|
43
|
+
ok: true, quantos: achados.length, categorias: CATEGORIAS,
|
|
44
|
+
projetos: achados.map((p) => ({ id: p.id, titulo: p.titulo, categoria: p.categoria, resumo: p.resumo, estilo: p.estilo, motor: p.ficha.motor })),
|
|
45
|
+
nota: 'estudio_midia acao:"projeto" nome:"<id>" traz a receita inteira.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A receita inteira, pelo id (ou, na falta, o melhor palpite do texto). */
|
|
50
|
+
function ver(id) {
|
|
51
|
+
const q = sem(id).trim();
|
|
52
|
+
if (!q) return { ok: false, error: 'diga o id da receita (acao:"projetos" lista).' };
|
|
53
|
+
const p = todos().find((x) => x.id === q) || procurar({ termo: q, limite: 1 }).projetos.map((x) => todos().find((y) => y.id === x.id))[0];
|
|
54
|
+
if (!p) return { ok: false, error: `não há receita "${id}".`, ids: todos().map((x) => x.id) };
|
|
55
|
+
// Os scripts do After Effects ficam em disco: a resposta leva o caminho, e o agente lê só o que precisar.
|
|
56
|
+
if (p.scripts_ae) p.scripts_ae = { ...p.scripts_ae, caminho: path.join(__dirname, 'dados', 'projetos-ae', p.id.replace(/^clube-/, '')) };
|
|
57
|
+
return { ok: true, projeto: p };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { procurar, ver, CATEGORIAS };
|
package/lib/headless.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headless.js — `primocode -p "pedido"`: sem tela, para script e CI.
|
|
3
|
+
*
|
|
4
|
+
* primocode -p "arruma o teste" texto no stdout
|
|
5
|
+
* primocode -p "..." --output-format json um JSON no fim: {type, is_error, result, duration_ms}
|
|
6
|
+
* git diff | primocode -p "revisa isso" o stdin entra no pedido
|
|
7
|
+
* primocode -p "..." --permission-mode plan só lê (ver lib/perigo.js)
|
|
8
|
+
*
|
|
9
|
+
* Mesma ideia do `claude -p` e do `codex exec` (flags documentadas deles; o
|
|
10
|
+
* código é nosso). O pedido passa pelo MESMO caminho do digitado (sendPrompt).
|
|
11
|
+
* Código de saída: 0 ok, 1 erro.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { stripAnsi } = require('./ui');
|
|
16
|
+
|
|
17
|
+
const FORMATOS = ['text', 'json'];
|
|
18
|
+
|
|
19
|
+
/** Tira -p/--print, --output-format e --permission-mode dos argumentos. */
|
|
20
|
+
function analisar(argv) {
|
|
21
|
+
const r = { ativo: false, formato: 'text', permissao: null, resto: [], erro: null };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
const [chave, valor] = a.startsWith('--') && a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, null];
|
|
25
|
+
if (a === '-p' || a === '--print') r.ativo = true;
|
|
26
|
+
else if (chave === '--output-format') {
|
|
27
|
+
const v = valor !== null ? valor : argv[++i];
|
|
28
|
+
if (!FORMATOS.includes(v)) r.erro = `--output-format aceita ${FORMATOS.join(' ou ')}`;
|
|
29
|
+
else r.formato = v;
|
|
30
|
+
} else if (chave === '--permission-mode') r.permissao = valor !== null ? valor : argv[++i];
|
|
31
|
+
else r.resto.push(a);
|
|
32
|
+
}
|
|
33
|
+
return r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** O pedido: os argumentos soltos, mais o stdin (que vem depois, como contexto). */
|
|
37
|
+
function montarPedido(palavras, stdin) {
|
|
38
|
+
const pedido = palavras.join(' ').trim();
|
|
39
|
+
const extra = String(stdin || '').trim();
|
|
40
|
+
if (pedido && extra) return `${pedido}\n\n${extra}`;
|
|
41
|
+
return pedido || extra;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lerStdin() {
|
|
45
|
+
if (process.stdin.isTTY) return Promise.resolve('');
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let s = '';
|
|
48
|
+
process.stdin.setEncoding('utf8');
|
|
49
|
+
process.stdin.on('data', (d) => { s += d; });
|
|
50
|
+
process.stdin.on('end', () => resolve(s));
|
|
51
|
+
process.stdin.on('error', () => resolve(s));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Roda `fn` guardando tudo o que ela escreve. `calar` esconde da tela. */
|
|
56
|
+
async function capturar(fn, { calar = false } = {}) {
|
|
57
|
+
const log = console.log;
|
|
58
|
+
const escrever = process.stdout.write.bind(process.stdout);
|
|
59
|
+
let texto = '';
|
|
60
|
+
const pega = (s) => { texto += stripAnsi(s); };
|
|
61
|
+
console.log = (...a) => { pega(a.join(' ') + '\n'); if (!calar) log(...a); };
|
|
62
|
+
process.stdout.write = (s, ...resto) => { pega(typeof s === 'string' ? s : Buffer.from(s).toString()); return calar ? true : escrever(s, ...resto); };
|
|
63
|
+
try { await fn(); } finally { console.log = log; process.stdout.write = escrever; }
|
|
64
|
+
return texto.replace(/\n{3,}/g, '\n\n').trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resultadoJson({ texto, erro, inicio, permissao }) {
|
|
68
|
+
return JSON.stringify({ type: 'result', is_error: !!erro, result: erro ? String(erro) : texto, duration_ms: Date.now() - inicio, permission_mode: permissao || 'auto' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { analisar, montarPedido, lerStdin, capturar, resultadoJson, FORMATOS };
|