Communitygithub.com

yazelin/promo-video-skill

Pipeline de projeto para promoção com custo zero: captura de tela / Ken Burns / narração em terminal. Skill do Claude Code para criar anúncios de lançamento de projetos web sem gastar com APIs.

O que é promo-video-skill?

Esta skill do Claude Code, chamada `promo-video`, cria vídeos promocionais, trailers, anúncios para redes sociais e vídeos explicativos para qualquer repositório, sem custo algum. Ela suporta três modos principais de captura: MODE A (captura de tela) para aplicativos visuais como jogos ou sites 3D, MODE B (montagem Ken Burns) para galerias de imagens ou feeds, e MODE C (terminal animado) para ferramentas de linha de comando, bibliotecas ou backends. A skill também inclui um MODE E para criar vídeos verticais (9:16) estilo Reels/Shorts a partir de gravações ao vivo. O pipeline completo gera narração usando edge-tts, verifica o tom com Gemini, e compõe o vídeo final com marcas, legendas e CTAs usando ffmpeg, tudo através de templates reutilizáveis. É particularmente útil para desenvolvedores que desejam criar materiais de marketing de alta qualidade para seus projetos sem depender de serviços pagos de geração de vídeo. As lições documentadas (GOTCHAs) são um ativo valioso, cobrindo desde a sincronização de áudio e vídeo até a prevenção de problemas comuns de renderização, garantindo resultados profissionais.

Funciona com✓Claude Code✓Codex CLI~Cursor✓Gemini CLI
npx skills add yazelin/promo-video-skill

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação


name: promo-video description: Vídeo promocional / trailer / anúncio social / explicador com custo zero para qualquer repositório — um web app ou jogo cinematográfico (captura de tela), um feed de imagens/galeria (montagem Ken Burns), OU um CLI/biblioteca/backend não visual (explicador de terminal animado). Gera tráfego para um site, Telegram ou GitHub. Use quando o usuário quiser um trailer, promo, teaser, anúncio ou vídeo explicativo para um projeto — gera múltiplos formatos (16:9/9:16/1:1/4:5) com narração, legendas, marca e CTA. Tudo gratuito: captura de tela / Ken Burns / terminal-player + edge-tts + audição com Gemini + ffmpeg. Nenhuma API paga.

promo-video — Pipeline de Vídeo Promocional / Anúncio com Custo Zero

