Automatizar destaques de vídeo com Ruby e FFmpeg
Encontrar momentos úteis em gravações longas leva tempo. O Ruby pode automatizar a detecção de mudanças de cena, extrair clipes candidatos e reuni-los em uma compilação de revisão com o FFmpeg. Essa abordagem usa uma heurística de mudanças visuais, não um sistema que entende quais momentos são importantes.
Introdução aos destaques de vídeo automatizados
O fluxo de trabalho detecta grandes mudanças entre quadros adjacentes, seleciona instantes suficientemente espaçados e codifica clipes curtos ao redor deles. Um corte, um clarão ou uma transição de slide pode ser um candidato útil, mas um gol ou uma observação relevante dita em voz alta pode não ter nenhum corte de cena. Revise a compilação gerada antes de publicá-la.
Configurar Ruby e FFmpeg
Use o Bash no Linux com Ruby, FFmpeg e sua ferramenta complementar ffprobe
no PATH. Este tutorial foi testado com Ruby 3.4.10 e FFmpeg/ffprobe 9.0.1.
Essas são versões testadas, não requisitos exatos de instalação. Use uma versão de correção atual
do Ruby 3.4 seguindo o guia de instalação do Ruby e confira seu
status de manutenção.
No Ubuntu/Debian, instale FFmpeg e ffprobe juntos:
sudo apt-get update && sudo apt-get install -y --no-install-recommends ffmpeg
O exemplo usa as bibliotecas padrão do Ruby sem um wrapper de terceiros para o FFmpeg. Antes de salvar os scripts, verifique se o Ruby consegue carregar essas bibliotecas e inspecione os filtros e codificadores da sua compilação do FFmpeg:
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE ruby -v &&
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE ruby -e "require 'fileutils'; require 'json'; require 'open3'" &&
ffmpeg -version && ffprobe -version && ffmpeg -filters && ffmpeg -encoders
Compatibilidade de versões
Sua compilação do FFmpeg precisa dos filtros
select, metadata, atrim,
asetpts e concat e dos codificadores
libx264 e aac. A mesma receita também funciona
com FFmpeg 6.1.1; use um pacote com as correções disponíveis para sua distribuição. A chamada limpa
as configurações herdadas do carregador do Ruby e do Bundler, de modo que executar os scripts
dentro de um projeto Ruby não seleciona as gems desse projeto.
Escolha um vídeo local que você sabe estar em boas condições, com dimensões fixas dos quadros e cores SDR. O script seleciona a primeira faixa de vídeo e, quando presente, a primeira faixa de áudio. O áudio deve cobrir os intervalos de vídeo selecionados. Uploads não confiáveis, gravações danificadas e arquivos que mudam de resolução durante a reprodução precisam de validação separada e de uma unidade de processamento isolada; esta CLI não fornece esse isolamento. O FFmpeg pode se recuperar de um pacote danificado, imprimir um aviso e ainda retornar um status de sucesso. A conclusão de um lote não valida a integridade da fonte; revise os diagnósticos e os cortes gerados.
Extrair momentos importantes usando filtros do FFmpeg
A pontuação de cena do FFmpeg estima as diferenças visuais entre
quadros. Para uma análise manual em um novo diretório de trabalho, este comando grava os instantes
dos quadros selecionados em timestamps.txt:
ffmpeg -nostdin -n -threads 2 -filter_threads 1 -i input.mp4 -map 0:v:0 \
-vf "select='gt(scene,0.3)',metadata=print:file=timestamps.txt" \
-an -f null -
Mantenha a detecção de cenas e a extração de clipes na linha do tempo do FFmpeg relativa à entrada.
Não redefina os timestamps do fluxo de vídeo de forma independente: o áudio pode começar antes do
primeiro quadro de vídeo. Tanto a detecção quanto a extração selecionam explicitamente a primeira
faixa de vídeo. O arquivo de texto do filtro é separado das verificações habituais de arquivos de
saída do FFmpeg: não reutilize um timestamps.txt existente.
A implementação em Ruby cria um diretório privado da tarefa para esse arquivo.
Implementar scripts Ruby para automação
Salve esta classe como highlight_extractor.rb. Ela mantém os nomes dos arquivos gerados
fixos e passa os caminhos de entrada como argumentos separados do processo, de modo que esses
caminhos não entram na sintaxe do shell ou dos filtros. Uma etapa que falha gera um erro, em vez de
produzir silenciosamente um relatório de lote bem-sucedido.
require 'fileutils'
require 'json'
require 'open3'
class HighlightExtractor
def initialize(input_file, output_dir)
@input = File.expand_path(input_file)
raise ArgumentError, 'Input must be a regular file' unless File.file?(@input)
@output = File.expand_path(output_dir)
end
def generate(threshold: 0.3, clip_duration: 5.0, pre_roll: 2.0, min_gap: 10.0)
unless threshold.finite? && (0.0..1.0).cover?(threshold) &&
clip_duration.finite? && clip_duration > 0 &&
pre_roll.finite? && pre_roll >= 0 && min_gap.finite? && min_gap >= clip_duration
raise ArgumentError, 'Invalid scene or clip settings'
end
# Refuse earlier job outputs before taking ownership of this directory.
Dir.mkdir(@output, 0700)
begin
duration = video_duration
selected = detect_scene_changes(threshold).each_with_object([]) do |time, result|
next if time >= duration
result << time if result.empty? || time - result.last >= min_gap
end
# Cap output count so a noisy video cannot create an unlimited clip set.
lengths = []
clips = selected.first(20).each_with_index.map do |time, index|
start = [time - pre_roll, 0.0].max
length = [clip_duration, duration - start].min
lengths << length
name = format('highlight_%03d.mp4', index + 1)
execute('-i', @input, '-ss', start.to_s, '-t', length.to_s,
'-map', '0:v:0', '-map', '0:a:0?', '-c:v', 'libx264', '-preset', 'fast',
'-crf', '23', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-threads', '2', name)
name
end
create_highlight_compilation(clips, lengths) unless clips.empty?
clips.length
rescue StandardError
FileUtils.remove_entry(@output)
raise
end
end
private
def video_duration
output, status = Open3.capture2('ffprobe', '-v', 'error', '-show_entries',
'format=duration:stream=codec_type', '-of', 'json', @input)
raise IOError, 'Cannot inspect input video' unless status.success?
info = JSON.parse(output)
@has_audio = info.fetch('streams').any? { |stream| stream.fetch('codec_type') == 'audio' }
duration = Float(info.fetch('format').fetch('duration'))
raise ArgumentError, 'Invalid video duration' unless duration.finite? && duration > 0
duration
end
def execute(*arguments)
unless system('ffmpeg', '-nostdin', '-n', '-threads', '2', '-filter_threads', '1',
'-filter_complex_threads', '1',
*arguments, chdir: @output)
raise IOError, 'FFmpeg processing failed'
end
end
def detect_scene_changes(threshold)
execute('-i', @input, '-map', '0:v:0', '-vf',
"select='gt(scene,#{threshold})',metadata=print:file=timestamps.txt",
'-an', '-f', 'null', '-')
File.foreach(File.join(@output, 'timestamps.txt')).each_with_object([]) do |line, times|
match = line.match(/pts_time:([0-9.eE+-]+)/)
times << Float(match[1]) if match
end.uniq.sort
end
def create_highlight_compilation(clips, lengths)
list = clips.each_with_index.map do |name, index|
"file '#{name}'\nduration #{lengths[index]}\n"
end.join
File.write(File.join(@output, 'filelist.txt'), list)
arguments = ['-copyts', '-f', 'concat', '-safe', '1', '-i', 'filelist.txt']
if @has_audio
clips.each { |name| arguments.concat(['-i', name]) }
filters = clips.each_index.map do |index|
"[#{index + 1}:a:0]atrim=duration=#{lengths[index]},asetpts=PTS-STARTPTS[a#{index}]"
end
inputs = clips.each_index.map { |index| "[a#{index}]" }.join
filters << "#{inputs}concat=n=#{clips.length}:v=0:a=1[audio]"
arguments.concat(['-filter_complex', filters.join(';'), '-map', '0:v:0', '-map',
'[audio]', '-c:v', 'copy', '-c:a', 'aac'])
else
arguments.concat(['-map', '0:v:0', '-c:v', 'copy'])
end
execute(*arguments, 'highlight_reel.mp4')
end
end
O demultiplexador concat precisa de fluxos e bases de tempo compatíveis. Esses clipes compartilham a mesma fonte e as mesmas configurações do codificador; a lista contém apenas os nomes de arquivo gerados, sem os diretórios, de modo que o modo de caminhos seguros permanece ativado. Durações explícitas posicionam cada clipe de vídeo na linha do tempo da compilação. A compilação copia o vídeo codificado. Quando há áudio, ela decodifica cada clipe separadamente, corta o áudio para a duração solicitada do clipe e o une usando o filtro concat antes de recodificá-lo em AAC. Isso impede que o preenchimento adicionado pelo codificador dos clipes se acumule como atraso no áudio. Essa compilação de revisão não é um master de áudio sem lacunas nem um concatenador de arquivos sem relação entre si. Ouça os pontos de junção antes de publicar.
O codificador libx264 usa CRF 23 como meta
de qualidade, sem limite de taxa de bits. Um valor menor geralmente preserva mais detalhes e
aumenta o tamanho do arquivo. A predefinição fast controla o esforço de
codificação, não quais cenas são selecionadas.
A opção -ss aplicada à saída decodifica
e descarta o conteúdo anterior ao instante inicial de cada clipe. Esse método direto de
decodificação não depende de índices utilizáveis para saltar na linha do tempo, mas repete o
trabalho para destaques próximos do fim. Para gravações longas com índices, meça o desempenho do
posicionamento preciso na linha do tempo aplicado à entrada e verifique os limites dos clipes e
as defasagens entre áudio e vídeo antes de mudar esse comportamento.
Salve este script de chamada ao lado da classe como generate_highlights.rb:
require_relative 'highlight_extractor'
abort 'Usage: ruby generate_highlights.rb <input-video> <new-output-directory>' unless ARGV.length == 2
begin
count = HighlightExtractor.new(*ARGV).generate
puts(count.zero? ? 'No scene changes selected' : "Created #{count} candidate highlights")
rescue StandardError
warn 'Highlight generation failed. Check the input, destination, and FFmpeg installation.'
exit 1
end
Execute-o no terminal:
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE \
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Mantenha os dois scripts juntos e substitua my_video.mp4 pelo caminho do seu
arquivo de entrada. Coloque entre aspas os caminhos que contêm espaços. Uma execução bem-sucedida
informa a quantidade de candidatos e grava highlight_001.mp4, os clipes numerados
seguintes e highlight_reel.mp4 dentro de my_highlights_folder. Abra a
compilação no seu reprodutor de vídeo. Por padrão, cada clipe começa 2 segundos antes do corte
selecionado, sem ultrapassar o início da entrada, e dura até 5 segundos. Os cortes são selecionados
em ordem cronológica, com pelo menos 10 segundos de intervalo, até um máximo de 20 clipes.
Um diretório de saída existente é recusado e permanece intacto; escolha um novo nome para tentar novamente. Um erro de processamento após a criação do diretório da tarefa remove as saídas parciais dessa tarefa. Corrija a entrada ou a instalação e repita o mesmo comando. Um vídeo válido sem mudanças de cena selecionadas imprime No scene changes selected, mantém o arquivo de timestamps e termina com sucesso, sem clipes ou compilação. Os diagnósticos locais do FFmpeg podem incluir nomes de arquivos; evite incluir caminhos sensíveis em logs compartilhados.
Otimização de desempenho
Execute primeiro uma gravação curta e representativa e verifique os cortes resultantes. O limite de vinte clipes restringe os clipes gerados, não a duração da entrada, a análise completa das cenas ou o tamanho da saída. Deixe espaço suficiente em disco para os clipes individuais e a compilação. Os threads do decodificador e do codificador são limitados aqui; executar várias tarefas ainda multiplica o uso de CPU e armazenamento.
Otimizar a seleção de destaques
Um limiar de cena menor seleciona mais mudanças, incluindo ruído; um limiar maior pode deixar de
identificar transições sutis. A configuração min_gap mantém os timestamps
selecionados separados por pelo menos essa quantidade de segundos e deve ser de pelo menos
clip_duration. A seleção é cronológica, não uma classificação dos momentos mais
emocionantes. Ajuste o trecho antes do corte e a duração após revisar seu conteúdo.
Exemplos práticos e casos de uso
Os candidatos a mudanças de cena podem ajudar na revisão de entrevistas editadas, transições de slides e montagens de vídeo. Não suponha que a detecção de cenas reconheça gols, vitórias, pessoas específicas falando ou conclusões faladas. Essas tarefas exigem sinais adicionais e validação separada. A compilação gerada é um ponto de partida para a revisão editorial.
Conclusão e possíveis melhorias
Se a compilação deixar de incluir momentos de que você precisa, reduzir o limiar de cena pode apenas adicionar mais ruído visual. Adicione análise de áudio ou um detector de eventos validado quando sua seleção depender de fala ou atividade dentro de um plano sem mudanças.
Para processamento gerenciado, o serviço de codificação de vídeo da Transloadit oferece Robots baseados em FFmpeg para codificação e fluxos de trabalho de mídia.
