No description
Find a file
2026-09-21 18:08:29 +09:00
src/subs_translate initial commit 2026-09-21 18:00:33 +09:00
tests initial commit 2026-09-21 18:00:33 +09:00
.gitignore update gitignore 2026-09-21 18:08:29 +09:00
pyproject.toml initial commit 2026-09-21 18:00:33 +09:00
README.md initial commit 2026-09-21 18:00:33 +09:00
subs-translate-skill.md initial commit 2026-09-21 18:00:33 +09:00

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 &amp; and &lt;; existing entities such as &nbsp; 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