feat: per-node full_*_description first pin labels; README AIO height note

This commit is contained in:
BRADSEC
2026-06-21 22:36:19 +10:00
parent 71b678bd72
commit 7271b38878
3 changed files with 43 additions and 6 deletions
+4 -2
View File
@@ -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_<group>_description`) | `43 year old Russian female, angular face, ...` |
| `sex` | Resolved sex | `female` |
| `age` | Age as a plain number | `43` |
| `nationality` | Nationality descriptor | `Russian` |
+22 -2
View File
@@ -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:
+17 -2
View File
@@ -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