Connect character profiles to slots

This commit is contained in:
2026-06-24 20:15:09 +02:00
parent 5f4f166631
commit aca1e2f421
3 changed files with 65 additions and 19 deletions
+18 -11
View File
@@ -181,12 +181,15 @@ nodes remain useful for one reusable primary character, but slots are better
when you need different settings for each participant. when you need different settings for each participant.
`SxCP Character Profile Save` extracts a reusable woman/man profile from `SxCP Character Profile Save` extracts a reusable woman/man profile from
`metadata_json` or from manual fields. The profile stores age, body/body phrase, `metadata_json`, a `character_slot`, or from manual fields. The profile stores
skin, hair, eyes, figure, and subject type. It only writes a file when age, body/body phrase, skin, hair, eyes, figure, and subject type. To save a
the `Save Profile Now` button is clicked; otherwise it just outputs profile JSON Woman Slot, connect `Woman Slot.character_slot` to
for direct wiring. Saved files are written under `profiles/<profile_name>.json`; `Character Profile Save.character_slot`, set `source=character_slot`, enter a
saved profile files are ignored by git. The button is backed by the hidden name, then click `Save Profile Now`. It only writes a file when the button is
`save_now` trigger and queues the workflow once. 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 `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 an empty profile so connected prompt builders ignore it. When enabled, it loads
@@ -202,12 +205,16 @@ buttons are backed by hidden `delete_now` and `rename_now` triggers and queue
the workflow once. the workflow once.
Connect the loader's `character_profile` output to `SxCP Prompt Builder`, Connect the loader's `character_profile` output to `SxCP Prompt Builder`,
`SxCP Prompt Builder From Configs`, or `SxCP Insta/OF Prompt Pair`. `SxCP Prompt Builder From Configs`, or `SxCP Insta/OF Prompt Pair` for the
older single-primary-character path. Connect `character_cast` instead when you
want the loaded profile to act like a slot in a cast chain.
Profile reuse currently applies to structured JSON-category single woman/man When loaded through `character_profile`, profile reuse applies to structured
rows and to the primary creator in Insta/OF pair mode. The outfit, scene, pose, JSON-category single woman/man rows and to the primary creator in Insta/OF pair
expression, and composition can still change while the saved character mode. When loaded through `character_cast`, the same saved appearance behaves
appearance remains stable. like an auto-labeled slot and can participate in couple/group casts. 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. When an axis is set to `random`, the builder's optional `seed_config` input. When an axis is set to `random`, the
+24 -7
View File
@@ -989,7 +989,7 @@ class SxCPCharacterProfileSave:
return { return {
"required": { "required": {
"profile_name": ("STRING", {"default": "saved_character"}), "profile_name": ("STRING", {"default": "saved_character"}),
"source": (["metadata_json", "manual"], {"default": "metadata_json"}), "source": (["metadata_json", "character_slot", "manual"], {"default": "metadata_json"}),
"subject_type": (["woman", "man"], {"default": "woman"}), "subject_type": (["woman", "man"], {"default": "woman"}),
"age": ("STRING", {"default": ""}), "age": ("STRING", {"default": ""}),
"body": ("STRING", {"default": ""}), "body": ("STRING", {"default": ""}),
@@ -1002,11 +1002,12 @@ class SxCPCharacterProfileSave:
}, },
"optional": { "optional": {
"metadata_json": ("STRING", {"default": "", "multiline": True}), "metadata_json": ("STRING", {"default": "", "multiline": True}),
"character_slot": ("STRING", {"default": "", "multiline": True}),
}, },
} }
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status") RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "character_cast")
FUNCTION = "build" FUNCTION = "build"
CATEGORY = "prompt_builder" CATEGORY = "prompt_builder"
@@ -1024,11 +1025,13 @@ class SxCPCharacterProfileSave:
figure, figure,
save_now, save_now,
metadata_json="", metadata_json="",
character_slot="",
): ):
profile = build_character_profile_json( profile = build_character_profile_json(
profile_name=profile_name, profile_name=profile_name,
source=source, source=source,
metadata_json=metadata_json or "", metadata_json=metadata_json or "",
character_slot=character_slot or "",
subject_type=subject_type, subject_type=subject_type,
age=age, age=age,
body=body, body=body,
@@ -1039,7 +1042,14 @@ class SxCPCharacterProfileSave:
figure=figure, figure=figure,
save_now=save_now, save_now=save_now,
) )
return profile["profile_json"], profile["descriptor"], profile["profile_name"], profile["saved_path"], profile["status"] return (
profile["profile_json"],
profile["descriptor"],
profile["profile_name"],
profile["saved_path"],
profile["status"],
profile["profile_json"],
)
class SxCPCharacterProfileLoad: class SxCPCharacterProfileLoad:
@@ -1059,8 +1069,8 @@ class SxCPCharacterProfileLoad:
}, },
} }
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status") RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "character_cast")
FUNCTION = "build" FUNCTION = "build"
CATEGORY = "prompt_builder" CATEGORY = "prompt_builder"
@@ -1083,7 +1093,14 @@ class SxCPCharacterProfileLoad:
rename_now=rename_now, rename_now=rename_now,
rename_to=rename_to, rename_to=rename_to,
) )
return profile["profile_json"], profile["descriptor"], profile["profile_name"], profile["saved_path"], profile["status"] return (
profile["profile_json"],
profile["descriptor"],
profile["profile_name"],
profile["saved_path"],
profile["status"],
profile["profile_json"],
)
class SxCPCaptionNaturalizer: class SxCPCaptionNaturalizer:
+23 -1
View File
@@ -2847,6 +2847,13 @@ def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> di
return row return row
def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
slots = _parse_character_cast(character_slot)
if not slots:
return {}
return slots[-1]
def _character_profile_descriptor(profile: dict[str, Any]) -> str: def _character_profile_descriptor(profile: dict[str, Any]) -> str:
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip() subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
return _descriptor_from_parts( return _descriptor_from_parts(
@@ -2890,6 +2897,7 @@ def build_character_profile_json(
profile_name: str = "", profile_name: str = "",
source: str = "metadata_json", source: str = "metadata_json",
metadata_json: str | dict[str, Any] | None = "", metadata_json: str | dict[str, Any] | None = "",
character_slot: str | dict[str, Any] | None = "",
subject_type: str = "woman", subject_type: str = "woman",
age: str = "", age: str = "",
body: str = "", body: str = "",
@@ -2900,7 +2908,21 @@ def build_character_profile_json(
figure: str = "", figure: str = "",
save_now: bool = False, save_now: bool = False,
) -> dict[str, str]: ) -> dict[str, str]:
if source == "metadata_json": if source == "character_slot":
row = _row_from_character_slot(character_slot or metadata_json)
raw_profile = {
"profile_name": profile_name,
"subject_type": row.get("subject_type") or subject_type,
"age": row.get("age") or age,
"body": row.get("body") 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,
"descriptor_detail": row.get("descriptor_detail") or "auto",
}
elif source == "metadata_json":
row = _row_from_profile_metadata(metadata_json) row = _row_from_profile_metadata(metadata_json)
raw_profile = { raw_profile = {
"profile_name": profile_name, "profile_name": profile_name,