Flux 3 Keyframes (BFL)
The glue node that makes Flux 3 I2V keyframes actually usable
- keyframes
The name undersells this node. Flux 3 Keyframes doesn't generate anything - it's the small, boring node that makes the interesting one work. If you've ever stared at the keyframes socket on Flux 3 Video I2V (BFL) and wondered what on earth it wants, this is the answer: it wants a JSON string that Black Forest Labs' API uses to know which frame goes where in your clip. You could type that by hand. You don't want to.
This is a cloud-wrapper pack, remember. Nothing runs on your GPU - your frames go up to api.bfl.ai, BFL does the video math on their hardware, and you pay per request. That's the whole deal with gelasdev's ComfyUI-FLUX-BFL-API. The Keyframes node is the piece that packages your images into the exact payload that API expects, without you ever learning its JSON schema.
What it does
The node gives you up to ten image sockets: start_image, eight middles (image_2 through image_9), and end_image. Each takes a base64 string (from the Image to Base64 (BFL) node) or a plain image URL. Empty sockets are simply skipped - so a three-frame clip with just a start, a middle, and an end works fine, and so does a full ten-image sequence.
The one input that matters is timing, which is a two-way switch:
even- the connected images are sent as a plain list. The first starts the clip, the last ends it, and everything in between falls evenly across the duration. Simple, and usually what you want on a first pass.custom- every image gets an explicit timestamp as a[seconds, image]pair. The API won't let you mix timed and untimed frames, so the node pins everything:start_imageat 0, each middle at its owntime_Nwidget, andend_imageatend_time.
In custom mode, end_time doubles as the clip length when the video node's duration is set to auto - the video just runs to the last frame's second, rounded up. That's a neat trick worth knowing: set end_time and you don't have to touch the duration dropdown at all.
Output and wiring
One output: keyframes (STRING). Run it straight into the keyframes socket on Flux3VideoI2V_BFL, and you're done. That's the whole workflow - Keyframes node in, I2V node out.
Getting it installed
Same story as the rest of the pack. Easiest via ComfyUI Manager - search for "ComfyUI-FLUX-BFL-API" and install. Manual route:
cd ComfyUI/custom_nodes
git clone https://github.com/gelasdev/ComfyUI-FLUX-BFL-API.git
cd ComfyUI-FLUX-BFL-API
pip install -r requirements.txt
The dependency list is basically just torch, which you already have. The video nodes need ComfyUI ≥ 0.3.30 for the native VIDEO type, so update if you're on something older. Then put your BFL API key in the pack's config.ini - or better, keep it in a Flux Config (BFL) node wired into the video node, since config.ini has a habit of getting overwritten when the pack updates. That's a real community pain point with this pack.
Where people get burned
The node is pretty forgiving, but three things trip people up. First, if you use three or more plain images with even timing and the video node's duration is left on auto, BFL will reject the request - the API insists on a set duration for multi-frame keyframes. Second, in custom mode the node warns (doesn't fail) if you give two images the same timestamp, or if end_time isn't the largest time - in which case your "end" image won't actually end the video. Third, remember these are base64 strings flowing through wires; a single 4K frame can make the payload enormous, so sizing down before encoding is genuinely worth it.
Inputs (20)
| Name | Type | Default | Description |
|---|---|---|---|
| timing | COMBO | even | even: images fall evenly across the clip; custom: start at 0, middles at their time widgets, end at end_time |
| start_imageopt | STRING | Base64 image (from Image to Base64) or image URL | |
| image_2opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_2opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_3opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_3opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_4opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_4opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_5opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_5opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_6opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_6opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_7opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_7opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_8opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_8opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| image_9opt | STRING | Base64 image (from Image to Base64) or image URL | |
| time_9opt | FLOAT | 0.000–20 | Second to pin the matching image at — only used when timing is "custom" |
| end_imageopt | STRING | Base64 image (from Image to Base64) or image URL | |
| end_timeopt | FLOAT | 5.000–20 | Second the end image lands at (timing "custom") — with duration "auto" this is the clip length |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| keyframes | STRING | — |