Add seeded random character slots
This commit is contained in:
@@ -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
|
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.
|
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
|
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
|
`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`
|
body choices and omits figure bias. The older generic `SxCP Character Slot`
|
||||||
|
|||||||
@@ -756,6 +756,7 @@ class SxCPCharacterSlot:
|
|||||||
"enabled": ("BOOLEAN", {"default": True}),
|
"enabled": ("BOOLEAN", {"default": True}),
|
||||||
"subject_type": (["woman", "man"], {"default": "woman"}),
|
"subject_type": (["woman", "man"], {"default": "woman"}),
|
||||||
"label": (character_label_choices(), {"default": "auto_chain"}),
|
"label": (character_label_choices(), {"default": "auto_chain"}),
|
||||||
|
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
|
||||||
"age": (character_age_choices(), {"default": "random"}),
|
"age": (character_age_choices(), {"default": "random"}),
|
||||||
"manual_age": ("STRING", {"default": ""}),
|
"manual_age": ("STRING", {"default": ""}),
|
||||||
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
||||||
@@ -790,6 +791,7 @@ class SxCPCharacterSlot:
|
|||||||
enabled,
|
enabled,
|
||||||
subject_type,
|
subject_type,
|
||||||
label,
|
label,
|
||||||
|
slot_seed,
|
||||||
age,
|
age,
|
||||||
manual_age,
|
manual_age,
|
||||||
ethnicity,
|
ethnicity,
|
||||||
@@ -813,6 +815,7 @@ class SxCPCharacterSlot:
|
|||||||
result = build_character_slot_json(
|
result = build_character_slot_json(
|
||||||
subject_type=subject_type,
|
subject_type=subject_type,
|
||||||
label=label,
|
label=label,
|
||||||
|
slot_seed=slot_seed,
|
||||||
age=age,
|
age=age,
|
||||||
manual_age=manual_age,
|
manual_age=manual_age,
|
||||||
ethnicity=ethnicity,
|
ethnicity=ethnicity,
|
||||||
@@ -844,6 +847,7 @@ class SxCPWomanSlot:
|
|||||||
"required": {
|
"required": {
|
||||||
"enabled": ("BOOLEAN", {"default": True}),
|
"enabled": ("BOOLEAN", {"default": True}),
|
||||||
"label": (character_label_choices(), {"default": "auto_chain"}),
|
"label": (character_label_choices(), {"default": "auto_chain"}),
|
||||||
|
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
|
||||||
"age": (character_age_choices(), {"default": "random"}),
|
"age": (character_age_choices(), {"default": "random"}),
|
||||||
"manual_age": ("STRING", {"default": ""}),
|
"manual_age": ("STRING", {"default": ""}),
|
||||||
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
||||||
@@ -876,6 +880,7 @@ class SxCPWomanSlot:
|
|||||||
self,
|
self,
|
||||||
enabled,
|
enabled,
|
||||||
label,
|
label,
|
||||||
|
slot_seed,
|
||||||
age,
|
age,
|
||||||
manual_age,
|
manual_age,
|
||||||
ethnicity,
|
ethnicity,
|
||||||
@@ -898,6 +903,7 @@ class SxCPWomanSlot:
|
|||||||
result = build_character_slot_json(
|
result = build_character_slot_json(
|
||||||
subject_type="woman",
|
subject_type="woman",
|
||||||
label=label,
|
label=label,
|
||||||
|
slot_seed=slot_seed,
|
||||||
age=age,
|
age=age,
|
||||||
manual_age=manual_age,
|
manual_age=manual_age,
|
||||||
ethnicity=ethnicity,
|
ethnicity=ethnicity,
|
||||||
@@ -928,6 +934,7 @@ class SxCPManSlot:
|
|||||||
"required": {
|
"required": {
|
||||||
"enabled": ("BOOLEAN", {"default": True}),
|
"enabled": ("BOOLEAN", {"default": True}),
|
||||||
"label": (character_label_choices(), {"default": "auto_chain"}),
|
"label": (character_label_choices(), {"default": "auto_chain"}),
|
||||||
|
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
|
||||||
"age": (character_age_choices(), {"default": "random"}),
|
"age": (character_age_choices(), {"default": "random"}),
|
||||||
"manual_age": ("STRING", {"default": ""}),
|
"manual_age": ("STRING", {"default": ""}),
|
||||||
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
||||||
@@ -960,6 +967,7 @@ class SxCPManSlot:
|
|||||||
self,
|
self,
|
||||||
enabled,
|
enabled,
|
||||||
label,
|
label,
|
||||||
|
slot_seed,
|
||||||
age,
|
age,
|
||||||
manual_age,
|
manual_age,
|
||||||
ethnicity,
|
ethnicity,
|
||||||
@@ -982,6 +990,7 @@ class SxCPManSlot:
|
|||||||
result = build_character_slot_json(
|
result = build_character_slot_json(
|
||||||
subject_type="man",
|
subject_type="man",
|
||||||
label=label,
|
label=label,
|
||||||
|
slot_seed=slot_seed,
|
||||||
age=age,
|
age=age,
|
||||||
manual_age=manual_age,
|
manual_age=manual_age,
|
||||||
ethnicity=ethnicity,
|
ethnicity=ethnicity,
|
||||||
|
|||||||
+63
-4
@@ -178,6 +178,7 @@ CHARACTER_MAN_BODY_CHOICES = [
|
|||||||
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
|
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
|
||||||
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
|
||||||
|
|
||||||
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||||
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
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)
|
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:
|
def _mean(values: list[float]) -> float:
|
||||||
return sum(values) / len(values)
|
return sum(values) / len(values)
|
||||||
|
|
||||||
@@ -2468,6 +2510,7 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"profile_type": "character_slot",
|
"profile_type": "character_slot",
|
||||||
"subject_type": subject_type,
|
"subject_type": subject_type,
|
||||||
"label": label,
|
"label": label,
|
||||||
|
"slot_seed": _normalize_slot_seed(slot.get("slot_seed")),
|
||||||
"age": age,
|
"age": age,
|
||||||
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
|
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
|
||||||
"figure": figure,
|
"figure": figure,
|
||||||
@@ -2522,12 +2565,14 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
|
|||||||
parts = [
|
parts = [
|
||||||
subject,
|
subject,
|
||||||
label_text,
|
label_text,
|
||||||
|
f"seed={slot.get('slot_seed')}" if _slot_seed(slot) >= 0 else "",
|
||||||
f"age={slot.get('age', 'random')}",
|
f"age={slot.get('age', 'random')}",
|
||||||
f"ethnicity={slot.get('ethnicity', 'random')}",
|
f"ethnicity={slot.get('ethnicity', 'random')}",
|
||||||
f"figure={slot.get('figure', 'random')}",
|
f"figure={slot.get('figure', 'random')}",
|
||||||
f"body={slot.get('body', 'random')}",
|
f"body={slot.get('body', 'random')}",
|
||||||
f"detail={slot.get('descriptor_detail', 'auto')}",
|
f"detail={slot.get('descriptor_detail', 'auto')}",
|
||||||
]
|
]
|
||||||
|
parts = [part for part in parts if part]
|
||||||
if _slot_is_pov(slot):
|
if _slot_is_pov(slot):
|
||||||
parts.append("presence=pov")
|
parts.append("presence=pov")
|
||||||
if not _slot_expression_enabled(slot):
|
if not _slot_expression_enabled(slot):
|
||||||
@@ -2556,6 +2601,7 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
|
|||||||
def build_character_slot_json(
|
def build_character_slot_json(
|
||||||
subject_type: str = "woman",
|
subject_type: str = "woman",
|
||||||
label: str = "auto_chain",
|
label: str = "auto_chain",
|
||||||
|
slot_seed: int = -1,
|
||||||
age: str = "random",
|
age: str = "random",
|
||||||
manual_age: str = "",
|
manual_age: str = "",
|
||||||
ethnicity: str = "random",
|
ethnicity: str = "random",
|
||||||
@@ -2582,6 +2628,7 @@ def build_character_slot_json(
|
|||||||
{
|
{
|
||||||
"subject_type": subject_type,
|
"subject_type": subject_type,
|
||||||
"label": label,
|
"label": label,
|
||||||
|
"slot_seed": slot_seed,
|
||||||
"age": age,
|
"age": age,
|
||||||
"manual_age": manual_age,
|
"manual_age": manual_age,
|
||||||
"ethnicity": ethnicity,
|
"ethnicity": ethnicity,
|
||||||
@@ -2786,14 +2833,14 @@ def _context_from_character_slot(
|
|||||||
no_black: bool,
|
no_black: bool,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
slot_ethnicity = _slot_value(slot.get("ethnicity"))
|
slot_ethnicity = _slot_value(slot.get("ethnicity"))
|
||||||
slot_figure = _slot_value(slot.get("figure"))
|
|
||||||
slot_body = _slot_value(slot.get("body"))
|
slot_body = _slot_value(slot.get("body"))
|
||||||
effective_ethnicity = slot_ethnicity or ethnicity
|
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_plus = bool(no_plus_women) and not slot_body
|
||||||
effective_no_black = bool(no_black) and not slot_ethnicity
|
effective_no_black = bool(no_black) and not slot_ethnicity
|
||||||
|
appearance_rng = _slot_context_rng(slot, rng)
|
||||||
context = _appearance_for_subject(
|
context = _appearance_for_subject(
|
||||||
rng,
|
appearance_rng,
|
||||||
subject_type,
|
subject_type,
|
||||||
effective_ethnicity,
|
effective_ethnicity,
|
||||||
effective_figure,
|
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)
|
slots = _parse_character_cast(character_slot)
|
||||||
if not slots:
|
if not slots:
|
||||||
return {}
|
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:
|
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user