Add chainable hair and manual character nodes

This commit is contained in:
2026-06-25 00:01:19 +02:00
parent ed6c7b8dc0
commit 01b59df30e
3 changed files with 698 additions and 96 deletions
+26 -9
View File
@@ -20,6 +20,10 @@ The node is registered as:
- `prompt_builder / SxCP Cast Control` - `prompt_builder / SxCP Cast Control`
- `prompt_builder / SxCP Generation Profile` - `prompt_builder / SxCP Generation Profile`
- `prompt_builder / SxCP Ethnicity List` - `prompt_builder / SxCP Ethnicity List`
- `prompt_builder / SxCP Hair Length`
- `prompt_builder / SxCP Hair Color`
- `prompt_builder / SxCP Hair Style/Cut`
- `prompt_builder / SxCP Character Manual Details`
- `prompt_builder / SxCP Advanced Filters` - `prompt_builder / SxCP Advanced Filters`
- `prompt_builder / SxCP Prompt Builder From Configs` - `prompt_builder / SxCP Prompt Builder From Configs`
- `prompt_builder / SxCP Woman Slot` - `prompt_builder / SxCP Woman Slot`
@@ -150,10 +154,15 @@ ComfyUI image batches require matching dimensions. For mixed image formats, use
`SxCP Woman Slot` and `SxCP Man Slot` are the scalable per-participant control `SxCP Woman Slot` and `SxCP Man Slot` are the scalable per-participant control
nodes. `Cast Control` still decides how many women and men are generated; slot nodes. `Cast Control` still decides how many women and men are generated; slot
nodes decide who those people are. Each slot defines one participant with nodes decide who those people are. Each slot defines one participant with age,
optional overrides for age, ethnicity, body/body phrase, skin, hair, and eyes. ethnicity, body, expression, and config inputs. Leave fields on `random` to let
Leave any field on `random` or blank to let the generator fill that part from the generator fill that part from the normal pools.
the normal pools; set exact values only where you want control.
Use `SxCP Character Manual Details` when you need exact manual text for a slot:
manual age, manual body, body phrase, skin, hair, eyes, softcore outfit, or
hardcore clothing. Connect `Character Manual Details.manual` to the slot's
`manual` input. Manual detail nodes are chainable through their top `manual`
input/output.
Each slot has `slot_seed`. Leave it at `-1` to follow the generator's normal Each slot has `slot_seed`. Leave it at `-1` to follow the generator's normal
person seed. Set any fixed value when the slot's `random` fields should resolve person seed. Set any fixed value when the slot's `random` fields should resolve
@@ -166,6 +175,16 @@ that character should randomize inside a selected heritage list. This is useful
for narrowing broad groups, for example choosing French/Germanic/Nordic/Slavic for narrowing broad groups, for example choosing French/Germanic/Nordic/Slavic
European entries instead of the entire `european` pool. European entries instead of the entire `european` pool.
Hair can be controlled the same way with chainable characteristic nodes. Connect
the final `hair_config` output to a slot's `hair_config` input:
`Hair Length -> Hair Color -> Hair Style/Cut -> Woman Slot.hair_config`
Each hair node only changes its own axis and passes the rest through. For
example, select `long` in `Hair Length`, select the blonde variants you allow in
`Hair Color`, then select `waves` and `ponytail` in `Hair Style/Cut`. If the slot's
manual details include `hair`, that exact text overrides the hair config.
Use `Woman Slot` for women because it exposes woman-focused body choices and a Use `Woman Slot` for women because it exposes woman-focused body choices and a
`figure_bias` selector. Use `Man Slot` for men because it exposes man-focused `figure_bias` selector. Use `Man Slot` for men because it exposes man-focused
body choices and omits figure bias. The older generic `SxCP Character Slot` body choices and omits figure bias. The older generic `SxCP Character Slot`
@@ -204,11 +223,9 @@ For Insta/OF pairs, slots also expose character-level overrides:
- `softcore_expression_intensity` and `hardcore_expression_intensity`: override - `softcore_expression_intensity` and `hardcore_expression_intensity`: override
the option-node expression fallback for that character and that output half. the option-node expression fallback for that character and that output half.
- `softcore_outfit`: overrides the character's softcore clothing. For `Woman A` - `softcore_outfit` and `hardcore_clothing` are provided by
this replaces the generated teaser outfit; for partners it replaces random `SxCP Character Manual Details` and connected through the slot's `manual`
partner styling. input. For `Woman A`, a hardcore clothing override replaces the global
- `hardcore_clothing`: adds direct character clothing/nudity wording in the
hardcore output. A `Woman A` hardcore clothing override replaces the global
`hardcore_clothing_continuity` text to avoid contradictory clothing prompts. `hardcore_clothing_continuity` text to avoid contradictory clothing prompts.
Slots are chainable through the `character_cast` input/output. In automatic Slots are chainable through the `character_cast` input/output. In automatic
+176 -78
View File
@@ -19,10 +19,12 @@ try:
build_cast_config_json, build_cast_config_json,
build_category_config_json, build_category_config_json,
build_character_slot_json, build_character_slot_json,
build_character_manual_config_json,
build_character_profile_json, build_character_profile_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,
build_hair_config_json,
build_insta_of_options_json, build_insta_of_options_json,
build_insta_of_pair, build_insta_of_pair,
build_prompt, build_prompt,
@@ -48,6 +50,9 @@ try:
character_descriptor_detail_choices, character_descriptor_detail_choices,
character_ethnicity_choices, character_ethnicity_choices,
character_figure_choices, character_figure_choices,
character_hair_color_choices,
character_hair_length_choices,
character_hair_style_choices,
character_label_choices, character_label_choices,
character_man_body_choices, character_man_body_choices,
character_presence_choices, character_presence_choices,
@@ -72,10 +77,12 @@ except ImportError:
build_cast_config_json, build_cast_config_json,
build_category_config_json, build_category_config_json,
build_character_slot_json, build_character_slot_json,
build_character_manual_config_json,
build_character_profile_json, build_character_profile_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,
build_hair_config_json,
build_insta_of_options_json, build_insta_of_options_json,
build_insta_of_pair, build_insta_of_pair,
build_prompt, build_prompt,
@@ -101,6 +108,9 @@ except ImportError:
character_descriptor_detail_choices, character_descriptor_detail_choices,
character_ethnicity_choices, character_ethnicity_choices,
character_figure_choices, character_figure_choices,
character_hair_color_choices,
character_hair_length_choices,
character_hair_style_choices,
character_label_choices, character_label_choices,
character_man_body_choices, character_man_body_choices,
character_presence_choices, character_presence_choices,
@@ -797,6 +807,119 @@ class SxCPEthnicityList:
return result["ethnicity"], result["filter_config"], result["summary"] return result["ethnicity"], result["filter_config"], result["summary"]
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": ("STRING", {"default": "", "multiline": True}),
},
}
RETURN_TYPES = ("STRING", "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"
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": ("STRING", {"default": "", "multiline": True}),
},
}
RETURN_TYPES = ("STRING", "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: class SxCPPromptBuilderFromConfigs:
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
@@ -877,27 +1000,21 @@ class SxCPCharacterSlot:
"subject_type": (["woman", "man"], {"default": "woman"}), "subject_type": (["woman", "man"], {"default": "woman"}),
"label": (character_label_choices(), {"default": "auto_chain"}), "label": (character_label_choices(), {"default": "auto_chain"}),
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
"age": (character_age_choices(), {"default": "random"}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}),
"manual_age": ("STRING", {"default": ""}),
"ethnicity": (character_ethnicity_choices(), {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}),
"figure": (character_figure_choices(), {"default": "random"}), "figure": (character_figure_choices(), {"default": "random"}),
"body": (character_body_choices(), {"default": "random"}), "body": ([choice for choice in character_body_choices() if choice != "manual"], {"default": "random"}),
"manual_body": ("STRING", {"default": ""}),
"body_phrase": ("STRING", {"default": ""}),
"skin": ("STRING", {"default": ""}),
"hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
"expression_enabled": ("BOOLEAN", {"default": True}), "expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"presence_mode": (character_presence_choices(), {"default": "visible"}), "presence_mode": (character_presence_choices(), {"default": "visible"}),
"softcore_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}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"softcore_outfit": ("STRING", {"default": ""}),
"hardcore_clothing": ("STRING", {"default": ""}),
}, },
"optional": { "optional": {
"manual": ("STRING", {"default": "", "multiline": True}),
"ethnicity_list": ("STRING", {"default": "", "multiline": True}), "ethnicity_list": ("STRING", {"default": "", "multiline": True}),
"hair_config": ("STRING", {"default": "", "multiline": True}),
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
}, },
} }
@@ -914,48 +1031,43 @@ class SxCPCharacterSlot:
label, label,
slot_seed, slot_seed,
age, age,
manual_age,
ethnicity, ethnicity,
figure, figure,
body, body,
manual_body,
body_phrase,
skin,
hair,
eyes,
descriptor_detail="auto", descriptor_detail="auto",
expression_enabled=True, expression_enabled=True,
expression_intensity=-1.0, expression_intensity=-1.0,
presence_mode="visible", presence_mode="visible",
softcore_expression_intensity=-1.0, softcore_expression_intensity=-1.0,
hardcore_expression_intensity=-1.0, hardcore_expression_intensity=-1.0,
softcore_outfit="",
hardcore_clothing="",
character_cast="", character_cast="",
ethnicity_list="", ethnicity_list="",
hair_config="",
manual="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
subject_type=subject_type, subject_type=subject_type,
label=label, label=label,
slot_seed=slot_seed, slot_seed=slot_seed,
age=age, age=age,
manual_age=manual_age, manual=manual,
ethnicity=ethnicity_list or ethnicity, ethnicity=ethnicity_list or ethnicity,
figure=figure, figure=figure,
body=body, body=body,
manual_body=manual_body, manual_body="",
body_phrase=body_phrase, body_phrase="",
skin=skin, skin="",
hair=hair, hair="",
eyes=eyes, hair_config=hair_config,
eyes="",
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled, expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
presence_mode=presence_mode, presence_mode=presence_mode,
softcore_expression_intensity=softcore_expression_intensity, softcore_expression_intensity=softcore_expression_intensity,
hardcore_expression_intensity=hardcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity,
softcore_outfit=softcore_outfit, softcore_outfit="",
hardcore_clothing=hardcore_clothing, hardcore_clothing="",
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -970,26 +1082,20 @@ class SxCPWomanSlot:
"enabled": ("BOOLEAN", {"default": True}), "enabled": ("BOOLEAN", {"default": True}),
"label": (character_label_choices(), {"default": "auto_chain"}), "label": (character_label_choices(), {"default": "auto_chain"}),
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
"age": (character_age_choices(), {"default": "random"}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}),
"manual_age": ("STRING", {"default": ""}),
"ethnicity": (character_ethnicity_choices(), {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}),
"figure_bias": (character_figure_choices(), {"default": "random"}), "figure_bias": (character_figure_choices(), {"default": "random"}),
"body": (character_woman_body_choices(), {"default": "random"}), "body": ([choice for choice in character_woman_body_choices() if choice != "manual"], {"default": "random"}),
"manual_body": ("STRING", {"default": ""}),
"body_phrase": ("STRING", {"default": ""}),
"skin": ("STRING", {"default": ""}),
"hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
"expression_enabled": ("BOOLEAN", {"default": True}), "expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "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}), "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}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"softcore_outfit": ("STRING", {"default": ""}),
"hardcore_clothing": ("STRING", {"default": ""}),
}, },
"optional": { "optional": {
"manual": ("STRING", {"default": "", "multiline": True}),
"ethnicity_list": ("STRING", {"default": "", "multiline": True}), "ethnicity_list": ("STRING", {"default": "", "multiline": True}),
"hair_config": ("STRING", {"default": "", "multiline": True}),
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
}, },
} }
@@ -1005,46 +1111,41 @@ class SxCPWomanSlot:
label, label,
slot_seed, slot_seed,
age, age,
manual_age,
ethnicity, ethnicity,
figure_bias, figure_bias,
body, body,
manual_body,
body_phrase,
skin,
hair,
eyes,
descriptor_detail="auto", descriptor_detail="auto",
expression_enabled=True, expression_enabled=True,
expression_intensity=-1.0, expression_intensity=-1.0,
softcore_expression_intensity=-1.0, softcore_expression_intensity=-1.0,
hardcore_expression_intensity=-1.0, hardcore_expression_intensity=-1.0,
softcore_outfit="",
hardcore_clothing="",
character_cast="", character_cast="",
ethnicity_list="", ethnicity_list="",
hair_config="",
manual="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
subject_type="woman", subject_type="woman",
label=label, label=label,
slot_seed=slot_seed, slot_seed=slot_seed,
age=age, age=age,
manual_age=manual_age, manual=manual,
ethnicity=ethnicity_list or ethnicity, ethnicity=ethnicity_list or ethnicity,
figure=figure_bias, figure=figure_bias,
body=body, body=body,
manual_body=manual_body, manual_body="",
body_phrase=body_phrase, body_phrase="",
skin=skin, skin="",
hair=hair, hair="",
eyes=eyes, hair_config=hair_config,
eyes="",
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled, expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
softcore_expression_intensity=softcore_expression_intensity, softcore_expression_intensity=softcore_expression_intensity,
hardcore_expression_intensity=hardcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity,
softcore_outfit=softcore_outfit, softcore_outfit="",
hardcore_clothing=hardcore_clothing, hardcore_clothing="",
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -1059,26 +1160,20 @@ class SxCPManSlot:
"enabled": ("BOOLEAN", {"default": True}), "enabled": ("BOOLEAN", {"default": True}),
"label": (character_label_choices(), {"default": "auto_chain"}), "label": (character_label_choices(), {"default": "auto_chain"}),
"slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}),
"age": (character_age_choices(), {"default": "random"}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}),
"manual_age": ("STRING", {"default": ""}),
"ethnicity": (character_ethnicity_choices(), {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}),
"body": (character_man_body_choices(), {"default": "random"}), "body": ([choice for choice in character_man_body_choices() if choice != "manual"], {"default": "random"}),
"manual_body": ("STRING", {"default": ""}),
"body_phrase": ("STRING", {"default": ""}),
"skin": ("STRING", {"default": ""}),
"hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}),
"expression_enabled": ("BOOLEAN", {"default": True}), "expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"presence_mode": (character_presence_choices(), {"default": "visible"}), "presence_mode": (character_presence_choices(), {"default": "visible"}),
"softcore_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}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"softcore_outfit": ("STRING", {"default": ""}),
"hardcore_clothing": ("STRING", {"default": ""}),
}, },
"optional": { "optional": {
"manual": ("STRING", {"default": "", "multiline": True}),
"ethnicity_list": ("STRING", {"default": "", "multiline": True}), "ethnicity_list": ("STRING", {"default": "", "multiline": True}),
"hair_config": ("STRING", {"default": "", "multiline": True}),
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
}, },
} }
@@ -1094,47 +1189,42 @@ class SxCPManSlot:
label, label,
slot_seed, slot_seed,
age, age,
manual_age,
ethnicity, ethnicity,
body, body,
manual_body,
body_phrase,
skin,
hair,
eyes,
descriptor_detail="compact", descriptor_detail="compact",
expression_enabled=True, expression_enabled=True,
expression_intensity=-1.0, expression_intensity=-1.0,
presence_mode="visible", presence_mode="visible",
softcore_expression_intensity=-1.0, softcore_expression_intensity=-1.0,
hardcore_expression_intensity=-1.0, hardcore_expression_intensity=-1.0,
softcore_outfit="",
hardcore_clothing="",
character_cast="", character_cast="",
ethnicity_list="", ethnicity_list="",
hair_config="",
manual="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
subject_type="man", subject_type="man",
label=label, label=label,
slot_seed=slot_seed, slot_seed=slot_seed,
age=age, age=age,
manual_age=manual_age, manual=manual,
ethnicity=ethnicity_list or ethnicity, ethnicity=ethnicity_list or ethnicity,
figure="random", figure="random",
body=body, body=body,
manual_body=manual_body, manual_body="",
body_phrase=body_phrase, body_phrase="",
skin=skin, skin="",
hair=hair, hair="",
eyes=eyes, hair_config=hair_config,
eyes="",
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled, expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
presence_mode=presence_mode, presence_mode=presence_mode,
softcore_expression_intensity=softcore_expression_intensity, softcore_expression_intensity=softcore_expression_intensity,
hardcore_expression_intensity=hardcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity,
softcore_outfit=softcore_outfit, softcore_outfit="",
hardcore_clothing=hardcore_clothing, hardcore_clothing="",
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -1595,6 +1685,10 @@ NODE_CLASS_MAPPINGS = {
"SxCPCastControl": SxCPCastControl, "SxCPCastControl": SxCPCastControl,
"SxCPGenerationProfile": SxCPGenerationProfile, "SxCPGenerationProfile": SxCPGenerationProfile,
"SxCPEthnicityList": SxCPEthnicityList, "SxCPEthnicityList": SxCPEthnicityList,
"SxCPHairLength": SxCPHairLength,
"SxCPHairColor": SxCPHairColor,
"SxCPHairStyle": SxCPHairStyle,
"SxCPCharacterManualDetails": SxCPCharacterManualDetails,
"SxCPAdvancedFilters": SxCPAdvancedFilters, "SxCPAdvancedFilters": SxCPAdvancedFilters,
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs, "SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
"SxCPWomanSlot": SxCPWomanSlot, "SxCPWomanSlot": SxCPWomanSlot,
@@ -1621,6 +1715,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SxCPCastControl": "SxCP Cast Control", "SxCPCastControl": "SxCP Cast Control",
"SxCPGenerationProfile": "SxCP Generation Profile", "SxCPGenerationProfile": "SxCP Generation Profile",
"SxCPEthnicityList": "SxCP Ethnicity List", "SxCPEthnicityList": "SxCP Ethnicity List",
"SxCPHairLength": "SxCP Hair Length",
"SxCPHairColor": "SxCP Hair Color",
"SxCPHairStyle": "SxCP Hair Style/Cut",
"SxCPCharacterManualDetails": "SxCP Character Manual Details",
"SxCPAdvancedFilters": "SxCP Advanced Filters", "SxCPAdvancedFilters": "SxCP Advanced Filters",
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs", "SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
"SxCPWomanSlot": "SxCP Woman Slot", "SxCPWomanSlot": "SxCP Woman Slot",
+496 -9
View File
@@ -219,6 +219,58 @@ CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "min
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"] CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"} CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
CHARACTER_HAIR_COLOR_CHOICES = [
"random",
"black",
"brown",
"dark_brown",
"chestnut",
"auburn",
"copper",
"red",
"blonde",
"platinum_blonde",
"ash_blonde",
"honey_blonde",
"strawberry_blonde",
"dark_blonde",
"silver_gray",
"white",
]
CHARACTER_HAIR_LENGTH_CHOICES = [
"random",
"very_short",
"short",
"bob_lob",
"shoulder_length",
"medium",
"long",
"very_long",
"updo",
]
CHARACTER_HAIR_STYLE_CHOICES = [
"random",
"straight",
"waves",
"loose_waves",
"curls",
"tight_curls",
"pixie_cut",
"bob",
"lob",
"shag",
"ponytail",
"braid",
"braids",
"bun",
"messy_bun",
"locs",
"twists",
"afro",
"natural_curls",
"wet_hair",
"slicked_back",
]
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"] CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"] HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
@@ -1697,6 +1749,18 @@ def character_presence_choices() -> list[str]:
return list(CHARACTER_PRESENCE_CHOICES) return list(CHARACTER_PRESENCE_CHOICES)
def character_hair_color_choices() -> list[str]:
return list(CHARACTER_HAIR_COLOR_CHOICES)
def character_hair_length_choices() -> list[str]:
return list(CHARACTER_HAIR_LENGTH_CHOICES)
def character_hair_style_choices() -> list[str]:
return list(CHARACTER_HAIR_STYLE_CHOICES)
def character_ethnicity_choices() -> list[str]: def character_ethnicity_choices() -> list[str]:
return ["random"] + list(ETHNICITY_FILTER_CHOICES) return ["random"] + list(ETHNICITY_FILTER_CHOICES)
@@ -2426,6 +2490,74 @@ def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[st
return raw return raw
CHARACTER_MANUAL_FIELDS = (
"manual_age",
"manual_body",
"body_phrase",
"skin",
"hair",
"eyes",
"softcore_outfit",
"hardcore_clothing",
)
def _parse_character_manual_config(value: str | dict[str, Any] | None) -> dict[str, str]:
if not value:
return {}
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return {}
if not isinstance(raw, dict):
return {}
return {
key: str(raw.get(key) or "").strip()
for key in CHARACTER_MANUAL_FIELDS
if str(raw.get(key) or "").strip()
}
def _character_manual_summary(config: dict[str, str]) -> str:
parts = [f"{key}={value}" for key, value in config.items() if value]
return "; ".join(parts) if parts else "manual unrestricted"
def build_character_manual_config_json(
manual: str | dict[str, Any] | None = "",
combine_mode: str = "merge_nonempty",
manual_age: str = "",
manual_body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
eyes: str = "",
softcore_outfit: str = "",
hardcore_clothing: str = "",
) -> str:
base = {} if combine_mode == "replace_all" else _parse_character_manual_config(manual)
updates = {
"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,
}
for key, value in updates.items():
value = str(value or "").strip()
if value:
base[key] = value
result = {"config_type": "character_manual", **base}
result["summary"] = _character_manual_summary(base)
return json.dumps(result, ensure_ascii=True, sort_keys=True)
def _slot_value(value: Any) -> str: def _slot_value(value: Any) -> str:
text = str(value or "").strip() text = str(value or "").strip()
if text.lower() in CHARACTER_RANDOM_TOKENS: if text.lower() in CHARACTER_RANDOM_TOKENS:
@@ -2663,6 +2795,305 @@ def _normalize_slot_ethnicity(value: Any) -> str:
return normalize_ethnicity_filter(value, "random", allow_random=True) return normalize_ethnicity_filter(value, "random", allow_random=True)
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
text = str(value or "random").strip().lower().replace("-", "_").replace(" ", "_")
return text if text in choices else "random"
def _infer_hair_color_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("platinum_blonde", ("platinum-blonde", "platinum blonde", "platinum")),
("strawberry_blonde", ("strawberry-blonde", "strawberry blonde")),
("honey_blonde", ("honey-blonde", "honey blonde")),
("ash_blonde", ("ash-blonde", "ash blonde")),
("dark_blonde", ("dark-blonde", "dark blonde")),
(
"blonde",
(
"light-blonde",
"light blonde",
"blonde",
"flaxen",
"wheat-blonde",
"wheat blonde",
"beige-blonde",
"beige blonde",
"sandy-blonde",
"sandy blonde",
),
),
("silver_gray", ("silver-gray", "silver grey", "silver", "gray", "grey")),
("dark_brown", ("dark-brown", "dark brown", "espresso")),
("chestnut", ("chestnut",)),
("auburn", ("auburn",)),
("copper", ("copper",)),
("red", ("red hair", "redhead")),
("black", ("black",)),
("brown", ("brown", "brunette", "caramel")),
("white", ("white",)),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def _infer_hair_length_key(text: Any) -> str:
value = str(text or "").lower()
if any(token in value for token in ("very long", "waist-length", "hip-length")):
return "very_long"
if "long" in value:
return "long"
if "shoulder-length" in value or "shoulder length" in value:
return "shoulder_length"
if "medium-length" in value or "medium length" in value:
return "medium"
if any(token in value for token in ("bob", "lob")):
return "bob_lob"
if any(token in value for token in ("pixie", "short", "cropped", "tapered")):
return "short"
if any(token in value for token in ("bun", "updo")):
return "updo"
return "random"
def _infer_hair_style_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("pixie_cut", ("pixie",)),
("messy_bun", ("messy bun",)),
("bun", ("bun", "updo")),
("ponytail", ("ponytail",)),
("braids", ("braids", "box braids", "cornrow")),
("braid", ("braid",)),
("locs", ("locs", "dreadlocks")),
("twists", ("twists",)),
("afro", ("afro",)),
("natural_curls", ("natural curls", "natural coils", "coils")),
("tight_curls", ("tight curls", "tight coils")),
("curls", ("curls", "curly")),
("loose_waves", ("loose waves",)),
("waves", ("waves", "wavy")),
("lob", ("lob",)),
("bob", ("bob",)),
("shag", ("shag",)),
("wet_hair", ("wet hair", "damp hair")),
("slicked_back", ("slicked-back", "slicked back")),
("straight", ("straight", "sleek")),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def _choose_hair_key(rng: random.Random, choices: list[str]) -> str:
pool = [choice for choice in choices if choice != "random"]
return g.choose(rng, pool) if pool else "random"
def _normalize_hair_values(values: Any, choices: list[str]) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[,;\n]+", values) if part.strip()]
elif isinstance(values, (list, tuple, set)):
raw_values = list(values)
else:
raw_values = []
normalized: list[str] = []
for value in raw_values:
key = _normalize_hair_choice(value, choices)
if key != "random" and key not in normalized:
normalized.append(key)
return normalized
def _empty_hair_config() -> dict[str, Any]:
return {"config_type": "hair_characteristics", "colors": [], "lengths": [], "styles": []}
def _parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_hair_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_hair_config()
if not isinstance(raw, dict):
return _empty_hair_config()
return {
"config_type": "hair_characteristics",
"colors": _normalize_hair_values(raw.get("colors"), CHARACTER_HAIR_COLOR_CHOICES),
"lengths": _normalize_hair_values(raw.get("lengths"), CHARACTER_HAIR_LENGTH_CHOICES),
"styles": _normalize_hair_values(raw.get("styles"), CHARACTER_HAIR_STYLE_CHOICES),
}
def _hair_config_summary(config: dict[str, Any]) -> str:
parts = []
for label, key in (("colors", "colors"), ("lengths", "lengths"), ("styles", "styles")):
values = config.get(key) or []
if values:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "hair unrestricted"
def build_hair_config_json(
hair_config: str | dict[str, Any] | None = "",
axis: str = "color",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = _parse_hair_config(hair_config)
axis_key = {"color": "colors", "length": "lengths", "style": "styles"}.get(str(axis or "").strip().lower())
choice_map = {
"colors": CHARACTER_HAIR_COLOR_CHOICES,
"lengths": CHARACTER_HAIR_LENGTH_CHOICES,
"styles": CHARACTER_HAIR_STYLE_CHOICES,
}
if axis_key:
values = _normalize_hair_values(selected_values, choice_map[axis_key])
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = _hair_config_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _hair_color_text(key: str) -> str:
return {
"black": "black",
"brown": "brown",
"dark_brown": "dark-brown",
"chestnut": "chestnut",
"auburn": "auburn",
"copper": "copper",
"red": "red",
"blonde": "blonde",
"platinum_blonde": "platinum-blonde",
"ash_blonde": "ash-blonde",
"honey_blonde": "honey-blonde",
"strawberry_blonde": "strawberry-blonde",
"dark_blonde": "dark-blonde",
"silver_gray": "silver-gray",
"white": "white",
}.get(key, "brown")
def _hair_length_text(key: str) -> str:
return {
"very_short": "very short",
"short": "short",
"bob_lob": "",
"shoulder_length": "shoulder-length",
"medium": "medium-length",
"long": "long",
"very_long": "very long",
"updo": "",
}.get(key, "")
def _hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) -> str:
color = _hair_color_text(color_key)
length = _hair_length_text(length_key)
prefix = " ".join(part for part in (length, color) if part)
if style_key == "pixie_cut":
return f"short {color} pixie cut"
if style_key == "bob":
return f"{color} bob" if length_key in ("random", "bob_lob", "short") else f"{prefix} bob"
if style_key == "lob":
return f"shoulder-length {color} lob" if length_key in ("random", "bob_lob") else f"{prefix} lob"
if style_key == "shag":
return f"{prefix or color} shag"
if style_key == "ponytail":
return f"{prefix or color} ponytail"
if style_key == "braid":
return f"{prefix or color} braid"
if style_key == "braids":
return f"{prefix or color} braids"
if style_key == "bun":
return f"{prefix} hair in a bun" if length else f"{color} bun"
if style_key == "messy_bun":
return f"{prefix} hair in a messy bun" if length else f"messy {color} bun"
if style_key == "locs":
return f"{prefix or color} locs"
if style_key == "twists":
return f"{prefix or color} twists"
if style_key == "afro":
return f"{color} afro"
if style_key == "natural_curls":
return f"{prefix or color} natural curls"
if style_key == "wet_hair":
return f"{prefix or color} wet hair"
if style_key == "slicked_back":
return f"slicked-back {color} hair"
if style_key == "straight":
return f"{prefix or color} straight hair"
if style_key == "loose_waves":
return f"{prefix or color} loose waves"
if style_key == "tight_curls":
return f"{prefix or color} tight curls"
if style_key == "curls":
return f"{prefix or color} curls"
return f"{prefix or color} waves"
def _hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
hair_config = _parse_hair_config(slot.get("hair_config"))
color_choice = _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES)
length_choice = _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES)
style_choice = _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES)
color_options = hair_config.get("colors") or []
length_options = hair_config.get("lengths") or []
style_options = hair_config.get("styles") or []
if (
color_choice == "random"
and length_choice == "random"
and style_choice == "random"
and not color_options
and not length_options
and not style_options
):
return ""
if color_choice != "random":
color_key = color_choice
elif color_options:
color_key = g.choose(rng, color_options)
else:
color_key = _infer_hair_color_key(base_hair)
if length_choice != "random":
length_key = length_choice
elif length_options:
length_key = g.choose(rng, length_options)
else:
length_key = _infer_hair_length_key(base_hair)
if style_choice != "random":
style_key = style_choice
elif style_options:
style_key = g.choose(rng, style_options)
else:
style_key = _infer_hair_style_key(base_hair)
if color_key == "random":
color_key = _choose_hair_key(rng, CHARACTER_HAIR_COLOR_CHOICES)
if length_key == "random":
length_key = _choose_hair_key(rng, CHARACTER_HAIR_LENGTH_CHOICES)
if style_key == "random":
style_key = _choose_hair_key(rng, CHARACTER_HAIR_STYLE_CHOICES)
if length_key == "updo" and style_key not in ("ponytail", "braid", "braids", "bun", "messy_bun", "locs", "twists"):
style_key = g.choose(rng, ["ponytail", "braid", "bun", "messy_bun"])
return _hair_phrase_from_parts(color_key, length_key, style_key)
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]: def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower() subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
if subject_type not in ("woman", "man"): if subject_type not in ("woman", "man"):
@@ -2674,12 +3105,31 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
if label not in CHARACTER_LABEL_CHOICES: if label not in CHARACTER_LABEL_CHOICES:
label = "auto_chain" label = "auto_chain"
age = _slot_manual_or_choice(str(slot.get("age") or "random"), str(slot.get("manual_age") or "")) manual_config = _parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))
body = _slot_manual_or_choice(str(slot.get("body") or "random"), str(slot.get("manual_body") or ""))
raw_age = str(slot.get("age") or "random")
raw_manual_age = str(slot.get("manual_age") or "").strip()
if not raw_manual_age and manual_config.get("manual_age"):
raw_manual_age = manual_config["manual_age"]
if raw_age.lower() in CHARACTER_RANDOM_TOKENS:
raw_age = "manual"
age = _slot_manual_or_choice(raw_age, raw_manual_age)
raw_body = str(slot.get("body") or "random")
raw_manual_body = str(slot.get("manual_body") or "").strip()
if not raw_manual_body and manual_config.get("manual_body"):
raw_manual_body = manual_config["manual_body"]
if raw_body.lower() in CHARACTER_RANDOM_TOKENS:
raw_body = "manual"
body = _slot_manual_or_choice(raw_body, raw_manual_body)
figure = str(slot.get("figure") or "random").strip() figure = str(slot.get("figure") or "random").strip()
if figure not in character_figure_choices(): if figure not in character_figure_choices():
figure = "random" figure = "random"
def manual_fallback(field: str) -> str:
direct = _slot_value(slot.get(field))
return direct or manual_config.get(field, "")
normalized = { normalized = {
"profile_type": "character_slot", "profile_type": "character_slot",
"subject_type": subject_type, "subject_type": subject_type,
@@ -2689,14 +3139,26 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")), "ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
"figure": figure, "figure": figure,
"body": body, "body": body,
"body_phrase": _slot_value(slot.get("body_phrase")), "body_phrase": manual_fallback("body_phrase"),
"skin": _slot_value(slot.get("skin")), "skin": manual_fallback("skin"),
"hair": _slot_value(slot.get("hair")), "hair": manual_fallback("hair"),
"eyes": _slot_value(slot.get("eyes")), "manual": manual_config,
"hair_config": (
slot.get("hair_config")
if isinstance(slot.get("hair_config"), dict)
else _slot_value(slot.get("hair_config"))
),
"hair_color": _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES),
"hair_length": _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES),
"hair_style": _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES),
"eyes": manual_fallback("eyes"),
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")), "descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
"presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type), "presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type),
"softcore_outfit": _slot_value(slot.get("softcore_outfit")), "softcore_outfit": manual_fallback("softcore_outfit"),
"hardcore_clothing": _slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit")), "hardcore_clothing": (
_slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
or manual_config.get("hardcore_clothing", "")
),
"expression_enabled": not _is_false(slot.get("expression_enabled", True)), "expression_enabled": not _is_false(slot.get("expression_enabled", True)),
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")), "expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
"softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")), "softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
@@ -2765,6 +3227,14 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
parts.append(f"soft_outfit={slot['softcore_outfit']}") parts.append(f"soft_outfit={slot['softcore_outfit']}")
if slot.get("hardcore_clothing"): if slot.get("hardcore_clothing"):
parts.append(f"hard_clothing={slot['hardcore_clothing']}") parts.append(f"hard_clothing={slot['hardcore_clothing']}")
hair_config = _parse_hair_config(slot.get("hair_config"))
hair_config_summary = _hair_config_summary(hair_config)
if hair_config_summary != "hair unrestricted":
parts.append(f"hair={hair_config_summary}")
for key in ("hair_color", "hair_length", "hair_style"):
value = slot.get(key)
if value and value != "random":
parts.append(f"{key}={value}")
for key in ("body_phrase", "skin", "hair", "eyes"): for key in ("body_phrase", "skin", "hair", "eyes"):
value = slot.get(key) value = slot.get(key)
if value: if value:
@@ -2778,6 +3248,7 @@ def build_character_slot_json(
slot_seed: int = -1, slot_seed: int = -1,
age: str = "random", age: str = "random",
manual_age: str = "", manual_age: str = "",
manual: str | dict[str, Any] | None = "",
ethnicity: str = "random", ethnicity: str = "random",
figure: str = "random", figure: str = "random",
body: str = "random", body: str = "random",
@@ -2785,6 +3256,10 @@ def build_character_slot_json(
body_phrase: str = "", body_phrase: str = "",
skin: str = "", skin: str = "",
hair: str = "", hair: str = "",
hair_config: str | dict[str, Any] | None = "",
hair_color: str = "random",
hair_length: str = "random",
hair_style: str = "random",
eyes: str = "", eyes: str = "",
descriptor_detail: str = "auto", descriptor_detail: str = "auto",
expression_enabled: bool = True, expression_enabled: bool = True,
@@ -2805,6 +3280,7 @@ def build_character_slot_json(
"slot_seed": slot_seed, "slot_seed": slot_seed,
"age": age, "age": age,
"manual_age": manual_age, "manual_age": manual_age,
"manual": manual,
"ethnicity": ethnicity, "ethnicity": ethnicity,
"figure": figure, "figure": figure,
"body": body, "body": body,
@@ -2812,6 +3288,10 @@ def build_character_slot_json(
"body_phrase": body_phrase, "body_phrase": body_phrase,
"skin": skin, "skin": skin,
"hair": hair, "hair": hair,
"hair_config": hair_config,
"hair_color": hair_color,
"hair_length": hair_length,
"hair_style": hair_style,
"eyes": eyes, "eyes": eyes,
"descriptor_detail": descriptor_detail, "descriptor_detail": descriptor_detail,
"presence_mode": presence_mode, "presence_mode": presence_mode,
@@ -3034,10 +3514,17 @@ def _context_from_character_slot(
context["body_phrase"] = f"{slot_body} figure" context["body_phrase"] = f"{slot_body} figure"
if body_phrase: if body_phrase:
context["body_phrase"] = body_phrase context["body_phrase"] = body_phrase
for key in ("skin", "hair", "eyes"): for key in ("skin", "eyes"):
value = _slot_value(slot.get(key)) value = _slot_value(slot.get(key))
if value: if value:
context[key] = value context[key] = value
hair_value = _slot_value(slot.get("hair"))
if hair_value:
context["hair"] = hair_value
else:
hair_descriptor = _hair_descriptor_from_slot(context.get("hair"), slot, appearance_rng)
if hair_descriptor:
context["hair"] = hair_descriptor
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail")) context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type) context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type)
context["expression_enabled"] = _slot_expression_enabled(slot) context["expression_enabled"] = _slot_expression_enabled(slot)