Extract character config policy
This commit is contained in:
@@ -0,0 +1,682 @@
|
|||||||
|
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_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_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
|
||||||
|
_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
|
||||||
@@ -110,6 +110,10 @@ Already isolated:
|
|||||||
- ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter
|
- ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter
|
||||||
parsing, and ethnicity normalization live in `filter_config.py`; character
|
parsing, and ethnicity normalization live in `filter_config.py`; character
|
||||||
routes and builder filters use `prompt_builder.py` delegate wrappers.
|
routes and builder filters use `prompt_builder.py` delegate wrappers.
|
||||||
|
- character choice lists, descriptor detail/presence/slot-seed normalization,
|
||||||
|
characteristic-list JSON builders/parsers, eye labels, hair config
|
||||||
|
builders/parsers, and hair phrase helpers live in `character_config.py`;
|
||||||
|
`prompt_builder.py` still resolves full character slots and profiles.
|
||||||
- generation profile presets, override normalization, trigger policy, and
|
- generation profile presets, override normalization, trigger policy, and
|
||||||
profile config parsing live in `generation_profile_config.py`;
|
profile config parsing live in `generation_profile_config.py`;
|
||||||
`prompt_builder.py` keeps public delegate wrappers.
|
`prompt_builder.py` keeps public delegate wrappers.
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ Core helper ownership:
|
|||||||
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
|
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
|
||||||
| `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. |
|
| `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. |
|
||||||
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
|
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
|
||||||
|
| `character_config.py` | Character choice lists, descriptor detail/presence/slot-seed normalization, characteristic-list JSON builders/parsers, eye labels, hair config builders/parsers, and hair phrase helpers. |
|
||||||
| `filter_config.py` | Ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter parsing, and ethnicity normalization used by builder and character routes. |
|
| `filter_config.py` | Ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter parsing, and ethnicity normalization used by builder and character routes. |
|
||||||
| `generation_profile_config.py` | Generation profile presets, profile option overrides, trigger policy, expression/pose/clothing config normalization, and profile config parsing. |
|
| `generation_profile_config.py` | Generation profile presets, profile option overrides, trigger policy, expression/pose/clothing config normalization, and profile config parsing. |
|
||||||
| `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. |
|
| `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. |
|
||||||
|
|||||||
+26
-22
@@ -3,57 +3,61 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from .character_config import (
|
||||||
|
build_characteristics_config_json,
|
||||||
|
build_hair_config_json,
|
||||||
|
character_age_choices,
|
||||||
|
character_body_choices,
|
||||||
|
character_descriptor_detail_choices,
|
||||||
|
character_eye_color_choices,
|
||||||
|
character_hair_color_choices,
|
||||||
|
character_hair_length_choices,
|
||||||
|
character_hair_style_choices,
|
||||||
|
character_label_choices,
|
||||||
|
character_man_body_choices,
|
||||||
|
character_presence_choices,
|
||||||
|
character_woman_body_choices,
|
||||||
|
)
|
||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
build_character_manual_config_json,
|
build_character_manual_config_json,
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_character_slot_json,
|
build_character_slot_json,
|
||||||
|
character_ethnicity_choices,
|
||||||
|
character_figure_choices,
|
||||||
|
character_hardcore_clothing_state_choices,
|
||||||
|
character_hardcore_clothing_values,
|
||||||
|
character_profile_choices,
|
||||||
|
character_softcore_outfit_source_choices,
|
||||||
|
character_softcore_outfit_values,
|
||||||
|
load_character_profile_json,
|
||||||
|
)
|
||||||
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
from character_config import (
|
||||||
build_characteristics_config_json,
|
build_characteristics_config_json,
|
||||||
build_hair_config_json,
|
build_hair_config_json,
|
||||||
character_age_choices,
|
character_age_choices,
|
||||||
character_body_choices,
|
character_body_choices,
|
||||||
character_descriptor_detail_choices,
|
character_descriptor_detail_choices,
|
||||||
character_ethnicity_choices,
|
|
||||||
character_eye_color_choices,
|
character_eye_color_choices,
|
||||||
character_figure_choices,
|
|
||||||
character_hair_color_choices,
|
character_hair_color_choices,
|
||||||
character_hair_length_choices,
|
character_hair_length_choices,
|
||||||
character_hair_style_choices,
|
character_hair_style_choices,
|
||||||
character_hardcore_clothing_state_choices,
|
|
||||||
character_hardcore_clothing_values,
|
|
||||||
character_label_choices,
|
character_label_choices,
|
||||||
character_man_body_choices,
|
character_man_body_choices,
|
||||||
character_presence_choices,
|
character_presence_choices,
|
||||||
character_profile_choices,
|
|
||||||
character_softcore_outfit_source_choices,
|
|
||||||
character_softcore_outfit_values,
|
|
||||||
character_woman_body_choices,
|
character_woman_body_choices,
|
||||||
load_character_profile_json,
|
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests from the repository root.
|
|
||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
build_character_manual_config_json,
|
build_character_manual_config_json,
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_character_slot_json,
|
build_character_slot_json,
|
||||||
build_characteristics_config_json,
|
|
||||||
build_hair_config_json,
|
|
||||||
character_age_choices,
|
|
||||||
character_body_choices,
|
|
||||||
character_descriptor_detail_choices,
|
|
||||||
character_ethnicity_choices,
|
character_ethnicity_choices,
|
||||||
character_eye_color_choices,
|
|
||||||
character_figure_choices,
|
character_figure_choices,
|
||||||
character_hair_color_choices,
|
|
||||||
character_hair_length_choices,
|
|
||||||
character_hair_style_choices,
|
|
||||||
character_hardcore_clothing_state_choices,
|
character_hardcore_clothing_state_choices,
|
||||||
character_hardcore_clothing_values,
|
character_hardcore_clothing_values,
|
||||||
character_label_choices,
|
|
||||||
character_man_body_choices,
|
|
||||||
character_presence_choices,
|
|
||||||
character_profile_choices,
|
character_profile_choices,
|
||||||
character_softcore_outfit_source_choices,
|
character_softcore_outfit_source_choices,
|
||||||
character_softcore_outfit_values,
|
character_softcore_outfit_values,
|
||||||
character_woman_body_choices,
|
|
||||||
load_character_profile_json,
|
load_character_profile_json,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+61
-525
@@ -24,6 +24,7 @@ try:
|
|||||||
template_list as _template_list,
|
template_list as _template_list,
|
||||||
)
|
)
|
||||||
from . import camera_config as camera_policy
|
from . import camera_config as camera_policy
|
||||||
|
from . import character_config as character_policy
|
||||||
from . import category_cast_config as category_cast_policy
|
from . import category_cast_config as category_cast_policy
|
||||||
from . import filter_config as filter_policy
|
from . import filter_config as filter_policy
|
||||||
from . import generate_prompt_batches as g
|
from . import generate_prompt_batches as g
|
||||||
@@ -66,6 +67,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
template_list as _template_list,
|
template_list as _template_list,
|
||||||
)
|
)
|
||||||
import camera_config as camera_policy
|
import camera_config as camera_policy
|
||||||
|
import character_config as character_policy
|
||||||
import category_cast_config as category_cast_policy
|
import category_cast_config as category_cast_policy
|
||||||
import filter_config as filter_policy
|
import filter_config as filter_policy
|
||||||
import generate_prompt_batches as g
|
import generate_prompt_batches as g
|
||||||
@@ -117,180 +119,19 @@ ETHNICITY_BASE_LIST_KEYS = filter_policy.ETHNICITY_BASE_LIST_KEYS
|
|||||||
EUROPEAN_REGIONAL_LIST_KEYS = filter_policy.EUROPEAN_REGIONAL_LIST_KEYS
|
EUROPEAN_REGIONAL_LIST_KEYS = filter_policy.EUROPEAN_REGIONAL_LIST_KEYS
|
||||||
MEDITERRANEAN_REGIONAL_LIST_KEYS = filter_policy.MEDITERRANEAN_REGIONAL_LIST_KEYS
|
MEDITERRANEAN_REGIONAL_LIST_KEYS = filter_policy.MEDITERRANEAN_REGIONAL_LIST_KEYS
|
||||||
|
|
||||||
CHARACTER_LABEL_CHOICES = [
|
CHARACTER_LABEL_CHOICES = character_policy.CHARACTER_LABEL_CHOICES
|
||||||
"auto_chain",
|
CHARACTER_AGE_CHOICES = character_policy.CHARACTER_AGE_CHOICES
|
||||||
"A",
|
CHARACTER_BODY_CHOICES = character_policy.CHARACTER_BODY_CHOICES
|
||||||
"B",
|
CHARACTER_WOMAN_BODY_CHOICES = character_policy.CHARACTER_WOMAN_BODY_CHOICES
|
||||||
"C",
|
CHARACTER_MAN_BODY_CHOICES = character_policy.CHARACTER_MAN_BODY_CHOICES
|
||||||
"D",
|
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = character_policy.CHARACTER_DESCRIPTOR_DETAIL_CHOICES
|
||||||
"E",
|
CHARACTER_PRESENCE_CHOICES = character_policy.CHARACTER_PRESENCE_CHOICES
|
||||||
"F",
|
CHARACTER_RANDOM_TOKENS = character_policy.CHARACTER_RANDOM_TOKENS
|
||||||
"G",
|
CHARACTER_SLOT_SEED_MAX = character_policy.CHARACTER_SLOT_SEED_MAX
|
||||||
"H",
|
CHARACTER_HAIR_COLOR_CHOICES = character_policy.CHARACTER_HAIR_COLOR_CHOICES
|
||||||
"I",
|
CHARACTER_HAIR_LENGTH_CHOICES = character_policy.CHARACTER_HAIR_LENGTH_CHOICES
|
||||||
"J",
|
CHARACTER_HAIR_STYLE_CHOICES = character_policy.CHARACTER_HAIR_STYLE_CHOICES
|
||||||
"K",
|
CHARACTER_EYE_COLOR_CHOICES = character_policy.CHARACTER_EYE_COLOR_CHOICES
|
||||||
"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_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",
|
|
||||||
]
|
|
||||||
|
|
||||||
CAMERA_DETAIL_CHOICES = camera_policy.CAMERA_DETAIL_CHOICES
|
CAMERA_DETAIL_CHOICES = camera_policy.CAMERA_DETAIL_CHOICES
|
||||||
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
||||||
@@ -1556,47 +1397,47 @@ def ethnicity_choices() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def character_label_choices() -> list[str]:
|
def character_label_choices() -> list[str]:
|
||||||
return list(CHARACTER_LABEL_CHOICES)
|
return character_policy.character_label_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_age_choices() -> list[str]:
|
def character_age_choices() -> list[str]:
|
||||||
return list(CHARACTER_AGE_CHOICES)
|
return character_policy.character_age_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_body_choices() -> list[str]:
|
def character_body_choices() -> list[str]:
|
||||||
return list(CHARACTER_BODY_CHOICES)
|
return character_policy.character_body_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_woman_body_choices() -> list[str]:
|
def character_woman_body_choices() -> list[str]:
|
||||||
return list(CHARACTER_WOMAN_BODY_CHOICES)
|
return character_policy.character_woman_body_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_man_body_choices() -> list[str]:
|
def character_man_body_choices() -> list[str]:
|
||||||
return list(CHARACTER_MAN_BODY_CHOICES)
|
return character_policy.character_man_body_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_descriptor_detail_choices() -> list[str]:
|
def character_descriptor_detail_choices() -> list[str]:
|
||||||
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
|
return character_policy.character_descriptor_detail_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_presence_choices() -> list[str]:
|
def character_presence_choices() -> list[str]:
|
||||||
return list(CHARACTER_PRESENCE_CHOICES)
|
return character_policy.character_presence_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_hair_color_choices() -> list[str]:
|
def character_hair_color_choices() -> list[str]:
|
||||||
return list(CHARACTER_HAIR_COLOR_CHOICES)
|
return character_policy.character_hair_color_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_hair_length_choices() -> list[str]:
|
def character_hair_length_choices() -> list[str]:
|
||||||
return list(CHARACTER_HAIR_LENGTH_CHOICES)
|
return character_policy.character_hair_length_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_hair_style_choices() -> list[str]:
|
def character_hair_style_choices() -> list[str]:
|
||||||
return list(CHARACTER_HAIR_STYLE_CHOICES)
|
return character_policy.character_hair_style_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_eye_color_choices() -> list[str]:
|
def character_eye_color_choices() -> list[str]:
|
||||||
return list(CHARACTER_EYE_COLOR_CHOICES)
|
return character_policy.character_eye_color_choices()
|
||||||
|
|
||||||
|
|
||||||
def character_ethnicity_choices() -> list[str]:
|
def character_ethnicity_choices() -> list[str]:
|
||||||
@@ -2202,39 +2043,18 @@ def build_character_manual_config_json(
|
|||||||
|
|
||||||
|
|
||||||
def _slot_value(value: Any) -> str:
|
def _slot_value(value: Any) -> str:
|
||||||
text = str(value or "").strip()
|
return character_policy.slot_value(value)
|
||||||
if text.lower() in CHARACTER_RANDOM_TOKENS:
|
|
||||||
return ""
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
CHARACTER_CHARACTERISTIC_AXES = {
|
CHARACTER_CHARACTERISTIC_AXES = character_policy.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 _empty_characteristics_config() -> dict[str, Any]:
|
def _empty_characteristics_config() -> dict[str, Any]:
|
||||||
return {
|
return character_policy.empty_characteristics_config()
|
||||||
"config_type": "characteristics",
|
|
||||||
"ages": [],
|
|
||||||
"bodies": [],
|
|
||||||
"eyes": [],
|
|
||||||
"softcore_outfits": [],
|
|
||||||
"hardcore_clothing": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_characteristic_choice(value: Any, choices: list[str] | tuple[str, ...]) -> str:
|
def _normalize_characteristic_choice(value: Any, choices: list[str] | tuple[str, ...]) -> str:
|
||||||
text = str(value or "").strip()
|
return character_policy.normalize_characteristic_choice(value, choices)
|
||||||
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(
|
def _normalize_characteristic_values(
|
||||||
@@ -2243,63 +2063,15 @@ def _normalize_characteristic_values(
|
|||||||
*,
|
*,
|
||||||
allow_free_text: bool = False,
|
allow_free_text: bool = False,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
if isinstance(values, str):
|
return character_policy.normalize_characteristic_values(values, choices, allow_free_text=allow_free_text)
|
||||||
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]:
|
def _parse_characteristics_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
if not value:
|
return character_policy.parse_characteristics_config(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:
|
def _characteristics_summary(config: dict[str, Any]) -> str:
|
||||||
parts = []
|
return character_policy.characteristics_summary(config)
|
||||||
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(
|
def build_characteristics_config_json(
|
||||||
@@ -2308,69 +2080,28 @@ def build_characteristics_config_json(
|
|||||||
selected_values: list[str] | tuple[str, ...] | str | None = None,
|
selected_values: list[str] | tuple[str, ...] | str | None = None,
|
||||||
combine_mode: str = "replace_axis",
|
combine_mode: str = "replace_axis",
|
||||||
) -> str:
|
) -> str:
|
||||||
config = _parse_characteristics_config(characteristics)
|
return character_policy.build_characteristics_config_json(
|
||||||
axis_key = str(axis or "").strip().lower()
|
characteristics=characteristics,
|
||||||
if axis_key not in config:
|
axis=axis,
|
||||||
config["summary"] = _characteristics_summary(config)
|
selected_values=selected_values,
|
||||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
combine_mode=combine_mode,
|
||||||
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:
|
def _characteristic_choice(config: dict[str, Any], key: str, rng: random.Random) -> str:
|
||||||
values = config.get(key) or []
|
return character_policy.characteristic_choice(config, key, rng)
|
||||||
return g.choose(rng, values) if values else ""
|
|
||||||
|
|
||||||
|
|
||||||
def _eye_phrase_from_key(key: str) -> str:
|
def _eye_phrase_from_key(key: str) -> str:
|
||||||
return {
|
return character_policy.eye_phrase_from_key(key)
|
||||||
"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_descriptor_detail(value: Any) -> str:
|
def _normalize_descriptor_detail(value: Any) -> str:
|
||||||
text = str(value or "auto").strip()
|
return character_policy.normalize_descriptor_detail(value)
|
||||||
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_presence_mode(value: Any, subject_type: str) -> str:
|
def _normalize_presence_mode(value: Any, subject_type: str) -> str:
|
||||||
text = str(value or "visible").strip().lower()
|
return character_policy.normalize_presence_mode(value, subject_type)
|
||||||
if text not in CHARACTER_PRESENCE_CHOICES:
|
|
||||||
text = "visible"
|
|
||||||
if subject_type != "man":
|
|
||||||
return "visible"
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
|
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
|
||||||
@@ -2414,13 +2145,7 @@ def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_slot_seed(value: Any) -> int:
|
def _normalize_slot_seed(value: Any) -> int:
|
||||||
try:
|
return character_policy.normalize_slot_seed(value)
|
||||||
seed = int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return -1
|
|
||||||
if seed < 0:
|
|
||||||
return -1
|
|
||||||
return min(seed, CHARACTER_SLOT_SEED_MAX)
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_seed(slot: dict[str, Any] | None) -> int:
|
def _slot_seed(slot: dict[str, Any] | None) -> int:
|
||||||
@@ -2685,149 +2410,39 @@ def _normalize_slot_ethnicity(value: Any) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
|
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
|
||||||
text = str(value or "random").strip().lower().replace("-", "_").replace(" ", "_")
|
return character_policy.normalize_hair_choice(value, choices)
|
||||||
return text if text in choices else "random"
|
|
||||||
|
|
||||||
|
|
||||||
def _infer_hair_color_key(text: Any) -> str:
|
def _infer_hair_color_key(text: Any) -> str:
|
||||||
value = str(text or "").lower()
|
return character_policy.infer_hair_color_key(text)
|
||||||
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:
|
def _infer_hair_length_key(text: Any) -> str:
|
||||||
value = str(text or "").lower()
|
return character_policy.infer_hair_length_key(text)
|
||||||
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:
|
def _infer_hair_style_key(text: Any) -> str:
|
||||||
value = str(text or "").lower()
|
return character_policy.infer_hair_style_key(text)
|
||||||
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:
|
def _choose_hair_key(rng: random.Random, choices: list[str]) -> str:
|
||||||
pool = [choice for choice in choices if choice != "random"]
|
return character_policy.choose_hair_key(rng, choices)
|
||||||
return g.choose(rng, pool) if pool else "random"
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_hair_values(values: Any, choices: list[str]) -> list[str]:
|
def _normalize_hair_values(values: Any, choices: list[str]) -> list[str]:
|
||||||
if isinstance(values, str):
|
return character_policy.normalize_hair_values(values, choices)
|
||||||
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]:
|
def _empty_hair_config() -> dict[str, Any]:
|
||||||
return {"config_type": "hair_characteristics", "colors": [], "lengths": [], "styles": []}
|
return character_policy.empty_hair_config()
|
||||||
|
|
||||||
|
|
||||||
def _parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
def _parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
if not value:
|
return character_policy.parse_hair_config(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:
|
def _hair_config_summary(config: dict[str, Any]) -> str:
|
||||||
parts = []
|
return character_policy.hair_config_summary(config)
|
||||||
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(
|
def build_hair_config_json(
|
||||||
@@ -2836,103 +2451,24 @@ def build_hair_config_json(
|
|||||||
selected_values: list[str] | tuple[str, ...] | str | None = None,
|
selected_values: list[str] | tuple[str, ...] | str | None = None,
|
||||||
combine_mode: str = "replace_axis",
|
combine_mode: str = "replace_axis",
|
||||||
) -> str:
|
) -> str:
|
||||||
config = _parse_hair_config(hair_config)
|
return character_policy.build_hair_config_json(
|
||||||
axis_key = {"color": "colors", "length": "lengths", "style": "styles"}.get(str(axis or "").strip().lower())
|
hair_config=hair_config,
|
||||||
choice_map = {
|
axis=axis,
|
||||||
"colors": CHARACTER_HAIR_COLOR_CHOICES,
|
selected_values=selected_values,
|
||||||
"lengths": CHARACTER_HAIR_LENGTH_CHOICES,
|
combine_mode=combine_mode,
|
||||||
"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:
|
def _hair_color_text(key: str) -> str:
|
||||||
return {
|
return character_policy.hair_color_text(key)
|
||||||
"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:
|
def _hair_length_text(key: str) -> str:
|
||||||
return {
|
return character_policy.hair_length_text(key)
|
||||||
"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:
|
def _hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) -> str:
|
||||||
color = _hair_color_text(color_key)
|
return character_policy.hair_phrase_from_parts(color_key, length_key, style_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"
|
|
||||||
|
|
||||||
|
|
||||||
def _hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
|
def _hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ if str(ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
import caption_naturalizer # noqa: E402
|
import caption_naturalizer # noqa: E402
|
||||||
|
import character_config # noqa: E402
|
||||||
import category_cast_config # noqa: E402
|
import category_cast_config # noqa: E402
|
||||||
import category_library # noqa: E402
|
import category_library # noqa: E402
|
||||||
import filter_config # noqa: E402
|
import filter_config # noqa: E402
|
||||||
@@ -662,6 +663,54 @@ def smoke_filter_config_policy() -> None:
|
|||||||
_expect(pb.normalize_ethnicity_filter("random", "any", allow_random=False) == "any", "Ethnicity default normalization changed")
|
_expect(pb.normalize_ethnicity_filter("random", "any", allow_random=False) == "any", "Ethnicity default normalization changed")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_character_config_policy() -> None:
|
||||||
|
_expect(pb.CHARACTER_LABEL_CHOICES is character_config.CHARACTER_LABEL_CHOICES, "Prompt builder character choices are not delegated")
|
||||||
|
_expect("21-year-old adult" in character_config.character_age_choices(), "Character age choices lost adult ages")
|
||||||
|
_expect("fat" in character_config.character_man_body_choices(), "Man body pool lost fat option")
|
||||||
|
_expect("platinum_blonde" in character_config.character_hair_color_choices(), "Hair color choices lost platinum blonde")
|
||||||
|
|
||||||
|
traits = json.loads(
|
||||||
|
pb.build_characteristics_config_json(
|
||||||
|
axis="bodies",
|
||||||
|
selected_values=["slim", "bad value", "slim", "fat"],
|
||||||
|
combine_mode="replace_axis",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_expect(traits.get("bodies") == ["slim", "fat"], "Character body trait normalization changed")
|
||||||
|
merged_traits = json.loads(
|
||||||
|
character_config.build_characteristics_config_json(
|
||||||
|
characteristics=traits,
|
||||||
|
axis="eyes",
|
||||||
|
selected_values=["blue", "gray-brown", "blue"],
|
||||||
|
combine_mode="add_to_axis",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_expect(merged_traits.get("bodies") == ["slim", "fat"], "Character trait merge lost existing axis")
|
||||||
|
_expect(merged_traits.get("eyes") == ["blue", "gray_brown"], "Character eye trait normalization changed")
|
||||||
|
_expect(pb._characteristic_choice({"ages": ["21-year-old adult"]}, "ages", random.Random(1)) == "21-year-old adult", "Trait choice changed")
|
||||||
|
|
||||||
|
hair = json.loads(
|
||||||
|
pb.build_hair_config_json(
|
||||||
|
axis="color",
|
||||||
|
selected_values=["platinum blonde", "bad", "dark-brown"],
|
||||||
|
combine_mode="replace_axis",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_expect(hair.get("colors") == ["platinum_blonde", "dark_brown"], "Hair color normalization changed")
|
||||||
|
hair = json.loads(
|
||||||
|
character_config.build_hair_config_json(
|
||||||
|
hair_config=hair,
|
||||||
|
axis="style",
|
||||||
|
selected_values=["messy bun", "straight"],
|
||||||
|
combine_mode="add_to_axis",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_expect(hair.get("styles") == ["messy_bun", "straight"], "Hair style config merge changed")
|
||||||
|
_expect(pb._hair_phrase_from_parts("platinum_blonde", "long", "messy_bun") == "long platinum-blonde hair in a messy bun", "Hair phrase helper changed")
|
||||||
|
_expect(character_config.normalize_presence_mode("pov", "woman") == "visible", "POV presence should stay man-only")
|
||||||
|
_expect(character_config.normalize_slot_seed(0xFFFFFFFF + 99) == 0xFFFFFFFF, "Slot seed clamp changed")
|
||||||
|
|
||||||
|
|
||||||
def smoke_hardcore_position_config_policy() -> None:
|
def smoke_hardcore_position_config_policy() -> None:
|
||||||
_expect(
|
_expect(
|
||||||
pb.HARDCORE_POSITION_FAMILY_CHOICES is hardcore_position_config.HARDCORE_POSITION_FAMILY_CHOICES,
|
pb.HARDCORE_POSITION_FAMILY_CHOICES is hardcore_position_config.HARDCORE_POSITION_FAMILY_CHOICES,
|
||||||
@@ -2630,6 +2679,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|||||||
("category_cast_config_policy", smoke_category_cast_config_policy),
|
("category_cast_config_policy", smoke_category_cast_config_policy),
|
||||||
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
||||||
("filter_config_policy", smoke_filter_config_policy),
|
("filter_config_policy", smoke_filter_config_policy),
|
||||||
|
("character_config_policy", smoke_character_config_policy),
|
||||||
("hardcore_position_config_policy", smoke_hardcore_position_config_policy),
|
("hardcore_position_config_policy", smoke_hardcore_position_config_policy),
|
||||||
("category_library_route", smoke_category_library_route),
|
("category_library_route", smoke_category_library_route),
|
||||||
("hardcore_category_routes", smoke_hardcore_category_routes),
|
("hardcore_category_routes", smoke_hardcore_category_routes),
|
||||||
|
|||||||
Reference in New Issue
Block a user