Communitygithub.com

deck-to-video

Transforme slides (imagens ou PDF) e narrações por slide em um vídeo MP4 que se reproduz sozinho — um vídeo de apresentação narrada, com barra de progresso, contagem regressiva e contador de slides.

O que é deck-to-video?

Este skill permite converter uma sequência de slides (imagens ou PDF) juntamente com arquivos de áudio de narração por slide em um vídeo MP4 que se reproduz automaticamente, como uma apresentação narrada. O vídeo gerado inclui overlays como barra de progresso, contagem regressiva de segundos restantes e um contador de slides 'N / M'. A motivação principal é superar as limitações das funcionalidades de desenho do ffmpeg, que muitas vezes não funcionam corretamente (como a barra de progresso estática desde o primeiro quadro ou erros de filtro 'drawtext'). O skill utiliza Python com PIL (Pillow) para gerar os frames de overlay de forma determinística e confiável, alimentando depois o ffmpeg com uma sequência de imagens para codificação. Além disso, oferece suporte avançado para destacar elementos nos slides em sincronia com a narração, usando alinhamento forçado de áudio (como o da ElevenLabs) para criar animações de 'glow' ou destaque que acompanham a fala. O skill também cobre a geração de legendas (SRT, faixa soft ou queima na imagem) a partir dos mesmos timings de alinhamento, e inclui scripts auxiliares para construir os arquivos JSON de destaques e legendas. É compatível com plataformas como Claude Code, Cursor e Codex, e é especialmente útil para criar vídeos de apresentações para compartilhamento em WhatsApp, Slack, Google Drive ou Teams.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/shaharsha/claude-skills/tree/HEAD/skills/deck-to-video

Perguntar na sua IA favorita

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

Documentação


name: deck-to-video description: Use ao transformar um conjunto de slides mais narração por slide em um vídeo mp4 — um vídeo de apresentação que se reproduz sozinho, "slides para vídeo", "apresentação para mp4", um vídeo compartilhável de uma apresentação narrada (WhatsApp/Slack/Drive), ou quando um vídeo de slide precisar de uma barra de progresso / contagem regressiva / contador de slides. Use também quando um vídeo de slide precisa destacar, iluminar ou focar o elemento sendo falado em sincronia com a voz, ou quando a narração e a animação na tela precisam ser sincronizadas. Use também quando uma barra de progresso desenhada pelo ffmpeg aparece cheia/estática desde o primeiro quadro, ou o ffmpeg retorna o erro "No such filter: 'drawtext'". Use também ao queimar ou embutir legendas em um vídeo de apresentação, quando as legendas não aparecem no player inline do Slack ou Teams, quando o VLC mostra dois conjuntos de legendas ao mesmo tempo, ou quando o ffmpeg informa "No option name near" em um filtro de legendas. Use também quando a multiplexação final do ffmpeg corta o final do vídeo, ou o ffmpeg rejeita -filter_complex_script.

Deck para Vídeo (slides + narração → mp4 que se reproduz sozinho)

Visão Geral

Transforme N imagens de slides (ou um PDF) + N clipes de áudio por slide em um único mp4: cada slide é exibido exatamente pela duração da sua narração mais uma pequena pausa, e então corta para o próximo. Overlays: uma barra de progresso que se preenche + contagem regressiva de segundos restantes (canto inferior direito) e um contador de slides "N / M" (canto superior direito).

O comando principal (o script está no diretório scripts/ deste skill — chame-o pelo caminho completo):

python3 <caminho-deste-skill>/scripts/build_video.py SLIDES AUDIO_DIR out.mp4
# SLIDES = slides.pdf (rasterizado a 200dpi) OU um diretório com imagens por slide
#          (ordenação numérica: s2 antes de s10; nomes com zeros à esquerda ainda recomendados)
# AUDIO_DIR = slide01.mp3, slide02.mp3, … — a contagem DEVE ser igual ao número de slides
# opções: --size 1920x1080 · --pad 0.7 · --no-bar · --no-counter · --bar-color HEX · --crf 20
#          --highlights hl.json · --anim-fps 30   (veja "Destacando elementos" abaixo;
#          scripts/make_highlights.py constrói o hl.json)

