- Python 100%
| src/subs_translate | ||
| tests | ||
| .gitignore | ||
| pyproject.toml | ||
| README.md | ||
| subs-translate-skill.md | ||
subs-translate
A command line helper for agents translating WebVTT subtitles. The agent translates
text; subs keeps cue identity, timing, project state, and output generation consistent.
No LLM API or credentials are required.
Install and run
Python 3.11 or later is required.
uv sync
uv run subs --help
The runtime uses only the Python standard library. A VTT-specific structural parser
preserves features that the pysubs2 and webvtt-py round trips we evaluated lost.
This is a translation editor, not a complete WebVTT conformance validator.
Agent workflow
Initialize once, choosing the translation language:
uv run subs init original.vtt translated.vtt --language fr
uv run subs read translated.vtt --next 30 --context 3
Initialization creates translated.vtt with its header and preserved non-cue blocks,
and translated.vtt.state.json with the source fingerprint and translation state.
It refuses to overwrite an existing target or sidecar. No placeholder cues are emitted.
An existing Language: header is updated; other header metadata is retained.
read returns JSON with cues to translate and a separate context array. Context
may include pending or completed neighbors; use it for continuity, not as additional
work. Each cue has a stable, 1-based source ID, source text, original timestamps,
duration, settings, and its existing translation (null when pending).
IDs are independent of optional VTT identifiers and positions in the partial output.
Send translations as a JSON array. Stdin is the default; --input - is equivalent:
uv run subs write translated.vtt <<'JSON'
[
{"id": 1, "text": "Le chat est l’animal"},
{"id": 2, "text": "préféré des Européens."}
]
JSON
uv run subs status translated.vtt
uv run subs read translated.vtt --next 30 --context 3
Repeat reading and writing until remaining is zero, then run:
uv run subs check translated.vtt
write --input translations.json reads a UTF-8 file instead. Each object must contain
exactly an integer id and a string text; timestamps cannot be modified through the
write API. Batches can arrive out of order, and read --next N skips completed cues.
The generated VTT contains all completed cues in their original source order.
Text and markup
text is WebVTT cue text, not a complete cue block. Unicode and single newlines
are supported. Use \n in JSON for a line break. Blank lines, control characters, and
literal --> are rejected because they can corrupt cue boundaries.
Preserve original inline tags (including voice, styling, language, and inline timing
tags) verbatim and in the same order. Their positions within translated text may move.
Literal & and < must be escaped as & and <; existing entities such as
are accepted. Tags are checked independently of prose so a translation
cannot silently remove or introduce formatting. --allow-empty explicitly completes
a cue with empty text, including when the source had markup.
The original cue identifiers, timing/settings lines, STYLE, REGION, NOTE blocks, BOM, and source newline convention are retained. Blank-line separators are normalized. Translations may need shorter phrasing to fit fixed cue durations; this tool does not retime cues or evaluate translation quality. Inline word timings are preserved too, so their linguistic alignment needs care when translating languages with different word order.
Revisions, recovery, and checks
# Inspect an arbitrary source range, including completed translations.
uv run subs read translated.vtt --start 21 --count 10 --context 3
# Deliberately revise existing translations.
uv run subs write translated.vtt --input corrections.json --replace
# Optionally reject a batch based on a stale read (revision comes from read/status).
uv run subs write translated.vtt --input corrections.json --replace --expect-revision 4
# Check the current partial export without requiring every cue to be complete.
uv run subs check translated.vtt --allow-partial
# Regenerate a missing, edited, or interrupted VTT export.
uv run subs export translated.vtt
Identical retries succeed without changing the revision. Conflicting replacements
require --replace. A batch is fully validated before any state or output changes.
Mutations use an advisory project lock; simultaneous mutations fail with project_busy
and can be retried. Keep the small .state.json.lock file in place while commands run.
The sidecar is authoritative for translations; the original source provides structure. Keep both. The source path is relative to the sidecar so the project can be moved together. Source changes are detected by SHA-256 and rejected to keep IDs stable. Regenerating output replaces manual edits to the target VTT.
State and output are each staged and atomically replaced, with the sidecar committed
first. Two separate files cannot be replaced as a single transaction. An interruption
between replacements can leave the VTT behind the sidecar; export repairs it. A write
that reports export_failed has already saved its translations. No translations need
to be resubmitted for that error.
Successful commands print compact JSON on stdout. Errors print
{"error":{"code":"...","message":"..."}} on stderr and exit nonzero; argument
errors exit 2. check fails for incomplete work unless --allow-partial is set,
and always detects an output that differs from the generated VTT. Help and version
output are human-readable.
Development
uv run python -m unittest discover -s tests -v