diff --git a/README.md b/README.md index 0ea90d1..1cc120f 100644 --- a/README.md +++ b/README.md @@ -181,12 +181,15 @@ nodes remain useful for one reusable primary character, but slots are better when you need different settings for each participant. `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/.json`; -saved profile files are ignored by git. The button is backed by the hidden -`save_now` trigger and queues the workflow once. +`metadata_json`, a `character_slot`, or from manual fields. The profile stores +age, body/body phrase, skin, hair, eyes, figure, and subject type. To save a +Woman Slot, connect `Woman Slot.character_slot` to +`Character Profile Save.character_slot`, set `source=character_slot`, enter a +name, then click `Save Profile Now`. It only writes a file when the button is +clicked; otherwise it just outputs profile JSON for direct wiring. Saved files +are written under `profiles/.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 @@ -202,12 +205,16 @@ 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`. +`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 -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. +When loaded through `character_profile`, profile reuse applies to structured +JSON-category single woman/man rows and to the primary creator in Insta/OF pair +mode. When loaded through `character_cast`, the same saved appearance behaves +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 builder's optional `seed_config` input. When an axis is set to `random`, the diff --git a/__init__.py b/__init__.py index 1f5f860..d2f5c4e 100644 --- a/__init__.py +++ b/__init__.py @@ -989,7 +989,7 @@ class SxCPCharacterProfileSave: return { "required": { "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"}), "age": ("STRING", {"default": ""}), "body": ("STRING", {"default": ""}), @@ -1002,11 +1002,12 @@ class SxCPCharacterProfileSave: }, "optional": { "metadata_json": ("STRING", {"default": "", "multiline": True}), + "character_slot": ("STRING", {"default": "", "multiline": True}), }, } - RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") - RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status") + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "character_cast") FUNCTION = "build" CATEGORY = "prompt_builder" @@ -1024,11 +1025,13 @@ class SxCPCharacterProfileSave: figure, save_now, metadata_json="", + character_slot="", ): profile = build_character_profile_json( profile_name=profile_name, source=source, metadata_json=metadata_json or "", + character_slot=character_slot or "", subject_type=subject_type, age=age, body=body, @@ -1039,7 +1042,14 @@ class SxCPCharacterProfileSave: figure=figure, 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: @@ -1059,8 +1069,8 @@ class SxCPCharacterProfileLoad: }, } - RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") - RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status") + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "character_cast") FUNCTION = "build" CATEGORY = "prompt_builder" @@ -1083,7 +1093,14 @@ class SxCPCharacterProfileLoad: rename_now=rename_now, 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: diff --git a/prompt_builder.py b/prompt_builder.py index 1caf841..b9b691d 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -2847,6 +2847,13 @@ def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> di 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: subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip() return _descriptor_from_parts( @@ -2890,6 +2897,7 @@ def build_character_profile_json( profile_name: str = "", source: str = "metadata_json", metadata_json: str | dict[str, Any] | None = "", + character_slot: str | dict[str, Any] | None = "", subject_type: str = "woman", age: str = "", body: str = "", @@ -2900,7 +2908,21 @@ def build_character_profile_json( figure: str = "", save_now: bool = False, ) -> 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) raw_profile = { "profile_name": profile_name,