FLUX.3 Box Reconcile KJ
Your Prompt Upsampler Renamed Your Boxes. This Puts Them Back.
- prompt
If you've ever handed a carefully built layout caption to an LLM upsampler and got back something almost right, this node is the fix. Almost right is the problem: the upsampler embellishes your descriptions beautifully and then quietly re-plans your boxes, renames them, and rewrites your <obj_1> citations to <balloon_1>, at which point everything downstream that referenced your ids points at nothing.
What it does
FLUX.3 Box Reconcile KJ is a two-string repair step that sits between your prompt upsampler and the rest of the graph. Feed it the upsampler's answer plus the FLUX.3 Prompt Builder's original prompt output, and you get one caption back where your rows keep their exact coordinates, the ids are yours again, the descriptions are the upsampler's better ones, and any genuinely new rows it added follow along.
It only makes sense in FLUX.3's box mode - the upsampler keeps the boxes only when you drive it with the t2i_bbox system prompt (or i2i_bbox if you're feeding reference images). That's exactly what the builder's upsampler_input output is built to be, so the intended chain is: builder → upsampler → this node. Skip box mode and the upsampler returns prose with no table - nothing to reconcile.
FLUX 3 itself is BFL's gated multimodal flagship from July 2026, open Dev backbone promised later in the year - so this is a node for the moment you have weights and a caption pipeline. It's flagged experimental and only landed in KJNodes on 2026-10-15.
How the repair works
The node splits the upsampler's answer into a caption and a trailing JSON table (it scans from the last [ backwards for the first position where the rest parses as a list), then treats every row in your builder output as locked. Locked rows keep their coordinates verbatim; their description is replaced by the upsampler's version when the ids line up, since the prose is the part worth having.
The renaming is a geometry match. For each locked row whose id is missing from the upsampler's table, it looks for a row whose box overlaps the locked box with an IoU of 0.9 or better - same region, different name - renames that row back to your id, and rewrites the <old_id> citation to <new_id> throughout the caption. Your <obj_1> citations survive even though the model wrote <balloon_1>.
New rows are on probation: the id must be a plain identifier (letters first, no dashes or spaces), the description non-empty, the row actually cited as <id> in the caption text, and the box a well-formed 0–1000 rectangle. Fail any of those and it's silently dropped.
The inputs and the output
Two inputs, both socket-only - there's nothing to type. caption takes the upsampler's answer, which the tooltip names as a TextGenerate text output, i.e. whatever text node ran your upsampler prompt. prompt takes the builder's prompt output; its box table is what gets locked. Order matters only in that the builder output must be the original, not something already reconciled.
One output, prompt, a string. That's your final caption - wire it into the text encode and you're done. If the builder-side string has no parseable box table, the node hands the caption back untouched rather than mangling it, which makes it safe to leave in a graph while you toggle box mode.
Installing it
Same pack, no extra dependencies:
cd ComfyUI/custom_nodes
git clone https://github.com/kijai/ComfyUI-KJNodes
pip install -r ComfyUI-KJNodes/requirements.txt
then restart. In Manager, search KJNodes. No model downloads - this node only touches the pack's Pillow path.
Where people get burned
A nudge is not a match. IoU 0.9 is a tight threshold. If the upsampler shifts a box meaningfully, no rename happens - you keep your locked row and get the upsampler's row appended, i.e. two rows for one object. Keep your layout roughly intact between passes and let the upsampler improve wording.
Rows with 0–1 coordinates vanish. The validator rejects any box whose largest value is ≤ 1, on the assumption that a 0–1 box isn't on the 0–1000 grid. Hand-edited tables and upsamplers that normalize the other way lose rows with no error message.
An uncited row is a dropped row. Anything the upsampler invented but didn't mention as <id> in the prose disappears. That's deliberate - an uncited box is a hallucinated region - but it will look like the node ate your work.
Prose after the table. The split needs the JSON list to be the tail of the string. If the upsampler adds "Let me know if you'd like changes!" after the closing bracket, the table is unparseable and you get your locked rows back with no improvements. Trim or retry.
Your descriptions can be overwritten. A matching upsampler desc wins over yours by design - copy the builder's text if you care about your wording.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| caption | STRING | The prompt upsampler's answer (TextGenerate text output). | |
| prompt | STRING | The FLUX.3 Prompt Builder's prompt output: its box table is the locked rows. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| prompt | STRING | — |