Em caso de sucesso, ele imprime uma linha por slide — a duração de cada slide já inclui o pad — e termina com duration matches segment sum (caso contrário, sai com código de erro).

Preparando as entradas corretamente

  1. Slides: se começar de um pptx, rasterize a partir do PowerPoint real, não do LibreOffice. Exporte o pptx para PDF usando o PowerPoint original (o skill office-render faz isso no macOS), então entregue o PDF ao script — o LibreOffice substitui fontes e reorganiza layouts, então o vídeo não corresponderia ao que o autor da apresentação vê. 200 dpi mantém o texto denso dos slides nítido em 1080p. (Já tem imagens por slide? Use-as diretamente — pule esta etapa.)
  2. Áudio: um clipe por slide, nomeado slideNN.mp3. Se os clipes vieram do generate_tts.py do elevenlabs-tts (que o narrating-pptx usa), eles já estão neste formato exato — reutilize-o para que o pptx e o vídeo soem idênticos.
  3. Se a velocidade da narração foi ajustada (ex.: ffmpeg -filter:a atempo=1.1), construa o vídeo a partir dos arquivos ajustados.

Por que os overlays são gerados com PIL, não desenhados pelo ffmpeg (pago com sangue)

  • drawbox com uma expressão de largura baseada em t renderiza uma barra CHEIA desde o quadro 0 em várias compilações do ffmpeg — a expressão é avaliada uma vez na inicialização, não por quadro. A falha é silenciosa: o vídeo codifica perfeitamente e a barra simplesmente nunca se move. Expressões drawtext atualizam por quadro nas mesmas compilações, o que torna a barra quebrada fácil de passar despercebida ao lado de uma contagem regressiva funcional.
  • Os bottles do ffmpeg do Homebrew (2026+) são compilados sem libfreetype — No such filter: 'drawtext' — e as versões keg (ffmpeg@7, ffmpeg@8) são iguais. Não perca um ciclo de instalação descobrindo isso; se uma compilação completa for necessária para qualquer outra coisa, uv tool install static-ffmpeg fornece uma (o script faz fallback automaticamente para ela).

Então o script gera frames com PIL (preenchimento da barra, contagem regressiva, contador) e alimenta o ffmpeg com uma sequência de imagens codificada a -r 30. Determinístico, com controle de fonte e mais barato de codificar do que filtrar um stream de 30 fps. Um frame por segundo é o padrão e está correto enquanto nada no slide se move — atualizações com granularidade de segundo são naturais para uma contagem regressiva. Slides com --highlights são gerados a 30 fps, pelo motivo da próxima seção.

Escolhas de codificação que o script já faz (não as desfaça)

EscolhaPorquê
apad=pad_dur=PAD + -t seg_durpreenchimento exato no final; -shortest ingênuo corta a pausa após a fala
yuv420p + dimensões paresqualquer outra coisa não reproduz no QuickTime/WhatsApp/navegadores
-tune stillimage, CRF 20slides estáticos comprimem bem sem cintilação de texto
concat demuxer + -c copyjunção sem perdas de segmentos codificados identicamente
-movflags +faststarto vídeo é transmitido antes de terminar o download

Valide — assuma que o overlay está quebrado até que os quadros provem o contrário

O script verifica se a duração final ≈ soma dos segmentos. O overlay precisa de olhos:

ffmpeg -y -ss T1 -i out.mp4 -frames:v 1 a.jpg   # no início de algum slide
ffmpeg -y -ss T2 -i out.mp4 -frames:v 1 b.jpg   # no final do mesmo slide

Escolha T1/T2 a partir das durações por slide impressas pelo script (some-as cumulativamente para os limites dos slides; lembre-se de que elas incluem o pad). Leia ambas as imagens. A barra deve estar visivelmente mais cheia em b.jpg e a contagem regressiva menor — e deve resetar em cada novo slide. Uma barra com aparência cheia em um timestamp inicial = o bug da barra estática — esta verificação exata foi o que o detectou originalmente. Você não pode verificar o áudio sem uma interface visual — diga isso; nunca afirme que ouviu algo.

