Skip to content

Beautify time codes: snap to the video's real frame times - #14022

Merged
niksedk merged 1 commit into
mainfrom
claude/10235-exact-time-codes
Aug 23, 2026
Merged

Beautify time codes: snap to the video's real frame times#14022
niksedk merged 1 commit into
mainfrom
claude/10235-exact-time-codes

Conversation

@niksedk

@niksedk niksedk commented Aug 23, 2026

Copy link
Copy Markdown
Member

Fixes #10235.

The problem

"Beautify time codes" snaps cues to frame boundaries, but every SE5 call site passed TimeCodesBeautifier an empty time-code list, so it always fell back to nominal n/fps arithmetic. That grid is exact for genuinely constant-frame-rate material and wrong for everything else — VFR, remuxes, timestamp discontinuities.

SE4 did have the feature, extracting frame times with ffprobe -show_entries frame=pkt_dts_time. That was the original complaint in the issue: DTS is a decode/packet-domain timestamp, so with B-frames it is not the order frames are shown in, and it keeps the container's start offset (the reporter's own dump shows a ~1.9 s DTS→PTS gap). The port was dropped in SE5 rather than fixed, which took the feature with it and left ExtractExactTimeCodes as a dead setting.

The fix

Extract with ffmpeg's showinfo filter rather than ffprobe:

ffmpeg -i <video> -map 0:v:0 -an -sn -dn -vf showinfo -f null -

showinfo reports frames as they leave the decoder, so times are presentation-ordered, and without -copyts they are normalized to a zero-based timeline — the domain subtitle cues live in. It also needs no new binary: SE5 locates ffmpeg but never ffprobe.

Implementation notes

  • Sorted and de-duplicated before use. TimeCodesBeautifier binary searches the list (ClosestIndexTo) and indexes it by frame number, and validates neither — an out-of-order list silently yields wrong frame numbers rather than an error.
  • A partial list is rejected, not used. Because index maps to frame number, cues past the end of a truncated/cancelled extraction would snap to arbitrary frames instead of degrading to n/fps.
  • Progress uses the shared FfmpegProgressTracker on stdout while showinfo writes to stderr, so the two channels never interleave.
  • Extraction is async and cancellable; closing the window kills ffmpeg.
  • Results are cached per video under Se.TimeCodesFolder — the full decode happens once (~1 s per 2 min of 720p on this machine).
  • Batch convert uses a cached list when present; it never extracts on its own (that would decode every video in the batch).

UI

A checkbox, status and extract button in the Beautify window's top bar. Verified headlessly in all three states:

state
nothing loaded, extraction available [ ] Use exact time codes from video No time codes loaded [Extract time codes]
extracting [x] Use exact time codes from video Extracting time codes... [====43%----]
loaded [x] Use exact time codes from video 72,014 time codes loaded

Verification

Against real ffmpeg on purpose-built fixtures:

  • VFR clip — 24 frames at the true irregular 0 / .067 / .167 / .233 / .333 …, exactly matching ffmpeg. A nominal grid cannot express these.
  • B-frame clip — 100/100 frames, strictly ascending (presentation order confirmed: frame n:1 is type:B at pts_time:0.04).
  • Container with a 10 s start offset — extracted first=0, i.e. the offset that broke the SE4 DTS path is normalized away.

Unit tests cover showinfo parsing (including pts_time:N/A and progress lines), the sort/dedup guard, the incomplete-list guard, and an end-to-end beautify showing exact time codes land a cue on 233/900 ms (real frames) where the nominal 12.5 fps grid gives 240/880 ms (multiples of 80).

Full UI suite: 3493 passed.

English.json is regenerated from the language classes, so it also picks up already-merged speech-to-text strings that had not been regenerated yet.

🤖 Generated with Claude Code

Beautify could only assume a perfect n/fps frame grid: every SE5 call site
passed TimeCodesBeautifier an empty time-code list, so it always fell back to
nominal frame-rate arithmetic. That grid is exact for true CFR material and
wrong for everything else - variable frame rate, remuxes, timestamp
discontinuities - which is what issue #10235 reported.

SE4 had this feature and extracted the frame times with
"ffprobe -show_entries frame=pkt_dts_time". That was the original complaint:
DTS is a decode/packet-domain timestamp, so with B-frames it is not the order
frames are shown in, and it keeps the container's start offset. Cues snapped
against it drift away from the picture. The port was dropped in SE5 rather
than fixed, taking the feature with it and leaving ExtractExactTimeCodes as a
dead setting.

Extract with ffmpeg's showinfo filter instead of ffprobe:

  ffmpeg -i <video> -map 0:v:0 -an -sn -dn -vf showinfo -f null -

showinfo reports frames as they leave the decoder, so the times are
presentation-ordered, and without -copyts they are normalized to a zero-based
timeline - the same domain the subtitle cues live in. It also needs no new
binary: SE5 locates ffmpeg but never ffprobe.

Notes on the implementation:

- The list is sorted and de-duplicated before use. TimeCodesBeautifier binary
  searches it (ClosestIndexTo) and indexes it by frame number, and validates
  neither, so an out-of-order list would silently yield wrong frame numbers.
- A partial list is rejected rather than used. Since index maps to frame
  number, cues past the end of a truncated extraction would snap to arbitrary
  frames instead of falling back to n/fps.
- Progress comes from the shared FfmpegProgressTracker on stdout while
  showinfo writes frames to stderr, so the two never interleave.
- Extraction is async and cancellable, and closing the window kills ffmpeg.
- Results are cached per video under Se.TimeCodesFolder, so the full decode
  happens once (~1 s per 2 minutes of 720p here).
- Batch convert uses a cached list when present; it never extracts on its own.

English.json is regenerated from the language classes, so it also picks up
already-merged speech-to-text strings that had not been regenerated yet.

Fixes #10235

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@niksedk
niksedk merged commit 32f14bf into main Aug 23, 2026
1 check passed
@niksedk
niksedk deleted the claude/10235-exact-time-codes branch August 23, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-opening the timestamp issue here.

1 participant