Splice into the source cut
Give it the source video, its shot table and the picked clip for each shot you redid;
get back the source rebuilt segment by segment: replaced frame ranges come from the
clips (trimmed to exactly the range, their own audio dropped), every other frame is the
original footage, and the source's soundtrack plays under all of it. Shots you left out
fall back to the original, which is also how "use the original footage" is answered for
a shot. For a 9:16 version use ffmpeg-crop-to-vertical, which crops the source shots
and splices restaged renders the same way. When a clip's own sound must play (a
re-voiced dialogue line), this is the wrong leaf: see python-assemble-dialogue-track.
Inputs
- The source: frame rate, size and frame count (
ffprobe-inspect-media). - The shot table: first and last frame of each shot, inclusive (
ffmpeg-scene-cuts). Use the same table the shots were cut from. - Per replaced shot, the picked clip, already at the source's frame rate and at least
as many frames as its range. A 720p take is upscaled first (
topaz-upscale), though a full-frame shot that ships at 1080p quality should be rendered at 1080p instead. A clip at another rate (a lip-sync service returning 25 fps) is converted back before it comes here.
Run it
Build the segment list first: sort the replacements, check none overlap, and fill every gap with the original. Example: a 484-frame, 24 fps, 1920×1080 source with shots 03 (frames 41–73) and 09 (frames 286–371) replaced gives five segments:
| Segment | Frames | From |
|---|---|---|
| 0 | 0–40 | source |
| 1 | 41–73 (33) | rev2/shot03.mp4, its first 33 frames |
| 2 | 74–285 | source |
| 3 | 286–371 (86) | rev2/shot09.mp4, its first 86 frames |
| 4 | 372–483 | source |
Then one ffmpeg call. Input 0 is the source; each replacement is its own input:
exec ffmpeg -hide_banner -loglevel error -y
-i <job>/source/acme_office_spot.mp4
-i <job>/rev2/shot03.mp4
-i <job>/rev2/shot09.mp4
-filter_complex "
[0:v]trim=start_frame=0:end_frame=41,setpts=PTS-STARTPTS,scale=1920:1080,setsar=1,format=yuv420p[v0];
[1:v]trim=end_frame=33,setpts=PTS-STARTPTS,scale=1920:1080,setsar=1,format=yuv420p[v1];
[0:v]trim=start_frame=74:end_frame=286,setpts=PTS-STARTPTS,scale=1920:1080,setsar=1,format=yuv420p[v2];
[2:v]trim=end_frame=86,setpts=PTS-STARTPTS,scale=1920:1080,setsar=1,format=yuv420p[v3];
[0:v]trim=start_frame=372:end_frame=484,setpts=PTS-STARTPTS,scale=1920:1080,setsar=1,format=yuv420p[v4];
[v0][v1][v2][v3][v4]concat=n=5:v=1:a=0[v]"
-map "[v]" -map 0:a
-r 24 -t 20.166667
-c:v libx264 -crf 14 -preset slow -pix_fmt yuv420p -c:a aac -b:a 192k
-movflags +faststart
<job>/rev2/acme_spot_B_cut_rev2.mp4
(Pass the filter graph as one argument; the line breaks are for reading.)
end_frameis exclusive: a source segment isstart_frame=FIRST:end_frame=LAST+1; a clip segment isend_frame=LAST-FIRST+1from the clip's first frame.- Every segment is normalised (size, square pixels, yuv420p) so concat sees identical
streams. Use the source's own size in
scale=. -map 0:alays the source's soundtrack under the whole cut; the clips' audio is dropped.-r <fps>on the output, and nofps=in the segment chains (Tips and traps).-tis the source's length, frames / fps (484 / 24 = 20.166667), so a soundtrack that runs past the picture is cut there.
Copies for delivery
Make these from the finished cut or any shot file, as the job's deliverable layout asks
(AGENTS.md):
- small review copy:
-vf scale=960:540 -c:v libx264 -preset veryslow -crf 28 -pix_fmt yuv420p -c:a aac -b:a 96k - no-audio copy:
-an -c:v copy
What you get
The cut at the source's length, size and rate, over its own audio. List which segments
came from which clip in the round's notes (41-73: shot03.mp4, 0-40: original).
Tips and traps
- ffmpeg 7 drops frames without an explicit output rate.
-r <fps>on the output keeps every frame; anfps=filter inside each segment chain before the concat lost one frame per shot (472 of 484). - Not
-shortest. A source's AAC track often ends a few milliseconds before its picture, and-shortestthen drops the last video frame (483 of 484). Bound the output with-tinstead. - A short clip shifts everything after it.
trim=end_frame=Non a clip with fewer than N frames gives fewer frames, and the concat closes the gap. Count each clip's frames before splicing. - Splice the exact shot length; the 0.5 s tails on per-shot files are for the editor, not for the cut.
- Keep the folder you splice from apart from what goes to the client when the client's layout differs, so a shot left out on purpose still falls back to the original.
Check it
- The cut's frame count equals the source's
(
exec ffmpeg -v error -i <cut> -map 0:v -f framemd5 -, counting lines that don't start with#), and its duration matches. - A contact sheet of the frames either side of every join (FIRST−1, FIRST, LAST, LAST+1 of each replaced range), looked at, so no join shows a stray frame of the wrong shot:
exec ffmpeg -v error -y -i <cut>
-vf "select='eq(n,40)+eq(n,41)+eq(n,73)+eq(n,74)+eq(n,285)+eq(n,286)+eq(n,371)+eq(n,372)',scale=480:-2,tile=4x2:padding=4"
-frames:v 1 -update 1 <job>/rev2/joins_sheet.jpg
- Play the cut through once with sound before it goes on a review page.


