Repository navigation
Expand file tree
/
Copy pathperson_node.py
More file actions
176 lines (155 loc) · 8.64 KB
/
Copy pathperson_node.py
File metadata and controls
176 lines (155 loc) · 8.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""PerfectLab Persona: the persona composer.
Orthogonal fields, not baked phrases: gender, age, figure, hair, hair color,
eyes, makeup (multi), skin, a free signature line -- each with its own picks,
its own custom entries and its own preview where an asset exists. The text is
composed block by block and the second output (persona_data) hands the blocks
to the Shot Series so the description re-weights per framing.
A LoRA trigger sits in front: when set, the face-carrying blocks (hair, eyes,
makeup, skin) drop out of the text entirely -- the trigger wakes the LoRA and
the model supplies the face.
"""
import json
import re
HAIR = ["long platinum waves", "sleek high ponytail", "messy bun with loose strands",
"long tight braids", "beachy curls", "blunt shoulder-length bob",
"half-up half-down hair", "wet-look slicked back hair", "twin tails",
"space buns", "braided crown", "curtain bangs over loose hair",
"long straight hair with a middle part", "shaggy layered hair",
"voluminous blowout", "low chignon", "bubble braids", "crimped hair",
"wavy lob", "top knot", "fishtail braid", "wolf cut", "side-swept curls",
"pixie cut",
# afro-textured and modern
"an afro", "box braids", "cornrows", "locs",
"a buzz cut", "a french twist"]
# orthogonal fields -- key, label, options, multi, block it lands in
FIELDS = [
{"key": "gender", "label": "Gender", "options": ["a young woman", "a mature woman",
"a young man", "a mature man", "an androgynous person"], "multi": False},
{"key": "age", "label": "Age", "options": ["18 years old", "22 years old", "27 years old",
"in their early 30s", "ageless"], "multi": False},
{"key": "figure", "label": "Figure", "options": ["a very slim figure", "a slender figure",
"a lean toned figure", "an average build", "an athletic figure",
"a curvy hourglass figure", "a full curvy figure", "a plus-size figure",
"a petite build", "a tall statuesque build",
"a large bust", "a small bust", "wide hips", "narrow hips",
"a big round butt", "a flat butt", "thin arms", "muscular arms",
"a flat toned stomach", "a soft round belly", "broad shoulders",
"a wasp waist", "a broad back", "long legs", "muscular legs",
"narrow shoulders"], "multi": True},
{"key": "hair", "label": "Hair", "options": HAIR, "multi": False},
{"key": "hair_color", "label": "Hair color", "options": ["platinum blond", "honey blonde",
"chestnut brown", "jet black", "copper red", "silver gray", "pastel pink",
"dirty blonde with dark roots", "ash brown", "auburn", "blue-black",
"silver-lavender", "copper highlights", "two-tone ombre",
"money piece highlights", "strawberry blonde"], "multi": False},
{"key": "eyes", "label": "Eyes", "options": ["pale blue eyes", "deep brown eyes",
"emerald green eyes", "gray eyes", "amber eyes", "heterochromia",
"violet eyes", "hazel eyes", "very dark brown eyes", "grey-blue eyes"], "multi": False},
{"key": "makeup", "label": "Makeup", "options": [
"heavy black winged eyeliner", "dark smokey eyes", "sharp graphic liner",
"glitter eyeshadow", "electric blue eyeshadow", "purple winged liner",
"emerald green eyeshadow", "cut crease eyeshadow", "silver glitter liner",
"glossy pink lips", "bold red lipstick", "matte nude lips", "dark plum lipstick",
"coral orange lips", "black gothic lipstick", "glazed cherry lips",
"aubergine lipstick", "peach gloss lips", "berry stained lips",
"a no-makeup natural look",
# blush zone + trend wave
"dewy blush on the cheeks", "sun-kissed blush",
"sculpted contour with highlight", "cold girl makeup with frosted blush",
"siren eyes eyeliner", "glass skin glow"], "multi": True},
{"key": "skin", "label": "Skin", "options": ["porcelain skin", "sun-kissed skin",
"olive skin", "deep tan skin", "freckled skin", "deep ebony skin",
"golden brown skin", "cool ivory skin",
"beach tan lines", "body oil glow"], "multi": False},
{"key": "signature", "label": "Signature", "options": ["a tiny beauty mark under one eye",
"a thin gold nose ring", "an old scar over the left brow", "stacked thin bracelets",
"a single pearl necklace", "round wire-frame glasses",
"a small wrist tattoo", "a tiny nose stud", "a dimple in the left cheek",
"a beauty mark above the lip", "a slit in the left eyebrow",
"a bold signet ring on the index finger",
"a gap tooth", "a freckle constellation across the nose",
"a full tattoo sleeve on one arm"], "multi": True},
{"key": "nails", "label": "Nails", "options": [
"French tip manicure", "glossy red nails", "almond nude nails",
"black square-cut nails", "chrome nails", "pearl accent nails",
"milk-bath nails", "short natural nails"], "multi": False},
]
FIELD_BLOCK = {
"gender": "identity", "age": "identity", "figure": "figure",
"hair": "hair", "hair_color": "hair", "eyes": "face", "makeup": "face",
"skin": "skin", "signature": "signature",
"nails": "signature",
}
BLOCK_ORDER = ["identity", "figure", "hair", "face", "skin", "signature"]
FACE_BLOCKS = {"hair", "face", "skin"}
def _values(state, field):
"""picked values for a field: list of strings (custom entries included)"""
v = (state.get("fields") or {}).get(field["key"])
if v is None:
return []
vals = [v] if isinstance(v, str) else [str(x) for x in v if str(x).strip()]
if field["key"] == "age":
vals = [_floor_age(x) for x in vals]
return vals
# the kit describes adults: a typed age under 18 is floored, not rejected --
# the same guard the age presets already carry by starting at 18
_AGE_RE = re.compile(r"\b(\d{1,3})(?=\s*(?:years?[- ]old|yo\b))", re.I)
def _floor_age(text):
return _AGE_RE.sub(lambda m: str(max(int(m.group(1)), 18)), text)
def compose_blocks(state):
"""{block_key: text} from a persona state; trigger leads, face blocks drop"""
trigger = (state.get("trigger") or "").strip()
signature_text = (state.get("signature_text") or "").strip()
blocks = {}
for f in FIELDS:
vals = _values(state, f)
if not vals:
continue
block = FIELD_BLOCK[f["key"]]
text = ", ".join(vals)
if f["key"] == "skin":
text = "with " + text
# every block accumulates: two fields share most blocks (hair +
# hair_color, eyes + makeup, signature + nails), and the old
# identity-only accumulation silently dropped every field but the
# last of a block -- a picked hairstyle vanished the moment a
# hair color was picked next to it
prev = blocks.get(block, "")
blocks[block] = (prev + ", " + text).strip(", ")
if signature_text:
# the free-text signature rides WITH the signature chips, it does
# not replace them
prev = blocks.get("signature", "")
blocks["signature"] = (prev + ", " + signature_text).strip(", ")
if trigger:
for k in list(blocks):
if k in FACE_BLOCKS:
del blocks[k]
ident = blocks.get("identity", "")
blocks["identity"] = f"{ident} ({trigger})" if ident else f"a person ({trigger})"
return blocks
class PerfectLabPersona:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"persona_data": ("STRING", {"default": "{}", "multiline": False}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("person", "persona_data")
FUNCTION = "build"
CATEGORY = "PerfectLab"
DESCRIPTION = "Builds the person once, out of orthogonal fields: gender, age, figure, hair, hair color, eyes, makeup, skin and a signature detail. Every field takes a library pick or your own text. The text is composed block by block, and persona_data hands the blocks to the Shot Series, which re-weights the description per framing -- close-ups keep the face, wide shots keep the figure. A LoRA trigger drops the face fields and lets the LoRA draw the face."
def build(self, persona_data=None):
try:
state = json.loads(persona_data) if persona_data else {}
except (ValueError, TypeError):
state = {}
blocks = compose_blocks(state)
text = ", ".join(blocks[k] for k in BLOCK_ORDER if k in blocks)
payload = json.dumps({"blocks": blocks, "trigger": state.get("trigger", "")},
ensure_ascii=False)
return (text, payload)
NODE_CLASS_MAPPINGS = {"PerfectLabPersona": PerfectLabPersona}
NODE_DISPLAY_NAME_MAPPINGS = {"PerfectLabPersona": "🧪 PerfectLab – Persona"}