Automatiser les moments forts vidéo avec Ruby et FFmpeg
Trouver les moments intéressants dans de longues vidéos prend du temps. Ruby peut automatiser la détection des changements de scène, extraire des clips candidats et les assembler en un montage de visionnage avec FFmpeg. Il s’agit d’une heuristique fondée sur les changements visuels, et non d’un système capable de comprendre quels moments sont importants.
Introduction à l’automatisation des moments forts vidéo
Le processus détecte les changements importants entre des images adjacentes, sélectionne des horodatages suffisamment espacés et encode de courts clips autour de ces instants. Une coupe, un flash ou une transition entre diapositives peut constituer un candidat utile, mais un but ou une remarque pertinente formulée oralement peut ne s’accompagner d’aucune coupe. Examinez le montage généré avant de le publier.
Configurer Ruby et FFmpeg
Utilisez Bash sous Linux avec Ruby, FFmpeg et son outil complémentaire ffprobe
dans PATH. Ce guide a été testé avec Ruby 3.4.10 et FFmpeg/ffprobe 9.0.1.
Il s’agit des versions testées, et non des versions exactes requises pour l’installation. Utilisez
une version corrective actuelle de Ruby 3.4 en suivant le
guide d’installation de Ruby, et vérifiez son
statut de maintenance.
Sous Ubuntu/Debian, installez FFmpeg et ffprobe ensemble :
sudo apt-get update && sudo apt-get install -y --no-install-recommends ffmpeg
L’exemple utilise les bibliothèques standard de Ruby sans bibliothèque tierce d’intégration à FFmpeg. Avant d’enregistrer les scripts, vérifiez que Ruby peut charger ces bibliothèques et inspectez les filtres et les encodeurs de votre compilation de 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
Compatibilité des versions
Votre compilation de FFmpeg doit inclure les filtres
select, metadata, atrim,
asetpts et concat, ainsi que les encodeurs
libx264 et aac. La même procédure fonctionne aussi
avec FFmpeg 6.1.1 ; utilisez un paquet intégrant les correctifs pour votre distribution. La commande
supprime les paramètres hérités du chargeur de Ruby et de Bundler. Ainsi, une exécution depuis un
projet Ruby englobant ne sélectionne pas les gems de ce projet.
Choisissez une vidéo locale dont vous savez qu’elle est valide, avec des dimensions d’image fixes et des couleurs SDR. Le script sélectionne sa première piste vidéo et, si elle existe, sa première piste audio. L’audio doit couvrir les intervalles vidéo sélectionnés. Les fichiers téléversés non fiables, les enregistrements endommagés et les fichiers dont la résolution change en cours de lecture nécessitent une validation distincte et un processus de traitement isolé ; cet outil en ligne de commande ne fournit pas cette isolation. FFmpeg peut récupérer après un paquet endommagé, afficher un avertissement et tout de même signaler une réussite. Un traitement par lots terminé ne valide pas l’intégrité de la source ; examinez les diagnostics et les extraits générés.
Extraire les moments clés avec les filtres FFmpeg
Le score de changement de scène de FFmpeg estime les différences
visuelles entre les images. Pour un essai manuel dans un nouveau répertoire de travail, cette
commande écrit les horodatages des images sélectionnées dans 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 -
Gardez la détection des scènes et l’extraction des clips sur la chronologie de FFmpeg relative à
l’entrée. Ne réinitialisez pas indépendamment les horodatages du flux vidéo : l’audio peut commencer
avant la première image vidéo. La détection et l’extraction sélectionnent toutes deux explicitement
la première piste vidéo. Le fichier texte du filtre est distinct des vérifications habituelles des
fichiers de sortie de FFmpeg : ne réutilisez pas un fichier timestamps.txt existant.
L’implémentation Ruby crée pour ce fichier un répertoire de tâche privé.
Implémenter des scripts Ruby pour l’automatisation
Enregistrez cette classe sous le nom highlight_extractor.rb. Elle conserve des noms de
fichiers générés fixes et transmet les chemins d’entrée comme arguments distincts au processus.
Les chemins n’entrent donc pas dans la syntaxe du shell ou des filtres. Si une étape échoue, elle
lève une erreur plutôt que de produire silencieusement un rapport de réussite du traitement par
lots.
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
Le démultiplexeur concat nécessite des flux et des bases de temps identiques. Ces clips partagent la même source et les mêmes paramètres d’encodage ; la liste ne contient que les noms de base générés, de sorte que le mode de sécurisation des chemins reste activé. Les durées explicites positionnent chaque clip vidéo sur la chronologie du montage. Le montage copie la vidéo encodée. Lorsque l’audio est présent, il décode chaque clip séparément, limite l’audio à la durée demandée du clip et assemble les segments avec le filtre concat avant de les réencoder en AAC. Cela empêche le remplissage ajouté par l’encodeur des clips de s’accumuler sous forme de retard audio. Ce montage de visionnage n’est ni un master audio sans interruption ni un outil de concaténation pour des fichiers sans lien entre eux. Écoutez les raccords avant de publier.
L’encodeur libx264 utilise un CRF de 23 comme
objectif de qualité, sans plafond de débit. Une valeur plus faible conserve généralement davantage
de détails et augmente la taille du fichier. Le préréglage fast contrôle
l’effort d’encodage, et non la sélection des scènes.
L’option -ss côté sortie décode et écarte
les images précédant le début de chaque clip. Cette méthode de décodage simple ne dépend pas
d’index de recherche temporelle utilisables, mais répète le travail pour les moments forts situés
vers la fin. Pour les longs enregistrements indexés, mesurez les performances d’un positionnement
précis côté entrée et vérifiez les limites des clips ainsi que les décalages audio/vidéo avant de
modifier ce comportement.
Enregistrez ce script appelant à côté de la classe sous le nom 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
Exécutez-le depuis votre terminal :
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE \
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Gardez les deux scripts ensemble et remplacez my_video.mp4 par votre chemin
d’entrée. Mettez les chemins contenant des espaces entre guillemets. Une exécution réussie indique
le nombre de candidats et écrit highlight_001.mp4, les clips numérotés suivants et
highlight_reel.mp4 dans my_highlights_folder. Ouvrez le montage dans votre
lecteur vidéo. Chaque clip commence par défaut 2 secondes avant la coupe sélectionnée, sans
remonter avant le début de l’entrée, et dure jusqu’à 5 secondes. Les coupes sont sélectionnées par
ordre chronologique, avec un intervalle d’au moins 10 secondes, jusqu’à un maximum de 20 clips.
Un répertoire de sortie existant est refusé et reste intact ; choisissez un nouveau nom pour réessayer. Une erreur de traitement survenant après la création du répertoire de tâche supprime les sorties partielles de cette tâche. Corrigez l’entrée ou l’installation, puis répétez la même commande. Une vidéo valide sans changement de scène sélectionné affiche No scene changes selected, conserve son fichier d’horodatages et termine avec succès sans clips ni montage. Les diagnostics locaux de FFmpeg peuvent inclure des noms de fichiers ; évitez de placer des chemins sensibles dans les journaux partagés.
Optimiser les performances
Commencez par traiter un court enregistrement représentatif et vérifiez les extraits obtenus. La limite de vingt clips borne le nombre de clips générés, et non la durée de l’entrée, l’analyse complète des scènes ou la taille des sorties. Prévoyez suffisamment d’espace disque pour les clips individuels et le montage. Le nombre de threads du décodeur et de l’encodeur est limité ici ; exécuter plusieurs tâches multiplie néanmoins leur utilisation du processeur et du stockage.
Optimiser la sélection des moments forts
Un seuil de détection des scènes plus bas sélectionne davantage de changements, y compris du
bruit ; un seuil plus élevé peut manquer des transitions subtiles. Le paramètre
min_gap impose au moins ce nombre de secondes entre les horodatages
sélectionnés et doit être supérieur ou égal à clip_duration. La sélection suit
l’ordre chronologique ; elle ne classe pas les moments selon leur intérêt. Ajustez l’amorce et la
durée après avoir examiné votre contenu.
Exemples pratiques et cas d’usage
Les clips candidats issus des changements de scène peuvent faciliter l’examen d’entretiens montés, de transitions entre diapositives et de séquences de montage. Ne supposez pas que la détection des scènes reconnaît les buts, les victoires, les intervenants précis ou les conclusions formulées oralement. Ces tâches nécessitent des signaux supplémentaires et une validation distincte. Le montage généré constitue un point de départ pour l’examen éditorial.
Conclusion et améliorations possibles
Si le montage manque des moments dont vous avez besoin, abaisser le seuil de détection des scènes peut n’ajouter que davantage de bruit visuel. Ajoutez une analyse audio ou un détecteur d’événements validé lorsque votre sélection dépend de la parole ou de l’activité au sein d’un plan inchangé.
Pour un traitement géré, le service d’encodage vidéo de Transloadit fournit des Robots fondés sur FFmpeg pour l’encodage et les processus de traitement des médias.
