Search by

ymakhloufi / subtitle-toolbox

yama6a

A PHP library and command line tool that reads, edits and writes subtitles, transcripts and chapter lists in more than 30 formats.

Package info

github.com/yama6a/subtitle-toolbox

pkg:composer/ymakhloufi/subtitle-toolbox

Statistics

Installs: 7 351

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 153

2.14.0 2026-10-06 17:03 UTC

This package is auto-updated.

Last update: 2026-10-06 17:06:34 UTC


README

Packagist version CI Licence

A PHP library and command line tool that reads, edits and writes subtitles, transcripts and chapter lists in more than 30 formats.

Upgrading from 1.x? See the upgrade guide.

Install

composer require ymakhloufi/subtitle-toolbox
# Optional, for OCR of PGS and VobSub image subtitles:
composer require yama6a/php-glyph-ocr:^0.3   # php-glyph-ocr, the pure PHP OCR engine
apt install tesseract-ocr                    # or Tesseract, for more than 100 languages, on Debian and Ubuntu

The core package needs neither OCR engine. See ocr.md for the install commands of other systems and languages.

The library needs PHP 8.2 or later with ext-dom and ext-iconv. Optional: ext-mbstring for Unicode upper and lower case, ext-zlib for PGS output and compressed MKV tracks and ext-curl for the DeepL and Google translation engines.

The command line tool also comes as a PHAR file and as two container images. The -tesseract image includes Tesseract for OCR.

curl -fsSLO https://github.com/yama6a/subtitle-toolbox/releases/latest/download/subtitle-toolbox.phar
php subtitle-toolbox.phar formats

docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" ghcr.io/yama6a/subtitle-toolbox convert movie.srt --to vtt -o movie.vtt
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" ghcr.io/yama6a/subtitle-toolbox:tesseract convert movie.sup --to srt -o movie.srt --ocr

Supported formats

Format Case Name Extensions Read Write Notes
ASS, SSA Ass ass .ass, .ssa yes yes
CSV, TSV Csv, Tsv csv, tsv .csv, .tsv yes yes not detected from the content
EBU STL EbuStl stl .stl yes yes binary, 25 or 30 fps
iTunes Timed Text Itt itt .itt yes yes needs a frame rate to write
LRC Lyrics lrc .lrc yes yes with enhanced LRC word times
MicroDVD MicroDvd microdvd .sub yes yes needs the frame rate of the video
MPL2 Mpl2 mpl2 .txt yes yes
MPSub MpSub mpsub .mpsub yes yes
SAMI Sami sami .smi, .sami yes yes one language class per parse
SBV Sbv sbv .sbv yes yes
SCC Scc scc .scc yes yes CEA-608 closed captions
SubRip SubRip srt .srt yes yes
SubViewer 1 and 2 SubViewer subviewer .sub yes yes
TMPlayer TmPlayer tmplayer .txt yes yes
TTML, IMSC, DFXP Ttml ttml .ttml, .dfxp, .xml yes yes
WebVTT WebVtt vtt .vtt yes yes also chapters
PGS Pgs pgs .sup yes yes Blu-ray bitmaps as image cues
VobSub VobSub vobsub .idx with .sub yes no DVD bitmaps as image cues
JSON of this library Json json .json yes yes
Plain text PlainText txt .txt no yes transcript
Whisper JSON Whisper whisper .json yes no OpenAI API, openai-whisper, faster-whisper, WhisperX, whisper.cpp
Cloud speech-to-text JSON AwsTranscribe, Deepgram, AssemblyAi, GoogleSpeech aws-transcribe, deepgram, assemblyai, google-speech .json yes no not detected from the content
YouTube timed text YouTubeTimedText youtube .json3, .srv3, .srv1 yes no json3, srv1, srv2, srv3 and transcript XML
Podcasting 2.0 transcript JSON PodcastTranscript podcast-transcript .json yes yes
HTML transcript HtmlTranscript html .html, .htm yes yes the Podcasting 2.0 HTML format
YouTube chapters YouTubeChapters youtube-chapters .txt yes yes not detected from the content
Podcasting 2.0 chapters PodcastChapters podcast-chapters .json yes yes not detected from the content
FFmpeg metadata chapters FfMetadataChapters ffmeta-chapters .ffmeta yes yes not detected from the content
OGM chapters OgmChapters ogm-chapters .txt yes yes not detected from the content
MKV and WebM tracks .mkv, .webm yes no text, ASS, SSA, WebVTT and PGS tracks

