Variable frame rate breaks caption timing
Phone footage and screen recordings often carry variable frame rate. Detect VFR, transcode to a constant rate, then caption so cues stop drifting.
If the captions are right at 0:20 and half a second early at 2:00, do not retime the track by hand. That shape is linear drift, and linear drift against speech is almost never a bad word timestamp. It is a file whose frame rate is not the number the captioner assumed, played for long enough that the error became visible.
Phone cameras and screen recorders write variable frame rate (VFR) as a default. The container still reports something like 30 fps. Individual frames do not arrive on a 33.3ms grid. Caption engines, and most NLEs that ingest "30 fps" without checking, treat that header as a constant. Every cue is placed on a clock the picture is not using. The longer the clip, the further the captions walk.
The drift is linear, which is the giveaway
A single late word is interpolation or a pause the recognizer swallowed. A track that starts locked and then runs early (or late) by a steadily growing amount is a rate problem.
Work a small example. You assume 30.000 fps constant. The phone actually delivered an average of 29.97 fps. Over 120 seconds that 0.1% disagreement is 120ms. Over four minutes it is a quarter of a second. Karaoke styles show it first; block subtitles show it once a sentence starts while the speaker is still on the previous one.
This is a different arithmetic from 24 fps clips on a 25 fps timeline. That mismatch is constant: 4.17% fast, 40ms of picture/audio drift per second, every second. You can see it on a ten-second talking head. VFR is messier. The header looks like a rate you know. The actual delivery changes. Drift still grows with duration, but not at a round percentage you can reverse with one speed change.
Two other clocks pile on, and they are easy to blame instead of VFR:
- Segment rounding. Whisper-class recognizers work in windows (commonly 30 seconds). Rounding those windows, then concatenating them, accumulates. On a clean constant-rate file this stays small. On VFR it rides on top of the rate error.
- Interpolated word times. Inside a segment, word positions are often spaced rather than measured. That makes karaoke look drunk locally. It does not walk a whole file in one direction. Forced alignment fixes the local error. It does not fix a container whose picture clock disagrees with the caption clock.
If you align first and the captions still arrive earlier every minute, you aligned the audio to a picture that is not playing at the rate you think.
How to tell VFR from a constant-rate mismatch
Do not trust the badge in Finder or Photos. Read the stream.
In ffprobe, compare the two rates the file reports:
ffprobe -v error -select_streams v:0 \
-show_entries stream=r_frame_rate,avg_frame_rate,nb_frames,duration \
-of default=nw=1 input.mov
r_frame_rate is the nominal rate. avg_frame_rate is frames divided by duration. If they disagree, treat the file as VFR. A 30/1 nominal against a 2997/100 average is the classic phone case. MediaInfo says the same thing in one line: Frame rate mode : Variable.
In an NLE, the warning is usually on import: "this clip has a variable frame rate." Premiere, Resolve, and Final Cut all have a version of that sentence. If you ignored it six months ago, this is the bill.
A timeline check that does not need extra tools: park on a hard consonant at 0:15 and at 3:00. If the first lands and the second is early by a slice that grew with time, you are not looking at one bad cue. You are looking at a clock.
| Source | Typical rate behaviour | Caption risk |
|---|---|---|
| iPhone / Android camera roll | VFR, header often 24, 30, or 60 | High on anything past a minute |
| Device screen recording | VFR, drops frames under load | High, and the drops are uneven |
| Zoom / Meet local recording | Often VFR | High on long calls |
| Generated clip from a video model | Constant, whatever the model wrote | Low for VFR; still check 24 vs 25 |
| Conform you already transcoded to CFR | Constant | Use this for captioning |
Generated clips are usually constant-rate. Versely's default video frame rate is 25 fps, not 24. Mix a 25 fps generate with a phone original and you can have a constant mismatch and a VFR problem in the same sequence. Conform everything to one constant rate before you caption.
Transcode to constant frame rate before any caption job
The fix is boring, and it is not a caption setting.
- Pick the delivery rate. If the rest of the project lives in Versely's editor, 25 fps is the rate that will not fight the default timeline. If you are delivering 30 fps social, pick 30 and stay there. Do not caption at 30 and conform to 25 afterwards.
- Transcode the VFR original to CFR at that rate. One example:
ffmpeg -i input.mov -fps_mode cfr -r 25 \
-c:v libx264 -crf 18 -c:a aac -ar 48000 \
output-cfr.mp4
If your ffmpeg build does not accept -fps_mode, -vsync cfr is the older equivalent. You want every output frame on a regular grid. CRF 18 is a quality-preserving transcode, not a delivery compress. Audio is re-encoded so its timestamps sit on the same trip as the picture. Copying the audio bitstream (-c:a copy) is fine when the audio timestamps were already clean; it is not fine when the container was a mess.
Caption the CFR file, not the camera original. Add captions (
add_veed_captions) transcribes the video's own audio and burns styled cues. Those cues are stored against the file you handed it. Hand it the VFR original and you have timed a clock you are about to throw away.Do not "fix" drift with a speed ramp on the caption layer. That hides the first minute and guarantees the fourth minute is worse. Speed is a picture decision. Captions follow the picture you actually export.
Screen recordings often drop frames in clumps when the machine is busy. CFR transcode will duplicate frames to fill the grid. A hitch on a fast cursor is still cheaper than captions that walk.
Caption against the conformed file, not the phone original
Once the picture clock is constant, captioning is ordinary.
- Isolate the vocal if music is already under the speech. Recognition and speech-to-text both degrade when the bed shares the voice band.
- Run auto-captions on the CFR file.
add_veed_captionssupports 165 language codes; set the code rather than letting it infer, especially on a batch. - Preview a style on the first few seconds of this file with
preview_caption_stylebefore you burn the whole duration. A VFR bug will not show up in five seconds. A wrong language code will. - For long cuts, spot-check a hard line at the start, the middle, and the last 15 seconds. Linear drift is visible at the tail. Local interpolation is visible on a single uneven sentence.
If you assemble in the video editor, keep the same rate through the EDL. Mixing a 25 fps generate, a 30 fps screen grab, and a VFR phone take is how captions land on clip one and drift on clip three. Conform each source, then assemble, then caption the assembly.
Burned-in captions make this order non-negotiable. Burned-in captions live in the pixels. If you caption the VFR original and then transcode to CFR, the letters are already in the wrong place and you cannot nudge them. Keep a CFR master with no captions and burn as the last step.
A request that encodes the order:
"This is a phone recording. Check whether the frame rate is variable. If it is, transcode to constant 25 fps, then add captions on the transcoded file, not the original. Preview glass on the first five seconds, then spot-check a line near the end before the full burn."
The editor's 480p preview pass is free, with a short per-user cooldown, and the final export is a single charge. That pass is capped at three minutes of timeline, so it will not reach the tail of a longer cut. Drift hides at the end: play the last 15 seconds in a player, or lift that region onto a short timeline if you want a 480p pass of the close.
FAQ
Is VFR the same problem as 24 fps on a 25 fps timeline?
No. 24-on-25 is a constant, known ratio. You can choose a conform (duplicate a frame a second, retime, or standardise the render rate) and the error is the same every second. VFR is an unstable clock that only looks like 30 fps in the header. Transcode to a constant rate first. Then, if you still have a 24-versus-25 decision, handle that as its own conform.
Will WhisperX or a phoneme aligner fix VFR drift?
It will fix interpolated word times against the audio it is given. If the picture is playing on a different clock than that audio, aligned cues still walk against the mouth. Align after the CFR transcode, on the file you will actually export.
Can I leave VFR if the clip is under 20 seconds?
Often, yes. Linear drift needs duration to become visible. A 15-second Reel may never show a 0.1% rate error. The moment you recut that take into a 3-minute YouTube version, or stitch ten of them, the same files will. Conform once, at ingest, and you stop thinking about it.
Why did my generated clips caption cleanly and my phone clips did not?
Generated video is written at a constant rate (on Versely, the default is 25 fps). Phone and screen-recording files frequently are not. The captioner is the same. The clocks are not. Transcode the phone footage to CFR at the project rate and the difference goes away.