- All field labels, option values, and UI strings are now English (previously mixed German/English after local edits) - Remove the SDXLHumanPromptBuilder legacy alias/mapping - Rewrite README for the current Human Composer feature set: grouped fields, four output formats, custom values, and a short how-to Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
504 lines
19 KiB
Python
504 lines
19 KiB
Python
"""Human Composer - model-aware prompt builder for ComfyUI.
|
||
|
||
Creates a structured character description and renders it for SDXL, FLUX,
|
||
or Pony-style checkpoints without requiring an LLM.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Dict, Iterable, List, Sequence, Tuple
|
||
|
||
|
||
EMPTY_OPTION = "— not set —"
|
||
CUSTOM_OPTION = "Custom value…"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class FieldSpec:
|
||
key: str
|
||
label: str
|
||
options: Sequence[Tuple[str, str]]
|
||
group: str
|
||
|
||
@property
|
||
def display_options(self) -> List[str]:
|
||
return [display for display, _value in self.options]
|
||
|
||
@property
|
||
def value_map(self) -> Dict[str, str]:
|
||
return dict(self.options)
|
||
|
||
|
||
def opt(display: str, value: str) -> Tuple[str, str]:
|
||
"""Small helper to keep field declarations readable."""
|
||
return display, value
|
||
|
||
|
||
FIELDS: Sequence[FieldSpec] = [
|
||
FieldSpec("gender", "Gender", [
|
||
opt("Woman", "woman"),
|
||
opt("Man", "man"),
|
||
opt("Non-binary person", "non-binary person"),
|
||
], "subject"),
|
||
FieldSpec("age", "Age", [
|
||
opt("Child", "child"),
|
||
opt("Teenager", "teenager"),
|
||
opt("Young adult", "young adult"),
|
||
opt("Adult", "adult"),
|
||
opt("Middle-aged adult", "middle-aged adult"),
|
||
opt("Elderly person", "elderly person"),
|
||
], "subject"),
|
||
FieldSpec("ethnicity", "Ethnicity", [
|
||
opt("Caucasian / European", "Caucasian"),
|
||
opt("Black / African", "Black/African"),
|
||
opt("East Asian", "East Asian"),
|
||
opt("South Asian", "South Asian"),
|
||
opt("Southeast Asian", "Southeast Asian"),
|
||
opt("Middle Eastern", "Middle Eastern"),
|
||
opt("Hispanic / Latino", "Hispanic/Latino"),
|
||
opt("Indigenous", "Indigenous"),
|
||
opt("Mixed ethnicity", "mixed ethnicity"),
|
||
], "subject"),
|
||
FieldSpec("body_type", "Body Type", [
|
||
opt("Slim", "slim"),
|
||
opt("Athletic", "athletic"),
|
||
opt("Average", "average build"),
|
||
opt("Curvy", "curvy"),
|
||
opt("Plus-size", "plus-size"),
|
||
opt("Muscular", "muscular"),
|
||
opt("Petite", "petite"),
|
||
opt("Tall and lean", "tall and lean"),
|
||
], "appearance"),
|
||
FieldSpec("skin_tone", "Skin Tone", [
|
||
opt("Very pale", "pale"),
|
||
opt("Fair", "fair"),
|
||
opt("Light tan", "light tan"),
|
||
opt("Medium", "medium"),
|
||
opt("Olive", "olive"),
|
||
opt("Tan", "tan"),
|
||
opt("Brown", "brown"),
|
||
opt("Dark brown", "dark brown"),
|
||
opt("Very dark", "deep ebony"),
|
||
], "appearance"),
|
||
FieldSpec("facial_expression", "Facial Expression", [
|
||
opt("Smiling", "smiling"),
|
||
opt("Laughing", "laughing"),
|
||
opt("Neutral", "neutral expression"),
|
||
opt("Serious", "serious expression"),
|
||
opt("Surprised", "surprised"),
|
||
opt("Thoughtful", "thoughtful"),
|
||
opt("Confident", "confident expression"),
|
||
opt("Shy", "shy expression"),
|
||
opt("Angry", "angry"),
|
||
opt("Sad", "sad"),
|
||
], "appearance"),
|
||
FieldSpec("eye_color", "Eye Color", [
|
||
opt("Blue", "blue eyes"),
|
||
opt("Green", "green eyes"),
|
||
opt("Brown", "brown eyes"),
|
||
opt("Hazel", "hazel eyes"),
|
||
opt("Gray", "gray eyes"),
|
||
opt("Amber", "amber eyes"),
|
||
], "appearance"),
|
||
|
||
# Hair is deliberately split into independent dimensions.
|
||
FieldSpec("hair_length", "Hair Length", [
|
||
opt("Buzzed / stubble", "buzzed"),
|
||
opt("Very short", "very short"),
|
||
opt("Chin-length", "chin-length"),
|
||
opt("Shoulder-length", "shoulder-length"),
|
||
opt("Mid-back length", "mid-back length"),
|
||
opt("Waist-length", "waist-length"),
|
||
], "hair"),
|
||
FieldSpec("hair_texture", "Hair Texture", [
|
||
opt("Straight", "straight"),
|
||
opt("Slightly wavy", "slightly wavy"),
|
||
opt("Wavy", "wavy"),
|
||
opt("Curly", "curly"),
|
||
opt("Coily", "coily"),
|
||
], "hair"),
|
||
FieldSpec("hair_style", "Hair Style", [
|
||
opt("Loose", "loose hair"),
|
||
opt("Slicked-back", "slicked-back hair"),
|
||
opt("Bob", "bob haircut"),
|
||
opt("Pixie cut", "pixie cut"),
|
||
opt("Ponytail", "ponytail"),
|
||
opt("High ponytail", "high ponytail"),
|
||
opt("Low ponytail", "low ponytail"),
|
||
opt("Bun", "bun"),
|
||
opt("Elegant updo", "elegant updo"),
|
||
opt("Braided", "braided hair"),
|
||
opt("Afro", "afro"),
|
||
opt("Bald", "bald"),
|
||
], "hair"),
|
||
FieldSpec("hair_parting", "Hair Parting", [
|
||
opt("Center part", "center part"),
|
||
opt("Side part", "side part"),
|
||
opt("Deep side part", "deep side part"),
|
||
opt("No visible part", "no visible part"),
|
||
], "hair"),
|
||
FieldSpec("bangs", "Bangs", [
|
||
opt("Blunt bangs", "blunt bangs"),
|
||
opt("Curtain bangs", "curtain bangs"),
|
||
opt("Side-swept bangs", "side-swept bangs"),
|
||
opt("Wispy bangs", "wispy bangs"),
|
||
opt("No bangs", "no bangs"),
|
||
], "hair"),
|
||
FieldSpec("hair_color", "Hair Color", [
|
||
opt("Black", "black hair"),
|
||
opt("Dark brown", "dark brown hair"),
|
||
opt("Brown", "brown hair"),
|
||
opt("Blonde", "blonde hair"),
|
||
opt("Red", "red hair"),
|
||
opt("Auburn", "auburn hair"),
|
||
opt("Silver-gray", "silver-gray hair"),
|
||
opt("White", "white hair"),
|
||
opt("Vividly dyed", "vividly dyed hair"),
|
||
], "hair"),
|
||
FieldSpec("facial_hair", "Facial Hair", [
|
||
opt("Clean-shaven", "clean-shaven"),
|
||
opt("Stubble", "stubble"),
|
||
opt("Mustache", "mustache"),
|
||
opt("Goatee", "goatee"),
|
||
opt("Full beard", "full beard"),
|
||
], "appearance"),
|
||
|
||
# Clothing composition.
|
||
FieldSpec("clothing_category", "Clothing Category", [
|
||
opt("Casual wear", "casual wear"),
|
||
opt("Business", "business attire"),
|
||
opt("Formal evening wear", "formal evening wear"),
|
||
opt("Sportswear", "sportswear"),
|
||
opt("Historical clothing", "historical clothing"),
|
||
opt("Fantasy", "fantasy clothing"),
|
||
opt("Science fiction", "science-fiction clothing"),
|
||
opt("Uniform", "uniform"),
|
||
opt("Winter clothing", "winter clothing"),
|
||
opt("Swimwear", "swimwear"),
|
||
], "clothing"),
|
||
FieldSpec("main_garment", "Main Garment", [
|
||
opt("T-shirt", "T-shirt"),
|
||
opt("Blouse", "blouse"),
|
||
opt("Shirt", "shirt"),
|
||
opt("Sweater", "sweater"),
|
||
opt("Jacket", "jacket"),
|
||
opt("Blazer", "blazer"),
|
||
opt("Tailored suit", "tailored suit"),
|
||
opt("Short dress", "short dress"),
|
||
opt("Sheath dress", "sheath dress"),
|
||
opt("Cocktail dress", "cocktail dress"),
|
||
opt("Evening gown", "floor-length evening gown"),
|
||
opt("Skirt", "skirt"),
|
||
opt("Trousers", "trousers"),
|
||
opt("Jeans", "jeans"),
|
||
opt("Armor", "armor"),
|
||
opt("Robe", "robe"),
|
||
], "clothing"),
|
||
FieldSpec("clothing_material", "Clothing Material", [
|
||
opt("Cotton", "cotton"),
|
||
opt("Linen", "linen"),
|
||
opt("Wool", "wool"),
|
||
opt("Leather", "leather"),
|
||
opt("Suede", "suede"),
|
||
opt("Silk", "silk"),
|
||
opt("Satin", "satin"),
|
||
opt("Velvet", "velvet"),
|
||
opt("Lace", "lace"),
|
||
opt("Denim", "denim"),
|
||
opt("Metal", "metal"),
|
||
], "clothing"),
|
||
FieldSpec("clothing_fit", "Clothing Fit", [
|
||
opt("Figure-hugging", "figure-hugging"),
|
||
opt("Tailored", "tailored"),
|
||
opt("Loose-fitting", "loose-fitting"),
|
||
opt("Flowing", "flowing"),
|
||
opt("Oversized", "oversized"),
|
||
], "clothing"),
|
||
FieldSpec("clothing_color", "Clothing Color", [
|
||
opt("Black", "black"), opt("White", "white"), opt("Red", "red"),
|
||
opt("Burgundy", "burgundy"), opt("Blue", "blue"),
|
||
opt("Navy blue", "navy blue"), opt("Green", "green"),
|
||
opt("Gold", "gold"), opt("Silver", "silver"),
|
||
opt("Beige", "beige"), opt("Brown", "brown"), opt("Pink", "pink"),
|
||
], "clothing"),
|
||
|
||
FieldSpec("accessory_1", "Accessory 1", [
|
||
opt("Glasses", "glasses"), opt("Sunglasses", "sunglasses"),
|
||
opt("Earrings", "earrings"), opt("Necklace", "necklace"),
|
||
opt("Hat", "hat"), opt("Scarf", "scarf"), opt("Watch", "watch"),
|
||
opt("Gloves", "gloves"), opt("Handbag", "handbag"),
|
||
opt("Sword", "sword"), opt("Shield", "shield"),
|
||
], "accessories"),
|
||
FieldSpec("accessory_2", "Accessory 2", [
|
||
opt("Glasses", "glasses"), opt("Sunglasses", "sunglasses"),
|
||
opt("Earrings", "earrings"), opt("Necklace", "necklace"),
|
||
opt("Hat", "hat"), opt("Scarf", "scarf"), opt("Watch", "watch"),
|
||
opt("Gloves", "gloves"), opt("Handbag", "handbag"),
|
||
opt("Sword", "sword"), opt("Shield", "shield"),
|
||
], "accessories"),
|
||
FieldSpec("accessory_3", "Accessory 3", [
|
||
opt("Glasses", "glasses"), opt("Sunglasses", "sunglasses"),
|
||
opt("Earrings", "earrings"), opt("Necklace", "necklace"),
|
||
opt("Hat", "hat"), opt("Scarf", "scarf"), opt("Watch", "watch"),
|
||
opt("Gloves", "gloves"), opt("Handbag", "handbag"),
|
||
opt("Sword", "sword"), opt("Shield", "shield"),
|
||
], "accessories"),
|
||
|
||
FieldSpec("pose", "Pose", [
|
||
opt("Standing", "standing"), opt("Sitting", "sitting"),
|
||
opt("Walking", "walking"), opt("Running", "running"),
|
||
opt("Leaning", "leaning"), opt("Kneeling", "kneeling"),
|
||
opt("Dynamic action pose", "dynamic action pose"),
|
||
opt("Relaxed pose", "relaxed pose"),
|
||
], "scene"),
|
||
FieldSpec("orientation", "Orientation", [
|
||
opt("Front view", "front view"), opt("Three-quarter view", "three-quarter view"),
|
||
opt("Side view", "side view"), opt("Rear view", "rear view"),
|
||
opt("Looking over the shoulder", "looking over the shoulder"),
|
||
], "camera"),
|
||
FieldSpec("framing", "Framing", [
|
||
opt("Facial close-up", "facial close-up"),
|
||
opt("Head-and-shoulders portrait", "head-and-shoulders portrait"),
|
||
opt("Upper-body shot", "upper-body shot"),
|
||
opt("Waist-up shot", "waist-up shot"),
|
||
opt("Three-quarter body shot", "three-quarter body shot"),
|
||
opt("Full-body shot", "full-body shot"),
|
||
opt("Wide shot", "wide shot"),
|
||
], "camera"),
|
||
FieldSpec("viewpoint", "Viewpoint", [
|
||
opt("Eye-level", "eye-level view"), opt("Low-angle", "low-angle view"),
|
||
opt("High-angle", "high-angle view"), opt("Overhead view", "overhead view"),
|
||
], "camera"),
|
||
FieldSpec("environment", "Environment", [
|
||
opt("Studio backdrop", "studio backdrop"),
|
||
opt("Outdoor natural setting", "outdoor natural setting"), opt("Urban street", "urban street"),
|
||
opt("Home interior", "home interior"), opt("Office interior", "office interior"),
|
||
opt("Beach", "beach"), opt("Fantasy setting", "fantasy setting"),
|
||
opt("Science-fiction setting", "science-fiction setting"),
|
||
opt("Solid color background", "solid color background"),
|
||
], "scene"),
|
||
FieldSpec("lighting", "Lighting", [
|
||
opt("Soft natural light", "soft natural light"),
|
||
opt("Golden hour", "golden-hour lighting"),
|
||
opt("Studio lighting", "studio lighting"),
|
||
opt("Dramatic / high-contrast", "dramatic high-contrast lighting"),
|
||
opt("Backlighting", "backlighting"),
|
||
opt("Neon lighting", "neon lighting"),
|
||
opt("Overcast / diffused", "soft diffused overcast light"),
|
||
opt("Cinematic", "cinematic lighting"),
|
||
], "scene"),
|
||
FieldSpec("art_style", "Art Style", [
|
||
opt("Photorealistic", "photorealistic"), opt("Cinematic", "cinematic"),
|
||
opt("Digital painting", "digital painting"), opt("Anime / manga", "anime style"),
|
||
opt("3D render", "3D render"), opt("Oil painting", "oil painting"),
|
||
opt("Watercolor", "watercolor painting"),
|
||
opt("Stylized realism", "stylized realism"),
|
||
opt("Video game cutscene", "AAA video game cutscene"),
|
||
], "style"),
|
||
]
|
||
|
||
FIELD_BY_KEY = {field.key: field for field in FIELDS}
|
||
|
||
OUTPUT_FORMATS = [
|
||
"SDXL – Structured",
|
||
"SDXL – Comma Tags",
|
||
"FLUX – Natural Prompt",
|
||
"Pony – Comma Tags",
|
||
]
|
||
|
||
|
||
def _resolve_value(field: FieldSpec, kwargs: Dict[str, str]) -> str:
|
||
selected = kwargs.get(field.key, EMPTY_OPTION)
|
||
if selected == CUSTOM_OPTION:
|
||
return str(kwargs.get(f"{field.key}_custom", "")).strip()
|
||
if selected == EMPTY_OPTION:
|
||
return ""
|
||
return field.value_map.get(selected, str(selected)).strip()
|
||
|
||
|
||
def _unique(values: Iterable[str]) -> List[str]:
|
||
result: List[str] = []
|
||
seen = set()
|
||
for value in values:
|
||
normalized = value.strip()
|
||
if normalized and normalized.casefold() not in seen:
|
||
result.append(normalized)
|
||
seen.add(normalized.casefold())
|
||
return result
|
||
|
||
|
||
def _join_words(values: Iterable[str]) -> str:
|
||
return " ".join(value for value in values if value).strip()
|
||
|
||
|
||
def _join_english(values: Sequence[str]) -> str:
|
||
if not values:
|
||
return ""
|
||
if len(values) == 1:
|
||
return values[0]
|
||
if len(values) == 2:
|
||
return f"{values[0]} and {values[1]}"
|
||
return f"{', '.join(values[:-1])}, and {values[-1]}"
|
||
|
||
|
||
class HumanComposerPromptBuilder:
|
||
"""Compose a person and render the description for common model families."""
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
required = {
|
||
"output_format": (OUTPUT_FORMATS, {"default": OUTPUT_FORMATS[0]}),
|
||
}
|
||
for field in FIELDS:
|
||
required[field.key] = (
|
||
[EMPTY_OPTION] + field.display_options + [CUSTOM_OPTION],
|
||
{"default": EMPTY_OPTION},
|
||
)
|
||
required[f"{field.key}_custom"] = (
|
||
"STRING",
|
||
{"default": "", "multiline": False},
|
||
)
|
||
required["additional_details"] = (
|
||
"STRING",
|
||
{
|
||
"default": "",
|
||
"multiline": True,
|
||
"placeholder": "Additional English prompt details…",
|
||
},
|
||
)
|
||
return {"required": required}
|
||
|
||
RETURN_TYPES = ("STRING",)
|
||
RETURN_NAMES = ("prompt",)
|
||
FUNCTION = "build"
|
||
CATEGORY = "Human Composer"
|
||
|
||
def build(self, output_format: str, additional_details: str = "", **kwargs):
|
||
values = {field.key: _resolve_value(field, kwargs) for field in FIELDS}
|
||
additional = additional_details.strip()
|
||
|
||
if output_format == "SDXL – Structured":
|
||
prompt = self._render_structured(values, additional)
|
||
elif output_format == "SDXL – Comma Tags":
|
||
prompt = self._render_tags(values, additional, pony=False)
|
||
elif output_format == "Pony – Comma Tags":
|
||
prompt = self._render_tags(values, additional, pony=True)
|
||
else:
|
||
prompt = self._render_flux(values, additional)
|
||
|
||
return (prompt,)
|
||
|
||
@staticmethod
|
||
def _render_structured(values: Dict[str, str], additional: str) -> str:
|
||
parts = []
|
||
for field in FIELDS:
|
||
value = values[field.key]
|
||
if value:
|
||
parts.append(f"{field.label}: {value}")
|
||
if additional:
|
||
parts.append(f"Additional Details: {additional}")
|
||
return "\n\n".join(parts)
|
||
|
||
@staticmethod
|
||
def _clothing_phrase(values: Dict[str, str]) -> str:
|
||
descriptors = _join_words([
|
||
values["clothing_color"],
|
||
values["clothing_material"],
|
||
values["clothing_fit"],
|
||
values["main_garment"],
|
||
])
|
||
category = values["clothing_category"]
|
||
if descriptors and category:
|
||
return f"{descriptors}, {category}"
|
||
return descriptors or category
|
||
|
||
@staticmethod
|
||
def _hair_tags(values: Dict[str, str]) -> List[str]:
|
||
# Keeping separate tags is more robust for CLIP than forcing every
|
||
# combination into one grammatically perfect phrase.
|
||
return _unique([
|
||
values["hair_length"],
|
||
values["hair_texture"],
|
||
values["hair_style"],
|
||
values["hair_parting"],
|
||
values["bangs"],
|
||
values["hair_color"],
|
||
])
|
||
|
||
def _render_tags(self, values: Dict[str, str], additional: str, pony: bool) -> str:
|
||
subject = _join_words([
|
||
values["age"], values["ethnicity"], values["gender"]
|
||
])
|
||
|
||
tags = _unique([
|
||
"score_9, score_8_up, score_7_up" if pony else "",
|
||
subject,
|
||
values["body_type"], values["skin_tone"],
|
||
values["facial_expression"], values["eye_color"],
|
||
*self._hair_tags(values),
|
||
values["facial_hair"],
|
||
self._clothing_phrase(values),
|
||
values["accessory_1"], values["accessory_2"], values["accessory_3"],
|
||
values["pose"], values["orientation"], values["framing"],
|
||
values["viewpoint"], values["environment"], values["lighting"],
|
||
values["art_style"], additional,
|
||
])
|
||
return ", ".join(tags)
|
||
|
||
def _render_flux(self, values: Dict[str, str], additional: str) -> str:
|
||
subject_bits = [values["age"], values["ethnicity"], values["gender"]]
|
||
subject = _join_words(subject_bits) or "person"
|
||
|
||
article = "An" if subject[:1].lower() in "aeiou" else "A"
|
||
sentences = [f"{article} {subject}"]
|
||
|
||
appearance = _unique([
|
||
values["body_type"],
|
||
f"{values['skin_tone']} skin" if values["skin_tone"] else "",
|
||
values["eye_color"],
|
||
values["facial_hair"],
|
||
])
|
||
if appearance:
|
||
sentences[0] += f" with {_join_english(appearance)}"
|
||
|
||
hair = self._hair_tags(values)
|
||
if hair:
|
||
sentences.append(f"The person has {_join_english(hair)}")
|
||
|
||
if values["facial_expression"]:
|
||
sentences.append(f"The facial expression is {values['facial_expression']}")
|
||
|
||
clothing = self._clothing_phrase(values)
|
||
if clothing:
|
||
sentences.append(f"The person is wearing {clothing}")
|
||
|
||
accessories = _unique([
|
||
values["accessory_1"], values["accessory_2"], values["accessory_3"]
|
||
])
|
||
if accessories:
|
||
sentences.append(f"The person has {_join_english(accessories)}")
|
||
|
||
scene_details = _unique([
|
||
values["pose"], values["orientation"], values["framing"],
|
||
values["viewpoint"], values["environment"], values["lighting"],
|
||
])
|
||
if scene_details:
|
||
sentences.append(_join_english(scene_details).capitalize())
|
||
|
||
if values["art_style"]:
|
||
sentences.append(f"Rendered as {values['art_style']}")
|
||
if additional:
|
||
sentences.append(additional.rstrip(". "))
|
||
|
||
return ". ".join(sentence.rstrip(". ") for sentence in sentences if sentence) + "."
|
||
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"HumanComposerPromptBuilder": HumanComposerPromptBuilder,
|
||
}
|
||
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"HumanComposerPromptBuilder": "Human Composer Prompt Builder",
|
||
}
|