SuperAPI AI Directory

ken-burns-clip

Design & creation

View repository

Updated 10-16 05:49

About this skill

ken-burns-clip

Purpose

Take one still image, output an exact-duration MP4 with a subtle ken-burns push-in or pull-out. The motion primitive for the ffmpeg-motion strategy (see video-orchestrator/orchestrator.md State 3.4 motion strategy classifier).

This atom exists because:

  • Editorial illustration / 2D / halftone aesthetics break under generative i2v (Seedance hallucinates photoreal hands, Veo3 adds naturalistic motion, etc. — see LEARNINGS L1).
  • The right motion for those aesthetics is "printed page that moves" — subtle camera moves on still frames.
  • This recipe lived as a function inside edit-videos/scripts/build_dynamic.py and kept getting re-implemented. Promoted to a first-class atom.

When to use

  • Editorial illustration ads (New Yorker / Niemann / Steinberg lineage)
  • Hand-drawn / 2D / paper-and-ink styles
  • Halftone / risograph / printed-magazine aesthetics
  • Any AI-generated still image that needs subtle motion without generative video re-imagining
  • Orchestrator State 5 (Create clips) when motion_strategy: ffmpeg_motion is set

Don't use for: photoreal product demos (use Seedance/Veo3), real-person performance footage (use Hedra/Kling), or anything that needs in-frame subject motion (use generative i2v).

Inputs

Flag Required Default Notes
--src yes — Input PNG/JPG path. Should be ≥ target resolution.
--dst yes — Output MP4 path.
--duration yes — Seconds, float. Exact output length.
--zoom-end no 1.06 Final zoom factor. 1.03–1.08 for subtle, 1.10–1.15 for stronger.
--zoom-out no false If set, motion reverses: starts at zoom-end, ends at 1.0.
--fps no 24 Frame rate. 24 fps reinforces hand-drawn feel; 30 for crisp graphics.
--width no 1080 Output width.
--height no 1920 Output height.
--fade-in no 0 Fade-in duration in seconds.
--fade-out no 0 Fade-out duration in seconds.

Workflow

  1. Verify ffmpeg and ffprobe are on PATH.
  2. Call:
    python3 skills/atoms/editing/ken-burns-clip/scripts/render.py \
      --src path/to/still.png \
      --dst clips/beat-1.mp4 \
      --duration 4.0 \
      --zoom-end 1.06
    
  3. Verify output duration matches request: ffprobe -v error -show_entries format=duration -of csv=p=0 clips/beat-1.mp4

Output

A single MP4 at dst. H.264, yuv420p, faststart, exact duration ±1 frame.

Quality Checks

  • Output duration is within 0.05s of --duration (zoompan frame math can leave a 1-frame remainder; we use -t + trim=duration= to clamp).
  • Output codec is H.264 / yuv420p (web-safe).
  • First and last frame both render cleanly (no zoompan boundary glitch).

Failure Modes

  • -loop 1 used by mistake — zoompan's d=N runs once per input frame. With -loop 1, every loop replays the full zoom cycle, multiplying output length (we got 225s instead of 3s in the Rat Park session). The script does not use -loop 1. If you call the underlying ffmpeg directly, do NOT add it.
  • Source resolution too small — we scale source to 2× target before zooming so the pushed-in frame stays sharp. If source is < target, pixelation creeps in. Use ≥ 1080-wide sources.
  • Banding on flat gradients — H.264 at high CRF can band on green/cream gradient backgrounds. Bump to --crf 16 if visible (default 18).

Notes

  • Reverse motion (--zoom-out) is useful for "reflecting back" beats — e.g. holding on a previous scene's final frame as the next narrative unit begins. Mimics a slow camera pull-back.
  • For ping-pong / flip-book animation (3 keyframes alternating at ~6 fps), use add-zoom-cuts or create-jump-cuts instead — this atom is for single-image motion only.
  • Combine with hard-cut concat (no crossfade) for editorial-panel aesthetic. Crossfades between AI-generated stills expose geometry drift (LEARNINGS L2).

Reference implementation

Originally extracted from skills/atoms/editing/edit-videos/scripts/build_dynamic.py:65-90 (render_still function). The hardcoded -loop 1-incompatibility comment in that file is the canonical source of truth.