From 7271b38878fd07b4ddb83880c0c9cff9502156cd Mon Sep 17 00:00:00 2001 From: BRADSEC <7948876+bradsec@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:36:19 +1000 Subject: [PATCH] feat: per-node full_*_description first pin labels; README AIO height note --- README.md | 6 ++++-- nodes.py | 24 ++++++++++++++++++++++-- test_random_person.py | 19 +++++++++++++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7548c60..dd1791e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ The nodes appear in the **Add Node** menu under **Random Person**: - **Random Person: Body** - build, shoulders, chest, bust size and shape. - **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. +The AIO node exposes every category at once, so it is a very tall node. You do not have to use it: if you only need part of a person, or want a shorter, tidier graph, use the relevant segment node on its own. The segment nodes are independent generators - each has its own seed and sex and emits only its group's fragment. Use one alone, or wire several together (set the same `sex` on each) and concatenate their full description outputs with any string node to assemble a complete prompt. + +Each node's first output pin is its full description, named per node so it is clear where it came from: `full_description` (AIO), `full_identity_description`, `full_face_description`, `full_hair_description`, `full_body_description`, `full_style_description`. **Example output (male):** ``` @@ -49,7 +51,7 @@ The nodes appear in the **Add Node** menu under **Random Person**. | Pin | Contents | Example | |---|---|---| -| `description` | Full comma-separated description string | `43 year old Russian female, angular face, ...` | +| `full_description` | Full comma-separated description string (the AIO node's first pin; segment nodes name theirs `full__description`) | `43 year old Russian female, angular face, ...` | | `sex` | Resolved sex | `female` | | `age` | Age as a plain number | `43` | | `nationality` | Nationality descriptor | `Russian` | diff --git a/nodes.py b/nodes.py index e434d24..41f278e 100644 --- a/nodes.py +++ b/nodes.py @@ -499,17 +499,36 @@ NODE_SPECS = [ _NODE_DESCRIPTION = "Generate a randomised, structured physical person description for image prompts." +# Per-node display name for the first ("description") output pin. The internal +# lookup key stays "description"; only the shown pin label changes, so it is +# clear which node a full description came from. +_FIRST_PIN_LABEL = { + "RandomPersonNode": "full_description", + "RandomPersonIdentity": "full_identity_description", + "RandomPersonFace": "full_face_description", + "RandomPersonHair": "full_hair_description", + "RandomPersonBody": "full_body_description", + "RandomPersonStyle": "full_style_description", +} + + +def _display_names(node_id, output_names): + """Output names as shown in the UI: first pin relabelled per node, rest unchanged.""" + first = _FIRST_PIN_LABEL.get(node_id, output_names[0]) + return (first,) + tuple(output_names[1:]) + def make_v1_node(node_id, display_name, keys, include_age, output_names): """Build a ComfyUI V1 node class exposing only `keys` and returning `output_names`.""" key_set = set(keys) + display = _display_names(node_id, output_names) class _RandomPersonV1: CATEGORY = "Random Person" FUNCTION = "generate" OUTPUT_NODE = False DESCRIPTION = _NODE_DESCRIPTION - RETURN_NAMES = output_names + RETURN_NAMES = display RETURN_TYPES = tuple("INT" if n == "seed" else "STRING" for n in output_names) @classmethod @@ -575,6 +594,7 @@ try: def _make_v3_node(node_id, display_name, keys, include_age, output_names): key_set = set(keys) + display_pins = _display_names(node_id, output_names) class _RandomPersonV3(io.ComfyNode): @classmethod @@ -604,7 +624,7 @@ try: io.String.Input("extra_attributes", default="", multiline=True, placeholder="Add any extra details to pass into the description, e.g. wearing a red scarf, holding a coffee, tattoo on left arm", tooltip=_EXTRA_TIP)) outputs = [] - for name in output_names: + for name in display_pins: if name == "seed": outputs.append(io.Int.Output(id="seed", display_name="seed", tooltip=_SEED_TIP)) else: diff --git a/test_random_person.py b/test_random_person.py index d1702c8..9e5bfb5 100644 --- a/test_random_person.py +++ b/test_random_person.py @@ -290,6 +290,22 @@ class TestSegmentNodes(unittest.TestCase): for cls in core.NODE_CLASS_MAPPINGS.values(): self.assertEqual(cls.CATEGORY, "Random Person") + def test_first_pin_relabelled_per_node(self): + # Display label of the first pin is node-specific; internal value key stays "description". + expect = { + "RandomPersonNode": "full_description", + "RandomPersonIdentity": "full_identity_description", + "RandomPersonFace": "full_face_description", + "RandomPersonHair": "full_hair_description", + "RandomPersonBody": "full_body_description", + "RandomPersonStyle": "full_style_description", + } + for node_id, label in expect.items(): + self.assertEqual(core.NODE_CLASS_MAPPINGS[node_id].RETURN_NAMES[0], label, node_id) + # Module RETURN_NAMES (the value-lookup keys) is unchanged, so the first + # pin still resolves to the description value. + self.assertEqual(core.RETURN_NAMES[0], "description") + def test_identity_exposes_only_its_widgets(self): req = self._node("RandomPersonIdentity").INPUT_TYPES()["required"] self.assertIn("nationality_mode", req) @@ -314,8 +330,7 @@ class TestSegmentNodes(unittest.TestCase): elif name.endswith("_fixed"): kwargs[name] = "(none)" out = cls().generate(seed=3, randomize=False, sex="female", **kwargs) - names = cls.RETURN_NAMES - desc = out[names.index("description")] + desc = out[0] # first pin is always the full description self.assertNotIn("build", desc) # body excluded self.assertNotIn("hair", desc) # hair excluded self.assertNotIn("year old", desc) # age excluded