diff --git a/__init__.py b/__init__.py index 88acfab..58ee12c 100644 --- a/__init__.py +++ b/__init__.py @@ -404,6 +404,10 @@ try: NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS, ) + from .node_character import ( + NODE_CLASS_MAPPINGS as CHARACTER_NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as CHARACTER_NODE_DISPLAY_NAME_MAPPINGS, + ) from .node_profile_filter import ( NODE_CLASS_MAPPINGS as PROFILE_FILTER_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS, @@ -417,11 +421,6 @@ try: NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS, ) from .prompt_builder import ( - build_character_slot_json, - build_character_manual_config_json, - build_character_profile_json, - build_characteristics_config_json, - build_hair_config_json, build_hardcore_action_filter_json, build_hardcore_position_pool_json, build_insta_of_options_json, @@ -431,30 +430,11 @@ try: camera_detail_choices, camera_mode_choices, category_choices, - character_age_choices, - character_body_choices, - character_descriptor_detail_choices, - character_ethnicity_choices, - character_eye_color_choices, - character_figure_choices, - character_hair_color_choices, - character_hair_length_choices, - character_hair_style_choices, - character_label_choices, - character_man_body_choices, - character_presence_choices, - character_profile_choices, - character_hardcore_clothing_state_choices, - character_hardcore_clothing_values, - character_softcore_outfit_source_choices, - character_softcore_outfit_values, - character_woman_body_choices, ethnicity_choices, hardcore_position_family_choices, hardcore_position_focus_choices, hardcore_position_key_choices, hardcore_detail_density_choices, - load_character_profile_json, save_character_profile_payload, subcategory_choices, ) @@ -474,6 +454,10 @@ except ImportError: NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS, ) + from node_character import ( + NODE_CLASS_MAPPINGS as CHARACTER_NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as CHARACTER_NODE_DISPLAY_NAME_MAPPINGS, + ) from node_profile_filter import ( NODE_CLASS_MAPPINGS as PROFILE_FILTER_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS, @@ -487,11 +471,6 @@ except ImportError: NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS, ) from prompt_builder import ( - build_character_slot_json, - build_character_manual_config_json, - build_character_profile_json, - build_characteristics_config_json, - build_hair_config_json, build_hardcore_action_filter_json, build_hardcore_position_pool_json, build_insta_of_options_json, @@ -501,30 +480,11 @@ except ImportError: camera_detail_choices, camera_mode_choices, category_choices, - character_age_choices, - character_body_choices, - character_descriptor_detail_choices, - character_ethnicity_choices, - character_eye_color_choices, - character_figure_choices, - character_hair_color_choices, - character_hair_length_choices, - character_hair_style_choices, - character_label_choices, - character_man_body_choices, - character_presence_choices, - character_profile_choices, - character_hardcore_clothing_state_choices, - character_hardcore_clothing_values, - character_softcore_outfit_source_choices, - character_softcore_outfit_values, - character_woman_body_choices, ethnicity_choices, hardcore_position_family_choices, hardcore_position_focus_choices, hardcore_position_key_choices, hardcore_detail_density_choices, - load_character_profile_json, save_character_profile_payload, subcategory_choices, ) @@ -724,65 +684,6 @@ class SxCPPromptBuilder: ) -class _SxCPHairAxisNode: - AXIS = "color" - PREFIX = "include" - - @classmethod - def _choices(cls): - if cls.AXIS == "color": - return [choice for choice in character_hair_color_choices() if choice != "random"] - if cls.AXIS == "length": - return [choice for choice in character_hair_length_choices() if choice != "random"] - return [choice for choice in character_hair_style_choices() if choice != "random"] - - @classmethod - def INPUT_TYPES(cls): - required = { - "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), - } - for choice in cls._choices(): - required[f"{cls.PREFIX}_{choice}"] = ("BOOLEAN", {"default": False}) - return { - "required": required, - "optional": { - "hair_config": (SXCP_HAIR_CONFIG,), - }, - } - - RETURN_TYPES = (SXCP_HAIR_CONFIG, "STRING") - RETURN_NAMES = ("hair_config", "summary") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build(self, combine_mode="replace_axis", hair_config="", **kwargs): - selected = [ - choice - for choice in self._choices() - if bool(kwargs.get(f"{self.PREFIX}_{choice}", False)) - ] - config = build_hair_config_json( - hair_config=hair_config or "", - axis=self.AXIS, - selected_values=selected, - combine_mode=combine_mode, - ) - parsed = json.loads(config) - return config, parsed.get("summary", "") - - -class SxCPHairColor(_SxCPHairAxisNode): - AXIS = "color" - - -class SxCPHairLength(_SxCPHairAxisNode): - AXIS = "length" - - -class SxCPHairStyle(_SxCPHairAxisNode): - AXIS = "style" - - def _choice_input_key(prefix, choice): key = "".join(char if char.isalnum() else "_" for char in str(choice).lower()).strip("_") while "__" in key: @@ -790,183 +691,6 @@ def _choice_input_key(prefix, choice): return f"{prefix}_{key}" -class SxCPCharacterAgeRange: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), - "min_age": ("INT", {"default": 21, "min": 21, "max": 85, "step": 1}), - "max_age": ("INT", {"default": 35, "min": 21, "max": 85, "step": 1}), - }, - "optional": { - "characteristics": (SXCP_CHARACTERISTICS,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") - RETURN_NAMES = ("characteristics", "summary") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build(self, combine_mode, min_age, max_age, characteristics=""): - start = max(21, min(85, int(min_age))) - end = max(21, min(85, int(max_age))) - if end < start: - start, end = end, start - ages = [f"{age}-year-old adult" for age in range(start, end + 1)] - config = build_characteristics_config_json( - characteristics=characteristics or "", - axis="ages", - selected_values=ages, - combine_mode=combine_mode, - ) - return config, json.loads(config).get("summary", "") - - -class _SxCPBodyPoolNode: - SUBJECT = "character" - - @classmethod - def _choices(cls): - if cls.SUBJECT == "woman": - return [choice for choice in character_woman_body_choices() if choice not in ("random", "manual")] - if cls.SUBJECT == "man": - return [choice for choice in character_man_body_choices() if choice not in ("random", "manual")] - return [choice for choice in character_body_choices() if choice not in ("random", "manual")] - - @classmethod - def INPUT_TYPES(cls): - required = { - "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), - } - for choice in cls._choices(): - required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) - return { - "required": required, - "optional": { - "characteristics": (SXCP_CHARACTERISTICS,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") - RETURN_NAMES = ("characteristics", "summary") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build(self, combine_mode="replace_axis", characteristics="", **kwargs): - selected = [ - choice - for choice in self._choices() - if bool(kwargs.get(_choice_input_key("include", choice), False)) - ] - config = build_characteristics_config_json( - characteristics=characteristics or "", - axis="bodies", - selected_values=selected, - combine_mode=combine_mode, - ) - return config, json.loads(config).get("summary", "") - - -class SxCPCharacterBodyPool(_SxCPBodyPoolNode): - SUBJECT = "character" - - -class SxCPWomanBodyPool(_SxCPBodyPoolNode): - SUBJECT = "woman" - - -class SxCPManBodyPool(_SxCPBodyPoolNode): - SUBJECT = "man" - - -class SxCPEyeColorPool: - @classmethod - def INPUT_TYPES(cls): - required = { - "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), - } - for choice in character_eye_color_choices(): - if choice != "random": - required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) - return { - "required": required, - "optional": { - "characteristics": (SXCP_CHARACTERISTICS,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") - RETURN_NAMES = ("characteristics", "summary") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build(self, combine_mode="replace_axis", characteristics="", **kwargs): - selected = [ - choice - for choice in character_eye_color_choices() - if choice != "random" and bool(kwargs.get(_choice_input_key("include", choice), False)) - ] - config = build_characteristics_config_json( - characteristics=characteristics or "", - axis="eyes", - selected_values=selected, - combine_mode=combine_mode, - ) - return config, json.loads(config).get("summary", "") - - -class SxCPCharacterClothing: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), - "softcore_source": (character_softcore_outfit_source_choices(), {"default": "no_change"}), - "hardcore_state": (character_hardcore_clothing_state_choices(), {"default": "no_change"}), - "custom_softcore_outfits": ("STRING", {"default": "", "multiline": True}), - "custom_hardcore_clothing": ("STRING", {"default": "", "multiline": True}), - }, - "optional": { - "characteristics": (SXCP_CHARACTERISTICS,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") - RETURN_NAMES = ("characteristics", "summary") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - combine_mode, - softcore_source, - hardcore_state, - custom_softcore_outfits, - custom_hardcore_clothing, - characteristics="", - ): - config = characteristics or "" - if softcore_source != "no_change": - config = build_characteristics_config_json( - characteristics=config, - axis="softcore_outfits", - selected_values=character_softcore_outfit_values(softcore_source, custom_softcore_outfits), - combine_mode=combine_mode, - ) - if hardcore_state != "no_change": - config = build_characteristics_config_json( - characteristics=config, - axis="hardcore_clothing", - selected_values=character_hardcore_clothing_values(hardcore_state, custom_hardcore_clothing), - combine_mode=combine_mode, - ) - if not config: - config = build_characteristics_config_json(axis="", selected_values=[]) - return config, json.loads(config).get("summary", "") - - class SxCPHardcorePositionPool: @classmethod def INPUT_TYPES(cls): @@ -1062,60 +786,6 @@ class SxCPHardcoreActionFilter: return config, json.loads(config).get("summary", "") -class SxCPCharacterManualDetails: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "combine_mode": (["merge_nonempty", "replace_all"], {"default": "merge_nonempty"}), - "manual_age": ("STRING", {"default": ""}), - "manual_body": ("STRING", {"default": ""}), - "body_phrase": ("STRING", {"default": ""}), - "skin": ("STRING", {"default": ""}), - "hair": ("STRING", {"default": ""}), - "eyes": ("STRING", {"default": ""}), - "softcore_outfit": ("STRING", {"default": ""}), - "hardcore_clothing": ("STRING", {"default": ""}), - }, - "optional": { - "manual": (SXCP_CHARACTER_MANUAL,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTER_MANUAL, "STRING") - RETURN_NAMES = ("manual", "summary") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - combine_mode, - manual_age, - manual_body, - body_phrase, - skin, - hair, - eyes, - softcore_outfit, - hardcore_clothing, - manual="", - ): - config = build_character_manual_config_json( - manual=manual or "", - combine_mode=combine_mode, - manual_age=manual_age, - manual_body=manual_body, - body_phrase=body_phrase, - skin=skin, - hair=hair, - eyes=eyes, - softcore_outfit=softcore_outfit, - hardcore_clothing=hardcore_clothing, - ) - parsed = json.loads(config) - return config, parsed.get("summary", "") - - class SxCPPromptBuilderFromConfigs: @classmethod def INPUT_TYPES(cls): @@ -1196,414 +866,6 @@ class SxCPPromptBuilderFromConfigs: ) -class SxCPCharacterSlot: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "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": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), - "ethnicity": (character_ethnicity_choices(), {"default": "random"}), - "figure": (character_figure_choices(), {"default": "random"}), - "body": ([choice for choice in character_body_choices() if choice != "manual"], {"default": "random"}), - "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), - "expression_enabled": ("BOOLEAN", {"default": True}), - "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "presence_mode": (character_presence_choices(), {"default": "visible"}), - "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - }, - "optional": { - "manual": (SXCP_CHARACTER_MANUAL,), - "ethnicity_list": (SXCP_ETHNICITY_LIST,), - "characteristics": (SXCP_CHARACTERISTICS,), - "hair_config": (SXCP_HAIR_CONFIG,), - "character_cast": (SXCP_CHARACTER_CAST,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") - RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - enabled, - subject_type, - label, - slot_seed, - age, - ethnicity, - figure, - body, - descriptor_detail="auto", - expression_enabled=True, - expression_intensity=-1.0, - presence_mode="visible", - softcore_expression_intensity=-1.0, - hardcore_expression_intensity=-1.0, - character_cast="", - ethnicity_list="", - characteristics="", - hair_config="", - manual="", - ): - result = build_character_slot_json( - subject_type=subject_type, - label=label, - slot_seed=slot_seed, - age=age, - manual=manual, - ethnicity=ethnicity_list or ethnicity, - figure=figure, - body=body, - manual_body="", - body_phrase="", - skin="", - hair="", - characteristics=characteristics, - hair_config=hair_config, - eyes="", - descriptor_detail=descriptor_detail, - expression_enabled=expression_enabled, - expression_intensity=expression_intensity, - presence_mode=presence_mode, - softcore_expression_intensity=softcore_expression_intensity, - hardcore_expression_intensity=hardcore_expression_intensity, - softcore_outfit="", - hardcore_clothing="", - enabled=enabled, - character_cast=character_cast or "", - ) - return result["character_cast"], result["character_slot"], result["summary"], result["status"] - - -class SxCPWomanSlot: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True}), - "label": (character_label_choices(), {"default": "auto_chain"}), - "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), - "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), - "ethnicity": (character_ethnicity_choices(), {"default": "random"}), - "figure_bias": (character_figure_choices(), {"default": "random"}), - "body": ([choice for choice in character_woman_body_choices() if choice != "manual"], {"default": "random"}), - "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), - "expression_enabled": ("BOOLEAN", {"default": True}), - "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - }, - "optional": { - "manual": (SXCP_CHARACTER_MANUAL,), - "ethnicity_list": (SXCP_ETHNICITY_LIST,), - "characteristics": (SXCP_CHARACTERISTICS,), - "hair_config": (SXCP_HAIR_CONFIG,), - "character_cast": (SXCP_CHARACTER_CAST,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") - RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - enabled, - label, - slot_seed, - age, - ethnicity, - figure_bias, - body, - descriptor_detail="auto", - expression_enabled=True, - expression_intensity=-1.0, - softcore_expression_intensity=-1.0, - hardcore_expression_intensity=-1.0, - character_cast="", - ethnicity_list="", - characteristics="", - hair_config="", - manual="", - ): - result = build_character_slot_json( - subject_type="woman", - label=label, - slot_seed=slot_seed, - age=age, - manual=manual, - ethnicity=ethnicity_list or ethnicity, - figure=figure_bias, - body=body, - manual_body="", - body_phrase="", - skin="", - hair="", - characteristics=characteristics, - hair_config=hair_config, - eyes="", - descriptor_detail=descriptor_detail, - expression_enabled=expression_enabled, - expression_intensity=expression_intensity, - softcore_expression_intensity=softcore_expression_intensity, - hardcore_expression_intensity=hardcore_expression_intensity, - softcore_outfit="", - hardcore_clothing="", - enabled=enabled, - character_cast=character_cast or "", - ) - return result["character_cast"], result["character_slot"], result["summary"], result["status"] - - -class SxCPManSlot: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True}), - "label": (character_label_choices(), {"default": "auto_chain"}), - "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), - "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), - "ethnicity": (character_ethnicity_choices(), {"default": "random"}), - "body": ([choice for choice in character_man_body_choices() if choice != "manual"], {"default": "random"}), - "descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}), - "expression_enabled": ("BOOLEAN", {"default": True}), - "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "presence_mode": (character_presence_choices(), {"default": "visible"}), - "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), - }, - "optional": { - "manual": (SXCP_CHARACTER_MANUAL,), - "ethnicity_list": (SXCP_ETHNICITY_LIST,), - "characteristics": (SXCP_CHARACTERISTICS,), - "hair_config": (SXCP_HAIR_CONFIG,), - "character_cast": (SXCP_CHARACTER_CAST,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") - RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - enabled, - label, - slot_seed, - age, - ethnicity, - body, - descriptor_detail="compact", - expression_enabled=True, - expression_intensity=-1.0, - presence_mode="visible", - softcore_expression_intensity=-1.0, - hardcore_expression_intensity=-1.0, - character_cast="", - ethnicity_list="", - characteristics="", - hair_config="", - manual="", - ): - result = build_character_slot_json( - subject_type="man", - label=label, - slot_seed=slot_seed, - age=age, - manual=manual, - ethnicity=ethnicity_list or ethnicity, - figure="random", - body=body, - manual_body="", - body_phrase="", - skin="", - hair="", - characteristics=characteristics, - hair_config=hair_config, - eyes="", - descriptor_detail=descriptor_detail, - expression_enabled=expression_enabled, - expression_intensity=expression_intensity, - presence_mode=presence_mode, - softcore_expression_intensity=softcore_expression_intensity, - hardcore_expression_intensity=hardcore_expression_intensity, - softcore_outfit="", - hardcore_clothing="", - enabled=enabled, - character_cast=character_cast or "", - ) - return result["character_cast"], result["character_slot"], result["summary"], result["status"] - - -class SxCPCharacterProfileSave: - OUTPUT_NODE = True - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "profile_name": ("STRING", {"default": "saved_character"}), - "source": (["metadata_json", "character_slot", "manual"], {"default": "metadata_json"}), - "subject_type": (["woman", "man"], {"default": "woman"}), - "age": ("STRING", {"default": ""}), - "body": ("STRING", {"default": ""}), - "body_phrase": ("STRING", {"default": ""}), - "skin": ("STRING", {"default": ""}), - "hair": ("STRING", {"default": ""}), - "eyes": ("STRING", {"default": ""}), - "figure": ("STRING", {"default": ""}), - "save_now": ("BOOLEAN", {"default": False}), - }, - "optional": { - "metadata_json": ("STRING", {"default": "", "multiline": True}), - "character_slot": (SXCP_CHARACTER_SLOT,), - }, - } - - RETURN_TYPES = (SXCP_CHARACTER_PROFILE, "STRING", "STRING", "STRING", "STRING", SXCP_CHARACTER_PROFILE) - RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "profile_json") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - profile_name, - source, - subject_type, - age, - body, - body_phrase, - skin, - hair, - eyes, - figure, - save_now, - metadata_json="", - character_slot="", - ): - profile = build_character_profile_json( - profile_name=profile_name, - source=source, - metadata_json=metadata_json or "", - character_slot=character_slot or "", - subject_type=subject_type, - age=age, - body=body, - body_phrase=body_phrase, - skin=skin, - hair=hair, - eyes=eyes, - figure=figure, - save_now=save_now, - ) - result = ( - profile["profile_json"], - profile["descriptor"], - profile["profile_name"], - profile["saved_path"], - profile["status"], - profile["profile_json"], - ) - return { - "ui": { - "profile_json": [profile["profile_json"]], - "descriptor": [profile["descriptor"]], - "profile_name": [profile["profile_name"]], - "saved_path": [profile["saved_path"]], - "status": [profile["status"]], - }, - "result": result, - } - - -class SxCPCharacterProfileLoad: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "enabled": ("BOOLEAN", {"default": True}), - "profile_name": (character_profile_choices(), {"default": "manual"}), - "rename_to": ("STRING", {"default": ""}), - "delete_now": ("BOOLEAN", {"default": False}), - "rename_now": ("BOOLEAN", {"default": False}), - }, - "optional": { - "manual_profile_name": ("STRING", {"default": ""}), - "fallback_profile_json": (SXCP_CHARACTER_PROFILE,), - "override_subject_type": (["keep_profile", "woman", "man"], {"default": "keep_profile"}), - "override_age": ("STRING", {"default": ""}), - "override_body": ("STRING", {"default": ""}), - "override_body_phrase": ("STRING", {"default": ""}), - "override_skin": ("STRING", {"default": ""}), - "override_hair": ("STRING", {"default": ""}), - "override_eyes": ("STRING", {"default": ""}), - "override_figure": ("STRING", {"default": ""}), - "override_descriptor_detail": (["keep_profile"] + character_descriptor_detail_choices(), {"default": "keep_profile"}), - }, - } - - RETURN_TYPES = (SXCP_CHARACTER_PROFILE, "STRING", "STRING", "STRING", "STRING", SXCP_CHARACTER_PROFILE) - RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "profile_json") - FUNCTION = "build" - CATEGORY = "prompt_builder" - - def build( - self, - enabled, - profile_name, - rename_to, - delete_now, - rename_now, - manual_profile_name="", - fallback_profile_json="", - override_subject_type="keep_profile", - override_age="", - override_body="", - override_body_phrase="", - override_skin="", - override_hair="", - override_eyes="", - override_figure="", - override_descriptor_detail="keep_profile", - ): - chosen_name = manual_profile_name.strip() if profile_name == "manual" and manual_profile_name.strip() else profile_name - profile = load_character_profile_json( - profile_name=chosen_name, - fallback_profile_json=fallback_profile_json or "", - enabled=enabled, - delete_now=delete_now, - rename_now=rename_now, - rename_to=rename_to, - override_subject_type=override_subject_type, - override_age=override_age, - override_body=override_body, - override_body_phrase=override_body_phrase, - override_skin=override_skin, - override_hair=override_hair, - override_eyes=override_eyes, - override_figure=override_figure, - override_descriptor_detail=override_descriptor_detail, - ) - return ( - profile["profile_json"], - profile["descriptor"], - profile["profile_name"], - profile["saved_path"], - profile["status"], - profile["profile_json"], - ) - - class SxCPCaptionNaturalizer: @classmethod def INPUT_TYPES(cls): @@ -1991,27 +1253,13 @@ NODE_CLASS_MAPPINGS = { } NODE_CLASS_MAPPINGS.update(SEED_RESOLUTION_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(CAMERA_NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(CHARACTER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(ROUTE_CONFIG_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(PROFILE_FILTER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update({ - "SxCPHairLength": SxCPHairLength, - "SxCPHairColor": SxCPHairColor, - "SxCPHairStyle": SxCPHairStyle, - "SxCPCharacterAgeRange": SxCPCharacterAgeRange, - "SxCPCharacterBodyPool": SxCPCharacterBodyPool, - "SxCPWomanBodyPool": SxCPWomanBodyPool, - "SxCPManBodyPool": SxCPManBodyPool, - "SxCPEyeColorPool": SxCPEyeColorPool, - "SxCPCharacterClothing": SxCPCharacterClothing, "SxCPHardcorePositionPool": SxCPHardcorePositionPool, "SxCPHardcoreActionFilter": SxCPHardcoreActionFilter, - "SxCPCharacterManualDetails": SxCPCharacterManualDetails, "SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs, - "SxCPWomanSlot": SxCPWomanSlot, - "SxCPManSlot": SxCPManSlot, - "SxCPCharacterSlot": SxCPCharacterSlot, - "SxCPCharacterProfileSave": SxCPCharacterProfileSave, - "SxCPCharacterProfileLoad": SxCPCharacterProfileLoad, "SxCPCaptionNaturalizer": SxCPCaptionNaturalizer, "SxCPKrea2Formatter": SxCPKrea2Formatter, "SxCPSDXLFormatter": SxCPSDXLFormatter, @@ -2026,27 +1274,13 @@ 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(CHARACTER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update({ - "SxCPHairLength": "SxCP Hair Length", - "SxCPHairColor": "SxCP Hair Color", - "SxCPHairStyle": "SxCP Hair Style/Cut", - "SxCPCharacterAgeRange": "SxCP Character Age Range", - "SxCPCharacterBodyPool": "SxCP Character Body Pool", - "SxCPWomanBodyPool": "SxCP Woman Body Pool", - "SxCPManBodyPool": "SxCP Man Body Pool", - "SxCPEyeColorPool": "SxCP Eye Color Pool", - "SxCPCharacterClothing": "SxCP Character Clothing", "SxCPHardcorePositionPool": "SxCP Hardcore Position Pool", "SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter", - "SxCPCharacterManualDetails": "SxCP Character Manual Details", "SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs", - "SxCPWomanSlot": "SxCP Woman Slot", - "SxCPManSlot": "SxCP Man Slot", - "SxCPCharacterSlot": "SxCP Character Slot", - "SxCPCharacterProfileSave": "SxCP Character Profile Save", - "SxCPCharacterProfileLoad": "SxCP Character Profile Load", "SxCPCaptionNaturalizer": "SxCP Caption Naturalizer", "SxCPKrea2Formatter": "SxCP Krea2 Formatter", "SxCPSDXLFormatter": "SxCP SDXL Formatter", diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index a1bfade..e14e5b4 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -273,7 +273,8 @@ Improve later: ### Node / UI Path Owner: `__init__.py`, `node_seed_resolution.py`, `node_camera.py`, -`node_route_config.py`, `node_profile_filter.py`, `loop_nodes.py`, `web/*.js`. +`node_character.py`, `node_route_config.py`, `node_profile_filter.py`, +`loop_nodes.py`, `web/*.js`. Keep here: @@ -283,6 +284,7 @@ Keep here: - dynamic input slots. - seed and resolution utility node declarations in `node_seed_resolution.py`. - camera utility node declarations in `node_camera.py`. +- character pool, slot, and profile node declarations in `node_character.py`. - route/category/location/composition/cast config node declarations in `node_route_config.py`. - profile/filter/ethnicity-list node declarations in `node_profile_filter.py`. @@ -293,6 +295,9 @@ Already isolated: `node_seed_resolution.py`, with registration maps imported by `__init__.py`. - camera/orbit/Qwen translator utility nodes live in `node_camera.py`, with registration maps imported by `__init__.py`. +- hair, age/body/eyes/clothing pools, manual character details, character + slots, and profile save/load nodes live in `node_character.py`, with + 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`. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index d3bcbb0..010de59 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -25,8 +25,9 @@ When a result is wrong, first identify which layer owns the bad text: - Raw builder prompt acceptable, SDXL tags wrong: edit `sdxl_formatter.py`. - Natural caption/training caption wrong: edit `caption_naturalizer.py`. - UI/preview/loop behavior wrong: edit `__init__.py`, node family modules such - as `node_seed_resolution.py`, `node_camera.py`, `node_route_config.py`, or - `node_profile_filter.py`, `loop_nodes.py`, or `web/*.js`. + as `node_seed_resolution.py`, `node_camera.py`, `node_character.py`, + `node_route_config.py`, or `node_profile_filter.py`, `loop_nodes.py`, or + `web/*.js`. ## High-Level Routes @@ -694,6 +695,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. | | 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. | +| Character utility nodes | `node_character.py`, imported by `__init__.py` | Hair, age/body/eyes/clothing pools, manual details, character slots, and profile save/load nodes. | | Route config utility nodes | `node_route_config.py`, imported by `__init__.py` | Category preset, location/composition pool, location theme, and cast config helpers. | | Profile/filter utility nodes | `node_profile_filter.py`, imported by `__init__.py` | Generation profile, advanced filter config, and ethnicity list helpers. | diff --git a/node_character.py b/node_character.py new file mode 100644 index 0000000..3f9d171 --- /dev/null +++ b/node_character.py @@ -0,0 +1,809 @@ +from __future__ import annotations + +import json + +try: + from .prompt_builder import ( + build_character_manual_config_json, + build_character_profile_json, + build_character_slot_json, + build_characteristics_config_json, + build_hair_config_json, + character_age_choices, + character_body_choices, + character_descriptor_detail_choices, + character_ethnicity_choices, + character_eye_color_choices, + character_figure_choices, + character_hair_color_choices, + character_hair_length_choices, + character_hair_style_choices, + character_hardcore_clothing_state_choices, + character_hardcore_clothing_values, + character_label_choices, + character_man_body_choices, + character_presence_choices, + character_profile_choices, + character_softcore_outfit_source_choices, + character_softcore_outfit_values, + character_woman_body_choices, + load_character_profile_json, + ) +except ImportError: # Allows local smoke tests from the repository root. + from prompt_builder import ( + build_character_manual_config_json, + build_character_profile_json, + build_character_slot_json, + build_characteristics_config_json, + build_hair_config_json, + character_age_choices, + character_body_choices, + character_descriptor_detail_choices, + character_ethnicity_choices, + character_eye_color_choices, + character_figure_choices, + character_hair_color_choices, + character_hair_length_choices, + character_hair_style_choices, + character_hardcore_clothing_state_choices, + character_hardcore_clothing_values, + character_label_choices, + character_man_body_choices, + character_presence_choices, + character_profile_choices, + character_softcore_outfit_source_choices, + character_softcore_outfit_values, + character_woman_body_choices, + load_character_profile_json, + ) + + +SXCP_HAIR_CONFIG = "SXCP_HAIR_CONFIG" +SXCP_CHARACTERISTICS = "SXCP_CHARACTERISTICS" +SXCP_CHARACTER_MANUAL = "SXCP_CHARACTER_MANUAL" +SXCP_ETHNICITY_LIST = "SXCP_ETHNICITY_LIST" +SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" +SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT" +SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE" + + +class _SxCPHairAxisNode: + AXIS = "color" + PREFIX = "include" + + @classmethod + def _choices(cls): + if cls.AXIS == "color": + return [choice for choice in character_hair_color_choices() if choice != "random"] + if cls.AXIS == "length": + return [choice for choice in character_hair_length_choices() if choice != "random"] + return [choice for choice in character_hair_style_choices() if choice != "random"] + + @classmethod + def INPUT_TYPES(cls): + required = { + "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), + } + for choice in cls._choices(): + required[f"{cls.PREFIX}_{choice}"] = ("BOOLEAN", {"default": False}) + return { + "required": required, + "optional": { + "hair_config": (SXCP_HAIR_CONFIG,), + }, + } + + RETURN_TYPES = (SXCP_HAIR_CONFIG, "STRING") + RETURN_NAMES = ("hair_config", "summary") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build(self, combine_mode="replace_axis", hair_config="", **kwargs): + selected = [ + choice + for choice in self._choices() + if bool(kwargs.get(f"{self.PREFIX}_{choice}", False)) + ] + config = build_hair_config_json( + hair_config=hair_config or "", + axis=self.AXIS, + selected_values=selected, + combine_mode=combine_mode, + ) + parsed = json.loads(config) + return config, parsed.get("summary", "") + + +class SxCPHairColor(_SxCPHairAxisNode): + AXIS = "color" + + +class SxCPHairLength(_SxCPHairAxisNode): + AXIS = "length" + + +class SxCPHairStyle(_SxCPHairAxisNode): + AXIS = "style" + + +def _choice_input_key(prefix, choice): + key = "".join(char if char.isalnum() else "_" for char in str(choice).lower()).strip("_") + while "__" in key: + key = key.replace("__", "_") + return f"{prefix}_{key}" + + +class SxCPCharacterAgeRange: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), + "min_age": ("INT", {"default": 21, "min": 21, "max": 85, "step": 1}), + "max_age": ("INT", {"default": 35, "min": 21, "max": 85, "step": 1}), + }, + "optional": { + "characteristics": (SXCP_CHARACTERISTICS,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") + RETURN_NAMES = ("characteristics", "summary") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build(self, combine_mode, min_age, max_age, characteristics=""): + start = max(21, min(85, int(min_age))) + end = max(21, min(85, int(max_age))) + if end < start: + start, end = end, start + ages = [f"{age}-year-old adult" for age in range(start, end + 1)] + config = build_characteristics_config_json( + characteristics=characteristics or "", + axis="ages", + selected_values=ages, + combine_mode=combine_mode, + ) + return config, json.loads(config).get("summary", "") + + +class _SxCPBodyPoolNode: + SUBJECT = "character" + + @classmethod + def _choices(cls): + if cls.SUBJECT == "woman": + return [choice for choice in character_woman_body_choices() if choice not in ("random", "manual")] + if cls.SUBJECT == "man": + return [choice for choice in character_man_body_choices() if choice not in ("random", "manual")] + return [choice for choice in character_body_choices() if choice not in ("random", "manual")] + + @classmethod + def INPUT_TYPES(cls): + required = { + "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), + } + for choice in cls._choices(): + required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) + return { + "required": required, + "optional": { + "characteristics": (SXCP_CHARACTERISTICS,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") + RETURN_NAMES = ("characteristics", "summary") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build(self, combine_mode="replace_axis", characteristics="", **kwargs): + selected = [ + choice + for choice in self._choices() + if bool(kwargs.get(_choice_input_key("include", choice), False)) + ] + config = build_characteristics_config_json( + characteristics=characteristics or "", + axis="bodies", + selected_values=selected, + combine_mode=combine_mode, + ) + return config, json.loads(config).get("summary", "") + + +class SxCPCharacterBodyPool(_SxCPBodyPoolNode): + SUBJECT = "character" + + +class SxCPWomanBodyPool(_SxCPBodyPoolNode): + SUBJECT = "woman" + + +class SxCPManBodyPool(_SxCPBodyPoolNode): + SUBJECT = "man" + + +class SxCPEyeColorPool: + @classmethod + def INPUT_TYPES(cls): + required = { + "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), + } + for choice in character_eye_color_choices(): + if choice != "random": + required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) + return { + "required": required, + "optional": { + "characteristics": (SXCP_CHARACTERISTICS,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") + RETURN_NAMES = ("characteristics", "summary") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build(self, combine_mode="replace_axis", characteristics="", **kwargs): + selected = [ + choice + for choice in character_eye_color_choices() + if choice != "random" and bool(kwargs.get(_choice_input_key("include", choice), False)) + ] + config = build_characteristics_config_json( + characteristics=characteristics or "", + axis="eyes", + selected_values=selected, + combine_mode=combine_mode, + ) + return config, json.loads(config).get("summary", "") + + +class SxCPCharacterClothing: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), + "softcore_source": (character_softcore_outfit_source_choices(), {"default": "no_change"}), + "hardcore_state": (character_hardcore_clothing_state_choices(), {"default": "no_change"}), + "custom_softcore_outfits": ("STRING", {"default": "", "multiline": True}), + "custom_hardcore_clothing": ("STRING", {"default": "", "multiline": True}), + }, + "optional": { + "characteristics": (SXCP_CHARACTERISTICS,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") + RETURN_NAMES = ("characteristics", "summary") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + combine_mode, + softcore_source, + hardcore_state, + custom_softcore_outfits, + custom_hardcore_clothing, + characteristics="", + ): + config = characteristics or "" + if softcore_source != "no_change": + config = build_characteristics_config_json( + characteristics=config, + axis="softcore_outfits", + selected_values=character_softcore_outfit_values(softcore_source, custom_softcore_outfits), + combine_mode=combine_mode, + ) + if hardcore_state != "no_change": + config = build_characteristics_config_json( + characteristics=config, + axis="hardcore_clothing", + selected_values=character_hardcore_clothing_values(hardcore_state, custom_hardcore_clothing), + combine_mode=combine_mode, + ) + if not config: + config = build_characteristics_config_json(axis="", selected_values=[]) + return config, json.loads(config).get("summary", "") + + +class SxCPCharacterManualDetails: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "combine_mode": (["merge_nonempty", "replace_all"], {"default": "merge_nonempty"}), + "manual_age": ("STRING", {"default": ""}), + "manual_body": ("STRING", {"default": ""}), + "body_phrase": ("STRING", {"default": ""}), + "skin": ("STRING", {"default": ""}), + "hair": ("STRING", {"default": ""}), + "eyes": ("STRING", {"default": ""}), + "softcore_outfit": ("STRING", {"default": ""}), + "hardcore_clothing": ("STRING", {"default": ""}), + }, + "optional": { + "manual": (SXCP_CHARACTER_MANUAL,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTER_MANUAL, "STRING") + RETURN_NAMES = ("manual", "summary") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + combine_mode, + manual_age, + manual_body, + body_phrase, + skin, + hair, + eyes, + softcore_outfit, + hardcore_clothing, + manual="", + ): + config = build_character_manual_config_json( + manual=manual or "", + combine_mode=combine_mode, + manual_age=manual_age, + manual_body=manual_body, + body_phrase=body_phrase, + skin=skin, + hair=hair, + eyes=eyes, + softcore_outfit=softcore_outfit, + hardcore_clothing=hardcore_clothing, + ) + parsed = json.loads(config) + return config, parsed.get("summary", "") + + +class SxCPCharacterSlot: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "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": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), + "ethnicity": (character_ethnicity_choices(), {"default": "random"}), + "figure": (character_figure_choices(), {"default": "random"}), + "body": ([choice for choice in character_body_choices() if choice != "manual"], {"default": "random"}), + "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), + "expression_enabled": ("BOOLEAN", {"default": True}), + "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "presence_mode": (character_presence_choices(), {"default": "visible"}), + "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + }, + "optional": { + "manual": (SXCP_CHARACTER_MANUAL,), + "ethnicity_list": (SXCP_ETHNICITY_LIST,), + "characteristics": (SXCP_CHARACTERISTICS,), + "hair_config": (SXCP_HAIR_CONFIG,), + "character_cast": (SXCP_CHARACTER_CAST,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") + RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + enabled, + subject_type, + label, + slot_seed, + age, + ethnicity, + figure, + body, + descriptor_detail="auto", + expression_enabled=True, + expression_intensity=-1.0, + presence_mode="visible", + softcore_expression_intensity=-1.0, + hardcore_expression_intensity=-1.0, + character_cast="", + ethnicity_list="", + characteristics="", + hair_config="", + manual="", + ): + result = build_character_slot_json( + subject_type=subject_type, + label=label, + slot_seed=slot_seed, + age=age, + manual=manual, + ethnicity=ethnicity_list or ethnicity, + figure=figure, + body=body, + manual_body="", + body_phrase="", + skin="", + hair="", + characteristics=characteristics, + hair_config=hair_config, + eyes="", + descriptor_detail=descriptor_detail, + expression_enabled=expression_enabled, + expression_intensity=expression_intensity, + presence_mode=presence_mode, + softcore_expression_intensity=softcore_expression_intensity, + hardcore_expression_intensity=hardcore_expression_intensity, + softcore_outfit="", + hardcore_clothing="", + enabled=enabled, + character_cast=character_cast or "", + ) + return result["character_cast"], result["character_slot"], result["summary"], result["status"] + + +class SxCPWomanSlot: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "enabled": ("BOOLEAN", {"default": True}), + "label": (character_label_choices(), {"default": "auto_chain"}), + "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), + "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), + "ethnicity": (character_ethnicity_choices(), {"default": "random"}), + "figure_bias": (character_figure_choices(), {"default": "random"}), + "body": ([choice for choice in character_woman_body_choices() if choice != "manual"], {"default": "random"}), + "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), + "expression_enabled": ("BOOLEAN", {"default": True}), + "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + }, + "optional": { + "manual": (SXCP_CHARACTER_MANUAL,), + "ethnicity_list": (SXCP_ETHNICITY_LIST,), + "characteristics": (SXCP_CHARACTERISTICS,), + "hair_config": (SXCP_HAIR_CONFIG,), + "character_cast": (SXCP_CHARACTER_CAST,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") + RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + enabled, + label, + slot_seed, + age, + ethnicity, + figure_bias, + body, + descriptor_detail="auto", + expression_enabled=True, + expression_intensity=-1.0, + softcore_expression_intensity=-1.0, + hardcore_expression_intensity=-1.0, + character_cast="", + ethnicity_list="", + characteristics="", + hair_config="", + manual="", + ): + result = build_character_slot_json( + subject_type="woman", + label=label, + slot_seed=slot_seed, + age=age, + manual=manual, + ethnicity=ethnicity_list or ethnicity, + figure=figure_bias, + body=body, + manual_body="", + body_phrase="", + skin="", + hair="", + characteristics=characteristics, + hair_config=hair_config, + eyes="", + descriptor_detail=descriptor_detail, + expression_enabled=expression_enabled, + expression_intensity=expression_intensity, + softcore_expression_intensity=softcore_expression_intensity, + hardcore_expression_intensity=hardcore_expression_intensity, + softcore_outfit="", + hardcore_clothing="", + enabled=enabled, + character_cast=character_cast or "", + ) + return result["character_cast"], result["character_slot"], result["summary"], result["status"] + + +class SxCPManSlot: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "enabled": ("BOOLEAN", {"default": True}), + "label": (character_label_choices(), {"default": "auto_chain"}), + "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), + "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), + "ethnicity": (character_ethnicity_choices(), {"default": "random"}), + "body": ([choice for choice in character_man_body_choices() if choice != "manual"], {"default": "random"}), + "descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}), + "expression_enabled": ("BOOLEAN", {"default": True}), + "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "presence_mode": (character_presence_choices(), {"default": "visible"}), + "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + }, + "optional": { + "manual": (SXCP_CHARACTER_MANUAL,), + "ethnicity_list": (SXCP_ETHNICITY_LIST,), + "characteristics": (SXCP_CHARACTERISTICS,), + "hair_config": (SXCP_HAIR_CONFIG,), + "character_cast": (SXCP_CHARACTER_CAST,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") + RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + enabled, + label, + slot_seed, + age, + ethnicity, + body, + descriptor_detail="compact", + expression_enabled=True, + expression_intensity=-1.0, + presence_mode="visible", + softcore_expression_intensity=-1.0, + hardcore_expression_intensity=-1.0, + character_cast="", + ethnicity_list="", + characteristics="", + hair_config="", + manual="", + ): + result = build_character_slot_json( + subject_type="man", + label=label, + slot_seed=slot_seed, + age=age, + manual=manual, + ethnicity=ethnicity_list or ethnicity, + figure="random", + body=body, + manual_body="", + body_phrase="", + skin="", + hair="", + characteristics=characteristics, + hair_config=hair_config, + eyes="", + descriptor_detail=descriptor_detail, + expression_enabled=expression_enabled, + expression_intensity=expression_intensity, + presence_mode=presence_mode, + softcore_expression_intensity=softcore_expression_intensity, + hardcore_expression_intensity=hardcore_expression_intensity, + softcore_outfit="", + hardcore_clothing="", + enabled=enabled, + character_cast=character_cast or "", + ) + return result["character_cast"], result["character_slot"], result["summary"], result["status"] + + +class SxCPCharacterProfileSave: + OUTPUT_NODE = True + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "profile_name": ("STRING", {"default": "saved_character"}), + "source": (["metadata_json", "character_slot", "manual"], {"default": "metadata_json"}), + "subject_type": (["woman", "man"], {"default": "woman"}), + "age": ("STRING", {"default": ""}), + "body": ("STRING", {"default": ""}), + "body_phrase": ("STRING", {"default": ""}), + "skin": ("STRING", {"default": ""}), + "hair": ("STRING", {"default": ""}), + "eyes": ("STRING", {"default": ""}), + "figure": ("STRING", {"default": ""}), + "save_now": ("BOOLEAN", {"default": False}), + }, + "optional": { + "metadata_json": ("STRING", {"default": "", "multiline": True}), + "character_slot": (SXCP_CHARACTER_SLOT,), + }, + } + + RETURN_TYPES = (SXCP_CHARACTER_PROFILE, "STRING", "STRING", "STRING", "STRING", SXCP_CHARACTER_PROFILE) + RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "profile_json") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + profile_name, + source, + subject_type, + age, + body, + body_phrase, + skin, + hair, + eyes, + figure, + save_now, + metadata_json="", + character_slot="", + ): + profile = build_character_profile_json( + profile_name=profile_name, + source=source, + metadata_json=metadata_json or "", + character_slot=character_slot or "", + subject_type=subject_type, + age=age, + body=body, + body_phrase=body_phrase, + skin=skin, + hair=hair, + eyes=eyes, + figure=figure, + save_now=save_now, + ) + result = ( + profile["profile_json"], + profile["descriptor"], + profile["profile_name"], + profile["saved_path"], + profile["status"], + profile["profile_json"], + ) + return { + "ui": { + "profile_json": [profile["profile_json"]], + "descriptor": [profile["descriptor"]], + "profile_name": [profile["profile_name"]], + "saved_path": [profile["saved_path"]], + "status": [profile["status"]], + }, + "result": result, + } + + +class SxCPCharacterProfileLoad: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "enabled": ("BOOLEAN", {"default": True}), + "profile_name": (character_profile_choices(), {"default": "manual"}), + "rename_to": ("STRING", {"default": ""}), + "delete_now": ("BOOLEAN", {"default": False}), + "rename_now": ("BOOLEAN", {"default": False}), + }, + "optional": { + "manual_profile_name": ("STRING", {"default": ""}), + "fallback_profile_json": (SXCP_CHARACTER_PROFILE,), + "override_subject_type": (["keep_profile", "woman", "man"], {"default": "keep_profile"}), + "override_age": ("STRING", {"default": ""}), + "override_body": ("STRING", {"default": ""}), + "override_body_phrase": ("STRING", {"default": ""}), + "override_skin": ("STRING", {"default": ""}), + "override_hair": ("STRING", {"default": ""}), + "override_eyes": ("STRING", {"default": ""}), + "override_figure": ("STRING", {"default": ""}), + "override_descriptor_detail": (["keep_profile"] + character_descriptor_detail_choices(), {"default": "keep_profile"}), + }, + } + + RETURN_TYPES = (SXCP_CHARACTER_PROFILE, "STRING", "STRING", "STRING", "STRING", SXCP_CHARACTER_PROFILE) + RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "profile_json") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + enabled, + profile_name, + rename_to, + delete_now, + rename_now, + manual_profile_name="", + fallback_profile_json="", + override_subject_type="keep_profile", + override_age="", + override_body="", + override_body_phrase="", + override_skin="", + override_hair="", + override_eyes="", + override_figure="", + override_descriptor_detail="keep_profile", + ): + chosen_name = manual_profile_name.strip() if profile_name == "manual" and manual_profile_name.strip() else profile_name + profile = load_character_profile_json( + profile_name=chosen_name, + fallback_profile_json=fallback_profile_json or "", + enabled=enabled, + delete_now=delete_now, + rename_now=rename_now, + rename_to=rename_to, + override_subject_type=override_subject_type, + override_age=override_age, + override_body=override_body, + override_body_phrase=override_body_phrase, + override_skin=override_skin, + override_hair=override_hair, + override_eyes=override_eyes, + override_figure=override_figure, + override_descriptor_detail=override_descriptor_detail, + ) + return ( + profile["profile_json"], + profile["descriptor"], + profile["profile_name"], + profile["saved_path"], + profile["status"], + profile["profile_json"], + ) + + +NODE_CLASS_MAPPINGS = { + "SxCPHairLength": SxCPHairLength, + "SxCPHairColor": SxCPHairColor, + "SxCPHairStyle": SxCPHairStyle, + "SxCPCharacterAgeRange": SxCPCharacterAgeRange, + "SxCPCharacterBodyPool": SxCPCharacterBodyPool, + "SxCPWomanBodyPool": SxCPWomanBodyPool, + "SxCPManBodyPool": SxCPManBodyPool, + "SxCPEyeColorPool": SxCPEyeColorPool, + "SxCPCharacterClothing": SxCPCharacterClothing, + "SxCPCharacterManualDetails": SxCPCharacterManualDetails, + "SxCPWomanSlot": SxCPWomanSlot, + "SxCPManSlot": SxCPManSlot, + "SxCPCharacterSlot": SxCPCharacterSlot, + "SxCPCharacterProfileSave": SxCPCharacterProfileSave, + "SxCPCharacterProfileLoad": SxCPCharacterProfileLoad, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SxCPHairLength": "SxCP Hair Length", + "SxCPHairColor": "SxCP Hair Color", + "SxCPHairStyle": "SxCP Hair Style/Cut", + "SxCPCharacterAgeRange": "SxCP Character Age Range", + "SxCPCharacterBodyPool": "SxCP Character Body Pool", + "SxCPWomanBodyPool": "SxCP Woman Body Pool", + "SxCPManBodyPool": "SxCP Man Body Pool", + "SxCPEyeColorPool": "SxCP Eye Color Pool", + "SxCPCharacterClothing": "SxCP Character Clothing", + "SxCPCharacterManualDetails": "SxCP Character Manual Details", + "SxCPWomanSlot": "SxCP Woman Slot", + "SxCPManSlot": "SxCP Man Slot", + "SxCPCharacterSlot": "SxCP Character Slot", + "SxCPCharacterProfileSave": "SxCP Character Profile Save", + "SxCPCharacterProfileLoad": "SxCP Character Profile Load", +} diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 4fa334a..d1a6add 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -1848,6 +1848,170 @@ def smoke_node_route_config_registration() -> None: _expect("weighted cast:" in bias_a[3], "Cast Bias summary lost weighted cast label") +def smoke_node_character_registration() -> None: + required_nodes = [ + "SxCPHairLength", + "SxCPHairColor", + "SxCPHairStyle", + "SxCPCharacterAgeRange", + "SxCPCharacterBodyPool", + "SxCPWomanBodyPool", + "SxCPManBodyPool", + "SxCPEyeColorPool", + "SxCPCharacterClothing", + "SxCPCharacterManualDetails", + "SxCPWomanSlot", + "SxCPManSlot", + "SxCPCharacterSlot", + "SxCPCharacterProfileSave", + "SxCPCharacterProfileLoad", + ] + 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") + + woman_slot_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPWomanSlot"] + woman_slot_inputs = woman_slot_node.INPUT_TYPES().get("required") or {} + _expect("slot_seed" in woman_slot_inputs, "Woman Slot lost slot_seed input") + _expect("tooltip" in woman_slot_inputs["slot_seed"][1], "Woman Slot tooltip injection missing") + + hair_config, hair_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPHairColor"]().build( + "replace_axis", + "", + include_blonde=True, + ) + parsed_hair = json.loads(hair_config) + _expect(parsed_hair.get("colors") == ["blonde"], "Hair Color did not output selected blonde pool") + _expect("colors=blonde" in hair_summary, "Hair Color summary changed unexpectedly") + + age_config, age_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterAgeRange"]().build( + "replace_axis", + 25, + 27, + ) + parsed_age = json.loads(age_config) + _expect(parsed_age.get("ages") == ["25-year-old adult", "26-year-old adult", "27-year-old adult"], "Age Range output changed") + _expect("25-year-old adult" in age_summary, "Age Range summary lost selected ages") + + body_config, _body_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPWomanBodyPool"]().build( + "replace_axis", + "", + include_curvy=True, + ) + _expect(json.loads(body_config).get("bodies") == ["curvy"], "Woman Body Pool did not output selected body") + + eye_config, _eye_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPEyeColorPool"]().build( + "replace_axis", + "", + include_blue=True, + ) + _expect(json.loads(eye_config).get("eyes") == ["blue"], "Eye Color Pool did not output selected eye color") + + clothing_config, clothing_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterClothing"]().build( + "replace_axis", + "lingerie_tease", + "fully_nude", + "", + "", + ) + parsed_clothing = json.loads(clothing_config) + _expect(parsed_clothing.get("softcore_outfits"), "Character Clothing lost softcore outfit pool") + _expect(parsed_clothing.get("hardcore_clothing") == ["fully nude"], "Character Clothing lost hardcore clothing state") + _expect("soft_outfits=" in clothing_summary, "Character Clothing summary lost outfit count") + + manual_config, manual_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterManualDetails"]().build( + "merge_nonempty", + "31-year-old adult", + "curvy", + "custom body phrase", + "warm skin", + "short blonde hair", + "blue eyes", + "red dress", + "fully nude", + ) + parsed_manual = json.loads(manual_config) + _expect(parsed_manual.get("manual_age") == "31-year-old adult", "Manual Details lost manual_age") + _expect(parsed_manual.get("softcore_outfit") == "red dress", "Manual Details lost softcore outfit") + _expect("manual_age=31-year-old adult" in manual_summary, "Manual Details summary changed unexpectedly") + + cast, slot, slot_summary, slot_status = woman_slot_node().build( + True, + "A", + 123, + "25-year-old adult", + "western_european", + "balanced", + "curvy", + "full", + True, + 0.5, + 0.4, + 0.8, + "", + "", + "", + hair_config, + "", + ) + parsed_slot = json.loads(slot) + _expect(parsed_slot.get("subject_type") == "woman", "Woman Slot output lost subject type") + _expect(parsed_slot.get("slot_seed") == 123, "Woman Slot output lost slot seed") + _expect("Woman A" in slot_summary, "Woman Slot summary lost label") + _expect("1 slot(s)" in slot_status, "Woman Slot status lost cast count") + _expect(json.loads(cast).get("slots"), "Woman Slot did not output chained cast JSON") + + man_cast, man_slot, _man_summary, _man_status = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPManSlot"]().build( + True, + "A", + 124, + "40-year-old adult", + "western_european", + "average", + "compact", + True, + 0.3, + "pov", + 0.2, + 0.7, + cast, + "", + "", + "", + "", + ) + _expect(json.loads(man_slot).get("presence_mode") == "pov", "Man Slot output lost POV presence mode") + _expect(len(json.loads(man_cast).get("slots") or []) == 2, "Man Slot did not append to incoming cast") + + save_result = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterProfileSave"]().build( + "smoke_profile", + "character_slot", + "woman", + "", + "", + "", + "", + "", + "", + "", + False, + character_slot=slot, + ) + saved_profile = save_result["result"][0] + _expect(save_result["result"][2] == "smoke_profile", "Profile Save lost profile name") + _expect(save_result["result"][4] == "not_saved", "Profile Save should not write when save_now is false") + loaded_profile = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterProfileLoad"]().build( + True, + "manual", + "", + False, + False, + fallback_profile_json=saved_profile, + ) + _expect(loaded_profile[4] == "fallback", "Profile Load should consume fallback profile JSON") + _expect(json.loads(loaded_profile[0]).get("profile_type") == "character", "Profile Load returned wrong profile type") + + def smoke_node_profile_filter_registration() -> None: required_nodes = [ "SxCPGenerationProfile", @@ -1955,6 +2119,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("node_utility_registration", smoke_node_utility_registration), ("node_camera_registration", smoke_node_camera_registration), ("node_route_config_registration", smoke_node_route_config_registration), + ("node_character_registration", smoke_node_character_registration), ("node_profile_filter_registration", smoke_node_profile_filter_registration), ]