Subtitle Toolkit — convert, shift, merge, split, check avatar

Subtitle Toolkit — convert, shift, merge, split, check

Pricing

from $2.00 / 1,000 subtitle processeds

Go to Apify Store
Subtitle Toolkit — convert, shift, merge, split, check

Subtitle Toolkit — convert, shift, merge, split, check

Everyday subtitle jobs without transcription: convert SRT, WebVTT and ASS/SSA, shift timestamps, merge files with per-file offsets, split by cue count or seconds, and get a readability report with CPS outliers. Structured output, per-cue pricing.

Pricing

from $2.00 / 1,000 subtitle processeds

Rating

0.0

(0)

Developer

praestat media

praestat media

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 hours ago

Last modified

Share

One Apify Actor for the everyday subtitle jobs. It does not transcribe: you bring an existing subtitle file and get a cleaned, converted, shifted, merged or split file back, plus structured numbers about it. Everything runs inside the container — no ffmpeg, no external service, no network calls after the input file has been fetched.

Formats: SubRip (SRT), WebVTT (VTT), Advanced SubStation Alpha (ASS/SSA).

Operations

operationWhat it doesOutput
convertSwitch format: SRT ↔ WebVTT ↔ ASS/SSA; same rewrites the file in its own format (normalised, UTF-8)one subtitle file
shiftAdd seconds to every timestamp, negative allowed, never below 0; optionally only cues inside a time windowone subtitle file
mergeAppend files in the given order with an optional per-file offset, then sort by start timeone subtitle file
splitCut into parts by cue count or by secondsone file per part
reportCue count, duration, overlaps, line lengths, characters per second with outliers, plus language and speech-style metrics — see belowreport.json + flat metrics

Input examples

{
"operation": "convert",
"subtitleUrls": ["https://example.com/episode.srt"],
"targetFormat": "vtt"
}
{
"operation": "shift",
"subtitleUrls": ["https://example.com/episode.srt"],
"shiftSeconds": -1.5,
"windowFromSec": 600,
"windowToSec": 900
}
{
"operation": "merge",
"subtitleUrls": ["https://example.com/part1.srt", "https://example.com/part2.srt"],
"mergeOffsets": [0, 742.4],
"outputFormat": "ass"
}
{
"operation": "split",
"subtitleUrls": ["https://example.com/movie.srt"],
"splitEverySeconds": 600
}
{
"operation": "report",
"subtitleUrls": ["https://example.com/episode.vtt"],
"cpsThreshold": 20
}

Output

Every run pushes exactly one item to the dataset:

{
"operation": "convert",
"fmt": "vtt",
"sourceFormat": "srt",
"encoding": "utf-8",
"cueCount": 412,
"durationSec": 1450.2,
"file": "out/episode.vtt"
}

report returns the metrics flat, so they read well in the table view:

{
"operation": "report",
"cueCount": 412,
"durationSec": 1450.2,
"overlappingCues": 3,
"multiLineCues": 11,
"cuesWithoutEnd": 0,
"totalChars": 24118,
"averageCps": 16.63,
"maxCps": 31.4,
"cpsOutliers": [
{"index": 87, "cps": 31.4, "chars": 94, "durationSec": 2.99, "text": "…"}
],
"cpsOutlierCount": 1,
"longestLine": "…",
"longestLineChars": 94,
"file": "out/report.json"
}

split adds parts and a files list with one entry per part. All produced files land in the default key-value store under the prefix file-, so a follow-up run or your own script can pick them up directly.

What report gives you — and what it cannot

report reads one subtitle file and changes nothing. It writes report.json into the key-value store and returns exactly the same numbers flat in the dataset item, so the table view is already the report. Real output for samples/sample.srt (abridged):

{
"language": "de", "languageName": "Deutsch", "languageConfidence": 1.0,
"languages": [{"code": "de", "name": "Deutsch", "probability": 1.0, "matches": 5}],
"cueCount": 6, "durationSec": 17.9,
"overlappingCues": 1, "multiLineCues": 1, "cuesWithoutEnd": 0,
"totalChars": 321, "averageCps": 17.73, "maxCps": 48.57,
"cpsOutliers": [{"index": 4, "cps": 48.57, "chars": 136, "durationSec": 2.8, "text": "Diese Zeile …"}],
"cpsOutlierCount": 1, "longestLineChars": 136,
"style": {"wordsTotal": 50, "wordsPerMinute": 167.6, "avgWordsPerCue": 8.3,
"avgCharsPerLine": 31.0, "avgLinesPerCue": 1.5, "questionCues": 0,
"exclamationCues": 0, "uppercaseShare": 0.02,
"speakerLabels": [], "speakerLabelledCues": 0,
"formalAddress": 0, "informalAddress": 0, "register": null}
}

Timing and layout — cueCount, durationSec (first start to last end), overlappingCues (a cue that starts before the latest end seen so far), multiLineCues (more than two lines), cuesWithoutEnd, longestLine / longestLineChars.

