CLIP Text Encode (Ethnicity/Hairstyle/Expression)
A text encoder that rolls a random ethnicity, hairstyle, and expression into every generation
- clip
- positive_conditioning
- negative_conditioning
This node is a drop-in replacement for the stock CLIP Text Encode that quietly rewrites your prompt before it ever reaches the model. Instead of just encoding whatever you typed, it prepends an ethnicity, a hairstyle, and an expression - either ones you pick from a dropdown, or a random roll on every single run. It's a variety machine: set all three to "random" and your batch of 30 images gets 30 different faces instead of 30 near-clones. Built by Adam Bergh (ajbergh), it's a small, single-file pack with no heavy dependencies - genuinely one of the simpler conditioning utilities you'll install.
Where it fits: this is SD 1.5/SDXL lineage tooling. The author's example workflow is SDXL with a LoRA, and everything about the design - comma-separated descriptors, prepending for emphasis, one CLIP input - assumes a CLIP-based checkpoint. If your whole stack is Flux or an LLM-encoded model, this isn't the node for you.
How it works
The mechanism is almost embarrassingly simple, which is why it works. On import, the node reads three CSVs sitting next to it in the pack folder (ethnicity.csv, hairstyle.csv, expression.csv - roughly 50–80 entries each), sorts them, and slots a "random" entry at the top of every list. When you queue a run, it builds your positive prompt by prepending whatever's selected:
- ethnicity →
"Asian person, " - hairstyle →
"curly hair, " - expression →
"happy expression, "
then hands the whole string to the standard clip.tokenize / clip.encode_from_tokens pipeline with pooling enabled. Each toggle can be flipped "off" independently, so you can keep the random expression while pinning a specific hairstyle. The negative prompt is encoded untouched.
There's one behavior you should know about before you rely on it. The node's IS_CHANGED returns the current time whenever any of the three dropdowns is set to "random", which forces ComfyUI to re-run the encode on every queue. That's how the random picks stay fresh - but it also means a "random" selection is not a one-time thing. Every generation rolls again, and every roll costs a re-encode. If you wanted "random once, then keep it," there's no lock button here; you'd have to read the chosen value off the workflow and pin it manually.
Inputs and outputs that matter
You mostly set four things:
text_positive/text_negative- your normal prompt and negative, both multiline.- The three toggles -
ethnicity_toggle,hairstyle_toggle,expression_toggle("on"/"off"). - The three dropdowns -
ethnicity,hairstyle,expression, each defaulting to "random".
The inputs you don't touch much: clip (wire in your CLIP model the same way you feed CLIPTextEncode). One honest tip: the node prepends descriptors, and earlier tokens carry more attention weight, so with all three toggles on your prompt's first words are the auto-generated ones. If output starts feeling generic, flip a toggle off and let your own subject lead.
Two outputs: positive_conditioning and negative_conditioning (both CONDITIONING). They wire straight into the sampler - it replaces a pair of standard CLIPTextEncode nodes with one.
Installing it
The easy way: ComfyUI Manager → Install Custom Nodes → search comfyui-ethnicity_hairstyle_clip_encoder, install, restart. Or by hand:
cd ComfyUI/custom_nodes
git clone https://github.com/ajbergh/comfyui-ethnicity_hairstyle_clip_encoder
then restart ComfyUI. There are no pip dependencies beyond what ComfyUI already ships (torch, etc.) and no model files to download - the CSVs travel inside the repo. It's MIT licensed.
Common issues
- Console error about
expressionWidgetWidget. Real bug, not you. The bundled JS extension that syncs your dropdown choices to ComfyUI settings definesexpressionWidgetbut then references a typo'dexpressionWidgetWidgetwhen wiring the expression callback - so that callback throws when the node is created. The ethnicity and hairstyle callbacks are fine, and the Python side doesn't care about the JS at all, so generation still works. You'll notice it only if you rely on the expression choice persisting across sessions. - Dropdowns stuck after editing the CSVs. Options are read at import time. Edit
ethnicity.csvand you'll need a restart (or a reload) before the menu reflects it. - Every run looks different when you didn't ask for it. That's the "random" default doing its job. Pin a dropdown or set a toggle to "off" for determinism.
- "It ignores my prompt ordering." Not a bug - the descriptors are deliberately prepended. Restructure or toggle as above.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| text_positive | STRING | — | |
| text_negative | STRING | — | |
| clip | CLIP | — | |
| ethnicity_toggle | COMBO | on | 2 options: on, off |
| hairstyle_toggle | COMBO | on | 2 options: on, off |
| expression_toggle | COMBO | on | 2 options: on, off |
| ethnicityopt | COMBO | random | 54 options: random, Aboriginal Australian, Afro-descendant, Ainu, Arab, Armenian, +48 |
| hairstyleopt | COMBO | random | 59 options: random, Afro, Asymmetrical Cut, Bald, Bantu Knots, Blunt Cut, +53 |
| expressionopt | COMBO | random | 86 options: random, Amused, Angry, Awestruck, Blinking rapidly, Bored, +80 |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| positive_conditioning | CONDITIONING | — |
| negative_conditioning | CONDITIONING | — |