Clipify
Encontre momentos marcantes em um vídeo, corte-os como clipes independentes, opcionalmente reformate 16:9 → 9:16 (panorâmica facial ou tela dividida), faça movimentos de câmera temporais para revelações mais rápidas e queime legendas no estilo opus, palavra por palavra.
Modos
Escolha um antes do Passo 1. A escolha muda como você encontra os momentos e se as legendas se aplicam.
- Modo Diálogo (padrão para podcasts, entrevistas, cabeças falantes): Whisper transcreve, você procura por punchlines / reações / pausas estranhas. → Passo 1A. Legendas no Passo 5.
- Modo Ação (cortar madeira, esportes, percussão, bateria, qualquer coisa com sons de impacto repetidos): pule o Whisper completamente, execute
detect_transients.pypara encontrar cada golpe, corte em cada um. → Passo 1B. Pule o Passo 5; os sons de golpe são o ritmo. - Híbrido: modo diálogo para seções de fala, modo ação para o resto, intercale. Use ambos 1A e 1B.
Se o usuário disser "Não estou falando nisso", você está no modo ação. Se eles disserem "corte em cada X" (corte, golpe, murro, batida de bateria, balanço, pisada), você está no modo ação. Padrão para modo diálogo caso contrário.
Entradas
- Um caminho de arquivo de vídeo (o usuário fornecerá; caso contrário, pergunte)
- Opcional: formato solicitado (9:16, 16:9, 1:1) — se não for fornecido, pergunte após os candidatos serem selecionados
- Opcional: preferência de estilo de legenda — se não for fornecida, pergunte antes de legendagem
Ferramentas (use apenas o caminho mais rápido)
- Whisper:
whisper --model tiny.en --word_timestamps True --output_format json(≈10× mais rápido quesmall.en; qualidade suficiente para inglês). Para não-inglês:--model base(remova--language). - ffmpeg: adicione
-hwaccel videotoolboxpara decodificação e-preset ultrafastpara renders. Use-c:v libx264 -crf 20para o master final. - Numpy para alinhamento de áudio (correlação cruzada FFT). Sem scipy/cv2 necessário.
- Scripts:
<skill-dir>/scripts/(onde<skill-dir>é o diretório que contém este SKILL.md — tipicamente~/.claude/skills/clipify/)analyze.py— linha do tempo do falante a partir de dois arquivos de movimento ROIbuild_pan.py— expressão de corte x do ffmpeg com cortes secosbuild_ass.py— legendas ASS estilo opus a partir de JSON do Whisperaudio_align.py— encontrar deslocamento de um sub-clipe em uma fonte mais longadetect_transients.py— encontrar sons de impacto agudos (modo ação); veja--help
Diretório de trabalho: /tmp/clipify/ (crie o diretório no início, deixe artefatos para depuração).
Fluxo de Trabalho
Passo 1A — Encontrar momentos de diálogo (modo diálogo)
mkdir -p /tmp/clipify
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
whisper /tmp/clipify/audio.wav --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
Leia o JSON resultante (ou .txt) e escolha 3–5 clipes candidatos. Sinais engraçados para procurar:
- Punchlines e reações: palavras como "o quê", "espera", "não acredito", risadas, "haha", palavrões
- Momentos de reviravolta: pergunta de configuração → resposta inesperada
- Pausas estranhas: segmento Whisper com longo intervalo, ou preenchimentos ("uh", "um")
- Auto-depreciação / frases citáveis: frases declarativas curtas que se sustentam sozinhas
- Picos de áudio: detecte via
ffmpeg -af volumedetectou procure por idas e vindas rápidas (segmentos curtos alternados do Whisper)
Para cada candidato, proponha: [início, fim, por que é engraçado, título sugerido]. Mire em clipes de 10–25s. Mostre a lista e deixe o usuário confirmar/escolher.
Passo 1B — Encontrar cada golpe (modo ação)
Substitua o Whisper pela detecção de transitórios de áudio. O detector encontra impactos agudos (machado na madeira, punho no saco de pancadas, pele de bateria, bola no taco, madeira caindo em uma pilha) computando fluxo espectral na banda de 1–6 kHz — o impulso de banda larga que um impacto cria e que o ambiente/vento não cria.
ffmpeg -y -hwaccel videotoolbox -i "$VIDEO" -vn -ac 1 -ar 16000 /tmp/clipify/audio.wav
python3 <skill-dir>/scripts/detect_transients.py /tmp/clipify/audio.wav \
--min-flux 30 --min-gap 0.6 > /tmp/clipify/strikes.json
Ajustes:
--min-flux 30é o padrão são. Impactos reais registram 50–300; ambiente/vento fica em 5–15. Muitos falsos positivos → aumente para 40–50. Poucos acertos → diminua para 20.--min-gap 0.6(segundos) é a distância mínima entre dois golpes. Bateria rápida pode precisar de 0.2; cortar madeira é suficiente em 0.6–1.0.--band 1000:6000cobre impactos em madeira/metal/vidro. Batidas pesadas (bumbo, golpes no corpo) ficam mais abaixo (--band 200:2000); assobios, tinidos, estalos de caixa ficam mais acima (--band 4000:10000).- Para fontes com múltiplos clipes (uma pasta de clipes de celular), execute o detector em cada clipe e escolha um conjunto variado entre ângulos. Mire em 50–65 golpes para uma montagem de 45–60s a ~0.9s por cena.
Cada "cena" deve começar ~0.4s antes do golpe (mostrando a preparação) e terminar ~0.5s após (golpe + breve acompanhamento). Isso dá ~0.9s por batida.
Passo 1.5 — Verificar se cada candidato está em quadro
Sempre faça isso antes de renderizar, em qualquer modo. A detecção de áudio encontra o som de um evento; a câmera pode não tê-lo capturado. Pular isso é a forma número 1 de receber feedback do usuário como "você cortou em cortes onde nem estou visível".
# Amostre um quadro em cada tempo de golpe candidato T:
for T in $STRIKE_TIMES; do
ffmpeg -y -ss "$T" -i "$VIDEO" -frames:v 1 -vf scale=320:-1 \
/tmp/clipify/verify_${T}.jpg
done
Leia cada imagem de verificação e descarte o candidato se:
- O sujeito está abaixado, fora do quadro, ou atrás de um obstáculo
- Há um polegar na lente (sim, isso acontece — e você só pega visualmente)
- É claramente uma fonte de som diferente (batida de porta, ferramenta caída) que acionou o detector
Para ~30 candidatos, isso é uma chamada rápida de ffmpeg cada mais uma única leitura de lote de imagens. Um seguro barato.
Passo 2 — Cortar cada clipe escolhido
ffmpeg -y -ss "$START" -t "$DURATION" -i "$VIDEO" -c copy /tmp/clipify/clip_$N.mp4
(Use -c copy para corte instantâneo. Re-codifique apenas se os cortes precisarem ser precisos por quadro.)
Passo 3 — Decidir o formato de saída
Pergunte ao usuário (pule se já especificaram): "9:16 (TikTok / Reels), 16:9 (YouTube), ou 1:1 (feed do Insta)?"
Passo 4 — Se 16:9 → 9:16: panorama entre rostos vs tela dividida
Detecte a proporção de aspecto da fonte com ffprobe. Se a fonte for 16:9 e o destino for 9:16, pergunte:
"Duas opções: (a) panorama com corte seco que segue quem está falando (um rosto na tela por vez), ou (b) tela dividida empilhada com ambos os rostos visíveis. Qual você prefere?"
Pule a pergunta se houver apenas um rosto (clipe de um único falante). Para um único falante, apenas centralize o corte.
Passo 4a — Panorama entre rostos (recomendado para diálogo rápido de cabeças falantes)
-
Localize as duas ROIs de rosto. Amostre um quadro:
ffmpeg -ss <meio> -i <clipe> -frames:v 1 /tmp/clipify/probe.jpg. Leia-o. Observe a área da boca+queixo de cada rosto comox,y,w,hno espaço de pixels da fonte. (Sem necessidade de cv2 — a câmera é estática dentro de um clipe; um quadro é suficiente.) Verifique desenhando caixas:ffmpeg -i probe.jpg -vf "drawbox=x=$LX:y=$LY:w=$LW:h=$LH:[email protected]:t=4,drawbox=x=$RX:y=$RY:w=$RW:h=$RH:[email protected]:t=4" verify.jpgItere no máximo duas vezes. As caixas devem cobrir boca + queixo e evitar mãos/microfones. Não ajuste demais — a diferença de quadros é tolerante.
-
Extraia energia de movimento por quadro em cada ROI:
ffmpeg -y -i clip.mp4 -filter_complex " [0:v]split=2[a][b]; [a]crop=$LW:$LH:$LX:$LY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/L.txt[la]; [b]crop=$RW:$RH:$RX:$RY,format=gray,tblend=all_mode=difference,signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file=/tmp/clipify/R.txt[ra] " -map "[la]" -f null - -map "[ra]" -f null - -
Construa a linha do tempo do falante (tempo mínimo de permanência de 1.0s — interjeições curtas se fundem ao falante anterior):
python3 <skill-dir>/scripts/analyze.py /tmp/clipify/L.txt /tmp/clipify/R.txt 1.0 > /tmp/clipify/segments.json -
Escolha coordenadas x do panorama para uma faixa vertical 9:16 da fonte. Com fonte W=1920 e destino W=1080, largura da faixa de corte = 608.
- LEFT_X =
centro_rosto_esquerdo_x - 304(limitar ≥ 0) - RIGHT_X =
centro_rosto_direito_x - 304(limitar ≤ source_W - 608)
- LEFT_X =
-
Gere a expressão x de corte seco e renderize:
EXPR=$(python3 <skill-dir>/scripts/build_pan.py /tmp/clipify/segments.json $LEFT_X $RIGHT_X) ffmpeg -y -hwaccel videotoolbox -i clip.mp4 -filter_complex \ "[0:v]crop=608:1080:x='$EXPR':y=0,scale=1080:1920:flags=lanczos[v]" \ -map "[v]" -map 0:a -c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p \ -c:a aac -b:a 192k /tmp/clipify/clip_panned.mp4Fonte 1920×1080 assumida; para fonte 4K, reduza a resolução primeiro ou dobre todas as coordenadas.
Passo 4b — Tela dividida (ambos os rostos sempre visíveis)
Dois blocos empilhados, 1080×960 cada. O bloco do falante ativo fica no topo — a sobreposição alterna nas mudanças de falante.
[0:v]split=2[a0][a1];
[a0]crop=Wcrop:Hcrop:LX_tile:LY_tile,scale=1080:960,split=2[lt0][lt1];
[a1]crop=Wcrop:Hcrop:RX_tile:RY_tile,scale=1080:960,split=2[rt0][rt1];
[lt0][rt0]vstack[layoutL];
[rt1][lt1]vstack[layoutR];
[layoutL][layoutR]overlay=0:0:enable='<RIGHT_SPEAKER_ENABLE>'[v]
Construa <RIGHT_SPEAKER_ENABLE> a partir de segments.json como between(t,a,b)+between(t,a,b)+... sobre os segmentos do falante direito. Os cortes dos blocos devem mirar ~720×640 ao redor de cada rosto (1.125:1 para corresponder a 1080×960).
Passo 4c — Revelações com variação temporal (opcional)
Quando o usuário quer um plano lento / panorâmico / revelação encurtado ("corte isso pela metade") sem perder o arco, use rampa de velocidade em vez de cortar. Cortar força você a descartar parte do arco; a rampa de velocidade preserva todas as batidas em um andamento mais alto.
# Velocidade 2x: arco de 12s → saída de 6s, tom de áudio preservado via atempo
ffmpeg -y -ss "$START" -i "$CLIP" -t "$ORIG_DUR" \
-vf "setpts=PTS/2,scale=1080:1920:flags=lanczos,fps=30" \
-af "atempo=2.0" \
-c:v libx264 -preset fast -crf 21 -pix_fmt yuv420p \
-c:a aac -b:a 128k /tmp/clipify/clip_2x.mp4
Regras práticas:
- 1.5x — movimento humano que deve parecer "ligeiramente rápido" sem parecer acelerado
- 2x — revelação mais contundente; ainda parece natural
- 3x–4x — vibe de time-lapse (encadeie
atempo=2.0,atempo=2.0para 4x;atempoaceita apenas 0.5–2.0 por instância de filtro) - Remova
-af atempoe silencie o segmento se o áudio for apenas ambiente/vento e o efeito de esquilo for perturbador
Útil quando o resto do corte é rítmico (montagem de cortes, bate-papo de ida e volta) e a revelação pareceria um ponto morto.
Passo 5 — Adicionar legendas
Pule esta etapa no modo ação — os sons dos golpes são o ritmo e as legendas apenas poluem o visual.
Pergunte uma vez (apenas se o usuário ainda não especificou um estilo):
"Três estilos de legenda: opus (branco grande e negrito, destaque de palavra ativa amarelo), karaokê (blocos de 4 palavras, destaque verde), mínimo (Helvetica limpo, sem destaque). Ou cole um exemplo que você goste."
Se eles colarem uma imagem/exemplo de referência: corresponda a fonte, tamanho, peso, cor, posição e animação o mais próximo possível — escreva um ASS personalizado manualmente ou estenda build_ass.py.
Senão, use o predefinido:
# Re-execute whisper no clipe cortado para timestamps precisos relativos ao início do clipe
whisper /tmp/clipify/clip_panned.mp4 --model tiny.en --word_timestamps True --output_format json --output_dir /tmp/clipify --language en
python3 <skill-dir>/scripts/build_ass.py /tmp/clipify/clip_panned.json /tmp/clipify/captions.ass opus
Queime as legendas:
ffmpeg -y -i /tmp/clipify/clip_panned.mp4 -vf "subtitles=/tmp/clipify/captions.ass" \
-c:v libx264 -preset fast -crf 20 -c:a copy "$OUTPUT.mp4"
Passo 6 — Entregar
- Salve cada saída em
<source_dir>/clipify_out/(crie o diretório se não existir) - Imprima uma linha por clipe: nome, duração, o que era engraçado, caminho de saída
- Abra a primeira saída com
open <caminho>para que o usuário possa verificar - Ofereça-se para iterar (estilo diferente, ROI diferente, trocar para tela dividida, reajustar legendas)
Armadilhas (lições de execuções anteriores — não repita)
- Detecção de áudio ≠ evento visível. Sempre execute o Passo 1.5 (verificar cada quadro candidato) antes de renderizar. O detector encontra o som de um corte, não se o cortador está em quadro. Golpes onde o sujeito está curvado, fora da câmera, ou onde um polegar está na lente ainda acionam o detector de áudio. Pegue-os antes de renderizar.
- Banda de fluxo espectral importa. Padrão
--band 1000:6000cobre impactos em madeira/metal/vidro. Batidas pesadas graves (bumbo, golpes no corpo) precisam de--band 200:2000. Assobios, tinidos, estalos de caixa precisam de--band 4000:10000. Se--min-flux 30não retornar nada, tente baixar para 15 primeiro; se retornar milhares, tente uma banda diferente antes de aumentar o limite. - Áudio de rampa de velocidade em pares.
atempoaceita apenas 0.5–2.0 em um único filtro; para 4x, encadeieatempo=2.0,atempo=2.0. Sematempo,setptssozinho dá áudio de esquilo. - Não ajuste demais as ROIs. Máximo de duas iterações. A diferença de movimento é tolerante — ROIs mais largas cobrindo boca+queixo funcionam bem mesmo se não perfeitamente centradas na boca.
- Cuidado com cortes de cena dentro de um clipe. Execute
ffmpeg -filter:v "select='gt(scene,0.3)',showinfo" -f null -para contar cortes. Se um clipe 16:9→9:16 tiver muitos cortes, as ROIs fixas de rosto funcionam apenas para a cena dominante; avise o usuário e ofereça-se para selecionar um clipe de tomada única ou aceitar enquadramento descentralizado durante os cortes. - Resolução da fonte importa. Se a fonte for 4K, reduza a resolução para 1920×1080 primeiro (mais rápido, suficiente para saída 9:16) ou multiplique todas as coordenadas ROI/panorâmica por 2.
- Legendas queimadas na fonte. Alguns clipes "brutos" ainda têm legendas. Nesse caso, encontre o mestre sem legendas via correlação cruzada de áudio (
audio_align.py) e corte a partir dele. - Não execute whisper na fonte completa de longa-metragem se um clipe curto for suficiente. Execute whisper no clipe cortado após o Passo 2; execute whisper na fonte completa apenas no Passo 1 se você precisar de uma transcrição para encontrar momentos engraçados.
- Declare o plano em uma linha, depois aja. Não narre cada iteração.