Edição básica de vídeo (cortar takes brutos para corresponder a um roteiro)
Por que esta skill existe
Um único take contínuo cheio de retomadas é enganosamente difícil de cortar corretamente apenas a partir de uma transcrição. Na primeira vez que este fluxo de trabalho foi feito à mão, confiar nos timestamps de segmento do próprio Whisper de uma única passagem de arquivo completo produziu um plano de corte onde uma frase parecia levar 22 segundos para ser dita. Na verdade, levou 5,75 segundos; o Whisper havia mesclado 16 segundos de silêncio e dois falsos começos naquele único segmento. Confiar nessa transcrição para cortes teria enviado um vídeo cheio de silêncio morto.
Guarde essa lição: o objetivo deste fluxo de trabalho são pontos de corte precisos, e o Whisper sozinho não oferece isso de forma confiável em gravações com pausas, retomadas ou linhas repetidas. Tudo abaixo existe para compensar isso.
Fluxo de trabalho
1. Analisar a fonte
Examine o vídeo em busca de informações básicas antes de fazer qualquer outra coisa:
ffprobe -v error -show_entries format=duration,size -show_entries stream=width,height,codec_name,codec_type,avg_frame_rate -of default=noprint_wrappers=0 "<video>"
Extraia áudio mono de 16 kHz para transcrição (o Whisper prefere isso, e é muito menor/mais rápido de processar do que o vídeo completo):
ffmpeg -y -i "<video>" -vn -ac 1 -ar 16000 audio.wav -loglevel error
Se o usuário forneceu um arquivo de roteiro/cenário (txt, rtf, docx, etc.), leia-o
agora para ter o conteúdo pretendido para comparar. Um .rtf geralmente
precisa de textutil -convert txt -stdout arquivo.rtf (macOS) para obter texto limpo
em vez de marcação RTF bruta.
2. Obter uma visão geral aproximada com uma transcrição de primeira passagem
whisper audio.wav --language <lang> --model medium --output_format txt --output_format srt --output_dir .
Use isso APENAS para ter uma noção do conteúdo geral e aproximadamente até onde as diferentes linhas estão na gravação. Não use seus horários de início/fim de segmento como pontos de corte reais ainda - veja "Por que esta skill existe" acima. Se a gravação for longa, isso pode ser executado em paralelo (ou ignorado em favor da) etapa 3.
3. Encontrar limites reais da fala com detecção de silêncio
Este é o passo que realmente importa. Execute:
python3 scripts/detect_bursts.py audio.wav > bursts.txt
Isso fornece "rajadas" de fala com precisão de amostra (o complemento do silêncio detectado), filtradas para excluir blips menores que 0,5s que são quase sempre ruído de respiração, não palavras. Confie nesses timestamps, não nos do Whisper.
Verificação de sanidade de escala: a fala natural tem cerca de 2-3 palavras por segundo. Se a duração de uma rajada parecer desproporcionalmente grande em relação à quantidade de texto que você esperaria caber nela, algo está errado.
4. Transcrever cada rajada isoladamente
python3 scripts/transcribe_bursts.py audio.wav bursts.txt --language <lang> --model medium
Isso carrega o modelo Whisper uma vez e, em seguida, transcreve cada rajada como seu próprio
pequeno clipe com condition_on_previous_text=False e temperature=0. Essa
combinação é o que realmente revela retomadas ocultas: uma rajada que
uma passagem de arquivo completo rotula como uma linha limpa pode conter, uma vez isolada,
um falso começo imediatamente seguido pela entrega real - totalmente
invisível na transcrição de primeira passagem, que silenciosamente mesclou ou
suprimiu a repetição.
Espere que uma gravação com retomadas notórias produza mais rajadas do que linhas de roteiro - geralmente vários fragmentos de falso começo por linha, especialmente na linha com que o falante mais lutou. Isso é normal; é exatamente para isso que serve esta etapa.
5. Construir o plano de edição - e obter confirmação antes de cortar qualquer coisa
Agrupe as rajadas transcritas por qual linha/ideia do roteiro elas pertencem. Para cada linha, você normalmente verá um de:
- Uma rajada limpa e completa - fácil, esse é o take.
- Um falso começo seguido por uma rajada completa - use o completo, descarte o fragmento.
- Vários takes completos com as mesmas palavras - são duplicatas de desempenho genuínas; você não pode julgar entrega/emoção apenas por uma transcrição, então corte com um padrão razoável (por exemplo, a tentativa posterior, já que as pessoas geralmente se estabelecem em um take) e deixe o usuário escolher no modo Dublovi (abaixo).
- Vários takes completos com palavras genuinamente diferentes (o falante improvisou de forma diferente a cada vez) - esta é uma escolha de conteúdo real, não apenas uma de desempenho. Apresente as alternativas ao usuário explicitamente em vez de escolher silenciosamente - eles podem preferir sua própria frase improvisada ao roteirizado, ou vice-versa.
Para ambos os tipos de takes duplicados, o usuário decide junto com você,
assistindo a eles: os takes vão para o modo Dublovi do editor de plano (dois
cortes lado a lado, esquerda / direita, "Ovaj je bolji"), em TODO vídeo. Não
descreva takes no chat e não pergunte "dubl 1 ili 2" lá. Corte com sua
escolha padrão, liste cada duplicata em takes_src.json (skill editor de plano,
"Dublovi") e abra o editor; troque o take no corte se eles escolherem
outro.
Em seguida, escreva o plano completo como uma mensagem para o usuário: o texto exato sendo mantido para cada linha, seu intervalo de timestamp exato e o que está sendo cortado e por que (silêncio morto, falsos começos, takes alternativos descartados). Aguarde confirmação explícita antes de executar qualquer comando de corte. Isso não é uma formalidade - editar um vídeo parece destrutivo e é difícil verificar sem assistir à saída, então o usuário precisa ver o plano para detectar problemas (take errado escolhido, uma linha que não soa natural costurada ao lado de outra) antes que o tempo de renderização seja gasto nisso, não depois.
Se o usuário perguntar sobre duração total, apenas some as durações dos segmentos mantidos
- mas avise que a duração final real será um pouco maior depois que o preenchimento entre os cortes for adicionado (etapa 6).
6. Cortar e montar
Escreva as seleções confirmadas (um par início<TAB>fim por linha, na ordem
de saída) em um arquivo, então:
scripts/cut_and_concat.sh "<source_video>" selections.txt final_output.mp4
Leia o bloco de comentários no topo de cut_and_concat.sh antes de executá-lo
manualmente ou adaptá-lo - ele documenta pegadinhas (ffmpeg roubando stdin de um
loop bash, fades comendo a última sílaba de uma palavra, por que cada clipe precisa de
re-codificação individual antes da concatenação) que são fáceis de reintroduzir
silenciosamente se reescritos do zero em vez de usar o script como está.
Padrões: 0,15s de preenchimento antes de cada corte, 0,35s após cada corte,
um fade-in de 5 ms apenas (sem fade-out), h264 re-codificado (crf 18) + aac 192k
por clipe, cada clipe cortado para uma contagem exata de quadros com seu áudio cortado para o mesmo
comprimento, unidos com o filtro de concatenação ffmpeg (nunca o demuxer de
concatenação - veja "Verificar cada renderização" abaixo). Substitua via variáveis de
ambiente PRE_PAD, POST_PAD, CRF, FADE_IN se um corte específico precisar
de tratamento diferente (por exemplo, uma linha com muito ruído de respiração no final pode
precisar de mais POST_PAD).
Se a última letra de uma frase ainda soar cortada após uma primeira passagem, isso quase sempre significa que o POST_PAD está muito apertado para aquele corte específico (a consoante final do falante se estende além do limite que o detector de silêncio capturou) - aumente-o para aquele segmento em vez de recorrer a um fade, que apenas torna um corte apertado pior, não melhor.
Ritmo apertado - o padrão do usuário
O preenchimento padrão (0,15s / 0,35s) deixa uma pausa de ~0,5s em cada corte. O usuário descobriu que isso quebrava o ritmo ("mora biti sve zbijenije i tečnije"), então, para seus vídeos, aperte cada corte antes de montar:
python3 scripts/tighten_cuts.py audio.wav selections.txt tight.txt
PRE_PAD=0.04 POST_PAD=0.08 scripts/cut_and_concat.sh "<source_video>" tight.txt final_output.mp4 <abs workdir>
Isso move cada borda para onde a fala realmente começa / termina (-42 dB) e divide qualquer pausa interna maior que 0,30s. Em um rolo real, isso reduziu de 1:48 para 1:41 com cada palavra intacta. Depois, verifique transcrevendo o resultado: cada palavra do texto mantido ainda deve estar lá. Os tempos de qualquer coisa colocada no corte antigo (close-ups, legendas, título) devem ser re-mapeados através do tempo de origem, não deslocados por uma constante. Quando duas partes cortadas separadamente são unidas (por exemplo, principal + encerramento), pegue o ponto de junção da contagem real de quadros da primeira parte (nb_read_frames / fps), não da sua duração de contêiner: em um rolo real, a diferença de 0,04s deixou um quadro solta do plano geral entre um close-up e o encerramento, e a primeira legenda do encerramento apareceu um quadro antes. Uma sobreposição que termina na junção deve ir até aquele quadro.
Verificar cada renderização - antes de enviar qualquer coisa
Em um rolo real, o usuário notou um quadro solto do plano geral em
1:05 ("to ne sme da nam se dešava"). A causa real era maior: o antigo
cut_and_concat.sh unia clipes com o demuxer de concatenação, que coloca cada
clipe por sua duração de contêiner e preenchimento/priming AAC, não por seus quadros
reais. Isso deixou um buraco de 80 ms (2 quadros) em 10 dos cortes - um quadro
congelado na tela após a re-codificação - e o áudio lentamente se adiantou em relação à
imagem, então cada sobreposição e legenda com base nele atrasou um quadro ou dois.
O script corrigido (pegadinha #4 em seu cabeçalho) corta cada clipe para uma contagem exata
de quadros, preenche/corta seu áudio para exatamente esse comprimento e une com
o filtro de concatenação.
Regras que se seguem disso:
-
Execute o verificador em TODA renderização (corte, zoom, legendado, final) antes de enviá-lo ao usuário:
python3 scripts/check_video.py final.mp4 --ass captions.ass --edges edges.txt --cuts joins.txtjoins.txt= cada junção da lista de corte na linha do tempo final (somas crescentes dasdurs.txtde cada parte, mais os limites das partes). Sempre passe isso: um corte brusco dentro do mesmo enquadramento pontua muito abaixo do limite de cena, então sem ele o verificador perdeu uma mudança de zoom chegando 3 quadros antes de uma junção em um rolo real (1:15) - um flash de 3 quadros que o usuário viu.Ele relata buracos de timestamp, quadros congelados solitários, comprimento de áudio vs vídeo, tomadas com apenas alguns quadros de duração, legendas mais curtas que 0,4s ou começando / terminando 1-3 quadros de um corte de imagem, e bordas de sobreposição (
edges.txt: um início ou fim de close-up / B-roll por linha, segundos) a 2-5 quadros de um corte. Exit 1 = algo foi encontrado: corrija e renderize novamente; apenas "OK - nothing found" é enviado. -
Tire cada posição de clipe de contagens de quadros (
nb_frames / fps, ou odurs.txtque o script escreve), nunca deformat=duration. -
Quando o corte muda, mova close-ups, legendas e o título através do tempo de ORIGEM (linha do tempo antiga -> fonte -> nova linha do tempo), nunca por uma constante.
-
Uma sobreposição que termina em um corte (um close-up antes do encerramento) vai exatamente até o quadro daquele corte; uma legenda termina com seu clipe.
-
Encaixe toda mudança de zoom, fim de close-up e borda de legenda que cair dentro de 6 quadros de uma junção DENTRO DESSA junção (um início de close-up apenas quando a junção está logo antes dele). Duas mudanças de imagem a alguns quadros de distância sempre são lidas como uma falha; em um quadro, são lidas como um corte.
-
Dê ao último segmento um pequeno final (~0,5s) para que a última legenda e palavra não sejam cortadas.
7. Entregar
A saída mestre de cut_and_concat.sh é de qualidade total (re-codificada em
crf 18). Se precisar passar por uma ferramenta com limite de tamanho de arquivo, faça uma
cópia comprimida secundária para esse fim, 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
Se o usuário também quiser zoom dinâmico/cortes com aproximação para dinamismo, veja a
skill zoom - ela pode pegar a saída desta skill e re-cortá-la, ou
(mais eficientemente) você pode combinar ambas em uma passagem escrevendo as
cadeias de filtro por segmento da skill zoom diretamente na
terceira coluna de selections.txt antes de executar cut_and_concat.sh uma vez.
A rotação para vertical, se necessária, é tratada separadamente (não faz parte de nenhuma
skill) - pergunte ao usuário, pois nem sempre é desejada.
Notas de plataforma
- O macOS não tem
timeoutGNU por padrão - não envolva chamadas ffmpeg em um; elas são rápidas para clipes curtos, e o próprio tratamento de background/timeout da ferramenta de chamada cobre a passagem longa de arquivo completo na etapa 2. - Sempre que você escrever um loop
while read ...; done < filevocê mesmo (em vez de usar os scripts incluídos) e chamarffmpegdentro dele, adicione-nostdinà chamada ffmpeg e redirecione seu stdin de/dev/null- caso contrário, o ffmpeg consome linhas destinadas aoreaddo loop, corrompendo qual timestamp se aplica a qual iteração sem nenhum erro óbvio.
Após o corte: zoom e legendas passam pelo editor de plano
Quando o vídeo cortado também receber zoom/close-ups (skill zoom) e/ou
legendas (skill de legenda), o padrão do usuário é a skill
plan-editor: uma ferramenta interativa de clicar-em-uma-palavra onde eles decidem o zoom com
você e corrigem o texto da legenda nas mesmas frases ("Izmeni tekst"). Mantenha o
texto mantido por clipe do plano desta skill - ele se torna o
sentences.txt daquela ferramenta (uma linha por clipe cortado, mesma ordem que selections.txt).