Prompt Cleaner
Copy-Pasting Danbooru Tags Into Anima? Clean Them First.
- extra_texts
- cleaned_text
Here is a prompt you will actually end up with, because Danbooru gives you tags in its own format and you paste them:
drawing_bow, sunna_(zenless_zone_zero), (masterpiece:1.3), best_quality, tagme
Here is what Anima actually wants:
sunna \(zenless zone zero\), drawing bow, (masterpiece:1.3), best quality
Three things changed: underscores became spaces, the series brackets got escaped so they survive, and tagme - a pure metadata tag that means "someone should tag this" - vanished. Prompt Cleaner does all three in one node, at paste time.
Why the underscores matter, and why the brackets matter more
Anima is a 2B DiT on Cosmos-Predict2 with a Qwen3-0.6B text encoder, and it takes Danbooru tags markedly better than paraphrases of them. But it does not take them in Danbooru's storage format. The community rule from the Anima prompting thread is blunt: replace underscores with spaces, score_7 being the one exception. That is the whole market for this node - it is the difference between drawing_bow (a token sequence the model has never seen) and drawing bow (a concept it definitely has).
The bracket part is the subtler trap. In ComfyUI, (sunna (zenless zone zero)) is not a character name with a series in it - token_weights parses round brackets as attention syntax, so the series gets eaten as if it were ((...)) emphasis. The node escapes them to \(zenless zone zero\) because escape_important in comfy/sd1_clip.py is the one escape sequence that code path understands, and Anima's encoder routes through it. That's why the fix holds for Anima and not just for SDXL checkpoints.
What you set, and what you get
The node has a permanent text_in socket, an autogrowing bank of extra sockets, and the usual multiline text box. This is the part people get wrong: they are not either/or. Everything connected plus whatever is typed gets concatenated in the order text_in → extra sockets → text box, with newlines between, and then cleaned as one prompt. So dedupe and the blacklist work across sources - if your character node and your style node both emit masterpiece, you get it once. Autogrow gives you up to ten extra sockets and mints a fresh one whenever you connect the last free one; that's normal behaviour, remove it if you don't want it.
After that come the cleaning switches, all of which now live behind one ⚙ settings dialog on the node (they are still real, declared inputs - same names in the workflow JSON, same values in an API-format run, they are just drawn at zero height). For a beginner, four matter:
bracket_policy- leave it on escaped parentheses only. The "escape everything" option writes\[and\{, which ComfyUI never un-escapes because square and curly brackets are not syntax there, so you inject literal backslashes into your prompt for nothing.underscore_to_space- on by default, and it will turnscore_7intoscore 7. If you use score tags (the model card recommendsscore_7), turn this off.strip_blacklistplus theblacklistbox - the built-in list killstagme,watermark,signature,artist requestandyear 20xx. Plain lines are whole-tag matches, sosignaturewon't eatsignature move;re:lines are regexes, and a broken one prints a console warning instead of failing your run.dedupe- case-insensitive, keeps the first occurrence.
The single output is cleaned_text, a one-line string you wire into any CLIP Text Encode (or into the pack's own encode node). You don't need a preview node to see it - the result is painted onto the node, and this node is registered as an output node, so it runs and shows the text even with nothing plugged into it. That is the fastest way to check what your pipeline is actually sending.
One opinionated note on keep_weights: it protects (tag:1.2) from being escaped, which is the right call, since mangling a weight into \(tag:1.2\) would be worse. But on Anima prompt weighting is discarded by the LLM encoder path, so those brackets are literal punctuation to the tokenizer. Booru block, comma+space, no weights is still what gets you better images.
Install
No dependencies at all - requirements.txt is comments only and pyproject.toml declares an empty dependency list, so nothing gets pip-installed behind your back. The one requirement is a recent ComfyUI: this pack uses the V3 node API (comfy_api.latest) and declares requires-comfyui >= 0.3.60.
cd ComfyUI/custom_nodes
git clone https://github.com/L134283/comfyui-promote-cleaner
# restart ComfyUI
Or search Prompt Cleaner in ComfyUI Manager, or comfy node install comfyui-promote-cleaner. You will find it under the Prompt Cleaner category in the right-click menu and in Essentials → Basics. To update: git pull in that folder, then hard-refresh the browser.
Where people get burned
The buttons aren't there. + text input and ⚙ settings are added by a frontend extension (web/js/prompt_cleaner.js). If you don't see them, check the browser console and hit Ctrl+F5 - custom node JS is cached aggressively. The backend node still works without them; you just lose the dialog.
No nodes at all after restart. Almost always a ComfyUI older than 0.3.60. Update ComfyUI, not the pack.
The two dropdowns are in Chinese. Deliberate: they are parameter values written into the workflow JSON, not translatable UI strings, so the author kept them verbose rather than showing two identical-looking commas. The tooltips explain them.
Fair warning on expectations: this is a 2026 pack from a single author with essentially no community footprint yet, so you're early. But every failure mode above is a frontend or version issue - the cleaning engine is plain Python with no third-party imports, and that's the part that usually rots.
Inputs (12)
| Name | Type | Default | Description |
|---|---|---|---|
| text | STRING | Paste raw tags here: comma separated, one tag per line, or a mix of both. When 'text in' is connected, this box is appended after it (and after any extra text sockets), so you can mix an upstream prompt with extra tags typed here. Leave it empty to use only the sockets. | |
| bracket_policy | COMBO | 仅转义圆括号(推荐) | Bracket escaping policy. - Escaped parentheses only (recommended): sunna (zenless zone zero) -> sunna \(zenless zone zero\). In ComfyUI only \( \) is a real escape sequence (see escape_important in comfy/sd1_clip.py). - Escape everything: also backslash-escape square and curly brackets. Note that [ ] { } are NOT syntax in ComfyUI, so \[ \] \{ \} is never unescaped and the backslash ends up as a literal character for the Qwen2 tokenizer. - Leave as is: no escaping at all. Explicit weights such as (tag:1.2) are handled by the separate 'keep_weights' toggle, not by this policy. |
| keep_weights | BOOLEAN | true | Preserve explicit weight syntax: masterpiece, (best_quality:1.3) -> masterpiece, (best quality:1.3) A bracket group ending in :number is treated as a weight and kept as is so ComfyUI can parse it normally; all other brackets (such as the character series in 'sunna (zenless zone zero)') are escaped as usual. Nested weights such as ((tag:1.2)) are preserved too. Turn this off to escape every parenthesis, which would degrade (masterpiece:1.2) into \(masterpiece:1.2\) and drop the weight. |
| underscore_to_space | BOOLEAN | true | Convert underscores to spaces: drawing_bow -> drawing bow; sunna_(zenless_zone_zero) -> sunna (zenless zone zero). |
| dedupe | BOOLEAN | true | Drop repeated tags (case-insensitive, leading and trailing whitespace ignored), keeping the first occurrence. |
| strip_blacklist | BOOLEAN | true | Drop dirty tags such as tagme / watermark / signature using the blacklist below. |
| lowercase | BOOLEAN | false | Lowercase every tag, matching the Anima community convention of lowercase multi-word tags. |
| separator | COMBO | 逗号 + 空格 | String used to join the tags. '逗号 + 空格' = comma followed by a space (default, recommended); '仅逗号' = bare comma. |
| normalize_fullwidth | BOOLEAN | true | Convert full-width punctuation and full-width spaces to their half-width equivalents, e.g. () -> () and _ -> _. |
| blacklist | STRING | # ===== Prompt Cleaner 默认黑名单 ===== # 每行一条规则;# 开头为注释;清空本框 = 关闭黑名单过滤。 # 普通标签:忽略大小写、忽略空格/下划线差异,做「整标签」匹配。 # 正则规则:以 re: 开头,做包含匹配(如需整标签匹配请用 ^...$ 锚点)。 # --- Danbooru meta / 水印 / 署名类 --- tagme watermark sample watermark signature username twitter username patreon username artist name web address bad id bad source dated logo # --- 纯文字 / 对话框 / 翻译标注类 --- english text translated check translation speech bubble commentary # --- 请求类站位标签(对出图无意义) --- artist request character request reference request # --- 年份标签,如 year 2024 --- re:^year\s+\d{4}$ | Blacklist rules, one per line: - blank lines, and lines starting with #, are comments; - plain text = whole-tag match (case-insensitive, spaces and underscores treated as equal), e.g. watermark; - re: prefix = regular expression, partial match, e.g. re:^year\s+\d{4}$. Clear this box to disable blacklist filtering entirely. |
| text_inopt | STRING | Optional STRING socket. Whatever arrives here is cleaned first, then the extra text sockets (if any) and the content of the 'prompt text' box below are appended right after it and cleaned together. Leave it unconnected to clean the other sources alone. | |
| extra_textsopt | COMFY_AUTOGROW_V3 | Extra text inputs. - Connect a socket to make the next one appear (up to 10 extra sockets); - or use the '+ text input' button on the node / the settings window to add or remove sockets by hand. Everything that is connected is concatenated in order (text in -> extra inputs -> prompt text box) and cleaned as one prompt, so dedupe and blacklist also work across sources. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| cleaned_text | STRING | Cleaned single-line prompt, ready for a CLIP Text Encode node. |