Add seeded random character slots

This commit is contained in:
2026-06-24 21:33:28 +02:00
parent 79661c396a
commit cf4fec34b8
3 changed files with 78 additions and 4 deletions
+6
View File
@@ -118,6 +118,12 @@ optional overrides for age, ethnicity, body/body phrase, skin, hair, and eyes.
Leave any field on `random` or blank to let the generator fill that part from
the normal pools; set exact values only where you want control.
Each slot has `slot_seed`. Leave it at `-1` to follow the generator's normal
person seed. Set any fixed value when the slot's `random` fields should resolve
as one stable character across scene, pose, outfit, or row rerolls. This seed is
shared by that slot's random age/body/appearance choices, so you can keep the
same participant while changing other generation axes.
Use `Woman Slot` for women because it exposes woman-focused body choices and a
`figure_bias` selector. Use `Man Slot` for men because it exposes man-focused
body choices and omits figure bias. The older generic `SxCP Character Slot`
+9
View File
@@ -756,6 +756,7 @@ class SxCPCharacterSlot:
"enabled": ("BOOLEAN", {"default": True}),
"subject_type": (["woman", "man"], {"default": "woman"}),
"label": (character_label_choices(), {"default": "auto_chain"}),
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
"age": (character_age_choices(), {"default": "random"}),
"manual_age": ("STRING", {"default": ""}),
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
@@ -790,6 +791,7 @@ class SxCPCharacterSlot:
enabled,
subject_type,
label,
slot_seed,
age,
manual_age,
ethnicity,
@@ -813,6 +815,7 @@ class SxCPCharacterSlot:
result = build_character_slot_json(
subject_type=subject_type,
label=label,
slot_seed=slot_seed,
age=age,
manual_age=manual_age,
ethnicity=ethnicity,
@@ -844,6 +847,7 @@ class SxCPWomanSlot:
"required": {
"enabled": ("BOOLEAN", {"default": True}),
"label": (character_label_choices(), {"default": "auto_chain"}),
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
"age": (character_age_choices(), {"default": "random"}),
"manual_age": ("STRING", {"default": ""}),
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
@@ -876,6 +880,7 @@ class SxCPWomanSlot:
self,
enabled,
label,
slot_seed,
age,
manual_age,
ethnicity,
@@ -898,6 +903,7 @@ class SxCPWomanSlot:
result = build_character_slot_json(
subject_type="woman",
label=label,
slot_seed=slot_seed,
age=age,
manual_age=manual_age,
ethnicity=ethnicity,
@@ -928,6 +934,7 @@ class SxCPManSlot:
"required": {
"enabled": ("BOOLEAN", {"default": True}),
"label": (character_label_choices(), {"default": "auto_chain"}),
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
"age": (character_age_choices(), {"default": "random"}),
"manual_age": ("STRING", {"default": ""}),
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
@@ -960,6 +967,7 @@ class SxCPManSlot:
self,
enabled,
label,
slot_seed,
age,
manual_age,
ethnicity,
@@ -982,6 +990,7 @@ class SxCPManSlot:
result = build_character_slot_json(
subject_type="man",
label=label,
slot_seed=slot_seed,
age=age,
manual_age=manual_age,
ethnicity=ethnicity,
+63 -4
View File
@@ -178,6 +178,7 @@ CHARACTER_MAN_BODY_CHOICES = [
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
@@ -2307,6 +2308,47 @@ def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str
return _slot_expression_intensity(slot)
def _normalize_slot_seed(value: Any) -> int:
try:
seed = int(value)
except (TypeError, ValueError):
return -1
if seed < 0:
return -1
return min(seed, CHARACTER_SLOT_SEED_MAX)
def _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(_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:
return g.choose(seeded_rng, ["curvy", "balanced", "bombshell"])
return fallback_figure
def _mean(values: list[float]) -> float:
return sum(values) / len(values)
@@ -2468,6 +2510,7 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
"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,
@@ -2522,12 +2565,14 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
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):
@@ -2556,6 +2601,7 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
def build_character_slot_json(
subject_type: str = "woman",
label: str = "auto_chain",
slot_seed: int = -1,
age: str = "random",
manual_age: str = "",
ethnicity: str = "random",
@@ -2582,6 +2628,7 @@ def build_character_slot_json(
{
"subject_type": subject_type,
"label": label,
"slot_seed": slot_seed,
"age": age,
"manual_age": manual_age,
"ethnicity": ethnicity,
@@ -2786,14 +2833,14 @@ def _context_from_character_slot(
no_black: bool,
) -> dict[str, str]:
slot_ethnicity = _slot_value(slot.get("ethnicity"))
slot_figure = _slot_value(slot.get("figure"))
slot_body = _slot_value(slot.get("body"))
effective_ethnicity = slot_ethnicity or ethnicity
effective_figure = slot_figure if slot_figure in ("curvy", "balanced", "bombshell") else figure
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(
rng,
appearance_rng,
subject_type,
effective_ethnicity,
effective_figure,
@@ -2914,7 +2961,19 @@ def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dic
slots = _parse_character_cast(character_slot)
if not slots:
return {}
return slots[-1]
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: