primocode 10.15.2 → 10.17.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/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +74 -9
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +70 -12
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +64 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maquina-foto.js — a FOTO que a pessoa manda vira peça do motion (a primitiva "foto" do kit-ae).
|
|
3
|
+
*
|
|
4
|
+
* Antes (20/10): a foto do pedido virava só "referência" (cores, em index.js:importarReferencia) e nenhuma primitiva
|
|
5
|
+
* aceitava imagem: o motion não tinha onde pôr a foto e o pedido travava. Agora, ANTES do render e uma vez só:
|
|
6
|
+
*
|
|
7
|
+
* 1. confere o arquivo (formato, tamanho, se abre) e diz o porquê em português quando não serve;
|
|
8
|
+
* 2. copia para <projeto>/midia/fotos/ (o motion nunca aponta para fora do projeto), já com o lado maior em ≤ 2400 px;
|
|
9
|
+
* 3. faz o fundo desfocado (o plano de trás do parallax) com o ffmpeg;
|
|
10
|
+
* 4. tenta o recorte da pessoa: o Vision do macOS primeiro (lib/estudio/visao.js), o MediaPipe (studio/pessoa.py) depois;
|
|
11
|
+
* sem pessoa ou sem nenhum dos dois, a foto vai inteira, com moldura arredondada.
|
|
12
|
+
*
|
|
13
|
+
* preparar({fotos, pasta, deps}) → {fotos:[{arquivo, fundo, recorte, w, h, nome, de, moldura}], recusadas:[{arquivo, motivo}]}
|
|
14
|
+
*
|
|
15
|
+
* `deps` (injetáveis; testes): sondar(arq) → {w,h}|null, recortar(entrada, saida) → {ok, semPessoa?}, converter(entrada, saida) → bool.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const FORMATOS = ['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'];
|
|
25
|
+
const MAX_BYTES = 25 * 1024 * 1024;
|
|
26
|
+
const MAX_LADO = 2400;
|
|
27
|
+
const MAX_FOTOS = 6;
|
|
28
|
+
|
|
29
|
+
const slug = (s) => String(s).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/gi, '-').toLowerCase().replace(/^-|-$/g, '').slice(0, 30) || 'foto';
|
|
30
|
+
|
|
31
|
+
function ff(args, timeout = 60000) {
|
|
32
|
+
const r = spawnSync('ffmpeg', ['-y', '-hide_banner', '-loglevel', 'error', ...args], { encoding: 'utf8', timeout });
|
|
33
|
+
return r.status === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Largura e altura pelo ffprobe (o que o ffmpeg abre, o motion abre). null = não abre. */
|
|
37
|
+
function sondar(arq) {
|
|
38
|
+
const r = spawnSync('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height', '-of', 'csv=p=0:s=x', arq], { encoding: 'utf8', timeout: 20000 });
|
|
39
|
+
const m = r.status === 0 && String(r.stdout).trim().match(/^(\d+)x(\d+)/);
|
|
40
|
+
return m ? { w: Number(m[1]), h: Number(m[2]) } : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** HEIC/HEIF → JPG: sips (macOS) ou o ffmpeg, se ele leu HEIC. false = sem conversor. */
|
|
44
|
+
function converter(entrada, saida) {
|
|
45
|
+
if (process.platform === 'darwin') {
|
|
46
|
+
const r = spawnSync('sips', ['-s', 'format', 'jpeg', entrada, '--out', saida], { encoding: 'utf8', timeout: 60000 });
|
|
47
|
+
if (r.status === 0 && fs.existsSync(saida)) return true;
|
|
48
|
+
}
|
|
49
|
+
return ff(['-i', entrada, '-frames:v', '1', saida]) && fs.existsSync(saida);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extensao(arq) { return path.extname(String(arq || '')).replace('.', '').toLowerCase(); }
|
|
53
|
+
|
|
54
|
+
/** Confere uma foto SEM mexer nela. {ok:true, ext, bytes} ou {ok:false, error} (em português, dizendo o que fazer). */
|
|
55
|
+
function validar(arq, { sondar: sd = sondar } = {}) {
|
|
56
|
+
const nome = path.basename(String(arq || '')) || String(arq || '');
|
|
57
|
+
if (!arq || typeof arq !== 'string') return { ok: false, error: 'faltou o caminho da foto.' };
|
|
58
|
+
const ext = extensao(arq);
|
|
59
|
+
if (!FORMATOS.includes(ext)) {
|
|
60
|
+
return { ok: false, error: `a foto "${nome}" é ${ext ? '.' + ext : 'de um tipo sem extensão'}: aceito ${FORMATOS.map((f) => '.' + f).join(', ')}. Salve ou exporte como JPG ou PNG e mande de novo.` };
|
|
61
|
+
}
|
|
62
|
+
let st;
|
|
63
|
+
try { st = fs.statSync(arq); } catch { return { ok: false, error: `não achei a foto "${nome}" em ${path.dirname(arq)}. Confira o caminho.` }; }
|
|
64
|
+
if (!st.isFile()) return { ok: false, error: `"${nome}" não é um arquivo de foto.` };
|
|
65
|
+
if (st.size === 0) return { ok: false, error: `a foto "${nome}" está vazia (0 bytes).` };
|
|
66
|
+
if (st.size > MAX_BYTES) return { ok: false, error: `a foto "${nome}" tem ${(st.size / 1048576).toFixed(1)} MB: o máximo é ${MAX_BYTES / 1048576} MB. Reduza o tamanho e mande de novo.` };
|
|
67
|
+
if (ext !== 'heic' && ext !== 'heif') {
|
|
68
|
+
const d = sd(arq);
|
|
69
|
+
if (!d || !(d.w > 0 && d.h > 0)) return { ok: false, error: `não consegui abrir a foto "${nome}": o arquivo está corrompido ou não é uma imagem de verdade. Tente exportar de novo.` };
|
|
70
|
+
if (d.w < 200 || d.h < 200) return { ok: false, error: `a foto "${nome}" é pequena demais (${d.w}x${d.h}): precisa ter pelo menos 200 px de lado para não ficar borrada no vídeo.` };
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, ext, bytes: st.size };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function acharPython() {
|
|
76
|
+
for (const p of [process.env.PRIMOCODE_PYTHON, 'python3', 'python'].filter(Boolean)) {
|
|
77
|
+
const r = spawnSync(p, ['--version'], { encoding: 'utf8', timeout: 10000 });
|
|
78
|
+
if (r.status === 0) return p;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Apara a margem transparente do recorte (o que sobra é a pessoa): grava no próprio arquivo e devolve {w,h}; null se não deu. */
|
|
84
|
+
function aparar(png) {
|
|
85
|
+
const r = spawnSync('ffmpeg', ['-hide_banner', '-i', png, '-vf', 'alphaextract,cropdetect=limit=24:round=2:skip=0', '-f', 'null', '-'], { encoding: 'utf8', timeout: 60000 });
|
|
86
|
+
const todos = [...String(r.stderr || '').matchAll(/crop=(\d+):(\d+):(\d+):(\d+)/g)];
|
|
87
|
+
if (!todos.length) return null;
|
|
88
|
+
const [, w, h, x, y] = todos[todos.length - 1].map(Number);
|
|
89
|
+
if (!(w >= 64 && h >= 64)) return null;
|
|
90
|
+
const tmp = png.replace(/\.png$/, '-aparado.png');
|
|
91
|
+
if (!ff(['-i', png, '-vf', `crop=${w}:${h}:${x}:${y}`, tmp]) || !fs.existsSync(tmp)) return null;
|
|
92
|
+
fs.renameSync(tmp, png);
|
|
93
|
+
return { w, h };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** O recorte pelo MediaPipe (studio/pessoa.py). {ok:true} | {ok:false, semPessoa?, semMediaPipe?, error}. Nunca lança. */
|
|
97
|
+
function recortarMediaPipe(entrada, saida) {
|
|
98
|
+
const py = acharPython();
|
|
99
|
+
if (!py) return { ok: false, error: 'sem Python 3 para o recorte' };
|
|
100
|
+
const r = spawnSync(py, [path.join(__dirname, '..', '..', 'studio', 'pessoa.py'), 'recortar-foto', '--entrada', entrada, '--saida', saida], { encoding: 'utf8', timeout: 90000 });
|
|
101
|
+
try { return { ...JSON.parse(String(r.stdout || '').trim().split('\n').pop()), motor: 'mediapipe' }; } catch { return { ok: false, error: String(r.stderr || r.stdout || 'o recorte não respondeu').slice(-200) }; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* O recorte da pessoa, do melhor pro último caso: 1) o Vision do macOS (VNGeneratePersonSegmentationRequest, sem instalar
|
|
106
|
+
* nada; lib/estudio/visao.js), 2) o MediaPipe (studio/pessoa.py), 3) nenhum → a foto vai inteira, com moldura.
|
|
107
|
+
* {ok:true, motor} | {ok:false, semPessoa?, error}. Nunca lança. `deps.visao` é injetável (testes).
|
|
108
|
+
*/
|
|
109
|
+
function recortarPessoa(entrada, saida, { visao } = {}) {
|
|
110
|
+
let V = visao;
|
|
111
|
+
if (V === undefined) { try { V = require('./visao.js'); } catch { V = null; } }
|
|
112
|
+
if (V && V.disponivel()) {
|
|
113
|
+
const r = V.recortar(entrada, saida);
|
|
114
|
+
if (r && r.ok && fs.existsSync(saida)) return { ok: true, motor: 'vision', cobertura: r.cobertura };
|
|
115
|
+
if (r && r.semPessoa) return { ok: false, semPessoa: true, motor: 'vision', error: 'sem pessoa na foto' };
|
|
116
|
+
}
|
|
117
|
+
return recortarMediaPipe(entrada, saida);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Prepara as fotos do pedido. Nunca lança: o que não serve vai em `recusadas`, com o porquê.
|
|
122
|
+
* `opc.max` limita quantas entram (padrão 6).
|
|
123
|
+
*/
|
|
124
|
+
async function preparar({ fotos = [], pasta, deps = {}, max = MAX_FOTOS } = {}) {
|
|
125
|
+
const out = { fotos: [], recusadas: [] };
|
|
126
|
+
if (!pasta) return { ...out, recusadas: [{ arquivo: '', motivo: 'faltou a pasta do projeto.' }] };
|
|
127
|
+
const lista = (Array.isArray(fotos) ? fotos : [fotos]).filter(Boolean).map((f) => String(f).replace(/^file:\/\//, ''));
|
|
128
|
+
const unicas = [...new Set(lista)];
|
|
129
|
+
const sd = deps.sondar || sondar;
|
|
130
|
+
const dir = path.join(pasta, 'midia', 'fotos');
|
|
131
|
+
for (const orig of unicas) {
|
|
132
|
+
if (out.fotos.length >= max) { out.recusadas.push({ arquivo: orig, motivo: `o motion usa até ${max} fotos: "${path.basename(orig)}" ficou de fora.` }); continue; }
|
|
133
|
+
const v = validar(orig, { sondar: sd });
|
|
134
|
+
if (!v.ok) { out.recusadas.push({ arquivo: orig, motivo: v.error }); continue; }
|
|
135
|
+
try {
|
|
136
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
const n = out.fotos.length + 1;
|
|
138
|
+
const base = `foto-${n}-${slug(path.basename(orig, path.extname(orig)))}`;
|
|
139
|
+
let entrada = orig;
|
|
140
|
+
if (v.ext === 'heic' || v.ext === 'heif') {
|
|
141
|
+
const jpgTmp = path.join(dir, base + '-heic.jpg');
|
|
142
|
+
const ok = (deps.converter || converter)(orig, jpgTmp);
|
|
143
|
+
if (!ok || !fs.existsSync(jpgTmp)) {
|
|
144
|
+
out.recusadas.push({ arquivo: orig, motivo: `a foto "${path.basename(orig)}" é HEIC e esta máquina não tem conversor (no macOS vem o sips; no Windows/Linux, instale o ffmpeg com suporte a HEIC). Exporte como JPG e mande de novo.` });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
entrada = jpgTmp;
|
|
148
|
+
}
|
|
149
|
+
// cópia dentro do projeto, no lado máximo de 2400 px (a rotação do EXIF o ffmpeg já aplica)
|
|
150
|
+
const alvo = path.join(dir, base + (v.ext === 'png' ? '.png' : '.jpg'));
|
|
151
|
+
const filtro = `scale='if(gt(iw,ih),min(${MAX_LADO},iw),-2)':'if(gt(iw,ih),-2,min(${MAX_LADO},ih))'`;
|
|
152
|
+
const ok = ff(['-i', entrada, '-frames:v', '1', '-vf', filtro, ...(v.ext === 'png' ? [] : ['-q:v', '2']), alvo]);
|
|
153
|
+
// sem ffmpeg ou ele recusou: copia como veio (o kit ainda tenta abrir)
|
|
154
|
+
if (!ok || !fs.existsSync(alvo) || fs.statSync(alvo).size === 0) fs.copyFileSync(entrada, alvo);
|
|
155
|
+
if (entrada !== orig) fs.rmSync(entrada, { force: true });
|
|
156
|
+
const dims = sd(alvo);
|
|
157
|
+
const d = dims || { w: 1080, h: 1080 };
|
|
158
|
+
// o plano de trás do parallax: pequeno e muito desfocado (o kit amplia)
|
|
159
|
+
const fundo = path.join(dir, base + '-fundo.jpg');
|
|
160
|
+
const okF = ff(['-i', alvo, '-frames:v', '1', '-vf', 'scale=480:-2,gblur=sigma=16', '-q:v', '4', fundo]) && fs.existsSync(fundo);
|
|
161
|
+
// o recorte da pessoa, uma vez; sem pessoa ou sem MediaPipe: foto inteira com moldura
|
|
162
|
+
let recorte = null, motivoRecorte = null, rdims = null, motorRecorte = null;
|
|
163
|
+
if (deps.recortar !== false) {
|
|
164
|
+
const png = path.join(dir, base + '-recorte.png');
|
|
165
|
+
const rc = await (deps.recortar || ((e, s2) => recortarPessoa(e, s2, { visao: deps.visao })))(alvo, png);
|
|
166
|
+
if (rc && rc.ok && fs.existsSync(png)) { recorte = png; motorRecorte = rc.motor || null; rdims = (deps.aparar || aparar)(png) || sd(png); }
|
|
167
|
+
else motivoRecorte = (rc && rc.semPessoa) ? 'sem pessoa na foto' : ((rc && rc.error) || 'sem recorte');
|
|
168
|
+
}
|
|
169
|
+
out.fotos.push({ arquivo: alvo, fundo: okF ? fundo : alvo, recorte, w: d.w, h: d.h, nome: path.basename(orig), de: orig, moldura: !recorte, ...(rdims ? { rw: rdims.w, rh: rdims.h } : {}), ...(motorRecorte ? { recorteMotor: motorRecorte } : {}), ...(motivoRecorte ? { semRecorte: motivoRecorte } : {}) });
|
|
170
|
+
} catch (e) {
|
|
171
|
+
out.recusadas.push({ arquivo: orig, motivo: `não consegui preparar a foto "${path.basename(orig)}": ${e.message}` });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { FORMATOS, MAX_BYTES, MAX_LADO, MAX_FOTOS, validar, preparar, sondar, converter, recortarPessoa, recortarMediaPipe, aparar, slug, extensao };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maquina-guarda.js — a GUARDA de layout no Node, ANTES do render.
|
|
3
|
+
*
|
|
4
|
+
* As regras e os consertos moram em kit-ae/guarda.jsx (ES3), carregado aqui pelo `vm`: o MESMO código roda no kit (no fecha() de
|
|
5
|
+
* cada cena, com as caixas reais do After Effects) e aqui (sobre o plano, com a largura medida da fonte). Uma fonte da verdade.
|
|
6
|
+
*
|
|
7
|
+
* medirTexto(str, {fonte, peso, tamanho}) → largura em px (avanços do .ttf do cache; sem a fonte, estimativa)
|
|
8
|
+
* blocoTexto(str, opc, pl) → {w, h, tam, linhas}: a mesma quebra e o mesmo encolhe do texto() do kit
|
|
9
|
+
* zonaLayout(pl) → a zona segura no espaço do layout (antes da câmera)
|
|
10
|
+
* planejarFoto(cena, pl) → cena.layout {foto, halo, titulo, icones, particulas} + cena.guarda (log)
|
|
11
|
+
* guardar(pl) → roda a guarda em todas as cenas; devolve [{cena, log}]
|
|
12
|
+
* verificarCaixas(caixas, W, H, opc) → o que ainda viola (a conferência por caixa, sem o olho do Vision)
|
|
13
|
+
* core → as funções do guarda.jsx (gConsertar, gVerificar, gZona, ...)
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const vm = require('vm');
|
|
20
|
+
|
|
21
|
+
const ARQ = path.join(__dirname, 'kit-ae', 'guarda.jsx');
|
|
22
|
+
const core = (() => {
|
|
23
|
+
const ctx = vm.createContext({ Math });
|
|
24
|
+
vm.runInContext(fs.readFileSync(ARQ, 'utf8') + '\n;this.__g = { GUARDA, gZona, gConsertar, gVerificar, gToca, gCabe, gCaixa, gEscala, gMove, gW, gH, gCx, gCy, gLugarLivre, gCursor, gCaixaCursor, gDentroAlvo, gTextoNoCartao };', ctx, { filename: ARQ });
|
|
25
|
+
return ctx.__g;
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
// ── medir texto pela fonte ────────────────────────────────────────────────
|
|
29
|
+
const fontes = new Map();
|
|
30
|
+
function abrirFonte(familia, peso) {
|
|
31
|
+
const k = familia + '|' + peso;
|
|
32
|
+
if (fontes.has(k)) return fontes.get(k);
|
|
33
|
+
let f = null;
|
|
34
|
+
try {
|
|
35
|
+
const G = require('./glifos.js');
|
|
36
|
+
const base = String(familia).replace(/[^a-z0-9]+/gi, '-').toLowerCase();
|
|
37
|
+
for (const p of [peso, String(peso).replace(/i$/, ''), 800, 700, 400]) {
|
|
38
|
+
const arq = path.join(G.CACHE, base + '-' + p + '.ttf');
|
|
39
|
+
if (fs.existsSync(arq)) { f = G.abrirFonte(fs.readFileSync(arq)); break; }
|
|
40
|
+
}
|
|
41
|
+
} catch { f = null; }
|
|
42
|
+
fontes.set(k, f);
|
|
43
|
+
return f;
|
|
44
|
+
}
|
|
45
|
+
/** Largura (px) de `str` na fonte, pelos avanços do hmtx; sem a fonte no cache, 0,58 em por caractere (Poppins pesada). */
|
|
46
|
+
function medirTexto(str, { fonte = 'Poppins', peso = 800, tamanho = 100, tracking = 0 } = {}) {
|
|
47
|
+
const s = String(str || '');
|
|
48
|
+
const f = abrirFonte(fonte, peso);
|
|
49
|
+
let em = 0;
|
|
50
|
+
if (f) { for (const ch of s) em += f.avanco(f.glifo(ch.codePointAt(0))) / f.porEm; }
|
|
51
|
+
else em = s.length * 0.58;
|
|
52
|
+
return em * tamanho + (tracking / 1000) * tamanho * Math.max(0, s.length - 1);
|
|
53
|
+
}
|
|
54
|
+
function quebra(ws, sw, maxW) {
|
|
55
|
+
const linhas = [[]]; let larg = 0;
|
|
56
|
+
ws.forEach((w, i) => {
|
|
57
|
+
const add = w + (linhas[linhas.length - 1].length ? sw : 0);
|
|
58
|
+
if (larg + add > maxW && linhas[linhas.length - 1].length) { linhas.push([i]); larg = w; }
|
|
59
|
+
else { linhas[linhas.length - 1].push(i); larg += add; }
|
|
60
|
+
});
|
|
61
|
+
return linhas;
|
|
62
|
+
}
|
|
63
|
+
const SO_400 = new Set(['Anton', 'Archivo Black', 'Instrument Serif', 'Bebas Neue', 'Caveat']);
|
|
64
|
+
/** O bloco de texto como o texto() do kit monta (base.jsx): mesmo mínimo, mesma quebra equilibrada, mesmo encolhe de 8%. */
|
|
65
|
+
function blocoTexto(str, { tamanho = 90, largura, maxLinhas = 4, principal = false, entreLinhas } = {}, pl = {}) {
|
|
66
|
+
const W = pl.w || 1080, H = pl.h || 1920, tall = H / W > 1.6, princ = principal && tall;
|
|
67
|
+
const T = pl.tipo || {}, fontesPl = pl.fontes || {};
|
|
68
|
+
const fonte = fontesPl.titulo || 'Poppins', fonteD = T.realce === 'italico' ? (fontesPl.destaque || fonte) : fonte;
|
|
69
|
+
const pesoT = SO_400.has(fonte) ? 400 : 800, pesoD = T.realce === 'italico' ? (SO_400.has(fonteD) ? '400i' : '700i') : pesoT;
|
|
70
|
+
let s = String(str || ''); if (T.caixa === 'alta') s = s.toUpperCase();
|
|
71
|
+
const pal = s.replace(/\s+/g, ' ').split(' ').filter(Boolean).map((t) => ({ t: t.replace(/\*/g, ''), kw: t.charAt(0) === '*' }));
|
|
72
|
+
if (!pal.length) return { w: 0, h: 0, tam: 0, linhas: 0 };
|
|
73
|
+
let tam = tamanho * (pl.ajuste || 1) * (pl.ajusteCena || 1);
|
|
74
|
+
tam = Math.max(tam, H * 0.036); if (princ) tam = Math.max(tam, 120);
|
|
75
|
+
const maxW = Math.min(largura || W * 0.84, W * 0.84), maxLin = maxLinhas + (princ ? 1 : 0), trk = T.tracking || 0;
|
|
76
|
+
let linhas, ws, sw, tent = 0;
|
|
77
|
+
for (;;) {
|
|
78
|
+
ws = pal.map((p) => medirTexto(p.t, { fonte: p.kw ? fonteD : fonte, peso: p.kw ? pesoD : pesoT, tamanho: tam, tracking: trk }));
|
|
79
|
+
sw = Math.max(tam * 0.27, medirTexto('a a', { fonte, peso: pesoT, tamanho: tam }) - medirTexto('aa', { fonte, peso: pesoT, tamanho: tam }));
|
|
80
|
+
const mx = Math.max(...ws);
|
|
81
|
+
linhas = quebra(ws, sw, maxW);
|
|
82
|
+
if (linhas.length > 1) { const tot = ws.reduce((a, w) => a + w + sw, 0); const l2 = quebra(ws, sw, Math.min(maxW, Math.max(mx, tot / linhas.length * 1.12))); if (l2.length <= linhas.length) linhas = l2; }
|
|
83
|
+
if ((mx <= maxW && linhas.length <= maxLin) || tent > 14 || tam < (princ ? 112 : H * 0.036)) break;
|
|
84
|
+
tam *= 0.92; tent++;
|
|
85
|
+
}
|
|
86
|
+
const lh = tam * (entreLinhas || T.entreLinhas || 1.12);
|
|
87
|
+
const w = Math.max(...linhas.map((l) => l.reduce((a, i, k) => a + ws[i] + (k ? sw : 0), 0)));
|
|
88
|
+
return { w, h: linhas.length * lh, tam, linhas: linhas.length };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── a zona no espaço do layout ────────────────────────────────────────────
|
|
92
|
+
/** A zona segura do MUNDO traduzida para o LAYOUT (antes da câmera), como o kParaLayout do kit: enq × 1,12 de zoom máximo. */
|
|
93
|
+
function zonaLayout(pl) {
|
|
94
|
+
const W = pl.w, H = pl.h, legenda = !!(pl.legenda && pl.legenda.chunks && pl.legenda.chunks.length);
|
|
95
|
+
const z = core.gZona(W, H, { legenda });
|
|
96
|
+
const enq = legenda ? 0.85 : 0.88, dy = legenda ? H * 0.07 : 0, f = enq * 1.12, cx = W / 2, cy = H / 2;
|
|
97
|
+
const X = (x) => cx + (x - cx) / f, Y = (y) => cy + (y - cy + dy) / f;
|
|
98
|
+
return { x0: X(z.x0), x1: X(z.x1), y0: Y(z.y0), y1: Y(z.y1), trilho: z.trilho ? [X(z.trilho[0]), Y(z.trilho[1]), W * 2, H * 2] : null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Quanto do layout chega à tela: o kit monta o conteúdo a 88% (85% com legenda) e a câmera respira até ~6% (média ~0,9). */
|
|
102
|
+
function escalaMundo(pl) { const leg = !!(pl.legenda && pl.legenda.chunks && pl.legenda.chunks.length); return (leg ? 0.85 : 0.88) * 1.03; }
|
|
103
|
+
|
|
104
|
+
// ── a cena "foto" ─────────────────────────────────────────────────────────
|
|
105
|
+
/** Quanto a caixa cresce no passeio (escala e deslocamento por movimento): o envelope de todos os quadros-chave. */
|
|
106
|
+
function inflarFoto(recorte, mov, W, H) {
|
|
107
|
+
if (!recorte) return { kx: 1.04, ky: 1.04, dx: 0, dy: 0 }; // moldura: o passeio é DENTRO dela; só a rotação 3D (perspectiva) e a sombra
|
|
108
|
+
const esc = 1.06; // o kit (cenaFoto com layout) anda de 1,0 a 1,06 ou de 1,06 a 0,97
|
|
109
|
+
const lateral = mov === 'passeio' || mov === 'orbita';
|
|
110
|
+
return { kx: esc * 1.03, ky: esc * 1.03, dx: lateral ? W * 0.025 : 0, dy: lateral ? 0 : H * 0.015 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Planeja a cena "foto": a foto o maior possível (55–80% da largura no 9:16), o título sem encostar nela, os ícones só em espaço livre.
|
|
115
|
+
* Escreve `cena.layout` (o kit lê) e `cena.guarda` (o log). Sem caber, encolhe o título; sem saída, a cena troca de peça.
|
|
116
|
+
*/
|
|
117
|
+
function planejarFoto(c, pl, { tentativa = 0 } = {}) {
|
|
118
|
+
const F = c.foto;
|
|
119
|
+
if (!F) return null;
|
|
120
|
+
const W = pl.w, H = pl.h, v = H >= W * 0.95, alto = H / W > 1.6, u = Math.min(W, H) / 1080;
|
|
121
|
+
const z = zonaLayout(pl), folga = core.GUARDA.folga * u;
|
|
122
|
+
const mundo = escalaMundo(pl); // layout → o que o espectador vê (a câmera monta a 88% e respira até ~6%)
|
|
123
|
+
const recorte = !!F.recorte, fw = recorte ? (F.rw || F.w) : F.w, fh0 = recorte ? (F.rh || F.h) : F.h;
|
|
124
|
+
// foto alta (retrato, celular em pé, pessoa de corpo inteiro): no 9:16 a caixa vai até 4:5 e o resto é cortado (embaixo no
|
|
125
|
+
// recorte, com degradê; no meio na moldura, onde o passeio anda) — senão a foto não cabe grande junto do título
|
|
126
|
+
const ASP = v ? [1.25, 1.1, 1.0, 0.9] : [1.6, 1.3]; // sem caber, a caixa fica mais baixa (corta mais) antes de desistir
|
|
127
|
+
const maxAsp = ASP[Math.min(tentativa, ASP.length - 1)], corte = fh0 / fw > maxAsp ? maxAsp / (fh0 / fw) : 1, fh = fh0 * corte;
|
|
128
|
+
const inf = inflarFoto(recorte, c.movimento || 'aproxima', W, H);
|
|
129
|
+
const log = [];
|
|
130
|
+
// o título, medido na fonte (o kit pode mudar alguns px: a guarda do kit confere de novo com a caixa real)
|
|
131
|
+
const tamT = (v ? 86 : 74) * (c.ajuste || 1);
|
|
132
|
+
// no 9:16 o título fica entre a margem esquerda e o trilho de botões do Reels (centro ~30 px à esquerda: não se nota, e ganha uma linha)
|
|
133
|
+
const xDir = z.trilho ? Math.min(z.x1, z.trilho[0]) : z.x1;
|
|
134
|
+
const largT = v ? Math.min(W * 0.84, xDir - z.x0 - 8) : W * 0.42;
|
|
135
|
+
const tb = c.titulo ? blocoTexto(c.titulo, { tamanho: tamT, largura: largT, maxLinhas: 3, principal: true }, { ...pl, ajusteCena: 1 }) : null;
|
|
136
|
+
const tw = tb ? tb.w * 1.04 : 0, th = tb ? tb.h * 1.06 : 0;
|
|
137
|
+
const itens = [];
|
|
138
|
+
let fotoW, fotoH, px, py, tx, ty;
|
|
139
|
+
if (v) {
|
|
140
|
+
const zh = z.y1 - z.y0, livre = zh - (tb ? th + folga : 0);
|
|
141
|
+
fotoW = W * (alto ? Math.max(core.GUARDA.fotoMin, core.GUARDA.fotoMax * (c.fotoMenor || 1)) / mundo : 0.6); fotoH = fotoW * fh / fw; // fotoMenor: o olho achou texto no rosto
|
|
142
|
+
// a foto alta demais pro espaço: encolhe pela altura (o guarda reclama se ficar abaixo de 55%)
|
|
143
|
+
const envH = (h) => h * inf.ky + 2 * inf.dy;
|
|
144
|
+
if (envH(fotoH) > livre * 0.94) { fotoH = (livre * 0.94 - 2 * inf.dy) / inf.ky; fotoW = fotoH * fw / fh; }
|
|
145
|
+
// título embaixo (no fim da zona), a foto em cima dele com folga; o espaço que sobra fica em cima, pros ícones
|
|
146
|
+
ty = tb ? z.y1 - th / 2 : null; tx = z.trilho ? (z.x0 + xDir) / 2 : W / 2;
|
|
147
|
+
const baseFoto = tb ? ty - th / 2 - folga : z.y1;
|
|
148
|
+
py = baseFoto - envH(fotoH) / 2; px = W / 2;
|
|
149
|
+
} else {
|
|
150
|
+
fotoW = Math.min(W * 0.4, (H * 0.74) * fw / fh); fotoH = fotoW * fh / fw;
|
|
151
|
+
px = W * 0.3; py = H / 2; tx = W * 0.73; ty = H / 2;
|
|
152
|
+
}
|
|
153
|
+
const fotoItem = { id: 'foto', tipo: 'foto', nome: 'a foto', c: core.gCaixa(px, py, fotoW * inf.kx + 2 * inf.dx, fotoH * inf.ky + 2 * inf.dy), infla: inf.kx + 2 * inf.dx / fotoW, prio: 9 };
|
|
154
|
+
itens.push(fotoItem);
|
|
155
|
+
const titItem = tb ? { id: 'titulo', tipo: 'texto', nome: 'o título', c: core.gCaixa(tx, ty, tw, th), prio: 10 } : null;
|
|
156
|
+
if (titItem) itens.push(titItem);
|
|
157
|
+
// os ícones (elementos reais do assunto) começam onde o kit punha; a guarda tira de cima do texto e da foto
|
|
158
|
+
const nEl = Math.min((pl.elementos || []).length, v ? 3 : 2), S = Math.min(W, H) * (v ? 0.2 : 0.24);
|
|
159
|
+
const posV = [[W * 0.2, H * 0.17], [W * 0.84, H * 0.5], [W * 0.18, H * 0.58]], posH = [[W * 0.54, H * 0.2], [W * 0.5, H * 0.8]];
|
|
160
|
+
const ondula = (12 + 0.05 * W / u) * u, inflaI = 1.15; // o balanço (seno) e o quique da entrada
|
|
161
|
+
for (let i = 0; i < nEl; i++) {
|
|
162
|
+
const p = (v ? posV : posH)[i];
|
|
163
|
+
itens.push({ id: 'icone' + i, idx: i, tipo: 'icone', nome: 'ícone ' + ((pl.elementos[i] && pl.elementos[i].nome) || i + 1), c: core.gCaixa(p[0], p[1], S * inflaI + 2 * ondula, S * inflaI + 2 * 13 * u), infla: (S * inflaI + 2 * ondula) / S, prio: 3 - i });
|
|
164
|
+
}
|
|
165
|
+
const r = core.gConsertar(itens, W, H, { zona: z, folga, escalaMundo: mundo });
|
|
166
|
+
log.push(...r.log);
|
|
167
|
+
if (r.trocar) {
|
|
168
|
+
if (tentativa < ASP.length - 1 && fh0 / fw > ASP[tentativa + 1]) return planejarFoto(c, pl, { tentativa: tentativa + 1 });
|
|
169
|
+
c.guarda = [...log, 'a foto e o título não cabem juntos: a cena vira herói (sem a foto)'];
|
|
170
|
+
c.tipo = 'heroi'; c.layout = null;
|
|
171
|
+
return c.guarda;
|
|
172
|
+
}
|
|
173
|
+
if (corte < 1) log.unshift(`foto alta: a caixa vai até ${maxAsp === 1.25 ? '4:5' : '1:' + maxAsp} e ${Math.round((1 - corte) * 100)}% da altura fica de fora (${recorte ? 'embaixo, com degradê' : 'o passeio anda por ela'})`);
|
|
174
|
+
const fi = r.itens.find((x) => x.id === 'foto');
|
|
175
|
+
const k = fi.k, fW = fotoW * k, fH = fotoH * k;
|
|
176
|
+
const cxF = (fi.c[0] + fi.c[2]) / 2, cyF = (fi.c[1] + fi.c[3]) / 2;
|
|
177
|
+
const ti = r.itens.find((x) => x.id === 'titulo');
|
|
178
|
+
const fotoPct = Math.round(fW * mundo / W * 100); // no mundo (o que se vê)
|
|
179
|
+
c.layout = {
|
|
180
|
+
foto: { x: Math.round(cxF), y: Math.round(cyF), w: Math.round(fW), h: Math.round(fH), ...(corte < 1 ? { corte: Math.round(corte * 1000) / 1000 } : {}) },
|
|
181
|
+
// o halo abraça a foto (nunca vaza pro título): círculo atrás da cabeça no recorte, placa arredondada na moldura
|
|
182
|
+
halo: recorte ? { x: Math.round(cxF), y: Math.round(cyF - fH * 0.06), d: Math.round(Math.min(fW * 0.92, fH * 1.02)) } : { x: Math.round(cxF), y: Math.round(cyF), w: Math.round(fW * 1.04), h: Math.round(fH * 1.04) },
|
|
183
|
+
titulo: ti ? { x: Math.round((ti.c[0] + ti.c[2]) / 2), y: Math.round((ti.c[1] + ti.c[3]) / 2), largura: Math.round(largT), tam: Math.round(tamT * 100) / 100 } : null,
|
|
184
|
+
icones: r.itens.filter((x) => x.tipo === 'icone').map((x) => (x.removido ? { i: x.idx, fora: true } : { i: x.idx, x: Math.round((x.c[0] + x.c[2]) / 2), y: Math.round((x.c[1] + x.c[3]) / 2), w: Math.round(S * x.k) })),
|
|
185
|
+
particulas: [Math.round(fi.c[0]), Math.round(fi.c[1]), Math.round(fi.c[2]), Math.round(fi.c[3])],
|
|
186
|
+
fotoPct,
|
|
187
|
+
};
|
|
188
|
+
c.guarda = log.length ? log : [`ok: foto ${fotoPct}% da largura, nada encostado`];
|
|
189
|
+
return c.guarda;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A guarda em todas as cenas do plano (hoje a "foto" é planejada aqui; as outras, a guarda do kit conserta com a caixa real). */
|
|
193
|
+
function guardar(pl) {
|
|
194
|
+
const out = [];
|
|
195
|
+
(pl.cenas || []).forEach((c, i) => {
|
|
196
|
+
if (c.tipo === 'foto' && c.foto) { const l = planejarFoto(c, pl); if (l && l.length) out.push({ cena: i, log: l }); }
|
|
197
|
+
});
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** As caixas finais (a linha GUARDA do kit) conferidas de novo: o que ainda viola. */
|
|
202
|
+
function verificarCaixas(caixas, W, H, { legenda = false } = {}) {
|
|
203
|
+
const itens = (caixas || []).map((x, i) => ({ id: i, tipo: x.tipo, nome: x.nome, c: x.c, removido: !!x.fora, livre: !!x.livre }));
|
|
204
|
+
return core.gVerificar(itens, W, H, { zona: zonaLayout({ w: W, h: H, legenda: legenda ? { chunks: [1] } : null }), folga: 0 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { core, escalaMundo, medirTexto, blocoTexto, zonaLayout, inflarFoto, planejarFoto, guardar, verificarCaixas };
|
|
@@ -143,7 +143,11 @@ function corrigir(plano, r, { tentativa = 1, paletaFundo = null } = {}) {
|
|
|
143
143
|
const l = por[k];
|
|
144
144
|
const comAmbiente = l.some((x) => /ambiente/.test(x));
|
|
145
145
|
if (comAmbiente && !c.semAmbiente) { c.semAmbiente = true; mudou.push(`cena ${k}: sem o elemento de enfeite (${l.find((x) => /ambiente/.test(x))})`); }
|
|
146
|
-
|
|
146
|
+
// texto que a câmera leva pra cima da faixa de cima (o título do topo, com chicote/pulso): o título desce 1,2% em vez de
|
|
147
|
+
// encolher (encolher não tira ele do topo: só deixa a letra pequena)
|
|
148
|
+
const emCima = l.filter((x) => { const m = x.match(/texto fora da área segura[^[]*\[(-?\d+),(-?\d+),(-?\d+),(-?\d+)\]/); return m && Number(m[2]) < (plano.h || 1920) * 0.16 && Number(m[4]) < (plano.h || 1920) * 0.5; });
|
|
149
|
+
if (emCima.length && (c.tituloDesce || 0) < 0.05) { c.tituloDesce = Math.round(((c.tituloDesce || 0) + 0.012) * 1000) / 1000; mudou.push(`cena ${k}: o título desce ${Math.round(c.tituloDesce * 1000) / 10}% (${emCima[0]})`); }
|
|
150
|
+
const outros = l.filter((x) => !/ambiente/.test(x) && !/herói pequeno|texto principal pequeno/.test(x) && !/^guarda: /.test(x) && !(emCima.includes(x) && (c.tituloDesce || 0) <= 0.05));
|
|
147
151
|
if (outros.length) {
|
|
148
152
|
if ((c.ajuste || 1) <= 0.9 || c.semRotulos === undefined && (c.ajuste || 1) < 1) { // 2ª vez: some o que é opcional (rótulos e enfeite) em vez de encolher o texto até ficar ilegível
|
|
149
153
|
if (!c.semRotulos) { c.semRotulos = true; c.semAmbiente = true; mudou.push(`cena ${k}: sem rótulos opcionais e sem enfeite (${outros[0]})`); }
|
|
@@ -179,6 +183,23 @@ function corrigir(plano, r, { tentativa = 1, paletaFundo = null } = {}) {
|
|
|
179
183
|
c.f = Math.round(Math.min(2.4, Math.max((c.f || 1) * 1.2, (t.entrou * (c.f || 1)) / (0.36 * c.dur))) * 100) / 100;
|
|
180
184
|
mudou.push(`cena ${t.cena}: entrada tardia (${t.entrou} s de ${t.dur} s), acelera ${c.f}×`);
|
|
181
185
|
}
|
|
186
|
+
// a guarda do kit não achou saída: a peça troca (a foto encolhe o título antes; na 2ª vez vira herói)
|
|
187
|
+
for (const k of Object.keys(por)) {
|
|
188
|
+
const c = plano.cenas[Number(k)];
|
|
189
|
+
if (!c || !por[k].some((x) => /^guarda: troca a peça/.test(x))) continue;
|
|
190
|
+
if (c.tipo === 'foto' && !c.trocouGuarda) { c.trocouGuarda = true; c.ajuste = Math.round((c.ajuste || 1) * 0.85 * 100) / 100; mudou.push(`cena ${k}: guarda sem saída, título ${Math.round(c.ajuste * 100)}%`); }
|
|
191
|
+
else if (c.tipo !== 'cinetico') { mudou.push(`cena ${k}: guarda sem saída, ${c.tipo} vira cinetico`); c.tipo = 'cinetico'; c.elemento = false; }
|
|
192
|
+
}
|
|
193
|
+
// o olho depois do render (conferencia-visual.js): texto colado na borda, sobre o rosto, encavalado; caixa sobreposta ou fora da zona
|
|
194
|
+
const vistas = new Set();
|
|
195
|
+
for (const p of (r.pixels && r.pixels.visual) || []) {
|
|
196
|
+
const c = plano.cenas[p.cena];
|
|
197
|
+
if (!c || vistas.has(p.cena + p.tipo)) continue;
|
|
198
|
+
vistas.add(p.cena + p.tipo);
|
|
199
|
+
if (p.tipo === 'rosto' && c.tipo === 'foto') { c.fotoMenor = Math.round((c.fotoMenor || 1) * 0.9 * 100) / 100; mudou.push(`cena ${p.cena}: ${p.texto}; foto ${Math.round(c.fotoMenor * 100)}%`); }
|
|
200
|
+
else if (p.tipo === 'sobreposicao' && !c.semAmbiente) { c.semAmbiente = true; c.semRotulos = true; mudou.push(`cena ${p.cena}: ${p.texto}; sem enfeite e sem rótulos opcionais`); }
|
|
201
|
+
else { c.ajuste = Math.round((c.ajuste || 1) * 0.9 * 100) / 100; mudou.push(`cena ${p.cena}: ${p.texto}; corpo ${Math.round(c.ajuste * 100)}%`); }
|
|
202
|
+
}
|
|
182
203
|
for (const b of r.bordas || []) {
|
|
183
204
|
const c = plano.cenas[b.cena];
|
|
184
205
|
if (!c) continue;
|
package/lib/estudio/maquina.js
CHANGED
|
@@ -68,6 +68,25 @@ function paletaDoAssunto(assunto) {
|
|
|
68
68
|
return achou || PALETAS[hashTexto(a) % PALETAS.length];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Saturação (HSL, 0–1) e luminosidade de uma cor: separa cor de verdade de preto, branco e cinza. */
|
|
72
|
+
function saturacao(h) {
|
|
73
|
+
const c = (rgb(h) || [0, 0, 0]).map((v) => v / 255);
|
|
74
|
+
const mx = Math.max(...c), mn = Math.min(...c), l = (mx + mn) / 2;
|
|
75
|
+
if (mx === mn) return { s: 0, l };
|
|
76
|
+
return { s: (mx - mn) / (1 - Math.abs(2 * l - 1)), l };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* O cartão EM DESTAQUE de uma cena (o último da lista, o painel, a barra que ganha). Todo cartão vem da paleta da cena:
|
|
80
|
+
* o destaque só vira cartão quando é COR (saturada, nem quase preta nem quase branca); senão — o fundo de destaque
|
|
81
|
+
* ganha a tinta escura como acento e o cartão saía PRETO — o cartão é o brilho (o fundo puxado para o acento), e
|
|
82
|
+
* o texto em cima é o que mais contrasta com ele.
|
|
83
|
+
*/
|
|
84
|
+
function cartaoDestaque(bg, fg, ac, brilho) {
|
|
85
|
+
const { s, l } = saturacao(ac);
|
|
86
|
+
const cor = s >= 0.35 && l >= 0.18 && l <= 0.88 ? ac : brilho;
|
|
87
|
+
return { cardAc: cor, sobreCardAc: melhorCor(cor, [fg, bg, '#FFFFFF', '#14110F']) };
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/** As 3 variações da paleta: normal, fundo profundo e fundo de destaque, sempre com contraste de leitura. */
|
|
72
91
|
function variacoes(base) {
|
|
73
92
|
const fundo = base.fundo, tinta = base.tinta, dest = base.destaque;
|
|
@@ -77,7 +96,8 @@ function variacoes(base) {
|
|
|
77
96
|
if (contraste(fg, bg) < 4.5) fg = melhorCor(bg, ['#FFFFFF', '#111111']);
|
|
78
97
|
if (contraste(ac, bg) < 3) ac = melhorCor(bg, [dest, fg, tinta, fundo]);
|
|
79
98
|
const mudo = misturar(bg, fg, 0.66);
|
|
80
|
-
|
|
99
|
+
const brilho = misturar(bg, ac, 0.32);
|
|
100
|
+
return { bg, fg, ac, mudo: contraste(mudo, bg) >= 3 ? mudo : fg, card: misturar(bg, fg, 0.1), brilho, ...cartaoDestaque(bg, fg, ac, brilho) };
|
|
81
101
|
};
|
|
82
102
|
return {
|
|
83
103
|
normal: fix(fundo, tinta, dest),
|
|
@@ -114,7 +134,7 @@ function normalizarCena(c, i) {
|
|
|
114
134
|
return diretor.completar(c, tipo, { assunto: c.titulo || '', meta: diretor.METAFORAS.geral, proxima: null });
|
|
115
135
|
}
|
|
116
136
|
|
|
117
|
-
const maxDur = (tipo) => (tipo === 'celular' || tipo === 'prompt' ? 5 : 4.2);
|
|
137
|
+
const maxDur = (tipo) => (tipo === 'celular' || tipo === 'prompt' ? 5 : tipo === 'foto' ? 4.8 : 4.2);
|
|
118
138
|
|
|
119
139
|
/**
|
|
120
140
|
* Do pedido ao PLANO que o kit entende (sem rede nem disco; o histórico de assinaturas vem por `opc.historico`).
|
|
@@ -128,10 +148,15 @@ function plano(args = {}, opc = {}) {
|
|
|
128
148
|
const [w, h] = FORMATOS[formato];
|
|
129
149
|
const alvo = Math.max(6, Math.min(60, Number(args.duracao) || duracaoDaFicha(ficha) || 15));
|
|
130
150
|
const elementos = opc.elementos || [];
|
|
131
|
-
|
|
151
|
+
// só o que mora no projeto vai pro plano: o caminho de origem (`de`) fica de fora, pra nada apontar pra fora da pasta
|
|
152
|
+
const fotos = Array.isArray(opc.fotos) ? opc.fotos.filter((f) => f && f.arquivo).map(({ de, ...resto }) => resto) : [];
|
|
153
|
+
const dir = diretor.dirigir({ ...args, duracao: alvo, fotos }, { historico: opc.historico || [], semente: opc.semente != null ? opc.semente : null, temElemento: opc.elementos ? opc.elementos.length > 0 : true });
|
|
132
154
|
const cenas = dir.cenas;
|
|
155
|
+
// a cena "foto" leva a foto preparada (arquivo no projeto, fundo desfocado, recorte) e o passeio que o diretor sorteou
|
|
156
|
+
cenas.forEach((c) => { if (c.tipo === 'foto') { const f = fotos[(c.foto || 0) % Math.max(1, fotos.length)]; if (f) c.foto = { ...f }; else c.foto = null; } });
|
|
133
157
|
|
|
134
158
|
// o ritmo: sem pedido de duração por cena, ajusta ao alvo (cada cena entre 1,4 e 4,2 s)
|
|
159
|
+
cenas.forEach((c) => { if (c.tipo === 'foto') c.dur = Math.min(maxDur('foto'), c.dur * 1.5); }); // a foto precisa de tempo pro passeio
|
|
135
160
|
const soma = cenas.reduce((s, c) => s + c.dur, 0);
|
|
136
161
|
if (!args.cenas || args.duracao) {
|
|
137
162
|
const k = alvo / soma;
|
|
@@ -169,7 +194,7 @@ function plano(args = {}, opc = {}) {
|
|
|
169
194
|
const duracao = Math.round(cenas.reduce((s, c) => s + c.dur, 0) * 100) / 100;
|
|
170
195
|
cenas.forEach((c, i) => { c.inicio = Math.round(cenas.slice(0, i).reduce((s, x) => s + x.dur, 0) * 1000) / 1000; });
|
|
171
196
|
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 };
|
|
197
|
+
camera: dir.camera, elementos, fotos, bpm: 0, voz: null, legenda: null, direcao: dir.assinatura, semente: dir.semente, variacoes: v };
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
/** A voz manda: cada cena começa na 1ª palavra da fala dela e dura até a próxima. `voz` = resultado de falarEMedir. */
|
|
@@ -226,9 +251,9 @@ function ajustarCortes(pl, batidas, { tolerancia = 0.28 } = {}) {
|
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
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)`. */
|
|
254
|
+
/** O JSX completo: o kit, a guarda de layout, as primitivas, as cenas e a chamada `montar(PLANO)`. */
|
|
230
255
|
function script(pl) {
|
|
231
|
-
return lerKit('base.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
|
|
256
|
+
return lerKit('base.jsx') + '\n' + lerKit('guarda.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
|
|
232
257
|
}
|
|
233
258
|
/** Os tempos medidos de cada cena (início de cada uma e o instante de conferir). */
|
|
234
259
|
function tempos(pl) {
|
|
@@ -367,6 +392,18 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
367
392
|
const historico = deps.historico || diretor.lerHistorico(arqHist);
|
|
368
393
|
const assunto = limpa(args.assunto || (args.cenas[0] && (args.cenas[0].titulo || args.cenas[0].texto)) || '', 160);
|
|
369
394
|
|
|
395
|
+
// 0. as fotos do pedido: validadas, copiadas pro projeto, com fundo desfocado e recorte (uma vez, antes do render)
|
|
396
|
+
const pedidasFotos = Array.isArray(args.fotos) ? args.fotos : (args.fotos ? [args.fotos] : []);
|
|
397
|
+
let fotos = [], fotosRecusadas = [];
|
|
398
|
+
if (pedidasFotos.length) {
|
|
399
|
+
const F = require('./maquina-foto.js');
|
|
400
|
+
const pf = await F.preparar({ fotos: pedidasFotos, pasta, deps: deps.foto || {} });
|
|
401
|
+
fotos = pf.fotos; fotosRecusadas = pf.recusadas;
|
|
402
|
+
// a pessoa mandou foto (em `fotos`) e NENHUMA serve: diz o porquê em vez de entregar um vídeo sem a foto dela.
|
|
403
|
+
// As fotos achadas no texto do pedido (fotosImplicitas) só avisam: o vídeo sai sem elas.
|
|
404
|
+
if (!fotos.length && !args.fotosImplicitas) return { ok: false, error: 'nenhuma das fotos serve: ' + fotosRecusadas.map((x) => x.motivo).join(' '), fotosRecusadas };
|
|
405
|
+
}
|
|
406
|
+
|
|
370
407
|
// 1. os elementos reais do assunto
|
|
371
408
|
const meta = diretor.achar(assunto + ' ' + (args.metafora || ''));
|
|
372
409
|
const pedidos = Array.isArray(args.elementos) ? args.elementos : (args.elementos ? [args.elementos] : []);
|
|
@@ -375,8 +412,12 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
375
412
|
const elementos = (el && el.elementos) || [];
|
|
376
413
|
|
|
377
414
|
// 2. a direção (primitivas, transições, câmera, tipografia, paleta) com semente e conferência de assinatura
|
|
378
|
-
|
|
379
|
-
const
|
|
415
|
+
// o gosto salvo da pessoa entra como PESO no sorteio do diretor (não como molde)
|
|
416
|
+
const G = require('./gosto.js');
|
|
417
|
+
const gostoP = deps.gosto !== undefined ? deps.gosto : G.pesos(G.ler());
|
|
418
|
+
let pl = plano({ ...args, assunto, gosto: gostoP }, { receita, historico, elementos, fotos });
|
|
419
|
+
const avisosDeMotor = fotosRecusadas.map((x) => 'foto recusada: ' + x.motivo);
|
|
420
|
+
if (fotos.length && !pl.cenas.some((c) => c.tipo === 'foto')) avisosDeMotor.push('as fotos não entraram em nenhuma cena (o roteiro fixou todas as cenas com outro tipo).');
|
|
380
421
|
if (elementos.length < 3) avisosDeMotor.push(`só ${elementos.length} elemento(s) real(is) do assunto (o pedido é ≥ 3)`);
|
|
381
422
|
|
|
382
423
|
// 3. a narração (opcional) manda no tempo
|
|
@@ -435,7 +476,12 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
435
476
|
};
|
|
436
477
|
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
478
|
const tm0 = tempos(pl);
|
|
479
|
+
// a GUARDA de layout: antes de cada render, o plano é conferido e consertado (maquina-guarda.js: a cena "foto" sai planejada com a
|
|
480
|
+
// fonte medida); no kit, o fecha() de cada cena roda a mesma guarda com as caixas reais. Depois do render, o olho (Vision) confere.
|
|
481
|
+
const GU = require('./maquina-guarda.js'), CV = require('./conferencia-visual.js');
|
|
482
|
+
let guardaPlano = [], guardaKit = [], olho = null;
|
|
438
483
|
for (let tentativa = 1; tentativa <= 5; tentativa++) {
|
|
484
|
+
guardaPlano = GU.guardar(pl);
|
|
439
485
|
const tm = tempos(pl);
|
|
440
486
|
// 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
487
|
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));
|
|
@@ -457,11 +503,16 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
457
503
|
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
504
|
const nitMed = nitidezCena.slice().sort((a, b) => a - b)[Math.floor(nitidezCena.length / 2)] || 0;
|
|
459
505
|
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 }); });
|
|
506
|
+
guardaKit = CV.lerGuarda(r.saida);
|
|
507
|
+
olho = CV.conferir({ quadros: quadrosCena, pl, guarda: guardaKit, visao: deps.visao });
|
|
508
|
+
pxChecks.visual = olho.problemas;
|
|
460
509
|
const graves = c.baixoContraste.filter((b) => b.contraste < 3);
|
|
461
510
|
const contagem = classificar(avisos, auditorias, entradas, vazios, c.baixoContraste, tardias);
|
|
462
511
|
contagem.bordaDaComp = pxChecks.bordaDaComp.length; contagem.solidoForaDaPaleta = pxChecks.solidoForaDaPaleta.length; contagem.borrao = pxChecks.borrao.length;
|
|
512
|
+
contagem.visual = olho.problemas.length; contagem.consertosDaGuarda = guardaPlano.reduce((n, g) => n + g.log.filter((x) => !/^ok:/.test(x)).length, 0) + guardaKit.reduce((n, g) => n + g.log.length, 0);
|
|
463
513
|
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
|
|
514
|
+
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 && !olho.problemas.length,
|
|
515
|
+
guarda: { plano: guardaPlano, kit: guardaKit.filter((g) => g.log.length).map((g) => ({ cena: g.cena, log: g.log })), olho: olho.olho } };
|
|
465
516
|
if (conferencia.ok || tentativa === 5) break;
|
|
466
517
|
const mudou = layout.corrigir(pl, { avisos, vazios, baixoContraste: c.baixoContraste, bordas: [], tardias, pixels: pxChecks }, { tentativa, paletaFundo: pl.variacoes && pl.variacoes.fundo });
|
|
467
518
|
historicoDeTentativas.push({ tentativa, mudou });
|
|
@@ -502,15 +553,17 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
502
553
|
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
554
|
return {
|
|
504
555
|
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 })),
|
|
556
|
+
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, ...(c.tipo === 'foto' ? { movimento: c.movimento } : {}) })),
|
|
506
557
|
trocas: pl.trocas, tipografia: pl.tipo.nome, camera: pl.camera, paleta: pl.direcao.paleta,
|
|
507
558
|
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
559
|
voz: !!voz, vozMotor: voz && voz.motor, vozMedida: voz && voz.medido, conferencia, receita: pl.receita, projeto: r.projeto, assinatura,
|
|
560
|
+
fotos: fotos.map((f) => ({ arquivo: path.relative(pasta, f.arquivo).replace(/\\/g, '/'), recorte: !!f.recorte, moldura: !!f.moldura })), fotosRecusadas,
|
|
509
561
|
elementos: elementos.map((e) => ({ nome: e.nome, tipo: e.tipo, licenca: e.licenca })), creditos: (el && el.creditos) || [],
|
|
510
562
|
avisos: avisosDeMotor,
|
|
563
|
+
...(gostoP && gostoP.overshoot != null ? { gosto: { overshoot: gostoP.overshoot } } : {}),
|
|
511
564
|
_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
565
|
...(mix.ok || args.previa ? {} : { aviso: 'o vídeo saiu sem a mixagem de áudio: ' + (mix.error || 'sem sons') }),
|
|
513
566
|
};
|
|
514
567
|
}
|
|
515
568
|
|
|
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 };
|
|
569
|
+
module.exports = { cartaoDestaque, saturacao, 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 };
|
|
@@ -169,7 +169,7 @@ function instrucaoDasImagens(lista = IMAGENS) {
|
|
|
169
169
|
return `[REFERÊNCIA VISUAL] A pessoa mandou ${lista.length} imagem(ns): ${lista.map((f) => require('path').basename(f)).join(', ')}. `
|
|
170
170
|
+ 'Elas SÃO a referência do pedido, mesmo em "só motion". Se o pedido é vídeo, o Estúdio já guarda cada uma no projeto e tira as '
|
|
171
171
|
+ 'cores dela (com site, as cores do site mandam). Escreva o roteiro seguindo o que a referência mostra e diga no fim, '
|
|
172
|
-
+ 'em uma linha, o que dela o vídeo usou.';
|
|
172
|
+
+ 'em uma linha, o que dela o vídeo usou. No estudio_motion a foto também entra NA CENA (peça "foto": fundo desfocado, foto ou recorte da pessoa em 3D, passeio de câmera); passe os caminhos em "fotos" ou deixe o Estúdio pegar os do pedido.';
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
module.exports = { definir, atual, limpar, semImagem, termos, entender, inferir, instrucao, dominios, filtrar,
|