Legendar vídeos em Ruby com ferramentas de código aberto
Use Ruby e FFmpeg para incorporar um arquivo SRT existente aos quadros do vídeo, com
streamio-ffmpeg para os metadados e a gem srt para os tempos das legendas.
Os scripts abaixo produzem um MP4 com legendas incorporadas, um SRT com atraso de 2,5 segundos e um
arquivo WebVTT separado. Eles usam legendas que você fornece; não transcrevem o áudio.
Por que as legendas são importantes
Legendas incorporadas continuam visíveis em players que não conseguem carregar uma faixa de legendas. Elas passam a fazer parte da imagem, então quem assiste não pode desativá-las nem selecionar outro idioma. Mantenha o vídeo e o arquivo de legendas originais se também precisar de uma faixa selecionável no player.
Ferramentas Ruby de código aberto para legendas
As duas gems têm funções diferentes:
- streamio-ffmpeg lê metadados de vídeo com o FFprobe. A função auxiliar usa esses dados para verificar se há um fluxo de vídeo antes da codificação.
- srt analisa entradas de legendas SRT, desloca seus tempos e grava SRT. A conversão para WebVTT abaixo chama o FFmpeg diretamente a partir do Ruby.
Estas são linhas de versões antigas: a versão 3.0.2 de streamio-ffmpeg foi lançada em 2016,
e a versão 0.1.5 de srt, em 2021, conforme consta no
RubyGems, nas respectivas
páginas de versões. Por serem antigas, é útil ter uma combinação testada;
as notas de compatibilidade do wrapper não garantem compatibilidade com versões atuais do FFmpeg.
Adicionar legendas com streamio-ffmpeg
Os exemplos foram testados no Linux com Ruby 3.4.10 e FFmpeg/FFprobe 9.0.1. Essas são versões testadas,
não versões mínimas intrínsecas; vale testar versões de correção mais recentes que recebam manutenção
nessas linhas de versão usando as verificações abaixo. Instale o
Ruby 3.4 e o
FFmpeg com FFprobe separadamente. Você precisa dos codificadores
libx264, AAC e WebVTT, do
filtro subtitles da libass e de uma fonte como DejaVu
Sans que contenha os caracteres do texto das legendas.
Use Bash em um diretório novo com permissão de escrita, sem definir RUBYOPT nem
BUNDLE_GEMFILE. Execute esta configuração nele e mantenha o mesmo shell aberto para os
comandos seguintes. A instalação e os metadados das gems ficam nesse diretório. O destino explícito
de instalação também evita uma instalação no diretório do usuário configurada no 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
)
A lista inclui a dependência multi_json do wrapper, por isso a instalação de
dependências está desativada e cada versão selecionada é ativada na função auxiliar abaixo. Se um
download falhar, execute este bloco novamente no mesmo diretório após corrigir a conexão. Verifique
a compilação instalada do FFmpeg antes de criar os arquivos de entrada:
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
A ajuda deve descrever cada filtro ou codificador indicado. Uma mensagem “unknown” significa que você precisa de outra compilação, mesmo que o comando de ajuda termine com sucesso.
Salve esta implementação compartilhada como subtitle_tools.rb. Todos os exemplos abaixo
a reutilizam. O diretório temporário mantém o nome do arquivo de legendas fora da sintaxe de filtros
do FFmpeg. A publicação usa um link físico no sistema de arquivos da saída; esse sistema de arquivos
deve oferecer suporte a links físicos.
A implementação de transcode do wrapper
valida metadados legíveis da saída sem verificar o status de saída do FFmpeg. Por isso, a função
auxiliar usa FFMPEG::Movie para os metadados e chama o FFmpeg com
system do Ruby, verificando o status antes de
publicar um resultado.
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
O processamento em lote abaixo chama essa função auxiliar sequencialmente. Ele recodifica o vídeo selecionado para H.264 e o áudio, quando presente, para AAC. A seleção padrão de fluxos do FFmpeg se aplica; ela não preserva todas as faixas de uma fonte com várias faixas. Use mídia local verificada: esses trechos não fornecem um ambiente isolado para uploads.
Salve esta interface da CLI ao lado desse arquivo como 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
Use seu MP4 e um arquivo SRT em UTF-8 ou gere este vídeo curto de teste. O comando recusa um
input.mp4 existente:
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
Salve estas entradas de legendas como 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
Abra output_with_subs.mp4 no seu player de vídeo. No clipe de teste, a primeira legenda
aparece de 0,6 a 1,4 segundos, a segunda de 2,1 a 3,2 segundos e a última de 4,0 a 4,2 segundos.
Os intervalos não devem conter legendas, e o vídeo de 4,3 segundos ainda deve ter seu tom sonoro.
Verificar se ele reproduz é apenas a primeira verificação: confirme o texto, a posição e os tempos
comparando com o SRT.
Manipular arquivos SRT com a gem SRT
Se todas as legendas aparecerem 2,5 segundos antes do momento correto, atrase todas as entradas
nesse intervalo. Salve isto como 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
As entradas de teste passam a ocupar os intervalos de 3,1–3,9, 4,6–5,7 e 6,5–6,7 segundos; o texto
permanece igual. O deslocamento altera a linha do tempo das legendas, então essas entradas mais
tardias ultrapassam o fim do vídeo curto de teste. Use o SRT sem deslocamento ao incorporar legendas
nesse clipe. O método timeshift documentado da gem
recebe um hash de opções. Ajuste o atraso para seu vídeo; este exemplo aplica o mesmo deslocamento
a todas as entradas e não corrige a perda progressiva de sincronização.
Converter formatos de legendas
Para produzir um arquivo WebVTT separado com os tempos originais das legendas, salve isto como
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
Para o SRT de exemplo, subtitles.vtt contém:
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
O WebVTT usa um cabeçalho e um ponto como separador de milissegundos. A conversão cria um arquivo separado; não adiciona uma faixa ao MP4. Para converter o SRT com os tempos deslocados:
ruby convert_subtitles.rb modified.srt shifted.vtt
Tratamento de erros na integração de legendas
Cada CLI retorna um status de saída diferente de zero quando a função auxiliar falha. A rotina de carregamento recusa arquivos ausentes, SRT maior que 1 MiB, UTF-8 inválido, erros relatados pelo analisador, texto de legenda vazio e durações não positivas das entradas. A gem pode registrar erros de análise em linhas individuais, então verificar apenas se uma exceção foi lançada não os detectaria.
Cada arquivo de saída deve ser novo, e seu diretório pai já deve existir. Uma nova execução recusa
um destino existente e preserva seus bytes. Preparar o resultado temporário junto ao destino e
publicá-lo com um link físico evita expor um resultado parcialmente gravado durante falhas comuns.
O encerramento forçado do processo pode deixar um diretório .subtitles-…; inspecione-o
antes de removê-lo.
O FFmpeg pode recuperar algumas mídias danificadas e ainda retornar sucesso. Em um teste com um pacote AAC danificado perto do fim, ele produziu um MP4 reproduzível com áudio mais curto. Metadados e decodificação bem-sucedida não podem provar que uma fonte desconhecida estava intacta. Compare a duração, o áudio e os quadros finais do resultado com sua entrada conhecida. Diagnósticos do FFmpeg podem aparecer em logs locais; mantenha-os separados das mensagens enviadas a clientes web.
Formatos de legendas compatíveis
Estes scripts aceitam SRT em UTF-8 e produzem SRT, WebVTT ou um MP4 com legendas incorporadas.
Os exemplos abrangem entradas simples de diálogo. Estilos ASS, posicionamento WebVTT e outros
formatos de legendas exigem outro fluxo de entrada; renomeá-los para .srt
não os converte.
Exemplo prático: processar vídeos com legendas em lote
Para um diretório de arquivos MP4, reutilize a mesma função auxiliar sequencialmente. Salve isto
como batch_subtitles.rb. O script associa lesson.mp4 a
lesson.srt; também aceita a extensão .MP4 em maiúsculas e
ignora entradas que são links simbólicos. O diretório de saída deve ser novo.
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
Crie videos e captions primeiro e coloque neles os arquivos
correspondentes. Legendas ausentes ou inválidas contam como falhas, e a mensagem identifica o nome
base do arquivo de vídeo. O processamento dos arquivos restantes ainda é tentado, e as saídas
bem-sucedidas são mantidas. O status de saída zero significa que todos os vídeos selecionados foram
convertidos; o status um significa que a preparação ou pelo menos uma conversão falhou. Para tentar
novamente, use um novo diretório de saída para que os arquivos concluídos continuem disponíveis.
Para uma alternativa gerenciada, consulte o Robot 🤖 /video/subtitle da Transloadit e seu serviço de codificação de vídeo.
