Nodes/crypswolfy69 nodes/✦ ALCHEMIST // LORA CAPTION
ComfyUI Node

✦ ALCHEMIST // LORA CAPTION

The Caption That Trains a LoRA Is the Opposite of a Prompt

By babydjac·Created 26 days ago·Updated 9 days ago· 1
✦ ALCHEMIST // LORA CAPTION
  • source_image
  • prompt
  • reasoning
  • model_used
  • status
◄actionidle►
◄providerxAI / Grok►
◄modelgrok-4.20-non-reasoning►
◄reasoningprovider default►
◄lora_kindbody concept►
◄trigger_word►
◄nsfwtrue►
◄simple_prompt►
◄caption_words45►
◄omit_terms►
◄extra_direction►
◄api_key►
◄temperature0.80►
◄max_tokens1400►
◄remember_keytrue►
◄custom_base_url►
◄reroll0►

Here's the rule that decides whether your LoRA works: a model learns whatever is constant across the dataset and not written in the caption. Caption the face every time and the face stays promptable - you've trained nothing. Stay silent about it and it binds to your trigger word.

So a training caption is not a short version of a generation prompt. It's close to the inverse. Alchemist's LORA CAPTION node is built around that one idea, and it's the node in this pack I'd argue people need most, because hand-captioning a set is where most first LoRAs quietly fail.

How it works

You pick lora_kind and the node swaps in the matching omission rule, all of it in the system prompt:

  • character - describe pose, clothing, framing, setting. Never the face, eyes, skin or hair, and never a name: the subject is "a woman".
  • body concept - describe pose, clothing, setting, and stay off the proportions you're training. It does name the ethnicity and hair it sees, deliberately, so those stay swappable and the LoRA doesn't collapse onto one face.
  • style - literal content only. No medium, no technique, no palette talk, no lighting mood.
  • clothing or object - describe the wearer, pose and setting; name the garment minimally and otherwise leave it alone.

The form rules matter as much as the content rules, and the node enforces them: one to three sentences, 30–50 words, hard ceiling 70; never open with "The image shows" or "A photo of"; no booru tags, no weight syntax, no camera gear or f-stops; and vary the wording between captions, because identical phrasing across a dataset teaches nothing.

trigger_word goes first in every caption, comma after. Leave it empty and the node writes the caption body only and assumes your trainer will prepend the trigger - which is fine, but be consistent about which path you're on.

Plug a source_image in and it captions what's actually in the picture. With no image, put what the image contains in simple_prompt and it writes from your description instead.

Inputs, outputs, and what you wire them into

Daily drivers: provider, model, lora_kind, trigger_word, nsfw, plus source_image or simple_prompt. caption_words (20–70, default 45) changes the target length; omit_terms is the escape hatch - a comma-separated list of extra things to never mention, which is how you keep a tattoo, a background or a prop from baking into your concept. extra_direction nudges tone.

Outputs are prompt, reasoning, model_used and status. prompt is the caption: wire it into a text-file save, or feed each one to a save node whose filename matches the image you just captioned. status tells you which provider and model actually answered, which is worth a note node when you're captioning the same set with two providers to compare.

One caption per run is the honest limit - that's what this node is. For a whole folder, the pack's DATASET CAPTIONER does the same recipe across every image in a directory and writes the .txt sidecars itself.

Install

Manager, search crypswolf69. Manual:

cd ComfyUI/custom_nodes
git clone https://github.com/babydjac/crypswolf69.git
pip install -r crypswolf69/requirements.txt

Restart and hard-refresh. aiohttp is the whole dependency list; nothing to download, no local model. You need one LLM key - paste it into api_key and run the action widget to vault it, or set the provider's env var like XAI_API_KEY.

Where people get burned

It refuses, and it's not shy about it. OpenAI, Anthropic and Google block explicit adult content; the node raises an error rather than returning the refusal text as a caption. xAI/Grok is the default for a reason. If you're captioning a set you'd rather not send anywhere, point provider at Ollama - free per call, no filter, and no key needed.

DeepSeek can't see. It's a text-only provider, so source_image does nothing there.

Multi-subject images. Every vision captioner, this one included, mixes up who wears what when there are two people in frame. For a small set, caption it by hand or fix the LLM's output - the automated path is for volume, not for the set that has to be perfect.

Free repeats are cached, not free. While the inputs are unchanged, re-queuing costs nothing; bump reroll for a fresh call. That also means if a caption looks identical twice, you're probably looking at the cache.

Categoryprompting/alchemist

Inputs (18)

NameTypeDefaultDescription
actionCOMBOidleRuns in the browser and resets to idle. Never queues a generation.
providerCOMBOxAI / GrokWhich LLM answers. Use FETCH MODELS after switching.
modelCOMBOgrok-4.20-non-reasoningModel id. FETCH MODELS repopulates this from the live API.
reasoningCOMBOprovider defaultOnly bites on reasoning models, where it is the difference between 9s and 42s. Ignored elsewhere.
lora_kindCOMBObody conceptDecides what is omitted so it binds to the trigger.
trigger_wordSTRINGPlaced first in every caption.
nsfwBOOLEANtrueNSFW allows all explicit adult content. SFW keeps it non-explicit.
source_imageoptIMAGE—
simple_promptoptSTRING—
caption_wordsoptINT4520–70—
omit_termsoptSTRING—
extra_directionoptSTRING—
api_keyoptSTRINGLeave empty to use the vaulted key or the provider env var.
temperatureoptFLOAT0.800–2—
max_tokensoptINT140064–32000—
remember_keyoptBOOLEANtrue—
custom_base_urloptSTRINGOverride the provider endpoint. Required for the Custom provider.
rerolloptINT00–4294967295Output is cached while inputs are unchanged, so repeat queues are free. Bump this to force a fresh call.

Outputs (4)

NameTypeDescription
promptSTRING—
reasoningSTRING—
model_usedSTRING—
statusSTRING—