Improve paired cast continuity and wording

This commit is contained in:
2026-06-24 10:57:46 +02:00
parent a1c6dc2391
commit 51d351679f
5 changed files with 184 additions and 8 deletions
+12
View File
@@ -52,6 +52,12 @@ more natural language. Connect the prompt builder's `metadata_json` output to
`prompt`; in that case the node falls back to prompt-label parsing or comma-tag
cleanup.
When connected to `SxCP Insta/OF Prompt Pair` metadata, the naturalizer emits a
single combined natural caption with the shared descriptor plus separate
softcore and hardcore version descriptions. It uses the final selected
expression and composition from the generated rows, including any expression
pool and intensity settings.
Naturalizer controls:
- `input_hint`: `auto`, `metadata_json`, or `caption_or_prompt`.
@@ -100,6 +106,12 @@ shared primary creator descriptor, then returns both a softcore prompt and a
hardcore prompt from that same descriptor. This is useful when you want the same
person/look/scene continuity but need two different prompt strengths.
When the hardcore cast includes partners, pair mode also creates deterministic
shared cast descriptors such as `woman A / primary creator` and `man A`. Use
`softcore_cast=same_as_hardcore`, `hardcore_cast=couple`, and
`continuity=same_creator_same_room` when you want a soft prompt with the same
woman, man, and location as the hardcore prompt.
It outputs:
- `softcore_prompt`
+54 -2
View File
@@ -180,6 +180,18 @@ def _clean_clothing(text: str) -> str:
return text.strip(" ,")
def _body_phrase(body: Any, figure_note: Any = "") -> str:
body = _clean_text(body)
figure_note = _clean_text(figure_note)
if not body:
return figure_note
if not figure_note:
return f"{body} figure"
if "figure" in figure_note.lower():
return f"{body} build and {figure_note}"
return f"{body} figure with {figure_note}"
def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
caption = _clean_text(row.get("caption"))
if not caption:
@@ -192,7 +204,7 @@ def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
if not body_phrase:
body = _clean_text(row.get("body_type") or row.get("body"))
figure = _clean_text(row.get("figure"))
body_phrase = f"{body} figure with {figure}" if body and figure else f"{body} figure".strip()
body_phrase = _body_phrase(body, figure)
front = f"{subject}, {age}, {body_phrase}, "
if subject in ("woman", "man") and age and body_phrase and caption.startswith(front):
try:
@@ -290,7 +302,7 @@ def _single_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -
if not body_phrase:
body = _clean_text(row.get("body_type") or row.get("body") or "")
figure = _clean_text(row.get("figure"))
body_phrase = f"{body} figure with {figure}" if body and figure else f"{body} figure".strip()
body_phrase = _body_phrase(body, figure)
skin = _row_value(row, "skin") or caption_front.get("caption_skin", "")
hair = _row_value(row, "hair") or caption_front.get("caption_hair", "")
@@ -454,8 +466,48 @@ def _group_or_layout_from_row(row: dict[str, Any], detail_level: str, keep_style
return _join_sentences(parts), "metadata(group_layout)"
def _insta_of_pair_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None:
if _clean_text(row.get("mode")).lower() != "insta/of":
return None
soft_row = row.get("softcore_row")
hard_row = row.get("hardcore_row")
if not isinstance(soft_row, dict) or not isinstance(hard_row, dict):
return None
hard_row_for_text = dict(hard_row)
options = row.get("options")
if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room":
if soft_row.get("scene_text"):
hard_row_for_text["scene_text"] = soft_row["scene_text"]
if soft_row.get("composition"):
hard_row_for_text["composition"] = soft_row["composition"]
soft_text, _soft_method = _metadata_to_prose(soft_row, detail_level, keep_style)
hard_text, _hard_method = _metadata_to_prose(hard_row_for_text, detail_level, keep_style)
descriptor = _clean_text(row.get("shared_descriptor"))
cast_descriptors = row.get("shared_cast_descriptors")
if isinstance(cast_descriptors, list):
cast_descriptor_text = _human_join([_clean_text(item) for item in cast_descriptors if _clean_text(item)])
else:
cast_descriptor_text = _clean_text(cast_descriptors)
parts = []
if cast_descriptor_text:
parts.append(f"The shared cast descriptors are {cast_descriptor_text}")
elif descriptor:
parts.append(f"The shared creator descriptor is {descriptor}")
if soft_text:
parts.append(f"Softcore version: {soft_text}")
if hard_text:
parts.append(f"Hardcore version: {hard_text}")
if not parts:
return None
return _join_sentences(parts), "metadata(insta_of_pair)"
def _metadata_to_prose(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str]:
for builder in (
_insta_of_pair_from_row,
_configured_cast_from_row,
_single_from_row,
_couple_from_row,
+13 -1
View File
@@ -2823,6 +2823,18 @@ def figure_pool(mode: str) -> list:
return FIGURE_CURVY # 'curvy' default: voluptuous-leaning mix
def make_body_phrase(body: str, figure_note: str = "") -> str:
body = str(body or "").strip()
figure_note = str(figure_note or "").strip()
if not body:
return figure_note
if not figure_note:
return f"{body} figure"
if "figure" in figure_note.lower():
return f"{body} build and {figure_note}"
return f"{body} figure with {figure_note}"
def choose_woman(rng: random.Random, ethnicity: str = "any", no_plus: bool = False, no_black: bool = False):
young = by_ethnicity(YOUNG_WOMEN, ethnicity)
mature = by_ethnicity(MATURE_WOMEN, ethnicity)
@@ -2889,7 +2901,7 @@ def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_de
subject, age, body, skin, hair, eyes = choose(rng, men_pool)
clothes = choose(rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES)
figure_note = ""
body_phrase = f"{body} figure with {figure_note}" if figure_note else f"{body} figure"
body_phrase = make_body_phrase(body, figure_note)
scene_slug, scene = choose(rng, SCENES)
if poses == "evocative":
+30 -3
View File
@@ -118,6 +118,18 @@ def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> s
return ""
def _body_phrase(body: Any, figure_note: Any = "") -> str:
body = _clean(body)
figure_note = _clean(figure_note)
if not body:
return figure_note
if not figure_note:
return f"{body} figure"
if "figure" in figure_note.lower():
return f"{body} build and {figure_note}"
return f"{body} figure with {figure_note}"
def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
caption = _strip_trigger(_clean(row.get("caption")), False)
if not caption:
@@ -128,7 +140,7 @@ def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
if not body:
body_type = _clean(row.get("body_type") or row.get("body"))
figure = _clean(row.get("figure"))
body = f"{body_type} figure with {figure}" if body_type and figure else f"{body_type} figure".strip()
body = _body_phrase(body_type, figure)
front = f"{subject}, {age}, {body}, "
if subject in ("woman", "man") and age and body and caption.startswith(front):
try:
@@ -260,6 +272,11 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str, str, str]:
descriptor = _clean(row.get("shared_descriptor"))
cast_descriptors = row.get("shared_cast_descriptors")
if isinstance(cast_descriptors, list):
cast_descriptor_text = "; ".join(_clean(item) for item in cast_descriptors if _clean(item))
else:
cast_descriptor_text = _clean(cast_descriptors)
soft = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
hard = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
soft_camera = _clean(row.get("softcore_camera_directive")) or _camera_phrase(soft)
@@ -274,9 +291,18 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
hard_cast_text = _clean(hard.get("cast_summary")) or (
f"{hard_cast} adult women and {hard_men} adult men" if hard_cast or hard_men else ""
)
same_room = options.get("continuity") == "same_creator_same_room"
hard_scene = soft.get("scene_text") if same_room and soft.get("scene_text") else hard.get("scene_text")
hard_composition = soft.get("composition") if same_room and soft.get("composition") else hard.get("composition")
soft_cast_descriptor_text = (
cast_descriptor_text
if options.get("softcore_cast") == "same_as_hardcore"
else f"Woman A / primary creator: {descriptor}"
)
soft_parts = [
f"A visibly adult creator, {descriptor}",
f"Shared cast descriptors: {soft_cast_descriptor_text}" if soft_cast_descriptor_text else "",
f"shown in a {soft_level or 'softcore'} Insta/OF creator image",
f"wearing {soft.get('item')}" if soft.get("item") else "",
f"{soft.get('pose')}" if soft.get("pose") else "",
@@ -288,12 +314,13 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
]
hard_parts = [
f"The same visibly adult creator, {descriptor}, is the visually central woman in a consensual explicit adult {hard_level or 'hardcore'} scene",
f"Shared cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "",
f"all participants are 21+ and visibly adult; the cast includes {hard_cast_text}" if hard_cast_text else "all participants are 21+ and visibly adult",
_clean(hard.get("role_graph")),
f"The sexual action is {hard.get('item')}" if hard.get("item") else "",
f"set in {row.get('hardcore_row', {}).get('scene_text') or hard.get('scene_text')}" if hard.get("scene_text") else "",
f"set in {hard_scene}" if hard_scene else "",
f"with {hard.get('expression')}" if hard.get("expression") else "",
f"framed as {hard.get('composition')}" if hard.get("composition") else "",
f"framed as {hard_composition}" if hard_composition else "",
hard_camera,
hard_style if detail_level != "concise" else "",
]
+75 -2
View File
@@ -1079,6 +1079,18 @@ def _merged_field(category: dict[str, Any], subcategory: dict[str, Any], item: A
return default
def _body_phrase(body: Any, figure_note: Any = "") -> str:
body = str(body or "").strip()
figure_note = str(figure_note or "").strip()
if not body:
return figure_note
if not figure_note:
return f"{body} figure"
if "figure" in figure_note.lower():
return f"{body} build and {figure_note}"
return f"{body} figure with {figure_note}"
def _appearance_for_subject(
rng: random.Random,
subject_type: str,
@@ -1116,7 +1128,7 @@ def _appearance_for_subject(
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": f"{body} figure with {figure_note}",
"body_phrase": _body_phrase(body, figure_note),
"figure": figure_note,
}
@@ -1187,7 +1199,7 @@ def _configured_cast_context(women_count: int, men_count: int) -> dict[str, str]
def _lettered(prefix: str, count: int) -> list[str]:
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
return [f"{prefix} {letters[index]}" for index in range(max(0, count))]
return [f"{prefix.capitalize()} {letters[index]}" for index in range(max(0, count))]
def _pick_distinct(rng: random.Random, items: list[str], count: int) -> list[str]:
@@ -2017,6 +2029,46 @@ def _insta_of_descriptor(row: dict[str, Any]) -> str:
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
def _insta_of_descriptor_from_context(context: dict[str, Any]) -> str:
age = str(context.get("age") or "").strip()
age = " ".join(age.split())
age = age.removesuffix(" adults").removesuffix(" adult").strip()
subject = str(context.get("subject") or context.get("subject_type") or "person").strip()
pieces = [
f"{age} adult {subject}".strip(),
context.get("body_phrase"),
context.get("skin"),
context.get("hair"),
context.get("eyes"),
]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
def _insta_of_cast_descriptors(
primary_descriptor: str,
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,
) -> 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)}")
return descriptors
def _insta_of_cast_phrase(women_count: int, men_count: int) -> str:
context = _configured_cast_context(women_count, men_count)
return context["cast_summary"]
@@ -2097,6 +2149,24 @@ def build_insta_of_pair(
)
descriptor = _insta_of_descriptor(soft_row)
cast_descriptors = _insta_of_cast_descriptors(
descriptor,
parsed_seed_config,
seed,
row_number,
ethnicity,
figure,
no_plus_women,
no_black,
hard_women_count,
hard_men_count,
)
cast_descriptor_text = "; ".join(cast_descriptors)
soft_cast_descriptor_text = (
cast_descriptor_text
if options["softcore_cast"] == "same_as_hardcore"
else f"Woman A / primary creator: {descriptor}"
)
platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]]
soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]]
hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]]
@@ -2121,6 +2191,7 @@ def build_insta_of_pair(
soft_prompt = (
f"Insta/OF softcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. "
f"Softcore setup: {soft_level}. Cast continuity: {soft_cast}. "
f"Shared cast descriptors: {soft_cast_descriptor_text}. "
f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
f"Facial expression: {soft_row['expression']}. Composition: {soft_row['composition']}. "
f"{soft_camera_sentence}"
@@ -2130,6 +2201,7 @@ def build_insta_of_pair(
hard_prompt = (
f"Insta/OF hardcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. "
f"Hardcore setup: {hard_level}. Cast: {hard_cast}. "
f"Shared cast descriptors: {cast_descriptor_text}. "
"Apply the shared descriptor to the most visually central woman, keeping her continuous with the softcore version. "
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
f"Setting: {hard_scene}. Facial expressions: {hard_row['expression']}. Composition: {hard_composition}. "
@@ -2159,6 +2231,7 @@ def build_insta_of_pair(
"mode": "Insta/OF",
"options": options,
"shared_descriptor": descriptor,
"shared_cast_descriptors": cast_descriptors,
"softcore_prompt": soft_prompt,
"hardcore_prompt": hard_prompt,
"softcore_negative_prompt": soft_negative,