Transforme um projeto web em um vídeo promocional ou anúncio para redes sociais. Insight central: se o aplicativo faz uma encenação cinematográfica por conta própria, grave a tela, não "gere" o vídeo; se for um projeto de imagens, baixe imagens selecionadas e faça uma montagem Ken Burns. O restante (narração / verificação de áudio / composição / marca / CTA) é compartilhado por ambos. Comprovado na prática: battlefield-editor/tools/trailer (captura de tela) + anúncio do catime "O gato que lê o mundo" (Ken Burns + notícia → revelação de imagem) + jogatina completa dos sete capítulos de "Glitch e Sr. Buraco Negro" (variante com áudio da captura de tela, 60 minutos, 700 falas dubladas automaticamente: https://youtu.be/J9OMebCjr9Y ).

Árvore de Decisão: Primeiro, Escolha o Modo de Captura

  • Aplicativo em movimento (3D/canvas/encenação automática, com auto-play) → MODO A Captura de Tela (capture-app.mjs).
  • Saída de imagens / Galeria / Feed de fotos (imagens geradas por IA, galeria) → MODO B Montagem Ken Burns (baixar imagens selecionadas + kenburns.sh).
  • Precisa gravar o som emitido pelo próprio aplicativo (Variante com áudio do MODO A; o que tocar e por quanto tempo é decidido pelo aplicativo na hora, você não tem a trilha de áudio completa) → Descarte o recordVideo, use ffmpeg para capturar vídeo e áudio simultaneamente, consulte a seção "Quando for preciso gravar o som emitido pelo próprio aplicativo: gravação real". Se você tem a trilha de áudio completa, não siga este caminho; a multiplexação posterior oferece melhor qualidade de áudio (GOTCHA 24).
  • Repositório não visual (CLI / biblioteca / API / backend / pipeline de dados, sem tela para gravar) → MODO C Terminal/Explicador (term-player.html + capture-term.mjs): transforme "comandos + saída real" em uma gravação de terminal com animação de digitação. Ferramentas de desenvolvimento precisam de uma "versão com narração didática", não de um "trailer de cortes rápidos" (veja GOTCHA 22): estrutura = gancho do problema → passo a passo (instalar → executar → entender a saída → comandos avançados) + narração edge-tts explicando passo a passo + legendas dos passos → CTA. A sincronização da narração é feita pelo window.__STEPLOG do term-player.html (segundos em que a saída de cada etapa aparece); o compose usa adelay para atrasar cada fala até o passo correspondente. Na prática: vídeo didático de CLI de estado entre repositórios (~60s, música de fundo em 0.13 para dar destaque à narração).
  • Vídeos curtos com frases de efeito de gravações ao vivo (9:16 Reels/Shorts) → MODO E Gravação com Template (short-9x16.html + capture-short.mjs): O course-pipeline primeiro seleciona frases de efeito da transcrição, corta o trecho em 16:9; aqui, ele insere o trecho no template HTML 9:16 e grava. Por que usar HTML em vez de filtros ffmpeg: o corte o ffmpeg também consegue fazer, mas a animação de palavras-chave em CSS são dez linhas; fazer o mesmo efeito com legendas no formato ASS é um inferno; ajustar o layout também é questão de alterar o CSS e ver na hora, sem ter que ajustar cegamente um filtergraph. Linha divisória: o course-pipeline corta, o promo-video empacota; a interface é "um mp4 já cortado + timestamps de transcrição no eixo do tempo do produto final".
  • Híbrido: capture de tela como corpo principal, Ken Burns / Terminal como complemento, tudo é possível.

Quatro Estágios

  1. Capture — MODO A: execute xvfb-run com Chromium com cabeçalho (necessário para WebGL), Playwright recordVideo grava a parte de destaque da reprodução automática; MODO E vai sem cabeçalho (headless, veja GOTCHA 47). MODO B: busque a lista de fontes, selecione, baixe.
  2. Narration (opcional) — edge-tts zh-TW-YunJheNeural --rate=-5% gera a narração.
  3. Verify (opcional) — Gemini pro ouve a narração e verifica o tom; Gemini flash seleciona a trilha sonora.
  4. Compose — brand.py gera os assets da marca (cartão de texto / logotipo / CTA / QR), ffmpeg sobrepõe legendas / concatena / mixa o áudio, exporta em múltiplos formatos + versão condensada.

Os Seis Modelos Aqui (Basta Alterar a Configuração no Topo / Passar Parâmetros)

  • brand.py — PIL: compositor que mantém a proporção com cover/contain, cartão de texto, logotipo fixo, cartão CTA, mini galeria, QR (via API).
  • kenburns.sh — Ken Burns sem tremor com clip + borda de texto nas imagens + concat + mixagem de áudio (principal do MODO B).
  • capture-app.mjs — Playwright+xvfb para captura de tela (MODO A); servidor http embutido, oculta o HUD, clica em nós para pular trechos / controla jogos por teclado.
  • term-player.html + capture-term.mjs — MODO C Terminal/Explicador: player de terminal HTML (animação de digitação + saída colorida) + gravação com xvfb. Sem necessidade de asciinema/xterm.
  • short-9x16.html + capture-short.mjs — MODO E: template 9:16 (slides centralizados, legendas grandes em cima e embaixo, palavras-chave ampliadas e pulsantes, logotipo fixo, barra de progresso) + gravação com xvfb + multiplexação posterior do áudio original. O espaço do slide tem movimentação de câmera automática: mede onde estão as letras na imagem, muda a composição a cada 2-3 segundos, e quando a palavra-chave é falada, aproxima e ilumina (GOTCHAS 32-37). Para regravar, use --tag v2 para alterar o nome do arquivo, não sobrescreva o produto final antigo — o antigo é a linha de base para comparação. Após a gravação, ele imprime automaticamente "número de eventos de brilho/segundo" e "volume do produto final" para controle de qualidade, sem necessidade de verificação extra.
  • relevel.py — Para vídeos já entregues que precisam de ajuste de volume: mexe apenas no áudio, -c:v copy; testado, o hash md5 do stream de vídeo antes e depois é o mesmo; leva um segundo por vídeo, sem necessidade de regravar. Vídeos novos não precisam (o capture-short.mjs já ajusta na multiplexação).
  • audition.py — Gemini ouve: music "<uso>" *.mp3 (seleciona trilhas CC0 com nota de 0 a 10), tone "<texto>" v.mp3 (verifica o tom da narração).

Quando for preciso gravar o som emitido pelo próprio aplicativo: gravação real

O GOTCHA 24 trata do caso em que você tem a trilha de áudio completa (videoclipe musical, narração edge-tts): use o arquivo original e multiplexe depois, a qualidade de áudio é a melhor. Mas há uma situação em que isso não funciona — o que tocar e por quanto tempo é decidido pelo aplicativo na hora. Exemplo prático: a jogatina completa de uma visual novel, setecentos arquivos de dublagem tocados na ordem da história, não há nenhuma "trilha de áudio completa" para alinhar.

Nesse caso, descarte completamente o recordVideo do Playwright e use ffmpeg para capturar vídeo e áudio simultaneamente:

# 1. Abra um receptor de áudio independente, direcione o som do navegador para ele, sem misturar outros sons do desktop
MOD=$(pactl load-module module-null-sink sink_name=cap)
# 2. Sua própria tela X
Xvfb :77 -screen 0 1280x808x24 -nolisten tcp &
# 3. Grave vídeo + áudio juntos, escreva em segmentos (se falhar, perde apenas um segmento)
ffmpeg -f x11grab -draw_mouse 0 -framerate 25 -video_size 1280x808 -i :77.0 \
       -f pulse -i cap.monitor \
       -vf crop=1280:720:0:88 \
       -c:v libx264 -preset veryfast -crf 24 -pix_fmt yuv420p \
       -c:a aac -b:a 160k \
       -f segment -segment_time 600 -reset_timestamps 1 seg/part-%03d.mp4 &
# 4. Execute o navegador (DISPLAY=:77 PULSE_SINK=cap), quando terminar, concatene os segmentos

Cinco pontos que aprendemos na prática:

  1. --kiosk não funciona no Playwright, ele gerencia suas próprias janelas. A barra de guias + barra de endereço somam 88 px fixos, aumente a tela em 88 px e use crop para cortar. Esconda o cursor com -draw_mouse 0.
  2. Não use segundos fixos para virar a página. Cada fala tem duração diferente; esperar pouco corta o final da fala, esperar muito dobra a duração do vídeo. Intercepte new Audio(), e só avance para o próximo slide quando a dublagem terminar (esses elementos não estão no DOM, querySelector não os encontra).
  3. É essencial ter uma verificação de "o conteúdo está avançando". Sem ela, se encontrar um cartão que não pode ser clicado, o script clicará repetidamente até o limite, e o ffmpeg não reportará nenhum erro: na prática, gravou 58 minutos, o arquivo parece normal, mas no final percebeu-se que só percorreu um quarto do livro. O critério é "se o texto na tela mudou", e texto vazio também conta como não avançou — a tela final não tem texto, e essa condição foi omitida, resultando em mais 33 minutos de gravação.
  4. Grave em segmentos (-f segment); se falhar no meio, perde apenas um segmento; no final, concatene com -c copy sem perdas.
  5. Ao terminar, verifique quatro coisas, não apenas se o arquivo foi gerado: duração total, quantos cartões foram percorridos, desvio padrão de frames amostrados (próximo de 0 = tela vazia), volume médio com volumedetect (silêncio indica que o null sink não foi conectado, e novamente, sem erro reportado).

Prática real: glitch-vn/tools/capture/ (record.sh + play_through.mjs). O resultado é "Glitch e Sr. Buraco Negro" completo, sete capítulos, 60 minutos, 700 falas, executado do início ao fim sem ninguém apertando botão: https://youtu.be/J9OMebCjr9Y

GOTCHAS — Este é o Ativo Mais Valioso, Verifique Sempre

  1. Preservação de proporção: NUNCA use scale=W:H para imagens com proporções mistas (vai distorcer). Use cover para o fundo (ampliação proporcional e corte), contain para o assunto principal (escala proporcional e encaixe). O scale do ffmpeg só pode ser múltiplo inteiro (2x) ou adicionar force_original_aspect_ratio. Antes de finalizar, execute uma auditoria de "proporção original vs. proporção exibida" (erro < 0,1% para aprovar).

  2. Tremor no Ken Burns: Amplie a fonte em 2x antes de reduzir (os saltos de pixels inteiros tornam-se subpixels) + use número de quadro linear para o zoom: z='1+K*on/(D-1)', NUNCA use z='min(zoom+inc,...)' que acumula arredondamentos.

  3. Explosão de quadros no zoompan: Não use -loop 1 -t (cada quadro de entrada é multiplicado por d); para uma única imagem, use -frames:v D + zoompan d=D.

  4. Borda de texto nas imagens: Centralize verticalmente a imagem primeiro; posição do texto = centro ± (altura da imagem × zmax)/2 ± gap, calcule para cada imagem (o PIL gera a altura de cada imagem como sidecar). Assim, imagens horizontais não ficarão muito distantes nem serão comprimidas.

  5. Fetch via file:// bloqueado: O aplicativo usa fetch(?pkg=)/carrega JSON → execute python3 -m http.server para usar http (ou o servidor embutido do capture-app.mjs).

  6. Headless WebGL perde swiftshader/fica preto → use xvfb-run -a --server-args="-screen 0 WxHx24" para executar Chromium com cabeçalho (headed).

  7. Aplicativo com módulo ES: gotoPhase não está na janela → use DOM para clicar nos nós da linha do tempo #tlNodes .node[i].click(), não use evaluate(gotoPhase).

  8. QR: PEP668 impede a instalação do qrcode → use api.qrserver.com/v1/create-qr-code/?data=<link público> (apenas links públicos); antes de publicar, escaneie com o celular para confirmar.

  9. Palavras com leitura irregular no edge-tts: Contorne primeiro ("não" → use sinônimos); verifique o tom com Gemini pro perguntando sobre a direção do tom, o whisper STT não consegue verificar o tom (mesmo que pronunciado errado, ele transcreve para o mesmo caractere).

  10. Trilha sonora: Gemini flash seleciona CC0, certifique-se de que combina (não coloque trilha de guerra em um vídeo sobre gatos); anote a fonte CC0.

  11. Múltiplos formatos: 16:9=YouTube, 9:16=Reels/Shorts/LINE, 1:1 & 4:5=FB/IG feed. MODO A: regrave nativamente para cada formato (viewport retrato); MODO B: recomponha para cada formato.

  12. Logotipo fixo (marca d'água): Marca + URL no canto da tela durante todo o vídeo (não apenas no cartão final); sobreponha uma PNG transparente no vídeo inteiro.

  13. Anúncios devem gerar ação: Gancho nos primeiros 2 segundos (reprodução automática sem som, com imagem/texto grande já no primeiro quadro); legendas queimadas (para FB sem som); CTA com um motivo para "agir agora".

  14. Detalhes da plataforma: FB: vídeo vai na postagem, link vai no primeiro comentário (para evitar redução de alcance); use +movflags +faststart; LINE: para reprodução incorporada, use o álbum/botão de vídeo, não "arquivo".

  15. Captura de tela deve esperar o carregamento completo + verificar cada uma para ver se não está vazia: MODO A/galeria: captura de tela item por item NÃO use segundos fixos (o item mais lento pode ser capturado com a tela de "carregando" vazia). Faça polling até o indicador de loading desaparecer; após capturar, meça o desvio padrão da região alvo de cada uma (≈0 = vazia/não renderizou); detecte automaticamente e recapture. Exemplo prático: roll-formosa Miaoli com fixed 1.8s capturou uma tela em branco; corrigido com wait-for-load + verificação de stdev.

  16. Amplie para ver, não confie apenas na miniatura: a verificação deve examinar resolução total de cada quadro individualmente (corte / bordas borradas / cortado na miniatura não são visíveis). Ao medir coordenadas, desenhe linhas de grade y/x na imagem original. Exemplo prático: a borda inferior de um ponto de referência foi cortada em 55 px, a metade inferior estava desfocada; tudo invisível na miniatura, só detectado ao ampliar e desenhar grades.

  17. Jogo interativo sem auto-play → envie comandos de teclado para controlá-lo (submodo do MODO A): Katamari/jogos de ação devem usar keyboard.down/up do Playwright para controlar (teclas de direção para rolar). O jogo geralmente começa de verdade ~10s após o carregamento (precache/PREPARING): primeiro aguarde o START (polling até o botão ficar pronto) → clique em START → depois envie comandos. Um bot cego pode não conseguir pegar itens/crescer (roll-formosa horizontal travou em 2cm) — escolha a "janela mais cinematográfica" (bola grande o suficiente/muitos objetos), ou use o próximo modo showcase.

  18. Jogabilidade difícil de capturar → use um showcase/galeria automática como material: Muitos aplicativos têm uma página de "galeria de objetos/pré-visualização" que se move sozinha (ex: preview.html?city=X); não precisa jogar para renderizar todo o conteúdo; é uma fonte confiável de montagem (roll-formosa 20 cidades pontos de referência). Esses objetos em fundo escuro use fundo puramente escuro, não desfoque o fundo (senão cria fantasmas).

  19. Tela de "pronto" no final da gravação: o recordVideo grava desde o carregamento; o ready PREPARING → START aparece no final; grave o suficiente (espere o START + mais 3s), faça o trim a partir do final para pegar o trecho do ready, não do início (que pega o loading).

  20. Viewport retrato do celular geralmente é melhor: O layout mobile do jogo às vezes é mais cheio, ou até se comporta de forma diferente (roll-formosa jogabilidade retrato a bola cresce, horizontal trava). Ao regravar nativamente para cada formato, veja qual viewport tem o melhor desempenho.

  21. Texto preciso: A descrição do protagonista/objeto deve usar personalidade/manias, não aparência (elenco do catime); a linha de "notícia → imagem" do anúncio deve ser inferida do campo idea/story dos dados de origem, não inventada (uma notícia errada pode causar vergonha).

  22. Trailer ≠ tutorial, não confunda os formatos: Projetos visuais (jogos/apps) são adequados para trailers de 15-20s com cortes rápidos (provocar, chamar atenção); mas ferramentas CLI/devtools com cortes rápidos ficam muito chatos, sem sensação de tutorial — faça um tutorial narrado de ~60-90s: gancho do problema → instalar→executar→entender a saída→avançado passo a passo + edge-tts explicando devagar (cada passo com pausa suficiente) + legendas dos passos → CTA, trilha sonora muito baixa (~0.13) para dar destaque à narração. Exemplo prático: a primeira versão do wip com cortes rápidos foi considerada chata; a versão com tutorial narrado trouxe a sensação real de "entendi, vou usar". A sincronização da narração é feita com __STEPLOG do term-player. O term-player deve limpar a tela a cada passo (problema de origem do GOTCHA 16: o acúmulo de conteúdo pode escrever para fora da janela, e o comando que está sendo explicado é empurrado para fora).

  23. Leia o README completo/--help primeiro para fazer uma lista de funcionalidades, depois escreva o roteiro: Para fazer um tutorial/explicador de ferramenta, não leia apenas a introdução e comece a gravar — você pode perder funcionalidades principais (wip perdeu o dashboard.py painel visual, wip done, --root, install-skill, suporte a Codex, see: detecção de arquivos de planejamento). Primeiro liste todas as funcionalidades, depois escolha quais demonstrar. O TTS não deve forçar a leitura de nomes de usuário/handles/grafias raras (ex: yazelin é a romanização do nome chinês "Ya Zelin", soa estranho lido letra por letra) — se a URL já está na tela, deixe a narração falar "link abaixo"; evite palavras de abertura que causam pausa ("Primeiro…" → "É muito simples…"). O edge-tts lê termos técnicos em inglês (cargo/markdown/Claude Code) OK por si só; use Gemini flash para ouvir uma frase e confirmar.

  24. MODO D Vídeo musical: música inteira + interação, não grave, multiplexe com o arquivo original depois. O recordVideo do Playwright não inclui faixa de áudio nem cursor (se você não tem a trilha de áudio completa, veja a seção "Gravação Real" acima), então: (a) o áudio deve ser multiplexado com o mp3 original depois (melhor qualidade, não afetado pelo null sink do xvfb); (b) injete um cursor falso (cole uma imagem que segue o pointermove, apenas para a gravação, não entra no repositório), senão as "interações do mouse" no vídeo parecerão apenas efeitos especiais se movendo sozinhos. A linha do tempo das ações deve ser vinculada ao audio.currentTime com polling (waitForFunction(t => au.currentTime >= X)), não use waitForTimeout acumulado — quando o gravador perde quadros, o relógio da parede deriva, mas o relógio do áudio não.

  25. Sincronize pelo relógio na tela, não pelo Date.now() do Node. Há uma diferença indefinida entre o primeiro quadro do webm e newContext(), e o Playwright adiciona um grande bloco de quadros congelados no final (na prática, 160 segundos de conteúdo foram gravados como um arquivo de 239 segundos), então a duração do ffprobe não pode ser usada como comprimento. Solução: recorte a faixa da barra de progresso, meça a posição x do slider quadro a quadro (pegue a coluna com maior valor de pixels rosa como pico, depois calcule o centro de massa — partículas que passam também são da mesma cor, a média simples seria puxada), faça um ajuste linear de tempo na tela → tempo no áudio para encontrar o ponto inicial do áudio. Valide com três estimativas (ajuste do slider, momento da mudança de linha da letra, relógio de parede no momento do clique) e pegue a mediana; a da linha da letra terá um atraso de ~0,1s devido à transição CSS, lembre-se de subtrair. Antes de finalizar, use correlação cruzada para verificar o atraso da faixa de áudio do produto final em relação ao arquivo original (o priming do AAC adiciona ~40ms fixos, normal), depois extraia três quadros "longe da mudança de linha" para confirmar que a linha correta está iluminada.

  26. Formato retrato (9:16) não deve usar o zoom do navegador para aumentar a resolução. O recordVideo cola a "imagem do tamanho CSS" no tamanho de quadro que você especificar: viewport 405×720 com recordVideo.size 1080×1920 resulta em uma imagem 405×720 colada no canto superior esquerdo, o resto preenchido com cinza; tentar usar deviceScaleFactor para aumentar os pixels do dispositivo, a simulação de viewport do Playwright redefine o DSF para 1; usar --force-device-scale-factor + --window-size, xvfb não tem gerenciador de janelas, --window-size, Browser.setWindowBounds, --kiosk não conseguem alterar a janela (o innerWidth fica travado em 501). html{zoom:...} também não funciona, a media query ainda lê a largura original. Solução viável: abra o viewport diretamente em 1080×1920 nativo (DSF 1) e injete um "estilo retrato ampliado" (layout da versão mobile + tamanho da fonte/controles ampliados proporcionalmente, com !important para sobrescrever o grid da versão desktop). O estilo exclusivo para gravação não entra no repositório, é injetado na mesma posição do HIDE. Se as letras estão borradas, amplie para ver (GOTCHA 16): as bordas das letras ao ampliar de 405 para 1080 são suaves; a renderização nativa é nítida.

  27. Se possível, faça com que "a tela fale por si mesma": Para gravações de interação, organize as operações a serem demonstradas em uma linha do tempo (clique, arraste, pause), e intencionalmente deixe alguns períodos completamente parados para que a UI que se oculta automaticamente se feche — movimento contínuo impede o espectador de ver o ponto principal. Ao gravar interações, lembre-se de que o recordVideo não inclui cursor; injete um cursor falso (uma imagem que segue o pointermove), senão o espectador verá apenas efeitos especiais se movendo sozinhos.

  28. O ponto de ancoragem da sincronização deve ser um "flash branco", não "preto total". A ideia era "a página fica preta primeiro, só quando o vídeo realmente começa a tocar é que mostra algo, e o primeiro quadro que acende é o t=0" — na prática não funciona: o recordVideo do Playwright não começa a capturar no primeiro quadro; o quadro mais escuro de toda a gravação tem 24,4/255, não há quadro preto para reconhecer. Em vez disso, antes de começar, mostre um flash totalmente branco por 400ms; no momento em que começa a tocar, remova o flash, e procure o "quadro seguinte ao fim do flash branco". O branco 250 contra o conteúdo 24-37 é impossível de errar. Verificado: o deslocamento é de ±0,05 segundos em toda a gravação, sem deriva.

  29. metadata=print escreve no stderr por padrão. A saída de ffmpeg -vf "...,metadata=print:key=X" não está no stdout; se o script só captura stdout, sempre lerá vazio e interpretará "leitura vazia" como "a tela está preta". Adicione :file=-.

  30. O servidor http embutido deve ler o arquivo primeiro, depois enviar o cabeçalho. Se res.writeHead(200) for escrito antes do await readFile, quando o arquivo não existe, o 200 já foi enviado; o catch tenta enviar 404 e encontra ERR_HTTP_HEADERS_SENT — a verdadeira causa (caminho errado) é mascarada por esse falso erro. O capture-app.mjs deste repositório tinha esse problema; já foi corrigido.

  31. Para verificar a sincronização A/V, use um método com poder de discriminação. "Extrair um quadro do mesmo momento e comparar a correlação" parece razoável, mas para conteúdo de slides, a imagem pode não mudar por dezenas de segundos; um controle negativo (atraso intencional de 6 segundos) também dá 0,996, ou seja, não serve. Solução viável: coloque uma barra de progresso no template (largura = currentTime / duration); medindo a largura da barra no vídeo final, você lê de volta o tempo interno do player e subtrai diretamente do tempo de saída. Esta barra também serve como elemento visual do produto final, não é adicionada apenas para teste.

  32. Vídeos curtos 9:16 com slides estáticos: a imagem é morta — e é possível medir isso. A diferença de cinza quadro a quadro no espaço do slide foi medida em apenas 0,072 / 0,092 (congelado de referência 0,000); o feedback dos usuários foi "muito monótono, sem impacto". Não use a sensação para definir o limite; pegue vídeos 9:16 que você já entregou e meça com a mesma régua para encontrar a faixa utilizável: neko-tensei videoclipe musical 0,233, k-rider trailer 1,045, battlefield-editor 1,293, roll-formosa 1,912, catime anúncio 3,965, catime trailer 6,168 (active_ratio 0,636 = cortes rápidos). Vídeos curtos com frases de efeito narradas não devem ser feitos com cortes rápidos. Faixa alvo de 0,30 a 1,05, active_ratio 0,10 a 0,40 — Este conjunto de limites está descartado: mean e active_ratio não foram normalizados pela taxa de quadros, e usar seus próprios produtos finais para definir seus próprios limites é uma argumentação circular. O critério atual é motion.slide.per_s ≥ 5,5 do punch-score.py (quantidade de mudança de cinza por segundo, mean × fps, o único número que não varia com a taxa de quadros), sem limite superior, o perigo de "flash" é tratado pelo GOTCHA 35 sobre eventos de brilho. Base: a versão v1 que os usuários consideraram "imagem morta" tinha 1,79 e 2,30, e o produto final mais calmo já entregue, neko-tensei, tem 5,82 e deve passar.

  33. Para a movimentação de câmera em slides estáticos, a posição da câmera deve ser medida a partir da própria imagem, não pode ser uma proporção fixa. Um slide de 960×720, na prática, tem apenas 250 linhas de varredura com texto; o resto é espaço em branco; cortes fixos superior/médio/inferior podem cortar a pequena linha no topo do slide ou um monte de espaço em branco. Solução: extraia um quadro com canvas → calcule a "tinta" de cada linha (diferença da mediana do fundo) → conecte em faixas → descarte faixas com tinta abaixo de 12% → o limite esquerdo-direito das faixas restantes é o intervalo do texto principal SP, e a câmera não pode, em nenhum movimento, cortar o SP. Não reduza a imagem ao medir: reduzir 8x (120×90), o drawImage não faz média de área, traços finos simplesmente desaparecem (a ponta direita do texto "de." não foi medida, o SP ficou 96 px a menos), e o gradiente de fundo foi tratado como algo que atravessa toda a largura. Não importa como você ajuste o limite, um lado sempre fica comprometido; usar a resolução original resolve o problema, e "tinta" pode ser definida com a mesma definição da verificação (28 acima da mediana do fundo). Digitalizar 960×720 de uma vez no Chromium headless leva apenas dezenas de milissegundos.

  34. Para enquadrar, meça o quadro que está sendo reproduzido no momento, não procure primeiro e meça depois. Para medir "a página onde a frase de efeito está sendo dita", faça V.currentTime = meio da frase e depois drawImage — seeked só garante que a decodificação terminou, a tela ainda não mudou, na prática, todo o quote-06 foi enquadrado com o layout do segundo 0 (página anterior): o SP foi medido como 596 em vez de 696, então o quadro "garantia de não cortar texto" estava errado desde o início, e nos 2,8 segundos da palavra-chave principal, metade dos quadros tinha o texto principal cortado. A abordagem confiável é medir durante a reprodução: use o quadro do segundo 0 para a abertura (que é exatamente o que está na tela naquele momento), e depois meça novamente a cada mudança de câmera. Isso também resolve o problema de "uma frase se estende por duas páginas" — após a virada de página, a próxima mudança de câmera já acompanha.

  35. Eventos de luz/sombra devem ter um limite superior mensurável, não apenas "eu faço bem devagar". Meça o YAVG quadro a quadro do slide, o limite é 10% relativo mais um piso absoluto de 4/255 — o fundo do slide tem Y em torno de 20; usar apenas o valor absoluto como limite não detectaria nem um aumento de 9% de brilho; usar apenas o valor relativo seria inundado por ruído em áreas escuras. Conte "quantas vezes no segundo mais denso", limite em 3 vezes (limiar de risco para epilepsia fotossensível). Medido: produto final 2 vezes/segundo; controle negativo: flicker de 5 Hz 26 vezes/segundo, pulsação lenta de 1 Hz ±6% 6 vezes/segundo; ambos são bloqueados. Esta verificação já está no capture-short.mjs; ele imprime o resultado após a gravação.

  36. O filtro eq do ffmpeg: a expressão de brilho é avaliada apenas uma vez por padrão (eval=init). Ao fazer um controle negativo de "flicker intencional", esqueci de adicionar :eval=frame, o filtro não teve efeito, e a medição foi idêntica à do produto final — por pouco não descartei um bom indicador como "sem poder de discriminação". O próprio controle negativo também precisa ser verificado se realmente está quebrado, senão ele só prova que sua ferramenta não consegue distinguir dois arquivos idênticos.

  37. Diferença de quadros nunca tem poder de discriminação para sincronização. Teste novamente o caso do GOTCHA 31 com um template diferente: desloque intencionalmente o words.json em 6 segundos; a diferença de quadros do slide foi exatamente a mesma 0,718 (ela simplesmente não olha para o words.json). O que muda é a linha "momento da troca de bloco de legenda vs. momento da maior diferença de quadros na região da legenda" (0,091 → 0,567). Indicadores dinâmicos só podem afirmar dinamismo; a sincronização deve ser medida separadamente.

  38. A divisão de blocos de legenda, quando encontra "duas palavras-chave adjacentes", as cola no mesmo bloco. A divisão só troca de bloco quando "não cabe mais", e ao mudar para outra palavra-chave, muitas vezes ainda cabe — na prática, "grande ponto de interrogação" "não foi verificado" sem outras palavras no meio, colou em um bloco de 11 caracteres, o bloco inteiro dourado, dois pontos de impacto se tornaram um (apenas 2 blocos iluminados). Correção: adicione uma regra: k >= 0 e k !== k da palavra anterior força a abertura de um novo bloco. Após a correção: 3 blocos iluminados, maior bloco de 11 para 6 caracteres. Nota: o punch-score.py tem uma lógica de divisão semelhante; deve ser alterado junto.

  39. Em tempo de execução, o zoom é aumentado silenciosamente; ao calcular a largura para o enquadramento, subtraia essa margem primeiro. O zoom suave (+3,5%) e o zoom para palavra-chave (+5%) somam 8,7%: uma posição de câmera calculada "exata" ao ser aproximada, empurra o texto principal para fora da tela, e acontece exatamente nos segundos da palavra-chave — o pior lugar para ter problemas. Duas coisas devem ser feitas: ao enquadrar, z = margem de largura / 1,087, e recalcular o limite a cada quadro (z ≤ margem de largura, cx deve manter ambas as extremidades do SP dentro do quadro). A invariante deve ser verdadeira no momento em que a imagem é exibida, não apenas "calculada corretamente no momento do enquadramento".

  40. Uma vez que o cabeçalho e o rodapé do próprio slide são colocados no centro da tela, as laterais certamente serão cortadas, e isso será interpretado como corte do texto principal. A pequena linha do cabeçalho (x 24 a 932) e a barra de progresso do rodapé (x 0 a 759) se estendem além do intervalo do texto principal; quando a câmera aproxima, eles saem dos dois lados da tela. A verificação corta 10% do topo e do fundo do slide como "cabeçalho/rodapé", e ao mudar a movimentação da câmera, os elementos caem dentro dessa linha, sendo então interpretados como texto principal cortado. Correção: marque todas as linhas onde "a tinta se estende além do SP" como "mobília" (o critério não é "atravessa toda a largura" — a barra de progresso do rodapé tem apenas 79% da largura no limite de tinta; usar "toda a largura" como critério deixaria passar), e restrinja o centro da câmera para que a "mobília" só possa ficar dentro do 10% superior ou inferior da tela, ou completamente fora do quadro. A margem deve ser de alguns pixels a mais: se estiver exatamente em cima do limite de 10%, o arredondamento da amostragem pode fazer com que a última ou duas linhas da mobília caiam no lado do texto principal (na prática, ainda havia 15 quadros, todos nas linhas 81 e 82).

  41. Para diagnosticar problemas de enquadramento, primeiro execute o algoritmo offline novamente; não tente ajustar parâmetros às cegas. Uma gravação de 40 segundos, ajustar limites às cegas leva a "quanto mais ajusto, pior fica": na prática, desliguei o zoom suave e o zoom para palavra-chave (achando que era a causa principal), e o corte de texto passou de 102 quadros para 189 quadros. Em vez disso, use ffmpeg para extrair o mesmo quadro de detecção, execute a mesma lógica de band/SP em Python, em dez segundos vi "o SP medido pelo navegador é exatamente o mesmo da página do segundo 0", e a causa principal ficou evidente na hora. Além disso, imprima as premissas do enquadramento no log (SP, z da margem de largura, posição da mobília) — quando a posição da câmera desmorona, olhar esses três números mostra se foi erro de medição de largura ou se o corredor foi comprimido.

  42. Ao relatar números do produto final, separe os "que vêm do conteúdo" dos "que vêm da imagem"; os últimos devem sempre vir acompanhados do md5 do template. Os que vêm do conteúdo (gancho, coloração, bloco iluminado, bloco de legenda, silêncio, velocidade da fala) estão vinculados ao words.json; se o md5 desse arquivo não mudar, os números não mudam nem um bit; os que vêm da imagem (diferença de quadros, corte de texto) só são válidos para a versão do template no momento da gravação. Misturar os dois ao relatar fará com que a próxima pessoa que seguir a documentação e regravar obtenha números diferentes: na prática, o mesmo clip, com a versão do template antes da alteração, gravou uma taxa de mudança por segundo de 14,80; após a alteração, 27,19 — diferença de 1,8x, enquanto os seis itens de conteúdo não mudaram nenhum. A pessoa, portanto, perdeu uma rodada tentando encontrar uma regressão que não existia. O capture-short.mjs agora, a cada gravação, imprime template short-9x16.html md5 …; ao relatar números, copie-o.

  43. A própria gravação tem tremor; o critério deve acomodar esse tremor. Fixe o template e a entrada com md5 e grave quatro vezes; a taxa de mudança por segundo do slide foi 21,27 / 21,33 / 20,82 / 21,3

...

[truncated for translation]

Habilidades Relacionadas