Extract route config nodes
This commit is contained in:
+10
-304
@@ -404,18 +404,19 @@ try:
|
|||||||
NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
|
from .node_route_config import (
|
||||||
|
NODE_CLASS_MAPPINGS as ROUTE_CONFIG_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
from .node_seed_resolution import (
|
from .node_seed_resolution import (
|
||||||
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
build_cast_config_json,
|
|
||||||
build_category_config_json,
|
|
||||||
build_character_slot_json,
|
build_character_slot_json,
|
||||||
build_character_manual_config_json,
|
build_character_manual_config_json,
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_characteristics_config_json,
|
build_characteristics_config_json,
|
||||||
build_composition_pool_json,
|
|
||||||
build_ethnicity_list_json,
|
build_ethnicity_list_json,
|
||||||
build_filter_config_json,
|
build_filter_config_json,
|
||||||
build_generation_profile_json,
|
build_generation_profile_json,
|
||||||
@@ -423,15 +424,11 @@ try:
|
|||||||
build_hardcore_action_filter_json,
|
build_hardcore_action_filter_json,
|
||||||
build_hardcore_position_pool_json,
|
build_hardcore_position_pool_json,
|
||||||
build_insta_of_options_json,
|
build_insta_of_options_json,
|
||||||
build_location_pool_json,
|
|
||||||
build_thematic_location_json,
|
|
||||||
build_insta_of_pair,
|
build_insta_of_pair,
|
||||||
build_prompt,
|
build_prompt,
|
||||||
build_prompt_from_configs,
|
build_prompt_from_configs,
|
||||||
camera_detail_choices,
|
camera_detail_choices,
|
||||||
camera_mode_choices,
|
camera_mode_choices,
|
||||||
cast_preset_choices,
|
|
||||||
category_preset_choices,
|
|
||||||
category_choices,
|
category_choices,
|
||||||
character_age_choices,
|
character_age_choices,
|
||||||
character_body_choices,
|
character_body_choices,
|
||||||
@@ -451,7 +448,6 @@ try:
|
|||||||
character_softcore_outfit_source_choices,
|
character_softcore_outfit_source_choices,
|
||||||
character_softcore_outfit_values,
|
character_softcore_outfit_values,
|
||||||
character_woman_body_choices,
|
character_woman_body_choices,
|
||||||
composition_pool_preset_choices,
|
|
||||||
ethnicity_choices,
|
ethnicity_choices,
|
||||||
generation_profile_choices,
|
generation_profile_choices,
|
||||||
hardcore_position_family_choices,
|
hardcore_position_family_choices,
|
||||||
@@ -459,8 +455,6 @@ try:
|
|||||||
hardcore_position_key_choices,
|
hardcore_position_key_choices,
|
||||||
hardcore_detail_density_choices,
|
hardcore_detail_density_choices,
|
||||||
load_character_profile_json,
|
load_character_profile_json,
|
||||||
location_theme_choices,
|
|
||||||
location_pool_preset_choices,
|
|
||||||
save_character_profile_payload,
|
save_character_profile_payload,
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
@@ -480,18 +474,19 @@ except ImportError:
|
|||||||
NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
|
from node_route_config import (
|
||||||
|
NODE_CLASS_MAPPINGS as ROUTE_CONFIG_NODE_CLASS_MAPPINGS,
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS as ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
|
)
|
||||||
from node_seed_resolution import (
|
from node_seed_resolution import (
|
||||||
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
|
||||||
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
|
||||||
)
|
)
|
||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
build_cast_config_json,
|
|
||||||
build_category_config_json,
|
|
||||||
build_character_slot_json,
|
build_character_slot_json,
|
||||||
build_character_manual_config_json,
|
build_character_manual_config_json,
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_characteristics_config_json,
|
build_characteristics_config_json,
|
||||||
build_composition_pool_json,
|
|
||||||
build_ethnicity_list_json,
|
build_ethnicity_list_json,
|
||||||
build_filter_config_json,
|
build_filter_config_json,
|
||||||
build_generation_profile_json,
|
build_generation_profile_json,
|
||||||
@@ -499,15 +494,11 @@ except ImportError:
|
|||||||
build_hardcore_action_filter_json,
|
build_hardcore_action_filter_json,
|
||||||
build_hardcore_position_pool_json,
|
build_hardcore_position_pool_json,
|
||||||
build_insta_of_options_json,
|
build_insta_of_options_json,
|
||||||
build_location_pool_json,
|
|
||||||
build_thematic_location_json,
|
|
||||||
build_insta_of_pair,
|
build_insta_of_pair,
|
||||||
build_prompt,
|
build_prompt,
|
||||||
build_prompt_from_configs,
|
build_prompt_from_configs,
|
||||||
camera_detail_choices,
|
camera_detail_choices,
|
||||||
camera_mode_choices,
|
camera_mode_choices,
|
||||||
cast_preset_choices,
|
|
||||||
category_preset_choices,
|
|
||||||
category_choices,
|
category_choices,
|
||||||
character_age_choices,
|
character_age_choices,
|
||||||
character_body_choices,
|
character_body_choices,
|
||||||
@@ -527,7 +518,6 @@ except ImportError:
|
|||||||
character_softcore_outfit_source_choices,
|
character_softcore_outfit_source_choices,
|
||||||
character_softcore_outfit_values,
|
character_softcore_outfit_values,
|
||||||
character_woman_body_choices,
|
character_woman_body_choices,
|
||||||
composition_pool_preset_choices,
|
|
||||||
ethnicity_choices,
|
ethnicity_choices,
|
||||||
generation_profile_choices,
|
generation_profile_choices,
|
||||||
hardcore_position_family_choices,
|
hardcore_position_family_choices,
|
||||||
@@ -535,8 +525,6 @@ except ImportError:
|
|||||||
hardcore_position_key_choices,
|
hardcore_position_key_choices,
|
||||||
hardcore_detail_density_choices,
|
hardcore_detail_density_choices,
|
||||||
load_character_profile_json,
|
load_character_profile_json,
|
||||||
location_theme_choices,
|
|
||||||
location_pool_preset_choices,
|
|
||||||
save_character_profile_payload,
|
save_character_profile_payload,
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
@@ -736,278 +724,6 @@ class SxCPPromptBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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 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):
|
|
||||||
if not seed_config:
|
|
||||||
return None
|
|
||||||
if isinstance(seed_config, dict):
|
|
||||||
raw = seed_config
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
raw = json.loads(str(seed_config))
|
|
||||||
except (TypeError, ValueError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return None
|
|
||||||
for key in ("category_seed", "content_seed", "role_seed", "seed", "global_seed"):
|
|
||||||
try:
|
|
||||||
value = int(raw.get(key))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
if value >= 0:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
|
|
||||||
class SxCPGenerationProfile:
|
class SxCPGenerationProfile:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -2482,13 +2198,8 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
NODE_CLASS_MAPPINGS.update(SEED_RESOLUTION_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(SEED_RESOLUTION_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update(CAMERA_NODE_CLASS_MAPPINGS)
|
NODE_CLASS_MAPPINGS.update(CAMERA_NODE_CLASS_MAPPINGS)
|
||||||
|
NODE_CLASS_MAPPINGS.update(ROUTE_CONFIG_NODE_CLASS_MAPPINGS)
|
||||||
NODE_CLASS_MAPPINGS.update({
|
NODE_CLASS_MAPPINGS.update({
|
||||||
"SxCPCategoryPreset": SxCPCategoryPreset,
|
|
||||||
"SxCPLocationPool": SxCPLocationPool,
|
|
||||||
"SxCPCompositionPool": SxCPCompositionPool,
|
|
||||||
"SxCPLocationTheme": SxCPLocationTheme,
|
|
||||||
"SxCPCastControl": SxCPCastControl,
|
|
||||||
"SxCPCastBias": SxCPCastBias,
|
|
||||||
"SxCPGenerationProfile": SxCPGenerationProfile,
|
"SxCPGenerationProfile": SxCPGenerationProfile,
|
||||||
"SxCPEthnicityList": SxCPEthnicityList,
|
"SxCPEthnicityList": SxCPEthnicityList,
|
||||||
"SxCPHairLength": SxCPHairLength,
|
"SxCPHairLength": SxCPHairLength,
|
||||||
@@ -2524,13 +2235,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(CAMERA_NODE_DISPLAY_NAME_MAPPINGS)
|
NODE_DISPLAY_NAME_MAPPINGS.update(CAMERA_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS.update(ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update({
|
NODE_DISPLAY_NAME_MAPPINGS.update({
|
||||||
"SxCPCategoryPreset": "SxCP Category Preset",
|
|
||||||
"SxCPLocationPool": "SxCP Location Pool",
|
|
||||||
"SxCPCompositionPool": "SxCP Composition Pool",
|
|
||||||
"SxCPLocationTheme": "SxCP Location Theme",
|
|
||||||
"SxCPCastControl": "SxCP Cast Control",
|
|
||||||
"SxCPCastBias": "SxCP Cast Bias",
|
|
||||||
"SxCPGenerationProfile": "SxCP Generation Profile",
|
"SxCPGenerationProfile": "SxCP Generation Profile",
|
||||||
"SxCPEthnicityList": "SxCP Ethnicity List",
|
"SxCPEthnicityList": "SxCP Ethnicity List",
|
||||||
"SxCPHairLength": "SxCP Hair Length",
|
"SxCPHairLength": "SxCP Hair Length",
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ Improve later:
|
|||||||
### Node / UI Path
|
### Node / UI Path
|
||||||
|
|
||||||
Owner: `__init__.py`, `node_seed_resolution.py`, `node_camera.py`,
|
Owner: `__init__.py`, `node_seed_resolution.py`, `node_camera.py`,
|
||||||
`loop_nodes.py`, `web/*.js`.
|
`node_route_config.py`, `loop_nodes.py`, `web/*.js`.
|
||||||
|
|
||||||
Keep here:
|
Keep here:
|
||||||
|
|
||||||
@@ -283,6 +283,8 @@ Keep here:
|
|||||||
- dynamic input slots.
|
- dynamic input slots.
|
||||||
- seed and resolution utility node declarations in `node_seed_resolution.py`.
|
- seed and resolution utility node declarations in `node_seed_resolution.py`.
|
||||||
- camera utility node declarations in `node_camera.py`.
|
- camera utility node declarations in `node_camera.py`.
|
||||||
|
- route/category/location/composition/cast config node declarations in
|
||||||
|
`node_route_config.py`.
|
||||||
|
|
||||||
Already isolated:
|
Already isolated:
|
||||||
|
|
||||||
@@ -290,6 +292,9 @@ Already isolated:
|
|||||||
`node_seed_resolution.py`, with registration maps imported by `__init__.py`.
|
`node_seed_resolution.py`, with registration maps imported by `__init__.py`.
|
||||||
- camera/orbit/Qwen translator utility nodes live in `node_camera.py`, with
|
- camera/orbit/Qwen translator utility nodes live in `node_camera.py`, with
|
||||||
registration maps imported by `__init__.py`.
|
registration maps imported by `__init__.py`.
|
||||||
|
- category preset, location/composition pool, location theme, and cast config
|
||||||
|
utility nodes live in `node_route_config.py`, with registration maps imported
|
||||||
|
by `__init__.py`.
|
||||||
|
|
||||||
Improve later:
|
Improve later:
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ When a result is wrong, first identify which layer owns the bad text:
|
|||||||
- Raw builder prompt acceptable, SDXL tags wrong: edit `sdxl_formatter.py`.
|
- Raw builder prompt acceptable, SDXL tags wrong: edit `sdxl_formatter.py`.
|
||||||
- Natural caption/training caption wrong: edit `caption_naturalizer.py`.
|
- Natural caption/training caption wrong: edit `caption_naturalizer.py`.
|
||||||
- UI/preview/loop behavior wrong: edit `__init__.py`, node family modules such
|
- UI/preview/loop behavior wrong: edit `__init__.py`, node family modules such
|
||||||
as `node_seed_resolution.py` or `node_camera.py`, `loop_nodes.py`, or
|
as `node_seed_resolution.py`, `node_camera.py`, or `node_route_config.py`,
|
||||||
`web/*.js`.
|
`loop_nodes.py`, or `web/*.js`.
|
||||||
|
|
||||||
## High-Level Routes
|
## High-Level Routes
|
||||||
|
|
||||||
@@ -694,6 +694,7 @@ These do not own prompt pool wording, but they affect execution and review:
|
|||||||
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
||||||
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | Global/per-axis seed configs plus SDXL/Krea width/height helpers. |
|
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | Global/per-axis seed configs plus SDXL/Krea width/height helpers. |
|
||||||
| Camera utility nodes | `node_camera.py`, imported by `__init__.py` | Direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation. |
|
| Camera utility nodes | `node_camera.py`, imported by `__init__.py` | Direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation. |
|
||||||
|
| Route config utility nodes | `node_route_config.py`, imported by `__init__.py` | Category preset, location/composition pool, location theme, and cast config helpers. |
|
||||||
|
|
||||||
## Drift Audit Helper
|
## Drift Audit Helper
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .prompt_builder import (
|
||||||
|
build_cast_config_json,
|
||||||
|
build_category_config_json,
|
||||||
|
build_composition_pool_json,
|
||||||
|
build_location_pool_json,
|
||||||
|
build_thematic_location_json,
|
||||||
|
cast_preset_choices,
|
||||||
|
category_preset_choices,
|
||||||
|
composition_pool_preset_choices,
|
||||||
|
location_pool_preset_choices,
|
||||||
|
location_theme_choices,
|
||||||
|
subcategory_choices,
|
||||||
|
)
|
||||||
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
from prompt_builder import (
|
||||||
|
build_cast_config_json,
|
||||||
|
build_category_config_json,
|
||||||
|
build_composition_pool_json,
|
||||||
|
build_location_pool_json,
|
||||||
|
build_thematic_location_json,
|
||||||
|
cast_preset_choices,
|
||||||
|
category_preset_choices,
|
||||||
|
composition_pool_preset_choices,
|
||||||
|
location_pool_preset_choices,
|
||||||
|
location_theme_choices,
|
||||||
|
subcategory_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"
|
||||||
|
|
||||||
|
|
||||||
|
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 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):
|
||||||
|
if not seed_config:
|
||||||
|
return None
|
||||||
|
if isinstance(seed_config, dict):
|
||||||
|
raw = seed_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(seed_config))
|
||||||
|
except (TypeError, ValueError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
for key in ("category_seed", "content_seed", "role_seed", "seed", "global_seed"):
|
||||||
|
try:
|
||||||
|
value = int(raw.get(key))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if value >= 0:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
@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,
|
||||||
|
"SxCPCastControl": SxCPCastControl,
|
||||||
|
"SxCPCastBias": SxCPCastBias,
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"SxCPCategoryPreset": "SxCP Category Preset",
|
||||||
|
"SxCPLocationPool": "SxCP Location Pool",
|
||||||
|
"SxCPCompositionPool": "SxCP Composition Pool",
|
||||||
|
"SxCPLocationTheme": "SxCP Location Theme",
|
||||||
|
"SxCPCastControl": "SxCP Cast Control",
|
||||||
|
"SxCPCastBias": "SxCP Cast Bias",
|
||||||
|
}
|
||||||
@@ -1778,6 +1778,76 @@ def smoke_node_camera_registration() -> None:
|
|||||||
_expect(json.loads(qwen_info).get("qwen_prompt", "").startswith("<sks>"), "Qwen info JSON lost original prompt")
|
_expect(json.loads(qwen_info).get("qwen_prompt", "").startswith("<sks>"), "Qwen info JSON lost original prompt")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_node_route_config_registration() -> None:
|
||||||
|
required_nodes = [
|
||||||
|
"SxCPCategoryPreset",
|
||||||
|
"SxCPLocationPool",
|
||||||
|
"SxCPCompositionPool",
|
||||||
|
"SxCPLocationTheme",
|
||||||
|
"SxCPCastControl",
|
||||||
|
"SxCPCastBias",
|
||||||
|
]
|
||||||
|
for node_name in required_nodes:
|
||||||
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
||||||
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
||||||
|
|
||||||
|
category_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCategoryPreset"]
|
||||||
|
category_inputs = category_node.INPUT_TYPES().get("required") or {}
|
||||||
|
_expect("preset" in category_inputs, "Category Preset lost preset input")
|
||||||
|
_expect("tooltip" in category_inputs["preset"][1], "Category Preset tooltip injection missing")
|
||||||
|
category_config, category, subcategory = category_node().build("auto_weighted", "random")
|
||||||
|
parsed_category = json.loads(category_config)
|
||||||
|
_expect(category == parsed_category.get("category") == "auto_weighted", "Category Preset output category mismatch")
|
||||||
|
_expect(subcategory == "random", "Category Preset output subcategory mismatch")
|
||||||
|
|
||||||
|
location_config, location_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPLocationPool"]().build(
|
||||||
|
True,
|
||||||
|
"replace",
|
||||||
|
"custom_only",
|
||||||
|
"classical library stacks with brass lamps",
|
||||||
|
)
|
||||||
|
parsed_location = json.loads(location_config)
|
||||||
|
_expect(parsed_location.get("scene_entries"), "Location Pool did not keep custom location")
|
||||||
|
_expect("locations=1" in location_summary, "Location Pool summary lost custom count")
|
||||||
|
|
||||||
|
composition_config, composition_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCompositionPool"]().build(
|
||||||
|
True,
|
||||||
|
"replace",
|
||||||
|
"no_outfit_check",
|
||||||
|
"long aisle composition with shelves repeating behind the subject",
|
||||||
|
)
|
||||||
|
parsed_composition = json.loads(composition_config)
|
||||||
|
_expect(parsed_composition.get("composition_entries"), "Composition Pool did not keep composition entries")
|
||||||
|
_expect("compositions=" in composition_summary, "Composition Pool summary lost composition count")
|
||||||
|
|
||||||
|
theme_location, theme_composition, theme_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPLocationTheme"]().build(
|
||||||
|
True,
|
||||||
|
"replace",
|
||||||
|
"semi_public_affair",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
_expect(json.loads(theme_location).get("scene_entries"), "Location Theme did not output locations")
|
||||||
|
_expect(json.loads(theme_composition).get("composition_entries"), "Location Theme did not output compositions")
|
||||||
|
_expect("semi_public_affair" in theme_summary, "Location Theme summary lost theme name")
|
||||||
|
|
||||||
|
cast_config, women_count, men_count, cast_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCastControl"]().build(
|
||||||
|
"mixed_couple",
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
parsed_cast = json.loads(cast_config)
|
||||||
|
_expect((women_count, men_count) == (parsed_cast.get("women_count"), parsed_cast.get("men_count")), "Cast Control count outputs mismatch")
|
||||||
|
_expect("1 women, 1 men" in cast_summary, "Cast Control summary changed unexpectedly")
|
||||||
|
|
||||||
|
cast_bias = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCastBias"]()
|
||||||
|
bias_a = cast_bias.build(123, 2, "0.7,0.3", 1, "0.4,0.6", 0, "force_one_woman")
|
||||||
|
bias_b = cast_bias.build(123, 2, "0.7,0.3", 1, "0.4,0.6", 0, "force_one_woman")
|
||||||
|
_expect(bias_a == bias_b, "Cast Bias should be deterministic for fixed seed and row")
|
||||||
|
_expect(bias_a[1] + bias_a[2] >= 1, "Cast Bias empty behavior allowed empty cast")
|
||||||
|
_expect("weighted cast:" in bias_a[3], "Cast Bias summary lost weighted cast label")
|
||||||
|
|
||||||
|
|
||||||
SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||||
("builtin_single_woman", smoke_builtin_single),
|
("builtin_single_woman", smoke_builtin_single),
|
||||||
("camera_scene_single", smoke_camera_scene_single),
|
("camera_scene_single", smoke_camera_scene_single),
|
||||||
@@ -1803,6 +1873,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|||||||
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
||||||
("node_utility_registration", smoke_node_utility_registration),
|
("node_utility_registration", smoke_node_utility_registration),
|
||||||
("node_camera_registration", smoke_node_camera_registration),
|
("node_camera_registration", smoke_node_camera_registration),
|
||||||
|
("node_route_config_registration", smoke_node_route_config_registration),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user