Videos in Ruby mit Open-Source-Tools untertiteln
Brennen Sie mit Ruby und FFmpeg eine vorhandene SRT-Datei in die Videobilder ein. Verwenden Sie
streamio-ffmpeg für Metadaten und das Gem srt für die Zeitsteuerung der Untertiteleinträge.
Die folgenden Skripte erzeugen ein MP4 mit Untertiteln, eine um 2,5 Sekunden verzögerte SRT-Datei
und eine separate WebVTT-Datei. Sie verarbeiten die von Ihnen bereitgestellten Untertitel;
sie transkribieren den Ton nicht.
Warum Untertitel wichtig sind
Eingebrannte Untertitel bleiben auch in Playern sichtbar, die keine Untertitelspur laden können. Sie werden Teil des Bildes, sodass Zuschauer sie weder ausschalten noch eine andere Sprache wählen können. Bewahren Sie das Originalvideo und die Untertiteldatei auf, wenn Sie auch eine im Player wählbare Spur benötigen.
Open-Source-Tools für Untertitel in Ruby
Die beiden Gems haben unterschiedliche Aufgaben:
- streamio-ffmpeg liest Videometadaten mit FFprobe. Das Hilfsprogramm prüft damit vor dem Encoding, ob ein Videostream vorhanden ist.
- srt analysiert SRT-Untertiteleinträge, verschiebt ihre Zeitstempel und schreibt SRT-Dateien. Die folgende WebVTT-Konvertierung ruft FFmpeg direkt aus Ruby auf.
Dies sind ältere Versionsreihen: streamio-ffmpeg 3.0.2 erschien 2016 und srt 0.1.5 im Jahr
2021, laut ihren RubyGems-Versionsseiten. Aufgrund ihres Alters ist eine getestete Kombination
hilfreich; die Kompatibilitätshinweise des Wrappers garantieren keine Kompatibilität mit aktuellen
FFmpeg-Versionen.
Untertitel mit streamio-ffmpeg hinzufügen
Die Beispiele wurden unter Linux mit Ruby 3.4.10 und FFmpeg/FFprobe 9.0.1 getestet. Dies sind
getestete Versionen, keine grundsätzlichen Mindestversionen. Neuere gepflegte Patch-Versionen dieser
Versionsreihen können Sie mit den folgenden Prüfungen ausprobieren. Installieren Sie
Ruby 3.4 und FFmpeg mit FFprobe separat. Sie benötigen die Encoder für
libx264, AAC und WebVTT, den Filter subtitles von libass sowie eine Schriftart wie DejaVu
Sans, die alle Zeichen Ihres Untertiteltexts abdeckt.
Verwenden Sie Bash in einem neuen, beschreibbaren Verzeichnis, ohne dass RUBYOPT und
BUNDLE_GEMFILE gesetzt sind. Führen Sie dort diese Einrichtung aus und lassen Sie dieselbe Shell für die
späteren Befehle geöffnet. Installation und Gem-Metadaten bleiben in diesem Verzeichnis.
Das explizite Installationsziel umgeht auch eine konfigurierte RubyGems-Benutzerinstallation.
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
)
Die Liste enthält die Abhängigkeit multi_json des Wrappers. Daher ist die Installation von
Abhängigkeiten deaktiviert, und jede ausgewählte Version wird im folgenden Hilfsprogramm aktiviert.
Falls ein Download fehlschlägt, führen Sie diesen Block nach Beheben des Verbindungsproblems erneut
im selben Verzeichnis aus. Prüfen Sie den installierten FFmpeg-Build, bevor Sie Eingabedateien
anlegen:
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
Die Hilfe muss jeden genannten Filter oder Encoder beschreiben. Eine Meldung mit „unknown“ bedeutet, dass Sie einen anderen Build benötigen, selbst wenn der Hilfebefehl erfolgreich beendet wird.
Speichern Sie diese gemeinsame Implementierung als subtitle_tools.rb. Alle folgenden Beispiele verwenden sie.
Das temporäre Verzeichnis hält den Untertiteldateinamen aus der FFmpeg-Filtersyntax heraus.
Das Ergebnis wird über einen Hardlink im Ausgabedateisystem bereitgestellt; das Dateisystem muss
Hardlinks unterstützen.
Die Implementierung von transcode im Wrapper
validiert lesbare Ausgabemetadaten, ohne den Exit-Status von FFmpeg zu prüfen. Das Hilfsprogramm
verwendet daher FFMPEG::Movie für Metadaten und ruft FFmpeg mit
system von Ruby auf. Es prüft den Status, bevor es ein Ergebnis
bereitstellt.
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
Die folgende Stapelverarbeitung ruft dieses Hilfsprogramm nacheinander auf. Es codiert das ausgewählte Video neu in H.264 und den Ton, sofern vorhanden, in AAC. Dabei gilt die Standardauswahl von Streams in FFmpeg; es bleiben nicht alle Spuren einer Quelle mit mehreren Spuren erhalten. Verwenden Sie geprüfte lokale Medien: Diese Beispiele bieten keine Sandbox für Uploads.
Speichern Sie diese CLI-Schnittstelle daneben als 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
Verwenden Sie Ihr MP4 und eine SRT-Datei in UTF-8 oder erzeugen Sie dieses kurze Testvideo.
Der Befehl lehnt eine bereits vorhandene Datei namens input.mp4 ab:
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
Speichern Sie diese Untertiteleinträge als 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
Öffnen Sie output_with_subs.mp4 in Ihrem Videoplayer. Im Testclip erscheint der erste Untertitel von
0,6 bis 1,4 Sekunden, der zweite von 2,1 bis 3,2 Sekunden und der letzte von 4,0 bis 4,2 Sekunden.
In den Lücken sollten keine Untertitel erscheinen, und das 4,3 Sekunden lange Video sollte weiterhin
seinen Ton haben. Die Wiedergabe zu prüfen ist nur der erste Schritt: Gleichen Sie Text, Platzierung
und Zeitsteuerung mit der SRT-Datei ab.
SRT-Dateien mit dem SRT-Gem bearbeiten
Wenn jeder Untertitel 2,5 Sekunden zu früh erscheint, verzögern Sie alle Untertiteleinträge um diesen
Wert. Speichern Sie dies als 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
Die Testeinträge liegen nun bei 3,1 – 3,9, 4,6 – 5,7 und 6,5 – 6,7 Sekunden; ihr Text bleibt gleich.
Eine Verschiebung verändert die Untertitelzeitleiste, sodass diese späteren Einträge über das Ende
des kurzen Testvideos hinausreichen. Verwenden Sie beim Einbrennen in diesen Clip die unverschobene
SRT-Datei. Die dokumentierte Methode timeshift des Gems nimmt einen Hash mit Optionen entgegen.
Passen Sie die Verzögerung an Ihr Video an; dieses Beispiel wendet denselben Zeitversatz auf jeden
Eintrag an und korrigiert keine fortschreitende zeitliche Abweichung.
Untertitelformate konvertieren
Um eine separate WebVTT-Datei mit der ursprünglichen Zeitsteuerung der Untertiteleinträge zu erzeugen,
speichern Sie dies als 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
Für die Beispiel-SRT-Datei enthält subtitles.vtt:
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 verwendet eine Kopfzeile und einen Punkt als Millisekundentrennzeichen. Die Konvertierung erzeugt eine separate Datei; sie fügt dem MP4 keine Spur hinzu. So konvertieren Sie stattdessen die verschobene SRT-Datei:
ruby convert_subtitles.rb modified.srt shifted.vtt
Fehlerbehandlung bei der Untertitelintegration
Jede CLI liefert bei einem Fehler im Hilfsprogramm einen Status ungleich null. Die Laderoutine lehnt fehlende Dateien, SRT-Dateien über 1 MiB, ungültiges UTF-8, vom Parser gemeldete Fehler, leeren Untertiteltext und Untertiteleinträge mit einer Dauer kleiner oder gleich null ab. Das Gem kann Analysefehler für einzelne Zeilen erfassen. Wer nur auf ausgelöste Ausnahmen prüft, übersieht sie.
Jede Ausgabedatei muss neu sein, und ihr übergeordnetes Verzeichnis muss bereits existieren.
Bei einer erneuten Ausführung wird ein vorhandenes Ziel abgelehnt, und seine Bytes bleiben erhalten.
Das Zwischenspeichern neben dem Ziel und die Bereitstellung per Hardlink verhindern bei gewöhnlichen
Fehlern, dass ein teilweise geschriebenes Ergebnis sichtbar wird. Ein erzwungener Prozessabbruch
kann ein Verzeichnis namens .subtitles-… hinterlassen; prüfen Sie es, bevor Sie es entfernen.
FFmpeg kann manche beschädigten Medien wiederherstellen und dennoch Erfolg melden. In einem Test mit einem beschädigten AAC-Paket gegen Ende erzeugte es ein abspielbares MP4 mit verkürztem Ton. Metadaten und erfolgreiches Decodieren können nicht belegen, dass eine unbekannte Quelle intakt war. Vergleichen Sie Dauer, Ton und letzte Bilder des Ergebnisses mit Ihrer bekannten Eingabedatei. FFmpeg-Diagnosen können in lokalen Protokollen erscheinen; halten Sie diese getrennt von Meldungen, die an Webclients gesendet werden.
Unterstützte Untertitelformate
Diese Skripte akzeptieren SRT in UTF-8 und erzeugen SRT, WebVTT oder ein MP4 mit eingebrannten
Untertiteln. Die Beispiele decken einfache Dialogeinträge ab. ASS-Formatierung, WebVTT-Positionierung
und andere Untertitelformate benötigen einen anderen Eingabeweg; das Umbenennen in
.srt konvertiert sie nicht.
Praxisbeispiel: Videos mit Untertiteln im Stapel verarbeiten
Verwenden Sie für ein Verzeichnis mit MP4-Dateien dasselbe Hilfsprogramm nacheinander. Speichern Sie
dies als batch_subtitles.rb. Es ordnet lesson.mp4 der Datei lesson.srt zu; es akzeptiert auch die großgeschriebene
Dateiendung .MP4 und überspringt symbolische Verknüpfungen. Das Ausgabeverzeichnis muss neu sein.
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
Erstellen Sie zuerst die Verzeichnisse videos und captions und legen Sie passende Dateien darin ab.
Fehlende oder ungültige Untertitel zählen als Fehler, und die Meldung nennt den Basisnamen des Videos.
Die verbleibenden Dateien werden trotzdem verarbeitet, und erfolgreiche Ausgaben bleiben erhalten.
Exit-Status null bedeutet, dass jedes ausgewählte Video konvertiert wurde; Status eins bedeutet,
dass die Vorbereitung oder mindestens eine Konvertierung fehlgeschlagen ist. Verwenden Sie für einen
erneuten Versuch ein neues Ausgabeverzeichnis, damit fertige Dateien verfügbar bleiben.
Für eine verwaltete Alternative sehen Sie sich bei Transloadit den Robot 🤖 /video/subtitle und den Dienst für Video-Encoding an.
