Files
arr/crates/arr-db/migrations/0034_subtitle_skeletons.sql
Miguel PalhasandClaude Opus 5 6cd2902341
ci / web (push) Successful in 1m0s
e2e / e2e (push) Failing after 3m17s
ci / rust (push) Successful in 4m0s
ci / image (push) Successful in 3m56s
feat(subs): translate from an image-only release
A release whose only subtitle is a PGS or VobSub track was stuck both
ways: the track satisfied English so no English SRT was ever fetched,
and bitmaps can never feed a translator. §15 is amended to separate
satisfying viewing from providing a translation source, and to carve a
fetch made to obtain a source out of the no-upgrade rule.

Timings come from the disc rather than from alass guessing at the audio.
At import, each non-forced image track's packet timestamps are paired
show-to-clear into a cue skeleton and stored; the fetched source is then
aligned against that skeleton, and the translation made from it skips
the post-translation pass, which could only move disc-exact timings off.

Pairing is validated before it is trusted — even packet count, plausible
durations, sane density for the runtime — because PGS allows several
composition segments per subtitle and a slipped pairing is quietly half
a second out. A track that fails validation gets no skeleton and falls
back to aligning against the video, as does any file imported before
this: there is no backfill.

Closes #268

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 11:53:26 +01:00

35 lines
1.9 KiB
SQL

-- #268. The cue structure of an image-format subtitle track (DESIGN.md §15).
--
-- A PGS or VobSub track carries bitmaps, so it can never feed a translator.
-- Its packet timings are still exact for the release it came off, and that is
-- what `alass` matches on: aligning a downloaded English subtitle against this
-- skeleton lands it on the disc's own cue structure instead of on a heuristic
-- read of the video's audio.
--
-- Derived once at import, where the file is being read and hardlinked anyway.
-- A track whose packets do not pair cleanly gets no row at all — the absence
-- is the fallback signal, and the lane aligns against the video instead.
CREATE TABLE subtitle_skeletons (
media_file_id INTEGER NOT NULL REFERENCES media_files (id) ON DELETE CASCADE,
-- The track's position among the file's subtitle streams, the same index
-- `ffmpeg -map 0:s:N` takes.
stream_index INTEGER NOT NULL CHECK (stream_index >= 0),
-- As `arr_core::Language` spells it. The language a source subtitle is
-- fetched in when this skeleton is the alignment reference.
language TEXT NOT NULL,
-- The cues, as a JSON array of `[start_ms, end_ms]` pairs in file order.
-- No text: there is none to read, and `alass` does not want any.
cues TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (media_file_id, stream_index)
) STRICT;
-- §15, as amended by #268: a subtitle aligned against a cue skeleton already
-- carries disc-exact timings, so the post-translation `alass` pass is skipped
-- for it and for anything translated from it. A second pass can only move
-- those timings off. This says which rows that applies to, durably — a crash
-- between the fetch and the translation must not lose the fact.
ALTER TABLE subtitle_files
ADD COLUMN skeleton_aligned INTEGER NOT NULL DEFAULT 0
CHECK (skeleton_aligned IN (0, 1));