Case is the case of the enum Format, for example Format::SubRip. Name is the format name for --from and --to.

Command line tool

Composer installs vendor/bin/subtitle-toolbox. Optional parts are in brackets.

subtitle-toolbox convert movie.srt --to vtt [--timing-fix-overlaps] [-o movie.vtt]
subtitle-toolbox convert movie.mkv --to srt --track 3 [-o movie.srt]
subtitle-toolbox convert movie.sup --to srt --ocr [--ocr-language deu] [-o movie.srt]
subtitle-toolbox retime movie.sub --input-fps 25 --from-fps 25 --to-fps 23.976 [-o movie.fixed.sub]
subtitle-toolbox info movie.srt [--json]
subtitle-toolbox validate movie.srt --preset netflix-en [--video-fps 23.976]
subtitle-toolbox sync movie.de.srt --reference movie.en.srt [-o movie.de.synced.srt]
subtitle-toolbox diff movie.v1.srt movie.v2.srt [--text-only]
subtitle-toolbox translate movie.de.srt --engine deepl --target-language en-US [-o movie.en.srt]
subtitle-toolbox dual --primary movie.en.srt --secondary movie.de.srt --to ass [--mode stack] [-o movie.en-de.ass]
subtitle-toolbox hls movie.vtt --output-dir hls/ [--segment 6]
subtitle-toolbox formats
  • Without -o, the output goes to standard output.
  • Several inputs need --output-dir, for example retime season1/ --shift 2 --output-dir fixed/.
  • No command overwrites a file.
  • When detection fails, pass --from.

subtitle-toolbox convert --help lists the option groups of convert. See cli.md for all commands and options.

Library

Load and write

use SubtitleToolbox\Format;
use SubtitleToolbox\LineEnding;
use SubtitleToolbox\Subtitle;
use SubtitleToolbox\WriteOptions;

$subtitle = Subtitle::load('movie.srt', Format::SubRip);
$subtitle = Subtitle::loadAutoDetectFormat('movie.srt');
$subtitle->getFormat();                           // Format::SubRip
$subtitle->save('movie.vtt');                     // the format comes from the extension
$vtt = $subtitle->toString(Format::WebVtt, new WriteOptions(lineEnding: LineEnding::Crlf, stripTags: true));

Read options

use SubtitleToolbox\Format;
use SubtitleToolbox\Parsers\Options\MicroDvdReadOptions;
use SubtitleToolbox\ReadOptions;
use SubtitleToolbox\Subtitle;

$latin1   = Subtitle::load('latin1.srt', Format::SubRip, new ReadOptions(encoding: 'Windows-1252'));
$microDvd = Subtitle::load('movie.sub', Format::MicroDvd, new ReadOptions(format: new MicroDvdReadOptions(frameRate: 23.976)));

See read-options.md for the options of each format.

Edit

use SubtitleToolbox\CaseMode;
use SubtitleToolbox\Format;
use SubtitleToolbox\HearingImpaired\HearingImpairedOptions;
use SubtitleToolbox\HearingImpaired\HearingImpairedRemover;
use SubtitleToolbox\Subtitle;

$subtitle = Subtitle::load('movie.srt', Format::SubRip);
$subtitle->shift(-2.5)                            // all cues 2.5 s earlier
         ->convertFrameRate(25, 23.976)
         ->fixOverlaps(0.083)                     // a gap of at least 0.083 s between cues
         ->wrapLines(42)                          // at most 42 characters per line, 2 lines
         ->changeCase(CaseMode::Sentence);
$firstMinute = $subtitle->withSlice(0, 60);       // a new Subtitle, $subtitle stays as it is

$report = HearingImpairedRemover::apply($subtitle, new HearingImpairedOptions(parentheses: false));
echo "$report->removedLines lines removed\n";

A service such as HearingImpairedRemover changes the subtitle in place and returns a report.

