From cf4fec34b82651eb68a2e53cffccd31ae9c000a9 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 24 Jun 2026 21:33:28 +0200 Subject: [PATCH] Add seeded random character slots --- README.md | 6 +++++ __init__.py | 9 +++++++ prompt_builder.py | 67 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bea2fb2..0e38ed6 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/__init__.py b/__init__.py index 69c21da..e392b08 100644 --- a/__init__.py +++ b/__init__.py @@ -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, diff --git a/prompt_builder.py b/prompt_builder.py index 685bd30..e055015 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -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: