srtlate
Context-aware subtitle translator for SRT and VTT files. It keeps timing intact, re-wraps lines to a readable 42x2 layout, keeps names and terms consistent across a whole film through an editable glossary, and does it cheaply: one Claude Haiku 5.5 call per ~60 cues, usually well under a cent per film.
Features
- Parses and writes SRT and VTT (keeps
<i>/<b>tags,{\an8}positioning, VTT cue settings, STYLE blocks); converts between the two by output extension. - Groups cues into dialogue scenes (gap > 2 s) and uses scenes to place batch boundaries and to give the model scene-change hints.
- Batches of ~60 numbered cues, each with the previous 5 translated cues as context. Structured JSON output; every id must come back exactly once, only the missing ids are retried.
- Glossary pass: names and recurring terms are extracted deterministically, one cheap call proposes renderings, you can edit the JSON and reuse it (
--glossary). --formality formal|informal(Hindi aap/tum, Spanish usted/tu, French vous/tu, ...).- Post-processing: balanced re-wrap to at most 42 chars and 2 lines (prefers punctuation), italics re-closed per line, dialogue dashes kept, reading speed flagged above 21 chars/s, RTL marks for Arabic/Hebrew/Persian/Urdu.
- Resumable: progress is written to a sidecar JSON after every batch; Ctrl-C is safe.
estimateand--dry-runwork without an API key. Responses are cached on disk, so re-runs are free.--bilingualstacks original and translation for language learners.
Install
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate
export ANTHROPIC_API_KEY=... # only needed for real translation
Quickstart
srtlate estimate examples/lighthouse.en.srt --to es
srtlate translate examples/lighthouse.en.srt --to es --dry-run
# offline demo against a recorded fixture, no key needed:
srtlate translate examples/lighthouse.en.srt --to es --fake examples/lighthouse.fixture.json
# real run (needs a key):
srtlate translate examples/lighthouse.en.srt --to hi --formality informal
Usage
srtlate translate movie.srt --to hi [--from auto] [--out movie.hi.srt]
[--glossary g.json | --no-glossary] [--formality formal|informal]
[--bilingual] [--batch-size 60] [--model ...] [--dry-run] [--fresh]
[--no-cache] [--cache-dir DIR] [-v]
srtlate glossary movie.srt --to hi [--out movie.glossary.json]
srtlate estimate movie.srt --to es
The default output name replaces a trailing language tag in the input name (movie.en.srt -> movie.hi.srt). The output format follows the --out extension, so --out movie.vtt converts.
Estimate (no key needed; real output)
$ srtlate estimate examples/lighthouse.en.srt --to hi --formality informal
120 cues in 3 batches (4 API calls)
input ~2,570 tokens
output ~4,378 tokens
cost ~$0.0024 (Haiku 5.5: $0.10 / 1M in, $0.50 / 1M out; rough, +/-30%)
Estimates use a character-based token heuristic and a per-language output expansion factor, so expect roughly +/-30%.
Dry run (no key needed; real output, abridged)
$ srtlate translate examples/lighthouse.en.srt --to hi --formality formal --dry-run
=== glossary call: user ===
Source language: the source language (detect it). Target language: Hindi.
Candidates (term, count, example):
Mira (7x): Mainland base, this is Mira at Lantern Point.
Bram (7x): Come on, Bram's old hinge, give me a break.
...
=== translate call (batch 1): user ===
Translate from the source language (detect it) to Hindi.
Register: formal (use aap and its verb forms).
Cues to translate (a blank line marks a scene change):
[1] <i>Mainland base, this is Mira at Lantern Point.</i>
[2] <i>Do you copy? Over.</i>
...
[32] - Is anyone up there?
- Yes! Who is that?
Offline demo against the fixture (real output)
--fake answers from examples/lighthouse.fixture.json instead of the API. It exercises the full pipeline (glossary, batching, validation, wrapping, writing) with recorded Spanish translations.
$ srtlate translate examples/lighthouse.en.srt --to es --fake examples/lighthouse.fixture.json -v
glossary: 6 terms -> examples/lighthouse.es.glossary.json (edit and pass --glossary to reuse)
batch 1/3 done
batch 2/3 done
batch 3/3 done
wrote examples/lighthouse.es.srt (120 cues)
2 cues need a look:
cue 10: speed (21.8 chars/sec)
cue 101: speed (22.1 chars/sec)
usage: 4 calls, 0 cache hits, 1,706 in / 2,492 out, $0.0014
First cues of the result (the long Spanish line was re-wrapped and the italics re-closed per line):
1
00:00:01,000 --> 00:00:04,520
<i>Base del continente, habla</i>
<i>Mira, desde Punta Linterna.</i>
2
00:00:04,870 --> 00:00:06,770
<i>¿Me recibe? Cambio.</i>
With --bilingual --out x.vtt:
WEBVTT
00:00:01.000 --> 00:00:04.520
<i>Mainland base, this is Mira at Lantern Point.</i>
<i>Base del continente, habla</i>
<i>Mira, desde Punta Linterna.</i>
Glossary
{
"to": "es",
"terms": { "Mira": "Mira", "Tomas": "Tomás", "Lantern": "Linterna" }
}
translate writes this next to the output when it extracts one. Edit it and pass --glossary file.json to pin renderings; a user glossary skips the extraction call. A flat {"source": "target"} file also works.
Sections above that call the model for real (plain translate without --fake/--dry-run, and glossary without --fake) require ANTHROPIC_API_KEY; live calls were not exercised when this README was written because no key was available.
Configuration
| Setting | Default |
|---|---|
ANTHROPIC_API_KEY |
required for live calls (or ant auth login) |
--model / SRTLATE_MODEL |
claude-haiku-5-5 |
SRTLATE_CACHE |
~/.cache/srtlate |
--batch-size |
60 |
Errors are reported cleanly: missing credentials, rate limits (progress is kept, re-run to resume), bad requests, connection and server errors, and refusals.
Architecture
| Module | Role |
|---|---|
subs.py |
SRT/VTT parse and write |
scenes.py |
scene grouping and batch cutting |
prompts.py |
system prompts, JSON schemas, prompt builders |
llm.py |
Haiku wrapper: structured output, usage, cost, disk cache |
glossary.py |
candidate extraction, glossary call, glossary file IO |
translate.py |
batching, id validation and retry, context, resume sidecar |
wrap.py |
wrapping, tag repair, reading speed |
render.py |
applies translations to cues, flags, bilingual and RTL |
estimate.py |
token and cost estimate |
fake.py |
fixture-backed stand-in client for offline demos |
cli.py |
click commands |
See docs/design.md for the algorithms.
Tests
.venv/bin/pytest -q # 58 tests, all use fake clients, no network
Limitations
- Token and cost estimates are heuristics.
- Wrapping only splits into two lines; a cue that cannot fit 2x42 characters is flagged, not shortened.
- Reading-speed flags are advisory; timing is never changed.
- RTL output only adds RLM marks; players differ in punctuation placement.
- Embedded styling beyond
<i>,<b>,<u>and leading{\an..}tags is passed through as the model sees it. - Glossary extraction finds capitalised names and terms; lowercase recurring terms need to be added to the glossary by hand.
- Live API calls have not been exercised against the real service in this environment.