Filter Plurals
`arm up` or `arms up`, pick one before the model picks both
- processed_text
- filtered_tags
Danbooru has both arm up and arms up. Both are real tags, both are trained, and they mean nearly the same thing - which is exactly why having both in one prompt is a problem. The model doesn't shrug and take one; it does its best to satisfy two pose tags at once, and you get the arm-that-isn't-quite-up-above-the-head look. Same story for hand on hip next to hands on hips.
Filter Plurals keeps the first spelling and drops the later one. If you've got any prompt assembly in your graph - an LLM writing the tags, a {...|...} group, a tagger's output merged with your own - you'll hit this more often than you'd think, because nothing upstream knows the two spellings are the same idea.
The mechanism is deliberately dumb
For each tag, it strips a trailing s off any word of four letters or more, anywhere in the tag, and compares the resulting skeleton against ones it has already seen. First skeleton wins, and the tag that produced it stays.
That crudeness is the point, and the failure modes it avoids are the interesting part. Words of three letters or fewer are left alone, so ass never collapses into as. And s isn't a plural marker anywhere except at the end of a word, so glasses becomes glasse while glass becomes glas - they don't collide. Ditto dress and dresses. The node is comparing two tags from the same prompt, and for that job, "does this look plural" beats a dictionary.
It's also worth knowing what's out of scope: the same tag twice is not this node's business. arms up, arms up passes straight through. Outright duplicates are FilterSubtags' territory.
Inputs, outputs
text is an input socket, so you can't type into it directly - wire whatever produced the prompt into it, the pack's TextPrompt or any string source will do.
processed_text is the prompt with the redundant spellings removed; that's the one you send to your positive CLIP Text Encode. filtered_tags is a comma-joined string of what got dropped. It's not decoration - plural filtering is a heuristic, and when a tag you wanted disappears, this output on a preview or note node tells you why in about two seconds.
Weights and BREAK don't shield a tag. Both the weight notation and the emphasis brackets are stripped before comparison, so (arm up:1.2) still gets matched against arms up, and it keeps its weight when it survives. Groups separated by BREAK are treated as one picture, so the second spelling goes wherever you put it. Whitespace and line breaks in what's kept are preserved.
Installing it
ComfyUI Manager → search ComfyUI-Alchemine-Pack → install → restart. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/alchemine/comfyui-alchemine-pack
cd comfyui-alchemine-pack && pip install -r requirements.txt
One dependency (python-dotenv), no models, no config, and the nodes appear under AlcheminePack/Prompt. If you run the pack's combined ProcessTags node, this filter is already inside it and on by default - that's the checkbox that explains a prompt coming out shorter than you typed it.
The traps
Case is not normalised. Arm up and arms up don't collide, because the skeleton for the first is Arm up. Capitalise your tags consistently or this node will miss the exact pair it exists to catch - the comparison is one word of downstream string handling away from being fixed, and it isn't there.
You can lose a tag you meant to keep. boots, boot are two different spellings of one thing for this node, so the later one goes. If you genuinely want both terms in the prompt for emphasis, put the plural elsewhere or don't run this node.
The singular is spelled plainly. Word-splitting means only the trailing s per word comes off, so anything irregular - feet/foot, teeth/tooth, children/child - sails through untouched. Expect it to catch the common, boring case and nothing else.
That's the whole node. It's the most narrowly scoped thing in the pack, and if you're building prompts programmatically, that's a feature.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| text | STRING | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| processed_text | STRING | — |
| filtered_tags | STRING | — |