Signs to Ideogram Caption
The JSON bridge you're not allowed to skip
- sign_data
- image
- caption_json
- box_prompts
- mask
- bboxes
- width
- height
- report
Ideogram 4 shipped its weights in June 2026 - 9.3B, non-commercial licence, quantized checkpoints only, and a safety filter so eager it emits a dedicated refusal image rather than your prompt. It was the best text-in-image model open weights had ever seen, and it talks in JSON, not prompts. That's the whole reason this node exists: it converts the pack's SIGN_DATA into the structured caption the Ideogram node actually consumes, so a chain that starts with "find the lettering and decide what it should say" can end in an Ideogram render. (It's also why the community's LoRA production moved to Krea 2 three weeks later - better renderer, worse licence terms, same old story.)
The one thing you must get right
Wire caption_json into Ideogram4PromptBuilderKJ's import_json input and set import_mode to always. That isn't a preference, it's the only route that works. The builder's elements_data widget is re-serialised from a live array in the browser every time the graph is queued, so anything written into it from outside gets overwritten the instant you press run - silently. If your caption seems to be ignored, this is why, and it's the first thing to check.
Inputs
sign_data- from Sign Text Proposer, or from the selector if you're supplying the words yourself.image- only its size is used, to convert boxes into Ideogram's 0–1000 grid. Feed the same image the selector scanned.high_level_description- one line about the whole picture. Blank is allowed, but blank is worse.background- the scene around the lettering: light, weather, materials. This is what keeps the replacement inside the same photograph as the rest of the frame.look(photographic/flat graphic) - Ideogram's untended pull is poster art: ask for a sign and it answers with flat vector lettering on a clean plate. The caption therefore states the camera, not the design. Only switch toflat graphicwhen the source genuinely is one.text_inset(0.07) - how far the text box sits inside the region. The mask stays full size; the box shrinks. A box that reaches the edge of a panel puts letters past that edge.mask_pad_px(4) - extra mask around each region. Err high: a mask that misses a stroke leaves the old scribble standing bit-identical, which is the single most visible failure in this pipeline.empty_too_small(on) - regions the selector judged too small for a readable word are described as blank rather than written on. Turn it off and you get letter-like shapes where words should be, and Ideogram counts those as invented text.min_word_px(0) - the last safety net before the caption. Box heights below this get no word; the surface is described as blank instead. Leave it on if you're splitting multi-line signs, because the selector measures regions before the split.fallback_text- used for a region the proposer left wordless. Blank means that region gets emptied.- Optional
auto_scene(on) withlm_base_url/lm_model_id- if the two text fields above are empty, the node writes the scene description itself with the local vision model, which is what lets the graph run from nothing but a chosen image.surface_hintoverrides the built-in wording per text class, oneclass: descriptionline each.
Outputs
caption_json into the builder's import_json; box_prompts for reading what was decided; mask for whatever inpaints or re-renders the regions; bboxes, width, height for debug and for grid math; report for the log.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/ping1979ping/comfyui-FVMtools
Restart ComfyUI, or install from ComfyUI Manager with comfyui-fvmtools. No extra Python packages for this node - it's numpy, torch and json. Ideogram 4 itself is a separate download, you'll want Kijai's prompt-builder node alongside it, and remember that Ideogram's weights are non-commercial.
Where people get burned
import_mode left on its default is the number one cause of "the caption did nothing". Check that first, before you touch a single parameter.
Leftover scribble around the new text means the mask is too tight - raise mask_pad_px. Text spilling off the edge of a sign means the opposite problem, the text box is too big - raise text_inset.
And resist the urge to describe the design here. This node's caption fields are about the photograph; the lettering itself comes from the proposer. Fill the design in by hand and you'll fight the model, because Ideogram's guidance - confirmed by the author's own testing - is that describing the scene as photographic is the counterweight to its pull toward flat poster lettering.
Inputs (14)
| Name | Type | Default | Description |
|---|---|---|---|
| sign_data | SIGN_DATA | From Sign Text Proposer (or the selector, if you supply the words yourself). | |
| image | IMAGE | The image the selector scanned. Only its size is used, to convert boxes into Ideogram's 0-1000 grid. | |
| high_level_description | STRING | One line about the whole picture. Blank is allowed. | |
| background | STRING | The scene around the lettering: light, weather, materials. This is what keeps the replacement in the same photograph as the rest of the frame. | |
| look | COMBO | photographic | Ideogram drifts towards poster art unless told otherwise. Use 'flat graphic' only when the source genuinely is one. |
| text_inset | FLOAT | 0.070–0.45 | How far the TEXT box sits inside the region. The mask stays full size. A box that reaches the edge of a surface puts letters past that edge. |
| mask_pad_px | INT | 40–64 | Extra mask around each region. Err high: what the mask misses is restored unchanged and stands there as leftover scribble. |
| empty_too_small | BOOLEAN | true | Regions the selector judged too small for a readable word are described as blank instead of being written on. Off: every region gets a word, which produces letter-like shapes that count as invented text. |
| min_word_px | INT | 00–200 | Box heights below this get no word — the surface is described as blank instead. 0 turns the test off. The selector has its own height floor, but it measures the region BEFORE the line splitter cuts it into bands, so a 90px sign split into four passes the selector and arrives here as four 20px strips that the splitter only checks against its own 8px 'is this a line at all' bar. This is the last station before the caption and the only one that sees the geometry that will actually be rendered. |
| fallback_text | STRING | Used for a region the proposer left without a word. Blank means that region is emptied. | |
| auto_sceneopt | BOOLEAN | true | Look at the picture and write the scene description itself when the two text fields above are empty. That is what lets the graph run from nothing but a chosen image. |
| lm_base_urlopt | STRING | http://localhost:1234/v1 | LM Studio endpoint for the scene description. |
| lm_model_idopt | STRING | qwen3-8b-vl-instruct-abliterated | Name it. 'Whatever is loaded' is not a default, it is a silent failure: LM Studio lists models it has not loaded, the call returns nothing, and the run continues with an empty description. Must be a VISION model. |
| surface_hintopt | STRING | Per class, one per line, 'class: description'. Overrides the built-in wording for that class. |
Outputs (7)
| Name | Type | Description |
|---|---|---|
| caption_json | STRING | — |
| box_prompts | STRING | — |
| mask | MASK | — |
| bboxes | BOUNDING_BOX | — |
| width | INT | — |
| height | INT | — |
| report | STRING | — |