Extract character slot policy
This commit is contained in:
@@ -107,6 +107,7 @@ CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "min
|
|||||||
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
|
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
|
||||||
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
||||||
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
|
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
|
||||||
|
CHARACTER_FIGURE_CHOICES = ["random", "curvy", "balanced", "bombshell"]
|
||||||
CHARACTER_HAIR_COLOR_CHOICES = [
|
CHARACTER_HAIR_COLOR_CHOICES = [
|
||||||
"random",
|
"random",
|
||||||
"black",
|
"black",
|
||||||
@@ -215,6 +216,10 @@ def character_presence_choices() -> list[str]:
|
|||||||
return list(CHARACTER_PRESENCE_CHOICES)
|
return list(CHARACTER_PRESENCE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def character_figure_choices() -> list[str]:
|
||||||
|
return list(CHARACTER_FIGURE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
def character_hair_color_choices() -> list[str]:
|
def character_hair_color_choices() -> list[str]:
|
||||||
return list(CHARACTER_HAIR_COLOR_CHOICES)
|
return list(CHARACTER_HAIR_COLOR_CHOICES)
|
||||||
|
|
||||||
@@ -661,6 +666,7 @@ _slot_value = slot_value
|
|||||||
_normalize_descriptor_detail = normalize_descriptor_detail
|
_normalize_descriptor_detail = normalize_descriptor_detail
|
||||||
_normalize_presence_mode = normalize_presence_mode
|
_normalize_presence_mode = normalize_presence_mode
|
||||||
_normalize_slot_seed = normalize_slot_seed
|
_normalize_slot_seed = normalize_slot_seed
|
||||||
|
_character_figure_choices = character_figure_choices
|
||||||
_empty_characteristics_config = empty_characteristics_config
|
_empty_characteristics_config = empty_characteristics_config
|
||||||
_normalize_characteristic_choice = normalize_characteristic_choice
|
_normalize_characteristic_choice = normalize_characteristic_choice
|
||||||
_normalize_characteristic_values = normalize_characteristic_values
|
_normalize_characteristic_values = normalize_characteristic_values
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import character_config as character_policy
|
||||||
|
from . import character_profile as character_profile_policy
|
||||||
|
from . import filter_config as filter_policy
|
||||||
|
from . import pov_policy
|
||||||
|
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 filter_config as filter_policy
|
||||||
|
import pov_policy
|
||||||
|
import seed_config as seed_policy
|
||||||
|
|
||||||
|
|
||||||
|
def _is_false(value: Any) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value is False
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip().lower() in ("false", "0", "no", "off")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
|
||||||
|
try:
|
||||||
|
number = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
return max(min_value, min(max_value, number))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_slot_expression_intensity(value: Any) -> float:
|
||||||
|
try:
|
||||||
|
intensity = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return -1.0
|
||||||
|
if intensity < 0:
|
||||||
|
return -1.0
|
||||||
|
return _clamped_float(intensity, 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
|
||||||
|
if not slot:
|
||||||
|
return True
|
||||||
|
return not _is_false(slot.get("expression_enabled", True))
|
||||||
|
|
||||||
|
|
||||||
|
def slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
|
||||||
|
if not slot or not slot_expression_enabled(slot):
|
||||||
|
return None
|
||||||
|
intensity = normalize_slot_expression_intensity(slot.get("expression_intensity"))
|
||||||
|
return intensity if intensity >= 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
|
||||||
|
if not slot or not slot_expression_enabled(slot):
|
||||||
|
return None
|
||||||
|
phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
|
||||||
|
if phase_key:
|
||||||
|
intensity = normalize_slot_expression_intensity(slot.get(phase_key))
|
||||||
|
if intensity >= 0:
|
||||||
|
return intensity
|
||||||
|
return slot_expression_intensity(slot)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_slot_seed(value: Any) -> int:
|
||||||
|
return character_policy.normalize_slot_seed(value)
|
||||||
|
|
||||||
|
|
||||||
|
def slot_seed(slot: dict[str, Any] | None) -> int:
|
||||||
|
if not slot:
|
||||||
|
return -1
|
||||||
|
return normalize_slot_seed(slot.get("slot_seed"))
|
||||||
|
|
||||||
|
|
||||||
|
def slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
|
||||||
|
seed = slot_seed(slot)
|
||||||
|
if seed < 0:
|
||||||
|
return None
|
||||||
|
return random.Random(seed_policy.row_seed(seed, 1, salt))
|
||||||
|
|
||||||
|
|
||||||
|
def slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
|
||||||
|
return slot_seeded_rng(slot, 701) or fallback_rng
|
||||||
|
|
||||||
|
|
||||||
|
def slot_effective_figure(
|
||||||
|
slot: dict[str, Any],
|
||||||
|
subject_type: str,
|
||||||
|
fallback_figure: str,
|
||||||
|
) -> str:
|
||||||
|
raw_figure = str(slot.get("figure") or "random").strip()
|
||||||
|
if raw_figure in ("curvy", "balanced", "bombshell"):
|
||||||
|
return raw_figure
|
||||||
|
seeded_rng = slot_seeded_rng(slot, 709)
|
||||||
|
if subject_type == "woman" and seeded_rng is not None:
|
||||||
|
options = ["curvy", "balanced", "bombshell"]
|
||||||
|
return options[seeded_rng.randrange(len(options))]
|
||||||
|
return fallback_figure
|
||||||
|
|
||||||
|
|
||||||
|
def slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
||||||
|
choice = str(choice or "").strip()
|
||||||
|
manual_value = str(manual_value or "").strip()
|
||||||
|
if choice == "manual":
|
||||||
|
return manual_value or "random"
|
||||||
|
if choice.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
|
||||||
|
return "random"
|
||||||
|
return choice
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_slot_ethnicity(value: Any) -> str:
|
||||||
|
return filter_policy.normalize_ethnicity_filter(value, "random", allow_random=True)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
|
||||||
|
if subject_type not in ("woman", "man"):
|
||||||
|
subject_type = "woman"
|
||||||
|
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
|
||||||
|
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
|
||||||
|
if label == "AUTO_CHAIN":
|
||||||
|
label = "auto_chain"
|
||||||
|
if label not in character_policy.CHARACTER_LABEL_CHOICES:
|
||||||
|
label = "auto_chain"
|
||||||
|
|
||||||
|
manual_config = character_profile_policy.parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))
|
||||||
|
|
||||||
|
raw_age = str(slot.get("age") or "random")
|
||||||
|
raw_manual_age = str(slot.get("manual_age") or "").strip()
|
||||||
|
if not raw_manual_age and manual_config.get("manual_age"):
|
||||||
|
raw_manual_age = manual_config["manual_age"]
|
||||||
|
if raw_age.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
|
||||||
|
raw_age = "manual"
|
||||||
|
age = slot_manual_or_choice(raw_age, raw_manual_age)
|
||||||
|
|
||||||
|
raw_body = str(slot.get("body") or "random")
|
||||||
|
raw_manual_body = str(slot.get("manual_body") or "").strip()
|
||||||
|
if not raw_manual_body and manual_config.get("manual_body"):
|
||||||
|
raw_manual_body = manual_config["manual_body"]
|
||||||
|
if raw_body.lower() in character_policy.CHARACTER_RANDOM_TOKENS:
|
||||||
|
raw_body = "manual"
|
||||||
|
body = slot_manual_or_choice(raw_body, raw_manual_body)
|
||||||
|
figure = str(slot.get("figure") or "random").strip()
|
||||||
|
if figure not in character_policy.CHARACTER_FIGURE_CHOICES:
|
||||||
|
figure = "random"
|
||||||
|
|
||||||
|
def manual_fallback(field: str) -> str:
|
||||||
|
direct = character_policy.slot_value(slot.get(field))
|
||||||
|
return direct or manual_config.get(field, "")
|
||||||
|
|
||||||
|
normalized = {
|
||||||
|
"profile_type": "character_slot",
|
||||||
|
"subject_type": subject_type,
|
||||||
|
"label": label,
|
||||||
|
"slot_seed": normalize_slot_seed(slot.get("slot_seed")),
|
||||||
|
"age": age,
|
||||||
|
"ethnicity": normalize_slot_ethnicity(slot.get("ethnicity")),
|
||||||
|
"figure": figure,
|
||||||
|
"body": body,
|
||||||
|
"body_phrase": manual_fallback("body_phrase"),
|
||||||
|
"skin": manual_fallback("skin"),
|
||||||
|
"hair": manual_fallback("hair"),
|
||||||
|
"manual": manual_config,
|
||||||
|
"characteristics": (
|
||||||
|
slot.get("characteristics")
|
||||||
|
if isinstance(slot.get("characteristics"), dict)
|
||||||
|
else character_policy.slot_value(slot.get("characteristics") or slot.get("characteristics_config"))
|
||||||
|
),
|
||||||
|
"hair_config": (
|
||||||
|
slot.get("hair_config")
|
||||||
|
if isinstance(slot.get("hair_config"), dict)
|
||||||
|
else character_policy.slot_value(slot.get("hair_config"))
|
||||||
|
),
|
||||||
|
"hair_color": character_policy.normalize_hair_choice(slot.get("hair_color"), character_policy.CHARACTER_HAIR_COLOR_CHOICES),
|
||||||
|
"hair_length": character_policy.normalize_hair_choice(
|
||||||
|
slot.get("hair_length"),
|
||||||
|
character_policy.CHARACTER_HAIR_LENGTH_CHOICES,
|
||||||
|
),
|
||||||
|
"hair_style": character_policy.normalize_hair_choice(slot.get("hair_style"), character_policy.CHARACTER_HAIR_STYLE_CHOICES),
|
||||||
|
"eyes": manual_fallback("eyes"),
|
||||||
|
"descriptor_detail": character_policy.normalize_descriptor_detail(slot.get("descriptor_detail")),
|
||||||
|
"presence_mode": character_policy.normalize_presence_mode(slot.get("presence_mode"), subject_type),
|
||||||
|
"softcore_outfit": manual_fallback("softcore_outfit"),
|
||||||
|
"hardcore_clothing": (
|
||||||
|
character_policy.slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
|
||||||
|
or manual_config.get("hardcore_clothing", "")
|
||||||
|
),
|
||||||
|
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
|
||||||
|
"expression_intensity": normalize_slot_expression_intensity(slot.get("expression_intensity")),
|
||||||
|
"softcore_expression_intensity": normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
|
||||||
|
"hardcore_expression_intensity": normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
|
||||||
|
}
|
||||||
|
normalized["summary"] = character_slot_summary(normalized)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
|
||||||
|
if not character_cast:
|
||||||
|
return []
|
||||||
|
if isinstance(character_cast, list):
|
||||||
|
raw = character_cast
|
||||||
|
elif isinstance(character_cast, dict):
|
||||||
|
raw = character_cast
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(character_cast))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
|
||||||
|
|
||||||
|
if isinstance(raw, list):
|
||||||
|
slots = raw
|
||||||
|
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
|
||||||
|
slots = raw["slots"]
|
||||||
|
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
|
||||||
|
slots = [raw]
|
||||||
|
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
|
||||||
|
slots = [raw]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
return [normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
|
||||||
|
|
||||||
|
|
||||||
|
def character_slot_summary(slot: dict[str, Any]) -> str:
|
||||||
|
subject = str(slot.get("subject_type") or "woman")
|
||||||
|
label = str(slot.get("label") or "auto_chain")
|
||||||
|
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
|
||||||
|
parts = [
|
||||||
|
subject,
|
||||||
|
label_text,
|
||||||
|
f"seed={slot.get('slot_seed')}" if slot_seed(slot) >= 0 else "",
|
||||||
|
f"age={slot.get('age', 'random')}",
|
||||||
|
f"ethnicity={slot.get('ethnicity', 'random')}",
|
||||||
|
f"figure={slot.get('figure', 'random')}",
|
||||||
|
f"body={slot.get('body', 'random')}",
|
||||||
|
f"detail={slot.get('descriptor_detail', 'auto')}",
|
||||||
|
]
|
||||||
|
parts = [part for part in parts if part]
|
||||||
|
if pov_policy.slot_is_pov(slot):
|
||||||
|
parts.append("presence=pov")
|
||||||
|
if not slot_expression_enabled(slot):
|
||||||
|
parts.append("expression=disabled")
|
||||||
|
else:
|
||||||
|
expression_intensity = slot_expression_intensity(slot)
|
||||||
|
if expression_intensity is not None:
|
||||||
|
parts.append(f"expression={expression_intensity:.2f}")
|
||||||
|
softcore_expression_intensity = slot_expression_intensity_for_phase(slot, "softcore")
|
||||||
|
hardcore_expression_intensity = slot_expression_intensity_for_phase(slot, "hardcore")
|
||||||
|
if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
|
||||||
|
parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
|
||||||
|
if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
|
||||||
|
parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
|
||||||
|
if slot.get("softcore_outfit"):
|
||||||
|
parts.append(f"soft_outfit={slot['softcore_outfit']}")
|
||||||
|
if slot.get("hardcore_clothing"):
|
||||||
|
parts.append(f"hard_clothing={slot['hardcore_clothing']}")
|
||||||
|
characteristics = character_policy.parse_characteristics_config(slot.get("characteristics"))
|
||||||
|
characteristics_summary = character_policy.characteristics_summary(characteristics)
|
||||||
|
if characteristics_summary != "characteristics unrestricted":
|
||||||
|
parts.append(f"characteristics={characteristics_summary}")
|
||||||
|
hair_config = character_policy.parse_hair_config(slot.get("hair_config"))
|
||||||
|
hair_config_summary = character_policy.hair_config_summary(hair_config)
|
||||||
|
if hair_config_summary != "hair unrestricted":
|
||||||
|
parts.append(f"hair={hair_config_summary}")
|
||||||
|
for key in ("hair_color", "hair_length", "hair_style"):
|
||||||
|
value = slot.get(key)
|
||||||
|
if value and value != "random":
|
||||||
|
parts.append(f"{key}={value}")
|
||||||
|
for key in ("body_phrase", "skin", "hair", "eyes"):
|
||||||
|
value = slot.get(key)
|
||||||
|
if value:
|
||||||
|
parts.append(f"{key}={value}")
|
||||||
|
return "; ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def build_character_slot_json(
|
||||||
|
subject_type: str = "woman",
|
||||||
|
label: str = "auto_chain",
|
||||||
|
slot_seed: int = -1,
|
||||||
|
age: str = "random",
|
||||||
|
manual_age: str = "",
|
||||||
|
manual: str | dict[str, Any] | None = "",
|
||||||
|
ethnicity: str = "random",
|
||||||
|
figure: str = "random",
|
||||||
|
body: str = "random",
|
||||||
|
manual_body: str = "",
|
||||||
|
body_phrase: str = "",
|
||||||
|
skin: str = "",
|
||||||
|
hair: str = "",
|
||||||
|
characteristics: str | dict[str, Any] | None = "",
|
||||||
|
hair_config: str | dict[str, Any] | None = "",
|
||||||
|
hair_color: str = "random",
|
||||||
|
hair_length: str = "random",
|
||||||
|
hair_style: str = "random",
|
||||||
|
eyes: str = "",
|
||||||
|
descriptor_detail: str = "auto",
|
||||||
|
expression_enabled: bool = True,
|
||||||
|
expression_intensity: float = -1.0,
|
||||||
|
enabled: bool = True,
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||||
|
presence_mode: str = "visible",
|
||||||
|
softcore_expression_intensity: float = -1.0,
|
||||||
|
hardcore_expression_intensity: float = -1.0,
|
||||||
|
softcore_outfit: str = "",
|
||||||
|
hardcore_clothing: str = "",
|
||||||
|
) -> dict[str, str]:
|
||||||
|
existing_slots = parse_character_cast(character_cast)
|
||||||
|
slot = normalize_character_slot(
|
||||||
|
{
|
||||||
|
"subject_type": subject_type,
|
||||||
|
"label": label,
|
||||||
|
"slot_seed": slot_seed,
|
||||||
|
"age": age,
|
||||||
|
"manual_age": manual_age,
|
||||||
|
"manual": manual,
|
||||||
|
"ethnicity": ethnicity,
|
||||||
|
"figure": figure,
|
||||||
|
"body": body,
|
||||||
|
"manual_body": manual_body,
|
||||||
|
"body_phrase": body_phrase,
|
||||||
|
"skin": skin,
|
||||||
|
"hair": hair,
|
||||||
|
"characteristics": characteristics,
|
||||||
|
"hair_config": hair_config,
|
||||||
|
"hair_color": hair_color,
|
||||||
|
"hair_length": hair_length,
|
||||||
|
"hair_style": hair_style,
|
||||||
|
"eyes": eyes,
|
||||||
|
"descriptor_detail": descriptor_detail,
|
||||||
|
"presence_mode": presence_mode,
|
||||||
|
"softcore_outfit": softcore_outfit,
|
||||||
|
"hardcore_clothing": hardcore_clothing,
|
||||||
|
"expression_enabled": expression_enabled,
|
||||||
|
"expression_intensity": expression_intensity,
|
||||||
|
"softcore_expression_intensity": softcore_expression_intensity,
|
||||||
|
"hardcore_expression_intensity": hardcore_expression_intensity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
slots = existing_slots + ([slot] if enabled else [])
|
||||||
|
cast = {
|
||||||
|
"profile_type": "character_cast",
|
||||||
|
"version": 1,
|
||||||
|
"slots": slots,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
|
||||||
|
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
|
||||||
|
"summary": slot["summary"] if enabled else "disabled",
|
||||||
|
"status": f"{len(slots)} slot(s)",
|
||||||
|
}
|
||||||
@@ -140,7 +140,12 @@ Already isolated:
|
|||||||
- character choice lists, descriptor detail/presence/slot-seed normalization,
|
- character choice lists, descriptor detail/presence/slot-seed normalization,
|
||||||
characteristic-list JSON builders/parsers, eye labels, hair config
|
characteristic-list JSON builders/parsers, eye labels, hair config
|
||||||
builders/parsers, and hair phrase helpers live in `character_config.py`;
|
builders/parsers, and hair phrase helpers live in `character_config.py`;
|
||||||
`prompt_builder.py` still resolves full character slots.
|
`prompt_builder.py` keeps public delegate wrappers.
|
||||||
|
- character slot JSON construction, character-cast parsing, slot normalization,
|
||||||
|
slot summary text, slot expression override policy, slot seed helpers, and
|
||||||
|
slot figure/ethnicity normalization live in `character_slot.py`;
|
||||||
|
`prompt_builder.py` keeps public delegate wrappers and still resolves
|
||||||
|
generation-time appearance context from normalized slots.
|
||||||
- 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
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ Core helper ownership:
|
|||||||
| `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. |
|
| `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. |
|
||||||
| `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. |
|
||||||
@@ -389,8 +390,9 @@ Important behavior:
|
|||||||
|
|
||||||
Edit targets:
|
Edit targets:
|
||||||
|
|
||||||
- Appearance field generation: `_context_from_character_slot`,
|
- Character slot JSON/parsing/summary: `character_slot.py`; appearance field
|
||||||
`_character_context_for_label`; character-slot label assignment:
|
generation: `_context_from_character_slot`, `_character_context_for_label`;
|
||||||
|
character-slot label 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`,
|
||||||
|
|||||||
+4
-4
@@ -10,6 +10,7 @@ try:
|
|||||||
character_body_choices,
|
character_body_choices,
|
||||||
character_descriptor_detail_choices,
|
character_descriptor_detail_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,
|
||||||
@@ -23,11 +24,10 @@ try:
|
|||||||
character_profile_choices,
|
character_profile_choices,
|
||||||
load_character_profile_json,
|
load_character_profile_json,
|
||||||
)
|
)
|
||||||
|
from .character_slot import build_character_slot_json
|
||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_character_slot_json,
|
|
||||||
character_ethnicity_choices,
|
character_ethnicity_choices,
|
||||||
character_figure_choices,
|
|
||||||
character_hardcore_clothing_state_choices,
|
character_hardcore_clothing_state_choices,
|
||||||
character_hardcore_clothing_values,
|
character_hardcore_clothing_values,
|
||||||
character_softcore_outfit_source_choices,
|
character_softcore_outfit_source_choices,
|
||||||
@@ -41,6 +41,7 @@ except ImportError: # Allows local smoke tests from the repository root.
|
|||||||
character_body_choices,
|
character_body_choices,
|
||||||
character_descriptor_detail_choices,
|
character_descriptor_detail_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,
|
||||||
@@ -54,11 +55,10 @@ except ImportError: # Allows local smoke tests from the repository root.
|
|||||||
character_profile_choices,
|
character_profile_choices,
|
||||||
load_character_profile_json,
|
load_character_profile_json,
|
||||||
)
|
)
|
||||||
|
from character_slot import build_character_slot_json
|
||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_character_slot_json,
|
|
||||||
character_ethnicity_choices,
|
character_ethnicity_choices,
|
||||||
character_figure_choices,
|
|
||||||
character_hardcore_clothing_state_choices,
|
character_hardcore_clothing_state_choices,
|
||||||
character_hardcore_clothing_values,
|
character_hardcore_clothing_values,
|
||||||
character_softcore_outfit_source_choices,
|
character_softcore_outfit_source_choices,
|
||||||
|
|||||||
+46
-237
@@ -28,6 +28,7 @@ try:
|
|||||||
from . import category_template_metadata as item_template_policy
|
from . import category_template_metadata as item_template_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 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
|
||||||
@@ -72,6 +73,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
import category_template_metadata as item_template_policy
|
import category_template_metadata as item_template_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 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
|
||||||
@@ -1240,7 +1242,7 @@ def character_ethnicity_choices() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def character_figure_choices() -> list[str]:
|
def character_figure_choices() -> list[str]:
|
||||||
return ["random", "curvy", "balanced", "bombshell"]
|
return character_policy.character_figure_choices()
|
||||||
|
|
||||||
|
|
||||||
def camera_detail_choices() -> list[str]:
|
def camera_detail_choices() -> list[str]:
|
||||||
@@ -1798,37 +1800,19 @@ def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_slot_expression_intensity(value: Any) -> float:
|
def _normalize_slot_expression_intensity(value: Any) -> float:
|
||||||
try:
|
return character_slot_policy.normalize_slot_expression_intensity(value)
|
||||||
intensity = float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return -1.0
|
|
||||||
if intensity < 0:
|
|
||||||
return -1.0
|
|
||||||
return _clamped_float(intensity, 0.5)
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
|
def _slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
|
||||||
if not slot:
|
return character_slot_policy.slot_expression_enabled(slot)
|
||||||
return True
|
|
||||||
return not _is_false(slot.get("expression_enabled", True))
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
|
def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
|
||||||
if not slot or not _slot_expression_enabled(slot):
|
return character_slot_policy.slot_expression_intensity(slot)
|
||||||
return None
|
|
||||||
intensity = _normalize_slot_expression_intensity(slot.get("expression_intensity"))
|
|
||||||
return intensity if intensity >= 0 else None
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
|
def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
|
||||||
if not slot or not _slot_expression_enabled(slot):
|
return character_slot_policy.slot_expression_intensity_for_phase(slot, phase)
|
||||||
return None
|
|
||||||
phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
|
|
||||||
if phase_key:
|
|
||||||
intensity = _normalize_slot_expression_intensity(slot.get(phase_key))
|
|
||||||
if intensity >= 0:
|
|
||||||
return intensity
|
|
||||||
return _slot_expression_intensity(slot)
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_slot_seed(value: Any) -> int:
|
def _normalize_slot_seed(value: Any) -> int:
|
||||||
@@ -1836,20 +1820,15 @@ def _normalize_slot_seed(value: Any) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _slot_seed(slot: dict[str, Any] | None) -> int:
|
def _slot_seed(slot: dict[str, Any] | None) -> int:
|
||||||
if not slot:
|
return character_slot_policy.slot_seed(slot)
|
||||||
return -1
|
|
||||||
return _normalize_slot_seed(slot.get("slot_seed"))
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
|
def _slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
|
||||||
seed = _slot_seed(slot)
|
return character_slot_policy.slot_seeded_rng(slot, salt)
|
||||||
if seed < 0:
|
|
||||||
return None
|
|
||||||
return random.Random(_row_seed(seed, 1, salt))
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
|
def _slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
|
||||||
return _slot_seeded_rng(slot, 701) or fallback_rng
|
return character_slot_policy.slot_context_rng(slot, fallback_rng)
|
||||||
|
|
||||||
|
|
||||||
def _slot_effective_figure(
|
def _slot_effective_figure(
|
||||||
@@ -1857,13 +1836,7 @@ def _slot_effective_figure(
|
|||||||
subject_type: str,
|
subject_type: str,
|
||||||
fallback_figure: str,
|
fallback_figure: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
raw_figure = str(slot.get("figure") or "random").strip()
|
return character_slot_policy.slot_effective_figure(slot, subject_type, fallback_figure)
|
||||||
if raw_figure in ("curvy", "balanced", "bombshell"):
|
|
||||||
return raw_figure
|
|
||||||
seeded_rng = _slot_seeded_rng(slot, 709)
|
|
||||||
if subject_type == "woman" and seeded_rng is not None:
|
|
||||||
return g.choose(seeded_rng, ["curvy", "balanced", "bombshell"])
|
|
||||||
return fallback_figure
|
|
||||||
|
|
||||||
|
|
||||||
def _mean(values: list[float]) -> float:
|
def _mean(values: list[float]) -> float:
|
||||||
@@ -2074,17 +2047,11 @@ def _descriptor_from_parts(
|
|||||||
|
|
||||||
|
|
||||||
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
||||||
choice = str(choice or "").strip()
|
return character_slot_policy.slot_manual_or_choice(choice, manual_value)
|
||||||
manual_value = str(manual_value or "").strip()
|
|
||||||
if choice == "manual":
|
|
||||||
return manual_value or "random"
|
|
||||||
if choice.lower() in CHARACTER_RANDOM_TOKENS:
|
|
||||||
return "random"
|
|
||||||
return choice
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_slot_ethnicity(value: Any) -> str:
|
def _normalize_slot_ethnicity(value: Any) -> str:
|
||||||
return normalize_ethnicity_filter(value, "random", allow_random=True)
|
return character_slot_policy.normalize_slot_ethnicity(value)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
|
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
|
||||||
@@ -2198,160 +2165,15 @@ def _hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
||||||
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
|
return character_slot_policy.normalize_character_slot(slot)
|
||||||
if subject_type not in ("woman", "man"):
|
|
||||||
subject_type = "woman"
|
|
||||||
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
|
|
||||||
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
|
|
||||||
if label == "AUTO_CHAIN":
|
|
||||||
label = "auto_chain"
|
|
||||||
if label not in CHARACTER_LABEL_CHOICES:
|
|
||||||
label = "auto_chain"
|
|
||||||
|
|
||||||
manual_config = _parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))
|
|
||||||
|
|
||||||
raw_age = str(slot.get("age") or "random")
|
|
||||||
raw_manual_age = str(slot.get("manual_age") or "").strip()
|
|
||||||
if not raw_manual_age and manual_config.get("manual_age"):
|
|
||||||
raw_manual_age = manual_config["manual_age"]
|
|
||||||
if raw_age.lower() in CHARACTER_RANDOM_TOKENS:
|
|
||||||
raw_age = "manual"
|
|
||||||
age = _slot_manual_or_choice(raw_age, raw_manual_age)
|
|
||||||
|
|
||||||
raw_body = str(slot.get("body") or "random")
|
|
||||||
raw_manual_body = str(slot.get("manual_body") or "").strip()
|
|
||||||
if not raw_manual_body and manual_config.get("manual_body"):
|
|
||||||
raw_manual_body = manual_config["manual_body"]
|
|
||||||
if raw_body.lower() in CHARACTER_RANDOM_TOKENS:
|
|
||||||
raw_body = "manual"
|
|
||||||
body = _slot_manual_or_choice(raw_body, raw_manual_body)
|
|
||||||
figure = str(slot.get("figure") or "random").strip()
|
|
||||||
if figure not in character_figure_choices():
|
|
||||||
figure = "random"
|
|
||||||
|
|
||||||
def manual_fallback(field: str) -> str:
|
|
||||||
direct = _slot_value(slot.get(field))
|
|
||||||
return direct or manual_config.get(field, "")
|
|
||||||
|
|
||||||
normalized = {
|
|
||||||
"profile_type": "character_slot",
|
|
||||||
"subject_type": subject_type,
|
|
||||||
"label": label,
|
|
||||||
"slot_seed": _normalize_slot_seed(slot.get("slot_seed")),
|
|
||||||
"age": age,
|
|
||||||
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
|
|
||||||
"figure": figure,
|
|
||||||
"body": body,
|
|
||||||
"body_phrase": manual_fallback("body_phrase"),
|
|
||||||
"skin": manual_fallback("skin"),
|
|
||||||
"hair": manual_fallback("hair"),
|
|
||||||
"manual": manual_config,
|
|
||||||
"characteristics": (
|
|
||||||
slot.get("characteristics")
|
|
||||||
if isinstance(slot.get("characteristics"), dict)
|
|
||||||
else _slot_value(slot.get("characteristics") or slot.get("characteristics_config"))
|
|
||||||
),
|
|
||||||
"hair_config": (
|
|
||||||
slot.get("hair_config")
|
|
||||||
if isinstance(slot.get("hair_config"), dict)
|
|
||||||
else _slot_value(slot.get("hair_config"))
|
|
||||||
),
|
|
||||||
"hair_color": _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES),
|
|
||||||
"hair_length": _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES),
|
|
||||||
"hair_style": _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES),
|
|
||||||
"eyes": manual_fallback("eyes"),
|
|
||||||
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
|
|
||||||
"presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type),
|
|
||||||
"softcore_outfit": manual_fallback("softcore_outfit"),
|
|
||||||
"hardcore_clothing": (
|
|
||||||
_slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
|
|
||||||
or manual_config.get("hardcore_clothing", "")
|
|
||||||
),
|
|
||||||
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
|
|
||||||
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
|
|
||||||
"softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
|
|
||||||
"hardcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
|
|
||||||
}
|
|
||||||
normalized["summary"] = _character_slot_summary(normalized)
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
|
def _parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
|
||||||
if not character_cast:
|
return character_slot_policy.parse_character_cast(character_cast)
|
||||||
return []
|
|
||||||
if isinstance(character_cast, list):
|
|
||||||
raw = character_cast
|
|
||||||
elif isinstance(character_cast, dict):
|
|
||||||
raw = character_cast
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
raw = json.loads(str(character_cast))
|
|
||||||
except json.JSONDecodeError as exc:
|
|
||||||
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
|
|
||||||
|
|
||||||
if isinstance(raw, list):
|
|
||||||
slots = raw
|
|
||||||
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
|
|
||||||
slots = raw["slots"]
|
|
||||||
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
|
|
||||||
slots = [raw]
|
|
||||||
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
|
|
||||||
slots = [raw]
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
return [_normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
|
|
||||||
|
|
||||||
|
|
||||||
def _character_slot_summary(slot: dict[str, Any]) -> str:
|
def _character_slot_summary(slot: dict[str, Any]) -> str:
|
||||||
subject = str(slot.get("subject_type") or "woman")
|
return character_slot_policy.character_slot_summary(slot)
|
||||||
label = str(slot.get("label") or "auto_chain")
|
|
||||||
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
|
|
||||||
parts = [
|
|
||||||
subject,
|
|
||||||
label_text,
|
|
||||||
f"seed={slot.get('slot_seed')}" if _slot_seed(slot) >= 0 else "",
|
|
||||||
f"age={slot.get('age', 'random')}",
|
|
||||||
f"ethnicity={slot.get('ethnicity', 'random')}",
|
|
||||||
f"figure={slot.get('figure', 'random')}",
|
|
||||||
f"body={slot.get('body', 'random')}",
|
|
||||||
f"detail={slot.get('descriptor_detail', 'auto')}",
|
|
||||||
]
|
|
||||||
parts = [part for part in parts if part]
|
|
||||||
if _slot_is_pov(slot):
|
|
||||||
parts.append("presence=pov")
|
|
||||||
if not _slot_expression_enabled(slot):
|
|
||||||
parts.append("expression=disabled")
|
|
||||||
else:
|
|
||||||
expression_intensity = _slot_expression_intensity(slot)
|
|
||||||
if expression_intensity is not None:
|
|
||||||
parts.append(f"expression={expression_intensity:.2f}")
|
|
||||||
softcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "softcore")
|
|
||||||
hardcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "hardcore")
|
|
||||||
if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
|
|
||||||
parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
|
|
||||||
if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
|
|
||||||
parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
|
|
||||||
if slot.get("softcore_outfit"):
|
|
||||||
parts.append(f"soft_outfit={slot['softcore_outfit']}")
|
|
||||||
if slot.get("hardcore_clothing"):
|
|
||||||
parts.append(f"hard_clothing={slot['hardcore_clothing']}")
|
|
||||||
characteristics = _parse_characteristics_config(slot.get("characteristics"))
|
|
||||||
characteristics_summary = _characteristics_summary(characteristics)
|
|
||||||
if characteristics_summary != "characteristics unrestricted":
|
|
||||||
parts.append(f"characteristics={characteristics_summary}")
|
|
||||||
hair_config = _parse_hair_config(slot.get("hair_config"))
|
|
||||||
hair_config_summary = _hair_config_summary(hair_config)
|
|
||||||
if hair_config_summary != "hair unrestricted":
|
|
||||||
parts.append(f"hair={hair_config_summary}")
|
|
||||||
for key in ("hair_color", "hair_length", "hair_style"):
|
|
||||||
value = slot.get(key)
|
|
||||||
if value and value != "random":
|
|
||||||
parts.append(f"{key}={value}")
|
|
||||||
for key in ("body_phrase", "skin", "hair", "eyes"):
|
|
||||||
value = slot.get(key)
|
|
||||||
if value:
|
|
||||||
parts.append(f"{key}={value}")
|
|
||||||
return "; ".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def build_character_slot_json(
|
def build_character_slot_json(
|
||||||
@@ -2385,50 +2207,37 @@ def build_character_slot_json(
|
|||||||
softcore_outfit: str = "",
|
softcore_outfit: str = "",
|
||||||
hardcore_clothing: str = "",
|
hardcore_clothing: str = "",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
existing_slots = _parse_character_cast(character_cast)
|
return character_slot_policy.build_character_slot_json(
|
||||||
slot = _normalize_character_slot(
|
subject_type=subject_type,
|
||||||
{
|
label=label,
|
||||||
"subject_type": subject_type,
|
slot_seed=slot_seed,
|
||||||
"label": label,
|
age=age,
|
||||||
"slot_seed": slot_seed,
|
manual_age=manual_age,
|
||||||
"age": age,
|
manual=manual,
|
||||||
"manual_age": manual_age,
|
ethnicity=ethnicity,
|
||||||
"manual": manual,
|
figure=figure,
|
||||||
"ethnicity": ethnicity,
|
body=body,
|
||||||
"figure": figure,
|
manual_body=manual_body,
|
||||||
"body": body,
|
body_phrase=body_phrase,
|
||||||
"manual_body": manual_body,
|
skin=skin,
|
||||||
"body_phrase": body_phrase,
|
hair=hair,
|
||||||
"skin": skin,
|
characteristics=characteristics,
|
||||||
"hair": hair,
|
hair_config=hair_config,
|
||||||
"characteristics": characteristics,
|
hair_color=hair_color,
|
||||||
"hair_config": hair_config,
|
hair_length=hair_length,
|
||||||
"hair_color": hair_color,
|
hair_style=hair_style,
|
||||||
"hair_length": hair_length,
|
eyes=eyes,
|
||||||
"hair_style": hair_style,
|
descriptor_detail=descriptor_detail,
|
||||||
"eyes": eyes,
|
expression_enabled=expression_enabled,
|
||||||
"descriptor_detail": descriptor_detail,
|
expression_intensity=expression_intensity,
|
||||||
"presence_mode": presence_mode,
|
enabled=enabled,
|
||||||
"softcore_outfit": softcore_outfit,
|
character_cast=character_cast,
|
||||||
"hardcore_clothing": hardcore_clothing,
|
presence_mode=presence_mode,
|
||||||
"expression_enabled": expression_enabled,
|
softcore_expression_intensity=softcore_expression_intensity,
|
||||||
"expression_intensity": expression_intensity,
|
hardcore_expression_intensity=hardcore_expression_intensity,
|
||||||
"softcore_expression_intensity": softcore_expression_intensity,
|
softcore_outfit=softcore_outfit,
|
||||||
"hardcore_expression_intensity": hardcore_expression_intensity,
|
hardcore_clothing=hardcore_clothing,
|
||||||
}
|
|
||||||
)
|
)
|
||||||
slots = existing_slots + ([slot] if enabled else [])
|
|
||||||
cast = {
|
|
||||||
"profile_type": "character_cast",
|
|
||||||
"version": 1,
|
|
||||||
"slots": slots,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
|
|
||||||
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
|
|
||||||
"summary": slot["summary"] if enabled else "disabled",
|
|
||||||
"status": f"{len(slots)} slot(s)",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _slot_explicit_label(slot: dict[str, Any]) -> str:
|
def _slot_explicit_label(slot: dict[str, Any]) -> str:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import cast_context # noqa: E402
|
|||||||
import category_template_metadata # noqa: E402
|
import category_template_metadata # 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 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
|
||||||
@@ -804,6 +805,7 @@ 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(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("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("fat" in character_config.character_man_body_choices(), "Man body pool lost fat option")
|
||||||
|
_expect(pb.character_figure_choices() == character_config.character_figure_choices(), "Character figure choices should delegate")
|
||||||
_expect("platinum_blonde" in character_config.character_hair_color_choices(), "Hair color choices lost platinum blonde")
|
_expect("platinum_blonde" in character_config.character_hair_color_choices(), "Hair color choices lost platinum blonde")
|
||||||
|
|
||||||
traits = json.loads(
|
traits = json.loads(
|
||||||
@@ -872,6 +874,67 @@ def smoke_character_config_policy() -> None:
|
|||||||
"Krea POV composition cleanup should delegate shared replacements and strip builder annotation",
|
"Krea POV composition cleanup should delegate shared replacements and strip builder annotation",
|
||||||
)
|
)
|
||||||
_expect(character_config.normalize_slot_seed(0xFFFFFFFF + 99) == 0xFFFFFFFF, "Slot seed clamp changed")
|
_expect(character_config.normalize_slot_seed(0xFFFFFFFF + 99) == 0xFFFFFFFF, "Slot seed clamp changed")
|
||||||
|
slot_result = character_slot.build_character_slot_json(
|
||||||
|
subject_type="man",
|
||||||
|
label="Man B",
|
||||||
|
slot_seed=123,
|
||||||
|
age="manual",
|
||||||
|
manual_age="44-year-old adult",
|
||||||
|
ethnicity="western_european",
|
||||||
|
figure="balanced",
|
||||||
|
body="manual",
|
||||||
|
manual_body="stocky",
|
||||||
|
descriptor_detail="compact",
|
||||||
|
expression_intensity=1.5,
|
||||||
|
softcore_expression_intensity=0.25,
|
||||||
|
hardcore_expression_intensity=-1,
|
||||||
|
presence_mode="pov",
|
||||||
|
hair_color="dark brown",
|
||||||
|
hair_length="short",
|
||||||
|
hair_style="straight",
|
||||||
|
softcore_outfit="buttoned shirt",
|
||||||
|
hardcore_clothing="shirt pushed open",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
pb.build_character_slot_json(
|
||||||
|
subject_type="man",
|
||||||
|
label="Man B",
|
||||||
|
slot_seed=123,
|
||||||
|
age="manual",
|
||||||
|
manual_age="44-year-old adult",
|
||||||
|
ethnicity="western_european",
|
||||||
|
figure="balanced",
|
||||||
|
body="manual",
|
||||||
|
manual_body="stocky",
|
||||||
|
descriptor_detail="compact",
|
||||||
|
expression_intensity=1.5,
|
||||||
|
softcore_expression_intensity=0.25,
|
||||||
|
hardcore_expression_intensity=-1,
|
||||||
|
presence_mode="pov",
|
||||||
|
hair_color="dark brown",
|
||||||
|
hair_length="short",
|
||||||
|
hair_style="straight",
|
||||||
|
softcore_outfit="buttoned shirt",
|
||||||
|
hardcore_clothing="shirt pushed open",
|
||||||
|
)
|
||||||
|
== slot_result,
|
||||||
|
"Prompt builder character slot JSON should delegate to character_slot",
|
||||||
|
)
|
||||||
|
slot = json.loads(slot_result["character_slot"])
|
||||||
|
_expect(slot.get("age") == "44-year-old adult", "Character slot manual age normalization changed")
|
||||||
|
_expect(slot.get("body") == "stocky", "Character slot manual body normalization changed")
|
||||||
|
_expect(slot.get("presence_mode") == "pov", "Character slot POV presence normalization changed")
|
||||||
|
_expect(slot.get("expression_intensity") == 1.0, "Character slot expression intensity clamp changed")
|
||||||
|
_expect(
|
||||||
|
character_slot.slot_expression_intensity_for_phase(slot, "softcore") == 0.25
|
||||||
|
and character_slot.slot_expression_intensity_for_phase(slot, "hardcore") == 1.0,
|
||||||
|
"Character slot phase expression fallback changed",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
pb._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",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def smoke_character_profile_policy() -> None:
|
def smoke_character_profile_policy() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user