Does a Tiny Desk AI Clip Keep the Reference's Cuts and Camera Moves? Measuring Shot Timing With NumPy Phase Correlation (Tested) A developer built a NumPy and ffmpeg-only tool that uses phase correlation to measure hard cuts and global camera motion in short vertical video clips, then applied it to eight public sample files from the Tiny Desk Outstanding photo-to-video studio to test its claim that generated clips preserve the source's length, camera and soundtrack. The script decodes at 12 fps and 144×256 grayscale, estimating pan and tilt from the phase-correlation peak while flagging cuts where the peak falls below 0.06. "Does the AI version keep the same camera moves as the original?" is one of the first things people ask about photo-to-video meme generators. The Tiny Desk Outstanding https://tinydeskoutstanding.video/ studio makes a strong claim here. Its about page says a photo "does not change the length, the camera, or the song already in the clip", and every scene page calls the source "a 15-second vertical clip with the original soundtrack". That's a testable claim. The home page has eight public sample files: the "Whole Band reference video" and seven generated or alternate cuts. So I measured two things in each one: where the hard cuts are and how much the whole picture moves between frames . I used NumPy and ffmpeg only, no OpenCV. Everything below was run on the box on November 14, 2026 Asia/Shanghai , and the outputs are pasted as they came out. When a camera pans, most of the frame shifts by the same amount. When you shift an image, its Fourier transform keeps the same magnitude and only the phase changes. If you divide the cross-power spectrum of two frames by its own magnitude and take the inverse FFT, you get one sharp peak at the shift. That peak's height also tells you something useful: So the same 10 lines give you a pan/tilt estimate and a cut detector. camera motion.py decodes the clip at 12 fps, 144×256 grayscale aspect forced to 9:16 so clips of different sizes compare , and runs phase correlation between consecutive frames: bash /usr/bin/env python3 """Global camera motion + cut map for a short vertical clip NumPy + ffmpeg only . usage: python3 camera motion.py clip.mp4 more.mp4 ... --fps 12 --csv out.csv """ import argparse, csv, subprocess, sys import numpy as np W, H = 144, 256 analysis size 9:16 ; small is enough for global motion def frames path, fps : cmd = "ffmpeg", "-v", "error", "-i", path, "-vf", f"fps={fps},scale={W}:{H}:force original aspect ratio=disable,format=gray", "-f", "rawvideo", "-" raw = subprocess.run cmd, capture output=True, check=True .stdout return np.frombuffer raw, np.uint8 .reshape -1, H, W .astype np.float32 WIN = np.outer np.hanning H , np.hanning W .astype np.float32 def phase corr a, b : """Return dx, dy, peak so that b is roughly a shifted by dx, dy .""" A = np.fft.fft2 a - a.mean WIN B = np.fft.fft2 b - b.mean WIN R = np.conj A B R /= np.abs R + 1e-9 r = np.fft.ifft2 R .real y, x = np.unravel index np.argmax r , r.shape if y H // 2: y -= H if x W // 2: x -= W return float x , float y , float r.max def analyse path, fps, cut peak=0.06 : f = frames path, fps rows = for i in range 1, len f : dx, dy, peak = phase corr f i - 1 , f i rows.append {"t": round i / fps, 3 , "dx": dx, "dy": dy, "peak": round peak, 3 , "cut": peak < cut peak} return rows def summary rows : cuts = r "t" for r in rows if r "cut" merge cut flags closer than 0.25 s one cut can flag two pairs merged = for t in cuts: if not merged or t - merged -1 0.25: merged.append t still = r for r in rows if not r "cut" pan = sum abs r "dx" for r in still tilt = sum abs r "dy" for r in still moving = sum 1 for r in still if abs r "dx" + abs r "dy" = 1 / max len still , 1 return merged, pan, tilt, moving if name == " main ": ap = argparse.ArgumentParser ap.add argument "clips", nargs="+" ap.add argument "--fps", type=float, default=12 ap.add argument "--csv" a = ap.parse args out = print f"{'clip':<14}{'cuts s ':<34}{'pan px': 8}{'tilt px': 8}{'moving': 8}" for c in a.clips: rows = analyse c, a.fps cuts, pan, tilt, moving = summary rows name = c.rsplit "/", 1 -1 :12 print f"{name:<14}{', '.join f'{t:.2f}' for t in cuts :<34}{pan: 8.0f}{tilt: 8.0f}{moving: 8.0%}" out += {"clip": name, r} for r in rows if a.csv: with open a.csv, "w", newline="" as fh: w = csv.DictWriter fh, fieldnames=list out 0 w.writeheader ; w.writerows out Before pointing it at real clips, I cut two windows out of the same blurred noise texture with a known offset, and also compared the frame with unrelated noise: python import numpy as np, camera motion as cm smooth random texture, then cut two windows offset by a known shift rng = np.random.default rng 0 big = rng.random cm.H + 40, cm.W + 40 .astype np.float32 for in range 3 : cheap blur so it looks like an image, not noise big = big + np.roll big, 1, 0 + np.roll big, 1, 1 + np.roll big, -1, 0 + np.roll big, -1, 1 / 5 a = big 20:20 + cm.H, 20:20 + cm.W for dx, dy in 3, 0 , -2, 5 , 7, -4 : b = big 20 - dy:20 - dy + cm.H, 20 - dx:20 - dx + cm.W x, y, peak = cm.phase corr a, b print "true", dx, dy , "measured", x, y , "peak", round peak, 3 other = rng.random cm.H, cm.W .astype np.float32 print "unrelated frame peak", round cm.phase corr a, other 2 , 3 true 3, 0 measured 3.0, 0.0 peak 0.977 true -2, 5 measured -2.0, 5.0 peak 0.972 true 7, -4 measured 7.0, -4.0 peak 0.936 unrelated frame peak 0.03 The shift comes back exactly, matching frames peak at 0.94 to 0.98, and unrelated frames peak at 0.03. In the real clips, cuts landed at 0.027 to 0.045 and ordinary frames had a median peak of 0.23 to 0.31, so a 0.06 threshold sits in the gap. python3 camera motion.py samples/ .mp4 --csv motion.csv clip cuts s pan px tilt px moving 0299078c3f43 5.33, 11.67, 15.00 152 160 57% 632c0129d45a 5.33, 11.67, 15.00 248 212 71% 8256cd873b7e 5.25, 11.42, 14.75 225 203 60% 875d2458181e 102 60 94% 947f5eb2a447 1.50, 7.00, 13.67, 17.33 257 258 59% db033545db00 1.58, 4.75, 7.83 160 145 64% db09b1c54359 5.17, 11.17 260 164 74% f7ff52686113 1.50, 6.92, 10.08, 13.17, 16.50 273 274 62% pan px / tilt px add up the absolute per-frame shift at 144×256 within shots, and moving is the share of frame pairs that shifted by at least 1 px. db09b1c54359 is the reference. 0299… , 632c… and 8256… are the three full recasts 632c… is the cat version the home page also shows . The others are alternate or partial cuts the site labels as such a 10-second take, a longer cut, a 4-second close-up . To line the recasts up against the reference shot by shot, shot compare.py splits each clip at its cuts and reports the mean shift per frame in each shot: bash /usr/bin/env python3 """Compare each clip's shot list and per-shot camera motion with a reference. usage: python3 shot compare.py reference.mp4 clip.mp4 clip.mp4 ... """ import sys import numpy as np from camera motion import analyse, summary FPS = 12 def shots path : rows = analyse path, FPS cuts, = summary rows end = round len rows + 1 / FPS, 2 edges = 0.0 + cuts + end out = for a, b in zip edges, edges 1: : seg = r for r in rows if a < r "t" < b and not r "cut" mag = np.mean np.hypot r "dx" , r "dy" for r in seg if seg else 0.0 out.append a, b, mag return out def fmt s : return " | ".join f"{a:5.2f}-{b:5.2f}s {m:4.2f}px/f" for a, b, m in s ref = shots sys.argv 1 print "reference ", fmt ref for clip in sys.argv 2: : s = shots clip name = clip.rsplit "/", 1 -1 :10 print f"{name:<10} ", fmt s n = min len ref , len s drift = s i 1 - ref i 1 for i in range n - 1 cut-time differences print f"{'':<11} shots {len s } vs {len ref }; cut drift vs reference: " + ", ".join f"{d:+.2f}s" for d in drift python3 shot compare.py samples/db09b1c5…mp4 samples/0299078c…mp4 samples/632c0129…mp4 samples/8256cd87…mp4 reference 0.00- 5.17s 0.94px/f | 5.17-11.17s 2.59px/f | 11.17-14.50s 2.58px/f 0299078c3f 0.00- 5.33s 0.22px/f | 5.33-11.67s 2.36px/f | 11.67-15.00s 1.42px/f | 15.00-17.58s 0.41px/f shots 4 vs 3; cut drift vs reference: +0.17s, +0.50s 632c0129d4 0.00- 5.33s 0.68px/f | 5.33-11.67s 2.91px/f | 11.67-15.00s 2.50px/f | 15.00-17.67s 0.74px/f shots 4 vs 3; cut drift vs reference: +0.17s, +0.50s 8256cd873b 0.00- 5.25s 0.13px/f | 5.25-11.42s 3.37px/f | 11.42-14.75s 1.87px/f | 14.75-17.42s 0.66px/f shots 4 vs 3; cut drift vs reference: +0.08s, +0.25s Caveats: phase correlation measures the single dominant shift, so a laughing head in a close-up counts as "motion" even if the camera is still. It doesn't detect zoom or rotation. Use it to compare clips against each other, not as an absolute camera tracker. A 12 fps analysis can't place a cut more precisely than about 0.08 s. If you plan to cut your own clip on the beat, add a slow-motion replay, or line up captions with the laugh, don't hard-code the reference's timestamps . Run camera motion.py on your file and use its cut list. The shots are in the same order, but a recast can be up to half a second late and about 3 seconds longer. The scene choices Just Me for the close-up, Back Row for the two standing singers in the opening shot, Whole Band for all five seats are compared on the scenes page https://tinydeskoutstanding.video/templates . Every scene uses the same reference, so these shot timings apply to all three.