Add POV participant mode

This commit is contained in:
2026-06-24 18:02:30 +02:00
parent fb6d99ac20
commit 89e499537e
4 changed files with 377 additions and 27 deletions
+15
View File
@@ -98,6 +98,13 @@ is emitted into named-cast descriptors:
couple/group prompts without turning every partner into a fully detailed primary couple/group prompts without turning every partner into a fully detailed primary
character. Set a man slot to `full` when the partner needs exact hair/eye detail. character. Set a man slot to `full` when the partner needs exact hair/eye detail.
`SxCP Man Slot` also has `presence_mode`. Use `visible` for a normal rendered
partner. Use `pov` when that man is the first-person camera participant: he
remains part of the cast and role graph, but his appearance descriptor and
per-character expression text are omitted, and pose wording is rendered from
the POV participant's viewpoint. The generic `SxCP Character Slot` exposes the
same field, but it only has an effect when `subject_type=man`.
Slots also expose `expression_enabled` and `expression_intensity`. Disable Slots also expose `expression_enabled` and `expression_intensity`. Disable
`expression_enabled` when that character should not receive a face/expression `expression_enabled` when that character should not receive a face/expression
directive. Leave `expression_intensity` at `-1` to use the generator or Insta/OF directive. Leave `expression_intensity` at `-1` to use the generator or Insta/OF
@@ -231,6 +238,9 @@ Important behavior:
- Insta/OF cast metadata is rewritten as direct named-character prose such as - 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 `Woman A is ...` and `Man A is ...`, so Krea2 does not have to interpret a
`Cast descriptors:` label. `Cast descriptors:` label.
- Man slots set to `presence_mode=pov` are not emitted as visible cast prose.
The formatter keeps them in the role graph, rewrites the action from the
first-person viewer position, and adds a POV camera sentence.
It outputs: It outputs:
@@ -261,6 +271,11 @@ the shared primary creator (`Woman A`) in both softcore and hardcore outputs;
additional woman/man slots fill partner descriptors before random fallback additional woman/man slots fill partner descriptors before random fallback
descriptors are used. descriptors are used.
For POV pair prompts, set the relevant `SxCP Man Slot` to
`presence_mode=pov`. The softcore output frames the primary creator from that
participant's camera, while the hardcore output keeps the same cast and scene
but converts the role graph into first-person positioning.
It outputs: It outputs:
- `softcore_prompt` - `softcore_prompt`
+8
View File
@@ -37,6 +37,7 @@ try:
character_figure_choices, character_figure_choices,
character_label_choices, character_label_choices,
character_man_body_choices, character_man_body_choices,
character_presence_choices,
character_profile_choices, character_profile_choices,
character_woman_body_choices, character_woman_body_choices,
ethnicity_choices, ethnicity_choices,
@@ -82,6 +83,7 @@ except ImportError:
character_figure_choices, character_figure_choices,
character_label_choices, character_label_choices,
character_man_body_choices, character_man_body_choices,
character_presence_choices,
character_profile_choices, character_profile_choices,
character_woman_body_choices, character_woman_body_choices,
ethnicity_choices, ethnicity_choices,
@@ -611,6 +613,7 @@ class SxCPCharacterSlot:
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
"expression_enabled": ("BOOLEAN", {"default": True}), "expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"presence_mode": (character_presence_choices(), {"default": "visible"}),
}, },
"optional": { "optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -640,6 +643,7 @@ class SxCPCharacterSlot:
descriptor_detail="auto", descriptor_detail="auto",
expression_enabled=True, expression_enabled=True,
expression_intensity=-1.0, expression_intensity=-1.0,
presence_mode="visible",
character_cast="", character_cast="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
@@ -658,6 +662,7 @@ class SxCPCharacterSlot:
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled, expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
presence_mode=presence_mode,
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -755,6 +760,7 @@ class SxCPManSlot:
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}),
"expression_enabled": ("BOOLEAN", {"default": True}), "expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"presence_mode": (character_presence_choices(), {"default": "visible"}),
}, },
"optional": { "optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -782,6 +788,7 @@ class SxCPManSlot:
descriptor_detail="compact", descriptor_detail="compact",
expression_enabled=True, expression_enabled=True,
expression_intensity=-1.0, expression_intensity=-1.0,
presence_mode="visible",
character_cast="", character_cast="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
@@ -800,6 +807,7 @@ class SxCPManSlot:
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled, expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
presence_mode=presence_mode,
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
+177 -15
View File
@@ -230,8 +230,16 @@ def _natural_label_text(text: Any, labels: list[str]) -> str:
return text return text
def _cast_prose(text: str, central_label: str = "Woman A") -> tuple[str, list[str]]: def _cast_prose(
entries = _cast_entries(text) text: str,
central_label: str = "Woman A",
omit_labels: list[str] | set[str] | tuple[str, ...] = (),
) -> tuple[str, list[str]]:
raw_entries = _cast_entries(text)
omitted = set(omit_labels or [])
entries = [(label, descriptor) for label, descriptor in raw_entries if label not in omitted]
if raw_entries and not entries:
return "", []
if not entries: if not entries:
return (f"{central_label} is {_clean(text)}" if _clean(text) else "", []) return (f"{central_label} is {_clean(text)}" if _clean(text) else "", [])
labels = [label for label, _descriptor in entries] labels = [label for label, _descriptor in entries]
@@ -250,6 +258,87 @@ def _cast_prose(text: str, central_label: str = "Woman A") -> tuple[str, list[st
return " ".join(sentences), labels return " ".join(sentences), labels
def _pov_labels_from_value(value: Any) -> list[str]:
labels: list[str] = []
if isinstance(value, list):
candidates = value
else:
candidates = re.split(r"[,;]\s*", _clean(value)) if _clean(value) else []
for candidate in candidates:
label = _clean(candidate)
if re.match(r"^Man [A-Z]$", label) and label not in labels:
labels.append(label)
return labels
def _merge_labels(*groups: list[str]) -> list[str]:
merged: list[str] = []
for group in groups:
for label in group:
if label and label not in merged:
merged.append(label)
return merged
def _filter_pov_labeled_clauses(text: Any, pov_labels: list[str]) -> str:
rendered = _clean(text)
if not rendered or not pov_labels:
return rendered
clauses = [clause.strip() for clause in rendered.split(";") if clause.strip()]
filtered = [
clause
for clause in clauses
if not any(re.match(rf"^{re.escape(label)}\b", clause) for label in pov_labels)
]
return "; ".join(filtered)
def _pov_action_phrase(action: Any, pov_labels: list[str]) -> str:
rendered = _clean(action)
if not rendered or not pov_labels:
return rendered
for label in sorted(pov_labels, key=len, reverse=True):
escaped = re.escape(label)
rendered = re.sub(rf"\b{escaped}'s\b", "the POV viewer's", rendered)
rendered = re.sub(rf"\b{escaped}\b", "the POV viewer", rendered)
if "Man A" in pov_labels:
rendered = re.sub(r"\bthe man's\b", "the POV viewer's", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bthe man\b", "the POV viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhe\b", "the POV viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhim\b", "the POV viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhis\b", "the POV viewer's", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
return rendered
def _pov_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str:
if not pov_labels:
return ""
if softcore:
return (
"first-person POV creator framing from the male participant; "
"the woman is the visible subject, and the participant is implied by camera position or foreground cues"
)
return (
"first-person POV from the male participant; keep the visible partners readable from the viewer's position, "
"using only foreground hands, body position, or camera perspective cues for the POV participant"
)
def _pov_composition_text(composition: Any, pov_labels: list[str]) -> str:
text = _clean(composition)
if not text or not pov_labels:
return text
text = re.sub(r"\ball participants visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball adult bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball three bodies readable\b", "visible partner bodies readable", text, flags=re.IGNORECASE)
text = re.sub(r"\bwide group-sex composition\b", "first-person group-sex POV composition", text, flags=re.IGNORECASE)
if "pov" not in text.lower() and "first-person" not in text.lower():
text = f"{text}, adapted for first-person POV with the POV participant kept off-camera"
return _clean(text)
def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str: def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
text = _clean(text) text = _clean(text)
if not text: if not text:
@@ -633,6 +722,34 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
return "" return ""
def _arrangement_duplicates_role(arrangement: str, role_graph: str) -> bool:
arrangement_lower = _clean(arrangement).lower()
role_lower = _clean(role_graph).lower()
if not arrangement_lower or not role_lower:
return False
markers = (
"bed edge",
"on all fours",
"face-down",
"hips raised",
"bent forward",
"straddl",
"on her back",
"on their sides",
"on her side",
"seated in",
"sits in",
"lap",
"kneeling between",
"kneels between",
"kneeling in front",
"kneels in front",
"positioned behind",
"standing",
)
return any(marker in arrangement_lower and marker in role_lower for marker in markers)
def _hardcore_item_detail(hard_item: str) -> str: def _hardcore_item_detail(hard_item: str) -> str:
text = _clean(hard_item).rstrip(".") text = _clean(hard_item).rstrip(".")
if not text: if not text:
@@ -1071,7 +1188,7 @@ def _hardcore_action_sentence(
detail = _limit_detail_for_density(detail, detail_density, False) detail = _limit_detail_for_density(detail, detail_density, False)
arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values) arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values)
anchor_phrase = _with_indefinite_article(anchor) if anchor else "" anchor_phrase = _with_indefinite_article(anchor) if anchor else ""
if arrangement and anchor_phrase: if arrangement and anchor_phrase and not _arrangement_duplicates_role(arrangement, role_graph):
anchor_phrase = f"{anchor_phrase} {arrangement}" anchor_phrase = f"{anchor_phrase} {arrangement}"
if role_graph and anchor_phrase: if role_graph and anchor_phrase:
sentence = f"In {anchor_phrase}, {role_graph}" sentence = f"In {anchor_phrase}, {role_graph}"
@@ -1258,6 +1375,12 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
if not _expression_disabled(row): if not _expression_disabled(row):
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression")) expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE) composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE)
source_composition = re.sub(
r"^vertical\s+",
"",
_clean(row.get("source_composition")) or composition,
flags=re.IGNORECASE,
)
camera = _camera_phrase(row) camera = _camera_phrase(row)
style = _style_phrase(row, style_mode) style = _style_phrase(row, style_mode)
@@ -1274,25 +1397,31 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
or _prompt_field(_clean(row.get("prompt")), "Characters") or _prompt_field(_clean(row.get("prompt")), "Characters")
or _prompt_field(_clean(row.get("prompt")), "Cast descriptors") or _prompt_field(_clean(row.get("prompt")), "Cast descriptors")
) )
cast_prose, cast_labels = _cast_prose(cast_descriptor_text) pov_labels = _pov_labels_from_value(row.get("pov_character_labels"))
cast_prose, cast_labels = _cast_prose(cast_descriptor_text, omit_labels=pov_labels)
if not cast_labels and women_count == 1 and men_count == 1: if not cast_labels and women_count == 1 and men_count == 1:
cast_labels = ["Woman A", "Man A"] cast_labels = ["Woman A", "Man A"]
cast_labels = _merge_labels(cast_labels, pov_labels)
expression = _filter_pov_labeled_clauses(expression, pov_labels)
expression = _natural_label_text(expression, cast_labels) expression = _natural_label_text(expression, cast_labels)
role_graph = _sanitize_scene_text_for_cast(row.get("role_graph"), cast_labels) role_graph = _sanitize_scene_text_for_cast(row.get("source_role_graph") or row.get("role_graph"), cast_labels)
item = _sanitize_scene_text_for_cast(item, cast_labels) item = _sanitize_scene_text_for_cast(item, cast_labels)
role_graph = _natural_label_text(role_graph, cast_labels) role_graph = _natural_label_text(role_graph, cast_labels)
item = _natural_label_text(item, cast_labels) item = _natural_label_text(item, cast_labels)
axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {} axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {}
detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density")) detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
action = _hardcore_action_sentence(role_graph, item, composition, axis_values, detail_density) action = _hardcore_action_sentence(role_graph, item, source_composition, axis_values, detail_density)
action = _pov_action_phrase(action, pov_labels)
output_composition = _pov_composition_text(composition, pov_labels)
parts = [ parts = [
action, action,
_pov_camera_phrase(pov_labels),
cast_prose, cast_prose,
f"A consensual explicit adult scene with {subject}" if not action else "", f"A consensual explicit adult scene with {subject}" if not action else "",
f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "",
f"The setting is {scene}" if scene else "", f"The setting is {scene}" if scene else "",
_expression_phrase(expression), _expression_phrase(expression),
_composition_phrase(composition, action, "The image is framed as", detail_density), _composition_phrase(output_composition, action, "The image is framed as", detail_density),
camera, camera,
style if detail_level != "concise" else "", style if detail_level != "concise" else "",
] ]
@@ -1367,15 +1496,26 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
same_room = options.get("continuity") == "same_creator_same_room" 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_scene = soft.get("scene_text") if same_room and soft.get("scene_text") else hard.get("scene_text")
hard_composition = hard.get("composition") hard_composition = hard.get("composition")
hard_source_composition = hard.get("source_composition") or hard_composition
pov_labels = _merge_labels(
_pov_labels_from_value(row.get("pov_character_labels")),
_pov_labels_from_value(soft.get("pov_character_labels")),
_pov_labels_from_value(hard.get("pov_character_labels")),
)
soft_cast_descriptor_text = ( soft_cast_descriptor_text = (
cast_descriptor_text cast_descriptor_text
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) soft_cast_prose, soft_labels = _cast_prose(
hard_cast_prose, hard_labels = _cast_prose(cast_descriptor_text) soft_cast_descriptor_text,
omit_labels=pov_labels if options.get("softcore_cast") == "same_as_hardcore" else [],
)
hard_cast_prose, hard_labels = _cast_prose(cast_descriptor_text, omit_labels=pov_labels)
soft_labels = _merge_labels(soft_labels, pov_labels if options.get("softcore_cast") == "same_as_hardcore" else [])
hard_labels = _merge_labels(hard_labels, pov_labels)
hard_item = _sanitize_scene_text_for_cast(hard.get("item"), hard_labels) 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) hard_role_graph = _sanitize_scene_text_for_cast(hard.get("source_role_graph") or hard.get("role_graph"), hard_labels)
hard_item = _natural_label_text(hard_item, hard_labels) hard_item = _natural_label_text(hard_item, hard_labels)
hard_role_graph = _natural_label_text(hard_role_graph, hard_labels) hard_role_graph = _natural_label_text(hard_role_graph, hard_labels)
hard_axis_values = hard.get("item_axis_values") if isinstance(hard.get("item_axis_values"), dict) else {} hard_axis_values = hard.get("item_axis_values") if isinstance(hard.get("item_axis_values"), dict) else {}
@@ -1385,11 +1525,20 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
hard_action = _hardcore_action_sentence( hard_action = _hardcore_action_sentence(
hard_role_graph, hard_role_graph,
hard_item, hard_item,
hard_composition, hard_source_composition,
hard_axis_values, hard_axis_values,
hard_detail_density, hard_detail_density,
) )
hard_action = _pov_action_phrase(hard_action, pov_labels)
hard_output_composition = _pov_composition_text(hard_composition, pov_labels)
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
soft_output_composition = _pov_composition_text(soft.get("composition"), pov_labels if same_soft_cast else [])
if same_soft_cast and pov_labels:
soft_cast_presence = (
"the woman is framed from the POV participant's first-person camera in a non-explicit teaser pose, "
"with the POV participant kept off-camera as the viewpoint and implied by camera position or foreground cues"
)
else:
soft_cast_presence = ( soft_cast_presence = (
f"{_label_join(soft_labels)} are 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
@@ -1403,18 +1552,29 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
else: else:
partner_outfit_text = "" partner_outfit_text = ""
partner_pose = "" partner_pose = ""
partner_outfit_text = _filter_pov_labeled_clauses(partner_outfit_text, pov_labels)
if pov_labels:
partner_pose = ""
partner_outfit_text = _natural_label_text(partner_outfit_text, soft_labels) partner_outfit_text = _natural_label_text(partner_outfit_text, soft_labels)
soft_expression = "" soft_expression = ""
if not _expression_disabled(soft): if not _expression_disabled(soft):
soft_expression = _natural_label_text( soft_expression_source = _filter_pov_labeled_clauses(
_clean(soft.get("character_expression_text")) or _clean(soft.get("expression")), _clean(soft.get("character_expression_text")) or _clean(soft.get("expression")),
pov_labels,
)
soft_expression = _natural_label_text(
soft_expression_source,
soft_labels, soft_labels,
) )
hard_expression = "" hard_expression = ""
if not _expression_disabled(hard): if not _expression_disabled(hard):
hard_expression = _natural_label_text( hard_expression_source = _filter_pov_labeled_clauses(
_clean(hard.get("character_expression_text")) or _clean(hard.get("expression")), _clean(hard.get("character_expression_text")) or _clean(hard.get("expression")),
pov_labels,
)
hard_expression = _natural_label_text(
hard_expression_source,
hard_labels, hard_labels,
) )
@@ -1423,21 +1583,23 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
soft_cast_presence, soft_cast_presence,
partner_outfit_text, partner_outfit_text,
partner_pose, partner_pose,
_pov_camera_phrase(pov_labels, softcore=True) if same_soft_cast else "",
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 "",
_expression_phrase(soft_expression), _expression_phrase(soft_expression),
f"in {soft.get('scene_text')}" if soft.get("scene_text") else "", f"in {soft.get('scene_text')}" if soft.get("scene_text") else "",
f"framed as {soft.get('composition')}" if soft.get("composition") else "", f"framed as {soft_output_composition}" if soft_output_composition else "",
soft_camera, soft_camera,
soft_style if detail_level != "concise" else "", soft_style if detail_level != "concise" else "",
] ]
hard_parts = [ hard_parts = [
hard_action, hard_action,
_pov_camera_phrase(pov_labels),
_natural_clothing_state(row.get("hardcore_clothing_state")), _natural_clothing_state(row.get("hardcore_clothing_state")),
hard_cast_prose, hard_cast_prose,
f"set in {hard_scene}" if hard_scene else "", f"set in {hard_scene}" if hard_scene else "",
_expression_phrase(hard_expression), _expression_phrase(hard_expression),
_composition_phrase(hard_composition, hard_action, detail_density=hard_detail_density), _composition_phrase(hard_output_composition, hard_action, detail_density=hard_detail_density),
hard_camera, hard_camera,
hard_style if detail_level != "concise" else "", hard_style if detail_level != "concise" else "",
] ]
+169 -4
View File
@@ -175,6 +175,7 @@ CHARACTER_MAN_BODY_CHOICES = [
"fat", "fat",
] ]
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"] CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"} CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"] CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
@@ -1429,6 +1430,10 @@ def character_descriptor_detail_choices() -> list[str]:
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES) return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
def character_presence_choices() -> list[str]:
return list(CHARACTER_PRESENCE_CHOICES)
def character_ethnicity_choices() -> list[str]: def character_ethnicity_choices() -> list[str]:
return ["random"] + list(ETHNICITY_FILTER_CHOICES) return ["random"] + list(ETHNICITY_FILTER_CHOICES)
@@ -1863,6 +1868,21 @@ def _normalize_descriptor_detail(value: Any) -> str:
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto" return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
def _normalize_presence_mode(value: Any, subject_type: str) -> str:
text = str(value or "visible").strip().lower()
if text not in CHARACTER_PRESENCE_CHOICES:
text = "visible"
if subject_type != "man":
return "visible"
return text
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
if not slot:
return False
return slot.get("subject_type") == "man" and slot.get("presence_mode") == "pov"
def _normalize_slot_expression_intensity(value: Any) -> float: def _normalize_slot_expression_intensity(value: Any) -> float:
try: try:
intensity = float(value) intensity = float(value)
@@ -1907,6 +1927,8 @@ def _cast_expression_intensity_override(
value_labels: list[str] = [] value_labels: list[str] = []
for label in labels: for label in labels:
slot = label_map.get(label) slot = label_map.get(label)
if _slot_is_pov(slot):
continue
if slot: if slot:
matching_slots.append(slot) matching_slots.append(slot)
value = _slot_expression_intensity(slot) value = _slot_expression_intensity(slot)
@@ -1943,6 +1965,8 @@ def _character_expression_entries(
slot = label_map.get(label) slot = label_map.get(label)
if not slot: if not slot:
continue continue
if _slot_is_pov(slot):
continue
if not _slot_expression_enabled(slot): if not _slot_expression_enabled(slot):
continue continue
intensity = _slot_expression_intensity(slot) intensity = _slot_expression_intensity(slot)
@@ -2050,6 +2074,7 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
"hair": _slot_value(slot.get("hair")), "hair": _slot_value(slot.get("hair")),
"eyes": _slot_value(slot.get("eyes")), "eyes": _slot_value(slot.get("eyes")),
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")), "descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
"presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type),
"expression_enabled": not _is_false(slot.get("expression_enabled", True)), "expression_enabled": not _is_false(slot.get("expression_enabled", True)),
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")), "expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
} }
@@ -2096,6 +2121,8 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
f"body={slot.get('body', 'random')}", f"body={slot.get('body', 'random')}",
f"detail={slot.get('descriptor_detail', 'auto')}", f"detail={slot.get('descriptor_detail', 'auto')}",
] ]
if _slot_is_pov(slot):
parts.append("presence=pov")
if not _slot_expression_enabled(slot): if not _slot_expression_enabled(slot):
parts.append("expression=disabled") parts.append("expression=disabled")
else: else:
@@ -2127,6 +2154,7 @@ def build_character_slot_json(
expression_intensity: float = -1.0, expression_intensity: float = -1.0,
enabled: bool = True, enabled: bool = True,
character_cast: str | dict[str, Any] | list[Any] | None = "", character_cast: str | dict[str, Any] | list[Any] | None = "",
presence_mode: str = "visible",
) -> dict[str, str]: ) -> dict[str, str]:
existing_slots = _parse_character_cast(character_cast) existing_slots = _parse_character_cast(character_cast)
slot = _normalize_character_slot( slot = _normalize_character_slot(
@@ -2144,6 +2172,7 @@ def build_character_slot_json(
"hair": hair, "hair": hair,
"eyes": eyes, "eyes": eyes,
"descriptor_detail": descriptor_detail, "descriptor_detail": descriptor_detail,
"presence_mode": presence_mode,
"expression_enabled": expression_enabled, "expression_enabled": expression_enabled,
"expression_intensity": expression_intensity, "expression_intensity": expression_intensity,
} }
@@ -2186,6 +2215,62 @@ def _character_slot_label_map(slots: list[dict[str, Any]]) -> dict[str, dict[str
return label_map return label_map
def _pov_character_labels(
label_map: dict[str, dict[str, Any]],
men_count: int | None = None,
) -> list[str]:
if men_count is None:
labels = sorted(label for label in label_map if label.startswith("Man "))
else:
labels = [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]
return [label for label in labels if _slot_is_pov(label_map.get(label))]
def _pov_text_with_viewer(text: Any, pov_labels: list[str]) -> str:
rendered = str(text or "").strip()
if not rendered or not pov_labels:
return rendered
for label in sorted(pov_labels, key=len, reverse=True):
escaped = re.escape(label)
rendered = re.sub(rf"\b{escaped}'s\b", "the POV viewer's", rendered)
rendered = re.sub(rf"\b{escaped}\b", "the POV viewer", rendered)
rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
return _clean_prompt_punctuation(rendered)
def _pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str:
role_graph_text = str(role_graph or "").strip()
if not role_graph_text or not pov_labels:
return role_graph_text
viewer_text = _pov_text_with_viewer(role_graph_text, pov_labels)
label_text = ", ".join(pov_labels)
return f"First-person POV from {label_text}; {viewer_text}"
def _pov_prompt_directive(pov_labels: list[str]) -> str:
if not pov_labels:
return ""
label_text = ", ".join(pov_labels)
return (
f"POV participant: {label_text} is the first-person camera viewpoint; "
"he remains the off-camera viewpoint, represented by foreground hands, body position, or camera perspective cues when needed."
)
def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
text = str(composition or "").strip()
if not text or not pov_labels:
return text
text = re.sub(r"\ball participants visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball adult bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball three bodies readable\b", "visible partner bodies readable", text, flags=re.IGNORECASE)
text = re.sub(r"\bwide group-sex composition\b", "first-person group-sex POV composition", text, flags=re.IGNORECASE)
if "pov" not in text.lower() and "first-person" not in text.lower():
text = f"{text}, adapted for first-person POV with the POV participant kept off-camera"
return _clean_prompt_punctuation(text)
def _context_from_character_slot( def _context_from_character_slot(
rng: random.Random, rng: random.Random,
slot: dict[str, Any], slot: dict[str, Any],
@@ -2228,6 +2313,7 @@ def _context_from_character_slot(
if value: if value:
context[key] = value context[key] = value
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail")) context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type)
context["expression_enabled"] = _slot_expression_enabled(slot) context["expression_enabled"] = _slot_expression_enabled(slot)
expression_intensity = _slot_expression_intensity(slot) expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None: if expression_intensity is not None:
@@ -2267,6 +2353,7 @@ def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]
"eyes", "eyes",
"figure", "figure",
"descriptor_detail", "descriptor_detail",
"presence_mode",
"expression_enabled", "expression_enabled",
"expression_intensity", "expression_intensity",
): ):
@@ -2304,6 +2391,8 @@ def _cast_descriptor_entries(
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}") descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
for index in range(max(0, men_count)): for index in range(max(0, men_count)):
label = f"Man {chr(ord('A') + index)}" label = f"Man {chr(ord('A') + index)}"
if _slot_is_pov(label_map.get(label)):
continue
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black) 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)}") descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
return descriptors, slots return descriptors, slots
@@ -2786,6 +2875,34 @@ def _role_graph(
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest." return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body." return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body."
def penetration_position_graph(woman: str, man: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if "missionary" in text:
return f"{woman} lies on her back with legs open while {man} is above her and {man}'s penis thrusts into her."
if "reverse cowgirl" in text:
return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her."
if "cowgirl" in text or "straddling" in text:
return f"{woman} straddles {man}'s hips facing him while {man} lies under her and {man}'s penis thrusts into her."
if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her."
if "standing" in text:
return f"{woman} stands braced with hips angled back while {man} stands behind her and {man}'s penis thrusts into her."
if "spooning" in text or "side-lying" in text:
return f"{woman} lies on her side with thighs parted while {man} presses behind her and {man}'s penis thrusts into her."
if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text:
return f"{woman} lies at the bed edge with hips near the edge while {man} kneels between her legs and {man}'s penis thrusts into her."
if "kneeling straddle" in text:
return f"{woman} kneels straddling {man}'s hips while {man} supports her waist and {man}'s penis thrusts into her."
if "lotus" in text:
return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her."
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and {man}'s penis thrusts into her."
if people_count == 1: if people_count == 1:
solo = people[0] solo = people[0]
if women_count == 1: if women_count == 1:
@@ -2870,7 +2987,7 @@ def _role_graph(
elif "cumshot" in slug or "climax" in slug: elif "cumshot" in slug or "climax" in slug:
graph = climax_position_graph(woman, man, third) graph = climax_position_graph(woman, man, third)
else: else:
graph = f"{man}'s penis thrusts into {woman} with penetration and body contact visible." graph = penetration_position_graph(woman, man)
return graph + support_sentence({woman, man, third} if third else {woman, man}) return graph + support_sentence({woman, man, third} if third else {woman, man})
@@ -3246,7 +3363,13 @@ def _build_custom_row(
else: 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) pov_character_labels = (
_pov_character_labels(character_slot_map, men_count)
if subject_type == "configured_cast"
else []
)
source_role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
cast_descriptors: list[str] = [] cast_descriptors: list[str] = []
cast_descriptor_text = "" cast_descriptor_text = ""
expression_intensity_source = "input" expression_intensity_source = "input"
@@ -3320,10 +3443,11 @@ def _build_custom_row(
character_expression_text = "; ".join(character_expressions) character_expression_text = "; ".join(character_expressions)
if character_expression_text: if character_expression_text:
expression = character_expression_text expression = character_expression_text
composition = _choose_text( source_composition = _choose_text(
composition_rng, composition_rng,
_compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count), _compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count),
) )
composition = _pov_composition_prompt(source_composition, pov_character_labels)
negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT)) negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX)) positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
@@ -3360,8 +3484,12 @@ def _build_custom_row(
"expression_intensity": expression_intensity, "expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source, "expression_intensity_source": expression_intensity_source,
"composition": composition, "composition": composition,
"source_composition": source_composition,
"composition_prompt": _composition_prompt(composition), "composition_prompt": _composition_prompt(composition),
"role_graph": role_graph, "role_graph": role_graph,
"source_role_graph": source_role_graph,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
"cast_descriptors": cast_descriptor_text, "cast_descriptors": cast_descriptor_text,
"positive_suffix": positive_suffix, "positive_suffix": positive_suffix,
"negative_prompt": negative_prompt, "negative_prompt": negative_prompt,
@@ -3392,6 +3520,8 @@ 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: 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}.") prompt = _insert_positive_directive(prompt, f"Characters: {cast_descriptor_text}.")
if subject_type == "configured_cast" and pov_character_labels:
prompt = _insert_positive_directive(prompt, _pov_prompt_directive(pov_character_labels))
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: if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in caption_template:
caption = f"{caption.rstrip()}, {cast_descriptor_text}" caption = f"{caption.rstrip()}, {cast_descriptor_text}"
@@ -3425,6 +3555,10 @@ def _build_custom_row(
"seed_config": seed_config, "seed_config": seed_config,
"content_seed_axis": content_axis, "content_seed_axis": content_axis,
"role_graph": role_graph, "role_graph": role_graph,
"source_role_graph": source_role_graph,
"source_composition": source_composition,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
"shared_expression": shared_expression, "shared_expression": shared_expression,
"character_expressions": character_expressions, "character_expressions": character_expressions,
"character_expression_text": character_expression_text, "character_expression_text": character_expression_text,
@@ -4020,15 +4154,19 @@ def _insta_of_partner_styling(
row_number: int, row_number: int,
women_count: int, women_count: int,
men_count: int, men_count: int,
pov_labels: list[str] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
content_rng = _axis_rng(seed_config, "content", seed, row_number + 421) content_rng = _axis_rng(seed_config, "content", seed, row_number + 421)
pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421) pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421)
pov_set = set(pov_labels or [])
outfits: list[str] = [] outfits: list[str] = []
for index in range(max(0, women_count - 1)): for index in range(max(0, women_count - 1)):
label = chr(ord("B") + index) label = chr(ord("B") + index)
outfits.append(f"Woman {label} wears {g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)}") outfits.append(f"Woman {label} wears {g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)}")
for index in range(max(0, men_count)): for index in range(max(0, men_count)):
label = chr(ord("A") + index) label = chr(ord("A") + index)
if f"Man {label}" in pov_set:
continue
outfits.append(f"Man {label} wears {g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)}") outfits.append(f"Man {label} wears {g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)}")
return { return {
"outfits": outfits, "outfits": outfits,
@@ -4071,6 +4209,7 @@ def build_insta_of_pair(
parsed_seed_config = _parse_seed_config(seed_config) parsed_seed_config = _parse_seed_config(seed_config)
character_slots = _parse_character_cast(character_cast) character_slots = _parse_character_cast(character_cast)
character_slot_map = _character_slot_label_map(character_slots) character_slot_map = _character_slot_label_map(character_slots)
pov_character_labels = _pov_character_labels(character_slot_map, hard_men_count)
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)
@@ -4143,6 +4282,18 @@ def build_insta_of_pair(
soft_row["item_label"] = "Insta/OF softcore outfit" soft_row["item_label"] = "Insta/OF softcore outfit"
soft_row["custom_item"] = "insta_of_softcore_outfit" soft_row["custom_item"] = "insta_of_softcore_outfit"
soft_row["softcore_outfit_policy"] = "insta_of_safe_softcore" soft_row["softcore_outfit_policy"] = "insta_of_safe_softcore"
soft_row["pov_character_labels"] = (
pov_character_labels
if options["softcore_cast"] == "same_as_hardcore"
else []
)
soft_row["pov_prompt_directive"] = _pov_prompt_directive(soft_row["pov_character_labels"])
if soft_row["pov_character_labels"]:
soft_row["source_composition"] = soft_row.get("source_composition") or soft_row.get("composition", "")
soft_row["composition"] = _pov_composition_prompt(
soft_row["source_composition"],
soft_row["pov_character_labels"],
)
hard_row = build_prompt( hard_row = build_prompt(
category="Hardcore sexual poses", category="Hardcore sexual poses",
subcategory=RANDOM_SUBCATEGORY, subcategory=RANDOM_SUBCATEGORY,
@@ -4170,6 +4321,8 @@ def build_insta_of_pair(
character_cast=character_cast or "", character_cast=character_cast or "",
) )
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"] hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
hard_row["pov_character_labels"] = pov_character_labels
hard_row["pov_prompt_directive"] = _pov_prompt_directive(pov_character_labels)
descriptor = _insta_of_descriptor(soft_row) descriptor = _insta_of_descriptor(soft_row)
cast_descriptors = _insta_of_cast_descriptors( cast_descriptors = _insta_of_cast_descriptors(
@@ -4197,6 +4350,7 @@ def build_insta_of_pair(
row_number, row_number,
hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1, hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1,
hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0, hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0,
pov_character_labels if options["softcore_cast"] == "same_as_hardcore" else [],
) )
if options["softcore_cast"] != "same_as_hardcore": if options["softcore_cast"] != "same_as_hardcore":
soft_partner_styling = {"outfits": [], "pose": ""} soft_partner_styling = {"outfits": [], "pose": ""}
@@ -4223,10 +4377,17 @@ def build_insta_of_pair(
else f"non-explicit teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}" else f"non-explicit teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}"
) )
soft_cast_presence = ( soft_cast_presence = (
(
"Frame Woman A from the POV participant's first-person camera in a non-explicit teaser setup; "
"keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. "
)
if options["softcore_cast"] == "same_as_hardcore" and pov_character_labels
else (
"Place Woman A and the listed partners together in a non-explicit teaser pose with no sex act or genital contact. " "Place Woman A and the listed partners together in a non-explicit teaser pose with no sex act or genital contact. "
if options["softcore_cast"] == "same_as_hardcore" if options["softcore_cast"] == "same_as_hardcore"
else "Keep the softcore version focused on Woman A alone. " else "Keep the softcore version focused on Woman A alone. "
) )
)
soft_cast_styling_sentence = ( soft_cast_styling_sentence = (
f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. " f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. "
if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text
@@ -4243,6 +4404,7 @@ def build_insta_of_pair(
"balanced": "", "balanced": "",
"dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ", "dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ",
}[hard_detail_density] }[hard_detail_density]
pov_directive = _pov_prompt_directive(pov_character_labels)
soft_descriptor_sentence = ( soft_descriptor_sentence = (
f"Cast descriptors: {soft_cast_descriptor_text}. " f"Cast descriptors: {soft_cast_descriptor_text}. "
if options["softcore_cast"] == "same_as_hardcore" if options["softcore_cast"] == "same_as_hardcore"
@@ -4266,7 +4428,8 @@ def build_insta_of_pair(
f"Insta/OF hardcore mode: {platform_style}. " f"Insta/OF hardcore mode: {platform_style}. "
f"Hardcore setup: {hard_level}. Cast: {hard_cast}. " f"Hardcore setup: {hard_level}. Cast: {hard_cast}. "
f"Cast descriptors: {cast_descriptor_text}. " f"Cast descriptors: {cast_descriptor_text}. "
"Keep Woman A visually central. " f"{pov_directive + ' ' if pov_directive else ''}"
f"{'Keep Woman A visually central from the POV camera. ' if pov_character_labels else 'Keep Woman A visually central. '}"
f"{hard_clothing_state} " f"{hard_clothing_state} "
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. " f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
f"Setting: {hard_scene}. " f"Setting: {hard_scene}. "
@@ -4316,6 +4479,8 @@ def build_insta_of_pair(
"options": options, "options": options,
"shared_descriptor": descriptor, "shared_descriptor": descriptor,
"shared_cast_descriptors": cast_descriptors, "shared_cast_descriptors": cast_descriptors,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": pov_directive,
"softcore_partner_styling": soft_partner_styling, "softcore_partner_styling": soft_partner_styling,
"hardcore_clothing_state": hard_clothing_state, "hardcore_clothing_state": hard_clothing_state,
"hardcore_detail_density": hard_detail_density, "hardcore_detail_density": hard_detail_density,