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 Advanced Filters`
|
||||
- `prompt_builder / SxCP Prompt Builder From Configs`
|
||||
- `prompt_builder / SxCP Character Slot`
|
||||
- `prompt_builder / SxCP Character Profile Save`
|
||||
- `prompt_builder / SxCP Character Profile Load`
|
||||
- `prompt_builder / SxCP Caption Naturalizer`
|
||||
@@ -51,7 +52,8 @@ node. For cleaner workflows, use the split nodes:
|
||||
The practical compact workflow is:
|
||||
|
||||
`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`.
|
||||
|
||||
An importable default workflow is included at
|
||||
@@ -68,6 +70,29 @@ as one long chain:
|
||||
|
||||
## 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
|
||||
`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
|
||||
@@ -173,6 +198,9 @@ Important behavior:
|
||||
`minimal` omits most style text.
|
||||
- For Insta/OF paired metadata, the node returns both `krea_softcore_prompt` and
|
||||
`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:
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
- `softcore_prompt`
|
||||
|
||||
+88
@@ -7,6 +7,7 @@ try:
|
||||
build_camera_config_json,
|
||||
build_cast_config_json,
|
||||
build_category_config_json,
|
||||
build_character_slot_json,
|
||||
build_character_profile_json,
|
||||
build_filter_config_json,
|
||||
build_generation_profile_json,
|
||||
@@ -28,6 +29,11 @@ try:
|
||||
cast_preset_choices,
|
||||
category_preset_choices,
|
||||
category_choices,
|
||||
character_age_choices,
|
||||
character_body_choices,
|
||||
character_ethnicity_choices,
|
||||
character_figure_choices,
|
||||
character_label_choices,
|
||||
character_profile_choices,
|
||||
ethnicity_choices,
|
||||
generation_profile_choices,
|
||||
@@ -41,6 +47,7 @@ except ImportError:
|
||||
build_camera_config_json,
|
||||
build_cast_config_json,
|
||||
build_category_config_json,
|
||||
build_character_slot_json,
|
||||
build_character_profile_json,
|
||||
build_filter_config_json,
|
||||
build_generation_profile_json,
|
||||
@@ -62,6 +69,11 @@ except ImportError:
|
||||
cast_preset_choices,
|
||||
category_preset_choices,
|
||||
category_choices,
|
||||
character_age_choices,
|
||||
character_body_choices,
|
||||
character_ethnicity_choices,
|
||||
character_figure_choices,
|
||||
character_label_choices,
|
||||
character_profile_choices,
|
||||
ethnicity_choices,
|
||||
generation_profile_choices,
|
||||
@@ -99,6 +111,7 @@ class SxCPPromptBuilder:
|
||||
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
||||
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
@@ -131,6 +144,7 @@ class SxCPPromptBuilder:
|
||||
seed_config="",
|
||||
camera_config="",
|
||||
character_profile="",
|
||||
character_cast="",
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
no_plus_women=False,
|
||||
@@ -161,6 +175,7 @@ class SxCPPromptBuilder:
|
||||
seed_config=seed_config or "",
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
)
|
||||
return (
|
||||
row["prompt"],
|
||||
@@ -474,6 +489,7 @@ class SxCPPromptBuilderFromConfigs:
|
||||
"seed_config": ("STRING", {"default": "", "multiline": True}),
|
||||
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
@@ -496,6 +512,7 @@ class SxCPPromptBuilderFromConfigs:
|
||||
seed_config="",
|
||||
camera_config="",
|
||||
character_profile="",
|
||||
character_cast="",
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
):
|
||||
@@ -510,6 +527,7 @@ class SxCPPromptBuilderFromConfigs:
|
||||
seed_config=seed_config or "",
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
extra_positive=extra_positive 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:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -821,6 +904,7 @@ class SxCPInstaOFPromptPair:
|
||||
"filter_config": ("STRING", {"default": "", "multiline": True}),
|
||||
"camera_config": ("STRING", {"default": "", "multiline": True}),
|
||||
"character_profile": ("STRING", {"default": "", "multiline": True}),
|
||||
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
@@ -854,6 +938,7 @@ class SxCPInstaOFPromptPair:
|
||||
filter_config="",
|
||||
camera_config="",
|
||||
character_profile="",
|
||||
character_cast="",
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
no_plus_women=False,
|
||||
@@ -874,6 +959,7 @@ class SxCPInstaOFPromptPair:
|
||||
filter_config=filter_config or "",
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
extra_positive=extra_positive or "",
|
||||
extra_negative=extra_negative or "",
|
||||
)
|
||||
@@ -899,6 +985,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SxCPGenerationProfile": SxCPGenerationProfile,
|
||||
"SxCPAdvancedFilters": SxCPAdvancedFilters,
|
||||
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
|
||||
"SxCPCharacterSlot": SxCPCharacterSlot,
|
||||
"SxCPCharacterProfileSave": SxCPCharacterProfileSave,
|
||||
"SxCPCharacterProfileLoad": SxCPCharacterProfileLoad,
|
||||
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
||||
@@ -917,6 +1004,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SxCPGenerationProfile": "SxCP Generation Profile",
|
||||
"SxCPAdvancedFilters": "SxCP Advanced Filters",
|
||||
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
|
||||
"SxCPCharacterSlot": "SxCP Character Slot",
|
||||
"SxCPCharacterProfileSave": "SxCP Character Profile Save",
|
||||
"SxCPCharacterProfileLoad": "SxCP Character Profile Load",
|
||||
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
||||
|
||||
@@ -17,6 +17,8 @@ PROMPT_FIELD_LABELS = (
|
||||
"Ages",
|
||||
"Body types",
|
||||
"Cast",
|
||||
"Cast descriptors",
|
||||
"Characters",
|
||||
"Scene",
|
||||
"Setting",
|
||||
"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"))
|
||||
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
||||
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"
|
||||
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+"]
|
||||
if cast:
|
||||
parts = [f"{_cap_first(subject)} {verb} shown as a consensual {scene_kind}"]
|
||||
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}")
|
||||
if role_graph:
|
||||
parts.append(role_graph)
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
"cunnilingus with tongue on pussy",
|
||||
"face-sitting cunnilingus",
|
||||
"sixty-nine oral sex",
|
||||
"blowjob while another partner watches",
|
||||
{"text": "blowjob while another partner watches", "min_people": 3},
|
||||
"pussy licking with thighs spread",
|
||||
"cock sucking with visible saliva",
|
||||
"oral sex with tongue and fingers",
|
||||
|
||||
+84
-13
@@ -14,6 +14,8 @@ PROMPT_FIELD_LABELS = (
|
||||
"Ages",
|
||||
"Body types",
|
||||
"Cast",
|
||||
"Cast descriptors",
|
||||
"Characters",
|
||||
"Scene",
|
||||
"Setting",
|
||||
"Pose",
|
||||
@@ -168,6 +170,65 @@ def _prompt_cast_descriptors(text: str) -> str:
|
||||
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:
|
||||
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")):
|
||||
subject = _clean(row.get("subject_phrase") or primary or "adult sexual scene")
|
||||
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"))
|
||||
parts = [
|
||||
f"A consensual explicit adult scene with {subject}, all participants 21+ and visibly adult",
|
||||
f"The cast includes {cast}" if cast else "",
|
||||
f"A consensual explicit adult scene with {subject}",
|
||||
cast_prose,
|
||||
f"The cast includes {cast}" if cast and not cast_prose else "",
|
||||
role_graph,
|
||||
f"The sexual action is {item}" if item 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"
|
||||
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"
|
||||
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
|
||||
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 = ""
|
||||
|
||||
soft_parts = [
|
||||
f"Cast descriptors: {soft_cast_descriptor_text}" if same_soft_cast and soft_cast_descriptor_text else "",
|
||||
soft_cast_descriptor_text if not same_soft_cast and soft_cast_descriptor_text else "",
|
||||
f"Softcore {soft_level or 'creator'} Insta/OF image",
|
||||
soft_cast_prose,
|
||||
soft_cast_presence,
|
||||
f"Partner softcore styling: {partner_outfit_text}" if partner_outfit_text else "",
|
||||
f"Cast pose: {partner_pose}" if partner_pose else "",
|
||||
f"shown in a {soft_level or 'softcore'} Insta/OF creator image",
|
||||
partner_outfit_text,
|
||||
f"The cast is {partner_pose}" if partner_pose else "",
|
||||
f"wearing {soft.get('item')}" if soft.get("item") else "",
|
||||
f"{soft.get('pose')}" if soft.get("pose") 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 "",
|
||||
]
|
||||
hard_parts = [
|
||||
f"{hard_level or 'hardcore'} scene with Woman A visually central",
|
||||
f"Cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "",
|
||||
_clean(row.get("hardcore_clothing_state")),
|
||||
_clean(hard.get("role_graph")),
|
||||
f"The sexual action is {hard.get('item')}" if hard.get("item") else "",
|
||||
f"{hard_level or 'hardcore'} scene",
|
||||
hard_cast_prose,
|
||||
_natural_clothing_state(row.get("hardcore_clothing_state")),
|
||||
hard_role_graph,
|
||||
f"The explicit detail shows {hard_item}" if hard_item else "",
|
||||
f"set in {hard_scene}" if hard_scene else "",
|
||||
f"with {hard.get('expression')}" if hard.get("expression") else "",
|
||||
f"framed as {hard_composition}" if hard_composition else "",
|
||||
|
||||
+456
-12
@@ -76,6 +76,69 @@ ETHNICITY_FILTER_CHOICES = [
|
||||
"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"]
|
||||
|
||||
GENERIC_POSITIVE_SUFFIX = (
|
||||
@@ -1221,6 +1284,26 @@ def ethnicity_choices() -> list[str]:
|
||||
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]:
|
||||
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
|
||||
|
||||
|
||||
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]:
|
||||
row = _load_json_object(metadata_json, "metadata_json")
|
||||
if isinstance(row.get("softcore_row"), dict):
|
||||
@@ -2432,6 +2795,7 @@ def _build_custom_row(
|
||||
seed_config: dict[str, int],
|
||||
expression_intensity: float,
|
||||
character_profile: str | dict[str, Any] | None = None,
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
categories = load_category_library()
|
||||
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)
|
||||
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, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
||||
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)
|
||||
subject_type = context["subject_type"]
|
||||
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))
|
||||
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_prompt": _composition_prompt(composition),
|
||||
"role_graph": role_graph,
|
||||
"cast_descriptors": cast_descriptor_text,
|
||||
"positive_suffix": positive_suffix,
|
||||
"negative_prompt": negative_prompt,
|
||||
}
|
||||
@@ -2550,7 +2952,11 @@ def _build_custom_row(
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
index = start_index + row_number - 1
|
||||
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,
|
||||
"role_graph": role_graph,
|
||||
"cast_summary": context.get("cast_summary", ""),
|
||||
"cast_descriptors": cast_descriptors,
|
||||
"cast_descriptor_text": cast_descriptor_text,
|
||||
"scene_kind": context.get("scene_kind", ""),
|
||||
"women_count": context.get("women_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 {},
|
||||
"character_profile": applied_profile,
|
||||
"character_profile_status": profile_status,
|
||||
"character_slot": applied_slot,
|
||||
"character_slot_status": slot_status,
|
||||
"character_cast_slots": character_slots,
|
||||
"source": "json_category",
|
||||
}
|
||||
)
|
||||
@@ -2622,6 +3033,7 @@ def build_prompt(
|
||||
camera_config: str | dict[str, Any] | None = None,
|
||||
expression_intensity: float = 0.5,
|
||||
character_profile: str | dict[str, Any] | None = None,
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
apply_pool_extensions()
|
||||
row_number = max(1, int(row_number))
|
||||
@@ -2686,6 +3098,7 @@ def build_prompt(
|
||||
parsed_seed_config,
|
||||
expression_intensity,
|
||||
character_profile,
|
||||
character_cast,
|
||||
)
|
||||
|
||||
if extra_positive.strip():
|
||||
@@ -2710,6 +3123,7 @@ def build_prompt_from_configs(
|
||||
seed_config: str | dict[str, Any] | None = "",
|
||||
camera_config: 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_negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
@@ -2742,6 +3156,7 @@ def build_prompt_from_configs(
|
||||
seed_config=seed_config or "",
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -3060,17 +3475,21 @@ def _insta_of_cast_descriptors(
|
||||
no_black: bool,
|
||||
women_count: int,
|
||||
men_count: int,
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||
) -> list[str]:
|
||||
descriptors = [f"Woman A / primary creator: {primary_descriptor}"]
|
||||
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
|
||||
for index in range(max(0, women_count - 1)):
|
||||
label = chr(ord("B") + index)
|
||||
context = _appearance_for_subject(rng, "woman", ethnicity, figure, no_plus_women, no_black)
|
||||
descriptors.append(f"Woman {label}: {_insta_of_descriptor_from_context(context)}")
|
||||
for index in range(max(0, men_count)):
|
||||
label = chr(ord("A") + index)
|
||||
context = _appearance_for_subject(rng, "man", ethnicity, figure, no_plus_women, no_black)
|
||||
descriptors.append(f"Man {label}: {_insta_of_descriptor_from_context(context)}")
|
||||
descriptors, _slots = _cast_descriptor_entries(
|
||||
seed_config,
|
||||
seed,
|
||||
row_number,
|
||||
ethnicity,
|
||||
figure,
|
||||
no_plus_women,
|
||||
no_black,
|
||||
women_count,
|
||||
men_count,
|
||||
character_cast,
|
||||
primary_descriptor=primary_descriptor,
|
||||
)
|
||||
return descriptors
|
||||
|
||||
|
||||
@@ -3164,6 +3583,7 @@ def build_insta_of_pair(
|
||||
filter_config: str | dict[str, Any] | None = None,
|
||||
camera_config: str | dict[str, Any] | None = None,
|
||||
character_profile: str | dict[str, Any] | None = "",
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||
extra_positive: str = "",
|
||||
extra_negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
@@ -3177,9 +3597,24 @@ def build_insta_of_pair(
|
||||
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
|
||||
active_trigger = trigger.strip() or g.TRIGGER
|
||||
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"])
|
||||
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_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(
|
||||
category=soft_category,
|
||||
@@ -3204,8 +3639,13 @@ def build_insta_of_pair(
|
||||
women_count=1,
|
||||
men_count=0,
|
||||
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["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key)
|
||||
soft_row["item_label"] = "Insta/OF softcore outfit"
|
||||
@@ -3234,6 +3674,7 @@ def build_insta_of_pair(
|
||||
women_count=hard_women_count,
|
||||
men_count=hard_men_count,
|
||||
expression_intensity=options["hardcore_expression_intensity"],
|
||||
character_cast=character_cast or "",
|
||||
)
|
||||
|
||||
descriptor = _insta_of_descriptor(soft_row)
|
||||
@@ -3248,6 +3689,7 @@ def build_insta_of_pair(
|
||||
no_black,
|
||||
hard_women_count,
|
||||
hard_men_count,
|
||||
character_slots,
|
||||
)
|
||||
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
|
||||
soft_cast_descriptor_text = (
|
||||
@@ -3382,6 +3824,8 @@ def build_insta_of_pair(
|
||||
"hardcore_row": hard_row,
|
||||
"hardcore_women_count": hard_women_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,
|
||||
"hardcore_camera_config": hard_camera_config,
|
||||
"softcore_camera_directive": soft_camera_directive,
|
||||
|
||||
Reference in New Issue
Block a user