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,66 @@
|
|
|
1
|
+
# Lyric edit em interfaces · 02 · Câmera
|
|
2
|
+
|
|
3
|
+
> Data: 2026-10-17 · Tipo: tutorial · Status: vigente
|
|
4
|
+
|
|
5
|
+
**Resumo:** a câmera do estilo é uma cadeia de nulls, um por movimento, sem camada de câmera. Aqui estão o rig (visto nas 3 timelines do autor), os números medidos no On My Way, como escrever a câmera de uma música nova (o que a IA faz) e como medir a câmera de uma referência para copiar idêntico. Volta para o [índice](../tutorial-lyric-edit-interfaces.md).
|
|
6
|
+
|
|
7
|
+
## 1. O rig do autor (observado nas timelines)
|
|
8
|
+
|
|
9
|
+
- Viewer em "Active Camera (Default)": camadas 3D, **nenhuma camada de câmera**.
|
|
10
|
+
- **Um null por movimento** (47 no On My Way, 54 no Love Story, até 65 no Rockabye), cada propriedade com **2 keyframes Easy Ease** (até 4 propriedades por null: provavelmente ponto de ancoragem, posição, escala, rotação).
|
|
11
|
+
- **Cada null é filho do null mais novo**; os objetos ficam presos ao null mais antigo do bloco. O filho anda primeiro e cada pai entra 0,4–1,0 s depois: o movimento novo embrulha os de antes e nada para.
|
|
12
|
+
- No Love Story a cadeia vem em 4 blocos (um por mundo); no Rockabye recomeça a cada parte.
|
|
13
|
+
|
|
14
|
+
## 2. Os números (ajuste da câmera medida a uma soma de movimentos)
|
|
15
|
+
|
|
16
|
+
- 40 movimentos reproduzem a câmera do On My Way com 3–11% de erro.
|
|
17
|
+
- **Duração mediana 1,37 s = 33 quadros a 24 qps** (quartis 0,98–1,82 s).
|
|
18
|
+
- **Influência ~67% no 1º keyframe e ~77% no 2º** (velocidade zero nas pontas): bem mais forte que o Easy Ease padrão de 33%.
|
|
19
|
+
- Cada movimento: 4–9 quadros acelerando, pico, 7–20 freando. Recuos de 20–50% de escala por quadro no pico das trocas; chicotes laterais de até 420 px/quadro e verticais de até 360 px/quadro.
|
|
20
|
+
- Translação de 30–150 px/quadro **para a direita junto com o cursor** enquanto digita.
|
|
21
|
+
- Giro quase zero em cenas planas; 1–4°/quadro nas cenas 3D e nos chicotes.
|
|
22
|
+
- Obturador ~90° (o rastro mede ~¼ do deslocamento; 180° borra demais).
|
|
23
|
+
|
|
24
|
+
## 3. Escrever a câmera de uma música nova (sem referência)
|
|
25
|
+
|
|
26
|
+
O rig nosso, `ae/camera-movimentos.jsx`, monta exatamente isso a partir de uma lista (conferido no AE contra o modelo em Python: valores idênticos):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{"base": {"zoom_log2": 3.9, "mira_x": 450, "mira_y": 290, "giro": 0},
|
|
30
|
+
"movimentos": [{"ini": 0.00, "dur": 1.37, "infl_saida": 67, "infl_entrada": 77, "zoom_log2": -1.2, "mira_x": 150, "mira_y": 0, "giro": 0}, ...]}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `zoom_log2`: 0 = tamanho do quadro de referência do mundo; +1 = 2×; −1 = metade. `mira_x/y`: o ponto do mundo que fica no centro da tela.
|
|
34
|
+
- Cria um null "MOV k" por movimento (Progresso 0 → 1 com as influências) e o null "CAM" que soma tudo; o mundo da cena fica filho do CAM.
|
|
35
|
+
- Regras para a IA escrever a lista:
|
|
36
|
+
1. **Recuo em cada troca de cena** (zoom −2 a −4 oitavas), começando no quadro em que o buraco enche a tela.
|
|
37
|
+
2. **Avanço curto nas palavras-chave** (+0,5 a +1,5 oitava), começando 0,1–0,2 s antes da palavra.
|
|
38
|
+
3. **Seguir a digitação**: mira anda para a direita na largura do texto, durante a digitação.
|
|
39
|
+
4. **Sobreposição**: o movimento seguinte começa quando o anterior está entre 40% e 60%.
|
|
40
|
+
5. **Nunca parar**: entre movimentos, deriva mínima (zoom +0,02 oitava/s); só o último meio segundo fica parado.
|
|
41
|
+
6. Duração 0,8–1,8 s; chicote (troca rápida) 0,4–0,6 s com borrão.
|
|
42
|
+
- Cada cena tem seu CAM próprio (o mundo é da cena). O 1º quadro de cada cena tem que enquadrar o encaixe exatamente: `zoom_log2 = log2(1080 / lado)`, `mira = centro do encaixe`, `giro = −giro do encaixe`.
|
|
43
|
+
|
|
44
|
+
## 4. Copiar a câmera de uma referência (o que fizemos no On My Way)
|
|
45
|
+
|
|
46
|
+
Scripts em `camera/` do projeto-modelo, nesta ordem:
|
|
47
|
+
|
|
48
|
+
1. `medir_quadro_a_quadro.py ref.mp4 saida` — fluxo óptico (Lucas-Kanade + afim parcial por RANSAC) quadro a quadro: zoom, dx, dy, giro.
|
|
49
|
+
2. `integrar.py csv cenas.json saida.json` — integra em câmera absoluta por cena. **Mundo da cena = a tela do original no instante de referência `ref`** (câmera identidade em `ref`). `cenas.json` lista id, início, fim e ref de cada cena-mundo.
|
|
50
|
+
3. `ancorar.py original.mp4 medida.json ancorada.json` — casa cada quadro direto com o `ref` (SIFT) e distribui a correção entre âncoras: tira a deriva da integração.
|
|
51
|
+
4. `assar.py` — limpa (giro zero em cena plana, trechos segurados onde quem se mexe é um elemento) e grava `cena-<id>.json` (chaves por quadro para o `ae/camera.jsx`) + `encaixes.json` (centro, lado e giro onde a cena vizinha tem que estar na troca).
|
|
52
|
+
5. `ajustar_movimentos.py` (opcional) — vira a lista de movimentos do item 3, para estudar ou reaproveitar.
|
|
53
|
+
6. `plot_cenas.py` — gráfico de zoom/mira/giro de todas as cenas, para conferir.
|
|
54
|
+
|
|
55
|
+
### Onde a medição erra (e como os agentes corrigiram)
|
|
56
|
+
|
|
57
|
+
| Problema | Sintoma | Correção |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Deriva da integração em cena longa | encaixe calculado fora do lugar (ex.: ícone fora da janela) | âncoras SIFT; e o layout do original manda: recalcular as primeiras chaves para enquadrar o encaixe |
|
|
60
|
+
| Elemento grande se mexe sozinho | globo entrando, janela do Spotify abrindo, fileira de notas rolando lida como câmera | segurar a câmera nesse trecho e animar o elemento |
|
|
61
|
+
| Interface inclinada em 3D | giro falso de ~14° e zoom errado (Google, mapa) | homografia quadro a quadro: parte plana no CAM, resto num **Corner Pin** na comp da cena |
|
|
62
|
+
| Jornal 3D | 2D não descreve a perspectiva | homografia do plano → null 3D "PLANO" com posição e rotação X/Y/Z por quadro, câmera padrão (zoom 1500); erro < 2 px |
|
|
63
|
+
| Prints em zooms diferentes no original (Maps) | trechos que não fecham entre si (~240 px) | corrigir pelos pontos do mapa, trecho a trecho |
|
|
64
|
+
| Quadro de referência errado | tudo deslocado 1 quadro | `ffmpeg -ss T` pega o quadro seguinte: extrair por índice e comparar em k/24 |
|
|
65
|
+
|
|
66
|
+
Ferramentas extras dos agentes (no projeto-modelo): `camera/homografia_jornal.py`, `camera3d_jornal.py`, `mesa_2d.py` (jornais), `wg_ajustar_h.py`, `wg_ecc_cadeia.py`, `wg_finalizar_cam.py`, `wg_corrige_cam.py`, `wg_mosaico.py` (Google/mapas), `camera/w-agenda/` (agenda/docs).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Lyric edit em interfaces · 03 · Montagem no After Effects
|
|
2
|
+
|
|
3
|
+
> Data: 2026-10-17 · Tipo: tutorial · Status: vigente
|
|
4
|
+
|
|
5
|
+
**Resumo:** como cada cena é construída no AE (contrato, interfaces, digitação, encaixes), como dividir o trabalho entre agentes em cópias paralelas do AE, como conferir contra a referência e como montar o vídeo final com o card do Spotify. Volta para o [índice](../tutorial-lyric-edit-interfaces.md).
|
|
6
|
+
|
|
7
|
+
## 1. Contrato de cena (vale para todas)
|
|
8
|
+
|
|
9
|
+
- Uma comp **"CENA <id>"** por cena-mundo: 1080×1080, 24 qps, **duração do vídeo inteiro e tempo interno = tempo do vídeo** (nunca deslocar a comp no tempo). Assim a mesma comp serve na comp principal e dentro do encaixe da cena vizinha, sempre no mesmo instante.
|
|
10
|
+
- **Mundo** = a tela no instante de referência da cena; com a câmera em identidade a comp mostra esse quadro. A interface se estende além do quadro por onde a câmera passa.
|
|
11
|
+
- **Câmera:** `$.evalFile(P + "ae/camera.jsx"); var cam = CAMERA_criar(comp, P + "camera/cena-<id>.json")` (chaves medidas) ou `ae/camera-movimentos.jsx` (lista de movimentos). Todo o mundo vira filho do CAM, com posição em coordenadas do mundo. Cenas com perspectiva podem ter um mundo maior ("MUNDO <id>") dentro da CENA, com Corner Pin.
|
|
12
|
+
- **Encaixe:** a cena vizinha entra como comp provisória **"ENCAIXE <id-da-vizinha>"** (sólido cinza 1080² com o nome), posta por `CAMERA_encaixe(comp, cam, item, centro, lado, giro)`. No quadro da troca ela enche a tela exatamente, sem máscara visível; depois anima (máscara que arredonda, escala, borda). Troca cai no **1º quadro inteiro** (ex.: 10,80 s → quadro 260 = 10,833 s).
|
|
13
|
+
- **Zoom alto (> 3×) só em vetor** (forma, texto); PNG de interface em 3× do tamanho do mundo (escala 33,3%).
|
|
14
|
+
- Gerador comentado por cena (`ae/<id>.jsx`), rodado num `eval.run` só com a comp fechada (ES3: `var`, `function`; nada de arrow/let/template).
|
|
15
|
+
|
|
16
|
+
## 2. Interfaces
|
|
17
|
+
|
|
18
|
+
- **HTML/CSS → PNG 3×** (Playwright, `device_scale_factor=3`), sem os textos digitados (esses são camadas de texto no AE). HTML e PNG ficam em `assets/<id>/`. Modelos no projeto: `assets/A-agenda/agenda.html`, `B-docs/docs.html`, `C-alttab/desktop.html`, `D-spotify/janela.html`, `E-notas/ui.html` e `notas.html`, `F-jornal1/pagina.html?p=1|2`, `J-jornal2/pagina.html`, e os de `G…K`.
|
|
19
|
+
- Medidas decalcadas do quadro de referência (posição, tamanho de fonte pela largura do texto, cor amostrada).
|
|
20
|
+
- Interface no idioma da música; rótulos que não são letra copiados do original (menus, abas, botões, cabeçalhos de jornal).
|
|
21
|
+
|
|
22
|
+
## 3. Digitação e letra
|
|
23
|
+
|
|
24
|
+
- `$.evalFile(P + "ae/letra.jsx"); var s = LETRA("A1");` → `s.texto`, `s.palavras`, `s.tempos` (o tempo de cada palavra cantada, do `letra_automatica.py`).
|
|
25
|
+
- `$.evalFile(P + "ae/digitar.jsx"); DIGITAR(camada, s.palavras, s.tempos, fim, {cursor: true})`: expressão de Texto-fonte que digita letra a letra, cursor "|" enquanto digita e 0,25 s depois.
|
|
26
|
+
- Ainda mais fiel: copiar do original **quantas letras aparecem em cada quadro** e quando o cursor pisca (8 quadros aceso, 8 apagado). Foi o que fechou as cenas A e B; os arquivos guardam só números (`A_QUADROS`, `B1Q`).
|
|
27
|
+
- Texto que só aparece (sem cursor): letra a letra com fade (balões das notas) ou palavra caindo desfocada em 3–6 quadros.
|
|
28
|
+
- Karaokê (Spotify): uma camada por linha, cor da palavra cantada animada nos tempos do slot.
|
|
29
|
+
- **Cursor fantasma:** com borrão ligado o AE amostra o texto 0,005 s antes do quadro; termine a digitação um pouco antes da troca.
|
|
30
|
+
|
|
31
|
+
## 4. Trabalho em paralelo (4 agentes)
|
|
32
|
+
|
|
33
|
+
- Divida as cenas em grupos que encostam pouco: no On My Way, A+B · C+D+E · F+J · G+H+I+K.
|
|
34
|
+
- Pedido comum: `BRIEF-AGENTES.md` do projeto-modelo (cópia própria do AE com `instance.start {name}` e `instance` em toda chamada, `.aep` próprio, sem render de vídeo, `ae_see` contra a referência, ficha em `revisao/<nome>.md`, sem escrever letra).
|
|
35
|
+
- **Personagens comuns gerados antes** em `assets/comum/`, para todos usarem as mesmas pessoas.
|
|
36
|
+
- Quem termina cedo avisa o vizinho do que já desenhou (ex.: a moldura do alt-tab ficou dentro da cena B, então a C não desenha outra).
|
|
37
|
+
- Lições que viraram regra no `ae_lessons`: `ref-quadro-por-indice` (ffmpeg `-ss` pega o quadro seguinte), `ref-video-cor-desconhecida` (gere uma cópia bt709 da referência para o `ae_see` comparar), `precomp-3d-colapsada-nao-inclina`, escala horizontal de texto é fração (1 = 100%).
|
|
38
|
+
|
|
39
|
+
## 5. Conferência de cada cena
|
|
40
|
+
|
|
41
|
+
- `ae_see` com `reference: {video: ref/original-bt709.mp4}` (REF | NOSSO | DIF) em pelo menos: ref, começo, fim, cada troca e 3 quadros no meio de movimentos. Diferença média que ficou boa: 1–4% nas cenas de interface, ~10–15% onde as imagens são nossas.
|
|
42
|
+
- Com a letra igual à do original, a coluna DIF mostra se cada frase caiu no mesmo lugar e tamanho.
|
|
43
|
+
- `ae_expressions_check` depois de expressões. O `ae_review_motion` dá nota baixa para câmera por chaves medidas a cada quadro: é alarme falso nesse estilo.
|
|
44
|
+
|
|
45
|
+
## 6. Montagem final
|
|
46
|
+
|
|
47
|
+
1. Novo projeto `ae/montagem.aep` e, num envio: `$.evalFile(P + "ae/montagem.jsx")` → `MONT.importar()` (traz os `cenas-w-*.aep`), `MONT.trocarEncaixes()` (cada camada "ENCAIXE <id>" vira a "CENA <id>" real, mantendo transformação e máscara), `MONT.blur()` (motion blur em todas as comps, obturador 90°, 16 amostras), `MONT.principal()` (comp "MAIN …" com as cenas em sequência, cada uma visível no seu trecho, + música). A lista `MONT.CENAS` tem os tempos de troca.
|
|
48
|
+
2. **Card do Spotify:** `$.evalFile(P + "ae/card-spotify.jsx"); CARD_criar()` congela o último quadro com deriva mínima, escurece 38% e sobe um card "tocando agora" (capa nossa, título, artistas, barra andando) até 36,5 s. Textos no bloco `CARD`. Áudio da cauda: `python -P audio/eco_cauda.py` (a última frase volta em 4 ecos cada vez mais baixos e abafados).
|
|
49
|
+
3. Conferir as trocas na comp principal: `ae_see` nos quadros antes, no e depois de cada troca.
|
|
50
|
+
4. Se a letra mudar: rodar os geradores das cenas de novo (eles leem o `letra.json`) e depois a montagem.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Lyric edit em interfaces · 04 · Render e conferência
|
|
2
|
+
|
|
3
|
+
> Data: 2026-10-17 · Tipo: tutorial · Status: vigente
|
|
4
|
+
|
|
5
|
+
**Resumo:** render sem travar o PC, conferência do vídeo inteiro, comparação numérica da câmera contra a referência, lado a lado e entrega. Volta para o [índice](../tutorial-lyric-edit-interfaces.md).
|
|
6
|
+
|
|
7
|
+
## 1. Render
|
|
8
|
+
|
|
9
|
+
- Antes: feche as cópias do AE dos agentes (`instance.stop {name, save: true}`) e veja a RAM livre (`Get-CimInstance Win32_OperatingSystem`). Um job pesado por vez.
|
|
10
|
+
- `ae_render_video` com `mode: "leve"`, `chunkSec: 4`, `range: [0, fim]`, `audio: audio/mix-com-card.wav` e **`outDir` em pasta ASCII** (`<PASTA_DE_PROJETOS>/render-<projeto>/v1`): o aerender não aceita "ç" no caminho. Ele copia o projeto para a pasta do job, renderiza trecho a trecho em prioridade baixa e junta sem recodificar.
|
|
11
|
+
- No On My Way: 10 trechos, ~7 min, 876 quadros, −14,2 LUFS, sem preto; o único "congelado" acusado foi o descanso final, que é de propósito.
|
|
12
|
+
- Copie o master para `render/<nome>-v1.mp4` e faça uma cópia leve (`-crf 20`) para mandar.
|
|
13
|
+
|
|
14
|
+
## 2. Conferência do vídeo inteiro
|
|
15
|
+
|
|
16
|
+
- Folha a 2 qps com carimbo de tempo: `python -P camera/folha_contato.py render/x.mp4 revisao/x-2qps 2 220`. Olhe todas as folhas: letra no lugar, nada cortado, nenhuma cena sem conteúdo.
|
|
17
|
+
- Trocas: `olho_quadro_a_quadro.py folha` na taxa nativa em volta de cada troca (original e nosso, um em cima do outro).
|
|
18
|
+
|
|
19
|
+
## 3. Câmera original × recriação (quando há referência)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -P camera/medir_quadro_a_quadro.py render/x-ate-o-fim-do-original.mp4 camera/recriacao-v1-quadro-a-quadro
|
|
23
|
+
python -P camera/comparar.py <csv do original> camera/recriacao-v1-quadro-a-quadro.csv camera/cenas.json revisao/camera-original-x-recriacao-v1
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Corte o nosso render no fim do original antes de medir (o card do Spotify não existe no original).
|
|
27
|
+
- Saída: tabela por cena (correlação de zoom, dx, dy e diferença média) + 3 gráficos sobrepostos. No On My Way: correlação 0,94–1,00 na maioria dos canais.
|
|
28
|
+
- Correlação baixa com curva visualmente igual = pico isolado de 1 quadro: falha do rastreador em quadro muito borrado. Confirme quadro a quadro antes de mexer.
|
|
29
|
+
|
|
30
|
+
## 4. Lado a lado
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ffmpeg -i ref/original-bt709.mp4 -i render/x.mp4 -i audio/musica-original.wav -filter_complex "[0:v]scale=720:720,drawtext=...ORIGINAL...[a];[1:v]trim=0:<dur>,setpts=PTS-STARTPTS,scale=720:720,drawtext=...RECRIACAO...[b];[a][b]vstack[v]" -map "[v]" -map 2:a render/comparativo.mp4
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Empilhado (720×1440) é o que melhor se vê no celular. O original do Reel vem sem metadado de cor: use a cópia `ref/original-bt709.mp4` (ou o filtro `setparams=colorspace=bt709:color_primaries=bt709:color_trc=bt709`).
|
|
37
|
+
|
|
38
|
+
## 5. Entrega e registro
|
|
39
|
+
|
|
40
|
+
- Mandar aqui na conversa (`SendUserFile`): o lado a lado, a recriação e o gráfico da câmera. Telegram só se ele pedir.
|
|
41
|
+
- `python 08-integracoes/sincronizar-drive.py` (a mídia nova vai para o Drive).
|
|
42
|
+
- README do projeto (checklist), anotação em `anotacoes/AAAA-MM/` e memória.
|
|
43
|
+
- Aprovado → Clube do Motion completo (projeto + referência + skills; fora do zip: música comercial, letra colada, originais do autor de referência e quadros dele em `ref/`).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Lyric edit em interfaces · 05 · Abertura "celular filmando a tela → mergulho"
|
|
2
|
+
|
|
3
|
+
> Data: 2026-10-17 · Tipo: tutorial · Status: vigente
|
|
4
|
+
|
|
5
|
+
**Resumo:** o autor grava com o iPhone a tela tocando o edit; o vídeo abre nessa gravação e a câmera mergulha na tela até virar o edit limpo, sem corte. Mesmo efeito da Shelby ep. 1 e ep. 2 (aprovados), adaptado ao edit quadrado a 24 qps. Scripts em `transicao/` do projeto-modelo. Volta para o [índice](../tutorial-lyric-edit-interfaces.md).
|
|
6
|
+
|
|
7
|
+
## 1. Como gravar (dicas para o autor)
|
|
8
|
+
|
|
9
|
+
- Tela com fundo preto e o edit numa janela/player no meio; quarto escuro com alguma luz de cena (as luzes do PC deram textura para o rastreio).
|
|
10
|
+
- **Deixe o vídeo parado no primeiro quadro por ~1 s antes de dar play.** É nesse trecho parado que a troca fica invisível e o rastreio trava firme.
|
|
11
|
+
- iPhone em pé, mão firme, a tela inteira no quadro com folga em volta; 4K 30 qps está ótimo. O som do quarto é aproveitado antes do play.
|
|
12
|
+
- Sem barra de controles por cima do vídeo depois do play (se houver, a troca começa depois que ela some).
|
|
13
|
+
|
|
14
|
+
## 2. Pipeline (rodar dentro de `transicao/`, nesta ordem)
|
|
15
|
+
|
|
16
|
+
`config.json`: `celular` (a gravação em `bruto/`), `master` (o render do edit), `lado_master` (1080), `fps` (24), `duracao_master` (36,5 com o card), `saida`.
|
|
17
|
+
|
|
18
|
+
| Script | O que faz | No On My Way |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `sincronia.py` | acha o deslocamento áudio do celular × mix do master (onset a 22 kHz, fino a 48 kHz) → `off.txt` | OFF 1,329 s, correlação 0,82 |
|
|
21
|
+
| `rastreio.py T0 T1` | acha o quadrado na tela (template matching por escala com o próprio master) e refina por ECC em homografia, quadro a quadro → `cantos.json` | correlação 0,987 com o quadro parado |
|
|
22
|
+
| `atraso_imagem.py` | atraso da imagem da tela em relação ao som → `off_video.txt` | +0,017 s |
|
|
23
|
+
| `fundo.py` | movimento da mão pelo **cenário fixo** (luzes, ícones, bordas) fora do quadrado, encadeado quadro a quadro, ancorado no ECC só enquanto a tela está parada → `cantos_final.json` alisado | lado do quadrado 1.026 → 980 px, contínuo |
|
|
24
|
+
| `montar.py` | troca invisível, mergulho, escurecimento em volta e pouso → `quadros/*.png` (prova: `SO=1.9,2.6 python -P montar.py`) | 66 quadros |
|
|
25
|
+
| `juntar.py` | quadros + master cru com preto em cima e embaixo (emenda no quadro exato) + áudio (quarto até o play, mix limpo depois, whoosh e golpe no mergulho) | 37,1 s, −14,2 LUFS |
|
|
26
|
+
|
|
27
|
+
Antes de montar, confira o rastreio desenhando o contorno sobre a gravação em 5–6 instantes (`prova/contorno.jpg`): tem que cair na borda do vídeo.
|
|
28
|
+
|
|
29
|
+
## 3. As decisões que fizeram funcionar
|
|
30
|
+
|
|
31
|
+
- **Saída 9:16 a 24 qps** (a do edit; misturar com os 30 qps do celular dava tremor). A gravação é lida no tempo exato, misturando os dois quadros vizinhos.
|
|
32
|
+
- **Fim do mergulho = o quadrado enchendo a largura do Reel** (y 420–1500); o que está fora do quadrado escurece até o preto conforme o mergulho avança, então o pouso cai no master com faixas pretas, como os posts do autor aparecem no Instagram.
|
|
33
|
+
- **Troca invisível antes do play** (`TS = OFF − 0,45`): no meio do play o celular capta o "fantasma" de atualização do monitor e a mistura mostrava o texto duplicado por ~7 quadros.
|
|
34
|
+
- **ECC só com a tela parada.** Durante o recuo do conteúdo, um descompasso mínimo de tempo parece mudança de escala e o ECC encolhia o quadrado de 1.003 para 680 px com correlação 0,98. Depois do play, o caminho vem do cenário fixo (`fundo.py`).
|
|
35
|
+
- **Mergulho de 1,1 s**, bezier(0.5, 0, 0.2, 1), zoom em escala log, 12 amostras de borrão com obturador 180°; brilho de tela que cresce no meio (0,10 × sin²); a cor casada com a câmera (curva por histograma) volta ao master puro a partir de k = 0,55.
|
|
36
|
+
- **Emenda pela grade de quadros:** saída e master a 24 qps, o quadro do master avança exatamente 1 por quadro; o pouso (7 quadros, mola começando em 1) e o master cru continuam do índice seguinte. Conferido: nenhuma diferença zero (quadro repetido) nem pico na emenda.
|
|
37
|
+
- Master lido pelo ffmpeg em bt709 com `accurate_rnd+full_chroma_int` e gravado de volta igual (o OpenCV dava degrau de brilho na emenda).
|
|
38
|
+
- A saída começa em 0,7 s da gravação (0,65 s antes do play) para não abrir com 1,3 s de quadro parado.
|
|
39
|
+
|
|
40
|
+
## 4. Projetos anteriores do mesmo efeito
|
|
41
|
+
|
|
42
|
+
- Shelby ep. 1 v2 (aprovado, publicado) e ep. 2 v3: `07-videos/projetos/2026-10-12-shelby-ep*-take-unico/transicao/` (master vertical 1080×1920 a 60 qps, tela escura).
|
|
43
|
+
- Reel do DJ: `07-videos/projetos/2026-10-13-reel-claude-dj/transicao/` (tela clara, rastreio por limiar, mergulho de 0,8 s).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Edit de letra dentro de interfaces (estilo @subzmotion): passo a passo completo
|
|
2
|
+
|
|
3
|
+
> Data: 2026-10-17 · Tipo: tutorial · Status: vigente
|
|
4
|
+
|
|
5
|
+
**Resumo:** como fazer, do zero até o Reel pronto, um edit de música em que cada frase da letra é digitada dentro de uma interface real (agenda, Google, Spotify, notas do Instagram, jornal, mapa) e cada cena vira um elemento pequeno da seguinte, com a câmera em cadeia de nulls que nunca para. Base aprovada: recriação do "On My Way" ("a recriação ficou perfeita", 17/10/2026), com câmera batendo 0,94–1,00 com a do original e abertura "celular filmando a tela → mergulho". Estilo hype no Reels (o original teve 1,87 mi plays num perfil de 5,6 mil seguidores).
|
|
6
|
+
|
|
7
|
+
**Quando usar:** edit de música (sync de letra) para Reels/Shorts, 30–40 s, 1:1 com abertura 9:16. Projeto-modelo: [`07-videos/projetos/2026-10-17-subzmotion-recriacao/`](../projetos/2026-10-17-subzmotion-recriacao/README.md) (copie a pasta, troque música, letra, roteiro e interfaces). Pesquisa e números do autor: [`05-pesquisa/referencias/2026-10-17-subzmotion/`](../../05-pesquisa/referencias/2026-10-17-subzmotion/README.md).
|
|
8
|
+
|
|
9
|
+
## As partes
|
|
10
|
+
|
|
11
|
+
| Parte | O que cobre |
|
|
12
|
+
|---|---|
|
|
13
|
+
| [01 · Gramática e roteiro](tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md) | o que faz o estilo funcionar, escolher música e trecho, frase → interface, encaixes, letra pelos campos (o autor cola, o script distribui), personagens e fontes |
|
|
14
|
+
| [02 · Câmera](tutorial-lyric-edit-interfaces/02-camera.md) | o rig do autor (um null por movimento), os números medidos, como escrever a câmera de uma música nova e como medir a câmera de uma referência |
|
|
15
|
+
| [03 · Montagem no AE](tutorial-lyric-edit-interfaces/03-montagem-no-ae.md) | contrato das cenas, interfaces em HTML → PNG 3×, digitação, 4 agentes em cópias do AE, conferência contra o original, montagem final e card do Spotify |
|
|
16
|
+
| [04 · Render e conferência](tutorial-lyric-edit-interfaces/04-render-e-conferencia.md) | render leve por trechos, folha a 2 qps, comparação numérica da câmera, lado a lado, entrega |
|
|
17
|
+
| [05 · Abertura celular → mergulho](tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md) | como gravar a tela com o iPhone e os 6 scripts da transição (sincronia, rastreio, cenário fixo, montagem, junção) |
|
|
18
|
+
|
|
19
|
+
## Passos (versão curta)
|
|
20
|
+
|
|
21
|
+
1. **Música e trecho** (30–40 s) e **letra**: o autor cola a letra inteira em `ae/LETRA-COMPLETA.txt`; o Claude não escreve letra de música. Interface no idioma da música, sem traduzir.
|
|
22
|
+
2. **Roteiro de cenas**: cada frase (2–6 palavras) numa interface que ilustra a palavra; anote o "buraco" onde a cena anterior entra. Defina os campos de letra (`ae/letra.json`).
|
|
23
|
+
3. **`python -P ae/letra_automatica.py`**: transcreve o áudio, casa com a letra colada e põe cada frase no campo certo com o tempo de cada palavra (só imprime contagens).
|
|
24
|
+
4. **Personagens e imagens** pelo GPT Image 2 (`08-integracoes/codex-imagem.py`), comuns em `assets/comum/`. Pessoa real vira personagem nosso.
|
|
25
|
+
5. **Câmera**: lista de movimentos (um por palavra-chave/troca, ~33 quadros, ease 67/77%, sobrepostos, nunca parada) → `ae/camera-movimentos.jsx`. Copiando uma referência: `camera/integrar.py` → `ancorar.py` → `assar.py`.
|
|
26
|
+
6. **Cenas no AE**: uma comp 1080² por cena-mundo, tempo interno = tempo do vídeo, mundo filho do null CAM, encaixe provisório da vizinha. Em paralelo: um agente por grupo de cenas, cada um na sua cópia do AE (`BRIEF-AGENTES.md`).
|
|
27
|
+
7. **Conferir cada cena** com `ae_see` contra a referência (quadros por índice, cópia bt709), inclusive no meio dos movimentos.
|
|
28
|
+
8. **Montagem**: `ae/montagem.jsx` (troca os encaixes pelas cenas reais, obturador 90°, comp principal + música) e `ae/card-spotify.jsx` (card "tocando agora" creditando a música, eco da última frase por `audio/eco_cauda.py`).
|
|
29
|
+
9. **Render leve** por trechos (`ae_render_video` leve, pasta ASCII em `<PASTA_DE_PROJETOS>/`), um job pesado por vez.
|
|
30
|
+
10. **Conferência**: folha a 2 qps do vídeo inteiro; câmera original × nossa (`camera/medir_quadro_a_quadro.py` + `comparar.py`); lado a lado.
|
|
31
|
+
11. **Abertura**: o autor grava a tela tocando o edit com o iPhone; `transicao/` faz o mergulho (parte 05).
|
|
32
|
+
12. **Entrega** aqui na conversa; aprovado → Clube do Motion completo.
|
|
33
|
+
|
|
34
|
+
## Checklist de qualidade
|
|
35
|
+
|
|
36
|
+
- [ ] Nenhuma cena parada fora do último meio segundo; câmera segue a digitação para a direita.
|
|
37
|
+
- [ ] Cada troca é cena-dentro-de-cena (sem corte), com máscara que arredonda durante o recuo.
|
|
38
|
+
- [ ] Digitação letra a letra com cursor, começando até 0,35 s antes da palavra cantada.
|
|
39
|
+
- [ ] Borrão de movimento ligado, obturador 90° (180° borra demais).
|
|
40
|
+
- [ ] Interface e letra no idioma da música; letra colada pelo autor, nunca escrita pelo Claude.
|
|
41
|
+
- [ ] Nada de rosto de pessoa real conhecida; card do Spotify no fim creditando a música.
|
|
42
|
+
- [ ] Render conferido: sem preto, sem quadro repetido na emenda, −14 LUFS.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Atraso da imagem da tela em relação ao som (o monitor e a câmera atrasam a imagem): correlação do master contra a
|
|
2
|
+
gravação desentortada pelos cantos rastreados, para vários deslocamentos. O pico dá o OFF da imagem (off_video.txt).
|
|
3
|
+
Adaptado da Shelby ep. 2 para o master quadrado a 24 qps."""
|
|
4
|
+
import cv2, json, sys, numpy as np
|
|
5
|
+
import json as _json, os as _os
|
|
6
|
+
CFG = _json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "config.json"), encoding="utf-8"))
|
|
7
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
8
|
+
OFF = float(open("off.txt").read())
|
|
9
|
+
C = json.load(open("cantos.json")); ks = [k for k in sorted(C, key=float) if 1.7 <= float(k) <= 3.2 and C[k]["cc"] > 0.88][::2]
|
|
10
|
+
cel = cv2.VideoCapture(CFG["celular"]); mas = cv2.VideoCapture(CFG["master"])
|
|
11
|
+
QC = {}; i = 0
|
|
12
|
+
while i <= int(3.3 * 30):
|
|
13
|
+
ok, f = cel.read(); QC[i] = cv2.cvtColor(f, cv2.COLOR_BGR2GRAY); i += 1
|
|
14
|
+
QM = []
|
|
15
|
+
for _ in range(int(2.5 * 24)): ok, f = mas.read(); QM.append(cv2.cvtColor(f, cv2.COLOR_BGR2GRAY))
|
|
16
|
+
R = np.float32([[0, 0], [1080, 0], [1080, 1080], [0, 1080]])
|
|
17
|
+
melhor = None
|
|
18
|
+
for d in np.arange(-0.15, 0.151, 1 / 120):
|
|
19
|
+
s = []
|
|
20
|
+
for k in ks:
|
|
21
|
+
t = float(k); q = np.float32(C[k]["q"]); H = cv2.getPerspectiveTransform(q, R)
|
|
22
|
+
a = cv2.warpPerspective(QC[int(round(t * 30))], H, (1080, 1080)).astype(np.float32)
|
|
23
|
+
x = (t - OFF - d) * 24; j = int(np.floor(x)); u = x - j
|
|
24
|
+
b = (QM[max(0, j)] * (1 - u) + QM[min(len(QM) - 1, j + 1)] * u).astype(np.float32)
|
|
25
|
+
a, b = np.log1p(a), np.log1p(b); a -= a.mean(); b -= b.mean()
|
|
26
|
+
s.append((a * b).sum() / np.sqrt((a * a).sum() * (b * b).sum()))
|
|
27
|
+
m = float(np.mean(s))
|
|
28
|
+
if melhor is None or m > melhor[1]: melhor = (d, m)
|
|
29
|
+
print(f"atraso {d:+.4f} s corr {m:.4f}")
|
|
30
|
+
print("melhor:", round(melhor[0], 4), "s; OFF da imagem =", round(OFF + melhor[0], 4))
|
|
31
|
+
open("off_video.txt", "w").write(f"{OFF + melhor[0]:.4f}\n")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Movimento da mão pelo cenário fixo (luzes do PC, ícones e bordas do monitor, teclado), fora do quadrado do vídeo:
|
|
2
|
+
Lucas-Kanade em pontos do fundo + homografia por RANSAC, encadeada quadro a quadro. Junta com o ECC (cantos.json):
|
|
3
|
+
nos quadros em que o ECC trava firme (cc > 0.9) ele é a âncora; entre âncoras, o caminho vem do fundo e o erro
|
|
4
|
+
é distribuído no tempo. Saída: cantos_final.json (mesmo formato), já suavizado."""
|
|
5
|
+
import cv2, json, sys, numpy as np
|
|
6
|
+
import json as _json, os as _os
|
|
7
|
+
CFG = _json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "config.json"), encoding="utf-8"))
|
|
8
|
+
from scipy.signal import savgol_filter
|
|
9
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
10
|
+
C = json.load(open("cantos.json")); ks = sorted(C, key=float)
|
|
11
|
+
cel = cv2.VideoCapture(CFG["celular"]); FPS = 30.0; N = int(3.6 * FPS) + 1
|
|
12
|
+
G = []
|
|
13
|
+
for _ in range(N):
|
|
14
|
+
ok, f = cel.read(); G.append(cv2.cvtColor(cv2.resize(f, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA), cv2.COLOR_BGR2GRAY))
|
|
15
|
+
def mascara(i):
|
|
16
|
+
q = (np.array(C[ks[min(i, len(ks) - 1)]]["q"]) * 0.5).astype(np.int32)
|
|
17
|
+
m = np.full(G[0].shape, 255, np.uint8); cv2.fillConvexPoly(m, cv2.convexHull(q), 0)
|
|
18
|
+
return cv2.erode(m, np.ones((41, 41), np.uint8)) # longe da borda do vídeo (o conteúdo se mexe)
|
|
19
|
+
Hs = [np.eye(3)] # Hs[i]: quadro 0 -> quadro i (px de meia resolução)
|
|
20
|
+
for i in range(1, N):
|
|
21
|
+
p0 = cv2.goodFeaturesToTrack(G[i - 1], 400, 0.01, 8, mask=mascara(i - 1))
|
|
22
|
+
p1, st, _ = cv2.calcOpticalFlowPyrLK(G[i - 1], G[i], p0, None, winSize=(31, 31), maxLevel=4)
|
|
23
|
+
b = st.ravel() == 1
|
|
24
|
+
H, inl = cv2.findHomography(p0[b], p1[b], cv2.RANSAC, 2.0)
|
|
25
|
+
Hs.append(H @ Hs[-1])
|
|
26
|
+
def aplica(H, q): # q em px cheios
|
|
27
|
+
return (cv2.perspectiveTransform((np.float32(q) * 0.5).reshape(-1, 1, 2), H).reshape(-1, 2) / 0.5)
|
|
28
|
+
# âncoras do ECC: só enquanto a tela mostra o quadro 0 parado (antes do play). Depois do play o conteúdo recua (zoom) e
|
|
29
|
+
# um descompasso mínimo de tempo vira "mudança de escala" no ECC (o quadrado encolhia de 1.003 para 680 px com cc 0,98)
|
|
30
|
+
OFFV = float(open("off_video.txt").read())
|
|
31
|
+
anc = [i for i in range(N) if i / FPS < OFFV - 0.03 and C[ks[i]]["cc"] > 0.95]
|
|
32
|
+
print("âncoras ECC:", len(anc), "de", N, "| faixas:", anc[:3], "...", anc[-3:])
|
|
33
|
+
Q0 = {i: np.array(C[ks[i]]["q"]) for i in anc}
|
|
34
|
+
# caminho pelo fundo a partir de cada âncora; em cada quadro, média ponderada das previsões das âncoras vizinhas
|
|
35
|
+
out = {}
|
|
36
|
+
for i in range(N):
|
|
37
|
+
prev = max([a for a in anc if a <= i], default=None); prox = min([a for a in anc if a >= i], default=None)
|
|
38
|
+
cands = []
|
|
39
|
+
for a in (prev, prox):
|
|
40
|
+
if a is None: continue
|
|
41
|
+
Hrel = Hs[i] @ np.linalg.inv(Hs[a]) # quadro a -> quadro i pelo fundo
|
|
42
|
+
cands.append((abs(i - a), aplica(Hrel, Q0[a])))
|
|
43
|
+
if len(cands) == 2 and cands[0][0] + cands[1][0] > 0:
|
|
44
|
+
d0, q0 = cands[0]; d1, q1 = cands[1]; w = d1 / (d0 + d1); q = q0 * w + q1 * (1 - w)
|
|
45
|
+
else:
|
|
46
|
+
q = cands[0][1]
|
|
47
|
+
out[i] = q
|
|
48
|
+
T = np.array([out[i] for i in range(N)]) # (N,4,2)
|
|
49
|
+
Ts = savgol_filter(T, 9, 2, axis=0)
|
|
50
|
+
lado = np.linalg.norm(Ts[:, 1] - Ts[:, 0], axis=1)
|
|
51
|
+
print("lado do quadrado: min", lado.min().round(1), "max", lado.max().round(1))
|
|
52
|
+
# erro do fundo contra o ECC nas âncoras (deixando cada âncora de fora não dá aqui; mede só a coerência)
|
|
53
|
+
err = [np.abs(Ts[i] - Q0[i]).max() for i in anc]
|
|
54
|
+
print("diferença máx. do caminho alisado para as âncoras (px cheios): mediana", round(float(np.median(err)), 1), "p95", round(float(np.percentile(err, 95)), 1))
|
|
55
|
+
json.dump({f"{i / FPS:.4f}": {"q": Ts[i].round(2).tolist(), "cc": 1.0} for i in range(N)}, open("cantos_final.json", "w"), indent=0)
|
|
56
|
+
print("ok cantos_final.json", N, "quadros")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Junta a transição (quadros PNG a 24 qps, 1080×1920) com o resto do master (quadrado, com preto em cima e embaixo) e monta o áudio.
|
|
2
|
+
Adaptado da Shelby ep. 2. Áudio: o som do quarto (celular) até o vídeo começar a tocar no monitor, depois o mix limpo do
|
|
3
|
+
master no mesmo instante, um whoosh que cresce no mergulho e um golpe suave no pouso.
|
|
4
|
+
Saída: ../render/on-my-way-recriacao-v2-celular.mp4 (rodar dentro de transicao/)"""
|
|
5
|
+
import os, subprocess, sys, numpy as np, soundfile as sf, librosa, pyloudnorm as pyln
|
|
6
|
+
import json as _json, os as _os
|
|
7
|
+
CFG = _json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "config.json"), encoding="utf-8"))
|
|
8
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
9
|
+
AQUI = os.path.dirname(os.path.abspath(__file__)); RAIZ = os.path.dirname(AQUI)
|
|
10
|
+
OFF = float(open(os.path.join(AQUI, "off_video.txt")).read())
|
|
11
|
+
NQ, CRU, T_INI = open(os.path.join(AQUI, "pouso.txt")).read().split(); NQ, CRU, T_INI = int(NQ), int(CRU), float(T_INI)
|
|
12
|
+
FPS = CFG["fps"]; Z0 = OFF + 0.70; Z1 = Z0 + 1.1
|
|
13
|
+
SR = 48000; SFX = "<BIBLIOTECA>/public/biblioteca/sfx/nivelados/"
|
|
14
|
+
MASTER = os.path.join(AQUI, CFG["master"])
|
|
15
|
+
DUR_M = CFG["duracao_master"]
|
|
16
|
+
DUR = NQ / FPS + (DUR_M - CRU / FPS) # duração da saída
|
|
17
|
+
ns = int(DUR * SR); db = lambda g: 10 ** (g / 20)
|
|
18
|
+
o_m = OFF - T_INI # instante (na saída) em que o master começa a tocar
|
|
19
|
+
# celular: só até o vídeo começar (depois o monitor tocando a música daria eco com o mix limpo)
|
|
20
|
+
cel, _ = librosa.load(os.path.join(AQUI, "celular-48k.wav"), sr=SR, mono=True, offset=T_INI)
|
|
21
|
+
cel = np.vstack([cel, cel])[:, :ns]; env = np.interp(np.arange(cel.shape[1]) / SR, [0, o_m - 0.05, o_m + 0.06], [1, 1, 0]); cel *= env
|
|
22
|
+
mix, _ = librosa.load(os.path.join(AQUI, "master-audio.wav"), sr=SR, mono=False)
|
|
23
|
+
out = np.zeros((2, ns)); a = int(o_m * SR); out[:, a:a + mix.shape[1]] = mix[:, : ns - a]
|
|
24
|
+
m = pyln.Meter(SR)
|
|
25
|
+
gc = -26 - m.integrated_loudness(cel[:, : max(int(o_m * SR), SR // 2)].T) # o quarto bem baixo
|
|
26
|
+
out[:, : cel.shape[1]] += cel * db(min(gc, 6))
|
|
27
|
+
def poe(f, t, g):
|
|
28
|
+
y, _ = librosa.load(f, sr=SR, mono=False); y = np.atleast_2d(y); y = y if y.shape[0] == 2 else np.vstack([y, y])
|
|
29
|
+
i = int(t * SR) - int(np.argmax(np.abs(y).max(0))); y = y[:, max(0, -i):]; i = max(0, i)
|
|
30
|
+
out[:, i:i + y.shape[1]] += y[:, : ns - i] * db(g)
|
|
31
|
+
poe(SFX + "whoosh-longo-2.wav", (Z1 - T_INI) - 0.06, -21) # cresce no mergulho e estoura no pouso
|
|
32
|
+
poe(SFX + "impacto-1.wav", (Z1 - T_INI) + 0.01, -25) # golpe suave no pouso
|
|
33
|
+
pk = np.abs(out).max(); out *= min(1.0, 0.89 / pk)
|
|
34
|
+
sf.write(os.path.join(AQUI, "audio-v2.wav"), out.T, SR, subtype="PCM_24")
|
|
35
|
+
print(f"áudio: {m.integrated_loudness(out.T):.1f} LUFS, {DUR:.2f} s; quarto {gc:+.1f} dB; master entra em {o_m:.3f} s")
|
|
36
|
+
# vídeo: quadros da transição + master cru a partir do quadro CRU, com preto em cima e embaixo (emenda exata no quadro)
|
|
37
|
+
cmd = ["ffmpeg", "-v", "error", "-y", "-framerate", str(FPS), "-i", os.path.join(AQUI, "quadros", "q%05d.png"), "-i", MASTER, "-i", os.path.join(AQUI, "audio-v2.wav"),
|
|
38
|
+
"-filter_complex",
|
|
39
|
+
f"[0:v]scale=in_range=pc:out_color_matrix=bt709:out_range=tv:sws_flags=accurate_rnd+full_chroma_int,format=yuv420p,setsar=1[a];"
|
|
40
|
+
f"[1:v]trim=start_frame={CRU},setpts=PTS-STARTPTS,pad=1080:1920:0:420:black,format=yuv420p,setsar=1[b];[a][b]concat=n=2:v=1:a=0[v]",
|
|
41
|
+
"-map", "[v]", "-map", "2:a", "-c:v", "libx264", "-crf", "16", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709",
|
|
42
|
+
"-color_range", "tv", "-preset", "slow", "-r", str(FPS), "-c:a", "aac", "-b:a", "256k", "-shortest",
|
|
43
|
+
os.path.join(AQUI, CFG["saida"])]
|
|
44
|
+
subprocess.run(cmd, check=True)
|
|
45
|
+
print("ok", CFG["saida"])
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Transição "celular filmando a tela → mergulho no edit limpo", 24 qps, 1080×1920 (Reel), master quadrado no meio.
|
|
2
|
+
|
|
3
|
+
Mesmo efeito da Shelby ep. 2 (07-videos/projetos/2026-10-12-shelby-ep2-take-unico/transicao/montar.py), adaptado:
|
|
4
|
+
- o master é QUADRADO (1080×1080, 24 qps): no fim do mergulho ele enche a largura do Reel e o resto vira preto,
|
|
5
|
+
como os posts do autor aparecem no Instagram;
|
|
6
|
+
- a saída é a 24 qps (a do edit); a gravação (30 qps) é lida no tempo exato, misturando os dois quadros vizinhos;
|
|
7
|
+
- os cantos vêm de cantos_final.json (ECC enquanto a tela mostra o quadro parado + cenário fixo depois do play:
|
|
8
|
+
o ECC sozinho errava a escala durante o recuo do conteúdo).
|
|
9
|
+
Etapas, sem corte:
|
|
10
|
+
1. troca invisível (TS → TS+0,3 s): o conteúdo da tela vira o próprio master no mesmo instante, na perspectiva da tela,
|
|
11
|
+
com a cor casada com a da câmera do celular (curva por histograma);
|
|
12
|
+
2. mergulho (Z0 → Z1, 1,1 s, bezier 0.5,0,0.2,1, zoom em escala log): a câmera virtual entra na tela até o quadrado encher
|
|
13
|
+
a largura; o que está fora do quadrado escurece até o preto; a cor volta ao master puro a partir de k = 0,55;
|
|
14
|
+
desfoque de movimento de verdade (12 amostras, obturador 180°);
|
|
15
|
+
3. pouso: mola curta começando em 1 + tremor; depois o master cru (juntar.py).
|
|
16
|
+
Uso (dentro de transicao/): python -P montar.py → quadros/*.png
|
|
17
|
+
"""
|
|
18
|
+
import cv2, json, math, os, subprocess, sys, numpy as np
|
|
19
|
+
import json as _json, os as _os
|
|
20
|
+
CFG = _json.load(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "config.json"), encoding="utf-8"))
|
|
21
|
+
from scipy.signal import savgol_filter
|
|
22
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
23
|
+
AQUI = os.path.dirname(os.path.abspath(__file__)); RAIZ = os.path.dirname(AQUI)
|
|
24
|
+
OFF = float(open(os.path.join(AQUI, "off_video.txt")).read()) # tempo do master = tempo do celular − OFF
|
|
25
|
+
W, H, FPS = 1080, 1920, CFG["fps"]
|
|
26
|
+
MS = CFG["lado_master"] # lado do master (quadrado)
|
|
27
|
+
Y0 = (H - MS) // 2 # o quadrado fica de y = 420 a 1500 no Reel
|
|
28
|
+
T_INI = 0.70 # a saída começa aqui na gravação (o vídeo fica parado no quadro 0 até OFF ≈ 1,35 s)
|
|
29
|
+
TS = OFF - 0.45 # troca invisível ANTES do play, com o quadro 0 parado na tela (no meio do play o celular
|
|
30
|
+
# capta o fantasma de atualização do monitor e a mistura mostrava o texto duplicado)
|
|
31
|
+
Z0 = OFF + 0.70; Z1 = Z0 + 1.1 # mergulho de 1,1 s (o de 0,8 s some no conteúdo escuro)
|
|
32
|
+
AMOSTRAS = 12
|
|
33
|
+
MASTER = os.path.join(AQUI, CFG["master"])
|
|
34
|
+
OUT = os.path.join(AQUI, "quadros"); os.makedirs(OUT, exist_ok=True)
|
|
35
|
+
def grava(caminho, img): # cv2.imwrite falha em silêncio com acento no caminho (Windows)
|
|
36
|
+
ok_, buf = cv2.imencode(".png", img); assert ok_; open(caminho, "wb").write(buf.tobytes())
|
|
37
|
+
|
|
38
|
+
C = json.load(open(os.path.join(AQUI, "cantos_final.json")))
|
|
39
|
+
ks = sorted(C, key=float); tq = np.array([float(k) for k in ks]); Q = np.array([C[k]["q"] for k in ks])
|
|
40
|
+
def quad(t):
|
|
41
|
+
t = min(max(t, tq[0]), tq[-1])
|
|
42
|
+
return np.stack([[np.interp(t, tq, Q[:, i, j]) for j in range(2)] for i in range(4)]).astype(np.float32)
|
|
43
|
+
|
|
44
|
+
cel = cv2.VideoCapture(os.path.join(AQUI, CFG["celular"])); FPS_C = cel.get(cv2.CAP_PROP_FPS)
|
|
45
|
+
_QC = []
|
|
46
|
+
while len(_QC) < int((Z1 + 0.3) * FPS_C) + 2:
|
|
47
|
+
ok_, f = cel.read(); assert ok_; _QC.append(f)
|
|
48
|
+
def quadro_cel(t):
|
|
49
|
+
x = t * FPS_C; i = int(math.floor(x)); u = x - i
|
|
50
|
+
a = _QC[min(i, len(_QC) - 1)].astype(np.float32)
|
|
51
|
+
return a if u < 1e-3 else a * (1 - u) + _QC[min(i + 1, len(_QC) - 1)].astype(np.float32) * u
|
|
52
|
+
CONV = "accurate_rnd+full_chroma_int" # mesma conversão do juntar.py: sem degrau de brilho na emenda com o master cru
|
|
53
|
+
def _carrega_master(n):
|
|
54
|
+
r = subprocess.run(["ffmpeg", "-v", "error", "-i", MASTER, "-frames:v", str(n), "-vf",
|
|
55
|
+
f"scale=in_color_matrix=bt709:in_range=tv:out_range=pc:sws_flags={CONV},format=bgr24",
|
|
56
|
+
"-f", "rawvideo", "-"], capture_output=True, check=True).stdout
|
|
57
|
+
return np.frombuffer(r, np.uint8).reshape(-1, MS, MS, 3)
|
|
58
|
+
_MAS = _carrega_master(int((Z1 - OFF + 0.6) * FPS))
|
|
59
|
+
def quadro_mas(tm):
|
|
60
|
+
return _MAS[min(max(0, int(round(tm * FPS))), len(_MAS) - 1)].copy()
|
|
61
|
+
def master_reel(tm):
|
|
62
|
+
"""o master no quadro do Reel: quadrado no meio, preto em cima e embaixo"""
|
|
63
|
+
o = np.zeros((H, W, 3), np.uint8); o[Y0:Y0 + MS] = quadro_mas(tm); return o
|
|
64
|
+
|
|
65
|
+
CEL_H, CEL_W = _QC[0].shape[:2]
|
|
66
|
+
FULL = np.float32([[0, 0], [CEL_W, 0], [CEL_W, CEL_H], [0, CEL_H]])
|
|
67
|
+
MRECT = np.float32([[0, 0], [MS, 0], [MS, MS], [0, MS]])
|
|
68
|
+
REEL_NO_MASTER = np.float32([[0, -Y0], [MS, -Y0], [MS, MS + Y0], [0, MS + Y0]]) # o quadro do Reel em px do master
|
|
69
|
+
SAIDA = np.float32([[0, 0], [W, 0], [W, H], [0, H]])
|
|
70
|
+
seg = lambda t, a, b: min(1.0, max(0.0, (t - a) / (b - a)))
|
|
71
|
+
smooth = lambda u: u * u * (3 - 2 * u)
|
|
72
|
+
def bezier(x1, y1, x2, y2):
|
|
73
|
+
def f(u):
|
|
74
|
+
if u <= 0: return 0.0
|
|
75
|
+
if u >= 1: return 1.0
|
|
76
|
+
lo, hi = 0.0, 1.0
|
|
77
|
+
for _ in range(40):
|
|
78
|
+
s = (lo + hi) / 2; x = 3 * (1 - s) ** 2 * s * x1 + 3 * (1 - s) * s * s * x2 + s ** 3
|
|
79
|
+
lo, hi = (s, hi) if x < u else (lo, s)
|
|
80
|
+
s = (lo + hi) / 2; return 3 * (1 - s) ** 2 * s * y1 + 3 * (1 - s) * s * s * y2 + s ** 3
|
|
81
|
+
return f
|
|
82
|
+
CURVA = bezier(0.5, 0.0, 0.2, 1.0)
|
|
83
|
+
|
|
84
|
+
QS = np.linspace(0, 100, 257)
|
|
85
|
+
def curvas():
|
|
86
|
+
"""curva de cor por canal (master → como a câmera do celular vê a tela), por histograma em vários quadros"""
|
|
87
|
+
pc, pm = [[], [], []], [[], [], []]
|
|
88
|
+
for tt in np.linspace(TS, Z0, 5):
|
|
89
|
+
f = quadro_cel(tt).astype(np.uint8); qq = quad(tt); Hm = cv2.getPerspectiveTransform(MRECT, qq)
|
|
90
|
+
mw = cv2.warpPerspective(quadro_mas(tt - OFF), Hm, (CEL_W, CEL_H), flags=cv2.INTER_AREA)
|
|
91
|
+
mk = np.zeros((CEL_H, CEL_W), np.uint8); cv2.fillConvexPoly(mk, qq.astype(np.int32), 255); mk = cv2.erode(mk, np.ones((25, 25), np.uint8))
|
|
92
|
+
for c in range(3): pc[c].append(f[..., c][mk > 0]); pm[c].append(mw[..., c][mk > 0])
|
|
93
|
+
L = []
|
|
94
|
+
for c in range(3):
|
|
95
|
+
a, b = np.percentile(np.concatenate(pm[c]), QS), np.percentile(np.concatenate(pc[c]), QS)
|
|
96
|
+
a = np.maximum.accumulate(a + np.arange(len(a)) * 1e-6)
|
|
97
|
+
L.append(np.interp(np.arange(256), a, b).astype(np.float32))
|
|
98
|
+
return np.stack(L, 1)
|
|
99
|
+
LUT = curvas()
|
|
100
|
+
print("OFF", OFF, "curva de cor (master → celular) em 0/16/64/128/255:", LUT[[0, 16, 64, 128, 255]].round(1).tolist())
|
|
101
|
+
def cor_celular(m):
|
|
102
|
+
mi = np.clip(m, 0, 255).astype(np.uint8)
|
|
103
|
+
return np.stack([LUT[..., c][mi[..., c]] for c in range(3)], -1)
|
|
104
|
+
|
|
105
|
+
PE, ESFUMA = 16, 14.0 # extensão espelhada e esfumado (a tela é preta em volta do vídeo: extensão mínima)
|
|
106
|
+
_d = cv2.distanceTransform(np.pad(np.ones((MS + 2 * PE - 2, MS + 2 * PE - 2), np.uint8), 1), cv2.DIST_L2, 5)
|
|
107
|
+
MASCARA = np.clip(_d / ESFUMA, 0, 1).astype(np.float32); MASCARA = MASCARA * MASCARA * (3 - 2 * MASCARA)
|
|
108
|
+
def camada_master(t, cor_k, Hsaida, esc_m):
|
|
109
|
+
q = quad(t); Hm = cv2.getPerspectiveTransform(MRECT, q) @ np.float64([[1, 0, -PE], [0, 1, -PE], [0, 0, 1]])
|
|
110
|
+
m = cv2.copyMakeBorder(quadro_mas(t - OFF), PE, PE, PE, PE, cv2.BORDER_REFLECT_101).astype(np.float32)
|
|
111
|
+
if cor_k > 0: m = cor_celular(m) * cor_k + m * (1 - cor_k)
|
|
112
|
+
if esc_m < 0.999: m = cv2.GaussianBlur(m, (0, 0), 0.5 / esc_m - 0.5)
|
|
113
|
+
Ht = Hsaida @ Hm
|
|
114
|
+
mw = cv2.warpPerspective(np.clip(m, 0, 255), Ht, (W, H), flags=cv2.INTER_CUBIC)
|
|
115
|
+
al = cv2.warpPerspective(MASCARA, Ht, (W, H), flags=cv2.INTER_LINEAR)[..., None]
|
|
116
|
+
return mw, al
|
|
117
|
+
def vista(t, k):
|
|
118
|
+
"""cantos (no quadro do celular) do que a câmera virtual enxerga: do quadro inteiro (k=0) até o Reel em volta do
|
|
119
|
+
quadrado (k=1: o quadrado enche a largura); zoom em escala log"""
|
|
120
|
+
q = quad(t); Hq = cv2.getPerspectiveTransform(MRECT, q)
|
|
121
|
+
V1 = cv2.perspectiveTransform(REEL_NO_MASTER.reshape(-1, 1, 2), Hq).reshape(-1, 2)
|
|
122
|
+
S = CEL_W / np.linalg.norm(V1[1] - V1[0])
|
|
123
|
+
sc_ = math.exp(math.log(S) * k); w = (1 - 1 / sc_) / (1 - 1 / S)
|
|
124
|
+
return FULL + (V1 - FULL) * w, Hq
|
|
125
|
+
|
|
126
|
+
def render(t_saida):
|
|
127
|
+
acc = np.zeros((H, W, 3), np.float32)
|
|
128
|
+
n = AMOSTRAS if Z0 - 0.03 < t_saida < Z1 + 0.03 else 1
|
|
129
|
+
for j in range(n):
|
|
130
|
+
t = t_saida + ((j + 0.5) / n - 0.5) * (0.5 / FPS) if n > 1 else t_saida # obturador 180°
|
|
131
|
+
k = CURVA(seg(t, Z0, Z1))
|
|
132
|
+
cor_k = 1 - smooth(seg(k, 0.55, 1.0))
|
|
133
|
+
if k >= 1:
|
|
134
|
+
acc += master_reel(t - OFF).astype(np.float32); continue
|
|
135
|
+
V, Hq = vista(t, k); Hv = cv2.getPerspectiveTransform(V, SAIDA)
|
|
136
|
+
esc = W / np.linalg.norm(V[1] - V[0])
|
|
137
|
+
f = quadro_cel(t)
|
|
138
|
+
if esc < 1: f = cv2.GaussianBlur(f, (0, 0), 0.5 / esc - 0.5 + 1e-3)
|
|
139
|
+
im = cv2.warpPerspective(f, Hv, (W, H), flags=cv2.INTER_CUBIC)
|
|
140
|
+
a = smooth(seg(t, TS, TS + 0.3))
|
|
141
|
+
if a > 0:
|
|
142
|
+
q = quad(t); esc_m = esc * np.linalg.norm(q[1] - q[0]) / MS
|
|
143
|
+
mw, al = camada_master(t, cor_k, Hv, esc_m)
|
|
144
|
+
im = im * (1 - al * a) + mw * al * a
|
|
145
|
+
# fora do quadrado escurece até o preto das faixas do Reel (no fim do mergulho só sobra o quadrado)
|
|
146
|
+
e = smooth(seg(k, 0.25, 1.0))
|
|
147
|
+
if e > 0:
|
|
148
|
+
qs = cv2.perspectiveTransform(MRECT.reshape(-1, 1, 2), Hv @ Hq).reshape(-1, 2)
|
|
149
|
+
mk = np.zeros((H, W), np.float32); cv2.fillConvexPoly(mk, qs.astype(np.int32), 1.0)
|
|
150
|
+
mk = cv2.GaussianBlur(mk, (0, 0), 6)[..., None]
|
|
151
|
+
im = im * (mk + (1 - mk) * (1 - e))
|
|
152
|
+
b = math.sin(math.pi * k) ** 2
|
|
153
|
+
if b > 0.02: im = im + 0.10 * b * cv2.GaussianBlur(im, (0, 0), 25)
|
|
154
|
+
acc += im
|
|
155
|
+
return np.clip(acc / n, 0, 255).astype(np.uint8)
|
|
156
|
+
|
|
157
|
+
I0 = int(round(T_INI * FPS)); N = int(round(Z1 * FPS)) + 1
|
|
158
|
+
if os.environ.get("SO"): # prova: SO=t1,t2 (segundos da gravação) → prova/p_<t>.png
|
|
159
|
+
for tt in map(float, os.environ["SO"].split(",")): grava(os.path.join(AQUI, "prova", f"p_{tt:.3f}.png"), render(tt))
|
|
160
|
+
sys.exit()
|
|
161
|
+
for f_ in os.listdir(OUT): os.remove(os.path.join(OUT, f_))
|
|
162
|
+
for i in range(I0, N):
|
|
163
|
+
grava(os.path.join(OUT, f"q{i - I0:05d}.png"), render(i / FPS))
|
|
164
|
+
if (i - I0) % 12 == 0: print("quadro", i - I0, "de", N - I0)
|
|
165
|
+
POUSO = 7 # 0,3 s a 24 qps
|
|
166
|
+
# grade de quadros: saída e master estão a 24 qps, então o quadro do master avança exatamente 1 por quadro de saída
|
|
167
|
+
m_ult = int(round(((N - 1) / FPS - OFF) * FPS)) # quadro do master no último quadro do mergulho
|
|
168
|
+
for j in range(POUSO):
|
|
169
|
+
u = (j + 1) / FPS
|
|
170
|
+
s = 1 + 0.03 * math.exp(-u * 9) * math.sin(u * 18) # mola começando em 1
|
|
171
|
+
dx, dy = 4 * math.exp(-u * 12) * math.sin(u * 45), 3 * math.exp(-u * 12) * math.sin(u * 37)
|
|
172
|
+
M = cv2.getRotationMatrix2D((W / 2, H / 2), 0, s); M[0, 2] += dx; M[1, 2] += dy
|
|
173
|
+
grava(os.path.join(OUT, f"q{N - I0 + j:05d}.png"), cv2.warpAffine(master_reel((m_ult + j + 1) / FPS), M, (W, H), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT))
|
|
174
|
+
nq = N - I0 + POUSO
|
|
175
|
+
cru = m_ult + POUSO + 1 # 1º quadro do master cru (sem repetir o último do pouso)
|
|
176
|
+
open(os.path.join(AQUI, "pouso.txt"), "w").write(f"{nq} {cru} {T_INI:.4f}" + chr(10))
|
|
177
|
+
print("ok:", nq, "quadros de transição; master cru a partir do quadro", cru, f"({cru / FPS:.4f} s)")
|