Communitygithub.com

Istok-Media-Tool/zoom

Planeje zoom junto com o usuário na ferramenta de edição de plano (clique-na-palavra), depois adicione cortes dinâmicos de zoom a um vídeo de cabeça falante já cortado, nos momentos impulsionados pelo conteúdo, centralizados corretamente no sujeito com o espaçamento superior adequado — para que os cortes entre diferentes níveis de zoom pareçam uma escolha cinematográfica intencional, em vez de o sujeito pular visivelmente pelo quadro.

O que é zoom?

Esta skill para agentes de IA (como Claude Code, Cursor ou Codex) permite planejar e renderizar zoom dinâmico em vídeos de cabeça falante já cortados. Ela resolve problemas comuns de zoom automático, como cortes que fazem o sujeito pular no quadro, zoom excessivo ou mal posicionado, e a falta de tratamento consistente entre planos abertos e fechados. O fluxo de trabalho exige que o agente use uma ferramenta de edição de plano (plan-editor) para que o usuário decida, palavra por palavra, onde o zoom deve ocorrer — com mudanças em palavras-chave específicas, não aleatoriamente. A skill mede a posição do sujeito em cada segmento de vídeo (usando `ffmpeg` para extrair quadros de referência), calcula cortes com `compute_crop.py` que centralizam o rosto e mantêm o espaçamento superior adequado (aproximadamente 9% da altura do quadro), e aplica zoom usando `build_zoom_selections.py`. Um dos diferenciais é que até mesmo os planos abertos recebem um corte suave (não ficam sem corte), garantindo que o centro óptico e o espaçamento superior sejam consistentes entre todos os planos — eliminando a sensação de que o sujeito "pula" lateralmente entre um plano aberto e um zoom. A skill também verifica objetos importantes no quadro (como um tablet ou produto) para garantir que não sejam cortados, ajustando o zoom ou a posição do corte conforme necessário. A skill é compatível com plataformas que usam o formato de skill markdown (skills/) e depende de scripts Python e comandos ffmpeg. Ela inclui validação passo a passo: renderizar um segmento de teste, extrair múltiplos quadros e verificar se o sujeito não está cortado antes de renderizar o vídeo completo. O resultado final é um vídeo onde os cortes de zoom parecem intencionais e suaves.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Istok-Media-Tool/video-edit-skills/tree/HEAD/video-edit/skills/zoom

Perguntar na sua IA favorita

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

Documentação

$SKILLS abaixo é a pasta que contém a pasta desta skill: o pai do diretório base mostrado quando a skill carrega (a pasta skills/ do plugin).

Zoom dinâmico

Por que esta skill existe

Adicionar zoom "dinâmico" a um vídeo cortado de cabeça falante é enganosamente fácil de errar de maneiras que só aparecem quando você assiste ao resultado, não apenas pelos números. Em um caso real, o mesmo vídeo passou por várias rodadas corrigidas de feedback antes de ficar certo — cada uma ensinou algo que vale a pena não reaprender do zero:

  1. Uma abordagem ingênua de zoom a cada corte parecia barata e chamativa — o julgamento real de edição é mais esparso e deliberado do que isso.
  2. Um corte centralizado simetricamente ao nível dos olhos, em vez de ancorado no topo real da cabeça, deixou espaço vazio excessivo acima da cabeça após a ampliação (~17% de espaçamento superior quando ~9% parecia certo).
  3. Reutilizar a posição da cabeça medida de UM segmento para o cálculo do corte em OUTROS segmentos da mesma gravação colocou o sujeito visivelmente descentralizado em segmentos onde ele se mexeu na cadeira minutos antes ou depois na tomada — mesmo sendo uma única gravação contínua.
  4. Deixar os planos "abertos"/sem zoom completamente sem corte, enquanto apenas corrigia os planos com zoom, fez com que cada corte entre um plano aberto e um com zoom ainda pulasse visivelmente o sujeito lateralmente — os planos abertos precisavam do mesmo tratamento de recentralização e espaçamento superior, apenas em um nível de zoom muito menor, para que os cortes entre eles parecessem uma única câmera coerente.
  5. Um nível de zoom por clipe, e então um zoom mecânico colocado no meio de cada frase, ambos pareceram ALEATÓRIOS para o usuário. O zoom deve seguir o roteiro: cada mudança atinge a palavra ou frase específica que a justifica (uma revelação, um termo-chave, uma pergunta pessoal, a piada), e os recuos atingem palavras sobre escala ("universo", "bilhões de anos"). As mudanças acontecem no meio do clipe, às vezes mais de uma vez por clipe.
  6. O usuário quer DECIDIR o plano de zoom juntos, palavra por palavra, usando a ferramenta de edição de plano (Passo 1). Use-a sempre — para qualquer roteiro.
  7. O usuário escolheu os níveis de zoom: 1,0x, 1,5x, 1,7x, 2,0x, 2,2x (níveis de plano 0-4). "1,0x" (nível 0) NÃO é o quadro bruto: renderizado como o quadro bruto sem corte, deixava o sujeito descentralizado com muito espaço acima da cabeça, e o usuário pediu que fosse centralizado com o mesmo espaçamento superior de todos os outros planos. Então nível 0 = o corte --baseline (corte mais aberto que atende aos 9% de espaçamento superior, recentralizado; ~1,2x na prática) — build_zoom_selections.py faz isso automaticamente. Mantenha o rótulo "1,0x" na ferramenta; significa "mais aberto, centralizado".

