Extract text subtitle streams to sidecar SRT #189

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

Extract text-format embedded subtitle tracks to sidecar SRT files, so an
embedded English track can serve as a translation source. Bazarr cannot do
this, and it is the single reason the operator asked for embedded handling at
all (DESIGN.md §15, added by #183).

  • arr-probe already reports subtitle tracks with resolved languages. It must
    additionally report the codec, because the split that matters is text versus
    bitmap.
  • Text formats — subrip, ass, mov_text — extract with ffmpeg, mapping
    the single stream and converting to SRT. Spawn-and-die, like ffprobe.
  • Image formats — PGS, VobSub — carry bitmaps. They satisfy their language for
    viewing and are recorded as satisfying, but they are never extracted and
    never become a translation source. No OCR (that is deliberately deferred; see
    the separate backlog issue).
  • Every embedded track, extracted or not, gets a subtitle row from #186 so the
    reconcile loop can tell a satisfied language from a gap without re-probing.

An extracted track is written as a sidecar under the naming rule in §15, with
no .mt segment — it is a real subtitle, not a machine translation.

Depends on: #184, #186

Extract text-format embedded subtitle tracks to sidecar SRT files, so an embedded English track can serve as a translation source. Bazarr cannot do this, and it is the single reason the operator asked for embedded handling at all (DESIGN.md §15, added by #183). - `arr-probe` already reports subtitle tracks with resolved languages. It must additionally report the codec, because the split that matters is text versus bitmap. - Text formats — `subrip`, `ass`, `mov_text` — extract with `ffmpeg`, mapping the single stream and converting to SRT. Spawn-and-die, like `ffprobe`. - Image formats — PGS, VobSub — carry bitmaps. They satisfy their language for viewing and are recorded as satisfying, but they are never extracted and never become a translation source. No OCR (that is deliberately deferred; see the separate backlog issue). - Every embedded track, extracted or not, gets a subtitle row from #186 so the reconcile loop can tell a satisfied language from a gap without re-probing. An extracted track is written as a sidecar under the naming rule in §15, with no `.mt` segment — it is a real subtitle, not a machine translation. Depends on: #184, #186
naps62-yolo added this to the Subtitles milestone 2026-08-24 21:28:32 +01:00
naps62-yolo added the area/subsphase/9-subtitlestype/featuredifficulty/moderate 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
Author
Owner

Scope addition from the milestone driver — please fold this in.

Two separate reports (#184 and #186) landed on the same gap: arr_core::SubtitleTrack carries only language. It needs two more facts, and both belong to the probe change this issue already owns:

  1. The codec — already in this issue's scope, splitting text formats (subrip, ass, mov_text) from image formats (PGS, VobSub) per §15.
  2. The forced disposition — not currently in any issue. §15 says a forced track never satisfies a want. #186's schema keys embedded rows on (media_file_id, language, forced, sdh), but with nothing supplying forced every embedded track records as forced = false, so a file carrying only a forced English track reads as satisfied for English and the reconcile loop stops looking. ffprobe reports it as disposition.forced (and disposition.hearing_impaired for SDH, worth carrying at the same time).

Add both to SubtitleTrack and to whatever arr-probe fills it from. This does not widen the issue much — it is the same struct and the same ffprobe read.

Scope addition from the milestone driver — please fold this in. Two separate reports (#184 and #186) landed on the same gap: `arr_core::SubtitleTrack` carries only `language`. It needs two more facts, and both belong to the probe change this issue already owns: 1. **The codec** — already in this issue's scope, splitting text formats (`subrip`, `ass`, `mov_text`) from image formats (PGS, VobSub) per §15. 2. **The `forced` disposition** — *not* currently in any issue. §15 says a forced track never satisfies a want. #186's schema keys embedded rows on `(media_file_id, language, forced, sdh)`, but with nothing supplying `forced` every embedded track records as `forced = false`, so a file carrying only a forced English track reads as satisfied for English and the reconcile loop stops looking. ffprobe reports it as `disposition.forced` (and `disposition.hearing_impaired` for SDH, worth carrying at the same time). Add both to `SubtitleTrack` and to whatever `arr-probe` fills it from. This does not widen the issue much — it is the same struct and the same ffprobe read.
Author
Owner

Implemented #189 on branch subtitles/189-extract-subs (two commits, based off blitz/subtitles). arr_core::SubtitleTrack now carries the codec — a new SubtitleCodec enum (SubRip, Ass, MovText, Pgs, VobSub, Other) with an is_text() split per §15 — plus the forced and sdh dispositions from the milestone-driver comment; arr-probe fills all three from ffprobe's codec_name and disposition. Extraction lives in a new arr_probe::Extractor: one spawn-and-die ffmpeg call maps a single subtitle stream and converts it to SRT, writing beside the target under a dot-name and renaming so no half-written sidecar is ever visible. A pure layout::subtitle_name helper builds §15's <video>.<lang>.srt / .mt.srt names. The committed fixture clip gained a forced track and was regenerated via make-probe-fixtures.sh; tests cover codec mapping (including PGS/VobSub names), live extraction of text tracks, stream indexing by subtitle position, and out-of-range-stream failure.

just ci: passed (gate exit 0; 499 tests green). Not merged, no PR opened.

Findings

  • ffmpeg has no PGS encoder (and refuses text→bitmap subtitle conversion generally), so the fixture uses a forced SRT track instead of a bitmap one; the PGS/VobSub codec-name mapping is covered by a unit test on ffprobe's documented names rather than a real clip.
  • The daemon's probed_json (crates/arr-daemon/src/import.rs) now serialises codec/forced/sdh into the media_files.probed column, but nothing yet writes embedded tracks to subtitle_files at import time — I read that wiring as #196's step 1 and left it there.
  • NewSubtitleFile::extracted in arr-db takes the path but ignores its own builder pattern (..Self::new) inconsistently with the other constructors — cosmetic only, works fine.
Implemented #189 on branch `subtitles/189-extract-subs` (two commits, based off `blitz/subtitles`). `arr_core::SubtitleTrack` now carries the codec — a new `SubtitleCodec` enum (`SubRip`, `Ass`, `MovText`, `Pgs`, `VobSub`, `Other`) with an `is_text()` split per §15 — plus the `forced` and `sdh` dispositions from the milestone-driver comment; `arr-probe` fills all three from ffprobe's `codec_name` and `disposition`. Extraction lives in a new `arr_probe::Extractor`: one spawn-and-die ffmpeg call maps a single subtitle stream and converts it to SRT, writing beside the target under a dot-name and renaming so no half-written sidecar is ever visible. A pure `layout::subtitle_name` helper builds §15's `<video>.<lang>.srt` / `.mt.srt` names. The committed fixture clip gained a forced track and was regenerated via `make-probe-fixtures.sh`; tests cover codec mapping (including PGS/VobSub names), live extraction of text tracks, stream indexing by subtitle position, and out-of-range-stream failure. `just ci`: **passed** (gate exit 0; 499 tests green). Not merged, no PR opened. ## Findings - ffmpeg has no PGS encoder (and refuses text→bitmap subtitle conversion generally), so the fixture uses a forced SRT track instead of a bitmap one; the PGS/VobSub codec-name mapping is covered by a unit test on ffprobe's documented names rather than a real clip. - The daemon's `probed_json` (`crates/arr-daemon/src/import.rs`) now serialises codec/forced/sdh into the `media_files.probed` column, but nothing yet writes embedded tracks to `subtitle_files` at import time — I read that wiring as #196's step 1 and left it there. - `NewSubtitleFile::extracted` in arr-db takes the path but ignores its own builder pattern (`..Self::new`) inconsistently with the other constructors — cosmetic only, works fine.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: yolo/arr#189