Automatiza los momentos destacados de video con Ruby y FFmpeg
Encontrar momentos útiles en material extenso toma tiempo. Ruby puede automatizar la detección de cambios de escena, extraer clips candidatos y armarlos en un reel de revisión con FFmpeg. Esto es una heurística de cambio visual, no un sistema que entienda qué momentos son importantes.
Introducción a los momentos destacados de video automáticos
El flujo de trabajo detecta cambios grandes entre fotogramas adyacentes, selecciona marcas de tiempo lo bastante separadas y codifica clips cortos alrededor de ellas. Un corte, un destello o una transición de diapositiva puede ser un candidato útil, pero un gol o una idea dicha en voz alta puede no tener ningún corte de escena. Revisa el reel generado antes de publicarlo.
Configurar Ruby y FFmpeg
Instala Ruby, FFmpeg y FFprobe. En macOS:
brew install ruby ffmpeg
En Ubuntu/Debian:
sudo apt-get update
sudo apt-get install ruby ffmpeg
El ejemplo usa la biblioteca estándar de Ruby en lugar de un wrapper de FFmpeg de terceros. Verifica las herramientas:
ruby -v
ffmpeg -version
ffprobe -version
Compatibilidad de versiones
Usa una versión de Ruby con mantenimiento en Linux o macOS y una compilación de FFmpeg con los
filtros select y metadata y con el codificador libx264. Esta CLI procesa un video local confiable a la vez.
Para subidas no confiables, usa un proceso de trabajo aislado con límites de sistema de archivos,
red, disco, CPU y tiempo real. Un proceso decodificador no es, por sí solo, un límite de seguridad.
Extraer momentos clave con filtros de FFmpeg
La puntuación de escena estima las diferencias visuales entre fotogramas. Para una prueba manual en
un directorio de trabajo nuevo, este comando escribe las marcas de tiempo de los fotogramas
seleccionados en 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 -
Mantén la detección de escenas y la extracción de clips en la línea de tiempo relativa a la entrada
de FFmpeg. No reinicies por separado las marcas de tiempo del stream de video: el audio puede
comenzar antes del primer fotograma de video. Tanto la detección como la extracción seleccionan de
forma explícita la primera pista de video. El archivo de texto del filtro es independiente de las
verificaciones habituales de archivos de salida de FFmpeg: no reutilices un timestamps.txt existente.
La implementación en Ruby crea un directorio privado para el trabajo.
Implementar scripts de Ruby para la automatización
Guarda esta clase como highlight_extractor.rb. Mantiene fijos los nombres de archivo generados y pasa las rutas
de entrada como argumentos de proceso separados, de modo que las rutas no entran en la sintaxis del
shell ni de los filtros. Una etapa fallida genera un error en lugar de producir en silencio un
informe de lote exitoso.
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
La lista de concatenación contiene solo nombres base generados, por lo que el modo de rutas seguras de FFmpeg permanece activo. Todos los clips provienen de la misma fuente y de los mismos ajustes del codificador. El audio es opcional, pero este ejemplo no es un concatenador de propósito general para archivos no relacionados con distintas disposiciones de streams.
La opción -ss del lado de la salida decodifica y descarta el material anterior a la marca de
tiempo de inicio de cada clip. Esta ruta de decodificación directa no depende de índices de búsqueda
utilizables, pero repite trabajo para los momentos destacados tardíos. Para grabaciones largas e
indexadas, compara el rendimiento de la búsqueda precisa del lado de la entrada y verifica los
límites de los clips y los desfases de audio/video antes de cambiar ese comportamiento.
Guarda este llamador junto a la clase 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
Ejecútalo desde tu terminal:
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Un directorio de salida existente se rechaza y permanece intacto. Si ocurre una falla después de crear el directorio del trabajo, se eliminan las salidas parciales de ese trabajo. Un video válido sin cambios de escena seleccionados se procesa correctamente, con un mensaje claro y sin archivo de compilación. Los diagnósticos locales de FFmpeg pueden incluir nombres de archivo: no los envíes a clientes web ni uses nombres de archivo sensibles en logs compartidos.
Optimización del rendimiento
- Procesa un clip corto representativo antes de una grabación larga.
- Limita los procesos de trabajo concurrentes y monitorea el uso de almacenamiento. El límite de veinte clips acota los clips generados, no la duración de la entrada ni el análisis de escenas.
- Aplica un límite de tiempo real a todo el proceso de trabajo y termina sus procesos hijos al cancelar. El ejemplo no implementa un supervisor de procesos ni garantiza la aplicación de tiempos de espera por trabajo.
- Elige codificadores por hardware solo después de validar la calidad y la compatibilidad de streams; un codificador más rápido puede requerir flags y ajustes de salida diferentes.
Optimizar la selección de momentos destacados
Un umbral de escena más bajo selecciona más cambios, incluido el ruido; un umbral más alto puede
pasar por alto transiciones sutiles. El ajuste min_gap mantiene las marcas de tiempo seleccionadas
separadas al menos esa cantidad de segundos y debe ser de al menos clip_duration. La selección es
cronológica, no una clasificación de los momentos más emocionantes. Ajusta el pre-roll y la duración
después de revisar tu contenido.
Ejemplos prácticos y casos de uso
Los candidatos por cambio de escena pueden ayudar a revisar entrevistas editadas, transiciones de diapositivas y material de montaje. No supongas que la detección de escenas reconoce goles, victorias, oradores específicos ni conclusiones habladas. Esas tareas requieren señales adicionales y una validación aparte. El reel generado es un punto de partida para la revisión editorial.
Conclusión y posibles mejoras
Ruby puede orquestar las interfaces documentadas de FFmpeg sin depender de un wrapper, mantener separadas las salidas de cada trabajo y exponer las etapas fallidas. Agrega análisis de audio o un detector de eventos validado solo cuando los cambios de escena no sean suficientes para tu caso de uso.
Para un procesamiento gestionado, el servicio de encoding de video de Transloadit ofrece Robots basados en FFmpeg para encoding y flujos de trabajo de medios.