Passo 1: Planeje o zoom JUNTOS na ferramenta de edição de plano

Obrigatório para cada vídeo: carregue e siga a skill plan-editor ($SKILLS/plan-editor/SKILL.md). É a ferramenta padrão de clique-na-palavra do usuário, onde ele decide o zoom com você e, via "Izmeni tekst", corrige o texto das legendas nas mesmas frases. Nunca renderize zoom a partir de um plano que o usuário não moldou lá.

Sua função nesse passo é a proposta inicial: construa-a a partir da curva de energia do conteúdo — estabelecimento/calmo = aberto, uma reviravolta ou piada merece um zoom EM SUA PALAVRA-CHAVE, uma batida explicativa recua para respirar, o clímax ganha o quadro mais fechado; diferenças de nível decisivas (sem aumento sutil); pelo menos um retorno real ao aberto após a abertura — e dê a razão para cada mudança. A resposta do usuário (PLAN_JSON, SENTENCES_JSON) é o plano confirmado; renderize-o exatamente no Passo 5.

Aplicar o plano precisa de timestamps de palavras (Passo 5) e posições do sujeito por clipe (Passo 2).

examples/sample/ contém um conjunto real confirmado (frases, plano do usuário, posições, cortes) para um vídeo gravado de lado e rotacionado com transpose=1 — uma referência para formatos de arquivo e para o tipo de plano que este usuário escolhe.

Passo 2: Meça a posição do sujeito — POR SEGMENTO, sempre

Nunca reutilize a posição da cabeça medida de um segmento para o cálculo do corte de outro segmento, mesmo dentro da mesma gravação contínua — alguém pode se mexer na cadeira quando você está vários minutos adiante em uma longa tomada, e um corte computado a partir do quadro de um segmento inicial pode descentralizar, ou em zoom mais fechado realmente cortar, o sujeito em um segmento posterior que parece semelhante, mas não está enquadrado exatamente da mesma forma.

Para cada segmento que você está prestes a cortar (incluindo os "abertos" — veja o passo 4):

ffmpeg -nostdin -y -ss <mid_timestamp_of_that_segment> -i source.mp4 -frames:v 1 ref.png
ffmpeg -nostdin -y -i ref.png -vf "drawgrid=w=54:h=96:t=1:[email protected]" grid.png

(54x96 fornece uma grade 20x20 em um quadro 1080x1920 — dimensione proporcionalmente para outras resoluções.) Olhe para grid.png e leia:

  • cx - o x do centro do rosto/nariz
  • head_top_y - o y do topo do cabelo (não os olhos — o espaçamento superior é medido a partir do topo da cabeça)

Passo 3: Calcule o corte

python3 scripts/compute_crop.py --cx <measured_x> --head-top-y <measured_y> --zoom 1.7
python3 scripts/compute_crop.py --cx <measured_x> --head-top-y <measured_y> --baseline
  • --zoom Z para um nível de zoom explícito (ex.: 1.7 para um plano de 170%).
  • --baseline para os planos "abertos": encontra o corte MAIS ABERTO que ainda satisfaz o alvo de espaçamento superior e recentraliza horizontalmente — isso é o que faz com que os planos abertos sejam apenas levemente cortados (não literalmente 100%/intocados), mas compartilhem exatamente o mesmo centro óptico e espaçamento superior que os planos com zoom, que é o que realmente evita que os cortes pareçam que o sujeito pula. Não pule o corte dos planos abertos — deixá-los brutos enquanto só corrige os com zoom foi a maior fonte única de "algo parece estranho" nos testes, maior do que errar qualquer nível de zoom individual.

O espaçamento superior padrão é 9% da altura do quadro cortado, que pareceu correto nos testes — abaixo disso parecia apertado, e não é apenas estético: uma plataforma vertical (Instagram/TikTok/Shorts) sobrepõe o nome de usuário/legenda/UI de fade no topo do quadro, então essa margem é espaço funcional que a plataforma cobrirá, não espaço extra. Substitua com --headroom se um projeto específico exigir algo diferente, mas não vá muito abaixo de ~9% para qualquer coisa destinada a essas plataformas.

A justificativa completa para tudo acima está no docstring de scripts/compute_crop.py — é curto, leia-o se algo aqui não estiver claro em um caso específico.

Passo 4: Verifique a saída renderizada real antes da renderização completa

