Extract character appearance policy

This commit is contained in:
2026-06-27 08:37:04 +02:00
parent e9cc75bd5f
commit 70a8698cbe
5 changed files with 326 additions and 195 deletions
+268
View File
@@ -0,0 +1,268 @@
from __future__ import annotations
import random
from typing import Any
try:
from . import character_config as character_policy
from . import character_profile as character_profile_policy
from . import character_slot as character_slot_policy
from . import generate_prompt_batches as g
from . import seed_config as seed_policy
except ImportError: # Allows local smoke tests with top-level imports.
import character_config as character_policy
import character_profile as character_profile_policy
import character_slot as character_slot_policy
import generate_prompt_batches as g
import seed_config as seed_policy
def _choose(rng: random.Random, items: list[Any]) -> Any:
return items[rng.randrange(len(items))]
def slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
outfit = character_policy.slot_value(slot.get("softcore_outfit"))
if outfit:
return outfit
if rng is None:
return ""
return character_policy.characteristic_choice(
character_policy.parse_characteristics_config(slot.get("characteristics")),
"softcore_outfits",
rng,
)
def slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
clothing = character_policy.slot_value(slot.get("hardcore_clothing"))
if clothing:
return clothing
if rng is None:
return ""
return character_policy.characteristic_choice(
character_policy.parse_characteristics_config(slot.get("characteristics")),
"hardcore_clothing",
rng,
)
def hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
hair_config = character_policy.parse_hair_config(slot.get("hair_config"))
color_choice = character_policy.normalize_hair_choice(slot.get("hair_color"), character_policy.CHARACTER_HAIR_COLOR_CHOICES)
length_choice = character_policy.normalize_hair_choice(slot.get("hair_length"), character_policy.CHARACTER_HAIR_LENGTH_CHOICES)
style_choice = character_policy.normalize_hair_choice(slot.get("hair_style"), character_policy.CHARACTER_HAIR_STYLE_CHOICES)
color_options = hair_config.get("colors") or []
length_options = hair_config.get("lengths") or []
style_options = hair_config.get("styles") or []
if (
color_choice == "random"
and length_choice == "random"
and style_choice == "random"
and not color_options
and not length_options
and not style_options
):
return ""
if color_choice != "random":
color_key = color_choice
elif color_options:
color_key = _choose(rng, color_options)
else:
color_key = character_policy.infer_hair_color_key(base_hair)
if length_choice != "random":
length_key = length_choice
elif length_options:
length_key = _choose(rng, length_options)
else:
length_key = character_policy.infer_hair_length_key(base_hair)
if style_choice != "random":
style_key = style_choice
elif style_options:
style_key = _choose(rng, style_options)
else:
style_key = character_policy.infer_hair_style_key(base_hair)
if color_key == "random":
color_key = character_policy.choose_hair_key(rng, character_policy.CHARACTER_HAIR_COLOR_CHOICES)
if length_key == "random":
length_key = character_policy.choose_hair_key(rng, character_policy.CHARACTER_HAIR_LENGTH_CHOICES)
if style_key == "random":
style_key = character_policy.choose_hair_key(rng, character_policy.CHARACTER_HAIR_STYLE_CHOICES)
if length_key == "updo" and style_key not in ("ponytail", "braid", "braids", "bun", "messy_bun", "locs", "twists"):
style_key = _choose(rng, ["ponytail", "braid", "bun", "messy_bun"])
return character_policy.hair_phrase_from_parts(color_key, length_key, style_key)
def appearance_for_subject(
rng: random.Random,
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> dict[str, str]:
if subject_type == "single_any":
subject_type = "woman" if rng.random() < 0.82 else "man"
if subject_type == "man":
men_ethnicity = ethnicity if ethnicity else "any"
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
return {
"subject_type": "man",
"subject": subject,
"subject_phrase": subject,
"age": age,
"body": body,
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": f"{body} figure",
}
subject, age, body, skin, hair, eyes = g.choose_woman(rng, ethnicity, no_plus_women, no_black)
figure_note = g.choose(rng, g.figure_pool(figure))
return {
"subject_type": "woman",
"subject": subject,
"subject_phrase": subject,
"age": age,
"body": body,
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": character_profile_policy.body_phrase(body, figure_note),
"figure": figure_note,
}
def context_from_character_slot(
rng: random.Random,
slot: dict[str, Any],
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> dict[str, Any]:
slot_ethnicity = character_policy.slot_value(slot.get("ethnicity"))
slot_body = character_policy.slot_value(slot.get("body"))
effective_ethnicity = slot_ethnicity or ethnicity
effective_figure = character_slot_policy.slot_effective_figure(slot, subject_type, figure)
effective_no_plus = bool(no_plus_women) and not slot_body
effective_no_black = bool(no_black) and not slot_ethnicity
appearance_rng = character_slot_policy.slot_context_rng(slot, rng)
context = appearance_for_subject(
appearance_rng,
subject_type,
effective_ethnicity,
effective_figure,
effective_no_plus,
effective_no_black,
)
characteristics = character_policy.parse_characteristics_config(slot.get("characteristics"))
age = character_policy.slot_value(slot.get("age")) or character_policy.characteristic_choice(characteristics, "ages", appearance_rng)
body_phrase = character_policy.slot_value(slot.get("body_phrase"))
if not slot_body:
slot_body = character_policy.characteristic_choice(characteristics, "bodies", appearance_rng)
if age:
context["age"] = age
if slot_body:
context["body"] = slot_body
if subject_type == "woman":
context["body_phrase"] = character_profile_policy.body_phrase(slot_body, context.get("figure", ""))
else:
context["body_phrase"] = f"{slot_body} figure"
if body_phrase:
context["body_phrase"] = body_phrase
skin_value = character_policy.slot_value(slot.get("skin"))
if skin_value:
context["skin"] = skin_value
eyes_value = character_policy.slot_value(slot.get("eyes"))
if not eyes_value:
eyes_value = character_policy.eye_phrase_from_key(character_policy.characteristic_choice(characteristics, "eyes", appearance_rng))
if eyes_value:
context["eyes"] = eyes_value
hair_value = character_policy.slot_value(slot.get("hair"))
if hair_value:
context["hair"] = hair_value
else:
hair_descriptor = hair_descriptor_from_slot(context.get("hair"), slot, appearance_rng)
if hair_descriptor:
context["hair"] = hair_descriptor
context["descriptor_detail"] = character_policy.normalize_descriptor_detail(slot.get("descriptor_detail"))
context["presence_mode"] = character_policy.normalize_presence_mode(slot.get("presence_mode"), subject_type)
context["expression_enabled"] = character_slot_policy.slot_expression_enabled(slot)
expression_intensity = character_slot_policy.slot_expression_intensity(slot)
if expression_intensity is not None:
context["expression_intensity"] = expression_intensity
context["subject_type"] = subject_type
context["subject"] = subject_type
context["subject_phrase"] = subject_type
return context
def character_context_for_label(
label: str,
label_map: dict[str, dict[str, Any]],
rng: random.Random,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> tuple[dict[str, Any], dict[str, Any] | None]:
subject_type = "man" if label.startswith("Man ") else "woman"
slot = label_map.get(label)
if slot:
return context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
return appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
def apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
"presence_mode",
"expression_enabled",
"expression_intensity",
):
value = context.get(key)
if value is not None and value != "":
row[key] = value
if context.get("age"):
row["age_band"] = context["age"]
return row
def row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
slots = character_slot_policy.parse_character_cast(character_slot)
if not slots:
return {}
slot = slots[-1]
if character_slot_policy.slot_seed(slot) >= 0:
subject_type = str(slot.get("subject_type") or "woman")
return context_from_character_slot(
random.Random(seed_policy.row_seed(character_slot_policy.slot_seed(slot), 1, 719)),
slot,
subject_type,
"any",
"curvy",
False,
False,
)
return slot