Validate and count

use SubtitleToolbox\Format;
use SubtitleToolbox\Subtitle;
use SubtitleToolbox\SubtitleStatistics;
use SubtitleToolbox\Validation\ValidationRules;

$subtitle = Subtitle::load('movie.srt', Format::SubRip);
foreach ($subtitle->validate(ValidationRules::netflixEnglish(23.976)) as $violation) {
    echo "cue index $violation->cueIndex: {$violation->rule->value} is $violation->value\n";
}
$stats = SubtitleStatistics::of($subtitle);
echo "$stats->cueCount cues, $stats->wordCount words\n";

MKV and WebM tracks

use SubtitleToolbox\Subtitle;

foreach (Subtitle::tracks('movie.mkv') as $track) {
    echo "$track->number: $track->codecId, $track->language, $track->name\n";   // 3: S_TEXT/UTF8, de, Deutsch (Forced)
}
$subtitle = Subtitle::loadTrack('movie.mkv', 3);

OCR

use SubtitleToolbox\Format;
use SubtitleToolbox\Ocr\OcrEngineChooser;
use SubtitleToolbox\Ocr\OcrEngineName;
use SubtitleToolbox\Subtitle;

$subtitle = Subtitle::load('movie.sup', Format::Pgs);
$subtitle->recognizeText(OcrEngineChooser::create());                       // Tesseract if installed, otherwise php-glyph-ocr
$subtitle->recognizeText(OcrEngineChooser::create(OcrEngineName::Glyph));   // always php-glyph-ocr
$subtitle->save('movie.srt');

create() takes the Tesseract language as its second argument, for example 'deu+eng'. For engine settings, pass TesseractOcrOptions to new TesseractOcrEngine() or GlyphOcrOptions to new GlyphOcrEngine().

Translate

use SubtitleToolbox\Format;
use SubtitleToolbox\Subtitle;
use SubtitleToolbox\Translation\DeepLEngine;
use SubtitleToolbox\Translation\DeepLOptions;
use SubtitleToolbox\Translation\TranslationRunner;

$subtitle = Subtitle::load('movie.de.srt', Format::SubRip);
(new TranslationRunner(new DeepLEngine(new DeepLOptions(apiKey: $apiKey))))->translate($subtitle, 'de', 'en-US');
$subtitle->save('movie.en.srt');

GoogleTranslateEngine works the same way. See translation.md.

Sync to a reference

use SubtitleToolbox\Format;
use SubtitleToolbox\Subtitle;
use SubtitleToolbox\Sync\ReferenceSync;
use SubtitleToolbox\Sync\ReferenceSyncOptions;

$german  = Subtitle::load('movie.de.srt', Format::SubRip);
$english = Subtitle::load('movie.en.srt', Format::SubRip);
$report  = ReferenceSync::apply($german, new ReferenceSyncOptions($english));
echo "offset $report->offset s, scale $report->scale, score $report->score\n";

Every exception implements SubtitleToolboxException. See errors.md.

OCR

OCR turns the bitmaps of PGS and VobSub subtitles into text. The library uses Tesseract when it is installed. Otherwise it uses php-glyph-ocr. When neither engine is installed, the call throws InvalidArgumentException that names both engines. Tesseract reads more than 100 languages, php-glyph-ocr reads only Latin-script fonts. See ocr.md for the install commands and a comparison of the engines.

Compatibility

Semantic versioning covers the public PHP API and the CLI commands, options, exit codes and --json shapes. See compatibility.md for what a minor or patch release can change.

Documentation

docs/README.md lists every page. The most used pages:

  • cli.md: all commands and options
  • formats.md: what each parser reads and each formatter writes
  • editing.md: retiming, cutting, joining and splitting cues
  • text.md: text changes, hearing-impaired removal, common error fixes
  • validation.md: rules and presets
  • sync.md: sync to a reference or to the speech
  • subtitle.md: metadata, comments, cue lookup, statistics

Contributing

Pull requests are welcome. Run the tests with composer test. Each pull request carries one label that sets the version bump: major, minor, patch or skip-release. Every merge to master publishes a release.

Licence

MIT, see LICENSE.