Add split workflow nodes and profile controls
This commit is contained in:
@@ -8,6 +8,13 @@ The node is registered as:
|
|||||||
- `prompt_builder / SxCP Prompt Builder`
|
- `prompt_builder / SxCP Prompt Builder`
|
||||||
- `prompt_builder / SxCP Seed Control`
|
- `prompt_builder / SxCP Seed Control`
|
||||||
- `prompt_builder / SxCP Camera Control`
|
- `prompt_builder / SxCP Camera Control`
|
||||||
|
- `prompt_builder / SxCP Category Preset`
|
||||||
|
- `prompt_builder / SxCP Cast Control`
|
||||||
|
- `prompt_builder / SxCP Generation Profile`
|
||||||
|
- `prompt_builder / SxCP Advanced Filters`
|
||||||
|
- `prompt_builder / SxCP Prompt Builder From Configs`
|
||||||
|
- `prompt_builder / SxCP Character Profile Save`
|
||||||
|
- `prompt_builder / SxCP Character Profile Load`
|
||||||
- `prompt_builder / SxCP Caption Naturalizer`
|
- `prompt_builder / SxCP Caption Naturalizer`
|
||||||
- `prompt_builder / SxCP Krea2 Formatter`
|
- `prompt_builder / SxCP Krea2 Formatter`
|
||||||
- `prompt_builder / SxCP Insta/OF Options`
|
- `prompt_builder / SxCP Insta/OF Options`
|
||||||
@@ -22,6 +29,71 @@ It outputs:
|
|||||||
- `category`
|
- `category`
|
||||||
- `subcategory`
|
- `subcategory`
|
||||||
|
|
||||||
|
## Split Workflow Nodes
|
||||||
|
|
||||||
|
The original `SxCP Prompt Builder` remains available as the full all-in-one
|
||||||
|
node. For cleaner workflows, use the split nodes:
|
||||||
|
|
||||||
|
- `SxCP Category Preset` outputs `category_config` for broad intent such as
|
||||||
|
women casual, men casual, couple casual, provocative erotic, or hardcore pose.
|
||||||
|
- `SxCP Cast Control` outputs `cast_config` plus raw `women_count` and
|
||||||
|
`men_count`, so couple/two-women/two-men/group setups can be reused.
|
||||||
|
- `SxCP Generation Profile` outputs `generation_profile` for common behavior
|
||||||
|
presets such as casual-clean, evocative-softcore, hardcore-intense,
|
||||||
|
Krea2-friendly, or Flux-original.
|
||||||
|
- `SxCP Advanced Filters` outputs `filter_config` for ethnicity, figure, and
|
||||||
|
exclusion filters.
|
||||||
|
- `SxCP Prompt Builder From Configs` consumes those config outputs and produces
|
||||||
|
the same prompt, negative, caption, metadata, category, and subcategory
|
||||||
|
outputs as the full builder.
|
||||||
|
|
||||||
|
The practical compact workflow is:
|
||||||
|
|
||||||
|
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
||||||
|
`Advanced Filters`, `Seed Control`, `Camera Control`, and `Character Profile`
|
||||||
|
into `Prompt Builder From Configs`.
|
||||||
|
|
||||||
|
An importable example is included at
|
||||||
|
`examples/split_profile_reuse_workflow.json`. It shows a two-pass setup:
|
||||||
|
|
||||||
|
1. Generate a casual woman prompt and extract a character profile from
|
||||||
|
`metadata_json`.
|
||||||
|
2. Reuse that profile in a second prompt builder with a different seed, keeping
|
||||||
|
the same character while changing the outfit/scene.
|
||||||
|
3. Send the reused-profile metadata to both `Caption Naturalizer` and
|
||||||
|
`Krea2 Formatter`.
|
||||||
|
|
||||||
|
## Character Profiles
|
||||||
|
|
||||||
|
`SxCP Character Profile Save` extracts a reusable woman/man profile from
|
||||||
|
`metadata_json` or from manual fields. The profile stores age, body/body phrase,
|
||||||
|
skin, hair, eyes, figure, and subject type. It only writes a file when
|
||||||
|
the `Save Profile Now` button is clicked; otherwise it just outputs profile JSON
|
||||||
|
for direct wiring. Saved files are written under `profiles/<profile_name>.json`;
|
||||||
|
saved profile files are ignored by git. The button is backed by the hidden
|
||||||
|
`save_now` trigger and queues the workflow once.
|
||||||
|
|
||||||
|
`SxCP Character Profile Load` has an `enabled` switch. When disabled, it returns
|
||||||
|
an empty profile so connected prompt builders ignore it. When enabled, it loads
|
||||||
|
a saved profile by selector or passes through a connected fallback profile JSON.
|
||||||
|
It also has explicit file-operation triggers:
|
||||||
|
|
||||||
|
- `Delete Selected Profile`: deletes the selected saved profile.
|
||||||
|
- `Rename Selected Profile` + `rename_to`: renames the selected saved profile.
|
||||||
|
|
||||||
|
Delete and rename are conservative: if both triggers are enabled together,
|
||||||
|
nothing happens; rename does not overwrite an existing target profile. The
|
||||||
|
buttons are backed by hidden `delete_now` and `rename_now` triggers and queue
|
||||||
|
the workflow once.
|
||||||
|
|
||||||
|
Connect the loader's `character_profile` output to `SxCP Prompt Builder`,
|
||||||
|
`SxCP Prompt Builder From Configs`, or `SxCP Insta/OF Prompt Pair`.
|
||||||
|
|
||||||
|
Profile reuse currently applies to structured JSON-category single woman/man
|
||||||
|
rows and to the primary creator in Insta/OF pair mode. The outfit, scene, pose,
|
||||||
|
expression, and composition can still change while the saved character
|
||||||
|
appearance remains stable.
|
||||||
|
|
||||||
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
|
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
|
||||||
builder's optional `seed_config` input.
|
builder's optional `seed_config` input.
|
||||||
|
|
||||||
@@ -190,6 +262,8 @@ or reload ComfyUI after changing JSON so dropdown choices are rebuilt.
|
|||||||
Included JSON categories:
|
Included JSON categories:
|
||||||
|
|
||||||
- `Casual clothes`
|
- `Casual clothes`
|
||||||
|
- `Men casual clothes`
|
||||||
|
- `Couple casual clothes`
|
||||||
- `Provocative erotic clothes`
|
- `Provocative erotic clothes`
|
||||||
- `Hardcore sexual poses`
|
- `Hardcore sexual poses`
|
||||||
|
|
||||||
|
|||||||
+334
@@ -5,9 +5,15 @@ import json
|
|||||||
try:
|
try:
|
||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
|
build_cast_config_json,
|
||||||
|
build_category_config_json,
|
||||||
|
build_character_profile_json,
|
||||||
|
build_filter_config_json,
|
||||||
|
build_generation_profile_json,
|
||||||
build_insta_of_options_json,
|
build_insta_of_options_json,
|
||||||
build_insta_of_pair,
|
build_insta_of_pair,
|
||||||
build_prompt,
|
build_prompt,
|
||||||
|
build_prompt_from_configs,
|
||||||
build_seed_config_json,
|
build_seed_config_json,
|
||||||
camera_angle_choices,
|
camera_angle_choices,
|
||||||
camera_distance_choices,
|
camera_distance_choices,
|
||||||
@@ -17,7 +23,12 @@ try:
|
|||||||
camera_phone_choices,
|
camera_phone_choices,
|
||||||
camera_priority_choices,
|
camera_priority_choices,
|
||||||
camera_shot_choices,
|
camera_shot_choices,
|
||||||
|
cast_preset_choices,
|
||||||
|
category_preset_choices,
|
||||||
category_choices,
|
category_choices,
|
||||||
|
character_profile_choices,
|
||||||
|
generation_profile_choices,
|
||||||
|
load_character_profile_json,
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
from .caption_naturalizer import naturalize_caption
|
from .caption_naturalizer import naturalize_caption
|
||||||
@@ -25,9 +36,15 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
|
build_cast_config_json,
|
||||||
|
build_category_config_json,
|
||||||
|
build_character_profile_json,
|
||||||
|
build_filter_config_json,
|
||||||
|
build_generation_profile_json,
|
||||||
build_insta_of_options_json,
|
build_insta_of_options_json,
|
||||||
build_insta_of_pair,
|
build_insta_of_pair,
|
||||||
build_prompt,
|
build_prompt,
|
||||||
|
build_prompt_from_configs,
|
||||||
build_seed_config_json,
|
build_seed_config_json,
|
||||||
camera_angle_choices,
|
camera_angle_choices,
|
||||||
camera_distance_choices,
|
camera_distance_choices,
|
||||||
@@ -37,7 +54,12 @@ except ImportError:
|
|||||||
camera_phone_choices,
|
camera_phone_choices,
|
||||||
camera_priority_choices,
|
camera_priority_choices,
|
||||||
camera_shot_choices,
|
camera_shot_choices,
|
||||||
|
cast_preset_choices,
|
||||||
|
category_preset_choices,
|
||||||
category_choices,
|
category_choices,
|
||||||
|
character_profile_choices,
|
||||||
|
generation_profile_choices,
|
||||||
|
load_character_profile_json,
|
||||||
subcategory_choices,
|
subcategory_choices,
|
||||||
)
|
)
|
||||||
from caption_naturalizer import naturalize_caption
|
from caption_naturalizer import naturalize_caption
|
||||||
@@ -72,6 +94,7 @@ class SxCPPromptBuilder:
|
|||||||
"optional": {
|
"optional": {
|
||||||
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||||
},
|
},
|
||||||
@@ -105,6 +128,7 @@ class SxCPPromptBuilder:
|
|||||||
prepend_trigger_to_prompt,
|
prepend_trigger_to_prompt,
|
||||||
seed_config="",
|
seed_config="",
|
||||||
camera_config="",
|
camera_config="",
|
||||||
|
character_profile="",
|
||||||
extra_positive="",
|
extra_positive="",
|
||||||
extra_negative="",
|
extra_negative="",
|
||||||
):
|
):
|
||||||
@@ -132,6 +156,7 @@ class SxCPPromptBuilder:
|
|||||||
extra_negative=extra_negative or "",
|
extra_negative=extra_negative or "",
|
||||||
seed_config=seed_config or "",
|
seed_config=seed_config or "",
|
||||||
camera_config=camera_config or "",
|
camera_config=camera_config or "",
|
||||||
|
character_profile=character_profile or "",
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
row["prompt"],
|
row["prompt"],
|
||||||
@@ -239,6 +264,294 @@ class SxCPCameraControl:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPCategoryPreset:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"preset": (category_preset_choices(), {"default": "auto_weighted"}),
|
||||||
|
"subcategory": (subcategory_choices(), {"default": "random"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = ("category_config", "category", "subcategory")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(self, preset, subcategory):
|
||||||
|
config = build_category_config_json(preset=preset, subcategory=subcategory)
|
||||||
|
parsed = json.loads(config)
|
||||||
|
return config, parsed["category"], parsed["subcategory"]
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPCastControl:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"cast_mode": (cast_preset_choices(), {"default": "mixed_couple"}),
|
||||||
|
"women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
|
||||||
|
"men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "INT", "INT", "STRING")
|
||||||
|
RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(self, cast_mode, women_count, men_count):
|
||||||
|
config = build_cast_config_json(cast_mode=cast_mode, women_count=women_count, men_count=men_count)
|
||||||
|
parsed = json.loads(config)
|
||||||
|
summary = f"{parsed['women_count']} women, {parsed['men_count']} men"
|
||||||
|
return config, parsed["women_count"], parsed["men_count"], summary
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPGenerationProfile:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"profile": (generation_profile_choices(), {"default": "balanced"}),
|
||||||
|
"clothing_override": (["profile_default", "full", "minimal"], {"default": "profile_default"}),
|
||||||
|
"poses_override": (["profile_default", "standard", "evocative"], {"default": "profile_default"}),
|
||||||
|
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||||
|
"backside_bias": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||||
|
"minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||||
|
"standard_pose_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
|
||||||
|
"trigger_policy": (["profile_default", "prepend_trigger", "do_not_prepend"], {"default": "profile_default"}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING")
|
||||||
|
RETURN_NAMES = ("generation_profile", "summary")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
profile,
|
||||||
|
clothing_override,
|
||||||
|
poses_override,
|
||||||
|
expression_intensity,
|
||||||
|
backside_bias,
|
||||||
|
minimal_clothing_ratio,
|
||||||
|
standard_pose_ratio,
|
||||||
|
trigger_policy,
|
||||||
|
):
|
||||||
|
config = build_generation_profile_json(
|
||||||
|
profile=profile,
|
||||||
|
clothing_override=clothing_override,
|
||||||
|
poses_override=poses_override,
|
||||||
|
expression_intensity=expression_intensity,
|
||||||
|
backside_bias=backside_bias,
|
||||||
|
minimal_clothing_ratio=minimal_clothing_ratio,
|
||||||
|
standard_pose_ratio=standard_pose_ratio,
|
||||||
|
trigger_policy=trigger_policy,
|
||||||
|
)
|
||||||
|
parsed = json.loads(config)
|
||||||
|
summary = f"{parsed['profile']}: {parsed['clothing']}, {parsed['poses']}, expression {parsed['expression_intensity']}"
|
||||||
|
return config, summary
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPAdvancedFilters:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"ethnicity": (["any", "asian", "white_asian"], {"default": "any"}),
|
||||||
|
"figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}),
|
||||||
|
"no_plus_women": ("BOOLEAN", {"default": False}),
|
||||||
|
"no_black": ("BOOLEAN", {"default": False}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
RETURN_NAMES = ("filter_config",)
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(self, ethnicity, figure, no_plus_women, no_black):
|
||||||
|
return (
|
||||||
|
build_filter_config_json(
|
||||||
|
ethnicity=ethnicity,
|
||||||
|
figure=figure,
|
||||||
|
no_plus_women=no_plus_women,
|
||||||
|
no_black=no_black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPPromptBuilderFromConfigs:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}),
|
||||||
|
"start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}),
|
||||||
|
"seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"category_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"cast_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"generation_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"filter_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = ("prompt", "negative_prompt", "caption", "metadata_json", "category", "subcategory")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
row_number,
|
||||||
|
start_index,
|
||||||
|
seed,
|
||||||
|
category_config="",
|
||||||
|
cast_config="",
|
||||||
|
generation_profile="",
|
||||||
|
filter_config="",
|
||||||
|
seed_config="",
|
||||||
|
camera_config="",
|
||||||
|
character_profile="",
|
||||||
|
extra_positive="",
|
||||||
|
extra_negative="",
|
||||||
|
):
|
||||||
|
row = build_prompt_from_configs(
|
||||||
|
row_number=row_number,
|
||||||
|
start_index=start_index,
|
||||||
|
seed=seed,
|
||||||
|
category_config=category_config or "",
|
||||||
|
cast_config=cast_config or "",
|
||||||
|
generation_profile=generation_profile or "",
|
||||||
|
filter_config=filter_config or "",
|
||||||
|
seed_config=seed_config or "",
|
||||||
|
camera_config=camera_config or "",
|
||||||
|
character_profile=character_profile or "",
|
||||||
|
extra_positive=extra_positive or "",
|
||||||
|
extra_negative=extra_negative or "",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
row["prompt"],
|
||||||
|
row["negative_prompt"],
|
||||||
|
row["caption"],
|
||||||
|
json.dumps(row, ensure_ascii=True, sort_keys=True),
|
||||||
|
row.get("main_category", ""),
|
||||||
|
row.get("subcategory", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPCharacterProfileSave:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"profile_name": ("STRING", {"default": "saved_character"}),
|
||||||
|
"source": (["metadata_json", "manual"], {"default": "metadata_json"}),
|
||||||
|
"subject_type": (["woman", "man"], {"default": "woman"}),
|
||||||
|
"age": ("STRING", {"default": ""}),
|
||||||
|
"body": ("STRING", {"default": ""}),
|
||||||
|
"body_phrase": ("STRING", {"default": ""}),
|
||||||
|
"skin": ("STRING", {"default": ""}),
|
||||||
|
"hair": ("STRING", {"default": ""}),
|
||||||
|
"eyes": ("STRING", {"default": ""}),
|
||||||
|
"figure": ("STRING", {"default": ""}),
|
||||||
|
"save_now": ("BOOLEAN", {"default": False}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"metadata_json": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
profile_name,
|
||||||
|
source,
|
||||||
|
subject_type,
|
||||||
|
age,
|
||||||
|
body,
|
||||||
|
body_phrase,
|
||||||
|
skin,
|
||||||
|
hair,
|
||||||
|
eyes,
|
||||||
|
figure,
|
||||||
|
save_now,
|
||||||
|
metadata_json="",
|
||||||
|
):
|
||||||
|
profile = build_character_profile_json(
|
||||||
|
profile_name=profile_name,
|
||||||
|
source=source,
|
||||||
|
metadata_json=metadata_json or "",
|
||||||
|
subject_type=subject_type,
|
||||||
|
age=age,
|
||||||
|
body=body,
|
||||||
|
body_phrase=body_phrase,
|
||||||
|
skin=skin,
|
||||||
|
hair=hair,
|
||||||
|
eyes=eyes,
|
||||||
|
figure=figure,
|
||||||
|
save_now=save_now,
|
||||||
|
)
|
||||||
|
return profile["profile_json"], profile["descriptor"], profile["profile_name"], profile["saved_path"], profile["status"]
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPCharacterProfileLoad:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"enabled": ("BOOLEAN", {"default": True}),
|
||||||
|
"profile_name": (character_profile_choices(), {"default": "manual"}),
|
||||||
|
"rename_to": ("STRING", {"default": ""}),
|
||||||
|
"delete_now": ("BOOLEAN", {"default": False}),
|
||||||
|
"rename_now": ("BOOLEAN", {"default": False}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"manual_profile_name": ("STRING", {"default": ""}),
|
||||||
|
"fallback_profile_json": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
enabled,
|
||||||
|
profile_name,
|
||||||
|
rename_to,
|
||||||
|
delete_now,
|
||||||
|
rename_now,
|
||||||
|
manual_profile_name="",
|
||||||
|
fallback_profile_json="",
|
||||||
|
):
|
||||||
|
chosen_name = manual_profile_name.strip() if profile_name == "manual" and manual_profile_name.strip() else profile_name
|
||||||
|
profile = load_character_profile_json(
|
||||||
|
profile_name=chosen_name,
|
||||||
|
fallback_profile_json=fallback_profile_json or "",
|
||||||
|
enabled=enabled,
|
||||||
|
delete_now=delete_now,
|
||||||
|
rename_now=rename_now,
|
||||||
|
rename_to=rename_to,
|
||||||
|
)
|
||||||
|
return profile["profile_json"], profile["descriptor"], profile["profile_name"], profile["saved_path"], profile["status"]
|
||||||
|
|
||||||
|
|
||||||
class SxCPCaptionNaturalizer:
|
class SxCPCaptionNaturalizer:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -428,6 +741,7 @@ class SxCPInstaOFPromptPair:
|
|||||||
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
"options_json": ("STRING", {"default": "", "multiline": True}),
|
"options_json": ("STRING", {"default": "", "multiline": True}),
|
||||||
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||||
},
|
},
|
||||||
@@ -461,6 +775,7 @@ class SxCPInstaOFPromptPair:
|
|||||||
seed_config="",
|
seed_config="",
|
||||||
options_json="",
|
options_json="",
|
||||||
camera_config="",
|
camera_config="",
|
||||||
|
character_profile="",
|
||||||
extra_positive="",
|
extra_positive="",
|
||||||
extra_negative="",
|
extra_negative="",
|
||||||
):
|
):
|
||||||
@@ -477,6 +792,7 @@ class SxCPInstaOFPromptPair:
|
|||||||
seed_config=seed_config or "",
|
seed_config=seed_config or "",
|
||||||
options_json=options_json or "",
|
options_json=options_json or "",
|
||||||
camera_config=camera_config or "",
|
camera_config=camera_config or "",
|
||||||
|
character_profile=character_profile or "",
|
||||||
extra_positive=extra_positive or "",
|
extra_positive=extra_positive or "",
|
||||||
extra_negative=extra_negative or "",
|
extra_negative=extra_negative or "",
|
||||||
)
|
)
|
||||||
@@ -496,6 +812,13 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SxCPPromptBuilder": SxCPPromptBuilder,
|
"SxCPPromptBuilder": SxCPPromptBuilder,
|
||||||
"SxCPSeedControl": SxCPSeedControl,
|
"SxCPSeedControl": SxCPSeedControl,
|
||||||
"SxCPCameraControl": SxCPCameraControl,
|
"SxCPCameraControl": SxCPCameraControl,
|
||||||
|
"SxCPCategoryPreset": SxCPCategoryPreset,
|
||||||
|
"SxCPCastControl": SxCPCastControl,
|
||||||
|
"SxCPGenerationProfile": SxCPGenerationProfile,
|
||||||
|
"SxCPAdvancedFilters": SxCPAdvancedFilters,
|
||||||
|
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
|
||||||
|
"SxCPCharacterProfileSave": SxCPCharacterProfileSave,
|
||||||
|
"SxCPCharacterProfileLoad": SxCPCharacterProfileLoad,
|
||||||
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
||||||
"SxCPKrea2Formatter": SxCPKrea2Formatter,
|
"SxCPKrea2Formatter": SxCPKrea2Formatter,
|
||||||
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
||||||
@@ -506,8 +829,19 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SxCPPromptBuilder": "SxCP Prompt Builder",
|
"SxCPPromptBuilder": "SxCP Prompt Builder",
|
||||||
"SxCPSeedControl": "SxCP Seed Control",
|
"SxCPSeedControl": "SxCP Seed Control",
|
||||||
"SxCPCameraControl": "SxCP Camera Control",
|
"SxCPCameraControl": "SxCP Camera Control",
|
||||||
|
"SxCPCategoryPreset": "SxCP Category Preset",
|
||||||
|
"SxCPCastControl": "SxCP Cast Control",
|
||||||
|
"SxCPGenerationProfile": "SxCP Generation Profile",
|
||||||
|
"SxCPAdvancedFilters": "SxCP Advanced Filters",
|
||||||
|
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
|
||||||
|
"SxCPCharacterProfileSave": "SxCP Character Profile Save",
|
||||||
|
"SxCPCharacterProfileLoad": "SxCP Character Profile Load",
|
||||||
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
||||||
"SxCPKrea2Formatter": "SxCP Krea2 Formatter",
|
"SxCPKrea2Formatter": "SxCP Krea2 Formatter",
|
||||||
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
||||||
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WEB_DIRECTORY = "./web"
|
||||||
|
|
||||||
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 12,
|
||||||
|
"last_link_id": 17,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "SxCPCategoryPreset",
|
||||||
|
"pos": [-980, -300],
|
||||||
|
"size": [320, 82],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "category_config", "type": "STRING", "links": [1, 2], "slot_index": 0},
|
||||||
|
{"name": "category", "type": "STRING", "links": null, "slot_index": 1},
|
||||||
|
{"name": "subcategory", "type": "STRING", "links": null, "slot_index": 2}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPCategoryPreset"},
|
||||||
|
"widgets_values": ["women_casual", "Casual clothes / Smart casual"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "SxCPCastControl",
|
||||||
|
"pos": [-980, -160],
|
||||||
|
"size": [320, 106],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "cast_config", "type": "STRING", "links": [3, 4], "slot_index": 0},
|
||||||
|
{"name": "women_count", "type": "INT", "links": null, "slot_index": 1},
|
||||||
|
{"name": "men_count", "type": "INT", "links": null, "slot_index": 2},
|
||||||
|
{"name": "cast_summary", "type": "STRING", "links": null, "slot_index": 3}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPCastControl"},
|
||||||
|
"widgets_values": ["solo_woman", 1, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "SxCPGenerationProfile",
|
||||||
|
"pos": [-980, 20],
|
||||||
|
"size": [320, 226],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "generation_profile", "type": "STRING", "links": [5, 6], "slot_index": 0},
|
||||||
|
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPGenerationProfile"},
|
||||||
|
"widgets_values": ["casual_clean", "profile_default", "profile_default", -1.0, -1.0, -1.0, -1.0, "profile_default"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "SxCPAdvancedFilters",
|
||||||
|
"pos": [-980, 310],
|
||||||
|
"size": [320, 130],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "filter_config", "type": "STRING", "links": [7, 8], "slot_index": 0}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPAdvancedFilters"},
|
||||||
|
"widgets_values": ["any", "balanced", false, false]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "SxCPSeedControl",
|
||||||
|
"pos": [-600, -300],
|
||||||
|
"size": [320, 250],
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "seed_config", "type": "STRING", "links": [9, 10], "slot_index": 0}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPSeedControl"},
|
||||||
|
"widgets_values": [-1, -1, -1, -1, -1, -1, -1, -1, -1]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "SxCPCameraControl",
|
||||||
|
"pos": [-600, 10],
|
||||||
|
"size": [320, 226],
|
||||||
|
"flags": {},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "camera_config", "type": "STRING", "links": [11, 12], "slot_index": 0}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPCameraControl"},
|
||||||
|
"widgets_values": ["mirror_selfie", "full_body", "mirror_reflection", "smartphone_wide", "arm_length", "vertical_story", "phone_visible", "locked"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "SxCPPromptBuilderFromConfigs",
|
||||||
|
"pos": [-180, -300],
|
||||||
|
"size": [360, 246],
|
||||||
|
"flags": {},
|
||||||
|
"order": 6,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "category_config", "type": "STRING", "link": 1},
|
||||||
|
{"name": "cast_config", "type": "STRING", "link": 3},
|
||||||
|
{"name": "generation_profile", "type": "STRING", "link": 5},
|
||||||
|
{"name": "filter_config", "type": "STRING", "link": 7},
|
||||||
|
{"name": "seed_config", "type": "STRING", "link": 9},
|
||||||
|
{"name": "camera_config", "type": "STRING", "link": 11}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "prompt", "type": "STRING", "links": null, "slot_index": 0},
|
||||||
|
{"name": "negative_prompt", "type": "STRING", "links": null, "slot_index": 1},
|
||||||
|
{"name": "caption", "type": "STRING", "links": null, "slot_index": 2},
|
||||||
|
{"name": "metadata_json", "type": "STRING", "links": [13], "slot_index": 3},
|
||||||
|
{"name": "category", "type": "STRING", "links": null, "slot_index": 4},
|
||||||
|
{"name": "subcategory", "type": "STRING", "links": null, "slot_index": 5}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPPromptBuilderFromConfigs"},
|
||||||
|
"widgets_values": [1, 41, 20260624]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "SxCPCharacterProfileSave",
|
||||||
|
"pos": [260, -300],
|
||||||
|
"size": [360, 294],
|
||||||
|
"flags": {},
|
||||||
|
"order": 7,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "metadata_json", "type": "STRING", "link": 13}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "character_profile", "type": "STRING", "links": [14], "slot_index": 0},
|
||||||
|
{"name": "descriptor", "type": "STRING", "links": null, "slot_index": 1},
|
||||||
|
{"name": "profile_name", "type": "STRING", "links": null, "slot_index": 2},
|
||||||
|
{"name": "saved_path", "type": "STRING", "links": null, "slot_index": 3},
|
||||||
|
{"name": "status", "type": "STRING", "links": null, "slot_index": 4}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPCharacterProfileSave"},
|
||||||
|
"widgets_values": ["example_creator", "metadata_json", "woman", "", "", "", "", "", "", "", false]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "SxCPCharacterProfileLoad",
|
||||||
|
"pos": [700, -300],
|
||||||
|
"size": [340, 174],
|
||||||
|
"flags": {},
|
||||||
|
"order": 8,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "fallback_profile_json", "type": "STRING", "link": 14}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "character_profile", "type": "STRING", "links": [15], "slot_index": 0},
|
||||||
|
{"name": "descriptor", "type": "STRING", "links": null, "slot_index": 1},
|
||||||
|
{"name": "profile_name", "type": "STRING", "links": null, "slot_index": 2},
|
||||||
|
{"name": "saved_path", "type": "STRING", "links": null, "slot_index": 3},
|
||||||
|
{"name": "status", "type": "STRING", "links": null, "slot_index": 4}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPCharacterProfileLoad"},
|
||||||
|
"widgets_values": [true, "manual", "", false, false, ""]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "SxCPPromptBuilderFromConfigs",
|
||||||
|
"pos": [1120, -300],
|
||||||
|
"size": [370, 270],
|
||||||
|
"flags": {},
|
||||||
|
"order": 9,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "category_config", "type": "STRING", "link": 2},
|
||||||
|
{"name": "cast_config", "type": "STRING", "link": 4},
|
||||||
|
{"name": "generation_profile", "type": "STRING", "link": 6},
|
||||||
|
{"name": "filter_config", "type": "STRING", "link": 8},
|
||||||
|
{"name": "seed_config", "type": "STRING", "link": 10},
|
||||||
|
{"name": "camera_config", "type": "STRING", "link": 12},
|
||||||
|
{"name": "character_profile", "type": "STRING", "link": 15}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "prompt", "type": "STRING", "links": null, "slot_index": 0},
|
||||||
|
{"name": "negative_prompt", "type": "STRING", "links": null, "slot_index": 1},
|
||||||
|
{"name": "caption", "type": "STRING", "links": null, "slot_index": 2},
|
||||||
|
{"name": "metadata_json", "type": "STRING", "links": [16, 17], "slot_index": 3},
|
||||||
|
{"name": "category", "type": "STRING", "links": null, "slot_index": 4},
|
||||||
|
{"name": "subcategory", "type": "STRING", "links": null, "slot_index": 5}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPPromptBuilderFromConfigs"},
|
||||||
|
"widgets_values": [1, 41, 20260777]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "SxCPCaptionNaturalizer",
|
||||||
|
"pos": [1560, -420],
|
||||||
|
"size": [360, 198],
|
||||||
|
"flags": {},
|
||||||
|
"order": 10,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "metadata_json", "type": "STRING", "link": 17}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "natural_caption", "type": "STRING", "links": null, "slot_index": 0},
|
||||||
|
{"name": "method", "type": "STRING", "links": null, "slot_index": 1}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPCaptionNaturalizer"},
|
||||||
|
"widgets_values": ["", "metadata_json", "balanced", "drop_style_tail", "sxcppnl7", false]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "SxCPKrea2Formatter",
|
||||||
|
"pos": [1560, -150],
|
||||||
|
"size": [380, 246],
|
||||||
|
"flags": {},
|
||||||
|
"order": 11,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{"name": "metadata_json", "type": "STRING", "link": 16}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "krea_prompt", "type": "STRING", "links": null, "slot_index": 0},
|
||||||
|
{"name": "negative_prompt", "type": "STRING", "links": null, "slot_index": 1},
|
||||||
|
{"name": "krea_softcore_prompt", "type": "STRING", "links": null, "slot_index": 2},
|
||||||
|
{"name": "krea_hardcore_prompt", "type": "STRING", "links": null, "slot_index": 3},
|
||||||
|
{"name": "softcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 4},
|
||||||
|
{"name": "hardcore_negative_prompt", "type": "STRING", "links": null, "slot_index": 5},
|
||||||
|
{"name": "method", "type": "STRING", "links": null, "slot_index": 6}
|
||||||
|
],
|
||||||
|
"properties": {"Node name for S&R": "SxCPKrea2Formatter"},
|
||||||
|
"widgets_values": ["", "metadata_json", "auto", "balanced", "minimal", false, "", "", ""]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[1, 1, 0, 7, 0, "STRING"],
|
||||||
|
[2, 1, 0, 10, 0, "STRING"],
|
||||||
|
[3, 2, 0, 7, 1, "STRING"],
|
||||||
|
[4, 2, 0, 10, 1, "STRING"],
|
||||||
|
[5, 3, 0, 7, 2, "STRING"],
|
||||||
|
[6, 3, 0, 10, 2, "STRING"],
|
||||||
|
[7, 4, 0, 7, 3, "STRING"],
|
||||||
|
[8, 4, 0, 10, 3, "STRING"],
|
||||||
|
[9, 5, 0, 7, 4, "STRING"],
|
||||||
|
[10, 5, 0, 10, 4, "STRING"],
|
||||||
|
[11, 6, 0, 7, 5, "STRING"],
|
||||||
|
[12, 6, 0, 10, 5, "STRING"],
|
||||||
|
[13, 7, 3, 8, 0, "STRING"],
|
||||||
|
[14, 8, 0, 9, 0, "STRING"],
|
||||||
|
[15, 9, 0, 10, 6, "STRING"],
|
||||||
|
[16, 10, 3, 11, 0, "STRING"],
|
||||||
|
[17, 10, 3, 12, 0, "STRING"]
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Shared controls",
|
||||||
|
"bounding": [-1010, -350, 760, 840],
|
||||||
|
"color": "#3f789e",
|
||||||
|
"font_size": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pass 1: generate and extract profile",
|
||||||
|
"bounding": [-220, -350, 890, 390],
|
||||||
|
"color": "#8f6d31",
|
||||||
|
"font_size": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Pass 2: reuse profile with a different seed",
|
||||||
|
"bounding": [670, -350, 870, 390],
|
||||||
|
"color": "#4d7f45",
|
||||||
|
"font_size": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Optional reformats",
|
||||||
|
"bounding": [1530, -470, 440, 600],
|
||||||
|
"color": "#5f4d8f",
|
||||||
|
"font_size": 24
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 0.82,
|
||||||
|
"offset": [1090, 430]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -14,6 +15,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
|
|
||||||
ROOT_DIR = Path(__file__).resolve().parent
|
ROOT_DIR = Path(__file__).resolve().parent
|
||||||
CATEGORY_DIR = ROOT_DIR / "categories"
|
CATEGORY_DIR = ROOT_DIR / "categories"
|
||||||
|
PROFILE_DIR = ROOT_DIR / "profiles"
|
||||||
|
|
||||||
BUILTIN_CATEGORIES = [
|
BUILTIN_CATEGORIES = [
|
||||||
"auto_weighted",
|
"auto_weighted",
|
||||||
@@ -688,6 +690,267 @@ def subcategory_choices() -> list[str]:
|
|||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
|
||||||
|
CATEGORY_PRESETS = {
|
||||||
|
"auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY),
|
||||||
|
"women_casual": ("Casual clothes", RANDOM_SUBCATEGORY),
|
||||||
|
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
|
||||||
|
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
|
||||||
|
"provocative_erotic": ("Provocative erotic clothes", RANDOM_SUBCATEGORY),
|
||||||
|
"hardcore_pose": ("Hardcore sexual poses", RANDOM_SUBCATEGORY),
|
||||||
|
"custom_random": ("custom_random", RANDOM_SUBCATEGORY),
|
||||||
|
}
|
||||||
|
|
||||||
|
CAST_PRESETS = {
|
||||||
|
"solo_woman": (1, 0),
|
||||||
|
"solo_man": (0, 1),
|
||||||
|
"mixed_couple": (1, 1),
|
||||||
|
"two_women": (2, 0),
|
||||||
|
"two_men": (0, 2),
|
||||||
|
"threesome_2w1m": (2, 1),
|
||||||
|
"small_group_3w2m": (3, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
GENERATION_PROFILE_PRESETS = {
|
||||||
|
"balanced": {
|
||||||
|
"clothing": "full",
|
||||||
|
"poses": "standard",
|
||||||
|
"expression_intensity": 0.5,
|
||||||
|
"backside_bias": 0.0,
|
||||||
|
"minimal_clothing_ratio": -1.0,
|
||||||
|
"standard_pose_ratio": -1.0,
|
||||||
|
"trigger": "sxcpinup_coloredpencil",
|
||||||
|
"prepend_trigger_to_prompt": True,
|
||||||
|
},
|
||||||
|
"casual_clean": {
|
||||||
|
"clothing": "full",
|
||||||
|
"poses": "standard",
|
||||||
|
"expression_intensity": 0.35,
|
||||||
|
"backside_bias": 0.0,
|
||||||
|
"minimal_clothing_ratio": -1.0,
|
||||||
|
"standard_pose_ratio": -1.0,
|
||||||
|
"trigger": "sxcpinup_coloredpencil",
|
||||||
|
"prepend_trigger_to_prompt": True,
|
||||||
|
},
|
||||||
|
"evocative_softcore": {
|
||||||
|
"clothing": "minimal",
|
||||||
|
"poses": "evocative",
|
||||||
|
"expression_intensity": 0.65,
|
||||||
|
"backside_bias": 0.2,
|
||||||
|
"minimal_clothing_ratio": -1.0,
|
||||||
|
"standard_pose_ratio": -1.0,
|
||||||
|
"trigger": "sxcpinup_coloredpencil",
|
||||||
|
"prepend_trigger_to_prompt": True,
|
||||||
|
},
|
||||||
|
"hardcore_intense": {
|
||||||
|
"clothing": "minimal",
|
||||||
|
"poses": "evocative",
|
||||||
|
"expression_intensity": 0.9,
|
||||||
|
"backside_bias": 0.0,
|
||||||
|
"minimal_clothing_ratio": -1.0,
|
||||||
|
"standard_pose_ratio": -1.0,
|
||||||
|
"trigger": "sxcpinup_coloredpencil",
|
||||||
|
"prepend_trigger_to_prompt": True,
|
||||||
|
},
|
||||||
|
"krea2_friendly": {
|
||||||
|
"clothing": "full",
|
||||||
|
"poses": "standard",
|
||||||
|
"expression_intensity": 0.55,
|
||||||
|
"backside_bias": 0.0,
|
||||||
|
"minimal_clothing_ratio": -1.0,
|
||||||
|
"standard_pose_ratio": -1.0,
|
||||||
|
"trigger": "sxcpinup_coloredpencil",
|
||||||
|
"prepend_trigger_to_prompt": False,
|
||||||
|
},
|
||||||
|
"flux_original": {
|
||||||
|
"clothing": "full",
|
||||||
|
"poses": "standard",
|
||||||
|
"expression_intensity": 0.5,
|
||||||
|
"backside_bias": 0.0,
|
||||||
|
"minimal_clothing_ratio": -1.0,
|
||||||
|
"standard_pose_ratio": -1.0,
|
||||||
|
"trigger": "sxcpinup_coloredpencil",
|
||||||
|
"prepend_trigger_to_prompt": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def category_preset_choices() -> list[str]:
|
||||||
|
return list(CATEGORY_PRESETS)
|
||||||
|
|
||||||
|
|
||||||
|
def cast_preset_choices() -> list[str]:
|
||||||
|
return list(CAST_PRESETS) + ["custom_counts"]
|
||||||
|
|
||||||
|
|
||||||
|
def generation_profile_choices() -> list[str]:
|
||||||
|
return list(GENERATION_PROFILE_PRESETS)
|
||||||
|
|
||||||
|
|
||||||
|
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
|
||||||
|
category, default_subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
||||||
|
chosen_subcategory = subcategory if subcategory and subcategory != RANDOM_SUBCATEGORY else default_subcategory
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"preset": preset if preset in CATEGORY_PRESETS else "auto_weighted",
|
||||||
|
"category": category,
|
||||||
|
"subcategory": chosen_subcategory,
|
||||||
|
},
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_category_config(category_config: str | dict[str, Any] | None) -> tuple[str, str]:
|
||||||
|
if not category_config:
|
||||||
|
return CATEGORY_PRESETS["auto_weighted"]
|
||||||
|
if isinstance(category_config, dict):
|
||||||
|
raw = category_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(category_config))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid category_config JSON: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError("category_config must be a JSON object")
|
||||||
|
preset = str(raw.get("preset") or "auto_weighted")
|
||||||
|
category, subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
||||||
|
category = str(raw.get("category") or category)
|
||||||
|
subcategory = str(raw.get("subcategory") or subcategory or RANDOM_SUBCATEGORY)
|
||||||
|
return category, subcategory
|
||||||
|
|
||||||
|
|
||||||
|
def build_cast_config_json(cast_mode: str = "mixed_couple", women_count: int = 1, men_count: int = 1) -> str:
|
||||||
|
if cast_mode in CAST_PRESETS:
|
||||||
|
women_count, men_count = CAST_PRESETS[cast_mode]
|
||||||
|
else:
|
||||||
|
women_count = max(0, min(12, int(women_count)))
|
||||||
|
men_count = max(0, min(12, int(men_count)))
|
||||||
|
if women_count + men_count == 0:
|
||||||
|
women_count = 1
|
||||||
|
cast_mode = "custom_counts"
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"cast_mode": cast_mode,
|
||||||
|
"women_count": int(women_count),
|
||||||
|
"men_count": int(men_count),
|
||||||
|
},
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cast_config(cast_config: str | dict[str, Any] | None) -> dict[str, int | str]:
|
||||||
|
if not cast_config:
|
||||||
|
return {"cast_mode": "mixed_couple", "women_count": 1, "men_count": 1}
|
||||||
|
if isinstance(cast_config, dict):
|
||||||
|
raw = cast_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(cast_config))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid cast_config JSON: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError("cast_config must be a JSON object")
|
||||||
|
return json.loads(build_cast_config_json(str(raw.get("cast_mode") or "custom_counts"), raw.get("women_count", 1), raw.get("men_count", 1)))
|
||||||
|
|
||||||
|
|
||||||
|
def build_generation_profile_json(
|
||||||
|
profile: str = "balanced",
|
||||||
|
clothing_override: str = "profile_default",
|
||||||
|
poses_override: str = "profile_default",
|
||||||
|
expression_intensity: float = -1.0,
|
||||||
|
backside_bias: float = -1.0,
|
||||||
|
minimal_clothing_ratio: float = -1.0,
|
||||||
|
standard_pose_ratio: float = -1.0,
|
||||||
|
trigger_policy: str = "profile_default",
|
||||||
|
) -> str:
|
||||||
|
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
|
||||||
|
config = dict(GENERATION_PROFILE_PRESETS[profile])
|
||||||
|
if clothing_override in ("full", "minimal"):
|
||||||
|
config["clothing"] = clothing_override
|
||||||
|
if poses_override in ("standard", "evocative"):
|
||||||
|
config["poses"] = poses_override
|
||||||
|
if float(expression_intensity) >= 0:
|
||||||
|
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
|
||||||
|
if float(backside_bias) >= 0:
|
||||||
|
config["backside_bias"] = _clamped_float(backside_bias, config["backside_bias"])
|
||||||
|
if float(minimal_clothing_ratio) >= 0:
|
||||||
|
config["minimal_clothing_ratio"] = _clamped_float(minimal_clothing_ratio, config["minimal_clothing_ratio"])
|
||||||
|
if float(standard_pose_ratio) >= 0:
|
||||||
|
config["standard_pose_ratio"] = _clamped_float(standard_pose_ratio, config["standard_pose_ratio"])
|
||||||
|
if trigger_policy == "prepend_trigger":
|
||||||
|
config["prepend_trigger_to_prompt"] = True
|
||||||
|
elif trigger_policy == "do_not_prepend":
|
||||||
|
config["prepend_trigger_to_prompt"] = False
|
||||||
|
config["profile"] = profile
|
||||||
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
if not profile_config:
|
||||||
|
return dict(GENERATION_PROFILE_PRESETS["balanced"])
|
||||||
|
if isinstance(profile_config, dict):
|
||||||
|
raw = profile_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(profile_config))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid generation_profile JSON: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError("generation_profile must be a JSON object")
|
||||||
|
profile = str(raw.get("profile") or "balanced")
|
||||||
|
parsed = dict(GENERATION_PROFILE_PRESETS.get(profile, GENERATION_PROFILE_PRESETS["balanced"]))
|
||||||
|
parsed.update(raw)
|
||||||
|
parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal") else "full"
|
||||||
|
parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative") else "standard"
|
||||||
|
parsed["expression_intensity"] = _clamped_float(parsed.get("expression_intensity"), 0.5)
|
||||||
|
parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0)
|
||||||
|
parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0)
|
||||||
|
parsed["standard_pose_ratio"] = _clamped_float(parsed.get("standard_pose_ratio"), -1.0, -1.0, 1.0)
|
||||||
|
parsed["trigger"] = str(parsed.get("trigger") or "sxcpinup_coloredpencil")
|
||||||
|
parsed["prepend_trigger_to_prompt"] = bool(parsed.get("prepend_trigger_to_prompt"))
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def build_filter_config_json(
|
||||||
|
ethnicity: str = "any",
|
||||||
|
figure: str = "curvy",
|
||||||
|
no_plus_women: bool = False,
|
||||||
|
no_black: bool = False,
|
||||||
|
) -> str:
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"ethnicity": ethnicity if ethnicity in ("any", "asian", "white_asian") else "any",
|
||||||
|
"figure": figure if figure in ("curvy", "balanced", "bombshell") else "curvy",
|
||||||
|
"no_plus_women": bool(no_plus_women),
|
||||||
|
"no_black": bool(no_black),
|
||||||
|
},
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
defaults = {"ethnicity": "any", "figure": "curvy", "no_plus_women": False, "no_black": False}
|
||||||
|
if not filter_config:
|
||||||
|
return defaults
|
||||||
|
if isinstance(filter_config, dict):
|
||||||
|
raw = filter_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(filter_config))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError("filter_config must be a JSON object")
|
||||||
|
parsed = {**defaults, **raw}
|
||||||
|
parsed["ethnicity"] = parsed["ethnicity"] if parsed.get("ethnicity") in ("any", "asian", "white_asian") else "any"
|
||||||
|
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell") else "curvy"
|
||||||
|
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
|
||||||
|
parsed["no_black"] = bool(parsed.get("no_black"))
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
def _ratio_or_none(value: float) -> float | None:
|
def _ratio_or_none(value: float) -> float | None:
|
||||||
try:
|
try:
|
||||||
ratio = float(value)
|
ratio = float(value)
|
||||||
@@ -1162,6 +1425,238 @@ def _body_phrase(body: Any, figure_note: Any = "") -> str:
|
|||||||
return f"{body} figure with {figure_note}"
|
return f"{body} figure with {figure_note}"
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_profile_name(profile_name: str) -> str:
|
||||||
|
profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
|
||||||
|
return profile_name[:64] or "profile"
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_path(profile_name: str) -> Path:
|
||||||
|
return PROFILE_DIR / f"{_safe_profile_name(profile_name)}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def character_profile_choices() -> list[str]:
|
||||||
|
if not PROFILE_DIR.exists():
|
||||||
|
return ["manual"]
|
||||||
|
names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
|
||||||
|
return ["manual"] + names
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
|
||||||
|
if not value:
|
||||||
|
return {}
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(value))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid {label} JSON: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError(f"{label} must be a JSON object")
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
row = _load_json_object(metadata_json, "metadata_json")
|
||||||
|
if isinstance(row.get("softcore_row"), dict):
|
||||||
|
return row["softcore_row"]
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||||
|
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
|
||||||
|
age = str(profile.get("age") or "").strip()
|
||||||
|
age = age.removesuffix(" adults").removesuffix(" adult").strip()
|
||||||
|
subject_phrase = f"{age} adult {subject}".strip() if age else f"adult {subject}"
|
||||||
|
pieces = [
|
||||||
|
subject_phrase,
|
||||||
|
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
|
||||||
|
profile.get("skin"),
|
||||||
|
profile.get("hair"),
|
||||||
|
profile.get("eyes"),
|
||||||
|
]
|
||||||
|
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
|
||||||
|
subject_type = str(profile.get("subject_type") or profile.get("primary_subject") or profile.get("subject") or "").strip()
|
||||||
|
if subject_type not in ("woman", "man"):
|
||||||
|
subject_type = "woman"
|
||||||
|
body = str(profile.get("body") or profile.get("body_type") or "").strip()
|
||||||
|
figure = str(profile.get("figure") or "").strip()
|
||||||
|
body_phrase = str(profile.get("body_phrase") or "").strip() or _body_phrase(body, figure)
|
||||||
|
normalized = {
|
||||||
|
"profile_type": "character",
|
||||||
|
"profile_name": _safe_profile_name(profile_name or str(profile.get("profile_name") or "")),
|
||||||
|
"subject_type": subject_type,
|
||||||
|
"subject": subject_type,
|
||||||
|
"subject_phrase": subject_type,
|
||||||
|
"age": str(profile.get("age") or profile.get("age_band") or "").strip(),
|
||||||
|
"body": body,
|
||||||
|
"body_phrase": body_phrase,
|
||||||
|
"skin": str(profile.get("skin") or "").strip(),
|
||||||
|
"hair": str(profile.get("hair") or "").strip(),
|
||||||
|
"eyes": str(profile.get("eyes") or "").strip(),
|
||||||
|
"figure": figure,
|
||||||
|
}
|
||||||
|
normalized["descriptor"] = _character_profile_descriptor(normalized)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def build_character_profile_json(
|
||||||
|
profile_name: str = "",
|
||||||
|
source: str = "metadata_json",
|
||||||
|
metadata_json: str | dict[str, Any] | None = "",
|
||||||
|
subject_type: str = "woman",
|
||||||
|
age: str = "",
|
||||||
|
body: str = "",
|
||||||
|
body_phrase: str = "",
|
||||||
|
skin: str = "",
|
||||||
|
hair: str = "",
|
||||||
|
eyes: str = "",
|
||||||
|
figure: str = "",
|
||||||
|
save_now: bool = False,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
if source == "metadata_json":
|
||||||
|
row = _row_from_profile_metadata(metadata_json)
|
||||||
|
raw_profile = {
|
||||||
|
"profile_name": profile_name,
|
||||||
|
"subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
|
||||||
|
"age": row.get("age") or row.get("age_band") or age,
|
||||||
|
"body": row.get("body") or row.get("body_type") or body,
|
||||||
|
"body_phrase": row.get("body_phrase") or body_phrase,
|
||||||
|
"skin": row.get("skin") or skin,
|
||||||
|
"hair": row.get("hair") or hair,
|
||||||
|
"eyes": row.get("eyes") or eyes,
|
||||||
|
"figure": row.get("figure") or figure,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raw_profile = {
|
||||||
|
"profile_name": profile_name,
|
||||||
|
"subject_type": subject_type,
|
||||||
|
"age": age,
|
||||||
|
"body": body,
|
||||||
|
"body_phrase": body_phrase,
|
||||||
|
"skin": skin,
|
||||||
|
"hair": hair,
|
||||||
|
"eyes": eyes,
|
||||||
|
"figure": figure,
|
||||||
|
}
|
||||||
|
profile = _normalize_character_profile(raw_profile, profile_name)
|
||||||
|
saved_path = ""
|
||||||
|
status = "not_saved"
|
||||||
|
if save_now:
|
||||||
|
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = _profile_path(profile["profile_name"])
|
||||||
|
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
saved_path = str(path)
|
||||||
|
status = "saved"
|
||||||
|
return {
|
||||||
|
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||||
|
"profile_name": profile["profile_name"],
|
||||||
|
"descriptor": profile["descriptor"],
|
||||||
|
"saved_path": saved_path,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_profile_result(status: str = "empty") -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"profile_json": "",
|
||||||
|
"profile_name": "",
|
||||||
|
"descriptor": "",
|
||||||
|
"saved_path": "",
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_character_profile_json(
|
||||||
|
profile_name: str = "",
|
||||||
|
fallback_profile_json: str | dict[str, Any] | None = "",
|
||||||
|
enabled: bool = True,
|
||||||
|
delete_now: bool = False,
|
||||||
|
rename_now: bool = False,
|
||||||
|
rename_to: str = "",
|
||||||
|
) -> dict[str, str]:
|
||||||
|
if not enabled:
|
||||||
|
return _empty_profile_result("disabled")
|
||||||
|
if delete_now and rename_now:
|
||||||
|
return _empty_profile_result("choose_delete_or_rename")
|
||||||
|
|
||||||
|
raw_profile = _load_json_object(fallback_profile_json, "fallback_profile_json")
|
||||||
|
saved_path = ""
|
||||||
|
if profile_name and profile_name != "manual":
|
||||||
|
path = _profile_path(profile_name)
|
||||||
|
if delete_now:
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
return _empty_profile_result(f"deleted:{path.stem}")
|
||||||
|
return _empty_profile_result(f"delete_missing:{_safe_profile_name(profile_name)}")
|
||||||
|
if rename_now:
|
||||||
|
new_name = _safe_profile_name(rename_to)
|
||||||
|
if not rename_to.strip():
|
||||||
|
return _empty_profile_result("rename_missing_name")
|
||||||
|
if not path.exists():
|
||||||
|
return _empty_profile_result(f"rename_missing:{_safe_profile_name(profile_name)}")
|
||||||
|
target = _profile_path(new_name)
|
||||||
|
if target.exists() and target != path:
|
||||||
|
return _empty_profile_result(f"rename_target_exists:{target.stem}")
|
||||||
|
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
||||||
|
profile = _normalize_character_profile(raw_profile, new_name)
|
||||||
|
target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
if target != path:
|
||||||
|
path.unlink()
|
||||||
|
return {
|
||||||
|
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||||
|
"profile_name": profile["profile_name"],
|
||||||
|
"descriptor": profile["descriptor"],
|
||||||
|
"saved_path": str(target),
|
||||||
|
"status": f"renamed:{path.stem}->{target.stem}",
|
||||||
|
}
|
||||||
|
if path.exists():
|
||||||
|
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
||||||
|
saved_path = str(path)
|
||||||
|
if not raw_profile:
|
||||||
|
return _empty_profile_result("empty")
|
||||||
|
profile = _normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
|
||||||
|
return {
|
||||||
|
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||||
|
"profile_name": profile["profile_name"],
|
||||||
|
"descriptor": profile["descriptor"],
|
||||||
|
"saved_path": saved_path,
|
||||||
|
"status": "loaded" if saved_path else "fallback",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
raw = _load_json_object(character_profile, "character_profile")
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
|
||||||
|
return _normalize_character_profile(raw, str(raw.get("profile_name") or ""))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_character_profile_to_context(
|
||||||
|
context: dict[str, Any],
|
||||||
|
character_profile: str | dict[str, Any] | None,
|
||||||
|
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||||
|
profile = _parse_character_profile(character_profile)
|
||||||
|
if not profile:
|
||||||
|
return context, {}, "none"
|
||||||
|
if context.get("subject_type") not in ("woman", "man"):
|
||||||
|
return context, profile, "skipped_non_single_subject"
|
||||||
|
if profile["subject_type"] != context.get("subject_type"):
|
||||||
|
return context, profile, "skipped_subject_mismatch"
|
||||||
|
updated = dict(context)
|
||||||
|
for key in ("subject_type", "subject", "subject_phrase", "age", "body", "body_phrase", "skin", "hair", "eyes", "figure"):
|
||||||
|
value = profile.get(key)
|
||||||
|
if value:
|
||||||
|
updated[key] = value
|
||||||
|
updated["subject"] = profile["subject_type"]
|
||||||
|
updated["subject_phrase"] = profile["subject_type"]
|
||||||
|
return updated, profile, "applied"
|
||||||
|
|
||||||
|
|
||||||
def _composition_prompt(composition: str) -> str:
|
def _composition_prompt(composition: str) -> str:
|
||||||
composition = str(composition or "").strip()
|
composition = str(composition or "").strip()
|
||||||
if not composition:
|
if not composition:
|
||||||
@@ -1760,6 +2255,7 @@ def _build_custom_row(
|
|||||||
seed: int,
|
seed: int,
|
||||||
seed_config: dict[str, int],
|
seed_config: dict[str, int],
|
||||||
expression_intensity: float,
|
expression_intensity: float,
|
||||||
|
character_profile: str | dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
categories = load_category_library()
|
categories = load_category_library()
|
||||||
category_rng = _axis_rng(seed_config, "category", seed, row_number)
|
category_rng = _axis_rng(seed_config, "category", seed, row_number)
|
||||||
@@ -1797,6 +2293,7 @@ def _build_custom_row(
|
|||||||
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
|
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
|
||||||
subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any"))
|
subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any"))
|
||||||
context = _subject_context(person_rng, subject_type, ethnicity, figure, no_plus_women, no_black, women_count, men_count)
|
context = _subject_context(person_rng, subject_type, ethnicity, figure, no_plus_women, no_black, women_count, men_count)
|
||||||
|
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
||||||
subject_type = context["subject_type"]
|
subject_type = context["subject_type"]
|
||||||
role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
|
role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
|
||||||
|
|
||||||
@@ -1914,6 +2411,8 @@ def _build_custom_row(
|
|||||||
"men_count": context.get("men_count", ""),
|
"men_count": context.get("men_count", ""),
|
||||||
"person_count": context.get("person_count", ""),
|
"person_count": context.get("person_count", ""),
|
||||||
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {},
|
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {},
|
||||||
|
"character_profile": applied_profile,
|
||||||
|
"character_profile_status": profile_status,
|
||||||
"source": "json_category",
|
"source": "json_category",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1946,6 +2445,7 @@ def build_prompt(
|
|||||||
men_count: int = 1,
|
men_count: int = 1,
|
||||||
camera_config: str | dict[str, Any] | None = None,
|
camera_config: str | dict[str, Any] | None = None,
|
||||||
expression_intensity: float = 0.5,
|
expression_intensity: float = 0.5,
|
||||||
|
character_profile: str | dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
apply_pool_extensions()
|
apply_pool_extensions()
|
||||||
row_number = max(1, int(row_number))
|
row_number = max(1, int(row_number))
|
||||||
@@ -2009,6 +2509,7 @@ def build_prompt(
|
|||||||
seed,
|
seed,
|
||||||
parsed_seed_config,
|
parsed_seed_config,
|
||||||
expression_intensity,
|
expression_intensity,
|
||||||
|
character_profile,
|
||||||
)
|
)
|
||||||
|
|
||||||
if extra_positive.strip():
|
if extra_positive.strip():
|
||||||
@@ -2022,6 +2523,52 @@ def build_prompt(
|
|||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt_from_configs(
|
||||||
|
row_number: int,
|
||||||
|
start_index: int,
|
||||||
|
seed: int,
|
||||||
|
category_config: str | dict[str, Any] | None = "",
|
||||||
|
cast_config: str | dict[str, Any] | None = "",
|
||||||
|
generation_profile: str | dict[str, Any] | None = "",
|
||||||
|
filter_config: str | dict[str, Any] | None = "",
|
||||||
|
seed_config: str | dict[str, Any] | None = "",
|
||||||
|
camera_config: str | dict[str, Any] | None = "",
|
||||||
|
character_profile: str | dict[str, Any] | None = "",
|
||||||
|
extra_positive: str = "",
|
||||||
|
extra_negative: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
category, subcategory = _parse_category_config(category_config)
|
||||||
|
cast = _parse_cast_config(cast_config)
|
||||||
|
profile = _parse_generation_profile(generation_profile)
|
||||||
|
filters = _parse_filter_config(filter_config)
|
||||||
|
return build_prompt(
|
||||||
|
category=category,
|
||||||
|
subcategory=subcategory,
|
||||||
|
row_number=row_number,
|
||||||
|
start_index=start_index,
|
||||||
|
seed=seed,
|
||||||
|
clothing=profile["clothing"],
|
||||||
|
ethnicity=filters["ethnicity"],
|
||||||
|
poses=profile["poses"],
|
||||||
|
expression_intensity=profile["expression_intensity"],
|
||||||
|
backside_bias=profile["backside_bias"],
|
||||||
|
figure=filters["figure"],
|
||||||
|
no_plus_women=filters["no_plus_women"],
|
||||||
|
no_black=filters["no_black"],
|
||||||
|
women_count=int(cast["women_count"]),
|
||||||
|
men_count=int(cast["men_count"]),
|
||||||
|
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
|
||||||
|
standard_pose_ratio=profile["standard_pose_ratio"],
|
||||||
|
trigger=profile["trigger"],
|
||||||
|
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
|
||||||
|
extra_positive=extra_positive or "",
|
||||||
|
extra_negative=extra_negative or "",
|
||||||
|
seed_config=seed_config or "",
|
||||||
|
camera_config=camera_config or "",
|
||||||
|
character_profile=character_profile or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
INSTA_OF_SOFT_LEVELS = {
|
INSTA_OF_SOFT_LEVELS = {
|
||||||
"social_tease": "Instagram-style thirst-trap post, suggestive but non-explicit, polished social feed energy",
|
"social_tease": "Instagram-style thirst-trap post, suggestive but non-explicit, polished social feed energy",
|
||||||
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate but without explicit sex",
|
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate but without explicit sex",
|
||||||
@@ -2264,6 +2811,7 @@ def build_insta_of_pair(
|
|||||||
seed_config: str | dict[str, Any] | None = None,
|
seed_config: str | dict[str, Any] | None = None,
|
||||||
options_json: str | dict[str, Any] | None = None,
|
options_json: str | dict[str, Any] | None = None,
|
||||||
camera_config: str | dict[str, Any] | None = None,
|
camera_config: str | dict[str, Any] | None = None,
|
||||||
|
character_profile: str | dict[str, Any] | None = "",
|
||||||
extra_positive: str = "",
|
extra_positive: str = "",
|
||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -2295,6 +2843,7 @@ def build_insta_of_pair(
|
|||||||
women_count=1,
|
women_count=1,
|
||||||
men_count=0,
|
men_count=0,
|
||||||
expression_intensity=options["softcore_expression_intensity"],
|
expression_intensity=options["softcore_expression_intensity"],
|
||||||
|
character_profile=character_profile or "",
|
||||||
)
|
)
|
||||||
hard_row = build_prompt(
|
hard_row = build_prompt(
|
||||||
category="Hardcore sexual poses",
|
category="Hardcore sexual poses",
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
|
|
||||||
|
const EXTENSION = "ethanfel.prompt_builder.profile_buttons";
|
||||||
|
|
||||||
|
function widget(node, name) {
|
||||||
|
return node.widgets?.find((w) => w.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideWidget(w) {
|
||||||
|
if (!w) return;
|
||||||
|
if (w.origType === undefined) w.origType = w.type;
|
||||||
|
w.type = "hidden";
|
||||||
|
w.hidden = true;
|
||||||
|
w.computeSize = () => [0, -4];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeNode(node) {
|
||||||
|
const size = node.computeSize?.();
|
||||||
|
if (size) node.setSize?.(size);
|
||||||
|
app.graph?.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queueOnce(node, triggerName) {
|
||||||
|
const trigger = widget(node, triggerName);
|
||||||
|
if (!trigger) {
|
||||||
|
alert(`Missing trigger widget: ${triggerName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.value = true;
|
||||||
|
node.setDirtyCanvas?.(true, true);
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await app.queuePrompt(0, 1);
|
||||||
|
} catch (_err) {
|
||||||
|
await app.queuePrompt(0);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[${EXTENSION}] queue failed`, err);
|
||||||
|
alert(`Queue failed: ${err}`);
|
||||||
|
} finally {
|
||||||
|
trigger.value = false;
|
||||||
|
node.setDirtyCanvas?.(true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSaveNode(node) {
|
||||||
|
hideWidget(widget(node, "save_now"));
|
||||||
|
if (!node._sxcpSaveButton) {
|
||||||
|
node._sxcpSaveButton = node.addWidget("button", "Save Profile Now", null, () => queueOnce(node, "save_now"));
|
||||||
|
}
|
||||||
|
resizeNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupLoadNode(node) {
|
||||||
|
hideWidget(widget(node, "delete_now"));
|
||||||
|
hideWidget(widget(node, "rename_now"));
|
||||||
|
if (!node._sxcpDeleteButton) {
|
||||||
|
node._sxcpDeleteButton = node.addWidget("button", "Delete Selected Profile", null, () => {
|
||||||
|
const profile = widget(node, "profile_name")?.value || "";
|
||||||
|
if (!profile || profile === "manual") {
|
||||||
|
alert("Select a saved profile before deleting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Delete saved profile "${profile}"?`)) return;
|
||||||
|
queueOnce(node, "delete_now");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!node._sxcpRenameButton) {
|
||||||
|
node._sxcpRenameButton = node.addWidget("button", "Rename Selected Profile", null, () => {
|
||||||
|
const profile = widget(node, "profile_name")?.value || "";
|
||||||
|
const target = widget(node, "rename_to")?.value || "";
|
||||||
|
if (!profile || profile === "manual") {
|
||||||
|
alert("Select a saved profile before renaming.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!target.trim()) {
|
||||||
|
alert("Fill rename_to before renaming.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Rename saved profile "${profile}" to "${target}"?`)) return;
|
||||||
|
queueOnce(node, "rename_now");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resizeNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: EXTENSION,
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData.name !== "SxCPCharacterProfileSave" && nodeData.name !== "SxCPCharacterProfileLoad") return;
|
||||||
|
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
const result = onNodeCreated?.apply(this, arguments);
|
||||||
|
if (nodeData.name === "SxCPCharacterProfileSave") setupSaveNode(this);
|
||||||
|
if (nodeData.name === "SxCPCharacterProfileLoad") setupLoadNode(this);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfigure = nodeType.prototype.onConfigure;
|
||||||
|
nodeType.prototype.onConfigure = function () {
|
||||||
|
const result = onConfigure?.apply(this, arguments);
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (nodeData.name === "SxCPCharacterProfileSave") setupSaveNode(this);
|
||||||
|
if (nodeData.name === "SxCPCharacterProfileLoad") setupLoadNode(this);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user