A PHP library and command line tool that reads, edits and writes subtitles, transcripts and chapter lists in more than 30 formats.
Upgrading from 2.x? The package has a new name. See the upgrade guide.
Upgrading from 3.x? See the 4.0 upgrade guide.
composer require yama6a/subtitle-toolbox-php
# Optional, for OCR of PGS, VobSub and DVB image subtitles:
composer require yama6a/php-glyph-ocr:^0.3 # php-glyph-ocr, the pure PHP OCR engine
apt install tesseract-ocr # or Tesseract on Debian and Ubuntu, for more than 100 languagesThe 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, ext-iconv and ext-mbstring. These extensions are optional:
ext-zlibfor PGS output and compressed MKV tracksext-curlfor the translation engines
The command line tool also comes as a PHAR file and as two container images. The -tesseract image includes Tesseract for OCR. See Install without Composer for the commands.
Case is the case of the enum Format, for example Format::SubRip. Name is the format name for --from and --to. An image cue holds a bitmap in place of text.
| Format | Case | Name | Extensions | Read | Write | Notes |
|---|---|---|---|---|---|---|
| ASS, SSA | Ass |
ass |
.ass, .ssa |
✅ | ✅ | |
| Cavena 890 | Cavena890 |
cavena890 |
.890 |
✅ | ✅ | binary, no public spec, not detected from the content |
| CSV, TSV | Csv, Tsv |
csv, tsv |
.csv, .tsv |
✅ | ✅ | not detected from the content |
| DCP subtitles: SMPTE ST 428-7, Interop | Dcp |
dcp |
.xml |
✅ | ✅ | digital cinema text subtitles |
| EBU STL | EbuStl |
stl |
.stl |
✅ | ✅ | binary, 25 or 30 fps |
| iTunes Timed Text | Itt |
itt |
.itt |
✅ | ✅ | needs a frame rate to write |
| JSON of this library | Json |
json |
.json |
✅ | ✅ | |
| LRC | Lyrics |
lrc |
.lrc |
✅ | ✅ | with enhanced LRC word times |
| MCC | Mcc |
mcc |
.mcc |
✅ | ✅ | CEA-608 and CEA-708 closed captions of MacCaption and CaptionMaker |
| MicroDVD | MicroDvd |
microdvd |
.sub |
✅ | ✅ | needs the frame rate of the video |
| MPL2 | Mpl2 |
mpl2 |
.txt |
✅ | ✅ | |
| MPSub | MpSub |
mpsub |
.mpsub |
✅ | ✅ | |
| PAC by Screen Electronics, PAC Unicode | Pac |
pac |
.pac, .fpc |
✅ | ✅ | binary, no public spec, code page detected |
| QuickTime text | QuickTimeText |
qttext |
.txt |
✅ | ✅ | {QTtext} text tracks of QuickTime |
| SAMI | Sami |
sami |
.smi, .sami |
✅ | ✅ | one language class per parse |
| SBV | Sbv |
sbv |
.sbv |
✅ | ✅ | |
| SCC | Scc |
scc |
.scc |
✅ | ✅ | CEA-608 closed captions |
| Spruce STL | SpruceStl |
spruce-stl |
.stl |
✅ | ✅ | DVD Studio Pro text subtitles |
| SubRip | SubRip |
srt |
.srt |
✅ | ✅ | |
| SubViewer 1 and 2 | SubViewer |
subviewer |
.sub |
✅ | ✅ | |
| TMPlayer | TmPlayer |
tmplayer |
.txt |
✅ | ✅ | |
| TTML, IMSC, DFXP | Ttml |
ttml |
.ttml, .dfxp, .xml |
✅ | ✅ | |
| WebVTT | WebVtt |
vtt |
.vtt |
✅ | ✅ | also chapters |
| YouTube timed text | YouTubeTimedText |
youtube |
.srv3, .json3, .srv1, .ytt |
✅ | ✅ | json3, srv1, srv2, srv3 and transcript XML |
| Format | Case | Name | Extensions | Read | Write | Notes |
|---|---|---|---|---|---|---|
| PGS | Pgs |
pgs |
.sup |
✅ | ✅ | Blu-ray bitmaps as image cues |
| VobSub | VobSub |
vobsub |
.idx with .sub |
✅ | ❌ | DVD bitmaps as image cues |
| Format | Case | Name | Extensions | Read | Write | Notes |
|---|---|---|---|---|---|---|
| Cloud speech-to-text JSON: AWS Transcribe, Deepgram, AssemblyAI, Google Speech-to-Text | AwsTranscribe, Deepgram, AssemblyAi, GoogleSpeech |
aws-transcribe, deepgram, assemblyai, google-speech |
.json |
✅ | ✅ | not detected from the content |
| HTML transcript | HtmlTranscript |
html |
.html, .htm |
✅ | ✅ | the Podcasting 2.0 HTML format |
| Plain text | PlainText |
txt |
.txt |
❌ | ✅ | transcript |
| Podcasting 2.0 transcript JSON | PodcastTranscript |
podcast-transcript |
.json |
✅ | ✅ | |
| Whisper JSON | Whisper |
whisper |
.json |
✅ | ✅ | OpenAI API, openai-whisper, faster-whisper, WhisperX, whisper.cpp |
| Format | Case | Name | Extensions | Read | Write | Notes |
|---|---|---|---|---|---|---|
| FFmpeg metadata chapters | FfMetadataChapters |
ffmeta-chapters |
.ffmeta |
✅ | ✅ | not detected from the content |
| OGM chapters | OgmChapters |
ogm-chapters |
.txt |
✅ | ✅ | not detected from the content |
| Podcasting 2.0 chapters | PodcastChapters |
podcast-chapters |
.json |
✅ | ✅ | not detected from the content |
| YouTube chapters | YouTubeChapters |
youtube-chapters |
.txt |
✅ | ✅ | not detected from the content |
| Format | Case | Name | Extensions | Read | Write | Notes |
|---|---|---|---|---|---|---|
| MKV and WebM tracks | .mkv, .webm |
✅ | ❌ | text, ASS, SSA, WebVTT, PGS, VobSub and DVB subtitle tracks | ||
| MP4 tracks | .mp4, .m4v, .mov |
✅ | ❌ | tx3g (mov_text) tracks, c608 and c708 closed caption tracks |
||
| MPEG-TS ARIB captions | .ts, .m2ts |
✅ | ❌ | Japanese ISDB and Brazilian ISDB-Tb captions with furigana and gaiji | ||
| MPEG-TS DVB subtitles | .ts |
✅ | ❌ | DVB bitmap subtitles as image cues | ||
| MPEG-TS teletext | .ts |
✅ | ❌ | DVB teletext subtitle pages, see ETS 300 706 |
Composer installs vendor/bin/stbox. Optional parts are in brackets.
stbox convert movie.srt --to vtt [--timing-fix-overlaps] [-o movie.vtt]
stbox convert movie.mkv --to srt --track 3 [-o movie.srt]
stbox convert movie.sup --to srt --ocr [--ocr-language deu] [-o movie.srt]
stbox convert movie.sub --input-fps 25 --from-fps 25 --to-fps 23.976 [-o movie.fixed.sub]
stbox info movie.srt [--json]
stbox validate movie.srt --preset netflix-en [--video-fps 23.976]
stbox sync movie.de.srt --reference movie.en.srt [-o movie.de.synced.srt]
stbox diff movie.v1.srt movie.v2.srt [--text-only]
stbox translate movie.de.srt --engine deepl --target-language en-US [-o movie.en.srt]
stbox dual --primary movie.en.srt --secondary movie.de.srt --to ass [--mode stack] [-o movie.en-de.ass]
stbox hls movie.vtt --output-dir hls/ [--segment 6]
stbox formats- Without
-o, the output goes to standard output. - Several inputs need
--output-dir, for exampleconvert season1/ --shift 2 --output-dir fixed/. - No command overwrites a file.
- When detection fails, pass
--from.
stbox convert --help lists the option groups of convert. See cli.md for all commands and options.
use SubtitleToolbox\Format;
use SubtitleToolbox\LineEnding;
use SubtitleToolbox\Subtitle;
use SubtitleToolbox\WriteOptions;
$subtitle = Subtitle::load('movie.srt', Format::SubRip);
$subtitle = Subtitle::load('movie.srt');
$subtitle->findFormat(); // Format::SubRip
$subtitle->save('movie.vtt'); // the format comes from the extension
$vtt = $subtitle->toString(Format::WebVtt, new WriteOptions(lineEnding: LineEnding::Crlf, stripTags: true));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(formats: [new MicroDvdReadOptions(frameRate: 23.976)]));See read-options.md for the options of each format.
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.
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";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);
$subtitle = Subtitle::loadTrack('recording.ts', 777); // teletext page 777 of a DVB recording
$subtitle = Subtitle::loadTrack('recording.ts', 2050); // DVB bitmap subtitles with PID 2050, as image cuesuse 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().
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.
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(reference: $english));
echo "offset $report->offset s, scale $report->scale, score $report->score\n";Every exception implements SubtitleToolboxException. See errors.md.
OCR turns the bitmaps of PGS, VobSub and DVB 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.
See compatibility.md for the parts that semantic versioning covers and for what a minor or patch release can change.
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
Pull requests are welcome. See CONTRIBUTING.md for the licence rules and the credit rule. 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.
MIT, see LICENSE. Material from third parties keeps its own licence, see THIRD_PARTY_NOTICES.md.
The PHP libraries mantas-done/subtitles and captioning/captioning shaped this library. Their formats, test files and user reports guided many decisions here. THIRD_PARTY_NOTICES.md lists every file copied from other projects.