Files
ComfyUI-Ethanfel-Prompt-Bui…/character_config.py
T

689 lines
20 KiB
Python

from __future__ import annotations
import json
import random
import re
from typing import Any
CHARACTER_LABEL_CHOICES = [
"auto_chain",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
]
CHARACTER_AGE_CHOICES = (
["random", "manual"]
+ [f"{age}-year-old adult" for age in range(21, 86)]
+ [
"late 20s adult",
"early 30s adult",
"mid 30s adult",
"late 30s adult",
"early 40s adult",
"mid 40s adult",
"late 40s adult",
"early 50s adult",
"mid 50s adult",
"late 50s adult",
"early 60s adult",
"mid 60s adult",
"late 60s adult",
"early 70s adult",
"mid 70s adult",
"late 70s adult",
"early 80s adult",
]
)
CHARACTER_BODY_CHOICES = [
"random",
"manual",
"slim",
"petite adult",
"toned",
"athletic",
"average",
"curvy",
"soft curvy",
"curvy athletic",
"hourglass",
"slim busty",
"busty",
"busty curvy",
"voluptuous",
"plus-size",
"heavyset",
"fat",
"stocky",
"broad",
"muscular",
]
CHARACTER_WOMAN_BODY_CHOICES = [
"random",
"manual",
"slim",
"petite adult",
"toned",
"athletic",
"average",
"curvy",
"soft curvy",
"curvy athletic",
"hourglass",
"slim busty",
"busty",
"busty curvy",
"voluptuous",
"plus-size",
"heavyset",
"fat",
]
CHARACTER_MAN_BODY_CHOICES = [
"random",
"manual",
"slim",
"lean",
"lean athletic",
"toned",
"average",
"athletic",
"muscular",
"broad",
"broad-shouldered",
"stocky",
"heavyset",
"fat",
]
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
CHARACTER_FIGURE_CHOICES = ["random", "curvy", "balanced", "bombshell"]
CHARACTER_HAIR_COLOR_CHOICES = [
"random",
"black",
"brown",
"dark_brown",
"chestnut",
"auburn",
"copper",
"red",
"blonde",
"platinum_blonde",
"ash_blonde",
"honey_blonde",
"strawberry_blonde",
"dark_blonde",
"silver_gray",
"white",
]
CHARACTER_HAIR_LENGTH_CHOICES = [
"random",
"very_short",
"short",
"bob_lob",
"shoulder_length",
"medium",
"long",
"very_long",
"updo",
]
CHARACTER_HAIR_STYLE_CHOICES = [
"random",
"straight",
"waves",
"loose_waves",
"curls",
"tight_curls",
"pixie_cut",
"bob",
"lob",
"shag",
"ponytail",
"braid",
"braids",
"bun",
"messy_bun",
"locs",
"twists",
"afro",
"natural_curls",
"wet_hair",
"slicked_back",
]
CHARACTER_EYE_COLOR_CHOICES = [
"random",
"blue",
"pale_blue",
"ice_blue",
"blue_gray",
"green",
"emerald_green",
"hazel",
"light_hazel",
"green_hazel",
"amber",
"amber_brown",
"honey_brown",
"brown",
"deep_brown",
"dark_brown",
"dark",
"gray",
"gray_brown",
]
CHARACTER_CHARACTERISTIC_AXES = {
"ages": CHARACTER_AGE_CHOICES,
"bodies": list(dict.fromkeys([*CHARACTER_BODY_CHOICES, *CHARACTER_WOMAN_BODY_CHOICES, *CHARACTER_MAN_BODY_CHOICES])),
"eyes": CHARACTER_EYE_COLOR_CHOICES,
}
def character_label_choices() -> list[str]:
return list(CHARACTER_LABEL_CHOICES)
def character_age_choices() -> list[str]:
return list(CHARACTER_AGE_CHOICES)
def character_body_choices() -> list[str]:
return list(CHARACTER_BODY_CHOICES)
def character_woman_body_choices() -> list[str]:
return list(CHARACTER_WOMAN_BODY_CHOICES)
def character_man_body_choices() -> list[str]:
return list(CHARACTER_MAN_BODY_CHOICES)
def character_descriptor_detail_choices() -> list[str]:
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
def character_presence_choices() -> list[str]:
return list(CHARACTER_PRESENCE_CHOICES)
def character_figure_choices() -> list[str]:
return list(CHARACTER_FIGURE_CHOICES)
def character_hair_color_choices() -> list[str]:
return list(CHARACTER_HAIR_COLOR_CHOICES)
def character_hair_length_choices() -> list[str]:
return list(CHARACTER_HAIR_LENGTH_CHOICES)
def character_hair_style_choices() -> list[str]:
return list(CHARACTER_HAIR_STYLE_CHOICES)
def character_eye_color_choices() -> list[str]:
return list(CHARACTER_EYE_COLOR_CHOICES)
def slot_value(value: Any) -> str:
text = str(value or "").strip()
if text.lower() in CHARACTER_RANDOM_TOKENS:
return ""
return text
def normalize_descriptor_detail(value: Any) -> str:
text = str(value or "auto").strip()
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
def normalize_presence_mode(value: Any, subject_type: str) -> str:
text = str(value or "visible").strip().lower()
if text not in CHARACTER_PRESENCE_CHOICES:
text = "visible"
if subject_type != "man":
return "visible"
return text
def normalize_slot_seed(value: Any) -> int:
try:
seed = int(value)
except (TypeError, ValueError):
return -1
if seed < 0:
return -1
return min(seed, CHARACTER_SLOT_SEED_MAX)
def empty_characteristics_config() -> dict[str, Any]:
return {
"config_type": "characteristics",
"ages": [],
"bodies": [],
"eyes": [],
"softcore_outfits": [],
"hardcore_clothing": [],
}
def normalize_characteristic_choice(value: Any, choices: list[str] | tuple[str, ...]) -> str:
text = str(value or "").strip()
if not text:
return ""
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
for choice in choices:
if normalized == re.sub(r"[^a-z0-9]+", "_", str(choice).lower()).strip("_"):
return str(choice)
return ""
def normalize_characteristic_values(
values: Any,
choices: list[str] | tuple[str, ...] | None = None,
*,
allow_free_text: bool = False,
) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[\n;]+", values) if part.strip()]
if len(raw_values) == 1 and "," in raw_values[0] and not allow_free_text:
raw_values = [part.strip() for part in raw_values[0].split(",") if part.strip()]
elif isinstance(values, (list, tuple, set)):
raw_values = list(values)
else:
raw_values = []
normalized: list[str] = []
for raw_value in raw_values:
value = str(raw_value or "").strip() if choices is None else normalize_characteristic_choice(raw_value, choices)
if not value or value in ("random", "manual"):
continue
if value not in normalized:
normalized.append(value)
return normalized
def parse_characteristics_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return empty_characteristics_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return empty_characteristics_config()
if not isinstance(raw, dict):
return empty_characteristics_config()
return {
"config_type": "characteristics",
"ages": normalize_characteristic_values(raw.get("ages"), CHARACTER_AGE_CHOICES),
"bodies": normalize_characteristic_values(raw.get("bodies"), CHARACTER_CHARACTERISTIC_AXES["bodies"]),
"eyes": normalize_characteristic_values(raw.get("eyes"), CHARACTER_EYE_COLOR_CHOICES),
"softcore_outfits": normalize_characteristic_values(raw.get("softcore_outfits"), None, allow_free_text=True),
"hardcore_clothing": normalize_characteristic_values(raw.get("hardcore_clothing"), None, allow_free_text=True),
}
def characteristics_summary(config: dict[str, Any]) -> str:
parts = []
for key, label in (
("ages", "ages"),
("bodies", "bodies"),
("eyes", "eyes"),
("softcore_outfits", "soft_outfits"),
("hardcore_clothing", "hard_clothing"),
):
values = config.get(key) or []
if not values:
continue
if key in ("softcore_outfits", "hardcore_clothing"):
parts.append(f"{label}={len(values)}")
else:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "characteristics unrestricted"
def build_characteristics_config_json(
characteristics: str | dict[str, Any] | None = "",
axis: str = "ages",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = parse_characteristics_config(characteristics)
axis_key = str(axis or "").strip().lower()
if axis_key not in config:
config["summary"] = characteristics_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
choices = CHARACTER_CHARACTERISTIC_AXES.get(axis_key)
values = normalize_characteristic_values(
selected_values,
choices,
allow_free_text=choices is None,
)
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = characteristics_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def characteristic_choice(config: dict[str, Any], key: str, rng: random.Random) -> str:
values = config.get(key) or []
return values[rng.randrange(len(values))] if values else ""
def eye_phrase_from_key(key: str) -> str:
return {
"blue": "blue eyes",
"pale_blue": "pale blue eyes",
"ice_blue": "ice blue eyes",
"blue_gray": "blue-gray eyes",
"green": "green eyes",
"emerald_green": "emerald green eyes",
"hazel": "hazel eyes",
"light_hazel": "light hazel eyes",
"green_hazel": "green-hazel eyes",
"amber": "amber eyes",
"amber_brown": "amber-brown eyes",
"honey_brown": "honey-brown eyes",
"brown": "brown eyes",
"deep_brown": "deep brown eyes",
"dark_brown": "dark brown eyes",
"dark": "dark eyes",
"gray": "gray eyes",
"gray_brown": "gray-brown eyes",
}.get(key, "")
def normalize_hair_choice(value: Any, choices: list[str]) -> str:
text = str(value or "random").strip().lower().replace("-", "_").replace(" ", "_")
return text if text in choices else "random"
def infer_hair_color_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("platinum_blonde", ("platinum-blonde", "platinum blonde", "platinum")),
("strawberry_blonde", ("strawberry-blonde", "strawberry blonde")),
("honey_blonde", ("honey-blonde", "honey blonde")),
("ash_blonde", ("ash-blonde", "ash blonde")),
("dark_blonde", ("dark-blonde", "dark blonde")),
(
"blonde",
(
"light-blonde",
"light blonde",
"blonde",
"flaxen",
"wheat-blonde",
"wheat blonde",
"beige-blonde",
"beige blonde",
"sandy-blonde",
"sandy blonde",
),
),
("silver_gray", ("silver-gray", "silver grey", "silver", "gray", "grey")),
("dark_brown", ("dark-brown", "dark brown", "espresso")),
("chestnut", ("chestnut",)),
("auburn", ("auburn",)),
("copper", ("copper",)),
("red", ("red hair", "redhead")),
("black", ("black",)),
("brown", ("brown", "brunette", "caramel")),
("white", ("white",)),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def infer_hair_length_key(text: Any) -> str:
value = str(text or "").lower()
if any(token in value for token in ("very long", "waist-length", "hip-length")):
return "very_long"
if "long" in value:
return "long"
if "shoulder-length" in value or "shoulder length" in value:
return "shoulder_length"
if "medium-length" in value or "medium length" in value:
return "medium"
if any(token in value for token in ("bob", "lob")):
return "bob_lob"
if any(token in value for token in ("pixie", "short", "cropped", "tapered")):
return "short"
if any(token in value for token in ("bun", "updo")):
return "updo"
return "random"
def infer_hair_style_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("pixie_cut", ("pixie",)),
("messy_bun", ("messy bun",)),
("bun", ("bun", "updo")),
("ponytail", ("ponytail",)),
("braids", ("braids", "box braids", "cornrow")),
("braid", ("braid",)),
("locs", ("locs", "dreadlocks")),
("twists", ("twists",)),
("afro", ("afro",)),
("natural_curls", ("natural curls", "natural coils", "coils")),
("tight_curls", ("tight curls", "tight coils")),
("curls", ("curls", "curly")),
("loose_waves", ("loose waves",)),
("waves", ("waves", "wavy")),
("lob", ("lob",)),
("bob", ("bob",)),
("shag", ("shag",)),
("wet_hair", ("wet hair", "damp hair")),
("slicked_back", ("slicked-back", "slicked back")),
("straight", ("straight", "sleek")),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def choose_hair_key(rng: random.Random, choices: list[str]) -> str:
pool = [choice for choice in choices if choice != "random"]
return pool[rng.randrange(len(pool))] if pool else "random"
def normalize_hair_values(values: Any, choices: list[str]) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[,;\n]+", values) if part.strip()]
elif isinstance(values, (list, tuple, set)):
raw_values = list(values)
else:
raw_values = []
normalized: list[str] = []
for value in raw_values:
key = normalize_hair_choice(value, choices)
if key != "random" and key not in normalized:
normalized.append(key)
return normalized
def empty_hair_config() -> dict[str, Any]:
return {"config_type": "hair_characteristics", "colors": [], "lengths": [], "styles": []}
def parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return empty_hair_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return empty_hair_config()
if not isinstance(raw, dict):
return empty_hair_config()
return {
"config_type": "hair_characteristics",
"colors": normalize_hair_values(raw.get("colors"), CHARACTER_HAIR_COLOR_CHOICES),
"lengths": normalize_hair_values(raw.get("lengths"), CHARACTER_HAIR_LENGTH_CHOICES),
"styles": normalize_hair_values(raw.get("styles"), CHARACTER_HAIR_STYLE_CHOICES),
}
def hair_config_summary(config: dict[str, Any]) -> str:
parts = []
for label, key in (("colors", "colors"), ("lengths", "lengths"), ("styles", "styles")):
values = config.get(key) or []
if values:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "hair unrestricted"
def build_hair_config_json(
hair_config: str | dict[str, Any] | None = "",
axis: str = "color",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = parse_hair_config(hair_config)
axis_key = {"color": "colors", "length": "lengths", "style": "styles"}.get(str(axis or "").strip().lower())
choice_map = {
"colors": CHARACTER_HAIR_COLOR_CHOICES,
"lengths": CHARACTER_HAIR_LENGTH_CHOICES,
"styles": CHARACTER_HAIR_STYLE_CHOICES,
}
if axis_key:
values = normalize_hair_values(selected_values, choice_map[axis_key])
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = hair_config_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def hair_color_text(key: str) -> str:
return {
"black": "black",
"brown": "brown",
"dark_brown": "dark-brown",
"chestnut": "chestnut",
"auburn": "auburn",
"copper": "copper",
"red": "red",
"blonde": "blonde",
"platinum_blonde": "platinum-blonde",
"ash_blonde": "ash-blonde",
"honey_blonde": "honey-blonde",
"strawberry_blonde": "strawberry-blonde",
"dark_blonde": "dark-blonde",
"silver_gray": "silver-gray",
"white": "white",
}.get(key, "brown")
def hair_length_text(key: str) -> str:
return {
"very_short": "very short",
"short": "short",
"bob_lob": "",
"shoulder_length": "shoulder-length",
"medium": "medium-length",
"long": "long",
"very_long": "very long",
"updo": "",
}.get(key, "")
def hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) -> str:
color = hair_color_text(color_key)
length = hair_length_text(length_key)
prefix = " ".join(part for part in (length, color) if part)
if style_key == "pixie_cut":
return f"short {color} pixie cut"
if style_key == "bob":
return f"{color} bob" if length_key in ("random", "bob_lob", "short") else f"{prefix} bob"
if style_key == "lob":
return f"shoulder-length {color} lob" if length_key in ("random", "bob_lob") else f"{prefix} lob"
if style_key == "shag":
return f"{prefix or color} shag"
if style_key == "ponytail":
return f"{prefix or color} ponytail"
if style_key == "braid":
return f"{prefix or color} braid"
if style_key == "braids":
return f"{prefix or color} braids"
if style_key == "bun":
return f"{prefix} hair in a bun" if length else f"{color} bun"
if style_key == "messy_bun":
return f"{prefix} hair in a messy bun" if length else f"messy {color} bun"
if style_key == "locs":
return f"{prefix or color} locs"
if style_key == "twists":
return f"{prefix or color} twists"
if style_key == "afro":
return f"{color} afro"
if style_key == "natural_curls":
return f"{prefix or color} natural curls"
if style_key == "wet_hair":
return f"{prefix or color} wet hair"
if style_key == "slicked_back":
return f"slicked-back {color} hair"
if style_key == "straight":
return f"{prefix or color} straight hair"
if style_key == "loose_waves":
return f"{prefix or color} loose waves"
if style_key == "tight_curls":
return f"{prefix or color} tight curls"
if style_key == "curls":
return f"{prefix or color} curls"
return f"{prefix or color} waves"
_slot_value = slot_value
_normalize_descriptor_detail = normalize_descriptor_detail
_normalize_presence_mode = normalize_presence_mode
_normalize_slot_seed = normalize_slot_seed
_character_figure_choices = character_figure_choices
_empty_characteristics_config = empty_characteristics_config
_normalize_characteristic_choice = normalize_characteristic_choice
_normalize_characteristic_values = normalize_characteristic_values
_parse_characteristics_config = parse_characteristics_config
_characteristics_summary = characteristics_summary
_characteristic_choice = characteristic_choice
_eye_phrase_from_key = eye_phrase_from_key
_normalize_hair_choice = normalize_hair_choice
_infer_hair_color_key = infer_hair_color_key
_infer_hair_length_key = infer_hair_length_key
_infer_hair_style_key = infer_hair_style_key
_choose_hair_key = choose_hair_key
_normalize_hair_values = normalize_hair_values
_empty_hair_config = empty_hair_config
_parse_hair_config = parse_hair_config
_hair_config_summary = hair_config_summary
_hair_color_text = hair_color_text
_hair_length_text = hair_length_text
_hair_phrase_from_parts = hair_phrase_from_parts