⬡ Polyhedron Control Fit
Making a stock clip obey your model's frame grid
- images
- mask
- images
- mask
- frame_count
- width
- height
- seconds
- info
Your control clip came off a phone: 30 fps, 1920×1080, 14.3 seconds, 429 frames. Your model wants 121 frames at 832×480 and will quietly throw away anything that doesn't divide right. ⬡ Polyhedron Control Fit is the boring node that stands between those two facts - and if you skip it, you'll spend an evening blaming the ControlNet.
The one idea worth understanding first
Video VAEs compress time in chunks, so the sampler only accepts certain frame counts. Wan's VAE turns N frames into ((N-1)//4)+1 latents and back; H3's grid is 17k+5 - 5, 22, 39, 124, 362. Feed a model 111 frames and you'll get 109 out, then wonder where two frames went. This arithmetic is the reason half the "auto-extend" nodes in the ecosystem exist, and it's the reason grid is a dropdown here instead of something the node guesses.
grid gives you 17n+5 (video latent grid), 8n+1, 4n+1, or any. Set it to match your model family, ask for the frames you want, and the node snaps down to the nearest valid count. 0 means "as many as the clip gives from start".
Everything else it does in one pass
images in, and the node re-times to target_fps (nearest frame - it doesn't interpolate, so a 60 fps clip downsampled to 24 loses frames, it doesn't get smoother), offsets by start seconds, takes a window of frames, and resizes to width × height rounded to multiple (32 by default, which suits video and image models).
size_mode is where taste enters: fit (pad) keeps the whole clip and gives you black bars, fill (crop) fills the frame and crops the overhang, stretch distorts. For control passes, fill is usually right - bars become part of the control and the model learns to paint nothing there. 0 in width or height means "derive it from the other side, keeping aspect".
When the source is shorter than your window, at_the_end decides: hold last frame, loop, or cut the count down to what's there. Holding a last frame for 40 frames of a 121-frame clip gives you a control that says "stand still" - which is sometimes exactly what you want and sometimes a frozen-looking render.
block and block_index are for pre-baked multi-view renders. A ring render is directions × frames, so block = frames per direction picks one direction; block_index picks which (0 = S, 1 = SE, 2 = E, in the ring's own order). With block = 1 it picks single images out of a batch.
Outputs: images, mask (fitted frame for frame - inpainting white = regenerate), and then the numbers that make wiring easy: frame_count (feed your Empty Latent's length), width, height, seconds (length at the target rate), plus info.
Wiring it
Media Loader (stock clip) → Control Fit → Control Map (pose / depth / canny)
└───→ Apply Control → Sampler
Or skip the mapping entirely and use a stock clip as the control itself - depth-of-a-real-scene is a legitimate control video. Wire fps from the loader into the optional fps input so source_fps = 0 has something honest to read; otherwise it falls back to the target rate.
Install
Manager, search "Polyhedron Suite". Manual:
cd ComfyUI/custom_nodes
git clone https://github.com/PolyhedronAI/ComfyUI-PolyhedronLoRAStack.git
# restart ComfyUI
Nothing to pip install - this pack declares no required dependencies. Control Fit itself needs no model file at all; the models live one node downstream in Control Map.
Common issues
The clip is a checkerboard. If the Media Loader found no file it emits a placeholder frame, and Control Fit refuses it by name instead of cheerfully fitting a checkerboard into your render. Check the loader before you debug anything else.
Frames disappear. frames snaps down, and at_the_end = cut shortens further. If you asked for 121 and the clip holds 100, you don't get 121 of anything - hold or loop if you need the length.
Everything looks soft. Stretch into a wildly different aspect, or fill-cropping into a landscape window from a portrait clip. The control looks fine in the node preview and terrible in the render; check info for what it actually took.
multiple fights you. Set it to 32 and type 900 pixels wide, and you get 896. That's the node doing its job - most video models will reject or pad non-multiples anyway.
Inputs (15)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | A clip (frames) or one image -- e.g. a stock video from the Media Loader. | |
| target_fps | FLOAT | 24.01–120 | Frame rate of the model's video. The clip is re-timed to it (nearest frame). |
| source_fps | FLOAT | 0.000–240 | Frame rate of the clip. 0 = take it from the fps input, else the target rate. |
| start | FLOAT | 0.000–36000 | Seconds into the clip where the control starts. |
| frames | INT | 00–100000 | Frames wanted (snapped down to the grid). 0 = as many as the clip gives from start. |
| grid | COMBO | 17n+5 (video latent grid) | Valid frame counts of the target model: 17n+5 (5, 22, 39 ... 124 ... 362), 8n+1, 4n+1 or any. |
| at_the_end | COMBO | hold last frame | When the clip is shorter than the frames wanted: hold its last frame, loop it, or cut the count down. |
| width | INT | 00–8192 | Target width. 0 = from the clip (or from height, keeping the aspect). |
| height | INT | 00–8192 | Target height. 0 = from the clip (or from width, keeping the aspect). |
| size_mode | COMBO | fill (crop) | fit: whole clip inside, black bars. fill: fills the frame, crops the overhang. stretch: distorts to the size. |
| multiple | INT | 321–128 | Sides are rounded to a multiple of this (32 suits the video and image models). |
| block | INT | 00–100000 | Take one block of this many frames first (0 = all). A ring render is directions x frames: block = the frames per direction picks one direction; 1 picks single images. |
| block_index | INT | 00–100000 | Which block (0 = the first; for a ring: 0 = S, 1 = SE, 2 = E ... in the ring's order). |
| fpsopt | FLOAT | The clip's frame rate, e.g. from the Media Loader. | |
| maskopt | MASK | Goes the same way, frame for frame (inpainting: white = regenerate). |
Outputs (7)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | The fitted frames. |
| mask | MASK | The fitted mask (empty when none came in). |
| frame_count | INT | Frames -- e.g. for the latent length. |
| width | INT | Width. |
| height | INT | Height. |
| seconds | FLOAT | Length at the target rate. |
| info | STRING | What was taken and how. |