Fetch a text translation source and align it to the embedded track's timings #268

Closed
opened 2026-08-30 10:38:32 +01:00 by naps62-yolo · 0 comments
Owner

DESIGN.md §15. Found while testing a 2160p BluRay import (Batman Begins, movie 5): pt-PT can never materialise for that file, for two compounding reasons.

The deadlock

The release carries one English subtitle track, hdmv_pgs_subtitle — bitmaps. §15 says an embedded track satisfies its language, so English reads as satisfied and arr never downloads an English SRT. §15 also says image tracks can never feed a translator. So translation_source() (crates/arr-daemon/src/subtitles.rs:913) returns None — no sidecar with a path, no embedded text track — and the lane stops with "no text source to translate from".

The file is stuck both ways: it cannot translate because it has no text, and it will not fetch text because it believes it already has English.

#206 describes the same dead end and proposes OCR, but its premise is "an English BluRay with no external subtitle available". That premise does not hold here — an English subtitle is readily available from OpenSubtitles, arr simply never asks for one. Closing the gap that way is far cheaper than OCR, and yields real text instead of OCR'd text that a translator would then turn into nonsense.

The timing objection, and why it does not require OCR

The embedded track's genuine advantage is that its timings are exact for this release, while a downloaded subtitle needs alass — a heuristic with no confidence value, which is why §15 already gates its output on plausibility.

That advantage is available without reading a single pixel: PGS timings are packet metadata. ffprobe -select_streams s:0 -show_packets returns them directly, so a reference can be synthesised from the disc's own cue structure and used as alass's reference instead of the video's audio.

Spike results

On the real file, over the whole film:

cues: 1473   implausible: 0   (even packet count: yes)

duration_time is N/A for PGS, but packets strictly alternate show/clear, so pairing them yields cues — every implied duration fell between 0.3s and 10s across all 1473.

Alignment, measured on a synthetic 125-minute film (923 cues) whose subtitle was mistimed by a 23.976→25fps drift plus an offset, i.e. 316 seconds out by the end:

reference median error worst within 150ms
before alignment 152,703 ms 316,288 ms 0%
skeleton, 70% of cues present 1 ms 2 ms 100%
skeleton, 20% of cues present 1 ms 2 ms 100%
skeleton, 8% of cues present 1 ms 2 ms 100%

alass matches on interval structure, not words, so the two cue sets never have to agree — a sparse skeleton is still a strong signal. That matters because a retail disc's track and a downloaded SDH subtitle never carry the same cues.

The pairing is load-bearing, though. Treating every packet as a cue start instead of pairing them gives a consistent ~486 ms error — better than 152 s, but visibly wrong:

reference built from packets median error within 150ms
paired as show/clear 0 ms 100%
naive, every packet a cue 486 ms 0%

Work

  1. At import, for each non-forced image-format subtitle track, derive a cue skeleton by pairing packet timestamps, and store it. Validate before use: even packet count, all durations plausible, cue density sane for the runtime. PGS permits several composition segments per subtitle, so a track that fails validation must fall back to today's video-based alignment rather than produce a skeleton that is quietly half a second out.
  2. When a wanted language needs translation and no text source exists, fetch a text subtitle in a source language even though that language reads as satisfied by an image track. Migration 0029's unique index is partial (WHERE path IS NOT NULL), so the fetched sidecar coexists with the embedded row — no schema change.
  3. Align the fetched source against the skeleton rather than the video, then translate.
  4. Skip the post-translation alass pass when the source was aligned to a skeleton. The timings are already disc-exact and a second pass can only move them off.
  5. Amend §15. Two sentences create the deadlock: that an embedded track satisfies its language, and that a satisfied language is never fetched again. The amendment should separate satisfying viewing from providing a translation source, and carve out a fetch made to obtain a source from the no-upgrade rule.

Derivation costs a full demux, so it belongs at import — while the file is being read and hardlinked anyway — not on the translation path.

Not this issue

