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
+182 -20
View File
@@ -230,8 +230,16 @@ def _natural_label_text(text: Any, labels: list[str]) -> str:
return text
def _cast_prose(text: str, central_label: str = "Woman A") -> tuple[str, list[str]]:
entries = _cast_entries(text)
def _cast_prose(
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:
return (f"{central_label} is {_clean(text)}" if _clean(text) else "", [])
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
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:
text = _clean(text)
if not text:
@@ -633,6 +722,34 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
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:
text = _clean(hard_item).rstrip(".")
if not text:
@@ -1071,7 +1188,7 @@ def _hardcore_action_sentence(
detail = _limit_detail_for_density(detail, detail_density, False)
arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values)
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}"
if role_graph and anchor_phrase:
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):
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)
source_composition = re.sub(
r"^vertical\s+",
"",
_clean(row.get("source_composition")) or composition,
flags=re.IGNORECASE,
)
camera = _camera_phrase(row)
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")), "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:
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)
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)
role_graph = _natural_label_text(role_graph, 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 {}
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 = [
action,
_pov_camera_phrase(pov_labels),
cast_prose,
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 setting is {scene}" if scene else "",
_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,
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"
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_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 = (
cast_descriptor_text
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)
soft_cast_prose, soft_labels = _cast_prose(
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_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_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 {}
@@ -1385,16 +1525,25 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
hard_action = _hardcore_action_sentence(
hard_role_graph,
hard_item,
hard_composition,
hard_source_composition,
hard_axis_values,
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"
soft_cast_presence = (
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 image focuses on the woman alone"
)
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 = (
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 image focuses on the woman alone"
)
partner_styling = row.get("softcore_partner_styling")
if isinstance(partner_styling, dict):
outfits = partner_styling.get("outfits")
@@ -1403,18 +1552,29 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
else:
partner_outfit_text = ""
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)
soft_expression = ""
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")),
pov_labels,
)
soft_expression = _natural_label_text(
soft_expression_source,
soft_labels,
)
hard_expression = ""
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")),
pov_labels,
)
hard_expression = _natural_label_text(
hard_expression_source,
hard_labels,
)
@@ -1423,21 +1583,23 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
soft_cast_presence,
partner_outfit_text,
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"{soft.get('pose')}" if soft.get("pose") else "",
_expression_phrase(soft_expression),
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_style if detail_level != "concise" else "",
]
hard_parts = [
hard_action,
_pov_camera_phrase(pov_labels),
_natural_clothing_state(row.get("hardcore_clothing_state")),
hard_cast_prose,
f"set in {hard_scene}" if hard_scene else "",
_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_style if detail_level != "concise" else "",
]