Sample Keyframes
16 stills out of a five-second clip, spaced on purpose
- frames
- keyframes
- timings
The clip is not the deliverable. The stills are. An auto-rigger reads poses off pictures, so what leaves this pipeline is 16 evenly spaced frames - each one a three-panel row - with the time of every pose recorded next to it.
This node is the step that picks which frames those are, and it's a small node doing one job carefully: even spacing in time, and index 0 always kept.
How it works
It takes the whole clip as an IMAGE batch and picks round(i * (last) / (n-1)) for i in 0..n-1. Even spacing, endpoints included.
Index 0 is always kept and that's the load-bearing bit. Frame 0 is the rest pose the rigging tool already matches - it anchors the whole sequence, and it's also the anchor that Identity QA measures drift against, for a reason: it came out of the same encoder as every other frame, so whatever it shares with frame 9 is codec, not drift.
loop changes the arithmetic. For a looping motion the final frame would duplicate the first, so the divisor becomes n instead of n-1 and the last frame is dropped rather than sampled.
Then it emits timings: a JSON list of {index, frame, t} with t = frame / fps rounded to 4 decimals. This is not decoration. Save Keyframe Manifest refuses to run without it, because timing is the one thing a rigging tool cannot recover from the images.
Inputs and outputs
frames - an IMAGE batch, which in practice is Get Video Components' images output. Not a video path; the pack works on decoded frames. (A 5-second clip at 24fps is 120 frames of 1536×672 in memory, so this is the point where a big canvas starts costing you RAM, not VRAM.)
count - 16 by default, 2 to 64. Sixteen over five seconds is a pose roughly every 0.31s, which is dense enough for a rigger and short enough to read as a sheet. More keys means a longer sheet and a slower manifest; fewer means the rig interpolates through the motion for you, badly.
fps - 24 by default. loop - as above.
fps_in (optional) - wire Get Video Components' frame-rate output here and it overrides the widget, so the timings match the clip that actually came back rather than the one you assumed. Do this. Providers return 24 when you asked for 24 and 30 when you didn't, and a wrong fps makes every timestamp proportionally wrong while still looking perfectly plausible.
Outputs: keyframes, the chosen stills as a batch, into Triptych Split. And timings, which goes to both Keyframe Sheet (for the row labels) and Save Keyframe Manifest (for the real thing). If you wire only one, wire the manifest.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/meryboth/from-mesh-to-motion
# restart ComfyUI
Or ComfyUI Manager → from-mesh-to-motion, or comfy node install from-mesh-to-motion. Pillow and numpy; no model files, no GPU. If you're running the CLI instead of the graph, the same logic is in pipeline/video.py, and that's where ffmpeg or imageio-ffmpeg comes in - inside ComfyUI you never touch it, because Get Video Components already decoded the clip.
Where people get burned
A silent fps mismatch. The pack's own history is the cautionary tale: an earlier manifest builder guessed t = index / fps when nothing was wired, which put k01 at 0.04s instead of 0.33s - a factor of eight, delivered without complaint. Wire fps_in, then read the timings once.
The clip length ceiling. One clip per run, 4–15s as MiniMax H3 accepts, and the documented runs used 5. That's the general state of video models, not a setting you got wrong; native windows are around 5 seconds and going past them without engineering produces warps and loops. Multi-beat sequences in this pipeline are built as several runs that share their end poses.
loop on a clip that doesn't loop quietly drops the last keyframe, which is the right call for a cycle and a small lie otherwise. Match the flag to the prompt - the Motion Prompt node's loop widget adds the "ends in the pose it started in" clause, and the two should agree.
count at or above the frame count just returns every frame, no error. If your sheet suddenly got long, check count.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| frames | IMAGE | — | |
| count | INT | 162–64 | — |
| fps | FLOAT | 24.001–120 | — |
| loop | BOOLEAN | false | — |
| fps_inopt | FLOAT | Wire Get Video Components' frame-rate output here; it overrides the widget so the timings match the clip that actually came back. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| keyframes | IMAGE | — |
| timings | STRING | — |