Readability — totalChars, averageCps (all characters including spaces over the summed cue durations), maxCps, and cpsOutliers with cps, chars, durationSec and the full cue text, so you can jump straight to the line that is too fast. The threshold is an input field (cpsThreshold, default 20 characters per second).

Language — language / languageName / languageConfidence plus languages, a ranking of up to three candidates. The ranking counts matched function words (the, der, le …) rather than trusting a single probability, which is what makes it usable on short subtitle text. Below 20 letters of real text the Actor claims nothing and returns null — a guess would be worse than no answer.

Style — wordsTotal, wordsPerMinute (words over the whole span, pauses included), avgWordsPerCue, avgCharsPerLine, avgLinesPerCue, questionCues, exclamationCues, uppercaseShare, recognised speaker labels (MUELLER: at the start of a line, up to 25 characters, upper case) and — for German — formal vs. informal address (formalAddress, informalAddress, register).

What it cannot do:

  • It cannot tell whether the subtitles are in sync with the video. All timings come from the file; nothing is compared with the actual audio or picture. Drift, wrong offsets and missing cues stay invisible.
  • The language is a statistical guess. languageConfidence: 1.0 is what langdetect reports for a clear case; it is not a calibrated probability. Short cues, names, songs and mixed-language files can be wrong. The function-word ranking covers German, English, French, Spanish, Italian, Dutch, Portuguese, Polish, Swedish and Danish — other languages fall back to langdetect's top three without a match count.
  • Overlap counting is coarse. It is one counter against the running maximum end, not a list of pairs, so it tells you that something overlaps, not which cue collides with which.
  • German Sie is ambiguous. "Sie" is both formal you and she/they, so formalAddress can overcount on third-person text. register is therefore a hint, not a verdict.
  • wordsPerMinute is not speech rate. It divides words by the file's whole span, pauses included. 100–160 wpm is a rule of thumb for normal speech, not a limit this Actor enforces.
  • Guidelines are reported, not enforced. Netflix-style rules (≈42 characters per line, at most two lines) show up as avgCharsPerLine and multiLineCues; the Actor does not rewrite anything. report never changes the file — use convert or shift for that.

Encoding

Sources are read tolerantly: UTF-8 with or without BOM first, then Windows-1252 (the usual case for subtitles written on Windows, umlauts survive), then Latin-1. The encoding that was actually used is reported in the item as encoding. Output is always UTF-8 without BOM.

Pricing (pay per event)

EventWhenPrice
apify-actor-startonce per run and per GB of memory (1 GB default)$0.005
cue-processedper cue in the result$0.0002
file-convertedper produced file (a split into 4 parts counts 4)$0.002

A 30-minute episode with roughly 400 cues therefore costs about $0.09 for a conversion, and the same for a report. Long files scale with cue count, not with wall-clock time.

Limits and honest notes

  • No transcription. There is no speech recognition in this Actor — if you have audio or video only, this is the wrong tool.
  • Cue text is treated as plain text. SRT and WebVTT styling markup (<i>, <b>, {\an8}) is kept as characters in the text but not interpreted; ASS override blocks ({\i1} …) are removed when reading ASS, because they would otherwise end up as literal text in SRT and WebVTT.
  • ASS → ASS keeps the original [Script Info], [V4+ Styles] and [Events] header, including all styles. SRT/VTT → ASS produces one Default style — a full style authoring tool this is not.
  • merge takes the header (and thus the styles) of the first file. Cues from later ASS files that reference other style names fall back to Default.
  • Overlapping cues are reported, not repaired. Shifting can only clamp at 0; it cannot decide which cue should win.
  • Cues without an end time stay without one when read and are written with zero duration; they get no CPS value, because characters per second needs a duration.
  • split never cuts a cue in half. With splitEverySeconds a part can therefore be slightly longer than requested.
  • Output file names come from the source name (episode.srt → episode.vtt), or from outputName. Special characters are kept, paths are stripped.

Reporting a problem

Please use the Issues tab on the Actor page instead of email. The report then sits with the right Actor, together with the input and the run it came from — usually exactly what is missing to reproduce it. Include the file if you can (or the cue that breaks); subtitle files are small.

What happens to your data

Input files and results stay in your run's storage on Apify. Apify keeps the ten most recent runs of an Actor indefinitely; older runs and their default storages are deleted under the platform's retention rules. I do not export or archive your files anywhere else, and the Actor never sends them to a third-party service.

Local development

python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m pytest tests -q
echo '{"operation":"convert","subtitleUrls":["samples/sample.srt"],"targetFormat":"vtt"}' > /tmp/in.json
.venv/bin/python -m src --input /tmp/in.json --outdir out --push

samples/ holds a small SRT, VTT and ASS file with the same six cues — one overlapping cue and one CPS outlier included — for trying the Actor out.

The CLI path is the same code as the actor run, so what passes locally behaves identically on Apify.