Destacando elementos em sincronia com a narração

Narração que diz "à esquerda… no meio… à direita" soa como uma faixa de audiodescrição (veja apresentações-autoexecutáveis). A correção é remover o ponteiro verbal e iluminar o elemento — mas apenas se o timing vier do áudio.

O áudio é o mestre; a animação o segue. Nunca escreva narração para atingir uma marca, e nunca estime quando uma frase é dita. Gere o clipe normalmente, então use o alinhamento forçado do ElevenLabs (POST /v1/forced-alignment, áudio + transcrição → start/end por caractere; execute-o com o scripts/align_narration.py do elevenlabs-tts) para ler quando cada coisa foi realmente dita, e derive as janelas a partir disso. Regravar um clipe depois e realinhar corrige automaticamente.

  • Âncore nas posições no script, não em palavras-chave. Uma palavra-chave solitária quebra no momento em que aparece duas vezes ("tags" aparece quatro vezes em um slide). Marque onde a narração se volta para cada elemento com uma frase curta copiada do script, na ordem falada, e busque cada uma apenas após a correspondência anterior, que é o que o make_highlights.py faz; uma palavra repetida anteriormente no script não pode capturá-la. (Manter deslocamentos de segmento por elemento no script unido também funciona.)
  • Remova as tags de áudio antes de alinhar — todas elas. [confiante] é interpretado, não falado; envie-o ao alinhador e ele procurará a palavra no áudio e arrastará todos os deslocamentos posteriores com ela. As tags do Eleven v4 são de forma livre ([Pausa, humor seco]), então remova qualquer coisa entre colchetes, não apenas letras e espaços (o alinhador do elevenlabs-tts faz isso). Calcule os deslocamentos contra a mesma string limpa.
  • Verifique se a contagem de caracteres retornada é igual ao comprimento do texto e observe loss (0.04-0.10 em uma apresentação saudável de 17 clipes em inglês, 0.59-0.77 em uma apresentação saudável de 26 clipes em hebraico; um valor claramente discrepante dentro da apresentação é o sinal, não um limite absoluto). Uma incompatibilidade significa que todas as janelas naquele slide estão erradas.

Construa o JSON --highlights com scripts/make_highlights.py a partir de três entradas: marcadores (por slide, cada região com a frase onde a narração se volta para ela), caixas (por slide, região → [x, y, w, h]) e os arquivos alignment/:

python3 <caminho-deste-skill>/scripts/make_highlights.py markers.json boxes.json narration/alignment/ hl.json \
  --size 1920x1080 [--inches 13.333]      # --inches: caixas estão em polegadas do slide, página com esta largura
# markers.json: {"09": [["chart", "Último trimestre"], ["callout", "O verdadeiro problema"], ["_end", "Agora, para"]],
#                "12": {"marks": [["a", "Primeiro"], ["b", "Segundo"]], "dim": "black", "colour": "#E07A5F"}}

A janela de uma região vai da sua frase até o próximo marcador (_end fecha uma sem abrir outra; caso contrário, a última vai até o final da fala), começando 0,12 s antes. Janelas abaixo de 1,4 s são ignoradas com um aviso, já que um fade de 0,5 s para entrar e sair não deixa nada para ver. Ele para, sem escrever nada, quando uma frase não está no script, uma região não tem caixa, uma caixa sai do quadro ou um arquivo de alinhamento não corresponde ao seu script. Ele avisa quando uma caixa está a menos de 19 px da borda, onde o brilho é cortado.

O JSON resultante, indexado pelo número do slide com zero à esquerda, caixas em pixels de saída:

{"09": [{"card": "chart", "box": [87, 468, 549, 374], "colour": "#B45309",
         "dim": "white", "start": 13.6, "end": 22.9}]}

Pegue box do próprio código de layout da apresentação (polegadas → pixels), nunca inspecionando a renderização visualmente — a geometria de um card já é exata no construtor que o desenhou. dim é white em slides claros, black em escuros; verifique a cor do brilho também em um slide escuro (um brilho ameixa escuro desapareceu em um slide de encerramento quase preto). Uma região pode ser destacada mais de uma vez em um slide. Slides ausentes do arquivo são gerados a 1 fps como de costume.