OCR (#206) stays filed for the case this does not reach: no provider has a text subtitle in any language, alongside #207. This issue should shrink #206's remaining scope to that case rather than replace it.

DESIGN.md §15. Found while testing a 2160p BluRay import (Batman Begins, movie 5): pt-PT can never materialise for that file, for two compounding reasons. ## The deadlock The release carries one English subtitle track, `hdmv_pgs_subtitle` — bitmaps. §15 says an embedded track satisfies its language, so English reads as satisfied and arr never downloads an English SRT. §15 also says image tracks can never feed a translator. So `translation_source()` (`crates/arr-daemon/src/subtitles.rs:913`) returns `None` — no sidecar with a path, no embedded *text* track — and the lane stops with "no text source to translate from". The file is stuck both ways: it cannot translate because it has no text, and it will not fetch text because it believes it already has English. #206 describes the same dead end and proposes OCR, but its premise is "an English BluRay with no external subtitle available". That premise does not hold here — an English subtitle is readily available from OpenSubtitles, arr simply never asks for one. Closing the gap that way is far cheaper than OCR, and yields real text instead of OCR'd text that a translator would then turn into nonsense. ## The timing objection, and why it does not require OCR The embedded track's genuine advantage is that its timings are exact for this release, while a downloaded subtitle needs alass — a heuristic with no confidence value, which is why §15 already gates its output on plausibility. That advantage is available without reading a single pixel: PGS timings are packet metadata. `ffprobe -select_streams s:0 -show_packets` returns them directly, so a reference can be synthesised from the disc's own cue structure and used as alass's reference instead of the video's audio. <details> <summary>Spike results</summary> On the real file, over the whole film: ``` cues: 1473 implausible: 0 (even packet count: yes) ``` `duration_time` is `N/A` for PGS, but packets strictly alternate show/clear, so pairing them yields cues — every implied duration fell between 0.3s and 10s across all 1473. Alignment, measured on a synthetic 125-minute film (923 cues) whose subtitle was mistimed by a 23.976→25fps drift plus an offset, i.e. 316 seconds out by the end: | reference | median error | worst | within 150ms | |---|---|---|---| | before alignment | 152,703 ms | 316,288 ms | 0% | | skeleton, 70% of cues present | 1 ms | 2 ms | 100% | | skeleton, 20% of cues present | 1 ms | 2 ms | 100% | | skeleton, 8% of cues present | 1 ms | 2 ms | 100% | alass matches on interval structure, not words, so the two cue sets never have to agree — a sparse skeleton is still a strong signal. That matters because a retail disc's track and a downloaded SDH subtitle never carry the same cues. The pairing is load-bearing, though. Treating every packet as a cue start instead of pairing them gives a consistent ~486 ms error — better than 152 s, but visibly wrong: | reference built from packets | median error | within 150ms | |---|---|---| | paired as show/clear | 0 ms | 100% | | naive, every packet a cue | 486 ms | 0% | </details> ## Work 1. At import, for each non-forced image-format subtitle track, derive a cue skeleton by pairing packet timestamps, and store it. Validate before use: even packet count, all durations plausible, cue density sane for the runtime. PGS permits several composition segments per subtitle, so a track that fails validation must fall back to today's video-based alignment rather than produce a skeleton that is quietly half a second out. 2. When a wanted language needs translation and no text source exists, fetch a text subtitle in a source language even though that language reads as satisfied by an image track. Migration 0029's unique index is partial (`WHERE path IS NOT NULL`), so the fetched sidecar coexists with the embedded row — no schema change. 3. Align the fetched source against the skeleton rather than the video, then translate. 4. Skip the post-translation alass pass when the source was aligned to a skeleton. The timings are already disc-exact and a second pass can only move them off. 5. Amend §15. Two sentences create the deadlock: that an embedded track satisfies its language, and that a satisfied language is never fetched again. The amendment should separate satisfying *viewing* from providing a *translation source*, and carve out a fetch made to obtain a source from the no-upgrade rule. Derivation costs a full demux, so it belongs at import — while the file is being read and hardlinked anyway — not on the translation path. ## Not this issue OCR (#206) stays filed for the case this does not reach: no provider has a text subtitle in *any* language, alongside #207. This issue should shrink #206's remaining scope to that case rather than replace it.
naps62-yolo added the area/subsdifficulty/moderatephase/9-subtitlestype/feature labels 2026-08-30 10:38:37 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#268