Add chainable character slot controls
This commit is contained in:
@@ -14,6 +14,7 @@ The node is registered as:
|
|||||||
- `prompt_builder / SxCP Generation Profile`
|
- `prompt_builder / SxCP Generation Profile`
|
||||||
- `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 Character Slot`
|
||||||
- `prompt_builder / SxCP Character Profile Save`
|
- `prompt_builder / SxCP Character Profile Save`
|
||||||
- `prompt_builder / SxCP Character Profile Load`
|
- `prompt_builder / SxCP Character Profile Load`
|
||||||
- `prompt_builder / SxCP Caption Naturalizer`
|
- `prompt_builder / SxCP Caption Naturalizer`
|
||||||
@@ -51,7 +52,8 @@ node. For cleaner workflows, use the split nodes:
|
|||||||
The practical compact workflow is:
|
The practical compact workflow is:
|
||||||
|
|
||||||
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
||||||
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control`, and `Character Profile`
|
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control`,
|
||||||
|
`Character Slot`, and `Character Profile`
|
||||||
into `Prompt Builder From Configs`.
|
into `Prompt Builder From Configs`.
|
||||||
|
|
||||||
An importable default workflow is included at
|
An importable default workflow is included at
|
||||||
@@ -68,6 +70,29 @@ as one long chain:
|
|||||||
|
|
||||||
## Character Profiles
|
## Character Profiles
|
||||||
|
|
||||||
|
`SxCP Character Slot` is the scalable per-participant control node. Each slot
|
||||||
|
defines one woman or man with optional overrides for age, ethnicity, figure,
|
||||||
|
body/body phrase, skin, hair, and eyes. Leave any field on `random` or blank to
|
||||||
|
let the generator fill that part from the normal pools; set exact values only
|
||||||
|
where you want control.
|
||||||
|
|
||||||
|
Slots are chainable through the `character_cast` input/output. In automatic
|
||||||
|
label mode, the slot closest to the final generator becomes `A` for its gender,
|
||||||
|
the next upstream slot becomes `B`, then `C`, and so on. Example:
|
||||||
|
|
||||||
|
`Woman slot 3 -> Woman slot 2 -> Woman slot 1 -> Insta/OF Prompt Pair`
|
||||||
|
|
||||||
|
In that chain, `Woman slot 1` resolves as `Woman A`, `Woman slot 2` resolves as
|
||||||
|
`Woman B`, and `Woman slot 3` resolves as `Woman C`. Men resolve separately the
|
||||||
|
same way, so the closest man slot becomes `Man A`.
|
||||||
|
|
||||||
|
Connect the final `character_cast` output to `SxCP Prompt Builder`,
|
||||||
|
`SxCP Prompt Builder From Configs`, or `SxCP Insta/OF Prompt Pair`. It applies
|
||||||
|
to JSON/custom single woman/man rows, JSON/custom configured-cast rows such as
|
||||||
|
`Hardcore sexual poses`, and Insta/OF named casts. The older profile save/load
|
||||||
|
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
|
`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` or from manual fields. The profile stores age, body/body phrase,
|
||||||
skin, hair, eyes, figure, and subject type. It only writes a file when
|
skin, hair, eyes, figure, and subject type. It only writes a file when
|
||||||
@@ -173,6 +198,9 @@ Important behavior:
|
|||||||
`minimal` omits most style text.
|
`minimal` omits most style text.
|
||||||
- For Insta/OF paired metadata, the node returns both `krea_softcore_prompt` and
|
- For Insta/OF paired metadata, the node returns both `krea_softcore_prompt` and
|
||||||
`krea_hardcore_prompt`, with separate softcore and hardcore negatives.
|
`krea_hardcore_prompt`, with separate softcore and hardcore negatives.
|
||||||
|
- Insta/OF cast metadata is rewritten as direct named-character prose such as
|
||||||
|
`Woman A is ...` and `Man A is ...`, so Krea2 does not have to interpret a
|
||||||
|
`Cast descriptors:` label.
|
||||||
|
|
||||||
It outputs:
|
It outputs:
|
||||||
|
|
||||||
@@ -197,6 +225,12 @@ and location. The generated positive prompts are still standalone: each output
|
|||||||
lists the relevant cast descriptors directly and does not depend on the image
|
lists the relevant cast descriptors directly and does not depend on the image
|
||||||
model carrying context from another prompt.
|
model carrying context from another prompt.
|
||||||
|
|
||||||
|
For per-character control, chain `SxCP Character Slot` nodes into the pair
|
||||||
|
node's `character_cast` input. The nearest woman slot controls the shared
|
||||||
|
primary creator (`Woman A`) in both softcore and hardcore outputs; additional
|
||||||
|
woman/man slots fill partner descriptors before random fallback descriptors are
|
||||||
|
used.
|
||||||
|
|
||||||
It outputs:
|
It outputs:
|
||||||
|
|
||||||
- `softcore_prompt`
|
- `softcore_prompt`
|
||||||
|
|||||||
+88
@@ -7,6 +7,7 @@ try:
|
|||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
build_cast_config_json,
|
build_cast_config_json,
|
||||||
build_category_config_json,
|
build_category_config_json,
|
||||||
|
build_character_slot_json,
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_filter_config_json,
|
build_filter_config_json,
|
||||||
build_generation_profile_json,
|
build_generation_profile_json,
|
||||||
@@ -28,6 +29,11 @@ try:
|
|||||||
cast_preset_choices,
|
cast_preset_choices,
|
||||||
category_preset_choices,
|
category_preset_choices,
|
||||||
category_choices,
|
category_choices,
|
||||||
|
character_age_choices,
|
||||||
|
character_body_choices,
|
||||||
|
character_ethnicity_choices,
|
||||||
|
character_figure_choices,
|
||||||
|
character_label_choices,
|
||||||
character_profile_choices,
|
character_profile_choices,
|
||||||
ethnicity_choices,
|
ethnicity_choices,
|
||||||
generation_profile_choices,
|
generation_profile_choices,
|
||||||
@@ -41,6 +47,7 @@ except ImportError:
|
|||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
build_cast_config_json,
|
build_cast_config_json,
|
||||||
build_category_config_json,
|
build_category_config_json,
|
||||||
|
build_character_slot_json,
|
||||||
build_character_profile_json,
|
build_character_profile_json,
|
||||||
build_filter_config_json,
|
build_filter_config_json,
|
||||||
build_generation_profile_json,
|
build_generation_profile_json,
|
||||||
@@ -62,6 +69,11 @@ except ImportError:
|
|||||||
cast_preset_choices,
|
cast_preset_choices,
|
||||||
category_preset_choices,
|
category_preset_choices,
|
||||||
category_choices,
|
category_choices,
|
||||||
|
character_age_choices,
|
||||||
|
character_body_choices,
|
||||||
|
character_ethnicity_choices,
|
||||||
|
character_figure_choices,
|
||||||
|
character_label_choices,
|
||||||
character_profile_choices,
|
character_profile_choices,
|
||||||
ethnicity_choices,
|
ethnicity_choices,
|
||||||
generation_profile_choices,
|
generation_profile_choices,
|
||||||
@@ -99,6 +111,7 @@ class SxCPPromptBuilder:
|
|||||||
"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}),
|
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"character_cast": ("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}),
|
||||||
},
|
},
|
||||||
@@ -131,6 +144,7 @@ class SxCPPromptBuilder:
|
|||||||
seed_config="",
|
seed_config="",
|
||||||
camera_config="",
|
camera_config="",
|
||||||
character_profile="",
|
character_profile="",
|
||||||
|
character_cast="",
|
||||||
extra_positive="",
|
extra_positive="",
|
||||||
extra_negative="",
|
extra_negative="",
|
||||||
no_plus_women=False,
|
no_plus_women=False,
|
||||||
@@ -161,6 +175,7 @@ class SxCPPromptBuilder:
|
|||||||
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 "",
|
character_profile=character_profile or "",
|
||||||
|
character_cast=character_cast or "",
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
row["prompt"],
|
row["prompt"],
|
||||||
@@ -474,6 +489,7 @@ class SxCPPromptBuilderFromConfigs:
|
|||||||
"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}),
|
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"character_cast": ("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}),
|
||||||
},
|
},
|
||||||
@@ -496,6 +512,7 @@ class SxCPPromptBuilderFromConfigs:
|
|||||||
seed_config="",
|
seed_config="",
|
||||||
camera_config="",
|
camera_config="",
|
||||||
character_profile="",
|
character_profile="",
|
||||||
|
character_cast="",
|
||||||
extra_positive="",
|
extra_positive="",
|
||||||
extra_negative="",
|
extra_negative="",
|
||||||
):
|
):
|
||||||
@@ -510,6 +527,7 @@ class SxCPPromptBuilderFromConfigs:
|
|||||||
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 "",
|
character_profile=character_profile or "",
|
||||||
|
character_cast=character_cast or "",
|
||||||
extra_positive=extra_positive or "",
|
extra_positive=extra_positive or "",
|
||||||
extra_negative=extra_negative or "",
|
extra_negative=extra_negative or "",
|
||||||
)
|
)
|
||||||
@@ -523,6 +541,71 @@ class SxCPPromptBuilderFromConfigs:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPCharacterSlot:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"enabled": ("BOOLEAN", {"default": True}),
|
||||||
|
"subject_type": (["woman", "man"], {"default": "woman"}),
|
||||||
|
"label": (character_label_choices(), {"default": "auto_chain"}),
|
||||||
|
"age": (character_age_choices(), {"default": "random"}),
|
||||||
|
"manual_age": ("STRING", {"default": ""}),
|
||||||
|
"ethnicity": (character_ethnicity_choices(), {"default": "random"}),
|
||||||
|
"figure": (character_figure_choices(), {"default": "random"}),
|
||||||
|
"body": (character_body_choices(), {"default": "random"}),
|
||||||
|
"manual_body": ("STRING", {"default": ""}),
|
||||||
|
"body_phrase": ("STRING", {"default": ""}),
|
||||||
|
"skin": ("STRING", {"default": ""}),
|
||||||
|
"hair": ("STRING", {"default": ""}),
|
||||||
|
"eyes": ("STRING", {"default": ""}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = ("character_cast", "character_slot", "summary", "status")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
enabled,
|
||||||
|
subject_type,
|
||||||
|
label,
|
||||||
|
age,
|
||||||
|
manual_age,
|
||||||
|
ethnicity,
|
||||||
|
figure,
|
||||||
|
body,
|
||||||
|
manual_body,
|
||||||
|
body_phrase,
|
||||||
|
skin,
|
||||||
|
hair,
|
||||||
|
eyes,
|
||||||
|
character_cast="",
|
||||||
|
):
|
||||||
|
result = build_character_slot_json(
|
||||||
|
subject_type=subject_type,
|
||||||
|
label=label,
|
||||||
|
age=age,
|
||||||
|
manual_age=manual_age,
|
||||||
|
ethnicity=ethnicity,
|
||||||
|
figure=figure,
|
||||||
|
body=body,
|
||||||
|
manual_body=manual_body,
|
||||||
|
body_phrase=body_phrase,
|
||||||
|
skin=skin,
|
||||||
|
hair=hair,
|
||||||
|
eyes=eyes,
|
||||||
|
enabled=enabled,
|
||||||
|
character_cast=character_cast or "",
|
||||||
|
)
|
||||||
|
return result["character_cast"], result["character_slot"], result["summary"], result["status"]
|
||||||
|
|
||||||
|
|
||||||
class SxCPCharacterProfileSave:
|
class SxCPCharacterProfileSave:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -821,6 +904,7 @@ class SxCPInstaOFPromptPair:
|
|||||||
"filter_config": ("STRING", {"default": "", "multiline": True}),
|
"filter_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||||
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"character_cast": ("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}),
|
||||||
},
|
},
|
||||||
@@ -854,6 +938,7 @@ class SxCPInstaOFPromptPair:
|
|||||||
filter_config="",
|
filter_config="",
|
||||||
camera_config="",
|
camera_config="",
|
||||||
character_profile="",
|
character_profile="",
|
||||||
|
character_cast="",
|
||||||
extra_positive="",
|
extra_positive="",
|
||||||
extra_negative="",
|
extra_negative="",
|
||||||
no_plus_women=False,
|
no_plus_women=False,
|
||||||
@@ -874,6 +959,7 @@ class SxCPInstaOFPromptPair:
|
|||||||
filter_config=filter_config or "",
|
filter_config=filter_config or "",
|
||||||
camera_config=camera_config or "",
|
camera_config=camera_config or "",
|
||||||
character_profile=character_profile or "",
|
character_profile=character_profile or "",
|
||||||
|
character_cast=character_cast or "",
|
||||||
extra_positive=extra_positive or "",
|
extra_positive=extra_positive or "",
|
||||||
extra_negative=extra_negative or "",
|
extra_negative=extra_negative or "",
|
||||||
)
|
)
|
||||||
@@ -899,6 +985,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SxCPGenerationProfile": SxCPGenerationProfile,
|
"SxCPGenerationProfile": SxCPGenerationProfile,
|
||||||
"SxCPAdvancedFilters": SxCPAdvancedFilters,
|
"SxCPAdvancedFilters": SxCPAdvancedFilters,
|
||||||
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
|
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
|
||||||
|
"SxCPCharacterSlot": SxCPCharacterSlot,
|
||||||
"SxCPCharacterProfileSave": SxCPCharacterProfileSave,
|
"SxCPCharacterProfileSave": SxCPCharacterProfileSave,
|
||||||
"SxCPCharacterProfileLoad": SxCPCharacterProfileLoad,
|
"SxCPCharacterProfileLoad": SxCPCharacterProfileLoad,
|
||||||
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
||||||
@@ -917,6 +1004,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SxCPGenerationProfile": "SxCP Generation Profile",
|
"SxCPGenerationProfile": "SxCP Generation Profile",
|
||||||
"SxCPAdvancedFilters": "SxCP Advanced Filters",
|
"SxCPAdvancedFilters": "SxCP Advanced Filters",
|
||||||
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
|
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
|
||||||
|
"SxCPCharacterSlot": "SxCP Character Slot",
|
||||||
"SxCPCharacterProfileSave": "SxCP Character Profile Save",
|
"SxCPCharacterProfileSave": "SxCP Character Profile Save",
|
||||||
"SxCPCharacterProfileLoad": "SxCP Character Profile Load",
|
"SxCPCharacterProfileLoad": "SxCP Character Profile Load",
|
||||||
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ PROMPT_FIELD_LABELS = (
|
|||||||
"Ages",
|
"Ages",
|
||||||
"Body types",
|
"Body types",
|
||||||
"Cast",
|
"Cast",
|
||||||
|
"Cast descriptors",
|
||||||
|
"Characters",
|
||||||
"Scene",
|
"Scene",
|
||||||
"Setting",
|
"Setting",
|
||||||
"Pose",
|
"Pose",
|
||||||
@@ -420,11 +422,14 @@ def _configured_cast_from_row(row: dict[str, Any], detail_level: str, keep_style
|
|||||||
scene = _row_value(row, "scene_text", ("Setting", "Scene"))
|
scene = _row_value(row, "scene_text", ("Setting", "Scene"))
|
||||||
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
||||||
composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
|
composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
|
||||||
|
cast_descriptor_text = _row_value(row, "cast_descriptor_text", ("Characters", "Cast descriptors"))
|
||||||
scene_kind = _row_value(row, "scene_kind") or "explicit adult sex scene"
|
scene_kind = _row_value(row, "scene_kind") or "explicit adult sex scene"
|
||||||
style = _row_value(row, "style") if keep_style else ""
|
style = _row_value(row, "style") if keep_style else ""
|
||||||
|
|
||||||
parts = [f"{_cap_first(subject)} {verb} shown as a consensual {scene_kind}, with all participants 21+"]
|
parts = [f"{_cap_first(subject)} {verb} shown as a consensual {scene_kind}"]
|
||||||
if cast:
|
if cast_descriptor_text:
|
||||||
|
parts.append(f"The named characters are {cast_descriptor_text}")
|
||||||
|
if cast and not cast_descriptor_text:
|
||||||
parts.append(f"The cast is {cast}")
|
parts.append(f"The cast is {cast}")
|
||||||
if role_graph:
|
if role_graph:
|
||||||
parts.append(role_graph)
|
parts.append(role_graph)
|
||||||
|
|||||||
@@ -340,7 +340,7 @@
|
|||||||
"cunnilingus with tongue on pussy",
|
"cunnilingus with tongue on pussy",
|
||||||
"face-sitting cunnilingus",
|
"face-sitting cunnilingus",
|
||||||
"sixty-nine oral sex",
|
"sixty-nine oral sex",
|
||||||
"blowjob while another partner watches",
|
{"text": "blowjob while another partner watches", "min_people": 3},
|
||||||
"pussy licking with thighs spread",
|
"pussy licking with thighs spread",
|
||||||
"cock sucking with visible saliva",
|
"cock sucking with visible saliva",
|
||||||
"oral sex with tongue and fingers",
|
"oral sex with tongue and fingers",
|
||||||
|
|||||||
+84
-13
@@ -14,6 +14,8 @@ PROMPT_FIELD_LABELS = (
|
|||||||
"Ages",
|
"Ages",
|
||||||
"Body types",
|
"Body types",
|
||||||
"Cast",
|
"Cast",
|
||||||
|
"Cast descriptors",
|
||||||
|
"Characters",
|
||||||
"Scene",
|
"Scene",
|
||||||
"Setting",
|
"Setting",
|
||||||
"Pose",
|
"Pose",
|
||||||
@@ -168,6 +170,65 @@ def _prompt_cast_descriptors(text: str) -> str:
|
|||||||
return _clean(text).replace("Woman A / primary creator:", "Woman A:")
|
return _clean(text).replace("Woman A / primary creator:", "Woman A:")
|
||||||
|
|
||||||
|
|
||||||
|
def _cast_entries(text: str) -> list[tuple[str, str]]:
|
||||||
|
text = _prompt_cast_descriptors(text)
|
||||||
|
entries: list[tuple[str, str]] = []
|
||||||
|
for part in text.split(";"):
|
||||||
|
part = _clean(part)
|
||||||
|
match = re.match(r"^((?:Woman|Man) [A-Z]):\s*(.+)$", part)
|
||||||
|
if match:
|
||||||
|
entries.append((match.group(1), _clean(match.group(2))))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _label_join(labels: list[str]) -> str:
|
||||||
|
labels = [_clean(label) for label in labels if _clean(label)]
|
||||||
|
if not labels:
|
||||||
|
return "the named adults"
|
||||||
|
if len(labels) == 1:
|
||||||
|
return labels[0]
|
||||||
|
if len(labels) == 2:
|
||||||
|
return f"{labels[0]} and {labels[1]}"
|
||||||
|
return f"{', '.join(labels[:-1])}, and {labels[-1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _cast_prose(text: str, central_label: str = "Woman A") -> tuple[str, list[str]]:
|
||||||
|
entries = _cast_entries(text)
|
||||||
|
if not entries:
|
||||||
|
return (f"{central_label} is {_clean(text)}" if _clean(text) else "", [])
|
||||||
|
labels = [label for label, _descriptor in entries]
|
||||||
|
count_phrase = "one named adult" if len(entries) == 1 else f"{len(entries)} named adults"
|
||||||
|
sentences = [f"The scene contains {count_phrase}."]
|
||||||
|
for label, descriptor in entries:
|
||||||
|
sentences.append(f"{label} is {descriptor}.")
|
||||||
|
if central_label in labels:
|
||||||
|
sentences.append(f"{central_label} is the central subject.")
|
||||||
|
return " ".join(sentences), labels
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
|
||||||
|
text = _clean(text)
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
if len(labels) < 3:
|
||||||
|
text = re.sub(r"\s*(?:while|as)\s+another partner watches\b", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\banother partner watches\b", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\bwhile blowjob\b", "during a blowjob", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\bfeaturing blowjob\b", "featuring a blowjob", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\s+,", ",", text)
|
||||||
|
text = re.sub(r"\s{2,}", " ", text).strip(" ,")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _natural_clothing_state(text: Any) -> str:
|
||||||
|
text = _clean(text)
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
text = re.sub(r"^Clothing state:\s*", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r";\s*softcore visual reference:\s*", ". Softcore visual reference: ", text, flags=re.IGNORECASE)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def _clean_age(age: Any) -> str:
|
def _clean_age(age: Any) -> str:
|
||||||
return _clean(age)
|
return _clean(age)
|
||||||
|
|
||||||
@@ -292,10 +353,17 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
|||||||
if subject_type == "configured_cast" or _clean(row.get("cast_summary")):
|
if subject_type == "configured_cast" or _clean(row.get("cast_summary")):
|
||||||
subject = _clean(row.get("subject_phrase") or primary or "adult sexual scene")
|
subject = _clean(row.get("subject_phrase") or primary or "adult sexual scene")
|
||||||
cast = _clean(row.get("cast_summary"))
|
cast = _clean(row.get("cast_summary"))
|
||||||
|
cast_descriptor_text = (
|
||||||
|
_clean(row.get("cast_descriptor_text"))
|
||||||
|
or _prompt_field(_clean(row.get("prompt")), "Characters")
|
||||||
|
or _prompt_field(_clean(row.get("prompt")), "Cast descriptors")
|
||||||
|
)
|
||||||
|
cast_prose, _cast_labels = _cast_prose(cast_descriptor_text)
|
||||||
role_graph = _clean(row.get("role_graph"))
|
role_graph = _clean(row.get("role_graph"))
|
||||||
parts = [
|
parts = [
|
||||||
f"A consensual explicit adult scene with {subject}, all participants 21+ and visibly adult",
|
f"A consensual explicit adult scene with {subject}",
|
||||||
f"The cast includes {cast}" if cast else "",
|
cast_prose,
|
||||||
|
f"The cast includes {cast}" if cast and not cast_prose else "",
|
||||||
role_graph,
|
role_graph,
|
||||||
f"The sexual action is {item}" if item else "",
|
f"The sexual action is {item}" if item else "",
|
||||||
f"The setting is {scene}" if scene else "",
|
f"The setting is {scene}" if scene else "",
|
||||||
@@ -380,9 +448,13 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
|||||||
if options.get("softcore_cast") == "same_as_hardcore"
|
if options.get("softcore_cast") == "same_as_hardcore"
|
||||||
else f"Woman A: {descriptor}"
|
else f"Woman A: {descriptor}"
|
||||||
)
|
)
|
||||||
|
soft_cast_prose, soft_labels = _cast_prose(soft_cast_descriptor_text)
|
||||||
|
hard_cast_prose, hard_labels = _cast_prose(cast_descriptor_text)
|
||||||
|
hard_item = _sanitize_scene_text_for_cast(hard.get("item"), hard_labels)
|
||||||
|
hard_role_graph = _sanitize_scene_text_for_cast(hard.get("role_graph"), hard_labels)
|
||||||
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
||||||
soft_cast_presence = (
|
soft_cast_presence = (
|
||||||
"Woman A and the listed partners are present together in a non-explicit teaser pose, with no sex act or genital contact"
|
f"{_label_join(soft_labels)} are together in a non-explicit teaser pose, with no sex act or genital contact"
|
||||||
if same_soft_cast
|
if same_soft_cast
|
||||||
else "The softcore version focuses on Woman A alone"
|
else "The softcore version focuses on Woman A alone"
|
||||||
)
|
)
|
||||||
@@ -396,12 +468,11 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
|||||||
partner_pose = ""
|
partner_pose = ""
|
||||||
|
|
||||||
soft_parts = [
|
soft_parts = [
|
||||||
f"Cast descriptors: {soft_cast_descriptor_text}" if same_soft_cast and soft_cast_descriptor_text else "",
|
f"Softcore {soft_level or 'creator'} Insta/OF image",
|
||||||
soft_cast_descriptor_text if not same_soft_cast and soft_cast_descriptor_text else "",
|
soft_cast_prose,
|
||||||
soft_cast_presence,
|
soft_cast_presence,
|
||||||
f"Partner softcore styling: {partner_outfit_text}" if partner_outfit_text else "",
|
partner_outfit_text,
|
||||||
f"Cast pose: {partner_pose}" if partner_pose else "",
|
f"The cast is {partner_pose}" if partner_pose else "",
|
||||||
f"shown in a {soft_level or 'softcore'} Insta/OF creator image",
|
|
||||||
f"wearing {soft.get('item')}" if soft.get("item") else "",
|
f"wearing {soft.get('item')}" if soft.get("item") else "",
|
||||||
f"{soft.get('pose')}" if soft.get("pose") else "",
|
f"{soft.get('pose')}" if soft.get("pose") else "",
|
||||||
f"with {soft.get('expression')}" if soft.get("expression") else "",
|
f"with {soft.get('expression')}" if soft.get("expression") else "",
|
||||||
@@ -411,11 +482,11 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
|||||||
soft_style if detail_level != "concise" else "",
|
soft_style if detail_level != "concise" else "",
|
||||||
]
|
]
|
||||||
hard_parts = [
|
hard_parts = [
|
||||||
f"{hard_level or 'hardcore'} scene with Woman A visually central",
|
f"{hard_level or 'hardcore'} scene",
|
||||||
f"Cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "",
|
hard_cast_prose,
|
||||||
_clean(row.get("hardcore_clothing_state")),
|
_natural_clothing_state(row.get("hardcore_clothing_state")),
|
||||||
_clean(hard.get("role_graph")),
|
hard_role_graph,
|
||||||
f"The sexual action is {hard.get('item')}" if hard.get("item") else "",
|
f"The explicit detail shows {hard_item}" if hard_item else "",
|
||||||
f"set in {hard_scene}" if hard_scene else "",
|
f"set in {hard_scene}" if hard_scene else "",
|
||||||
f"with {hard.get('expression')}" if hard.get("expression") else "",
|
f"with {hard.get('expression')}" if hard.get("expression") else "",
|
||||||
f"framed as {hard_composition}" if hard_composition else "",
|
f"framed as {hard_composition}" if hard_composition else "",
|
||||||
|
|||||||
+455
-11
@@ -76,6 +76,69 @@ ETHNICITY_FILTER_CHOICES = [
|
|||||||
"white_asian",
|
"white_asian",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CHARACTER_LABEL_CHOICES = [
|
||||||
|
"auto_chain",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"H",
|
||||||
|
"I",
|
||||||
|
"J",
|
||||||
|
"K",
|
||||||
|
"L",
|
||||||
|
]
|
||||||
|
CHARACTER_AGE_CHOICES = (
|
||||||
|
["random", "manual"]
|
||||||
|
+ [f"{age}-year-old adult" for age in range(21, 86)]
|
||||||
|
+ [
|
||||||
|
"late 20s adult",
|
||||||
|
"early 30s adult",
|
||||||
|
"mid 30s adult",
|
||||||
|
"late 30s adult",
|
||||||
|
"early 40s adult",
|
||||||
|
"mid 40s adult",
|
||||||
|
"late 40s adult",
|
||||||
|
"early 50s adult",
|
||||||
|
"mid 50s adult",
|
||||||
|
"late 50s adult",
|
||||||
|
"early 60s adult",
|
||||||
|
"mid 60s adult",
|
||||||
|
"late 60s adult",
|
||||||
|
"early 70s adult",
|
||||||
|
"mid 70s adult",
|
||||||
|
"late 70s adult",
|
||||||
|
"early 80s adult",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
CHARACTER_BODY_CHOICES = [
|
||||||
|
"random",
|
||||||
|
"manual",
|
||||||
|
"slim",
|
||||||
|
"petite adult",
|
||||||
|
"toned",
|
||||||
|
"athletic",
|
||||||
|
"average",
|
||||||
|
"curvy",
|
||||||
|
"soft curvy",
|
||||||
|
"curvy athletic",
|
||||||
|
"hourglass",
|
||||||
|
"slim busty",
|
||||||
|
"busty",
|
||||||
|
"busty curvy",
|
||||||
|
"voluptuous",
|
||||||
|
"plus-size",
|
||||||
|
"heavyset",
|
||||||
|
"fat",
|
||||||
|
"stocky",
|
||||||
|
"broad",
|
||||||
|
"muscular",
|
||||||
|
]
|
||||||
|
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
||||||
|
|
||||||
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||||
|
|
||||||
GENERIC_POSITIVE_SUFFIX = (
|
GENERIC_POSITIVE_SUFFIX = (
|
||||||
@@ -1221,6 +1284,26 @@ def ethnicity_choices() -> list[str]:
|
|||||||
return list(ETHNICITY_FILTER_CHOICES)
|
return list(ETHNICITY_FILTER_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def character_label_choices() -> list[str]:
|
||||||
|
return list(CHARACTER_LABEL_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def character_age_choices() -> list[str]:
|
||||||
|
return list(CHARACTER_AGE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def character_body_choices() -> list[str]:
|
||||||
|
return list(CHARACTER_BODY_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def character_ethnicity_choices() -> list[str]:
|
||||||
|
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def character_figure_choices() -> list[str]:
|
||||||
|
return ["random", "curvy", "balanced", "bombshell"]
|
||||||
|
|
||||||
|
|
||||||
def camera_detail_choices() -> list[str]:
|
def camera_detail_choices() -> list[str]:
|
||||||
return list(CAMERA_DETAIL_CHOICES)
|
return list(CAMERA_DETAIL_CHOICES)
|
||||||
|
|
||||||
@@ -1631,6 +1714,286 @@ def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[st
|
|||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _slot_value(value: Any) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text.lower() in CHARACTER_RANDOM_TOKENS:
|
||||||
|
return ""
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
||||||
|
choice = str(choice or "").strip()
|
||||||
|
manual_value = str(manual_value or "").strip()
|
||||||
|
if choice == "manual":
|
||||||
|
return manual_value or "random"
|
||||||
|
if choice.lower() in CHARACTER_RANDOM_TOKENS:
|
||||||
|
return "random"
|
||||||
|
return choice
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_slot_ethnicity(value: Any) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text.lower() in CHARACTER_RANDOM_TOKENS:
|
||||||
|
return "random"
|
||||||
|
if text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text:
|
||||||
|
return text
|
||||||
|
return "random"
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
if subject_type not in ("woman", "man"):
|
||||||
|
subject_type = "woman"
|
||||||
|
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
|
||||||
|
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
|
||||||
|
if label == "AUTO_CHAIN":
|
||||||
|
label = "auto_chain"
|
||||||
|
if label not in CHARACTER_LABEL_CHOICES:
|
||||||
|
label = "auto_chain"
|
||||||
|
|
||||||
|
age = _slot_manual_or_choice(str(slot.get("age") or "random"), str(slot.get("manual_age") or ""))
|
||||||
|
body = _slot_manual_or_choice(str(slot.get("body") or "random"), str(slot.get("manual_body") or ""))
|
||||||
|
figure = str(slot.get("figure") or "random").strip()
|
||||||
|
if figure not in character_figure_choices():
|
||||||
|
figure = "random"
|
||||||
|
|
||||||
|
normalized = {
|
||||||
|
"profile_type": "character_slot",
|
||||||
|
"subject_type": subject_type,
|
||||||
|
"label": label,
|
||||||
|
"age": age,
|
||||||
|
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
|
||||||
|
"figure": figure,
|
||||||
|
"body": body,
|
||||||
|
"body_phrase": _slot_value(slot.get("body_phrase")),
|
||||||
|
"skin": _slot_value(slot.get("skin")),
|
||||||
|
"hair": _slot_value(slot.get("hair")),
|
||||||
|
"eyes": _slot_value(slot.get("eyes")),
|
||||||
|
}
|
||||||
|
normalized["summary"] = _character_slot_summary(normalized)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
|
||||||
|
if not character_cast:
|
||||||
|
return []
|
||||||
|
if isinstance(character_cast, list):
|
||||||
|
raw = character_cast
|
||||||
|
elif isinstance(character_cast, dict):
|
||||||
|
raw = character_cast
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(character_cast))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
|
||||||
|
|
||||||
|
if isinstance(raw, list):
|
||||||
|
slots = raw
|
||||||
|
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
|
||||||
|
slots = raw["slots"]
|
||||||
|
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
|
||||||
|
slots = [raw]
|
||||||
|
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
|
||||||
|
slots = [raw]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
return [_normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
|
||||||
|
|
||||||
|
|
||||||
|
def _character_slot_summary(slot: dict[str, Any]) -> str:
|
||||||
|
subject = str(slot.get("subject_type") or "woman")
|
||||||
|
label = str(slot.get("label") or "auto_chain")
|
||||||
|
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
|
||||||
|
parts = [
|
||||||
|
subject,
|
||||||
|
label_text,
|
||||||
|
f"age={slot.get('age', 'random')}",
|
||||||
|
f"ethnicity={slot.get('ethnicity', 'random')}",
|
||||||
|
f"figure={slot.get('figure', 'random')}",
|
||||||
|
f"body={slot.get('body', 'random')}",
|
||||||
|
]
|
||||||
|
for key in ("body_phrase", "skin", "hair", "eyes"):
|
||||||
|
value = slot.get(key)
|
||||||
|
if value:
|
||||||
|
parts.append(f"{key}={value}")
|
||||||
|
return "; ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def build_character_slot_json(
|
||||||
|
subject_type: str = "woman",
|
||||||
|
label: str = "auto_chain",
|
||||||
|
age: str = "random",
|
||||||
|
manual_age: str = "",
|
||||||
|
ethnicity: str = "random",
|
||||||
|
figure: str = "random",
|
||||||
|
body: str = "random",
|
||||||
|
manual_body: str = "",
|
||||||
|
body_phrase: str = "",
|
||||||
|
skin: str = "",
|
||||||
|
hair: str = "",
|
||||||
|
eyes: str = "",
|
||||||
|
enabled: bool = True,
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||||
|
) -> dict[str, str]:
|
||||||
|
existing_slots = _parse_character_cast(character_cast)
|
||||||
|
slot = _normalize_character_slot(
|
||||||
|
{
|
||||||
|
"subject_type": subject_type,
|
||||||
|
"label": label,
|
||||||
|
"age": age,
|
||||||
|
"manual_age": manual_age,
|
||||||
|
"ethnicity": ethnicity,
|
||||||
|
"figure": figure,
|
||||||
|
"body": body,
|
||||||
|
"manual_body": manual_body,
|
||||||
|
"body_phrase": body_phrase,
|
||||||
|
"skin": skin,
|
||||||
|
"hair": hair,
|
||||||
|
"eyes": eyes,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
slots = existing_slots + ([slot] if enabled else [])
|
||||||
|
cast = {
|
||||||
|
"profile_type": "character_cast",
|
||||||
|
"version": 1,
|
||||||
|
"slots": slots,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
|
||||||
|
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
|
||||||
|
"summary": slot["summary"] if enabled else "disabled",
|
||||||
|
"status": f"{len(slots)} slot(s)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _slot_explicit_label(slot: dict[str, Any]) -> str:
|
||||||
|
label = str(slot.get("label") or "").strip().upper()
|
||||||
|
if label in CHARACTER_LABEL_CHOICES and label != "AUTO_CHAIN":
|
||||||
|
return label
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _character_slot_label_map(slots: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||||
|
label_map: dict[str, dict[str, Any]] = {}
|
||||||
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
for subject_type, prefix in (("woman", "Woman"), ("man", "Man")):
|
||||||
|
subject_slots = [slot for slot in slots if slot.get("subject_type") == subject_type]
|
||||||
|
auto_slots = [slot for slot in subject_slots if not _slot_explicit_label(slot)]
|
||||||
|
for index, slot in enumerate(reversed(auto_slots)):
|
||||||
|
if index >= len(letters):
|
||||||
|
break
|
||||||
|
label_map[f"{prefix} {letters[index]}"] = slot
|
||||||
|
for slot in subject_slots:
|
||||||
|
explicit = _slot_explicit_label(slot)
|
||||||
|
if explicit:
|
||||||
|
label_map[f"{prefix} {explicit}"] = slot
|
||||||
|
return label_map
|
||||||
|
|
||||||
|
|
||||||
|
def _context_from_character_slot(
|
||||||
|
rng: random.Random,
|
||||||
|
slot: dict[str, Any],
|
||||||
|
subject_type: str,
|
||||||
|
ethnicity: str,
|
||||||
|
figure: str,
|
||||||
|
no_plus_women: bool,
|
||||||
|
no_black: bool,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
slot_ethnicity = _slot_value(slot.get("ethnicity"))
|
||||||
|
slot_figure = _slot_value(slot.get("figure"))
|
||||||
|
slot_body = _slot_value(slot.get("body"))
|
||||||
|
effective_ethnicity = slot_ethnicity or ethnicity
|
||||||
|
effective_figure = slot_figure if slot_figure in ("curvy", "balanced", "bombshell") else figure
|
||||||
|
effective_no_plus = bool(no_plus_women) and not slot_body
|
||||||
|
effective_no_black = bool(no_black) and not slot_ethnicity
|
||||||
|
context = _appearance_for_subject(
|
||||||
|
rng,
|
||||||
|
subject_type,
|
||||||
|
effective_ethnicity,
|
||||||
|
effective_figure,
|
||||||
|
effective_no_plus,
|
||||||
|
effective_no_black,
|
||||||
|
)
|
||||||
|
|
||||||
|
age = _slot_value(slot.get("age"))
|
||||||
|
body_phrase = _slot_value(slot.get("body_phrase"))
|
||||||
|
if age:
|
||||||
|
context["age"] = age
|
||||||
|
if slot_body:
|
||||||
|
context["body"] = slot_body
|
||||||
|
if subject_type == "woman":
|
||||||
|
context["body_phrase"] = _body_phrase(slot_body, context.get("figure", ""))
|
||||||
|
else:
|
||||||
|
context["body_phrase"] = f"{slot_body} figure"
|
||||||
|
if body_phrase:
|
||||||
|
context["body_phrase"] = body_phrase
|
||||||
|
for key in ("skin", "hair", "eyes"):
|
||||||
|
value = _slot_value(slot.get(key))
|
||||||
|
if value:
|
||||||
|
context[key] = value
|
||||||
|
context["subject_type"] = subject_type
|
||||||
|
context["subject"] = subject_type
|
||||||
|
context["subject_phrase"] = subject_type
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _character_context_for_label(
|
||||||
|
label: str,
|
||||||
|
label_map: dict[str, dict[str, Any]],
|
||||||
|
rng: random.Random,
|
||||||
|
ethnicity: str,
|
||||||
|
figure: str,
|
||||||
|
no_plus_women: bool,
|
||||||
|
no_black: bool,
|
||||||
|
) -> tuple[dict[str, str], dict[str, Any] | None]:
|
||||||
|
subject_type = "man" if label.startswith("Man ") else "woman"
|
||||||
|
slot = label_map.get(label)
|
||||||
|
if slot:
|
||||||
|
return _context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
|
||||||
|
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
for key in ("subject_type", "subject", "subject_phrase", "age", "body", "body_phrase", "skin", "hair", "eyes", "figure"):
|
||||||
|
value = context.get(key)
|
||||||
|
if value:
|
||||||
|
row[key] = value
|
||||||
|
if context.get("age"):
|
||||||
|
row["age_band"] = context["age"]
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _cast_descriptor_entries(
|
||||||
|
seed_config: dict[str, int],
|
||||||
|
seed: int,
|
||||||
|
row_number: int,
|
||||||
|
ethnicity: str,
|
||||||
|
figure: str,
|
||||||
|
no_plus_women: bool,
|
||||||
|
no_black: bool,
|
||||||
|
women_count: int,
|
||||||
|
men_count: int,
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||||
|
primary_descriptor: str = "",
|
||||||
|
) -> tuple[list[str], list[dict[str, Any]]]:
|
||||||
|
slots = _parse_character_cast(character_cast)
|
||||||
|
label_map = _character_slot_label_map(slots)
|
||||||
|
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
|
||||||
|
descriptors: list[str] = []
|
||||||
|
for index in range(max(0, women_count)):
|
||||||
|
label = f"Woman {chr(ord('A') + index)}"
|
||||||
|
if index == 0 and primary_descriptor:
|
||||||
|
descriptors.append(f"Woman A / primary creator: {primary_descriptor}")
|
||||||
|
continue
|
||||||
|
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
|
||||||
|
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
|
||||||
|
for index in range(max(0, men_count)):
|
||||||
|
label = f"Man {chr(ord('A') + index)}"
|
||||||
|
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
|
||||||
|
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
|
||||||
|
return descriptors, slots
|
||||||
|
|
||||||
|
|
||||||
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
row = _load_json_object(metadata_json, "metadata_json")
|
row = _load_json_object(metadata_json, "metadata_json")
|
||||||
if isinstance(row.get("softcore_row"), dict):
|
if isinstance(row.get("softcore_row"), dict):
|
||||||
@@ -2432,6 +2795,7 @@ def _build_custom_row(
|
|||||||
seed_config: dict[str, int],
|
seed_config: dict[str, int],
|
||||||
expression_intensity: float,
|
expression_intensity: float,
|
||||||
character_profile: str | dict[str, Any] | None = None,
|
character_profile: str | dict[str, Any] | None = None,
|
||||||
|
character_cast: str | dict[str, Any] | list[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)
|
||||||
@@ -2469,9 +2833,46 @@ 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)
|
||||||
|
character_slots = _parse_character_cast(character_cast)
|
||||||
|
character_slot_map = _character_slot_label_map(character_slots)
|
||||||
|
applied_slot: dict[str, Any] = {}
|
||||||
|
slot_status = "none"
|
||||||
|
if context.get("subject_type") in ("woman", "man"):
|
||||||
|
slot_label = "Woman A" if context["subject_type"] == "woman" else "Man A"
|
||||||
|
if slot_label in character_slot_map:
|
||||||
|
context, applied_slot = _character_context_for_label(
|
||||||
|
slot_label,
|
||||||
|
character_slot_map,
|
||||||
|
person_rng,
|
||||||
|
ethnicity,
|
||||||
|
figure,
|
||||||
|
no_plus_women,
|
||||||
|
no_black,
|
||||||
|
)
|
||||||
|
slot_status = f"applied:{slot_label}"
|
||||||
|
applied_profile, profile_status = {}, "skipped_character_slot"
|
||||||
|
else:
|
||||||
|
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
||||||
|
else:
|
||||||
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
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)
|
||||||
|
cast_descriptors: list[str] = []
|
||||||
|
cast_descriptor_text = ""
|
||||||
|
if subject_type == "configured_cast" and character_slots:
|
||||||
|
cast_descriptors, _descriptor_slots = _cast_descriptor_entries(
|
||||||
|
seed_config,
|
||||||
|
seed,
|
||||||
|
row_number,
|
||||||
|
ethnicity,
|
||||||
|
figure,
|
||||||
|
no_plus_women,
|
||||||
|
no_black,
|
||||||
|
women_count,
|
||||||
|
men_count,
|
||||||
|
character_slots,
|
||||||
|
)
|
||||||
|
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
|
||||||
|
|
||||||
scene_slug, scene = _choose_pair(scene_rng, _compatible_entries(_scene_pool(category, subcategory, item, subject_type), women_count, men_count))
|
scene_slug, scene = _choose_pair(scene_rng, _compatible_entries(_scene_pool(category, subcategory, item, subject_type), women_count, men_count))
|
||||||
pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text(
|
pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text(
|
||||||
@@ -2523,6 +2924,7 @@ def _build_custom_row(
|
|||||||
"composition": composition,
|
"composition": composition,
|
||||||
"composition_prompt": _composition_prompt(composition),
|
"composition_prompt": _composition_prompt(composition),
|
||||||
"role_graph": role_graph,
|
"role_graph": role_graph,
|
||||||
|
"cast_descriptors": cast_descriptor_text,
|
||||||
"positive_suffix": positive_suffix,
|
"positive_suffix": positive_suffix,
|
||||||
"negative_prompt": negative_prompt,
|
"negative_prompt": negative_prompt,
|
||||||
}
|
}
|
||||||
@@ -2550,7 +2952,11 @@ def _build_custom_row(
|
|||||||
)
|
)
|
||||||
|
|
||||||
prompt = _format(template, context)
|
prompt = _format(template, context)
|
||||||
|
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in template:
|
||||||
|
prompt = _insert_positive_directive(prompt, f"Characters: {cast_descriptor_text}.")
|
||||||
caption = _format(caption_template, context)
|
caption = _format(caption_template, context)
|
||||||
|
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in caption_template:
|
||||||
|
caption = f"{caption.rstrip()}, {cast_descriptor_text}"
|
||||||
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
|
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
|
||||||
index = start_index + row_number - 1
|
index = start_index + row_number - 1
|
||||||
row = g.row_base(index, batch, context["subject"], context["age"], context["body"], scene_slug, composition)
|
row = g.row_base(index, batch, context["subject"], context["age"], context["body"], scene_slug, composition)
|
||||||
@@ -2582,6 +2988,8 @@ def _build_custom_row(
|
|||||||
"content_seed_axis": content_axis,
|
"content_seed_axis": content_axis,
|
||||||
"role_graph": role_graph,
|
"role_graph": role_graph,
|
||||||
"cast_summary": context.get("cast_summary", ""),
|
"cast_summary": context.get("cast_summary", ""),
|
||||||
|
"cast_descriptors": cast_descriptors,
|
||||||
|
"cast_descriptor_text": cast_descriptor_text,
|
||||||
"scene_kind": context.get("scene_kind", ""),
|
"scene_kind": context.get("scene_kind", ""),
|
||||||
"women_count": context.get("women_count", ""),
|
"women_count": context.get("women_count", ""),
|
||||||
"men_count": context.get("men_count", ""),
|
"men_count": context.get("men_count", ""),
|
||||||
@@ -2589,6 +2997,9 @@ def _build_custom_row(
|
|||||||
"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": applied_profile,
|
||||||
"character_profile_status": profile_status,
|
"character_profile_status": profile_status,
|
||||||
|
"character_slot": applied_slot,
|
||||||
|
"character_slot_status": slot_status,
|
||||||
|
"character_cast_slots": character_slots,
|
||||||
"source": "json_category",
|
"source": "json_category",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -2622,6 +3033,7 @@ def build_prompt(
|
|||||||
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,
|
character_profile: str | dict[str, Any] | None = None,
|
||||||
|
character_cast: str | dict[str, Any] | list[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))
|
||||||
@@ -2686,6 +3098,7 @@ def build_prompt(
|
|||||||
parsed_seed_config,
|
parsed_seed_config,
|
||||||
expression_intensity,
|
expression_intensity,
|
||||||
character_profile,
|
character_profile,
|
||||||
|
character_cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
if extra_positive.strip():
|
if extra_positive.strip():
|
||||||
@@ -2710,6 +3123,7 @@ def build_prompt_from_configs(
|
|||||||
seed_config: str | dict[str, Any] | None = "",
|
seed_config: str | dict[str, Any] | None = "",
|
||||||
camera_config: str | dict[str, Any] | None = "",
|
camera_config: str | dict[str, Any] | None = "",
|
||||||
character_profile: str | dict[str, Any] | None = "",
|
character_profile: str | dict[str, Any] | None = "",
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||||
extra_positive: str = "",
|
extra_positive: str = "",
|
||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -2742,6 +3156,7 @@ def build_prompt_from_configs(
|
|||||||
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 "",
|
character_profile=character_profile or "",
|
||||||
|
character_cast=character_cast or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -3060,17 +3475,21 @@ def _insta_of_cast_descriptors(
|
|||||||
no_black: bool,
|
no_black: bool,
|
||||||
women_count: int,
|
women_count: int,
|
||||||
men_count: int,
|
men_count: int,
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
descriptors = [f"Woman A / primary creator: {primary_descriptor}"]
|
descriptors, _slots = _cast_descriptor_entries(
|
||||||
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
|
seed_config,
|
||||||
for index in range(max(0, women_count - 1)):
|
seed,
|
||||||
label = chr(ord("B") + index)
|
row_number,
|
||||||
context = _appearance_for_subject(rng, "woman", ethnicity, figure, no_plus_women, no_black)
|
ethnicity,
|
||||||
descriptors.append(f"Woman {label}: {_insta_of_descriptor_from_context(context)}")
|
figure,
|
||||||
for index in range(max(0, men_count)):
|
no_plus_women,
|
||||||
label = chr(ord("A") + index)
|
no_black,
|
||||||
context = _appearance_for_subject(rng, "man", ethnicity, figure, no_plus_women, no_black)
|
women_count,
|
||||||
descriptors.append(f"Man {label}: {_insta_of_descriptor_from_context(context)}")
|
men_count,
|
||||||
|
character_cast,
|
||||||
|
primary_descriptor=primary_descriptor,
|
||||||
|
)
|
||||||
return descriptors
|
return descriptors
|
||||||
|
|
||||||
|
|
||||||
@@ -3164,6 +3583,7 @@ def build_insta_of_pair(
|
|||||||
filter_config: str | dict[str, Any] | None = None,
|
filter_config: 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 = "",
|
character_profile: str | dict[str, Any] | None = "",
|
||||||
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||||
extra_positive: str = "",
|
extra_positive: str = "",
|
||||||
extra_negative: str = "",
|
extra_negative: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -3177,9 +3597,24 @@ def build_insta_of_pair(
|
|||||||
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
|
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
|
||||||
active_trigger = trigger.strip() or g.TRIGGER
|
active_trigger = trigger.strip() or g.TRIGGER
|
||||||
parsed_seed_config = _parse_seed_config(seed_config)
|
parsed_seed_config = _parse_seed_config(seed_config)
|
||||||
|
character_slots = _parse_character_cast(character_cast)
|
||||||
|
character_slot_map = _character_slot_label_map(character_slots)
|
||||||
softcore_level_key = str(options["softcore_level"])
|
softcore_level_key = str(options["softcore_level"])
|
||||||
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
|
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
|
||||||
soft_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 311)
|
soft_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 311)
|
||||||
|
soft_person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
|
||||||
|
primary_slot_context = None
|
||||||
|
primary_slot = character_slot_map.get("Woman A")
|
||||||
|
if primary_slot:
|
||||||
|
primary_slot_context = _context_from_character_slot(
|
||||||
|
soft_person_rng,
|
||||||
|
primary_slot,
|
||||||
|
"woman",
|
||||||
|
ethnicity,
|
||||||
|
figure,
|
||||||
|
no_plus_women,
|
||||||
|
no_black,
|
||||||
|
)
|
||||||
|
|
||||||
soft_row = build_prompt(
|
soft_row = build_prompt(
|
||||||
category=soft_category,
|
category=soft_category,
|
||||||
@@ -3204,8 +3639,13 @@ 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 "",
|
character_profile="" if primary_slot else character_profile or "",
|
||||||
|
character_cast="",
|
||||||
)
|
)
|
||||||
|
if primary_slot_context:
|
||||||
|
soft_row = _apply_character_context_to_row(soft_row, primary_slot_context)
|
||||||
|
soft_row["character_slot"] = primary_slot
|
||||||
|
soft_row["character_slot_status"] = "applied:Woman A"
|
||||||
soft_row["item"] = _insta_of_softcore_outfit(soft_content_rng, softcore_level_key)
|
soft_row["item"] = _insta_of_softcore_outfit(soft_content_rng, softcore_level_key)
|
||||||
soft_row["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key)
|
soft_row["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key)
|
||||||
soft_row["item_label"] = "Insta/OF softcore outfit"
|
soft_row["item_label"] = "Insta/OF softcore outfit"
|
||||||
@@ -3234,6 +3674,7 @@ def build_insta_of_pair(
|
|||||||
women_count=hard_women_count,
|
women_count=hard_women_count,
|
||||||
men_count=hard_men_count,
|
men_count=hard_men_count,
|
||||||
expression_intensity=options["hardcore_expression_intensity"],
|
expression_intensity=options["hardcore_expression_intensity"],
|
||||||
|
character_cast=character_cast or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
descriptor = _insta_of_descriptor(soft_row)
|
descriptor = _insta_of_descriptor(soft_row)
|
||||||
@@ -3248,6 +3689,7 @@ def build_insta_of_pair(
|
|||||||
no_black,
|
no_black,
|
||||||
hard_women_count,
|
hard_women_count,
|
||||||
hard_men_count,
|
hard_men_count,
|
||||||
|
character_slots,
|
||||||
)
|
)
|
||||||
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
|
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
|
||||||
soft_cast_descriptor_text = (
|
soft_cast_descriptor_text = (
|
||||||
@@ -3382,6 +3824,8 @@ def build_insta_of_pair(
|
|||||||
"hardcore_row": hard_row,
|
"hardcore_row": hard_row,
|
||||||
"hardcore_women_count": hard_women_count,
|
"hardcore_women_count": hard_women_count,
|
||||||
"hardcore_men_count": hard_men_count,
|
"hardcore_men_count": hard_men_count,
|
||||||
|
"character_cast_slots": character_slots,
|
||||||
|
"character_slot_labels": sorted(character_slot_map),
|
||||||
"softcore_camera_config": soft_camera_config,
|
"softcore_camera_config": soft_camera_config,
|
||||||
"hardcore_camera_config": hard_camera_config,
|
"hardcore_camera_config": hard_camera_config,
|
||||||
"softcore_camera_directive": soft_camera_directive,
|
"softcore_camera_directive": soft_camera_directive,
|
||||||
|
|||||||
Reference in New Issue
Block a user