Files
Max DingsdaandClaude Sonnet 5 a2fd6a68e1 Translate node to English, drop legacy node ID, rewrite README
- 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>
2026-07-16 10:19:18 +02:00

504 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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",
}