Extract character appearance policy
This commit is contained in:
@@ -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
|
||||||
@@ -144,8 +144,11 @@ Already isolated:
|
|||||||
- character slot JSON construction, character-cast parsing, slot normalization,
|
- character slot JSON construction, character-cast parsing, slot normalization,
|
||||||
slot summary text, slot expression override policy, slot seed helpers, and
|
slot summary text, slot expression override policy, slot seed helpers, and
|
||||||
slot figure/ethnicity normalization live in `character_slot.py`;
|
slot figure/ethnicity normalization live in `character_slot.py`;
|
||||||
`prompt_builder.py` keeps public delegate wrappers and still resolves
|
`prompt_builder.py` keeps public delegate wrappers.
|
||||||
generation-time appearance context from normalized slots.
|
- generation-time subject appearance selection, normalized-slot context
|
||||||
|
resolution, slot hair/outfit/clothing selection, character-context row
|
||||||
|
application, and character-slot-to-profile-row conversion live in
|
||||||
|
`character_appearance.py`; `prompt_builder.py` keeps public delegate wrappers.
|
||||||
- character manual-detail config, profile name/path policy, profile JSON
|
- character manual-detail config, profile name/path policy, profile JSON
|
||||||
normalization, descriptor assembly, save/load/rename/delete operations,
|
normalization, descriptor assembly, save/load/rename/delete operations,
|
||||||
fallback profile loading, and context override application live in
|
fallback profile loading, and context override application live in
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ Core helper ownership:
|
|||||||
| `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. |
|
||||||
| `cast_context.py` | Generation-time cast count phrases, configured-cast context metadata, character-slot label assignment, cast-summary wording, scene-kind labels, and couple count normalization. |
|
| `cast_context.py` | Generation-time cast count phrases, configured-cast context metadata, character-slot label assignment, cast-summary wording, scene-kind labels, and couple count normalization. |
|
||||||
| `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_appearance.py` | Generation-time subject appearance selection, normalized-slot context resolution, slot hair/outfit/clothing selection, character-context row application, and character-slot-to-profile-row conversion. |
|
||||||
| `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. |
|
| `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. |
|
||||||
| `character_profile.py` | Character manual-detail config, profile name/path policy, profile JSON normalization, descriptor assembly, save/load/rename/delete operations, fallback profile loading, and context override application. |
|
| `character_profile.py` | Character manual-detail config, profile name/path policy, profile JSON normalization, descriptor assembly, save/load/rename/delete operations, fallback profile loading, and context override application. |
|
||||||
| `character_slot.py` | Character slot JSON construction, character-cast parsing, slot normalization, slot summary text, slot expression override policy, slot seed helpers, and slot figure/ethnicity normalization. |
|
| `character_slot.py` | Character slot JSON construction, character-cast parsing, slot normalization, slot summary text, slot expression override policy, slot seed helpers, and slot figure/ethnicity normalization. |
|
||||||
@@ -390,9 +391,9 @@ Important behavior:
|
|||||||
|
|
||||||
Edit targets:
|
Edit targets:
|
||||||
|
|
||||||
- Character slot JSON/parsing/summary: `character_slot.py`; appearance field
|
- Character slot JSON/parsing/summary: `character_slot.py`; generation-time
|
||||||
generation: `_context_from_character_slot`, `_character_context_for_label`;
|
appearance field resolution: `character_appearance.py`; character-slot label
|
||||||
character-slot label assignment:
|
assignment:
|
||||||
`cast_context.character_slot_label_map`; pair cast descriptor entry assembly:
|
`cast_context.character_slot_label_map`; pair cast descriptor entry assembly:
|
||||||
`pair_cast.cast_descriptor_entries`.
|
`pair_cast.cast_descriptor_entries`.
|
||||||
- Profile save/load: `SxCPCharacterProfileSave`,
|
- Profile save/load: `SxCPCharacterProfileSave`,
|
||||||
|
|||||||
+31
-190
@@ -26,6 +26,7 @@ try:
|
|||||||
from . import camera_config as camera_policy
|
from . import camera_config as camera_policy
|
||||||
from . import cast_context as cast_context_policy
|
from . import cast_context as cast_context_policy
|
||||||
from . import category_template_metadata as item_template_policy
|
from . import category_template_metadata as item_template_policy
|
||||||
|
from . import character_appearance as character_appearance_policy
|
||||||
from . import character_config as character_policy
|
from . import character_config as character_policy
|
||||||
from . import character_profile as character_profile_policy
|
from . import character_profile as character_profile_policy
|
||||||
from . import character_slot as character_slot_policy
|
from . import character_slot as character_slot_policy
|
||||||
@@ -71,6 +72,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
import camera_config as camera_policy
|
import camera_config as camera_policy
|
||||||
import cast_context as cast_context_policy
|
import cast_context as cast_context_policy
|
||||||
import category_template_metadata as item_template_policy
|
import category_template_metadata as item_template_policy
|
||||||
|
import character_appearance as character_appearance_policy
|
||||||
import character_config as character_policy
|
import character_config as character_policy
|
||||||
import character_profile as character_profile_policy
|
import character_profile as character_profile_policy
|
||||||
import character_slot as character_slot_policy
|
import character_slot as character_slot_policy
|
||||||
@@ -2117,51 +2119,7 @@ def _hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) ->
|
|||||||
|
|
||||||
|
|
||||||
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:
|
||||||
hair_config = _parse_hair_config(slot.get("hair_config"))
|
return character_appearance_policy.hair_descriptor_from_slot(base_hair, slot, rng)
|
||||||
color_choice = _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES)
|
|
||||||
length_choice = _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES)
|
|
||||||
style_choice = _normalize_hair_choice(slot.get("hair_style"), 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 = g.choose(rng, color_options)
|
|
||||||
else:
|
|
||||||
color_key = _infer_hair_color_key(base_hair)
|
|
||||||
|
|
||||||
if length_choice != "random":
|
|
||||||
length_key = length_choice
|
|
||||||
elif length_options:
|
|
||||||
length_key = g.choose(rng, length_options)
|
|
||||||
else:
|
|
||||||
length_key = _infer_hair_length_key(base_hair)
|
|
||||||
|
|
||||||
if style_choice != "random":
|
|
||||||
style_key = style_choice
|
|
||||||
elif style_options:
|
|
||||||
style_key = g.choose(rng, style_options)
|
|
||||||
else:
|
|
||||||
style_key = _infer_hair_style_key(base_hair)
|
|
||||||
if color_key == "random":
|
|
||||||
color_key = _choose_hair_key(rng, CHARACTER_HAIR_COLOR_CHOICES)
|
|
||||||
if length_key == "random":
|
|
||||||
length_key = _choose_hair_key(rng, CHARACTER_HAIR_LENGTH_CHOICES)
|
|
||||||
if style_key == "random":
|
|
||||||
style_key = _choose_hair_key(rng, CHARACTER_HAIR_STYLE_CHOICES)
|
|
||||||
if length_key == "updo" and style_key not in ("ponytail", "braid", "braids", "bun", "messy_bun", "locs", "twists"):
|
|
||||||
style_key = g.choose(rng, ["ponytail", "braid", "bun", "messy_bun"])
|
|
||||||
return _hair_phrase_from_parts(color_key, length_key, style_key)
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -2272,25 +2230,11 @@ def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
|
def _slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
|
||||||
if not slot:
|
return character_appearance_policy.slot_softcore_outfit(slot, rng)
|
||||||
return ""
|
|
||||||
outfit = _slot_value(slot.get("softcore_outfit"))
|
|
||||||
if outfit:
|
|
||||||
return outfit
|
|
||||||
if rng is None:
|
|
||||||
return ""
|
|
||||||
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "softcore_outfits", rng)
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
|
def _slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
|
||||||
if not slot:
|
return character_appearance_policy.slot_hardcore_clothing(slot, rng)
|
||||||
return ""
|
|
||||||
clothing = _slot_value(slot.get("hardcore_clothing"))
|
|
||||||
if clothing:
|
|
||||||
return clothing
|
|
||||||
if rng is None:
|
|
||||||
return ""
|
|
||||||
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "hardcore_clothing", rng)
|
|
||||||
|
|
||||||
|
|
||||||
def _context_from_character_slot(
|
def _context_from_character_slot(
|
||||||
@@ -2302,63 +2246,16 @@ def _context_from_character_slot(
|
|||||||
no_plus_women: bool,
|
no_plus_women: bool,
|
||||||
no_black: bool,
|
no_black: bool,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
slot_ethnicity = _slot_value(slot.get("ethnicity"))
|
return character_appearance_policy.context_from_character_slot(
|
||||||
slot_body = _slot_value(slot.get("body"))
|
rng,
|
||||||
effective_ethnicity = slot_ethnicity or ethnicity
|
slot,
|
||||||
effective_figure = _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 = _slot_context_rng(slot, rng)
|
|
||||||
context = _appearance_for_subject(
|
|
||||||
appearance_rng,
|
|
||||||
subject_type,
|
subject_type,
|
||||||
effective_ethnicity,
|
ethnicity,
|
||||||
effective_figure,
|
figure,
|
||||||
effective_no_plus,
|
no_plus_women,
|
||||||
effective_no_black,
|
no_black,
|
||||||
)
|
)
|
||||||
|
|
||||||
characteristics = _parse_characteristics_config(slot.get("characteristics"))
|
|
||||||
age = _slot_value(slot.get("age")) or _characteristic_choice(characteristics, "ages", appearance_rng)
|
|
||||||
body_phrase = _slot_value(slot.get("body_phrase"))
|
|
||||||
if not slot_body:
|
|
||||||
slot_body = _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"] = _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 = _slot_value(slot.get("skin"))
|
|
||||||
if skin_value:
|
|
||||||
context["skin"] = skin_value
|
|
||||||
eyes_value = _slot_value(slot.get("eyes"))
|
|
||||||
if not eyes_value:
|
|
||||||
eyes_value = _eye_phrase_from_key(_characteristic_choice(characteristics, "eyes", appearance_rng))
|
|
||||||
if eyes_value:
|
|
||||||
context["eyes"] = eyes_value
|
|
||||||
hair_value = _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"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
|
|
||||||
context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type)
|
|
||||||
context["expression_enabled"] = _slot_expression_enabled(slot)
|
|
||||||
expression_intensity = _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(
|
def _character_context_for_label(
|
||||||
label: str,
|
label: str,
|
||||||
@@ -2369,36 +2266,19 @@ def _character_context_for_label(
|
|||||||
no_plus_women: bool,
|
no_plus_women: bool,
|
||||||
no_black: bool,
|
no_black: bool,
|
||||||
) -> tuple[dict[str, str], dict[str, Any] | None]:
|
) -> tuple[dict[str, str], dict[str, Any] | None]:
|
||||||
subject_type = "man" if label.startswith("Man ") else "woman"
|
return character_appearance_policy.character_context_for_label(
|
||||||
slot = label_map.get(label)
|
label,
|
||||||
if slot:
|
label_map,
|
||||||
return _context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
|
rng,
|
||||||
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
|
ethnicity,
|
||||||
|
figure,
|
||||||
|
no_plus_women,
|
||||||
|
no_black,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||||
for key in (
|
return character_appearance_policy.apply_character_context_to_row(row, context)
|
||||||
"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 _cast_descriptor_entries(
|
def _cast_descriptor_entries(
|
||||||
@@ -2439,22 +2319,7 @@ def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> di
|
|||||||
|
|
||||||
|
|
||||||
def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
|
def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
slots = _parse_character_cast(character_slot)
|
return character_appearance_policy.row_from_character_slot(character_slot)
|
||||||
if not slots:
|
|
||||||
return {}
|
|
||||||
slot = slots[-1]
|
|
||||||
if _slot_seed(slot) >= 0:
|
|
||||||
subject_type = str(slot.get("subject_type") or "woman")
|
|
||||||
return _context_from_character_slot(
|
|
||||||
random.Random(_row_seed(_slot_seed(slot), 1, 719)),
|
|
||||||
slot,
|
|
||||||
subject_type,
|
|
||||||
"any",
|
|
||||||
"curvy",
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
)
|
|
||||||
return slot
|
|
||||||
|
|
||||||
|
|
||||||
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||||
@@ -2591,38 +2456,14 @@ def _appearance_for_subject(
|
|||||||
no_plus_women: bool,
|
no_plus_women: bool,
|
||||||
no_black: bool,
|
no_black: bool,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
if subject_type == "single_any":
|
return character_appearance_policy.appearance_for_subject(
|
||||||
subject_type = "woman" if rng.random() < 0.82 else "man"
|
rng,
|
||||||
|
subject_type,
|
||||||
if subject_type == "man":
|
ethnicity,
|
||||||
men_ethnicity = ethnicity if ethnicity else "any"
|
figure,
|
||||||
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
|
no_plus_women,
|
||||||
return {
|
no_black,
|
||||||
"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": _body_phrase(body, figure_note),
|
|
||||||
"figure": figure_note,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _count_phrase(count: int, singular: str, plural: str) -> str:
|
def _count_phrase(count: int, singular: str, plural: str) -> str:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import caption_naturalizer # noqa: E402
|
|||||||
import caption_policy # noqa: E402
|
import caption_policy # noqa: E402
|
||||||
import cast_context # noqa: E402
|
import cast_context # noqa: E402
|
||||||
import category_template_metadata # noqa: E402
|
import category_template_metadata # noqa: E402
|
||||||
|
import character_appearance # noqa: E402
|
||||||
import character_config # noqa: E402
|
import character_config # noqa: E402
|
||||||
import character_profile # noqa: E402
|
import character_profile # noqa: E402
|
||||||
import character_slot # noqa: E402
|
import character_slot # noqa: E402
|
||||||
@@ -935,6 +936,23 @@ def smoke_character_config_policy() -> None:
|
|||||||
== character_slot.slot_effective_figure({"slot_seed": 123, "figure": "random"}, "woman", "curvy"),
|
== character_slot.slot_effective_figure({"slot_seed": 123, "figure": "random"}, "woman", "curvy"),
|
||||||
"Prompt builder seeded slot figure should delegate to character_slot",
|
"Prompt builder seeded slot figure should delegate to character_slot",
|
||||||
)
|
)
|
||||||
|
_expect(
|
||||||
|
pb._appearance_for_subject(random.Random(9), "woman", "western_european", "balanced", False, False)
|
||||||
|
== character_appearance.appearance_for_subject(random.Random(9), "woman", "western_european", "balanced", False, False),
|
||||||
|
"Prompt builder appearance selection should delegate to character_appearance",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
pb._context_from_character_slot(random.Random(11), slot, "man", "any", "curvy", False, False)
|
||||||
|
== character_appearance.context_from_character_slot(random.Random(11), slot, "man", "any", "curvy", False, False),
|
||||||
|
"Prompt builder slot context should delegate to character_appearance",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
pb._row_from_character_slot(slot_result["character_slot"])
|
||||||
|
== character_appearance.row_from_character_slot(slot_result["character_slot"]),
|
||||||
|
"Prompt builder slot row conversion should delegate to character_appearance",
|
||||||
|
)
|
||||||
|
row = character_appearance.apply_character_context_to_row({}, {"age": "44-year-old adult", "body": "stocky"})
|
||||||
|
_expect(row.get("age_band") == "44-year-old adult" and row.get("body") == "stocky", "Character context row application changed")
|
||||||
|
|
||||||
|
|
||||||
def smoke_character_profile_policy() -> None:
|
def smoke_character_profile_policy() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user