Legendas vêm de graça uma vez alinhadas. Os mesmos timings de palavras produzem um arquivo .srt sem uma segunda passagem de transcrição:

python3 <caminho-deste-skill>/scripts/make_srt.py narration/alignment/ narration/audio/ out.srt --pad 0.7

--pad deve corresponder ao pad com que o vídeo foi construído, ou as pistas se desviam ainda mais a cada slide.

Três maneiras de enviá-las, e o destino decide qual.

FormaComoO que o espectador obtém
.srt lateralenvie o arquivo junto com o mp4legendas apenas se abrirem ambos em um player real
Faixa soft-c copy -c:s mov_text -disposition:s:0 default — multiplexação sem perdas, ~12 KB, segundosdependente do player; precisa de um controle de CC existente
Queimadasrecodifique através do libasssempre visíveis, em qualquer lugar, não podem ser desativadas

Padrão: o arquivo lateral ou a faixa soft — ambos são reversíveis e nenhum toca na imagem. Queime apenas quando o vídeo for reproduzido em algum lugar sem controle de legenda. O player inline de um tópico do Slack ou Teams é o caso que força isso: um espectador que apenas aperta play ali não obtém nada das outras duas formas.

Queimando as legendas

ffmpeg -i deck.srt /tmp/subs.ass          # converta PRIMEIRO; não passe force_style
# …edite a linha Style: em /tmp/subs.ass (veja abaixo), então:
<static-ffmpeg> -i master.mp4 -vf "ass=/tmp/subs.ass" \
  -c:v libx264 -crf 20 -preset medium -tune stillimage -pix_fmt yuv420p \
  -c:a copy -sn -movflags +faststart out.mp4

Quatro coisas que custam um ciclo cada:

  • O ffmpeg do Homebrew não tem libass — a mesma lacuna do drawtext/libfreetype, e o sinal é enganoso. Você obtém No option name near '<filename>', um erro de análise do filtergraph em vez de No such filter, então parece um problema de citação e faz você reescrever escapes que já estavam corretos. Verifique com ffmpeg -filters | grep -E ' (ass|subtitles) ' e use a compilação static-ffmpeg.
  • Estilize o arquivo .ass; nunca coloque force_style na string do filtro. Suas vírgulas são lidas como separadores de filtro, e escapar com barras invertidas é frágil o suficiente para falhar duas vezes antes de funcionar.
  • Mapeie os campos de estilo ASS pelo cabeçalho Format:, nunca contando posições. Um erro de um escreve Alignment=0 (inválido) e deixa MarginV não definido, e o libass renderiza sem reclamar.
  • Queime a partir do mestre pré-queima e mantenha esse mestre. Reestilizar a partir de um arquivo já queimado empilha uma segunda geração e grava o texto antigo permanentemente.

Posicionamento — faixa ou sobreposição. A apresentação decide, não o gosto.

ComoCusto
Faixa abaixo do slidepad=1920:1200:0:0:black então ass=, MarginV dentro da faixanada no slide é coberto; a saída deixa de ser 16:9
Sobreposição (o visual familiar do VLC)ass= sozinho; centralizado na parte inferior, negrito ~48 px em 1080p, contorno preto pesado, MarginV ~40clássico e autocontido, mas cobrirá o que a apresentação colocar em sua faixa inferior

Sobreposição é o que a maioria das pessoas quer dizer com "legendas". Em uma apresentação com muito texto, extraia um quadro e olhe antes de confirmar: ela cai sobre chamadas de rodapé, legendas e a barra de progresso.

1 fps é um limite de sincronia, não apenas de suavidade. A renderização padrão é um quadro por segundo, então um destaque só pode mudar em um segundo inteiro — até um segundo inteiro de distância da palavra que aponta, o que parece quebrado. Slides animados precisam de ~30 fps.

30 fps é barato se você armazenar frames em cache. Um slide de 60 s tem 1.800 quadros, mas quase todos são pixel-idênticos — o brilho só se move durante os fades. Chaveie cada quadro pelo seu estado visual e crie links físicos para os duplicados:

