Sous-titrer des vidéos en Ruby avec des outils open source
Utilisez Ruby et FFmpeg pour incruster un fichier SRT existant dans les images d’une vidéo, avec
streamio-ffmpeg pour les métadonnées et le gem srt pour
le minutage des entrées de sous-titres. Les scripts ci-dessous produisent un MP4 sous-titré, un SRT
retardé de 2,5 secondes et un fichier annexe WebVTT. Ils utilisent les sous-titres que vous fournissez ;
ils ne transcrivent pas l’audio.
Pourquoi les sous-titres sont importants
Les sous-titres incrustés restent visibles dans les lecteurs qui ne peuvent pas charger de piste de sous-titres. Ils font partie de l’image : les spectateurs ne peuvent donc ni les désactiver ni choisir une autre langue. Conservez la vidéo et le fichier de sous-titres d’origine si vous avez aussi besoin d’une piste sélectionnable dans le lecteur.
Outils Ruby open source pour les sous-titres
Les deux gems remplissent des fonctions différentes :
- streamio-ffmpeg lit les métadonnées vidéo avec FFprobe. Le script auxiliaire l’utilise pour vérifier la présence d’un flux vidéo avant l’encodage.
- srt analyse les entrées SRT, décale leurs horodatages et écrit du SRT. La conversion en WebVTT ci-dessous appelle FFmpeg directement depuis Ruby.
Il s’agit de branches de versions anciennes : streamio-ffmpeg 3.0.2 est sorti en 2016 et
srt 0.1.5 en 2021, selon
RubyGems
(pages de versions). Leur ancienneté rend utile une combinaison testée ;
les notes de compatibilité de la bibliothèque d’encapsulation ne garantissent pas la compatibilité
avec les versions actuelles de FFmpeg.
Ajouter des sous-titres avec streamio-ffmpeg
Les exemples ont été testés sous Linux avec Ruby 3.4.10 et FFmpeg/FFprobe 9.0.1. Ce sont les versions
testées, pas des versions minimales requises ; vous pouvez essayer des versions correctives plus
récentes et maintenues de ces branches en effectuant les vérifications ci-dessous. Installez
Ruby 3.4 et
FFmpeg avec FFprobe séparément. Vous avez besoin des encodeurs
libx264, AAC et WebVTT, du
filtre subtitles de libass et d’une police comme
DejaVu Sans qui couvre les caractères de vos sous-titres.
Utilisez Bash dans un nouveau répertoire accessible en écriture, sans définir
RUBYOPT ni BUNDLE_GEMFILE. Exécutez-y cette configuration, puis
gardez le même shell ouvert pour les commandes suivantes. L’installation et les métadonnées des gems
restent dans ce répertoire. La cible d’installation explicite évite aussi une installation utilisateur
configurée dans RubyGems.
export GEM_HOME="$PWD/.gems"
export GEM_PATH="$GEM_HOME"
export GEM_SPEC_CACHE="$PWD/.gem-spec-cache"
(
if [ -n "${RUBYOPT:-}" ] || [ -n "${BUNDLE_GEMFILE:-}" ]; then
printf 'Use a shell without Bundler or RUBYOPT overrides.\n' >&2
exit 1
fi
for tool in ruby gem ffmpeg ffprobe; do
command -v "$tool" >/dev/null || { printf 'Missing prerequisite: %s\n' "$tool" >&2; exit 1; }
done
gem install --norc --install-dir "$GEM_HOME" --no-user-install --no-document --ignore-dependencies \
multi_json:1.15.0 streamio-ffmpeg:3.0.2 srt:0.1.5
)
La liste inclut la dépendance multi_json de la bibliothèque d’encapsulation ;
l’installation des dépendances est donc désactivée et chaque version sélectionnée est activée dans
le script auxiliaire ci-dessous. Si un téléchargement échoue, réexécutez ce bloc dans le même répertoire
après avoir rétabli la connexion. Vérifiez la compilation de FFmpeg installée avant de créer les
fichiers d’entrée :
ffmpeg -hide_banner -h filter=subtitles &&
ffmpeg -hide_banner -h encoder=libx264 &&
ffmpeg -hide_banner -h encoder=aac &&
ffmpeg -hide_banner -h encoder=webvtt &&
ffprobe -version
L’aide doit décrire chaque filtre ou encodeur nommé. Un message « unknown » signifie que vous avez besoin d’une autre compilation, même si la commande d’aide se termine avec succès.
Enregistrez cette implémentation commune sous subtitle_tools.rb. Tous les exemples
ci-dessous la réutilisent. Le répertoire temporaire évite d’insérer le nom du fichier de sous-titres
dans la syntaxe des filtres FFmpeg. La publication utilise un lien physique sur le système de fichiers
de sortie ; celui-ci doit prendre en charge les liens physiques.
L’implémentation de transcode de la bibliothèque
d’encapsulation valide les métadonnées de sortie lisibles sans vérifier le code de sortie de FFmpeg.
Le script auxiliaire utilise donc FFMPEG::Movie pour les métadonnées et appelle FFmpeg
avec system de Ruby, en vérifiant son code de
sortie avant de publier un résultat.
gem 'multi_json', '1.15.0'
gem 'streamio-ffmpeg', '3.0.2'
gem 'srt', '0.1.5'
require 'streamio-ffmpeg'
require 'srt'
require 'tmpdir'
module SubtitleTools
def self.load_srt(path)
raise ArgumentError, 'Subtitle input must be a regular file' unless File.file?(path)
text = File.binread(path, 1024 * 1024 + 1)
raise ArgumentError, 'Subtitle input exceeds 1 MiB' if text.bytesize > 1024 * 1024
text.force_encoding(Encoding::UTF_8)
raise ArgumentError, 'Subtitles must be valid UTF-8' unless text.valid_encoding?
subtitles = SRT::File.parse(text)
if subtitles.lines.empty? || !subtitles.errors.empty?
raise ArgumentError, 'Invalid or empty SRT input'
end
unless subtitles.lines.all? { |line| line.start_time >= 0 && line.end_time > line.start_time }
raise ArgumentError, 'Invalid subtitle timing'
end
raise ArgumentError, 'Caption text must not be empty' if subtitles.lines.any? { |line| line.text.empty? }
subtitles
end
def self.with_output(output_path, extension)
output = File.join(File.realpath(File.dirname(output_path)), File.basename(output_path))
if File.exist?(output) || File.symlink?(output)
raise ArgumentError, 'Output must not exist'
end
Dir.mktmpdir('.subtitles-', File.dirname(output)) do |temporary|
candidate = File.join(temporary, "result#{extension}")
yield candidate, temporary
raise IOError, 'No nonempty output produced' unless File.file?(candidate) && File.size(candidate) > 0
File.link(candidate, output)
end
end
def self.burn(video_path, subtitle_path, output_path)
subtitles = load_srt(subtitle_path)
movie = FFMPEG::Movie.new(File.realpath(video_path))
raise ArgumentError, 'Invalid video input' unless movie.valid? && movie.video_codec
with_output(output_path, '.mp4') do |candidate, temporary|
File.write(File.join(temporary, 'captions.srt'), subtitles.to_s)
# Fixed basenames avoid FFmpeg's separate filter-path escaping rules.
unless system(FFMPEG.ffmpeg_binary, '-nostdin', '-n', '-i', movie.path,
'-c:v', 'libx264', '-c:a', 'aac', '-vf', 'subtitles=captions.srt',
'-threads', '2', candidate, chdir: temporary)
raise IOError, 'Video encoding failed'
end
end
end
def self.write_srt(subtitles, output_path)
with_output(output_path, '.srt') { |candidate| File.write(candidate, subtitles.to_s) }
end
def self.convert_to_vtt(subtitle_path, output_path)
subtitles = load_srt(subtitle_path)
with_output(output_path, '.vtt') do |candidate, temporary|
input = File.join(temporary, 'captions.srt')
File.write(input, subtitles.to_s)
unless system('ffmpeg', '-nostdin', '-n', '-i', input, '-c:s', 'webvtt', candidate)
raise IOError, 'Subtitle conversion failed'
end
end
end
end
Le traitement par lots ci-dessous appelle ce script auxiliaire de manière séquentielle. Il réencode la vidéo sélectionnée en H.264 et l’audio, s’il est présent, en AAC. La sélection des flux par défaut de FFmpeg s’applique ; elle ne conserve pas toutes les pistes d’une source multipiste. Utilisez des médias locaux vérifiés : ces extraits de code ne fournissent pas de bac à sable pour les téléversements.
Enregistrez cette interface en ligne de commande à côté du script sous add_subtitles.rb :
require_relative 'subtitle_tools'
abort 'Usage: ruby add_subtitles.rb <video.mp4> <subtitles.srt> <new-output.mp4>' unless ARGV.length == 3
begin
SubtitleTools.burn(*ARGV)
puts 'Subtitles added'
rescue StandardError
warn 'Subtitle processing failed. Check the inputs, output path, and FFmpeg installation.'
exit 1
end
Utilisez votre MP4 et un fichier SRT en UTF-8, ou générez cette courte vidéo de test. La commande
refuse un fichier input.mp4 existant :
ffmpeg -nostdin -n -f lavfi -i 'color=c=0x101020:s=320x180:r=10:d=4.3' \
-f lavfi -i 'sine=frequency=660:sample_rate=48000:duration=4.3' \
-c:v libx264 -threads 2 -pix_fmt yuv420p -c:a aac input.mp4
Enregistrez ces entrées de sous-titres sous subtitles.srt :
1
00:00:00,600 --> 00:00:01,400
First Ruby caption
2
00:00:02,100 --> 00:00:03,200
Second cue: café
3
00:00:04,000 --> 00:00:04,200
Final cue
ruby add_subtitles.rb input.mp4 subtitles.srt output_with_subs.mp4
Ouvrez output_with_subs.mp4 dans votre lecteur vidéo. Dans le clip de test, le premier
sous-titre apparaît de 0,6 à 1,4 secondes, le deuxième de 2,1 à 3,2 secondes et le dernier de 4,0 à
4,2 secondes. Les intervalles ne doivent contenir aucun sous-titre, et la vidéo de 4,3 secondes doit
conserver son signal sonore. Vérifier la lecture n’est qu’un premier contrôle : vérifiez le texte,
le placement et le minutage par rapport au SRT.
Manipuler des fichiers SRT avec le gem SRT
Si tous les sous-titres apparaissent 2,5 secondes trop tôt, retardez toutes les entrées de cette durée.
Enregistrez ce script sous shift_subtitles.rb :
require_relative 'subtitle_tools'
abort 'Usage: ruby shift_subtitles.rb <subtitles.srt> <new-output.srt>' unless ARGV.length == 2
begin
subtitles = SubtitleTools.load_srt(ARGV[0])
subtitles.timeshift(all: '+2.5s')
SubtitleTools.write_srt(subtitles, ARGV[1])
puts 'Subtitles shifted by 2.5 seconds'
rescue StandardError
warn 'Subtitle shift failed. Check the SRT input and use a new output path.'
exit 1
end
ruby shift_subtitles.rb subtitles.srt modified.srt
Les entrées de test passent à 3,1–3,9, 4,6–5,7 et 6,5–6,7 secondes ; leur texte reste identique. Un
décalage modifie la chronologie des sous-titres : ces entrées retardées dépassent donc la fin de la
courte vidéo de test. Utilisez le SRT non décalé pour incruster les sous-titres dans ce clip. La
méthode timeshift documentée du gem prend un hash
d’options. Ajustez le retard à votre vidéo ; cet exemple applique le même décalage à chaque entrée et
ne corrige pas une dérive progressive.
Convertir les formats de sous-titres
Pour produire un fichier annexe WebVTT avec le minutage d’origine des entrées, enregistrez ce script
sous convert_subtitles.rb :
require_relative 'subtitle_tools'
abort 'Usage: ruby convert_subtitles.rb <subtitles.srt> <new-output.vtt>' unless ARGV.length == 2
begin
SubtitleTools.convert_to_vtt(*ARGV)
puts 'WebVTT subtitles saved'
rescue StandardError
warn 'Subtitle conversion failed. Check the SRT input, output path, and FFmpeg installation.'
exit 1
end
ruby convert_subtitles.rb subtitles.srt subtitles.vtt
Pour le SRT d’exemple, subtitles.vtt contient :
WEBVTT
00:00.600 --> 00:01.400
First Ruby caption
00:02.100 --> 00:03.200
Second cue: café
00:04.000 --> 00:04.200
Final cue
WebVTT utilise un en-tête et un point comme séparateur des millisecondes. La conversion crée un fichier distinct ; elle n’ajoute pas de piste au MP4. Pour convertir plutôt le SRT décalé :
ruby convert_subtitles.rb modified.srt shifted.vtt
Gestion des erreurs d’intégration des sous-titres
Chaque interface en ligne de commande renvoie un code de sortie non nul en cas d’échec du script auxiliaire. Le chargeur refuse les fichiers manquants, les SRT de plus de 1 MiB, l’UTF-8 invalide, les erreurs signalées par l’analyseur, les sous-titres sans texte et les entrées dont la durée est nulle ou négative. Le gem peut enregistrer des erreurs d’analyse sur des lignes individuelles ; vérifier uniquement si une exception a été levée ne permettrait donc pas de les détecter.
Chaque fichier de sortie doit être nouveau et son répertoire parent doit déjà exister. Une nouvelle
exécution refuse une destination existante et en conserve les octets. Préparer le résultat à côté de
la destination et le publier avec un lien physique évite d’exposer un résultat partiellement écrit
lors d’échecs ordinaires. L’arrêt forcé d’un processus peut laisser un répertoire
.subtitles-… ; inspectez-le avant de le supprimer.
FFmpeg peut récupérer certains médias endommagés et tout de même signaler un succès. Lors d’un test avec un paquet AAC endommagé vers la fin, il a produit un MP4 lisible avec un audio raccourci. Les métadonnées et un décodage réussi ne peuvent pas prouver qu’une source inconnue était intacte. Comparez la durée, l’audio et les dernières images du résultat à votre fichier d’entrée connu. Les diagnostics de FFmpeg peuvent apparaître dans les journaux locaux ; gardez-les séparés des messages envoyés aux clients web.
Formats de sous-titres pris en charge
Ces scripts acceptent du SRT en UTF-8 et produisent du SRT, du WebVTT ou un MP4 avec des sous-titres
incrustés. Les exemples couvrent des entrées de dialogue simples. Les styles ASS, le positionnement
WebVTT et les autres formats de sous-titres nécessitent un autre mode de traitement des entrées ;
les renommer en .srt ne les convertit pas.
Exemple pratique : traiter des vidéos par lots avec des sous-titres
Pour un répertoire de fichiers MP4, réutilisez le même script auxiliaire de manière séquentielle.
Enregistrez ce script sous batch_subtitles.rb. Il associe
lesson.mp4 à lesson.srt ; il accepte aussi une extension
.MP4 en majuscules et ignore les entrées qui sont des liens symboliques.
Le répertoire de sortie doit être nouveau.
require_relative 'subtitle_tools'
def batch_process_videos(video_dir, subtitle_dir, output_dir)
video_files = Dir.children(video_dir).sort.filter_map do |name|
path = File.join(video_dir, name)
path if File.extname(name).downcase == '.mp4' && File.file?(path) && !File.symlink?(path)
end
raise ArgumentError, 'No MP4 files found' if video_files.empty?
Dir.mkdir(output_dir, 0700)
failures = 0
video_files.each do |video|
basename = File.basename(video, File.extname(video))
begin
SubtitleTools.burn(video, File.join(subtitle_dir, "#{basename}.srt"),
File.join(output_dir, "#{basename}_subtitled.mp4"))
puts "Converted #{basename}"
rescue StandardError
failures += 1
warn "Conversion failed for #{basename}"
end
end
failures.zero? ? 0 : 1
end
abort 'Usage: ruby batch_subtitles.rb <videos> <subtitles> <new-output-directory>' unless ARGV.length == 3
begin
exit batch_process_videos(*ARGV)
rescue StandardError
warn 'Cannot prepare batch. Check input directories and use a new output directory.'
exit 1
end
ruby batch_subtitles.rb videos captions subtitled
Créez d’abord videos et captions, puis placez-y des fichiers
correspondants. Les sous-titres manquants ou invalides comptent comme des échecs, et le message indique
le nom de base de la vidéo. Le traitement des fichiers restants est tout de même tenté et les sorties
réussies sont conservées. Un code de sortie zéro signifie que toutes les vidéos sélectionnées ont
été converties ; un code de sortie un signifie que la préparation ou au moins une conversion a échoué.
Pour réessayer, utilisez un nouveau répertoire de sortie afin que les fichiers terminés restent
disponibles.
Pour une solution gérée, consultez 🤖 /video/subtitle, le Robot de Transloadit, et son service d’encodage vidéo.
