site-reels · diff

git:20261028.b3b29d6 to git:20261028.a3c506e

194 added, 172 removed. Audit A to A.

---
name: site-reels
- description: 'Grava as melhores cenas de um site/app web (hero WebGL, 3D, scroll, wordmark) em MP4 1080p pra deck e apresentação, sobe como slides ocultos no Figma Slides, e monta showreel de marca ou de produto (lançamento de app) em Remotion. Use em "gerar vídeo do site", "gravar as cenas do app", "render mp4 pro figma", "vídeo pro deck", "animação pro slide", "showreel", "vídeo de lançamento", "reel do produto", /site-reels.'
+ description: 'Records the best scenes of a website or web app (WebGL hero, 3D, scroll) as 1080p MP4 for decks, uploads them to Figma Slides, and builds brand or product showreels in Remotion. Use on "record the site scenes", "site video for the deck", "showreel", "launch video", "gerar vídeo do site", "vídeo pro deck", "reel do produto", /site-reels.'
---
- # Site Reels — site → MP4 → deck
+ # Site Reels: site → MP4 → deck
- Grava cenas roteirizadas de um site rodando local e entrega MP4 prontos pro Figma
- (1920×1080, 30fps, x264, < 100MB). Motor validado: **puppeteer-core +
- puppeteer-screen-recorder** (screencast CDP + ffmpeg). Nada de gravar tela.
+ ## Requirements
- ## Rodar
+ | Needed for | What | Check |
+ |---|---|---|
+ | everything | Windows, macOS or Linux with a GPU and a visible desktop (Chrome runs headful) | n/a |
+ | everything | Node.js 18+ | `node -v` |
+ | everything | ffmpeg + ffprobe on PATH (or `FFMPEG=<full path>`) | `ffmpeg -version` |
+ | everything | Chrome, Chromium or Edge (auto-detected, see below) | `node -e "import('./scripts/find-chrome.mjs').then(m=>console.log(m.findChrome()))"` |
+ | `record.mjs` | `puppeteer-core` + `puppeteer-screen-recorder` (npm, in the project) | `npm ls puppeteer-core puppeteer-screen-recorder` |
+ | `snap.mjs` | `puppeteer-core` + `timesnap` (brings `timeweb`) | `npm ls timesnap` |
+ | showreel | Remotion project | `npx remotion --version` |
+ | audits (optional) | ImageMagick | `magick -version` |
+ | Figma upload | Figma MCP (`use_figma`, `upload_assets`) + skills `figma-use`, `figma-use-slides` | MCP tool list |
+ **Chrome detection** (`scripts/find-chrome.mjs`, used by `record.mjs` and `snap.mjs`): `CHROME`
+ env var if set; else installed Chrome/Edge (Windows: Program Files and LocalAppData; macOS:
+ `/Applications` and `~/Applications`; Linux: `google-chrome`, `chromium`, `microsoft-edge` on PATH);
+ else the newest `ms-playwright/chromium-<N>` (or `PLAYWRIGHT_BROWSERS_PATH`); else it stops and
+ tells you how to set `CHROME` or run `npx playwright install chromium`. On Windows it adds
+ `--use-angle=d3d11`; on other OSes Chrome keeps its default GPU backend.
+
+ ## What it does
+
+ Records scripted scenes of a site running locally and delivers MP4s ready for Figma (1920×1080,
+ 30fps, x264, < 100MB). Validated engine: **puppeteer-core + puppeteer-screen-recorder** (CDP
+ screencast + ffmpeg). Never screen-record.
+
+ ## Run
+
```
mkdir <proj>-reels && cd <proj>-reels && npm init -y && npm i puppeteer-core puppeteer-screen-recorder
- cp ~/.claude/skills/site-reels/scripts/scenes.example.mjs scenes.mjs # edite as cenas
- # no repo do site: npx next build && npx next start -p 3100 (produção: WebGL mais liso)
- OUT="<pasta no Drive>/Videos" node ~/.claude/skills/site-reels/scripts/record.mjs scenes.mjs [cena ...]
+ mkdir -p scripts && cp ~/.claude/skills/site-reels/scripts/{record,find-chrome}.mjs scripts/
+ cp ~/.claude/skills/site-reels/scripts/scenes.example.mjs scenes.mjs # edit the scenes
+ # in the site repo: npx next build && npx next start -p 3100 (production build: smoother WebGL)
+ OUT="<output folder>/Videos" node scripts/record.mjs scenes.mjs [scene ...]
```
- Cada cena = `{ name, path, theme?, trim?, crf?, css?, prepare(page, ctx), act(page, ctx) }`.
- `prepare` roda antes de gravar (rolar até a seção, esperar 3D carregar); `act` é o que
- vai pro vídeo. `ctx` tem `wait`, `scrollTo(page, y|seletor, ms)` e
- `scrollThrough(page, seletor|null, ms, de, até)` com easing. Intro que roda no load:
- faça `page.reload()` dentro do `act` e use `trim` pra cortar o carregamento.
+ Run the scripts **copied into the project**: ESM resolves `puppeteer-core` from the script's
+ folder, not from the cwd.
- Roteiro que funciona pra marca: logo/hero intro (light e dark), troca de mídia da
- hero, seção 3D com a câmera entrando pelo scroll, rota imersiva (caminhada), wordmark
- do rodapé e um percurso completo da página. 5–7 cenas de 12–55s.
+ Env: `BASE` (default `http://localhost:3100`), `OUT`, `CHROME`, `FFMPEG`, `W`, `H`, `DPR`,
+ `MOBILE=1` (touch emulation), `KEEP_UI=1` (keeps the nav), `MIN_FPS` (24). Phone 4:5:
+ `W=360 H=450 DPR=2 MOBILE=1` gives 720x900.
- ## Auditar antes de entregar (não confie no exit code)
+ Each scene = `{ name, path, theme?, trim?, crf?, css?, prepare(page, ctx), act(page, ctx) }`.
+ `prepare` runs before recording (scroll to the section, wait for 3D to load); `act` is what goes
+ into the video. `ctx` has `wait`, `scrollTo(page, y|selector, ms)` and
+ `scrollThrough(page, selector|null, ms, from, to)` with easing. Intro that runs on load:
+ `page.reload()` inside `act` and use `trim` to cut the loading.
- - Folha de contato por vídeo: `ffmpeg -i x.mp4 -vf "fps=6/<dur>,scale=480:-1,tile=3x2" -frames:v 1 sheet.png` e **olhe**.
- - Arquivo de 1MB pra 19s = cena em branco. Arquivo > 100MB = não entra no Figma (o `crf` da cena resolve; 24 pra cena com grão + rastro).
- - Faixa do topo (`crop=1920:120:0:0`) em vários tempos pra caçar nav vazando.
+ Brand script that works: logo/hero intro (light and dark), hero media swap, 3D section with the
+ camera entering on scroll, immersive route (walk), footer wordmark and one full page pass.
+ 5–7 scenes of 12–55s.
- ## Armadilhas já pagas
+ ## Audit before delivering (do not trust the exit code)
- 1. **Janela oculta congela o rAF** → quadros parados. O launch já passa
+ - Contact sheet per video: `ffmpeg -i x.mp4 -vf "fps=6/<dur>,scale=480:-1,tile=3x2" -frames:v 1 sheet.png` and **look**.
+ - 1MB for 19s = blank scene. > 100MB = does not go into Figma (scene `crf` fixes it; 24 for a scene with grain + trails).
+ - Top strip (`crop=1920:120:0:0`) at several times to catch a leaking nav.
+ - `LOW-FPS` with a long pause at the start/end is a false alarm (the meter counts still frames); check the contact sheet.
+
+ ## Known pitfalls
+
+ 1. **Hidden window freezes rAF** → frozen frames. The launch already passes
`--disable-backgrounding-occluded-windows --disable-renderer-backgrounding
--disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion`.
- `headless: false` de propósito: headless cai no SwiftShader e o WebGL engasga.
- 2. **CSS de esconder UI some** com o reload e com a hidratação do React limpando o
- `<head>` → o script reanexa um `<style id="reels-hide">` no body a cada 100ms.
- 3. **Um seletor inválido derruba a regra inteira**: `\[` num template literal JS vira `[`
- e `.z-[60]` invalida `header, nav, .z-[60]`. Uma regra por seletor arriscado.
- 4. **Não esconda `[data-cursor]`**: sites marcam o canvas 3D e carrosséis com ele.
- Mire no elemento do cursor custom (fixed + mix-blend + pointer-events-none).
- 5. **3D carregado sob demanda** (IntersectionObserver + import dinâmico): espere
- 5–7s depois de chegar na seção. Ler o canvas WebGL pra checar volta vazio sem
- `preserveDrawingBuffer` — use espera fixa ou screenshot.
- 6. Screencast entrega ~32 quadros únicos/s → **grave em 30fps** (60 fica com
- quadro repetido irregular). `trim` padrão 0,3s tira o 1º quadro em cache.
- 7. **Screencast engasga com a máquina carregada** (Photoshop aberto, render em
- paralelo): o MP4 sai "30fps" com 9–18 quadros únicos/s e o usuário vê travado.
- O `record.mjs` mede (`mpdecimate=hi=64:lo=24:frac=0.05`) e regrava até 2x abaixo
- de `MIN_FPS` (24). Pra placa de WebGL/3D, prefira o `snap.mjs` (abaixo).
+ `headless: false` on purpose: headless falls back to SwiftShader and WebGL stutters.
+ 2. **Hide-UI CSS disappears** on reload and when React hydration clears `<head>` → the script
+ re-attaches a `<style id="reels-hide">` to the body every 100ms.
+ 3. **One invalid selector kills the whole rule**: `\[` in a JS template literal becomes `[` and
+ `.z-[60]` invalidates `header, nav, .z-[60]`. One rule per risky selector.
+ 4. **Do not hide `[data-cursor]`**: sites tag the 3D canvas and carousels with it. Target the
+ custom cursor element (fixed + mix-blend + pointer-events-none).
+ 5. **Lazy-loaded 3D** (IntersectionObserver + dynamic import): wait 5–7s after reaching the
+ section. Reading the WebGL canvas comes back empty without `preserveDrawingBuffer`: use a
+ fixed wait or a screenshot.
+ 6. Screencast delivers ~32 unique frames/s → **record at 30fps** (60 gives irregular repeated
+ frames). Default `trim` 0.3s drops the first cached frame.
+ 7. **Screencast stutters on a loaded machine** (Photoshop open, parallel render): the MP4 says
+ "30fps" with 9–18 unique frames/s. `record.mjs` measures (`mpdecimate=hi=64:lo=24:frac=0.05`)
+ and re-records up to 2x below `MIN_FPS` (24). For WebGL/3D plates prefer `snap.mjs`.
+ 8. Cookie banner: accept it in `prepare` (generic click on "Accept/Aceitar/OK") before recording.
+ 9. A tool page without scrolling (generator, effect editor) records still: the scene has to
+ **interact** (upload a file to `input[type=file]`, click presets by button text). A custom
+ slider does not respond to a synthetic `input`.
- ## Placas em 60fps cravado: `snap.mjs` (determinístico)
+ ## 60fps locked plates: `snap.mjs` (deterministic)
- `scripts/snap.mjs` injeta o **timeweb** (o relógio virtual do timesnap: rAF,
- performance.now, setTimeout e vídeo), avança 1/60s por quadro e fotografa. Não depende
- da carga da máquina. Cena: `{ name, path, theme, css, dur, wait (ms reais pra carregar 3D),
- pre (ms virtuais antes de rolar, 2000; **0 pra intro que roda no load**), start (s virtuais
- pulados), scroll: { sel, from, to, at, len } | { bottom: [de, até] em alturas de tela },
- frame(page, t) (ex.: mouse) }`. A rolagem é função do tempo virtual.
+ `scripts/snap.mjs` injects **timeweb** (timesnap's virtual clock: rAF, performance.now,
+ setTimeout and video), advances 1/60s per frame and screenshots. Independent of machine load.
+ Scene: `{ name, path, theme, css, dur, wait (real ms to load 3D), pre (virtual ms before
+ scrolling, 2000; **0 for an intro that runs on load**), start (virtual s skipped),
+ scroll: { sel, from, to, at, len } | { bottom: [from, to] in screen heights },
+ frame(page, t) (e.g. mouse) }`. Scrolling is a function of virtual time.
```
- npm i puppeteer-core timesnap && mkdir -p scripts && cp ~/.claude/skills/site-reels/scripts/patch-timeweb.mjs scripts/
+ npm i puppeteer-core timesnap && mkdir -p scripts
+ cp ~/.claude/skills/site-reels/scripts/{snap,find-chrome,patch-timeweb}.mjs scripts/
npm pkg set scripts.postinstall="node scripts/patch-timeweb.mjs" && npm run postinstall
- FPS=60 OUT=... node ~/.claude/skills/site-reels/scripts/snap.mjs scenes-snap.mjs [cena]
+ FPS=60 OUT=... node scripts/snap.mjs scenes-snap.mjs [scene]
```
- - **Não use o `timesnap()` em si**: com Next + R3F + Lenis ele grava a página em
- branco, e no hero WebGL deixa trechos congelados (rAF não roda). O laço próprio
- (goTo + scrollTo + screenshot) funcionou em tudo: 60/60 quadros únicos.
- - **Patch do timeweb obrigatório**: ele troca `video.play()` por versão sem Promise e
- sem evento `playing` → `video.play().catch()` estoura e a página fica em branco.
- - `preserveDrawingBuffer: true` forçado no getContext (desligue com `preserve: false`).
- - Quadros vão pro tmp, nunca pro Drive (milhares de jpg sincronizando).
- - **Nada de GPU em paralelo** (render Remotion, Photoshop): o WebGL perde o contexto
- e grava segundos só com o fundo. O script avisa `FLASH` (quadro piscando por
- luminância); trecho longo em branco só aparece na folha de contato. Olhe.
- - Auditoria de fluidez de verdade: `magick compare -metric MAE` entre quadros
- vizinhos em resolução cheia. 0 = quadro repetido.
- - Entrega: `entrega.sh` (no projeto de referência) gera reveals, loops (`loop.sh`) e
- 3D limpo subindo o crf de 3 em 3 até < 95MB.
+ - **Do not use `timesnap()` itself**: with Next + R3F + Lenis it recorded a blank page and left
+ frozen stretches on the WebGL hero (rAF does not run). The own loop (goTo + scrollTo +
+ screenshot) worked everywhere: 60/60 unique frames.
+ - **timeweb patch is mandatory**: it swaps `video.play()` for a version with no Promise and no
+ `playing` event → `video.play().catch()` throws and the page goes blank.
+ - `preserveDrawingBuffer: true` forced on getContext (turn off with `preserve: false`).
+ - Frames go to the OS temp dir, never to a synced drive (thousands of jpgs syncing).
+ - **No GPU work in parallel** (Remotion render, Photoshop): WebGL loses its context and records
+ seconds of background only. The script warns `FLASH` (frame flashing by luminance); a long blank
+ stretch only shows on the contact sheet. Look.
+ - Real smoothness audit: `magick compare -metric MAE` between neighbor frames at full resolution.
+ 0 = repeated frame.
+ - Delivery: `scripts/entrega.example.sh` (copy and rename the plates; `V=` required) builds
+ reveals, loops (`loop.sh`) and clean 3D, raising crf by 3 until < 95MB.
- ## Subir no Figma Slides (oculto, sem atrapalhar o time)
+ ## Upload to Figma Slides (hidden, without disturbing the team)
- O MP4 **não entra por MCP**: o `use_figma` não tem rede e o Plugin API não cria
- vídeo por bytes; o `upload_assets` só aceita imagem (≤ 10MB). Então:
+ The MP4 **does not go in through MCP**: `use_figma` has no network and the Plugin API does not
+ create video from bytes; `upload_assets` only accepts images (≤ 10MB). So:
- 1. Capa de cada vídeo: `ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg`.
- 2. `use_figma` (skills `figma-use` + `figma-use-slides`): `createSlideRow(idx)` antes
- do slide final, `row.name = 'WIP · Motion (oculto)'`, um `createSlide(idx, i)` por
- vídeo com `isSkippedSlide = true`, fundo preto, retângulo 1920×1080 chamado
- `VIDEO · x.mp4` e a instrução nas `speakerNotes`.
- 3. `upload_assets` com `nodeIds` dos retângulos → POST multipart das capas.
- 4. O usuário arrasta o MP4 do Drive em cima do retângulo (troca a capa pelo vídeo).
- Diga isso claramente; não prometa vídeo inserido.
+ 1. Cover of each video: `ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg`.
+ 2. `use_figma` (skills `figma-use` + `figma-use-slides`): `createSlideRow(idx)` before the final
+ slide, `row.name = 'WIP · Motion (hidden)'`, one `createSlide(idx, i)` per video with
+ `isSkippedSlide = true`, black background, a 1920×1080 rectangle named `VIDEO · x.mp4` and the
+ instruction in `speakerNotes`.
+ 3. `upload_assets` with the rectangles' `nodeIds` → multipart POST of the covers.
+ 4. The user drags the MP4 onto the rectangle (replaces the cover with the video). Say this
+ clearly; never promise an inserted video.
- `getSlideGrid()` omite slides pulados: confira a estrutura por
- `SLIDE_GRID.children[i].children`.
+ `getSlideGrid()` omits skipped slides: check the structure via `SLIDE_GRID.children[i].children`.
- Origem: cliente Y (out/2026), 7 vídeos aprovados; projeto de referência em `D:\dev\marca-reels`.
+ ## Brand showreel (Remotion): plates + designed scenes
- ## Brand showreel (Remotion) — placas + cenas desenhadas
+ For a branding showreel (logo, colors, type, layouts, manifesto, mockups), record only **clean
+ plates** from the site (no text: per-scene CSS hides title/nav) and compose the rest in
+ **Remotion** (frame-by-frame render, no jitter). Reusable building blocks worth having in
+ `src/fx.tsx`: `Mist`, `MistWords`, `Grain`, `KenBurns`, `Plate`, a `nevoa()` (mist) transition,
+ with brand data in `brand.ts`.
- Quando pedirem showreel de branding (logo, cores, tipografia, layouts, manifesto,
- mockups), grave só **placas limpas** do site (sem texto: CSS por cena esconde
- título/nav) e componha o resto em **Remotion** (render quadro a quadro, sem tremido).
- Referência pronta: `D:\dev\marca-showreel` (copie `src/fx.tsx` — `Mist`,
- `MistWords`, `Grain`, `KenBurns`, `Plate`, transição `nevoa()` — e troque `brand.ts`).
+ - **Visual identity comes from the deck, not the site**: check official type and palette in the
+ slides (sites drift from the brand book).
+ - Scenes that work: opening phrase in mist, recorded reveal, logo construction (`evolvePath` +
+ guides measured on the logo raster), manifesto phrase by phrase, pillars with variable `wght`
+ axis, palette in bands, typography, layered graphics, 3D with title in
+ `mix-blend-difference`, scrolling layout columns, 1/2/3-up mockup montage, product synced to
+ the name, closing on the wordmark.
+ - Review by **stills** (`npx remotion still ... --frame=N` + montage) before rendering.
+ - Render with `--concurrency=3`: at 8, with Photoshop open, a 32GB machine ran out of memory.
+ - Loops: `scripts/loop.sh in out [start] [dur] [crossfade]`. Check the seam with
+ `magick compare -metric RMSE` between first and last frame (< 0.5% = invisible).
+ - Bad plate is a discard: a solid of revolution spinning shows no motion; a camera framed for a
+ portrait canvas looks small in 16:9.
+ - **Frantic reel** (approved rhythm): 25–35s, 60fps, `Series` with hard cuts on a 120 BPM grid
+ (30 frames), image bursts every 3–6 frames, giant logo crop over a grid, flashing font weights,
+ palette blocks rearranging, portrait in a circular mask, graphic burst, kinetic phrase, one-beat
+ mockups, closing logo held ~2.5s. **Zero labels**: text only as graphics. A slow 1:40
+ showreel was rejected.
+ - **Soft/smoky reel**: record the hero smoke **with no logo or photo**, black on pure white
+ (scene with `storage` = params in localStorage and `block: ['assets/logo.png']`; the blank PNG
+ must be transparent; **do not block the video**: a broken video freezes timeweb). Use the plate
+ as a `mix-blend-mode: multiply` overlay: opens at the start of the scene, an inverted version
+ (`ffmpeg -vf reverse`) closes it; the cut lands on black. Transition at ~1.1s (rate 2), or
+ half the scene goes black.
+ - **Figma Slides layouts assembling**: clone the slide (skipped), duplicate each card 3x with only
+ background / logo / text visible, download each node with `download_assets` (scale 2) and delete
+ the clones. Slides **does not export transparency**: export logo/text with the slide black and
+ then white and extract alpha by difference (ImageMagick). Never return base64 through
+ `use_figma` (blows the context).
- - **IDV vem do deck, não do site**: confira tipografia e paleta oficiais nos slides
- (o site da cliente Y usava Mozilla Text; a IDV é Zalando Sans).
- - Cenas que funcionam: abertura com frase em névoa, reveal gravado, construção do
- logo (`evolvePath` + guias medidas no raster do logo), manifesto frase a frase,
- pilares com eixo `wght` variável, paleta em faixas, tipografia, grafismo em
- camadas, 3D com título em `mix-blend-difference`, colunas de layouts correndo,
- montagem de mockups 1/2/3 quadros, produto sincronizado com o nome, fecho com wordmark.
- - Revise por **stills** (`npx remotion still ... --frame=N` + montage) antes de renderizar.
- - Render com `--concurrency=3`. Com 8, mais o Photoshop aberto, a máquina de 32GB
- ficou sem memória e o Claude Code matou a tarefa.
- - Loops: `scripts/loop.sh in out [inicio] [dur] [crossfade]`. Confira a emenda com
- `magick compare -metric RMSE` entre o 1º e o último quadro (< 0,5% = invisível).
- - Placa ruim é descarte: objeto de revolução girando não mostra movimento; câmera
- enquadrada pra canvas retrato fica pequena em 16:9.
- - **Reel frenético (o que o usuário aprovou como ritmo, ref. Arko® Brand Reel)**: 25–35s,
- 60fps, `Series` com corte seco em grade de 120 BPM (30 quadros), rajadas de imagem a
- cada 3–6 quadros, crop gigante do logo sobre grid, pesos de fonte piscando, blocos da
- paleta se rearranjando, retrato em máscara circular, burst de grafismo, frase cinética,
- mockups de 1 batida, fecho com logo parado ~2,5s. **Zero label**: texto só como grafismo.
- Referência: `D:\dev\marca-showreel\src\reel.tsx`. Showreel lento de 1:40 foi reprovado.
- - **Reel suave/esfumaçado (v2, cliente Y)**: grave a fumaça do hero **sem logo nem foto**, preto no
- branco puro (cena com `storage` = params no localStorage e `block: ['assets/logo.png']`; o PNG
- vazio tem que ser transparente; **não bloqueie o vídeo**: vídeo quebrado trava o timeweb). Use a
- placa como overlay `mix-blend-mode: multiply`: abre no começo da cena, versão invertida
- (`ffmpeg -vf reverse`) fecha no fim; o corte cai no preto. Transição a ~1,1s (rate 2), senão a
- cena vira metade preta. Referência: `D:\dev\marca-showreel\src\reel2.tsx`.
- - **Layouts do Figma Slides se montando**: clone o slide (pulado), duplique cada card 3x com só
- fundo / logo / texto visível, baixe cada nó por `download_assets` (scale 2) e apague os clones.
- Slides **não exporta transparência** (vem com o fundo do slide): exporte logo/texto com o slide
- preto e depois branco e tire o alfa por diferença (`layers-src/matte.mjs`, ImageMagick). Não
- devolva base64 pelo `use_figma` (estoura contexto).
+ ## Product showreel (real app filmed)
- ## Showreel de produto (app real filmado) — produto B, out/2026
+ Screens are filmed for real (Playwright + CDP screencast at 2x, steps
+ `goto/move/click/type/scroll/wait`, pointer trail saved as `<id>.cursor.json`) and the cursor is
+ redrawn in Remotion on top.
- Referência: `D:\dev\produto-showreel`. As telas são filmadas de verdade (`scripts/capture.mjs`:
- Playwright + screencast CDP em 2x, passos `goto/move/click/type/scroll/wait`, rastro do ponteiro em
- `public/clips/<id>.cursor.json`) e o cursor é redesenhado no Remotion por cima. Corte atual:
- `src/v6/Punch.tsx`; render + loudness: `pwsh -File scripts/render-fast.ps1 -Only <corte>`.
+ **Rejected, do not repeat:**
+ - **Gluing a designed reel to the real screen** (illustrated opening + filmed middle + drawn CTA):
+ two languages, the character vanishes and returns, the black card between acts stops the rhythm.
+ One language per video.
+ - **Small, sped-up product screen** in a 16:9 window: feed and ranking become an unreadable wall.
+ - **Zoom that crops the screen** and **text stamped over the video** (giant verb, 01/07 counter,
+ gradient): intrusive.
+ - Empty waiting on screen ("waiting…"): cut straight to the action.
+ - Vague copy: it has to be obvious and in the audience's plain spoken language.
- **O que o usuário reprovou (não repita):**
- - **Colar o reel desenhado com a tela real** (abertura ilustrada + meio filmado + CTA desenhado):
- duas linguagens, o personagem some e volta, a cartela preta entre os atos para o ritmo. Uma
- linguagem por vídeo.
- - **Tela do produto pequena e acelerada** numa janela 16:9: o feed e o ranking viram um paredão
- ilegível.
- - **Zoom que corta a tela** (tela cheia com crop na ação) e **texto carimbado por cima** do vídeo
- (verbo gigante, contador 01/07, degradê): ficou intrusivo.
- - Espera vazia na tela ("waiting…", "cole isso no seu bot"): corta direto pro momento da ação.
- - Frase vaga ("Ele chega sozinho."): a copy tem que ser óbvia e brasileira ("Ele mesmo cria o perfil.").
+ **The form that works:** per action, **text alone first**, centered and short (2 beats), **then
+ the whole screen, uncropped**, in a centered window at the capture aspect, on a clean background.
+ Discreet text (weight 700, per-word entry with light blur), no counter, no gradient. Cuts on the
+ track grid (14 frames = 128.57 BPM). 40s max.
- **A forma que funciona:** por ação, **primeiro o texto sozinho**, centralizado e curto (2 batidas),
- **depois a tela inteira, sem corte**, numa janela centralizada no aspecto da captura, sobre fundo
- limpo. Texto discreto (peso 700, entrada por palavra com blur leve, ponto final em ciano), sem
- contador e sem degradê. Cortes na grade da trilha (14 quadros = 128,57 BPM). No máximo 40s.
+ **Technique that paid off:**
+ - Speeding a capture: `<OffthreadVideo trimBefore={from} playbackRate={rate}>` and recompute click
+ frames `/ rate` (sound and cursor ring).
+ - Starting another composition mid-way: `<Sequence from={X}><Sequence from={-start}><Comp/></Sequence></Sequence>`.
+ Check the first frame of the stretch: a shared element "flying" in shows up loose. Skip a few
+ frames and re-place the impact sound that fell before.
+ - Language per composition with a module-level timeline: a `globalThis` flag set by its own entry
+ file imported before the Root.
+ - Continuous track across cuts: `trimBefore` on the bed so it continues from where the previous
+ stretch stopped.
+ - Loudness: render, measure and normalize with 2-pass `loudnorm` (-14 LUFS / -1 dBTP), copying
+ the video. Measure only at the end: Remotion's raw mix goes over 0 dBFS.
+ - Audit: timestamped contact sheet (`fps=2,drawtext=%{pts\:hms},tile=8x6`) of every render, and
+ a dense sheet (fps=6) at every stretch junction.
+ - A project without git, edited by another session at the same time: new cut in a new file,
+ `*.bak-*` backup before editing a shared file, and tell the user.
- **Técnica que já se pagou:**
- - Acelerar captura: `<OffthreadVideo trimBefore={from} playbackRate={rate}>` e recalcular os
- quadros de clique `/ rate` (som e anel do cursor).
- - Começar outra composição no meio: `<Sequence from={X}><Sequence from={-inicio}><Comp/></Sequence></Sequence>`.
- Confira o 1º quadro do trecho: elemento compartilhado "voando" (o rosto vindo do painel anterior)
- aparece solto. Pule uns quadros e recoloque o som de impacto que ficou antes.
- - Idioma de composição com timeline no módulo: flag em `globalThis` setada por um entry próprio
- (`src/pt.ts` importa `pt-flag.ts` antes do Root), igual ao teaser.
- - Trilha contínua entre cortes: `trimBefore` no bed pra seguir do quadro onde o trecho anterior parou.
- - Loudness: renderiza, mede e normaliza com `loudnorm` em 2 passes (-14 LUFS / -1 dBTP), copiando o
- vídeo. Medir só no fim: o mix bruto do Remotion passa de 0 dBFS.
- - Auditoria: folha de contato com timestamp (`fps=2,drawtext=%{pts\:hms},tile=8x6`) de cada render,
- e uma folha densa (fps=6) em toda junção de trecho.
- - Link do Drive sem navegador logado: o id da pasta sai do banco do Drive para desktop
- (`%LOCALAPPDATA%\Google\DriveFS\<conta>\mirror_metadata_sqlite.db`, tabela `items`, coluna `id`,
- abra **read-only**). URL: `https://drive.google.com/drive/folders/<id>`.
- - Anexo no chat do OpenBot não aceita MP4: remuxe pra `.mov` (`-c copy`) e anexe da pasta do agente.
- - O projeto não tem git e outra sessão pode editar ao mesmo tempo: corte novo em arquivo novo,
- backup `*.bak-*` antes de editar arquivo compartilhado, e avise o usuário.
+ ## Portfolio cards and studio reel
- ## Cards de portfolio e reel de estúdio (cliente T, out/2026)
- Referência: `Shared\cliente-t\motion\` (README + tools) e `D:\dev\estudio-cliente\reels` (cenas) / `D:\dev\estudio-cliente\motion` (Remotion).
- - `record.mjs` agora aceita env `W`, `H`, `DPR`, `MOBILE=1` (toque) e `KEEP_UI=1` (mantém a nav: card mostra o site
- como ele é). Celular 4:5: `W=360 H=450 DPR=2 MOBILE=1` → 720x900. Rode o script **copiado pro projeto**: ESM resolve
- `puppeteer-core` a partir da pasta do script, não do cwd.
- - Cenas geradas do inventário (`portfolio.json`, só `publico:true` e com URL); sem URL viva = Ken Burns do screenshot.
- - Aceite o banner de cookies no `prepare` (clique genérico em "Aceitar/Accept/OK") antes de gravar.
- - Ferramenta sem rolagem (gerador, editor de efeito) grava parada: a cena tem que **interagir** (subir arquivo no
- `input[type=file]`, clicar os presets por texto do botão). Slider custom não responde a `input` sintético.
- - `LOW-FPS` com pausa longa no começo/fim é falso alarme (o medidor conta quadro parado); olhe a folha de contato.
- - Pacote do card: loop por crossfade do fim no começo, 2 passes de bitrate pelo teto em KB, WebM VP9 junto, poster
- WebP do 1º quadro. Script: `Shared\cliente-t\motion\tools\pack-cards.ps1`.
- - PowerShell: variável ignora caixa (`$d` sobrescreve `$D`) e `R` é alias do `Invoke-History`. Nomes únicos.
- - Logo com alpha pro site: `--codec=vp9 --pixel-format=yuva420p` (Safari não toca) + ProRes 4444 pra edição;
- `Config.setVideoImageFormat("png")` pra manter o alpha.
- - Estética recorte/stop-motion no Remotion: tempo quantizado em dois (24 fps, pose a cada 2 quadros), filtro SVG por
- pose (feTurbulence + feDisplacementMap na borda, feMorphology pra borda creme, sombra dura offset) com 3 seeds em
- ciclo ("boil"), textura de papel em multiply. Peças do mascote lidas por id do SVG do designer (DOMParser), então a
- versão nova do branding entra sem mexer no código.
- - Trilha CC0 que serviu: Free Music Archive, John Bartmann (álbum CC0). O MP3 sai do `fileUrl` no HTML da página da
- faixa. Andamento e trecho mais cheio com `librosa.beat.beat_track` + RMS por janela.
+ - `record.mjs` with `KEEP_UI=1` (the card shows the site as it is) and the phone preset above.
+ - Scenes generated from an inventory file (only public items with a live URL); no live URL =
+ Ken Burns of the screenshot.
+ - Card package: crossfade loop from end to start, 2-pass bitrate to a KB ceiling, VP9 WebM
+ alongside, WebP poster of frame 1.
+ - Logo with alpha for the web: `--codec=vp9 --pixel-format=yuva420p` (Safari does not play it) +
+ ProRes 4444 for editing; `Config.setVideoImageFormat("png")` keeps the alpha.
+ - Cut-out/stop-motion look in Remotion: time quantized on twos (24 fps, pose every 2 frames), SVG
+ filter per pose (feTurbulence + feDisplacementMap on the edge, feMorphology for a cream border,
+ hard offset shadow) with 3 seeds cycling ("boil"), paper texture in multiply. Mascot parts read
+ by id from the designer's SVG (DOMParser), so a new branding version drops in without code.
+ - CC0 music: Free Music Archive (CC0 albums). Tempo and fullest stretch with
+ `librosa.beat.beat_track` + windowed RMS (Python, optional).
+
+ Origin stories, reference projects and machine-specific tips: `references/cases.md`.