LC ✂️ Duplicate Tags 🏷️
Your prompt is wearing the same tag twice
- text
- removed
You pasted a WD tagger's output into your prompt, added your own tags on top, and now long_hair is in there twice. On a tag-driven model that isn't a cosmetic problem - a tag that appears twice carries more weight than the same tag once, which is how you end up with hair over the whole frame and a character whose head is suddenly 30% of the image.
LC ✂️ Duplicate Tags 🏷️ is the boring fix: it takes the prompt, removes every repeated tag, and hands back one clean string. Boring is the point - this is plumbing, not a model.
Why double tags actually change your image
Prompt weighting on the CLIP-era models is multiplicative and stackable. (keyword:1.3) is +30%, (keyword) is shorthand for 1.1, and ((keyword)) compounds to about 1.21. So when the same concept arrives twice, you get extra attention to it whether you asked for it or not - and weight stacking is a known artifact generator, which is why (((((important))))) wrecks images even where weights work properly. The practical range is 0.5–1.5, and the standing advice has been to stop stacking weights at all.
One caveat before you install anything: this is a tag-era problem. It bites on SD 1.5, SDXL, Illustrious, NoobAI, Pony - anything still fed through a CLIP encoder with tags and weights, which is exactly the family that generates tag lists in the first place. On the LLM-encoded 2026 models (Z-Image, Flux 2 Klein, Anima, Krea 2) prompt weighting is disabled - (word:1.3) gets passed through as literal punctuation - so duplicates there cost you tokens and clarity, not weight. The node still cleans those prompts fine; it just isn't fixing a weighting bug.
What it does under the hood
The node splits your prompt on commas and single line breaks that aren't inside brackets, so a weighted group like (red hoodie, jeans:1.2) stays in one piece. Then each piece is reduced to a comparison key: weight syntax is stripped, each (...) layer divides out as ×1.1 and each [...] layer as ×0.9, underscores become spaces, case is folded, whitespace collapses.
That's why long_hair, Long Hair, (long hair:1.2) and ((long hair)) are all one tag to it - which is the actual useful behaviour, not the exact-string matching you'd write yourself.
Things it deliberately leaves alone: sentences (a block with real punctuation and more than three words per comma piece), anything longer than six words, BREAK, and blank-line structure - paragraphs come back in the same order they went in, each internally joined with commas.
The one setting that matters:
- keep -
firstleaves the first copy exactly as written.strongestkeeps the most heavily weighted copy but places it where the first one was. Usestrongestwhen your own prompt is the one with the weight you want, and the captioner's bare tag came first.
Inputs are text (optional, so either leg can be bypassed upstream) and text_2 (optional second prompt - the captioner's output, say). Both are optional on purpose so you can blow away one input on the canvas without the node erroring. Outputs are text (the cleaned prompt - wire it into your CLIP Text Encode) and removed (a comma-joined list of what got cut, which is your audit trail; drop it into LC Show Text or LC Save Text if you want to see what the node decided).
Install
ComfyUI Manager → search ComfyUI_LC123_nodes → Install, or:
cd ComfyUI/custom_nodes
git clone https://github.com/lonecatone23/ComfyUI_LC123_nodes.git
Restart. There is no requirements.txt in this pack on purpose - it only uses what ComfyUI already ships (torch, numpy), so there's no pip step and no dependency roulette. If nothing shows up, the usual cause is a zip that unpacked one folder too deep: __init__.py must sit directly inside ComfyUI/custom_nodes/ComfyUI_LC123_nodes/. The console prints an [LC123] + line per module as it loads.
Where it goes wrong
- It doesn't merge synonyms.
long_hairandlong hairare one tag; "long hair" and "hair past shoulders" are two tags to a regex. It's normalization, not semantics. - Weights aren't added together. The surviving copy keeps its own weight. If you genuinely want
1.1 + 1.2, dedupe to one copy first, then type the weight you actually want. - An empty
removedoutput is good news. That means nothing was duplicated. - Line breaks inside a tag block become commas. Comma-separated tags are fine; one-tag-per-line comes back as a comma-joined list. Blank-line-separated blocks keep their structure.
Put it on the wire between the merge and the text encoder. Clean prompt in, one tag each, no silent double weighting.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| keep | COMBO | first | Which copy of a repeated tag stays. first: the first one, as written. strongest: the most heavily weighted one, placed where the first was. |
| textopt | STRING | The prompt to clean. Optional so either input can be bypassed upstream. | |
| text_2opt | STRING | Optional second prompt, added after text and cleaned together (e.g. your own prompt plus LC Vision Danbooru Caption's). |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| text | STRING | The prompt with every tag once. |
| removed | STRING | The tags that were taken out, for checking. |