Captioner
One training caption per frame, wired straight into your dataset folder
- image
- lm
- config
- image
- caption
Auto-captioning a LoRA dataset used to mean running a standalone captioner (JoyCaption, Florence-2, the WD14 tagger), writing .txt sidecars next to your images, and manhandling the pairing yourself. Captioner folds that into the graph: image in, captions out, in the same order, as lists - and the pack's own Save Image With Caption writes image and caption side by side under the same name.
That's the whole pitch, and it's a good one. If you're building a dataset, set_00001_.png sitting next to set_00001_.txt is the shape you wanted anyway.
What it does per frame
Each frame goes to the model on its own - one generate per frame, never the batch in one prompt, which is the opposite of how Qwen LM behaves. No system turn, no prefill, thinking off, capped at 600 new tokens. The frame's long side is capped at 1024 (never upscaled) before the family's own image sizing runs on it, because that's the cap the image-to-caption training and serving code this node was built for uses. If you trained a captioning LoRA on 1024-long-side crops, you're feeding it the same thing you trained on.
The model comes from an LM Loader on the lm input, and it's required - there's no default, so a bare Captioner refuses to queue with Required input is missing: lm.
Inputs worth setting
style - the built-in prompt, and the most opinionated thing here.
tags- one lowercase line of attribute groups: class word, shot type, pose, expression, clothing, accessory, setting, lighting, background. Any field the image doesn't actually show gets left out along with its comma. No genre or style label.prose- two or three sentences, 30 to 50 words.none- no prompt at all; the model gets the image alone.
Both built-in styles open with the class word, woman or man, read off the image, and deliberately never describe hair or identity. That's character-LoRA discipline: the caption describes what's variable, the LoRA owns the face and hair. If you've internalised the "describe the variable, leave the fixed" rule from the training side, this is that rule baked into the prompt.
instruction (optional socket) - your own prompt, replacing the built-in one for any style. The model gets the image and then your text. Whitespace-only counts as not connected.
config (optional) - an LM Config, which replaces the Captioner's sampling wholesale. Careful: an LM Config with nothing edited gives you the model's own defaults, which include Qwen's presence penalty of 1.5. A caption repeats tokens by design and a penalty of 1.5 pushes it off its format. Without a config the node uses its own sampling - temperature 0.7, top_p 0.8, top_k 20, no presence penalty - which is the right default for captions. Only wire a config here if you plan to edit something.
seed - one seed for every frame, and its control starts at fixed. Queue again and you get ComfyUI's cached captions rather than a fresh pass. That's usually what you want (a dataset you can reproduce), but set it to randomize if you're sampling a few variants of a caption and picking.
Outputs
image is every frame as it came in - a view of the input batch, not the resized copy the model saw, and not a copy, so no memory blow-up. caption is the matching strings, same order.
Both are lists, and that's the part to internalise: a downstream node runs once per element. Connect image + caption to Save Image With Caption and you get one PNG plus one TXT per frame with matching names. Connect them to something that doesn't expect a list and you'll get N runs of whatever it is. The input side is flexible by comparison - a single image, a batch, or a list of batches (from Join Image Lists, a folder loader, …) - in which case ComfyUI calls the node once per batch and joins the results, so batches of 3 and 2 frames give you 5 images and 5 captions, in order.
Rolling your own captioner LoRA
If you've trained a model to caption from an image alone, put the adapter in models/Qwen-LM/loras/, pick it in the LM Loader, set lora_strength to 1.0, and set style to none. The frame then arrives as the user turn with no text, no system turn, no prefill - and whatever the LoRA writes is your caption. If it was trained with a brief after the image, connect that brief as instruction and you get framework, frame, then brief. The none style is never chosen for you; leave the style on tags and your LoRA still gets the tags prompt after the image.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/0xBeycan/ComfyUI-BCNodes
pip install -r ComfyUI-BCNodes/requirements.txt
Restart and search caption or qwen. None of the requirements belong to this node - opencv, postfx and caption-audit back the mask, PostFx and Caption Audit nodes.
First run downloads the model: Qwen3.5-9B_int8_convrot.safetensors lands in models/Qwen-LM/ from beycanai/Qwen-LM unless it's already in models/Qwen-LM or under models/text_encoders. Progress prints to the console every 10%, not on the node.
Where it goes wrong
An empty batch is an error, raised before the model is prepared, and it tells you what to connect. Fine - but if your image source is a filter that returned nothing, that's where it'll bite.
Throughput is host-dependent, and badly. Qwen3.5's prefill runs as thousands of small kernels, so per-launch CPU cost dominates. Measured tag captions with Qwen3.5-9B INT8: about 1.15 s on a 4090 host, 0.71 s on a 5090 host, and 20.4 s on a 3090 cloud host. Same GPU class, wildly different results. If captioning a 200-image set is taking an hour, the host is the problem.
The abliterated builds matter here. The instruct Qwen3-VL models carry assistant-style refusals, so plain NSFW captioning on them is unreliable - that's the reason Qwen3.5-9B Abliterated and Qwen3-VL-8B Instruct Abliterated exist in the model list. For an explicit dataset, pick one.
Don't expect it to fix multi-subject attribution. Every VLM in this category mixes up who wears what when two people are in frame. Captioner's single-frame prompt style dodges some of the bleed, but it can't dodge the model's. Auto-caption a large set, audit a small one by hand.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | The images to caption, one caption per frame: a single image, a batch, or a list of batches (each frame of each, in order). The model sees each frame with its long side capped at 1024 (never enlarged), then sized as its family sizes images. | |
| lm | BC_LM | The chat model, from an LM Loader: its model, precision and LoRA (at its strength), and whether it stays loaded after this node (off: unloaded once after the last frame, after an error too). | |
| style | COMBO | prose | The built-in prompt. tags: one line of attribute groups (class word, shot type, pose, expression, clothing, accessory, setting, lighting, background), each field only when the image shows it. prose: two or three sentences, 30 to 50 words. Both open with the class word, woman or man, read off the image, and never describe hair or identity. none: no prompt, the model gets the image alone (a LoRA trained to caption without one). A connected instruction replaces it. Any style works with a LoRA. |
| seed | INT | 00–18446744073709550000 | The sampling seed, the same for every frame. Its control starts at fixed, so queueing again gives the cached captions instead of a new run; set it to randomize for new captions each time. |
| instructionopt | STRING | Your own prompt in place of the style's built-in one, with any style; the model gets the image, then this text. Not connected, or only whitespace: the style decides. The answer is cut at 600 tokens. | |
| configopt | BC_LM_CONFIG | Sampling from an LM Config node in place of the Captioner's own (temperature 0.7, top_p 0.8, top_k 20, no presence penalty): the model's thinking-off defaults with the fields edited there, or fed by a link, over them. Not connected: the Captioner's own. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | Every frame of the input as it came in (not the resized copy the model saw), one list item per frame. |
| caption | STRING | Each frame's caption, in the same order: connect both outputs to Save Image With Caption to save every frame with its caption. |