Beautify time codes: snap to the video's real frame times - #14022
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #10235.
The problem
"Beautify time codes" snaps cues to frame boundaries, but every SE5 call site passed
TimeCodesBeautifieran empty time-code list, so it always fell back to nominaln/fpsarithmetic. 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 leftExtractExactTimeCodesas a dead setting.The fix
Extract with ffmpeg's
showinfofilter rather than ffprobe:showinforeports frames as they leave the decoder, so times are presentation-ordered, and without-copytsthey 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
TimeCodesBeautifierbinary 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.n/fps.FfmpegProgressTrackeron stdout whileshowinfowrites to stderr, so the two channels never interleave.Se.TimeCodesFolder— the full decode happens once (~1 s per 2 min of 720p on this machine).UI
A checkbox, status and extract button in the Beautify window's top bar. Verified headlessly in all three states:
[ ] Use exact time codes from video No time codes loaded [Extract time codes][x] Use exact time codes from video Extracting time codes... [====43%----][x] Use exact time codes from video 72,014 time codes loadedVerification
Against real ffmpeg on purpose-built fixtures:
0 / .067 / .167 / .233 / .333 …, exactly matching ffmpeg. A nominal grid cannot express these.n:1istype:Batpts_time:0.04).first=0, i.e. the offset that broke the SE4 DTS path is normalized away.Unit tests cover showinfo parsing (including
pts_time:N/Aand 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.jsonis 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