Um corte que parece bom em um único quadro estático ainda pode cortar ou descentralizar o sujeito quando você verifica o movimento. Para cada segmento (especialmente aquele que mostrou mais movimento corporal — inclinar-se, inclinar a cabeça — quando você estava escolhendo o quadro de referência):

  1. Renderize apenas aquele segmento com seu corte aplicado.
  2. Amostre 3+ quadros distribuídos ao longo de sua duração real (não apenas aquele que você mediu), ex.: ffmpeg -i clip.mp4 -vf fps=3 check_%02d.png.
  3. Olhe para eles. Se a cabeça estiver cortada ou apertada demais contra uma borda, afrouxe o zoom desse segmento ou amplie sua margem antes de prosseguir.

Somente após esta verificação passar você deve renderizar todos os segmentos e concatenar.

Passo 5: Transforme o plano confirmado em cortes ancorados em palavras e monte

  1. Obtenha timestamps de palavras a partir de uma renderização do mesmo corte (um corte simples, ou uma renderização de zoom anterior — o padding deve corresponder) e anote as durações por clipe dessa renderização (use ffprobe em cada clip_NNN.mp4 em seu diretório de trabalho):
    python3 scripts/word_times.py cut_render.mp4 words.json --language sr
    
  2. Construa o arquivo de seleções. cuts.txt = os start<TAB>end não preenchidos da skill básica por clipe; pos.json = [[cx, head_top_y], ...] por clipe do Passo 2 (medido no quadro rotacionado se --pre-filter girar):
    python3 scripts/build_zoom_selections.py --cuts cuts.txt --positions pos.json \
      --plan plan.json --sentences sentences.txt --words words.json \
      --durations 6.28,6.08,... [--pre-filter transpose=1] > selections.txt
    
    Leia o relatório de correspondência que ele imprime: toda palavra planejada deve dizer exact (ou uma variante de ortografia fuzzy~ sensata — o Whisper pode escrever nomes de forma diferente ou em cirílico). Qualquer linha proportional (no match!) deve ser corrigida manualmente antes de renderizar. A mudança ocorre 0,08s antes da palavra (--lead). Clipes com vários níveis são renderizados como um clipe contínuo (dividido + overlay temporizado), então um zoom no meio da frase nunca adiciona um corte ou uma costura de áudio.
  3. Verifique quadros 0,2s antes e depois de cada mudança de zoom (lógica do Passo 4: cabeça nunca cortada, mudança claramente visível), então renderize:
scripts/cut_and_concat.sh "<source_video>" selections.txt final_output.mp4 "$PWD/build"

Passe o diretório de trabalho como um caminho ABSOLUTO — com um relativo, a etapa final de concatenação falha ao encontrar concat_list.txt.

As durações dos clipes renderizados podem diferir das da renderização anterior em alguns centésimos de segundo, portanto, se as legendas (skill de legenda) foram temporizadas em relação a uma renderização anterior, desloque cada legenda pelo novo início de seu clipe antes de queimá-las.

Leia o bloco de comentários no topo de cut_and_concat.sh antes de adaptá-lo — ele documenta pegadinhas do ffmpeg/bash (roubo de stdin dentro de loops, fades-out comendo a última sílaba de um corte, por que os clipes precisam de re-encoding individual antes da concatenação) que são fáceis de reintroduzir silenciosamente de outra forma.

Exemplo de arquivo de seleções misturando um segmento de linha de base (aberto) e dois zooms:

63.82	69.57	crop=908:1614:152:304,scale=1080:1920
80.37	85.90	crop=636:1130:288:348,scale=1080:1920
97.25	100.81	crop=908:1614:152:304,scale=1080:1920

Se a fonte também precisar de uma correção de rotação/orientação, isso é tratado separadamente (não faz parte desta skill, pois nem sempre é necessário) — anexe o filtro de rotação acordado à cadeia de filtros de cada linha você mesmo, se aplicável aqui.

Entrega

Se a saída mestre exceder o limite de tamanho de arquivo de uma ferramenta, faça uma cópia comprimida secundária em vez de comprimir o mestre:

ffmpeg -nostdin -y -i final_output.mp4 -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 128k final_output_share.mp4 -loglevel error

Objetos que o apresentador mostra devem permanecer inteiros em todos os zooms

Em um vídeo real, o apresentador mostra fotos em um tablet ao lado dele; cortes centralizados no rosto cortavam sua borda esquerda ("ne sme tablet da bude isečen ovako nigde"). Antes de renderizar, liste tudo no quadro sobre o que o vídeo TRATA (um tablet, um produto, uma página segurada) e meça sua caixa na grade. Cada corte de zoom deve conter essa caixa com ~30px de ar: desloque o x/y do corte em direção a ela (o rosto pode ficar descentralizado, até ~70% para o lado), e se o rosto tocar a borda, use um nível mais aberto para essa mudança (1,7x -> 1,6x em um vídeo real) em vez de cortar qualquer um. Olhe para um quadro de cada faixa de zoom para confirmar que ambos estão inteiros.

Individual skills in this repo

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

Habilidades Relacionadas