501 lines
22 KiB
Python
501 lines
22 KiB
Python
"""
|
|
Random Person Description Node for ComfyUI
|
|
==========================================
|
|
|
|
Produces a clean, consistently structured person description, e.g.:
|
|
|
|
34 year old Brazilian female, heart face, warm beige complexion,
|
|
hazel eyes, button nose, bow-shaped lips, auburn long hair worn
|
|
loose waves, hourglass build
|
|
|
|
Each category also has its own individual output pin.
|
|
|
|
Per-category MODE:
|
|
"random" - pick any entry from the (sex-appropriate) full list
|
|
"allow_list" - comma-separated label subset (blank = all)
|
|
"fixed" - use the exact label selected in the dropdown
|
|
"off" - omit this category entirely
|
|
|
|
Age uses numeric age_min / age_max (0 = no bound) or age_fixed.
|
|
|
|
Dual-API: the shared core (generate_person) is wrapped by a V1 class
|
|
(NODE_CLASS_MAPPINGS, authoritative on the 0.25.0 if/elif loader) and an
|
|
import-guarded V3 class (comfy_entrypoint) for builds whose loader prefers
|
|
the comfy_api schema. node_id "RandomPersonNode" is fixed in both so the
|
|
js/ sex-aware widget extension keeps matching.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
from functools import lru_cache
|
|
|
|
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
|
|
MODES = ["random", "allow_list", "fixed", "off"]
|
|
SEX_MODES = ["random", "male", "female"]
|
|
AGE_MODES = ["random", "fixed", "off"]
|
|
|
|
|
|
# -- Data helpers --------------------------------------------------------------
|
|
|
|
@lru_cache(maxsize=None)
|
|
def load(subdir, filename):
|
|
# Parsed once per process. Callers must treat the result as read-only.
|
|
# Matches the documented "edit data, restart ComfyUI" workflow.
|
|
with open(os.path.join(DATA_DIR, subdir, filename), encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
def load_shared(fn): return load("shared", fn)
|
|
def load_sex(sex, fn): return load(sex, fn)
|
|
|
|
|
|
def _labels(data):
|
|
"""Extract label list from a JSON data list."""
|
|
return [item["label"] if isinstance(item, dict) else item for item in data]
|
|
|
|
def _union(*lists):
|
|
"""Merge multiple label lists, deduplicated and sorted."""
|
|
seen, out = set(), []
|
|
for lst in lists:
|
|
for x in lst:
|
|
if x not in seen:
|
|
seen.add(x)
|
|
out.append(x)
|
|
return sorted(out)
|
|
|
|
|
|
# Build dropdown option lists once at import time.
|
|
_NAT_LABELS = ["(none)"] + _labels(load_shared("nationality.json"))
|
|
_COMP_LABELS = ["(none)"] + _labels(load_shared("complexion.json"))
|
|
_EYES_LABELS = ["(none)"] + _labels(load_shared("eyes.json"))
|
|
|
|
# Sex-gated lists: union of male + female so the dropdown is valid for any sex.
|
|
_HAIR_COLOR_LABELS = ["(none)"] + _union(_labels(load_sex("male", "hair_color.json")),
|
|
_labels(load_sex("female", "hair_color.json")))
|
|
_HAIR_STYLE_LABELS = ["(none)"] + _union(_labels(load_sex("male", "hair_style.json")),
|
|
_labels(load_sex("female", "hair_style.json")))
|
|
_HAIR_LENGTH_LABELS = ["(none)"] + _union(_labels(load_sex("male", "hair_length.json")),
|
|
_labels(load_sex("female", "hair_length.json")))
|
|
_BODY_TYPE_LABELS = ["(none)"] + _union(_labels(load_sex("male", "body_type.json")),
|
|
_labels(load_sex("female", "body_type.json")))
|
|
_FACE_SHAPE_LABELS = ["(none)"] + _union(_labels(load_sex("male", "face_shape.json")),
|
|
_labels(load_sex("female", "face_shape.json")))
|
|
_NOSE_SHAPE_LABELS = ["(none)"] + _union(_labels(load_sex("male", "nose_shape.json")),
|
|
_labels(load_sex("female", "nose_shape.json")))
|
|
_MOUTH_SHAPE_LABELS = ["(none)"] + _union(_labels(load_sex("male", "mouth_shape.json")),
|
|
_labels(load_sex("female", "mouth_shape.json")))
|
|
_FACE_FEATURE_LABELS = ["(none)"] + _union(_labels(load_sex("male", "face_feature.json")),
|
|
_labels(load_sex("female", "face_feature.json")))
|
|
_FACIAL_HAIR_LABELS = ["(none)"] + _union(_labels(load_sex("male", "facial_hair.json")),
|
|
_labels(load_sex("female", "facial_hair.json")))
|
|
_MAKEUP_LABELS = ["(none)"] + _union(_labels(load_sex("male", "makeup.json")),
|
|
_labels(load_sex("female", "makeup.json")))
|
|
|
|
# Shared (sex-agnostic) lists.
|
|
_SKIN_TEXTURE_LABELS = ["(none)"] + _labels(load_shared("skin_texture.json"))
|
|
_ACCESSORIES_LABELS = ["(none)"] + _labels(load_shared("accessories.json"))
|
|
_EXPRESSION_LABELS = ["(none)"] + _labels(load_shared("expression.json"))
|
|
|
|
# Per-category spec drives BOTH API wrappers so they cannot drift.
|
|
# (key, fixed_label_list, allow_list_placeholder)
|
|
CATEGORY_SPECS = [
|
|
("nationality", _NAT_LABELS, "e.g. American, Japanese, Italian"),
|
|
("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"),
|
|
("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"),
|
|
("face_feature", _FACE_FEATURE_LABELS, "e.g. high cheekbones, dimples, freckles"),
|
|
("hair_color", _HAIR_COLOR_LABELS, "e.g. jet black, auburn, platinum blonde"),
|
|
("hair_style", _HAIR_STYLE_LABELS, "e.g. bun, braids, pixie cut"),
|
|
("hair_length", _HAIR_LENGTH_LABELS, "e.g. short, medium, long"),
|
|
("facial_hair", _FACIAL_HAIR_LABELS, "e.g. full beard, stubble, goatee"),
|
|
("body_type", _BODY_TYPE_LABELS, "e.g. athletic, slim, curvy, stocky"),
|
|
("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"),
|
|
]
|
|
|
|
# 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"}
|
|
|
|
# Optional "flair" categories default to mode "off" so the base description stays
|
|
# 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",
|
|
"expression", "accessories", "makeup"}
|
|
|
|
|
|
def _mode_default(key):
|
|
return "off" if key in DEFAULT_OFF else "random"
|
|
|
|
# Tooltips (shared by both APIs).
|
|
_SEED_TIP = "Seed for reproducibility. With randomize off, the same seed reproduces the same person."
|
|
_RAND_TIP = "On: pick a new random person every run, ignoring the seed widget. Off: use the seed."
|
|
_SEX_TIP = "random, male, or female. male/female filter the sex-specific dropdowns in the UI."
|
|
_MODE_TIP = "How this attribute is chosen: random (any), allow_list (subset you list), fixed (the dropdown value), off (omit it)."
|
|
_ALLOW_TIP = "Comma-separated values used when mode=allow_list. Unknown values are added as literal descriptors."
|
|
_FIXED_TIP = "Exact value used when mode=fixed. (none) falls back to random."
|
|
_AGEMIN_TIP = "Minimum age. 0 = no lower bound (defaults to 21). Cannot go below 21."
|
|
_AGEMAX_TIP = "Maximum age. 0 = no upper bound (defaults to 90). Minimum is always 21."
|
|
_AGEFIX_TIP = "Exact age when mode=fixed. 0 = ignored. Values below 21 are clamped to 21."
|
|
_EXTRA_TIP = "Free text appended to the description as-is, comma-separated. Clothing, accessories, expression, etc."
|
|
|
|
RETURN_NAMES = ("description", "sex", "age", "nationality", "complexion",
|
|
"face", "hair", "facial_hair", "body_type",
|
|
"expression", "accessories", "makeup", "seed")
|
|
|
|
|
|
# -- Selection logic -----------------------------------------------------------
|
|
|
|
def item_matches(item, token):
|
|
"""
|
|
Fuzzy multi-field match (case-insensitive).
|
|
Priority: exact (2) > token-in-field or field-in-token (1) > no match (0).
|
|
"""
|
|
if isinstance(item, dict):
|
|
fields = [item.get("label", ""), item.get("descriptor", ""),
|
|
item.get("from", ""), item.get("description", "")]
|
|
else:
|
|
fields = [str(item)]
|
|
|
|
fields = [f.lower() for f in fields if f]
|
|
for field in fields:
|
|
if token == field: return 2
|
|
for field in fields:
|
|
if token in field: return 1 # "america" in "american"
|
|
if field in token: return 1 # "french" in "french canadian"
|
|
return 0
|
|
|
|
|
|
def _best_match(full_list, token):
|
|
"""Return the best-matching item for token, or None."""
|
|
scored = [(item_matches(item, token), item) for item in full_list]
|
|
scored = [(s, item) for s, item in scored if s > 0]
|
|
if not scored:
|
|
return None
|
|
best_score = max(s for s, _ in scored)
|
|
candidates = [item for s, item in scored if s == best_score]
|
|
# Among equal-score candidates, prefer shortest label (most specific match).
|
|
if isinstance(candidates[0], dict):
|
|
candidates.sort(key=lambda x: len(x.get("label", "")))
|
|
return candidates[0]
|
|
|
|
|
|
def resolve(mode, full_list, allow_str="", fixed_val=""):
|
|
"""Pick one item from full_list according to mode. Returns None if off."""
|
|
if mode == "off":
|
|
return None
|
|
|
|
if mode == "fixed" and fixed_val.strip() and fixed_val.strip() != "(none)":
|
|
token = fixed_val.strip().lower()
|
|
match = _best_match(full_list, token)
|
|
if match is not None:
|
|
return match
|
|
print(f"[RandomPersonNode] WARNING: fixed '{fixed_val}' not found - using random.")
|
|
|
|
if mode == "allow_list" and allow_str.strip():
|
|
tokens = [s.strip() for s in allow_str.split(",") if s.strip()]
|
|
|
|
matched_items, custom_strings = [], []
|
|
for t in tokens:
|
|
tl = t.lower()
|
|
hits = [item for item in full_list if item_matches(item, tl) > 0]
|
|
if hits:
|
|
matched_items.extend(hits)
|
|
else:
|
|
custom_strings.append(t) # not in JSON - treat as literal custom value
|
|
|
|
seen, deduped = set(), []
|
|
for item in matched_items:
|
|
key = id(item)
|
|
if key not in seen:
|
|
seen.add(key)
|
|
deduped.append(item)
|
|
|
|
pool = deduped + [{"label": s, "description": s} for s in custom_strings]
|
|
if pool:
|
|
return random.choice(pool)
|
|
print(f"[RandomPersonNode] WARNING: allow_list '{allow_str}' matched nothing - using random.")
|
|
|
|
return random.choice(full_list)
|
|
|
|
|
|
def resolve_age(mode, age_min, age_max, age_fixed_val, all_ages):
|
|
if mode == "off":
|
|
return None
|
|
if mode == "fixed" and age_fixed_val > 0:
|
|
age_fixed_val = max(age_fixed_val, 21)
|
|
exact = [a for a in all_ages if a["age"] == age_fixed_val]
|
|
if exact:
|
|
return exact[0]
|
|
print(f"[RandomPersonNode] WARNING: age {age_fixed_val} not in data - using random.")
|
|
lo = max(age_min, 21) if age_min > 0 else 21
|
|
hi = min(age_max, 90) if age_max > 0 else 90
|
|
if lo > hi:
|
|
lo, hi = hi, lo
|
|
pool = [a for a in all_ages if lo <= a["age"] <= hi]
|
|
if not pool:
|
|
print(f"[RandomPersonNode] WARNING: no ages in {lo}-{hi} - using full list.")
|
|
pool = all_ages
|
|
return random.choice(pool)
|
|
|
|
|
|
def d(item, key="description"):
|
|
"""Extract a string from a dict item, or the item itself."""
|
|
return item.get(key, "") if isinstance(item, dict) else str(item)
|
|
|
|
|
|
def nat_text(item):
|
|
"""Nationality display text. Falls back when a custom allow_list value has
|
|
no 'descriptor' key (custom items carry only label/description)."""
|
|
if not item:
|
|
return ""
|
|
if isinstance(item, dict):
|
|
return item.get("descriptor") or item.get("description") or item.get("label") or ""
|
|
return str(item)
|
|
|
|
|
|
# -- Description builder -------------------------------------------------------
|
|
|
|
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=""):
|
|
"""Produce a single comma-separated descriptor string."""
|
|
parts = []
|
|
|
|
def add(item, fmt="{}"):
|
|
"""Append a category's description if it resolves to non-empty text."""
|
|
s = d(item).strip() if item else ""
|
|
if s:
|
|
parts.append(fmt.format(s))
|
|
|
|
# Opener: "34 year old Brazilian female"
|
|
age_str = f"{age['age']} year old" if age else None
|
|
nat_str = nat_text(nat) or None
|
|
sex_str = sex if sex else None
|
|
opener_parts = [p for p in [age_str, nat_str, sex_str] if p]
|
|
if opener_parts:
|
|
parts.append(" ".join(opener_parts))
|
|
|
|
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")
|
|
add(nose_shape)
|
|
add(mouth_shape)
|
|
add(face_feature)
|
|
|
|
# Hair: "auburn long hair worn loose waves"
|
|
hair_color_str = d(hair_color) if hair_color else None
|
|
hair_length_str = d(hair_length) if hair_length else None
|
|
hair_style_str = d(hair_style) if hair_style else None
|
|
if hair_color_str or hair_length_str or hair_style_str:
|
|
pre_hair = " ".join(p for p in [hair_color_str, hair_length_str] if p)
|
|
if pre_hair and hair_style_str:
|
|
parts.append(f"{pre_hair} hair worn {hair_style_str}")
|
|
elif pre_hair:
|
|
parts.append(f"{pre_hair} hair")
|
|
elif hair_style_str:
|
|
parts.append(f"hair worn {hair_style_str}")
|
|
|
|
add(facial_hair)
|
|
|
|
if body:
|
|
build_str = d(body)
|
|
parts.append(build_str if build_str.lower().endswith("build") else f"{build_str} build")
|
|
|
|
add(expression)
|
|
add(accessories)
|
|
add(makeup)
|
|
|
|
if extra_attributes and extra_attributes.strip():
|
|
for attr in extra_attributes.split(","):
|
|
attr = attr.strip().strip(".")
|
|
if attr:
|
|
parts.append(attr)
|
|
|
|
return ", ".join(parts)
|
|
|
|
|
|
# -- Shared core (called by both API wrappers) ---------------------------------
|
|
|
|
def generate_person(seed, randomize, sex, category_args,
|
|
age_mode, age_min, age_max, age_fixed, extra_attributes):
|
|
"""
|
|
category_args: dict keyed by category prefix with (mode, allow_list, fixed)
|
|
tuples for each entry in CATEGORY_SPECS. Returns the output tuple matching
|
|
RETURN_NAMES.
|
|
"""
|
|
if randomize:
|
|
seed = random.randint(0, 0xFFFFFFFF)
|
|
random.seed(seed)
|
|
|
|
resolved_sex = sex if sex in ("male", "female") else random.choice(["male", "female"])
|
|
|
|
def load_list(key):
|
|
sub = resolved_sex if key in SEX_GATED else "shared"
|
|
return load(sub, f"{key}.json")
|
|
|
|
p = {}
|
|
for key, _, _ in CATEGORY_SPECS:
|
|
mode, allow, fixed = category_args[key]
|
|
p[key] = resolve(mode, load_list(key), allow, fixed)
|
|
age = resolve_age(age_mode, age_min, age_max, age_fixed, load_shared("age.json"))
|
|
|
|
description = build_description(
|
|
resolved_sex, p["nationality"], age, p["skin_texture"],
|
|
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)
|
|
|
|
def dd(key, sub_key="description"):
|
|
item = p[key]
|
|
return d(item, sub_key) if item else ""
|
|
|
|
out_age = str(age["age"]) if age else ""
|
|
out_nat = nat_text(p["nationality"])
|
|
out_comp = ", ".join(filter(None, [dd("complexion"), dd("skin_texture")]))
|
|
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 ""
|
|
|
|
out_face = ", ".join(filter(None, [
|
|
f"{dd('face_shape')} face" if dd("face_shape") else "",
|
|
_eyes_str(),
|
|
dd("nose_shape"),
|
|
dd("mouth_shape"),
|
|
dd("face_feature"),
|
|
]))
|
|
|
|
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"), seed)
|
|
|
|
|
|
def _collect_category_args(kwargs):
|
|
"""Pull the (mode, allow_list, fixed) triple for each category from kwargs."""
|
|
return {
|
|
key: (kwargs[f"{key}_mode"], kwargs[f"{key}_allow_list"], kwargs[f"{key}_fixed"])
|
|
for key, _, _ in CATEGORY_SPECS
|
|
}
|
|
|
|
|
|
# -- V1 node (legacy dict API) -------------------------------------------------
|
|
|
|
class RandomPersonNode:
|
|
|
|
CATEGORY = "utils"
|
|
FUNCTION = "generate"
|
|
OUTPUT_NODE = False
|
|
DESCRIPTION = "Generate a randomised, structured physical person description for image prompts."
|
|
|
|
RETURN_TYPES = ("STRING",) * (len(RETURN_NAMES) - 1) + ("INT",)
|
|
RETURN_NAMES = RETURN_NAMES
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
required = {
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF, "tooltip": _SEED_TIP}),
|
|
"randomize": ("BOOLEAN", {"default": True, "tooltip": _RAND_TIP}),
|
|
"sex": (SEX_MODES, {"default": "random", "tooltip": _SEX_TIP}),
|
|
}
|
|
for key, fixed_labels, placeholder in CATEGORY_SPECS:
|
|
required[f"{key}_mode"] = (MODES, {"default": _mode_default(key), "tooltip": _MODE_TIP})
|
|
required[f"{key}_allow_list"] = ("STRING", {"default": "", "multiline": False,
|
|
"placeholder": placeholder, "tooltip": _ALLOW_TIP})
|
|
required[f"{key}_fixed"] = (fixed_labels, {"default": "(none)", "tooltip": _FIXED_TIP})
|
|
required["age_mode"] = (AGE_MODES, {"default": "random"})
|
|
required["age_min"] = ("INT", {"default": 0, "min": 0, "max": 90, "tooltip": _AGEMIN_TIP})
|
|
required["age_max"] = ("INT", {"default": 0, "min": 0, "max": 90, "tooltip": _AGEMAX_TIP})
|
|
required["age_fixed"] = ("INT", {"default": 0, "min": 0, "max": 90, "tooltip": _AGEFIX_TIP})
|
|
required["extra_attributes"] = ("STRING", {"default": "", "multiline": True,
|
|
"placeholder": "e.g. wearing glasses, tattoo on left arm",
|
|
"tooltip": _EXTRA_TIP})
|
|
return {"required": required}
|
|
|
|
def generate(self, seed, randomize, sex, age_mode, age_min, age_max,
|
|
age_fixed, extra_attributes, **kwargs):
|
|
return generate_person(seed, randomize, sex, _collect_category_args(kwargs),
|
|
age_mode, age_min, age_max, age_fixed, extra_attributes)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, randomize, seed, **kwargs):
|
|
# randomize=True -> always re-run; otherwise re-run only when seed changes.
|
|
return float("nan") if randomize else seed
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {"RandomPersonNode": RandomPersonNode}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {"RandomPersonNode": "Random Person Description"}
|
|
|
|
|
|
# -- V3 node (import-guarded comfy_api schema) ---------------------------------
|
|
|
|
try:
|
|
from comfy_api.v0_0_2 import io, ComfyExtension
|
|
|
|
class RandomPersonNodeV3(io.ComfyNode):
|
|
@classmethod
|
|
def define_schema(cls) -> io.Schema:
|
|
inputs = [
|
|
io.Int.Input("seed", default=0, min=0, max=0xFFFFFFFF, tooltip=_SEED_TIP),
|
|
io.Boolean.Input("randomize", default=True, tooltip=_RAND_TIP),
|
|
io.Combo.Input("sex", options=SEX_MODES, default="random", tooltip=_SEX_TIP),
|
|
]
|
|
for key, fixed_labels, placeholder in CATEGORY_SPECS:
|
|
inputs += [
|
|
io.Combo.Input(f"{key}_mode", options=MODES, default=_mode_default(key), tooltip=_MODE_TIP),
|
|
io.String.Input(f"{key}_allow_list", default="", multiline=False,
|
|
placeholder=placeholder, tooltip=_ALLOW_TIP),
|
|
io.Combo.Input(f"{key}_fixed", options=fixed_labels, default="(none)", tooltip=_FIXED_TIP),
|
|
]
|
|
inputs += [
|
|
io.Combo.Input("age_mode", options=AGE_MODES, default="random"),
|
|
io.Int.Input("age_min", default=0, min=0, max=90, tooltip=_AGEMIN_TIP),
|
|
io.Int.Input("age_max", default=0, min=0, max=90, tooltip=_AGEMAX_TIP),
|
|
io.Int.Input("age_fixed", default=0, min=0, max=90, tooltip=_AGEFIX_TIP),
|
|
io.String.Input("extra_attributes", default="", multiline=True,
|
|
placeholder="e.g. wearing glasses, tattoo on left arm", tooltip=_EXTRA_TIP),
|
|
]
|
|
outputs = [io.String.Output(id=n, display_name=n) for n in RETURN_NAMES[:-1]]
|
|
outputs.append(io.Int.Output(id="seed", display_name="seed", tooltip=_SEED_TIP))
|
|
return io.Schema(
|
|
node_id="RandomPersonNode",
|
|
display_name="Random Person Description",
|
|
category="utils",
|
|
description=RandomPersonNode.DESCRIPTION,
|
|
inputs=inputs,
|
|
outputs=outputs,
|
|
)
|
|
|
|
@classmethod
|
|
def fingerprint_inputs(cls, randomize, seed, **kwargs):
|
|
return float("nan") if randomize else seed
|
|
|
|
@classmethod
|
|
def execute(cls, seed, randomize, sex, age_mode, age_min, age_max,
|
|
age_fixed, extra_attributes, **kwargs) -> io.NodeOutput:
|
|
result = generate_person(seed, randomize, sex, _collect_category_args(kwargs),
|
|
age_mode, age_min, age_max, age_fixed, extra_attributes)
|
|
return io.NodeOutput(*result)
|
|
|
|
class RandomPersonExtension(ComfyExtension):
|
|
async def get_node_list(self):
|
|
return [RandomPersonNodeV3]
|
|
|
|
async def comfy_entrypoint() -> "RandomPersonExtension":
|
|
return RandomPersonExtension()
|
|
|
|
except ImportError:
|
|
# Older ComfyUI without comfy_api: V1 mappings above are the only path.
|
|
pass
|