# Video Still Animator

> Turn a single still image (PNG/JPG) into a short MP4 with a slow Ken-Burns zoom and a silent audio track. Pure ffmpeg wrapper. Designed as the on_failure substitute for AI video-gen steps that get blocked by content moderation: when seedance refuses, this skill emits a valid replacement clip from the already-generated still so a downstream merge can still produce a complete deliverable.

- Skill: `gabrielmoreira/video-still-animator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/video-still-animator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/video-still-animator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/video-still-animator

---


# video-still-animator — Ken-Burns fallback clip from a still

Produces a short MP4 from a single PNG/JPG using ffmpeg's `zoompan`
filter, so a downstream merge step still has a clip for every shot when
an upstream AI video generation step gets blocked.

## Why this skill exists

`seedance-2-prompt` (and any platform-moderated video model) can refuse
individual requests for reasons that have nothing to do with the
prompt's narrative quality:

- input photo contains a recognisable real person face
- output audio gets flagged by the safety classifier
- the moderation model's similarity threshold drifts day to day

In `meta-short-drama` we always have a fresh PNG on disk before the
seedance call runs, so the cheapest "the show must go on" fallback is to
turn that PNG into a Ken-Burns clip and feed it to the merger. The clip
won't have the camera motion the prompt asked for, but the character,
framing, and timing will still match the script.

## Inputs (`with:`)

| key | required | default | notes |
|---|---|---|---|
| `input_image` | yes | — | PNG or JPG path. |
| `output_path` | yes | — | Output `.mp4` path. Parent dir created if missing. |
| `duration` | no | `5` | Clip length in seconds. |
| `width` | no | `720` | Output width. Match the merge step's pipeline. |
| `height` | no | `1280` | Output height. 720x1280 = 9:16. |
| `fps` | no | `24` | Output frame rate. |
| `zoom_rate` | no | `0.0015` | Per-frame zoom delta; 0.0015 over 5s ≈ 1.18× final zoom. |

## Output specs

- H.264 video at the requested resolution + fps
- AAC silent audio track (so merge steps that demux/remux audio do not
  trip on a missing stream)
- `+faststart` MP4 (web playback friendly)

## Dependencies

- ffmpeg ≥ 5.0 on PATH (or pass `--ffmpeg-path` directly).

On Windows the script also probes the winget Gyan.FFmpeg install path,
Scoop, Chocolatey, and `C:\Program Files\ffmpeg\bin` before giving up.

## Use as `on_failure` substitute

In a meta-skill DAG, hang it off the real video step:

```yaml
- id: shot1_video
  kind: skill_exec
  skill: seedance-2-prompt
  on_failure: shot1_video_fallback
  with: { ... }

- id: shot1_video_fallback
  kind: skill_exec
  skill: video-still-animator
  with:
    input_image: "{{ inputs.workspace_dir }}/.../1_shot.png"
    output_path: "{{ inputs.workspace_dir }}/.../1_shot.mp4"
```

The fallback must be a standalone step (no `depends_on`, no own
`on_failure`) per the meta-skill engine rules. The PNG must already
exist on disk by the time the parent step fails — that's true here
because the image step always runs before the video step.

## Limits

- Static framing only. The zoom is uniform centre-anchored; no pan, no
  rotation, no parallax. For richer fallback motion, generate a fresh
  set of stills and call this multiple times.
- No real audio. The intent is a placeholder that survives the merge;
  add a real soundtrack downstream if needed.
- ffmpeg version drift: very old ffmpeg (<4) may not accept the exact
  `zoompan` filter syntax; install ≥ 5.

