# 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.
- `estimate` and `--dry-run` work without an API key. Responses are cached on disk, so re-runs are free.
- `--bilingual` stacks original and translation for language learners.

## Install

```bash
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

```bash
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

```json
{
  "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

```bash
.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.
