from __future__ import annotations import json import random try: from .category_cast_config import ( build_cast_config_json, build_category_config_json, cast_preset_choices, category_preset_choices, ) from .prompt_builder import ( subcategory_choices, ) from .seed_config import configured_seed_from_axes from .location_config import ( build_composition_pool_json, build_location_pool_json, build_thematic_location_json, composition_pool_preset_choices, location_pool_preset_choices, location_theme_choices, ) from .style_config import ( build_style_config_json, style_combine_mode_choices, style_pool_preset_choices, ) except ImportError: # Allows local smoke tests from the repository root. from category_cast_config import ( build_cast_config_json, build_category_config_json, cast_preset_choices, category_preset_choices, ) from prompt_builder import ( subcategory_choices, ) from seed_config import configured_seed_from_axes from location_config import ( build_composition_pool_json, build_location_pool_json, build_thematic_location_json, composition_pool_preset_choices, location_pool_preset_choices, location_theme_choices, ) from style_config import ( build_style_config_json, style_combine_mode_choices, style_pool_preset_choices, ) SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG" SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG" SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG" SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG" SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG" SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG" class SxCPCategoryPreset: @classmethod def INPUT_TYPES(cls): return { "required": { "preset": (category_preset_choices(), {"default": "auto_weighted"}), "subcategory": (subcategory_choices(), {"default": "random"}), } } RETURN_TYPES = (SXCP_CATEGORY_CONFIG, "STRING", "STRING") RETURN_NAMES = ("category_config", "category", "subcategory") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, preset, subcategory): config = build_category_config_json(preset=preset, subcategory=subcategory) parsed = json.loads(config) return config, parsed["category"], parsed["subcategory"] class SxCPLocationPool: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "preset": (location_pool_preset_choices(), {"default": "custom_only"}), "custom_locations": ("STRING", {"default": "", "multiline": True}), }, "optional": { "location_config": (SXCP_LOCATION_CONFIG,), }, } RETURN_TYPES = (SXCP_LOCATION_CONFIG, "STRING") RETURN_NAMES = ("location_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, enabled, combine_mode, preset, custom_locations, location_config=""): config = build_location_pool_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_locations=custom_locations or "", location_config=location_config or "", ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPCompositionPool: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "preset": (composition_pool_preset_choices(), {"default": "no_outfit_check"}), "custom_compositions": ("STRING", {"default": "", "multiline": True}), }, "optional": { "composition_config": (SXCP_COMPOSITION_CONFIG,), }, } RETURN_TYPES = (SXCP_COMPOSITION_CONFIG, "STRING") RETURN_NAMES = ("composition_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, enabled, combine_mode, preset, custom_compositions, composition_config=""): config = build_composition_pool_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_compositions=custom_compositions or "", composition_config=composition_config or "", ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPLocationTheme: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "theme": (location_theme_choices(), {"default": "semi_public_affair"}), "custom_locations": ("STRING", {"default": "", "multiline": True}), "custom_compositions": ("STRING", {"default": "", "multiline": True}), }, "optional": { "location_config": (SXCP_LOCATION_CONFIG,), "composition_config": (SXCP_COMPOSITION_CONFIG,), }, } RETURN_TYPES = (SXCP_LOCATION_CONFIG, SXCP_COMPOSITION_CONFIG, "STRING") RETURN_NAMES = ("location_config", "composition_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, combine_mode, theme, custom_locations, custom_compositions, location_config="", composition_config="", ): return build_thematic_location_json( enabled=enabled, combine_mode=combine_mode, theme=theme, custom_locations=custom_locations or "", custom_compositions=custom_compositions or "", location_config=location_config or "", composition_config=composition_config or "", ) class SxCPStylePool: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (style_combine_mode_choices(), {"default": "replace"}), "preset": (style_pool_preset_choices(), {"default": "realistic_photo"}), "custom_style": ("STRING", {"default": "", "multiline": True}), "custom_positive_suffix": ("STRING", {"default": "", "multiline": True}), "custom_negative": ("STRING", {"default": "", "multiline": True}), }, "optional": { "style_config": (SXCP_STYLE_CONFIG,), }, } RETURN_TYPES = (SXCP_STYLE_CONFIG, "STRING") RETURN_NAMES = ("style_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, combine_mode, preset, custom_style, custom_positive_suffix, custom_negative, style_config="", ): config = build_style_config_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_style=custom_style or "", custom_positive_suffix=custom_positive_suffix or "", custom_negative=custom_negative or "", style_config=style_config or "", ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPCastControl: @classmethod def INPUT_TYPES(cls): return { "required": { "cast_mode": (cast_preset_choices(), {"default": "mixed_couple"}), "women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), } } RETURN_TYPES = (SXCP_CAST_CONFIG, "INT", "INT", "STRING") RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, cast_mode, women_count, men_count): config = build_cast_config_json(cast_mode=cast_mode, women_count=women_count, men_count=men_count) parsed = json.loads(config) summary = f"{parsed['women_count']} women, {parsed['men_count']} men" return config, parsed["women_count"], parsed["men_count"], summary class SxCPCastBias: @classmethod def INPUT_TYPES(cls): return { "required": { "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}), "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "women_weights": ("STRING", {"default": "0.60,0.25,0.10,0.05"}), "women_start_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "men_weights": ("STRING", {"default": "0.45,0.40,0.10,0.05"}), "men_start_count": ("INT", {"default": 0, "min": 0, "max": 12, "step": 1}), "empty_behavior": (["force_one_woman", "force_one_man", "allow_empty"], {"default": "force_one_woman"}), }, "optional": { "seed_config": (SXCP_SEED_CONFIG,), }, } RETURN_TYPES = (SXCP_CAST_CONFIG, "INT", "INT", "STRING") RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary") FUNCTION = "build" CATEGORY = "prompt_builder" @staticmethod def _configured_cast_seed(seed_config): return configured_seed_from_axes( seed_config, ("category", "content", "role"), extra_keys=("seed", "global_seed"), ) @staticmethod def _weight_pairs(weights_text, start_count): pairs = [] start = max(0, min(12, int(start_count))) parts = str(weights_text or "").replace("\n", ",").split(",") for offset, raw in enumerate(parts): count = start + offset if count > 12: break try: weight = float(raw.strip()) except (TypeError, ValueError): continue if weight > 0: pairs.append((count, weight)) return pairs or [(start, 1.0)] @staticmethod def _weighted_count(rng, pairs): total = sum(weight for _count, weight in pairs) point = rng.random() * total upto = 0.0 for count, weight in pairs: upto += weight if point <= upto: return int(count) return int(pairs[-1][0]) @classmethod def IS_CHANGED(cls, *args, **kwargs): seed_value = kwargs.get("seed") if seed_value is None and args: seed_value = args[0] seed_config = kwargs.get("seed_config", "") if not seed_config and len(args) > 7: seed_config = args[7] try: seed = int(seed_value) except (TypeError, ValueError): seed = -1 if seed < 0 and cls._configured_cast_seed(seed_config) is None: return random.random() return tuple(args), tuple(sorted(kwargs.items())) def build( self, seed, row_number, women_weights, women_start_count, men_weights, men_start_count, empty_behavior, seed_config="", ): configured_seed = self._configured_cast_seed(seed_config) if configured_seed is None and int(seed) < 0: rng = random.Random(random.getrandbits(64)) else: cast_seed = configured_seed if configured_seed is not None else int(seed) rng = random.Random(f"sxcp_cast_bias:{cast_seed}:{int(row_number)}") women_pairs = self._weight_pairs(women_weights, women_start_count) men_pairs = self._weight_pairs(men_weights, men_start_count) women_count = self._weighted_count(rng, women_pairs) men_count = self._weighted_count(rng, men_pairs) if women_count + men_count == 0: if empty_behavior == "force_one_man": men_count = 1 elif empty_behavior != "allow_empty": women_count = 1 config = build_cast_config_json(cast_mode="custom_counts", women_count=women_count, men_count=men_count) parsed = json.loads(config) summary = f"weighted cast: {parsed['women_count']} women, {parsed['men_count']} men" return config, parsed["women_count"], parsed["men_count"], summary NODE_CLASS_MAPPINGS = { "SxCPCategoryPreset": SxCPCategoryPreset, "SxCPLocationPool": SxCPLocationPool, "SxCPCompositionPool": SxCPCompositionPool, "SxCPLocationTheme": SxCPLocationTheme, "SxCPStylePool": SxCPStylePool, "SxCPCastControl": SxCPCastControl, "SxCPCastBias": SxCPCastBias, } NODE_DISPLAY_NAME_MAPPINGS = { "SxCPCategoryPreset": "SxCP Category Preset", "SxCPLocationPool": "SxCP Location Pool", "SxCPCompositionPool": "SxCP Composition Pool", "SxCPLocationTheme": "SxCP Location Theme", "SxCPStylePool": "SxCP Style Pool", "SxCPCastControl": "SxCP Cast Control", "SxCPCastBias": "SxCP Cast Bias", }