diff --git a/README.md b/README.md index fd783c0..ca78ccf 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, face shape and distinctive features, hair, facial hair, build, 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, 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. The node will be located under **Add Node > utils**. Node name: **Random Person Description**. @@ -45,7 +45,7 @@ The node appears in the **Add Node** menu under **utils > Random Person Descript | `age` | Age as a plain number | `43` | | `nationality` | Nationality descriptor | `Russian` | | `complexion` | Skin tone and texture | `light medium, smooth skin` | -| `face` | Combined face attributes (shape, eyes, nose, mouth, distinctive feature) | `angular face, natural grey eyes, button nose, wide-set lips, a faint scar through one eyebrow` | +| `face` | Combined face attributes (shape, eye colour and shape, eyebrows, nose, mouth, distinctive feature) | `angular face, almond natural grey eyes, arched eyebrows, button nose, wide-set lips, a faint scar through one eyebrow` | | `hair` | Combined hair description | `red very short hair worn twist out` | | `facial_hair` | Beard / moustache style (empty for clean shaven) | `a van dyke beard` | | `body_type` | Build | `muscular` | diff --git a/data/shared/accessories.json b/data/shared/accessories.json index 6b24baa..94af265 100644 --- a/data/shared/accessories.json +++ b/data/shared/accessories.json @@ -27,6 +27,10 @@ "label": "sunglasses", "description": "sunglasses" }, + { + "label": "aviator sunglasses", + "description": "aviator sunglasses" + }, { "label": "stud earrings", "description": "stud earrings" @@ -35,6 +39,22 @@ "label": "hoop earrings", "description": "hoop earrings" }, + { + "label": "drop earrings", + "description": "drop earrings" + }, + { + "label": "ear cuff", + "description": "an ear cuff" + }, + { + "label": "multiple ear piercings", + "description": "multiple ear piercings" + }, + { + "label": "small ear piercings", + "description": "small ear piercings" + }, { "label": "nose ring", "description": "a nose ring" @@ -44,7 +64,39 @@ "description": "a nose stud" }, { - "label": "small ear piercings", - "description": "small ear piercings" + "label": "septum ring", + "description": "a septum ring" + }, + { + "label": "eyebrow piercing", + "description": "an eyebrow piercing" + }, + { + "label": "labret piercing", + "description": "a labret piercing" + }, + { + "label": "pendant necklace", + "description": "a pendant necklace" + }, + { + "label": "choker", + "description": "a choker" + }, + { + "label": "headscarf", + "description": "a headscarf" + }, + { + "label": "beanie", + "description": "a beanie" + }, + { + "label": "cap", + "description": "a cap" + }, + { + "label": "bindi", + "description": "a bindi" } -] \ No newline at end of file +] diff --git a/data/shared/expression.json b/data/shared/expression.json index 0f2642b..bd4706d 100644 --- a/data/shared/expression.json +++ b/data/shared/expression.json @@ -15,6 +15,10 @@ "label": "slight smile", "description": "a slight smile" }, + { + "label": "gentle smile", + "description": "a gentle smile" + }, { "label": "serious", "description": "a serious expression" @@ -27,6 +31,10 @@ "label": "pensive", "description": "a pensive look" }, + { + "label": "contemplative", + "description": "a contemplative expression" + }, { "label": "confident", "description": "a confident expression" @@ -39,8 +47,56 @@ "label": "intense gaze", "description": "an intense gaze" }, + { + "label": "focused", + "description": "a focused expression" + }, + { + "label": "smirk", + "description": "a slight smirk" + }, + { + "label": "raised eyebrow", + "description": "one eyebrow raised" + }, + { + "label": "playful", + "description": "a playful look" + }, + { + "label": "mischievous", + "description": "a mischievous grin" + }, + { + "label": "surprised", + "description": "a surprised look" + }, + { + "label": "slight frown", + "description": "a slight frown" + }, + { + "label": "downcast", + "description": "a downcast expression" + }, + { + "label": "wistful", + "description": "a wistful look" + }, + { + "label": "weary", + "description": "a weary expression" + }, + { + "label": "brooding", + "description": "a brooding expression" + }, { "label": "laughing", "description": "laughing" + }, + { + "label": "joyful", + "description": "a joyful, beaming smile" } -] \ No newline at end of file +] diff --git a/data/shared/eye_shape.json b/data/shared/eye_shape.json new file mode 100644 index 0000000..4993005 --- /dev/null +++ b/data/shared/eye_shape.json @@ -0,0 +1,50 @@ +[ + { + "label": "almond", + "description": "almond" + }, + { + "label": "round", + "description": "round" + }, + { + "label": "hooded", + "description": "hooded" + }, + { + "label": "monolid", + "description": "monolid" + }, + { + "label": "deep-set", + "description": "deep-set" + }, + { + "label": "wide-set", + "description": "wide-set" + }, + { + "label": "close-set", + "description": "close-set" + }, + { + "label": "upturned", + "description": "upturned" + }, + { + "label": "downturned", + "description": "downturned" + }, + { + "label": "heavy-lidded", + "description": "heavy-lidded" + }, + { + "label": "narrow", + "description": "narrow" + }, + { + "label": "large", + "description": "large, expressive" + } +] diff --git a/data/shared/eyebrows.json b/data/shared/eyebrows.json new file mode 100644 index 0000000..d7b1856 --- /dev/null +++ b/data/shared/eyebrows.json @@ -0,0 +1,50 @@ +[ + { + "label": "thick", + "description": "thick eyebrows" + }, + { + "label": "thin", + "description": "thin eyebrows" + }, + { + "label": "arched", + "description": "arched eyebrows" + }, + { + "label": "high arched", + "description": "high arched eyebrows" + }, + { + "label": "straight", + "description": "straight brows" + }, + { + "label": "bushy", + "description": "bushy eyebrows" + }, + { + "label": "full natural", + "description": "full natural brows" + }, + { + "label": "sparse", + "description": "sparse brows" + }, + { + "label": "well-groomed", + "description": "neatly groomed eyebrows" + }, + { + "label": "rounded", + "description": "softly rounded brows" + }, + { + "label": "angular", + "description": "sharply angled eyebrows" + }, + { + "label": "feathered", + "description": "feathered brows" + } +] diff --git a/data/shared/nationality.json b/data/shared/nationality.json index fdda3dd..20f9868 100644 --- a/data/shared/nationality.json +++ b/data/shared/nationality.json @@ -38,5 +38,30 @@ { "label": "Thai", "descriptor": "Thai", "from": "from Thailand" }, { "label": "Bangladeshi", "descriptor": "Bangladeshi", "from": "from Bangladesh" }, { "label": "Ugandan", "descriptor": "Ugandan", "from": "from Uganda" }, - { "label": "Sudanese", "descriptor": "Sudanese", "from": "from Sudan" } + { "label": "Sudanese", "descriptor": "Sudanese", "from": "from Sudan" }, + { "label": "Indonesian", "descriptor": "Indonesian", "from": "from Indonesia" }, + { "label": "Malaysian", "descriptor": "Malaysian", "from": "from Malaysia" }, + { "label": "Cambodian", "descriptor": "Cambodian", "from": "from Cambodia" }, + { "label": "Nepali", "descriptor": "Nepali", "from": "from Nepal" }, + { "label": "Sri Lankan", "descriptor": "Sri Lankan", "from": "from Sri Lanka" }, + { "label": "Mongolian", "descriptor": "Mongolian", "from": "from Mongolia" }, + { "label": "Afghan", "descriptor": "Afghan", "from": "from Afghanistan" }, + { "label": "Iraqi", "descriptor": "Iraqi", "from": "from Iraq" }, + { "label": "Lebanese", "descriptor": "Lebanese", "from": "from Lebanon" }, + { "label": "Israeli", "descriptor": "Israeli", "from": "from Israel" }, + { "label": "Saudi", "descriptor": "Saudi Arabian", "from": "from Saudi Arabia" }, + { "label": "Emirati", "descriptor": "Emirati", "from": "from the UAE" }, + { "label": "Ukrainian", "descriptor": "Ukrainian", "from": "from Ukraine" }, + { "label": "Portuguese", "descriptor": "Portuguese", "from": "from Portugal" }, + { "label": "Irish", "descriptor": "Irish", "from": "from Ireland" }, + { "label": "Norwegian", "descriptor": "Norwegian", "from": "from Norway" }, + { "label": "Finnish", "descriptor": "Finnish", "from": "from Finland" }, + { "label": "Somali", "descriptor": "Somali", "from": "from Somalia" }, + { "label": "Congolese", "descriptor": "Congolese", "from": "from the Congo" }, + { "label": "Tanzanian", "descriptor": "Tanzanian", "from": "from Tanzania" }, + { "label": "Cuban", "descriptor": "Cuban", "from": "from Cuba" }, + { "label": "Venezuelan", "descriptor": "Venezuelan", "from": "from Venezuela" }, + { "label": "Chilean", "descriptor": "Chilean", "from": "from Chile" }, + { "label": "Maori", "descriptor": "Maori", "from": "from New Zealand" }, + { "label": "Samoan", "descriptor": "Samoan", "from": "from Samoa" } ] diff --git a/data/shared/skin_texture.json b/data/shared/skin_texture.json index 9a5dd8e..372ddec 100644 --- a/data/shared/skin_texture.json +++ b/data/shared/skin_texture.json @@ -15,10 +15,50 @@ "label": "glowing", "description": "glowing skin" }, + { + "label": "matte", + "description": "matte skin" + }, + { + "label": "oily", + "description": "an oily sheen" + }, + { + "label": "large pores", + "description": "visibly large pores" + }, { "label": "freckled", "description": "freckled skin" }, + { + "label": "beauty spots", + "description": "scattered beauty spots" + }, + { + "label": "moles", + "description": "a few small moles" + }, + { + "label": "blemished", + "description": "blemished skin" + }, + { + "label": "acne scars", + "description": "faint acne scarring" + }, + { + "label": "rosacea", + "description": "a rosacea flush across the cheeks" + }, + { + "label": "vitiligo", + "description": "patches of vitiligo" + }, + { + "label": "ruddy", + "description": "a ruddy complexion" + }, { "label": "fine lines", "description": "fine lines" @@ -32,11 +72,11 @@ "description": "sun-spotted skin" }, { - "label": "acne scars", - "description": "faint acne scarring" + "label": "weathered", + "description": "weathered skin" }, { - "label": "ruddy", - "description": "a ruddy complexion" + "label": "leathery", + "description": "leathery, sun-aged skin" } -] \ No newline at end of file +] diff --git a/nodes.py b/nodes.py index 0250c0e..941883e 100644 --- a/nodes.py +++ b/nodes.py @@ -93,6 +93,8 @@ _MAKEUP_LABELS = ["(none)"] + _union(_labels(load_sex("male", "makeup.json # Shared (sex-agnostic) lists. _SKIN_TEXTURE_LABELS = ["(none)"] + _labels(load_shared("skin_texture.json")) +_EYE_SHAPE_LABELS = ["(none)"] + _labels(load_shared("eye_shape.json")) +_EYEBROWS_LABELS = ["(none)"] + _labels(load_shared("eyebrows.json")) _ACCESSORIES_LABELS = ["(none)"] + _labels(load_shared("accessories.json")) _EXPRESSION_LABELS = ["(none)"] + _labels(load_shared("expression.json")) @@ -103,6 +105,8 @@ CATEGORY_SPECS = [ ("complexion", _COMP_LABELS, "e.g. fair, olive, deep brown"), ("skin_texture", _SKIN_TEXTURE_LABELS, "e.g. smooth, freckled, wrinkled"), ("eyes", _EYES_LABELS, "e.g. deep brown, ice blue, hazel"), + ("eye_shape", _EYE_SHAPE_LABELS, "e.g. almond, hooded, monolid, round"), + ("eyebrows", _EYEBROWS_LABELS, "e.g. thick, arched, straight, bushy"), ("face_shape", _FACE_SHAPE_LABELS, "e.g. oval, square, heart"), ("nose_shape", _NOSE_SHAPE_LABELS, "e.g. straight nose, broad nose, snub nose"), ("mouth_shape", _MOUTH_SHAPE_LABELS, "e.g. full lips, thin lips, wide mouth"), @@ -125,7 +129,7 @@ SEX_GATED = {"hair_color", "hair_style", "hair_length", "body_type", "face_shape # clean and realistic; the user opts them in per category. Core identity # categories (nationality, complexion, eyes, face/nose/mouth shape, hair, body) # default to "random". -DEFAULT_OFF = {"skin_texture", "face_feature", "facial_hair", +DEFAULT_OFF = {"skin_texture", "eyebrows", "face_feature", "facial_hair", "expression", "accessories", "makeup"} @@ -264,7 +268,8 @@ def nat_text(item): def build_description(sex, nat, age, skin_texture, face_shape, complexion, eyes, nose_shape, mouth_shape, face_feature, hair_color, hair_style, hair_length, facial_hair, - body, expression, accessories, makeup, extra_attributes=""): + body, expression, accessories, makeup, extra_attributes="", + eye_shape=None, eyebrows=None): """Produce a single comma-separated descriptor string.""" parts = [] @@ -285,9 +290,12 @@ def build_description(sex, nat, age, skin_texture, add(face_shape, "{} face") add(complexion, "{} complexion") add(skin_texture) - if eyes: - eyes_str = d(eyes) - parts.append(eyes_str if "eyes" in eyes_str.lower() else f"{eyes_str} eyes") + shape_str = d(eye_shape).strip() if eye_shape else "" + if eyes or shape_str: + color_str = d(eyes).strip() if eyes else "" + base = (color_str if "eyes" in color_str.lower() else f"{color_str} eyes") if color_str else "eyes" + parts.append(f"{shape_str} {base}" if shape_str else base) + add(eyebrows) add(nose_shape) add(mouth_shape) add(face_feature) @@ -354,7 +362,7 @@ def generate_person(seed, randomize, sex, category_args, p["face_shape"], p["complexion"], p["eyes"], p["nose_shape"], p["mouth_shape"], p["face_feature"], p["hair_color"], p["hair_style"], p["hair_length"], p["facial_hair"], p["body_type"], p["expression"], p["accessories"], p["makeup"], - extra_attributes) + extra_attributes, eye_shape=p["eye_shape"], eyebrows=p["eyebrows"]) def dd(key, sub_key="description"): item = p[key] @@ -366,12 +374,17 @@ def generate_person(seed, randomize, sex, category_args, out_hair = " ".join(filter(None, [dd("hair_color"), dd("hair_length"), dd("hair_style")])) def _eyes_str(): - s = dd("eyes") - return (s if "eyes" in s.lower() else f"{s} eyes") if s else "" + shape = dd("eye_shape") + color = dd("eyes") + if not (shape or color): + return "" + base = (color if "eyes" in color.lower() else f"{color} eyes") if color else "eyes" + return f"{shape} {base}" if shape else base out_face = ", ".join(filter(None, [ f"{dd('face_shape')} face" if dd("face_shape") else "", _eyes_str(), + dd("eyebrows"), dd("nose_shape"), dd("mouth_shape"), dd("face_feature"), diff --git a/pyproject.toml b/pyproject.toml index 4e55f3a..cd5fe4e 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, and individual output pins. Located under Add Node > utils. Node name: Random Person Description." -version = "1.0.7" +version = "1.0.8" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [] diff --git a/test_random_person.py b/test_random_person.py index e0cf591..f4c3cd7 100644 --- a/test_random_person.py +++ b/test_random_person.py @@ -88,6 +88,29 @@ class TestSelection(unittest.TestCase): self.assertIn("tan", out[4]) self.assertIn("freckled", out[4]) + def test_eye_shape_merges_into_eyes(self): + # eye_shape sits in front of the eye colour, single "eyes" suffix, and + # surfaces in the face pin. + out = run(eye_shape=("fixed", "", "almond"), + eyes=("fixed", "", "green")) + self.assertIn("almond", out[5]) # face pin + self.assertIn("eyes", out[5]) + self.assertNotIn("eyes eyes", out[0]) + + def test_eye_shape_alone_gets_suffix(self): + out = run(eye_shape=("fixed", "", "hooded"), eyes=("off", "", "(none)")) + self.assertIn("hooded eyes", out[5]) + + def test_eyebrows_pin_and_off(self): + on = run(eyebrows=("fixed", "", "thick")) + self.assertIn("thick", on[5]) # face pin carries eyebrows + off = run(eyebrows=("off", "", "(none)")) + self.assertNotIn("eyebrow", off[5].lower()) + + def test_eyebrows_default_off(self): + req = core.RandomPersonNode.INPUT_TYPES()["required"] + self.assertEqual(req["eyebrows_mode"][1]["default"], "off") + def test_expression_pin_and_off(self): on = run(expression=("fixed", "", "serious")) self.assertIn("serious", on[9])