diff --git a/README.md b/README.md index 31f11ba..b1c5234 100644 --- a/README.md +++ b/README.md @@ -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, 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. +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, clothing, and footwear. 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, makeup, and clothing. +- **Random Person: Style** - accessories, makeup, clothing, and footwear. 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. @@ -65,6 +65,7 @@ The nodes appear in the **Add Node** menu under **Random Person**. | `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` | +| `footwear` | Footwear / shoes (empty when off) | `running shoes` | | `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. @@ -84,11 +85,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, 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. +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, footwear, 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, clothing) 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, footwear) has three controls: | Control | What it does | |---|---| @@ -111,7 +112,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`, `clothing` +`skin_texture`, `eyebrows`, `face_feature`, `facial_hair`, `expression`, `accessories`, `makeup`, `shoulders`, `chest`, `bust_size`, `bust_shape`, `clothing`, `footwear` The example outputs above have several of these enabled to show the full range. @@ -268,6 +269,14 @@ none, natural makeup, soft makeup, minimal makeup, bold makeup, glam makeup, smo > Sex-gated. Defaults to `off`. +### Footwear + +**Male (25):** ankle boots, bare feet, basketball shoes, boat shoes, brogues, canvas shoes, chelsea boots, combat boots, crocs, derby shoes, dress shoes, espadrilles, flip-flops, hiking boots, high-top sneakers, loafers, moccasins, oxford shoes, running shoes, sandals, skate shoes, slides, slippers, sneakers, work boots + +**Female (34):** ankle boots, ballet flats, bare feet, basketball shoes, boat shoes, brogues, canvas shoes, chelsea boots, combat boots, crocs, derby shoes, dress shoes, espadrilles, flip-flops, hiking boots, high heels, high-top sneakers, kitten heels, knee-high boots, loafers, mary janes, moccasins, oxford shoes, platform heels, running shoes, sandals, skate shoes, slides, slippers, sneakers, stilettos, strappy heels, wedges, work boots + +> Sex-gated. Heels and flats are female-only; the rest are shared. Defaults to `off`. + --- ## Tips diff --git a/js/random_person_node.js b/js/random_person_node.js index fc17e42..c76292c 100644 --- a/js/random_person_node.js +++ b/js/random_person_node.js @@ -15,6 +15,7 @@ const MALE_ONLY = { 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"], + footwear_fixed: [], shoulders_fixed: ["muscular"], chest_fixed: ["average","barrel","broad","flat","muscular"], bust_size_fixed: [], @@ -33,6 +34,7 @@ const FEMALE_ONLY = { 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"], + footwear_fixed: ["ballet flats","high heels","kitten heels","knee-high boots","mary janes","platform heels","stilettos","strappy heels","wedges"], shoulders_fixed: ["soft"], chest_fixed: [], bust_size_fixed: ["average","extra large","full","large","petite","small"], diff --git a/nodes.py b/nodes.py index 68a4552..e434d24 100644 --- a/nodes.py +++ b/nodes.py @@ -92,6 +92,8 @@ _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"))) +_FOOTWEAR_LABELS = ["(none)"] + _union(_labels(load_sex("male", "footwear.json")), + _labels(load_sex("female", "footwear.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")), @@ -134,12 +136,13 @@ CATEGORY_SPECS = [ ("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"), + ("footwear", _FOOTWEAR_LABELS, "e.g. sneakers, boots, sandals, heels"), ] # Categories whose data lives under data// 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", "clothing"} + "shoulders", "chest", "bust_size", "bust_shape", "clothing", "footwear"} # Optional "flair" categories default to mode "off" so the base description stays # clean and realistic; the user opts them in per category. Core identity @@ -147,7 +150,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", "clothing"} + "shoulders", "chest", "bust_size", "bust_shape", "clothing", "footwear"} def _mode_default(key): @@ -168,7 +171,7 @@ _EXTRA_TIP = "Free text appended to the description as-is, comma-separated. Pos RETURN_NAMES = ("description", "sex", "age", "nationality", "complexion", "face", "hair", "facial_hair", "body_type", "expression", "accessories", "makeup", - "shoulders", "chest", "bust", "clothing", "seed") + "shoulders", "chest", "bust", "clothing", "footwear", "seed") # -- Selection logic ----------------------------------------------------------- @@ -300,7 +303,7 @@ def build_description(sex, nat, age, skin_texture, body, expression, accessories, makeup, extra_attributes="", eye_shape=None, eyebrows=None, shoulders=None, chest=None, bust_size=None, bust_shape=None, - clothing=None): + clothing=None, footwear=None): """Produce a single comma-separated descriptor string.""" parts = [] @@ -365,6 +368,7 @@ def build_description(sex, nat, age, skin_texture, add(accessories) add(makeup) add(clothing) + add(footwear) if extra_attributes and extra_attributes.strip(): for attr in extra_attributes.split(","): @@ -408,7 +412,7 @@ def generate_person(seed, randomize, sex, category_args, 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"], - clothing=p["clothing"]) + clothing=p["clothing"], footwear=p["footwear"]) def dd(key, sub_key="description"): item = p[key] @@ -441,7 +445,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, dd("clothing"), seed) + dd("shoulders"), dd("chest"), out_bust, dd("clothing"), dd("footwear"), seed) def _collect_category_args(kwargs, keys=None): @@ -489,8 +493,8 @@ NODE_SPECS = [ ["body_type", "shoulders", "chest", "bust_size", "bust_shape"], False, ("description", "sex", "body_type", "shoulders", "chest", "bust", "seed")), ("RandomPersonStyle", "Random Person: Style", - ["clothing", "accessories", "makeup"], False, - ("description", "sex", "clothing", "accessories", "makeup", "seed")), + ["clothing", "accessories", "makeup", "footwear"], False, + ("description", "sex", "clothing", "accessories", "makeup", "footwear", "seed")), ] _NODE_DESCRIPTION = "Generate a randomised, structured physical person description for image prompts." diff --git a/pyproject.toml b/pyproject.toml index 7b2e7b7..909d977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.2.0" +version = "1.3.0" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [] diff --git a/test_random_person.py b/test_random_person.py index 54909bf..d1702c8 100644 --- a/test_random_person.py +++ b/test_random_person.py @@ -385,5 +385,40 @@ class TestClothing(unittest.TestCase): self.assertIn("clothing", core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].RETURN_NAMES) +class TestFootwear(unittest.TestCase): + + def test_footwear_defaults_off(self): + req = core.RandomPersonNode.INPUT_TYPES()["required"] + self.assertEqual(req["footwear_mode"][1]["default"], "off") + + def test_footwear_is_sex_gated(self): + self.assertIn("footwear", core.SEX_GATED) + + def test_footwear_pin_carries_value(self): + out = run(sex="female", clothing=("fixed", "", "leather jacket"), + footwear=("fixed", "", "high heels")) + idx = core.RETURN_NAMES.index("footwear") + self.assertEqual(out[idx], "high heels") + self.assertIn("high heels", out[0]) + + def test_male_never_gets_female_only_shoe(self): + female_only_shoes = { + "high heels", "stilettos", "kitten heels", "platform heels", + "strappy heels", "wedges", "ballet flats", "mary janes", "knee-high boots", + } + for seed in range(30): + out = run(seed=seed, sex="male", footwear=("random", "", "(none)")) + idx = core.RETURN_NAMES.index("footwear") + val = out[idx] + if val: + self.assertNotIn(val, female_only_shoes, + f"seed={seed}: male got female-only shoe '{val}'") + + def test_style_node_exposes_footwear(self): + req = core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].INPUT_TYPES()["required"] + self.assertIn("footwear_mode", req) + self.assertIn("footwear", core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].RETURN_NAMES) + + if __name__ == "__main__": unittest.main()