key = (active_element, round(fade_alpha, 2), int(t))   # int(t) = o passo da barra
if key in cache: os.link(cache[key], path); continue

120-200 composições reais por slide em vez de ~1.800, e sem disco extra. Mas quantize a barra de progresso para segundos inteiros primeiro — uma barra que desliza continuamente torna cada quadro único e destrói o cache completamente. (Em 1 fps já se comportava assim, então nada se perde.)

Inverta o dim em slides escuros. Lavar elementos inativos com branco translúcido os empurra para trás em um slide claro e os clareia em um escuro. Carregue a cor dim por slide, não por apresentação.

Não anime o que é dito rápido demais. Meça o intervalo falado antes de confirmar: quatro itens de lista ditos em ~1,2 s cada deixam ~0,2 s de brilho após um fade de 0,5 s para entrar e sair, e causam estroboscópio. Dê a cada item sua própria batida na narração — geralmente uma melhoria, já que listas ditas em um fôlego são a coisa mais densa em qualquer apresentação — ou deixe o slide estático.

Erros comuns

ErroConsequênciaCorreção
animação drawbox do ffmpeg com base em tbarra cheia desde o quadro 0use o script deste skill (overlays com PIL)
usar o ffmpeg do Homebrew sem libfreetype/libasserro No such filter: 'drawtext' ou erro No option name nearuse static-ffmpeg via uv tool install static-ffmpeg
pular a validação visual dos overlaysbarra quebrada ou contagem incorreta passam despercebidasextraia quadros no início e final do mesmo slide com ffmpeg -ss
não remover tags de áudio ([confiante]) antes do alinhamentodeslocamentos de caractere incorretos para todas as palavras seguinteslimpe qualquer coisa entre colchetes antes de alinhar
usar force_style na string do filtro assfalha de análise devido a vírgulasedite o arquivo .ass diretamente
posicionamento de legendas por tentativa e erro em uma apresentação densalegendas cobrem conteúdo importanteextraia um quadro e inspecione antes de codificar
animar destaques em janelas muito curtas (<1,4 s)efeito estroboscópico; nada é visívelmeça o intervalo falado; se muito curto, deixe o slide estático
não usar cache de frames para animações de destaque a 30 fpsalto uso de CPU e discochaveie por estado e crie links físicos para quadros duplicados

Controle de versão implícito

  • build_video.py lê scripts/version.py (não mostrado) e imprime a versão na saída padrão.
  • A saída de áudio e os arquivos de alinhamento são versionados pela ferramenta que os gerou (elevenlabs-tts).
  • Os JSONs de marcadores e caixas são textos pequenos; considere versioná-los junto com o pptx de origem se o projeto for colaborativo.

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

shaharsha/generating-video-clips

Use when turning a still image, illustration, comic panel or character art into a short animated clip (image-to-video), choosing a video model or provider (Gemini Omni, Veo, Seedance, Kling, ElevenLabs Flows) or hitting their API gates, lip-syncing a character's mouth to existing dialogue audio (Sync, fal.ai, active speaker), or fixing a generated clip with artifacts - duplicated props, a door or a second figure appearing, hands through bodies, glowing eyes, a gesture repeated at the start,...

shaharsha/image-generation

Generate logos, icons, UI mockups, hero images, product shots, and other design assets using OpenAI GPT Image 2.5 (Sunburst, Flare) or Google Gemini (Nano Banana 2, Nano Banana Pro, Nano Banana 2 Lite). Use whenever the user asks to create, design, generate, mock up, render, or illustrate a visual asset, needs a transparent PNG, or asks for image prompt engineering. Picks the right model and quality for the job, writes a model-specific prompt following the official prompting rules of each...

shaharsha/producing-animated-episodes

Use when making a narrated or voiced animated video with recurring characters - an anime, cartoon or comic episode, a short for a group chat, a story video about friends, family, a class or a team - from photos, a chat export, a story idea or a script, in any visual style, Hebrew/RTL or English. Also use when such a video has subtitles or cards too fast to read, music stopping early, the ending cut off, lines buried under music, mouths out of sync, a gesture before its line, or one face...

Habilidades Relacionadas