feat: add sex-gated clothing category (default off); bump 1.2.0

This commit is contained in:
BRADSEC
2026-06-21 20:38:41 +10:00
parent c4b1e93e0b
commit f043e0e1f1
5 changed files with 79 additions and 15 deletions
+15 -6
View File
@@ -6,7 +6,7 @@ Generates a randomised, structured physical description of a person to drop stra
## Purpose
When a prompt leaves appearance vague, diffusion models fall back on a narrow set of "default" faces, so every person in a batch looks the same. This node supplies a specific, well-formed description on each run, covering nationality, age, complexion and skin texture, eye colour and shape, eyebrows, face shape and distinctive features, hair, facial hair, build, shoulders, chest or bust, expression, accessories, and makeup. Varying those traits per seed pushes the model off its defaults toward distinct, individual people. Lock the traits you care about, let the rest randomise.
When a prompt leaves appearance vague, diffusion models fall back on a narrow set of "default" faces, so every person in a batch looks the same. This node supplies a specific, well-formed description on each run, covering nationality, age, complexion and skin texture, eye colour and shape, eyebrows, face shape and distinctive features, hair, facial hair, build, shoulders, chest or bust, expression, accessories, makeup, and clothing. Varying those traits per seed pushes the model off its defaults toward distinct, individual people. Lock the traits you care about, let the rest randomise.
The nodes appear in the **Add Node** menu under **Random Person**:
@@ -15,7 +15,7 @@ The nodes appear in the **Add Node** menu under **Random Person**:
- **Random Person: Face & Expression** - face shape, eyes, eyebrows, nose, mouth, distinctive features, expression.
- **Random Person: Hair** - hair colour, style, length, facial hair.
- **Random Person: Body** - build, shoulders, chest, bust size and shape.
- **Random Person: Style** - accessories and makeup.
- **Random Person: Style** - accessories, makeup, and clothing.
The segment nodes are independent generators: each has its own seed and sex and emits its group's fragment. Concatenate the `description` outputs with any string node to assemble a full prompt.
@@ -64,6 +64,7 @@ The nodes appear in the **Add Node** menu under **Random Person**.
| `expression` | Facial expression (empty for neutral) | `a soft smile` |
| `accessories` | Eyewear / jewellery (empty for none) | `reading glasses` |
| `makeup` | Makeup style (empty for none / male) | `bold lipstick` |
| `clothing` | Garment / outfit (empty when off) | `a leather jacket` |
| `seed` | The seed used (useful for reproducibility) | `481046155` |
Wire `description` directly into a text prompt node, or use individual pins to route specific attributes to other parts of your workflow.
@@ -83,11 +84,11 @@ Wire `description` directly into a text prompt node, or use individual pins to r
Dropdown: `random` / `male` / `female`
When set to `male` or `female`, the dropdown options for hair style, hair colour, hair length, face shape, nose, mouth, body type, facial feature, facial hair, makeup, shoulders, chest, bust size, and bust shape automatically filter to show only sex-appropriate options. Facial hair and makeup are effectively sex-specific: females never grow facial hair and males have no makeup options.
When set to `male` or `female`, the dropdown options for hair style, hair colour, hair length, face shape, nose, mouth, body type, facial feature, facial hair, makeup, clothing, shoulders, chest, bust size, and bust shape automatically filter to show only sex-appropriate options. Facial hair and makeup are effectively sex-specific: females never grow facial hair and males have no makeup options.
### Per-Category Controls
Every attribute category (nationality, complexion, skin texture, eyes, eye shape, eyebrows, face shape, nose shape, mouth shape, facial feature, hair colour, hair style, hair length, facial hair, body type, shoulders, chest, bust size, bust shape, expression, accessories, makeup) has three controls:
Every attribute category (nationality, complexion, skin texture, eyes, eye shape, eyebrows, face shape, nose shape, mouth shape, facial feature, hair colour, hair style, hair length, facial hair, body type, shoulders, chest, bust size, bust shape, expression, accessories, makeup, clothing) has three controls:
| Control | What it does |
|---|---|
@@ -110,7 +111,7 @@ Every attribute category (nationality, complexion, skin texture, eyes, eye shape
Core identity categories (nationality, complexion, eyes, eye shape, face shape, nose shape, mouth shape, hair colour/style/length, body type, plus age and sex) default to `random`. The optional "flair" categories default to `off` so the base person stays clean and realistic, and you opt them in per category:
`skin_texture`, `eyebrows`, `face_feature`, `facial_hair`, `expression`, `accessories`, `makeup`, `shoulders`, `chest`, `bust_size`, `bust_shape`
`skin_texture`, `eyebrows`, `face_feature`, `facial_hair`, `expression`, `accessories`, `makeup`, `shoulders`, `chest`, `bust_size`, `bust_shape`, `clothing`
The example outputs above have several of these enabled to show the full range.
@@ -143,7 +144,7 @@ A free-text area at the bottom of the node. Enter any custom descriptors separat
wearing glasses, tattoo on left arm, silver hoop earrings
```
This is the right place for clothing, accessories, expressions, or any detail not covered by the built-in categories.
This is the right place for accessories, expressions, poses, or any detail not covered by the built-in categories.
---
@@ -259,6 +260,14 @@ none, natural makeup, soft makeup, minimal makeup, bold makeup, glam makeup, smo
> Males have only `none`. `none` outputs nothing.
### Clothing
**Male (22):** plain t-shirt, fitted t-shirt, button-up shirt, flannel shirt, polo shirt, henley shirt, work shirt, tank top, hoodie, crew-neck sweater, turtleneck, denim jacket, leather jacket, bomber jacket, puffer jacket, parka, trench coat, wool overcoat, blazer, tailored suit, three-piece suit, tracksuit
**Female (24):** plain t-shirt, fitted t-shirt, button-up shirt, blouse, tank top, camisole, off-shoulder top, peplum top, knit sweater, cardigan, hoodie, turtleneck, denim jacket, leather jacket, puffer jacket, trench coat, wool coat, blazer, tailored suit, jumpsuit, sundress, wrap dress, maxi dress, cocktail dress
> Sex-gated. Defaults to `off`.
---
## Tips
+2
View File
@@ -14,6 +14,7 @@ const MALE_ONLY = {
face_feature_fixed: ["birthmark on jaw","heavy brow","mole on cheek","strong jaw","weathered skin"],
facial_hair_fixed: ["circle beard","full beard","goatee","heavy stubble","light stubble","moustache","mutton chops","short beard","soul patch","van dyke"],
makeup_fixed: [],
clothing_fixed: ["bomber jacket","crew-neck sweater","flannel shirt","henley shirt","parka","polo shirt","three-piece suit","tracksuit","wool overcoat","work shirt"],
shoulders_fixed: ["muscular"],
chest_fixed: ["average","barrel","broad","flat","muscular"],
bust_size_fixed: [],
@@ -31,6 +32,7 @@ const FEMALE_ONLY = {
face_feature_fixed: ["arched brows","beauty mark","birthmark","wide eyes"],
facial_hair_fixed: [],
makeup_fixed: ["bold lipstick","bold makeup","glam makeup","minimal makeup","natural makeup","red lipstick","smokey eye","soft makeup","winged eyeliner"],
clothing_fixed: ["blouse","camisole","cardigan","cocktail dress","jumpsuit","knit sweater","maxi dress","off-shoulder top","peplum top","sundress","wool coat","wrap dress"],
shoulders_fixed: ["soft"],
chest_fixed: [],
bust_size_fixed: ["average","full","large","petite","small"],
+14 -8
View File
@@ -90,6 +90,8 @@ _FACIAL_HAIR_LABELS = ["(none)"] + _union(_labels(load_sex("male", "facial_hair
_labels(load_sex("female", "facial_hair.json")))
_MAKEUP_LABELS = ["(none)"] + _union(_labels(load_sex("male", "makeup.json")),
_labels(load_sex("female", "makeup.json")))
_CLOTHING_LABELS = ["(none)"] + _union(_labels(load_sex("male", "clothing.json")),
_labels(load_sex("female", "clothing.json")))
_SHOULDERS_LABELS = ["(none)"] + _union(_labels(load_sex("male", "shoulders.json")),
_labels(load_sex("female", "shoulders.json")))
_CHEST_LABELS = ["(none)"] + _union(_labels(load_sex("male", "chest.json")),
@@ -131,12 +133,13 @@ CATEGORY_SPECS = [
("expression", _EXPRESSION_LABELS, "e.g. soft smile, serious, confident"),
("accessories", _ACCESSORIES_LABELS, "e.g. glasses, hoop earrings, nose ring"),
("makeup", _MAKEUP_LABELS, "e.g. natural makeup, bold lipstick, smokey eye"),
("clothing", _CLOTHING_LABELS, "e.g. t-shirt, leather jacket, blazer"),
]
# Categories whose data lives under data/<sex>/ rather than data/shared/.
SEX_GATED = {"hair_color", "hair_style", "hair_length", "body_type", "face_shape",
"nose_shape", "mouth_shape", "face_feature", "facial_hair", "makeup",
"shoulders", "chest", "bust_size", "bust_shape"}
"shoulders", "chest", "bust_size", "bust_shape", "clothing"}
# Optional "flair" categories default to mode "off" so the base description stays
# clean and realistic; the user opts them in per category. Core identity
@@ -144,7 +147,7 @@ SEX_GATED = {"hair_color", "hair_style", "hair_length", "body_type", "face_shape
# default to "random".
DEFAULT_OFF = {"skin_texture", "eyebrows", "face_feature", "facial_hair",
"expression", "accessories", "makeup",
"shoulders", "chest", "bust_size", "bust_shape"}
"shoulders", "chest", "bust_size", "bust_shape", "clothing"}
def _mode_default(key):
@@ -165,7 +168,7 @@ _EXTRA_TIP = "Free text appended to the description as-is, comma-separated. Clo
RETURN_NAMES = ("description", "sex", "age", "nationality", "complexion",
"face", "hair", "facial_hair", "body_type",
"expression", "accessories", "makeup",
"shoulders", "chest", "bust", "seed")
"shoulders", "chest", "bust", "clothing", "seed")
# -- Selection logic -----------------------------------------------------------
@@ -296,7 +299,8 @@ def build_description(sex, nat, age, skin_texture,
hair_color, hair_style, hair_length, facial_hair,
body, expression, accessories, makeup, extra_attributes="",
eye_shape=None, eyebrows=None,
shoulders=None, chest=None, bust_size=None, bust_shape=None):
shoulders=None, chest=None, bust_size=None, bust_shape=None,
clothing=None):
"""Produce a single comma-separated descriptor string."""
parts = []
@@ -360,6 +364,7 @@ def build_description(sex, nat, age, skin_texture,
add(expression)
add(accessories)
add(makeup)
add(clothing)
if extra_attributes and extra_attributes.strip():
for attr in extra_attributes.split(","):
@@ -402,7 +407,8 @@ def generate_person(seed, randomize, sex, category_args,
p["body_type"], p["expression"], p["accessories"], p["makeup"],
extra_attributes, eye_shape=p["eye_shape"], eyebrows=p["eyebrows"],
shoulders=p["shoulders"], chest=p["chest"],
bust_size=p["bust_size"], bust_shape=p["bust_shape"])
bust_size=p["bust_size"], bust_shape=p["bust_shape"],
clothing=p["clothing"])
def dd(key, sub_key="description"):
item = p[key]
@@ -435,7 +441,7 @@ def generate_person(seed, randomize, sex, category_args,
return (description, resolved_sex, out_age, out_nat, out_comp,
out_face, out_hair, dd("facial_hair"), dd("body_type"),
dd("expression"), dd("accessories"), dd("makeup"),
dd("shoulders"), dd("chest"), out_bust, seed)
dd("shoulders"), dd("chest"), out_bust, dd("clothing"), seed)
def _collect_category_args(kwargs, keys=None):
@@ -483,8 +489,8 @@ NODE_SPECS = [
["body_type", "shoulders", "chest", "bust_size", "bust_shape"], False,
("description", "sex", "body_type", "shoulders", "chest", "bust", "seed")),
("RandomPersonStyle", "Random Person: Style",
["accessories", "makeup"], False,
("description", "sex", "accessories", "makeup", "seed")),
["clothing", "accessories", "makeup"], False,
("description", "sex", "clothing", "accessories", "makeup", "seed")),
]
_NODE_DESCRIPTION = "Generate a randomised, structured physical person description for image prompts."
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_randomperson"
description = "Generate randomised, structured physical person descriptions for image-generation prompts. Sex-aware traits, per-category random/allow_list/fixed/off modes, age ranges, body details, and individual output pins. Full node plus Identity, Face, Hair, Body, and Style segment nodes under the Random Person menu category."
version = "1.1.0"
version = "1.2.0"
license = { file = "LICENSE" }
requires-python = ">=3.10"
dependencies = []
+47
View File
@@ -338,5 +338,52 @@ class TestSegmentNodes(unittest.TestCase):
self.assertIn(name, core.RETURN_NAMES, f"{_id}:{name}")
class TestClothing(unittest.TestCase):
def test_clothing_defaults_off(self):
req = core.RandomPersonNode.INPUT_TYPES()["required"]
self.assertEqual(req["clothing_mode"][1]["default"], "off")
def test_clothing_is_sex_gated(self):
self.assertIn("clothing", core.SEX_GATED)
def test_clothing_pin_carries_value(self):
out = run(sex="female", clothing=("fixed", "", "leather jacket"))
idx = core.RETURN_NAMES.index("clothing")
self.assertEqual(out[idx], "a leather jacket")
self.assertIn("a leather jacket", out[0])
def test_male_never_gets_female_only_garment(self):
female_only_descs = {
"a blouse", "a camisole", "a cardigan", "a cocktail dress",
"a jumpsuit", "a knit sweater", "a maxi dress", "an off-shoulder top",
"a peplum top", "a sundress", "a wool coat", "a wrap dress",
}
for seed in range(30):
out = run(seed=seed, sex="male", clothing=("random", "", "(none)"))
idx = core.RETURN_NAMES.index("clothing")
val = out[idx]
if val:
self.assertNotIn(val, female_only_descs, f"seed={seed}: male got female-only '{val}'")
def test_female_never_gets_male_only_garment(self):
male_only_descs = {
"a flannel shirt", "a polo shirt", "a henley shirt", "a work shirt",
"a bomber jacket", "a crew-neck sweater", "a parka", "a three-piece suit",
"a tracksuit", "a wool overcoat",
}
for seed in range(30):
out = run(seed=seed, sex="female", clothing=("random", "", "(none)"))
idx = core.RETURN_NAMES.index("clothing")
val = out[idx]
if val:
self.assertNotIn(val, male_only_descs, f"seed={seed}: female got male-only '{val}'")
def test_style_node_exposes_clothing(self):
req = core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].INPUT_TYPES()["required"]
self.assertIn("clothing_mode", req)
self.assertIn("clothing", core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].RETURN_NAMES)
if __name__ == "__main__":
unittest.main()