Automatize destaques de vídeo com Ruby e FFmpeg
Encontrar momentos úteis em gravações longas leva tempo. Com Ruby, você pode automatizar a detecção de mudanças de cena, extrair clipes candidatos e montá-los em um vídeo de revisão com o FFmpeg. Isso é uma heurística baseada em 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 timestamps suficientemente espaçados e codifica clipes curtos em torno deles. Um corte, um flash ou uma transição de slide pode ser um bom candidato, mas um gol ou uma fala relevante pode não ter nenhum corte de cena. Revise o vídeo gerado antes de publicá-lo.
Configurando Ruby e FFmpeg
Instale Ruby, FFmpeg e FFprobe. No macOS:
brew install ruby ffmpeg
No Ubuntu/Debian:
sudo apt-get update
sudo apt-get install ruby ffmpeg
O exemplo usa a biblioteca padrão do Ruby em vez de um wrapper de FFmpeg de terceiros. Verifique as ferramentas:
ruby -v
ffmpeg -version
ffprobe -version
Compatibilidade de versões
Use uma versão do Ruby com manutenção ativa no Linux ou macOS e uma build do FFmpeg com os filtros
select e metadata e o encoder libx264. Esta CLI processa um vídeo local confiável por vez.
Para uploads não confiáveis, use um worker isolado com limites de sistema de arquivos, rede, disco,
CPU e tempo total de execução decorrido. Um processo decodificador, por si só, não é uma fronteira de
segurança.
Extraindo momentos-chave com filtros do FFmpeg
A pontuação de cena estima as diferenças visuais entre quadros. Para uma sondagem manual em um
diretório de trabalho novo, este comando grava os timestamps dos quadros selecionados em timestamps.txt:
ffmpeg -nostdin -n -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 relativa à entrada do FFmpeg.
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 fica fora das verificações normais 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 ele.
Implementando scripts Ruby para automação
Salve esta classe como highlight_extractor.rb. Ela mantém fixos os nomes dos arquivos gerados e passa os caminhos de
entrada como argumentos de processo separados, para que os caminhos não entrem na sintaxe do shell
nem dos filtros. Uma etapa com falha lança 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.
clips = selected.first(20).each_with_index.map do |time, index|
start = [time - pre_roll, 0.0].max
length = [clip_duration, duration - start].min
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',
'-pix_fmt', 'yuv420p', '-c:a', 'aac', '-threads', '2', name)
name
end
create_highlight_compilation(clips) 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', '-of', 'json', @input)
raise IOError, 'Cannot inspect input video' unless status.success?
duration = Float(JSON.parse(output).fetch('format').fetch('duration'))
raise ArgumentError, 'Invalid video duration' unless duration.finite? && duration > 0
duration
end
def execute(*arguments)
unless system('ffmpeg', '-nostdin', '-n', *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)
File.write(File.join(@output, 'filelist.txt'), clips.map { |name| "file '#{name}'\n" }.join)
execute('-f', 'concat', '-safe', '1', '-i', 'filelist.txt', '-c', 'copy', 'highlight_reel.mp4')
end
end
A lista de concatenação contém apenas nomes base gerados, então o modo de caminhos seguros do FFmpeg continua ativado. Todos os clipes vêm da mesma origem e das mesmas configurações de encoder. O áudio é opcional, mas este exemplo não é um concatenador de uso geral para arquivos não relacionados com layouts de fluxos diferentes.
A opção -ss no lado da saída decodifica e descarta o material anterior ao início de cada clipe. Esse
caminho de decodificação simples não depende de índices de busca utilizáveis, mas repete trabalho
para destaques mais adiante no vídeo. Para gravações longas e indexadas, faça benchmarks da busca
precisa no lado da entrada e verifique os limites dos clipes e os deslocamentos 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:
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Um diretório de saída existente é recusado e permanece intacto. Uma falha depois da criação do diretório da tarefa remove as saídas parciais dessa tarefa. Um vídeo válido sem mudanças de cena selecionadas é concluído com sucesso, com uma mensagem clara e sem arquivo de compilação. Os diagnósticos locais do FFmpeg podem incluir nomes de arquivos; não os repasse para clientes web nem use nomes de arquivos sensíveis em logs compartilhados.
Otimização de desempenho
- Processe um clipe curto e representativo antes de uma gravação longa.
- Limite os workers simultâneos e monitore o uso de armazenamento. O limite de vinte clipes restringe os clipes gerados, não a duração da entrada nem o trabalho de análise de cenas.
- Aplique um limite de tempo total de execução decorrido ao worker inteiro e encerre seus processos filhos em caso de cancelamento. O exemplo não implementa um supervisor de processos nem promete aplicar timeouts por tarefa.
- Escolha encoders de hardware somente depois de validar a qualidade e a compatibilidade dos fluxos; um encoder mais rápido pode exigir flags e configurações de saída diferentes.
Otimizando a seleção de destaques
Um limiar de cena mais baixo seleciona mais mudanças, inclusive ruído; um limiar mais alto pode
deixar passar transições sutis. A configuração min_gap mantém os timestamps selecionados a pelo menos essa
quantidade de segundos de distância e precisa ser de no mínimo clip_duration. A seleção é cronológica, não um
ranking dos momentos mais empolgantes. Ajuste a antecedência (pre-roll) e a duração depois de
revisar seu conteúdo.
Exemplos práticos e casos de uso
Candidatos baseados em mudanças de cena podem ajudar na revisão de entrevistas editadas, transições de slides e montagens. Não presuma que a detecção de cenas reconhece gols, vitórias, pessoas específicas falando ou conclusões faladas. Essas tarefas exigem sinais adicionais e validação separada. O vídeo gerado é um ponto de partida para a revisão editorial.
Conclusão e possíveis melhorias
O Ruby pode orquestrar as interfaces documentadas do FFmpeg sem depender de um wrapper, manter separadas as saídas de cada tarefa e expor as etapas que falharam. Adicione análise de áudio ou um detector de eventos validado somente quando as mudanças de cena não forem suficientes para o seu caso de uso.
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.
