Add chainable hair and manual character nodes
This commit is contained in:
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user