Translate subtitles through a pluggable backend #190

Closed
opened 2026-08-24 21:28:32 +01:00 by naps62-yolo · 1 comment
Owner

Add the translation layer to arr-subs: a trait every backend implements, and
the chunking and validation that sit above all of them. Backends themselves are
separate issues (#191, #192, #193).

From DESIGN.md §15 (added by #183):

  • The unit sent to a backend is a batch of cues, not a whole file. A movie
    subtitle is 40-80k characters and a season several hundred thousand; one
    request per file is fragile and one per cue loses the context that makes
    dialogue translate well.
  • Timing data never leaves arr. Only text goes out, and the translated text is
    reassembled onto the original timings.
  • A reply whose cue count or numbering does not match the batch is rejected.
    A backend that silently merges or drops lines must fail loudly rather than
    produce a subtitle whose lines have drifted out of sync with the picture.
  • Source and target language are explicit. pt-PT and pt-BR are distinct
    targets; a backend that cannot express the difference says so rather than
    quietly returning the wrong one.
  • Every backend sits behind its own cargo feature. Which backend runs is a
    database setting (#198), so switching between compiled-in backends needs no
    rebuild.

Non-goals: any specific backend, any decision about when to translate — that
is the reconcile loop's (#196).

Depends on: #184

Add the translation layer to `arr-subs`: a trait every backend implements, and the chunking and validation that sit above all of them. Backends themselves are separate issues (#191, #192, #193). From DESIGN.md §15 (added by #183): - The unit sent to a backend is a batch of cues, not a whole file. A movie subtitle is 40-80k characters and a season several hundred thousand; one request per file is fragile and one per cue loses the context that makes dialogue translate well. - Timing data never leaves arr. Only text goes out, and the translated text is reassembled onto the original timings. - A reply whose cue count or numbering does not match the batch is rejected. A backend that silently merges or drops lines must fail loudly rather than produce a subtitle whose lines have drifted out of sync with the picture. - Source and target language are explicit. pt-PT and pt-BR are distinct targets; a backend that cannot express the difference says so rather than quietly returning the wrong one. - Every backend sits behind its own cargo feature. Which backend runs is a database setting (#198), so switching between compiled-in backends needs no rebuild. Non-goals: any specific backend, any decision about *when* to translate — that is the reconcile loop's (#196). Depends on: #184
naps62-yolo added this to the Subtitles milestone 2026-08-24 21:28:32 +01:00
naps62-yolo added the area/subsdifficulty/hardphase/9-subtitlestype/feature labels 2026-08-24 21:28:32 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:27 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:27 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:27 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:27 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:27 +01:00
naps62-yolo added a new dependency 2026-08-24 21:29:28 +01:00
Author
Owner

Implemented the translation layer in arr-subs on branch subtitles/190-translate. A new translate module carries the object-safe Backend trait (boxed futures, same shape as Provider), an id type for the database setting, and the shared machinery no backend can skip: chunking cues into character-budgeted batches (default 4096 chars, overridable per backend via batch_budget()), a supports(target) check so a backend that cannot express pt-PT vs pt-BR refuses before anything is sent, validation that rejects any reply whose cue count or numbering does not match the batch, and reassembly of translated text onto the original timings — timings never leave arr, and any failed batch fails the whole call so a partially translated subtitle never exists. A companion srt module provides the Cue type with tolerant SRT parsing and strict rendering, since reassembly onto original timings needs cues in structured form and sidecars are SRT per §15. Backend errors get their own translate::Error (transport / unauthorized / rate-limited / unsupported target / malformed / cue mismatch) with the same is_transient() split as the provider error.

just ci passed (511 tests).

Findings

  • No issue owns decoding downloaded subtitle bytes to UTF-8. Fetched.content is deliberately raw bytes (providers serve Latin-1/Windows-1252), and srt::parse deliberately takes &str; something between #187/#188 and #194/#195/#196 has to do the encoding detection, and no issue in the milestone mentions it.
  • Nothing converts non-SRT downloads (SubtitleFormat::Ass/Vtt) to SRT before syncing/writing. §15 says sidecars are SRT and model.rs notes converting "is not this crate's decision to make silently", but no issue decides it either.
Implemented the translation layer in `arr-subs` on branch `subtitles/190-translate`. A new `translate` module carries the object-safe `Backend` trait (boxed futures, same shape as `Provider`), an id type for the database setting, and the shared machinery no backend can skip: chunking cues into character-budgeted batches (default 4096 chars, overridable per backend via `batch_budget()`), a `supports(target)` check so a backend that cannot express pt-PT vs pt-BR refuses before anything is sent, validation that rejects any reply whose cue count or numbering does not match the batch, and reassembly of translated text onto the original timings — timings never leave arr, and any failed batch fails the whole call so a partially translated subtitle never exists. A companion `srt` module provides the `Cue` type with tolerant SRT parsing and strict rendering, since reassembly onto original timings needs cues in structured form and sidecars are SRT per §15. Backend errors get their own `translate::Error` (transport / unauthorized / rate-limited / unsupported target / malformed / cue mismatch) with the same `is_transient()` split as the provider error. `just ci` passed (511 tests). ## Findings - No issue owns decoding downloaded subtitle bytes to UTF-8. `Fetched.content` is deliberately raw bytes (providers serve Latin-1/Windows-1252), and `srt::parse` deliberately takes `&str`; something between #187/#188 and #194/#195/#196 has to do the encoding detection, and no issue in the milestone mentions it. - Nothing converts non-SRT downloads (`SubtitleFormat::Ass`/`Vtt`) to SRT before syncing/writing. §15 says sidecars are SRT and `model.rs` notes converting "is not this crate's decision to make silently", but no issue decides it either.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: yolo/arr#190