$SKILLSabaixo é a pasta que contém a pasta desta skill: o pai do diretório base mostrado quando a skill carrega (a pastaskills/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:
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.pyfaz 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/narizhead_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 Zpara um nível de zoom explícito (ex.:1.7para um plano de 170%).--baselinepara 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):
- Renderize apenas aquele segmento com seu corte aplicado.
- 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. - 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
- 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
ffprobeem cadaclip_NNN.mp4em seu diretório de trabalho):python3 scripts/word_times.py cut_render.mp4 words.json --language sr - Construa o arquivo de seleções.
cuts.txt= osstart<TAB>endnã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-filtergirar):
Leia o relatório de correspondência que ele imprime: toda palavra planejada deve dizerpython3 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.txtexact(ou uma variante de ortografiafuzzy~sensata — o Whisper pode escrever nomes de forma diferente ou em cirílico). Qualquer linhaproportional (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. - 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.