Add axis-aware hardcore position grammar

This commit is contained in:
2026-06-24 16:47:55 +02:00
parent 669a893e7c
commit 253b343c90
3 changed files with 282 additions and 28 deletions
+21 -5
View File
@@ -967,11 +967,11 @@
"item_templates": [ "item_templates": [
"{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", "{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
"{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}", "{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}",
"{angle} climax view featuring {climax_act}, {body_contact}, {fluid_detail}, and {visibility}", "{angle} climax view featuring {climax_act}, {body_position}, {body_contact}, {fluid_detail}, and {visibility}",
"hardcore post-climax scene with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", "hardcore post-climax scene with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
"{climax_act} on {surface}, with {body_contact}, {hand_detail}, and {fluid_detail}", "{climax_act} on {surface}, with {body_position}, {body_contact}, {hand_detail}, and {fluid_detail}",
"{angle} view of {fluid_location}, {body_position}, {climax_act}, and {visibility}", "{angle} view of {fluid_location}, {body_position}, {climax_act}, and {visibility}",
"explicit orgasm scene: {climax_act}, {fluid_detail}, {expression_detail}, and {body_contact}", "explicit orgasm scene: {climax_act}, {body_position}, {fluid_detail}, {expression_detail}, and {body_contact}",
"{body_position} with {fluid_location}, {hand_detail}, {visibility}, and {climax_act}" "{body_position} with {fluid_location}, {hand_detail}, {visibility}, and {climax_act}"
], ],
"item_axes": { "item_axes": {
@@ -1001,15 +1001,31 @@
], ],
"body_position": [ "body_position": [
"kneeling with mouth open", "kneeling with mouth open",
"lying on the back with legs spread", "lying on the back with legs spread and hips lifted",
"sitting on the edge of the bed", "sitting on the edge of the bed",
"bent over with ass raised", "bent over with ass raised",
"reclining with thighs open", "reclining with thighs open",
"standing with cum on the body", "standing with cum on the body",
"straddling a partner", "straddling a partner's hips in cowgirl position",
"reverse cowgirl over a partner's hips",
"on all fours with hips raised",
"face-down ass-up on the mattress",
"side-lying with thighs parted",
"kneeling in front of a standing partner",
"seated in a partner's lap facing them",
"lying at the bed edge with thighs open",
"squatting on top of a partner",
{ {
"text": "lying between two partners", "text": "lying between two partners",
"min_people": 3 "min_people": 3
},
{
"text": "kneeling between standing partners",
"min_people": 3
},
{
"text": "held between front-and-back partners",
"min_people": 3
} }
], ],
"climax_act": [ "climax_act": [
+206 -19
View File
@@ -280,9 +280,60 @@ def _natural_clothing_state(text: Any) -> str:
return text return text
def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "") -> str: def _axis_values_text(axis_values: Any) -> str:
text = " ".join(_clean(part).lower() for part in (role_graph, hard_item, composition) if _clean(part)) if not isinstance(axis_values, dict):
item_text = _clean(hard_item).lower() return ""
priority = (
"position",
"body_position",
"body_arrangement",
"arrangement",
"angle",
"surface",
"body_contact",
"leg_detail",
"oral_act",
"oral_detail",
"penetration_act",
"penetration_detail",
"anal_act",
"double_act",
"threesome_act",
"group_act",
)
parts = [_clean(axis_values.get(key)) for key in priority if _clean(axis_values.get(key))]
return " ".join(parts)
def _position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
return " ".join(
_clean(part).lower()
for part in (role_graph, hard_item, composition, _axis_values_text(axis_values))
if _clean(part)
)
def _mentions_ass(text: str) -> bool:
return bool(
re.search(
r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and",
text,
)
)
def _mentions_rear_entry(text: str) -> bool:
return bool(
re.search(
r"ass[- ](?:up|raised|exposed|lifted|stretched)|penis entering ass|cum (?:on|dripping from) ass|spread cheeks|lower back and ass|pussy, ass|rear[- ]entry",
text,
)
)
def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
text = _position_context_text(role_graph, hard_item, composition, axis_values)
item_text = " ".join(part for part in (_clean(hard_item).lower(), _axis_values_text(axis_values).lower()) if part)
if not text: if not text:
return "" return ""
if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text: if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text:
@@ -339,7 +390,7 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
if "chair oral" in text: if "chair oral" in text:
return "chair oral pose" return "chair oral pose"
return "mouth-to-genitals oral pose" return "mouth-to-genitals oral pose"
if "anal" in text or "ass" in text or "rear-entry" in text: if "anal" in text or _mentions_rear_entry(text) or "rear-entry" in text:
if "face-down ass-up" in text: if "face-down ass-up" in text:
return "face-down ass-up rear-entry anal pose" return "face-down ass-up rear-entry anal pose"
if "doggy style" in text or "doggy-style" in text: if "doggy style" in text or "doggy-style" in text:
@@ -381,8 +432,8 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
return "" return ""
def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "") -> str: def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
text = " ".join(_clean(part).lower() for part in (anchor, role_graph, hard_item, composition) if _clean(part)) text = _position_context_text(anchor, f"{role_graph} {hard_item}", composition, axis_values)
if not text: if not text:
return "" return ""
mixed_woman_man = "the woman" in text and "the man" in text mixed_woman_man = "the woman" in text and "the man" in text
@@ -407,8 +458,8 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
if "reclining cunnilingus" in text or "spread-leg oral" in text: if "reclining cunnilingus" in text or "spread-leg oral" in text:
if "takes the man's penis" in text or "penis in her mouth" in text: if "takes the man's penis" in text or "penis in her mouth" in text:
return cast_phrase( return cast_phrase(
"with the man seated or lying back with legs apart and the woman positioned at his hips", "with the man seated with legs apart and the woman positioned at his hips",
"with the receiver seated or lying back with legs apart and the giver positioned at the hips", "with the receiver seated with legs apart and the giver positioned at the hips",
) )
return cast_phrase( return cast_phrase(
"with the woman lying back, thighs spread, and the man positioned between her legs", "with the woman lying back, thighs spread, and the man positioned between her legs",
@@ -510,8 +561,8 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
) )
if "edge-of-bed" in text or "bed-edge" in text: if "edge-of-bed" in text or "bed-edge" in text:
return cast_phrase( return cast_phrase(
f"with the woman lying at the bed edge, hips at the edge, and the man standing or kneeling between her legs{double_tail() if is_double else ''}", f"with the woman lying at the bed edge, hips at the edge, and the man kneeling between her legs{double_tail() if is_double else ''}",
f"with the receiver lying at the bed edge, hips at the edge, and the penetrating partner standing or kneeling between the legs{double_tail() if is_double else ''}", f"with the receiver lying at the bed edge, hips at the edge, and the penetrating partner kneeling between the legs{double_tail() if is_double else ''}",
) )
if "standing" in text: if "standing" in text:
return cast_phrase( return cast_phrase(
@@ -526,8 +577,8 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
if "double-penetration" in text or "double penetration" in text: if "double-penetration" in text or "double penetration" in text:
if "toy" in text: if "toy" in text:
return cast_phrase( return cast_phrase(
"with the woman bent forward or on all fours, the man behind her, and the toy aligned at the second penetration point", "with the woman on all fours, the man behind her, and the toy aligned at the second penetration point",
"with the receiving body bent forward or on all fours and the toy aligned at the second penetration point", "with the receiving body on all fours and the toy aligned at the second penetration point",
) )
if "from the front" in text: if "from the front" in text:
return cast_phrase( return cast_phrase(
@@ -538,7 +589,7 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
"with the woman held in a front-and-back position so both contact points are visible", "with the woman held in a front-and-back position so both contact points are visible",
"with the central body held in a front-and-back position so both contact points are visible", "with the central body held in a front-and-back position so both contact points are visible",
) )
if "anal" in text or "rear-entry" in text: if "anal" in text or _mentions_rear_entry(text) or "rear-entry" in text:
return cast_phrase( return cast_phrase(
"with the woman's hips raised, ass exposed, and the man positioned behind her", "with the woman's hips raised, ass exposed, and the man positioned behind her",
"with the receiving partner's hips raised and the penetrating partner positioned behind", "with the receiving partner's hips raised and the penetrating partner positioned behind",
@@ -570,6 +621,7 @@ def _hardcore_item_detail(hard_item: str) -> str:
return "" return ""
text = re.sub(r"^hardcore\s+", "", text, flags=re.IGNORECASE) text = re.sub(r"^hardcore\s+", "", text, flags=re.IGNORECASE)
text = re.sub(r"^explicit\s+", "", text, flags=re.IGNORECASE) text = re.sub(r"^explicit\s+", "", text, flags=re.IGNORECASE)
text = re.sub(r"^(?:orgasm|climax)\s+scene:\s*", "", text, flags=re.IGNORECASE)
text = re.sub(r"^(?:mouth-to-genitals|double-contact sex|adult group pile|sex pile)\s+pose:\s*", "", text, flags=re.IGNORECASE) text = re.sub(r"^(?:mouth-to-genitals|double-contact sex|adult group pile|sex pile)\s+pose:\s*", "", text, flags=re.IGNORECASE)
text = re.sub(r"^(?:oral|threesome|orgy)\s+scene\s+with\s+", "", text, flags=re.IGNORECASE) text = re.sub(r"^(?:oral|threesome|orgy)\s+scene\s+with\s+", "", text, flags=re.IGNORECASE)
text = re.sub(r"^(?:threesome|orgy)\s+pose:\s*", "", text, flags=re.IGNORECASE) text = re.sub(r"^(?:threesome|orgy)\s+pose:\s*", "", text, flags=re.IGNORECASE)
@@ -591,6 +643,7 @@ def _hardcore_item_detail(hard_item: str) -> str:
r"pussy stretched around a penis", r"pussy stretched around a penis",
r"deep vaginal sex", r"deep vaginal sex",
r"explicit penetrative sex", r"explicit penetrative sex",
r"penetrative sex",
r"hardcore vaginal thrusting", r"hardcore vaginal thrusting",
r"full-body penetrative sex", r"full-body penetrative sex",
r"close-contact vaginal sex", r"close-contact vaginal sex",
@@ -669,6 +722,9 @@ def _dedupe_hardcore_detail(detail: str, anchor: str) -> str:
"cowgirl": (r"cowgirl position",), "cowgirl": (r"cowgirl position",),
"doggy-style": (r"doggy style position",), "doggy-style": (r"doggy style position",),
"edge-of-bed": (r"edge-of-bed position",), "edge-of-bed": (r"edge-of-bed position",),
"lotus": (r"lotus sex position",),
"standing sex": (r"standing sex position",),
"spooning": (r"spooning sex position", r"spooning anal position"),
} }
for anchor_token, phrases in duplicate_phrases.items(): for anchor_token, phrases in duplicate_phrases.items():
if anchor_token in anchor_lower: if anchor_token in anchor_lower:
@@ -679,7 +735,129 @@ def _dedupe_hardcore_detail(detail: str, anchor: str) -> str:
return _clean(detail).strip(" ,;") return _clean(detail).strip(" ,;")
def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str = "") -> str: def _is_climax_text(*parts: str) -> bool:
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
return any(
token in text
for token in (
"cumshot",
"ejaculation",
"post-orgasm",
"post-climax",
"orgasm aftermath",
"orgasm scene",
"orgasm during",
"shared climax",
"hardcore climax",
"external cumshot",
"visible external ejaculation",
"climaxes on",
"climax lands",
)
)
def _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) -> str:
role_graph = _clean(role_graph).rstrip(".")
text = " ".join(part.lower() for part in (role_graph, _clean(hard_item), _axis_values_text(axis_values)) if part)
if "the woman" not in text or "the man" not in text:
return role_graph
if "lying between two partners" in text or "lies between" in text:
return "the woman lies between two partners, the man under her hips and another partner over her torso as the climax lands on her body"
if "held between front-and-back partners" in text:
return "the woman is held between the man behind her and another partner in front of her as the climax lands across her body"
if "kneeling between standing partners" in text:
return "the woman kneels between standing partners gathered around her face and torso as the climax lands across her body"
if "side-lying with thighs parted" in text:
return "the woman lies on her side with thighs parted while the man kneels beside her hips as the climax lands across her thighs and pussy"
if "sitting on the edge of the bed" in text:
return "the woman sits on the edge of the bed with knees spread while the man stands close between her legs as the climax lands across her body"
if "lying at the bed edge with thighs open" in text:
return "the woman lies at the bed edge with thighs open while the man kneels between her legs as the climax lands across her pussy and thighs"
if "reclining with thighs open" in text or "lying on the back with legs spread" in text:
return "the woman lies on her back with thighs open while the man kneels between her legs as the climax lands across her pussy and thighs"
if "on all fours with hips raised" in text:
return "the woman is on all fours with hips raised while the man is positioned behind her as the climax lands across her ass, thighs, and lower back"
if "face-down ass-up" in text:
return "the woman lies face-down with ass raised while the man is positioned behind her as the climax lands across her lower back and ass"
if "bent over with ass raised" in text or "bent over" in text:
return "the woman bends forward with hips raised while the man stands behind her as the climax lands across her lower back, ass, and thighs"
if "kneeling with mouth open" in text:
return "the woman kneels in front of the man at hip height as the climax lands across her face, lips, and chest"
if "kneeling in front of a standing partner" in text:
return "the woman kneels in front of the man at hip height while he stands over her for the climax"
if "standing with cum on the body" in text:
return "the woman stands braced in front of the man while he stands close at hip level as the climax lands across her body"
if "squatting on top of a partner" in text:
return "the woman squats over the man's hips while the man lies on his back under her as the climax lands on her body"
if "reverse cowgirl over a partner's hips" in text:
return "the woman straddles the man's hips facing away while the man lies on his back under her as the climax lands on her body"
if "straddles" in text or "straddling a partner" in text or "straddling a partner's hips" in text or "shared climax after penetration" in text:
return "the woman straddles the man's hips while the man lies on his back under her as the climax lands on her body"
if "seated in a partner's lap facing them" in text:
return "the woman sits in the man's lap facing him, legs wrapped around his hips as the climax lands across her body"
if "lower back" in text or "cum dripping from ass" in text or "cum on lower back" in text or _mentions_rear_entry(text):
return "the woman bends forward with hips raised while the man stands behind her as the climax lands across her lower back, ass, and thighs"
if "cum on face" in text or "cum on tongue" in text or "cum on lips" in text or "cum on tongue and chin" in text:
return "the woman kneels in front of the man at hip height as the climax lands across her face, lips, and chest"
if (
"cum dripping from pussy" in text
or "arousal dripping from pussy" in text
or "open thighs" in text
):
return "the woman lies on her back with thighs open while the man kneels between her legs as the climax lands across her pussy and thighs"
if role_graph:
return role_graph
return "the woman lies on her back with thighs open while the man kneels between her legs as the climax lands across her body"
def _dedupe_climax_detail(detail: str, role_graph: str) -> str:
detail = _clean(detail)
lower = role_graph.lower()
patterns: list[str] = []
if "lies on her back" in lower:
patterns.extend((r"lying on the back with legs spread and hips lifted", r"reclining with thighs open", r"lying on the back with legs spread"))
detail = re.sub(r"\bcum on lower back and ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
detail = re.sub(r"\bcum (?:on|dripping from) ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
if "straddles" in lower:
patterns.extend(
(
r"straddling a partner's hips in cowgirl position",
r"reverse cowgirl over a partner's hips",
r"straddling a partner",
r"squatting on top of a partner",
)
)
if "squats over" in lower:
patterns.append(r"squatting on top of a partner")
if "sits in the man's lap" in lower:
patterns.append(r"seated in a partner's lap facing them")
if "bends forward" in lower:
patterns.append(r"bent over with ass raised")
if "on all fours" in lower:
patterns.append(r"on all fours with hips raised")
if "face-down" in lower:
patterns.append(r"face-down ass-up on the mattress")
if "lies on her side" in lower:
patterns.append(r"side-lying with thighs parted")
if "sits on the edge" in lower:
patterns.append(r"sitting on the edge of the bed")
if "bed edge" in lower:
patterns.append(r"lying at the bed edge with thighs open")
if "kneels in front" in lower:
patterns.extend((r"kneeling with mouth open", r"kneeling in front of a standing partner"))
if "stands braced" in lower:
patterns.append(r"standing with cum on the body")
for pattern in patterns:
detail = re.sub(rf"\b{pattern}\b,?\s*", "", detail, flags=re.IGNORECASE)
detail = re.sub(r",\s*,", ",", detail)
detail = re.sub(r"\bwith\s*,\s*", "with ", detail, flags=re.IGNORECASE)
detail = re.sub(r"^with\s+", "", detail, flags=re.IGNORECASE)
detail = re.sub(r"^and\s+", "", detail, flags=re.IGNORECASE)
return _clean(detail).strip(" ,;")
def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
role_graph = _clean(role_graph).rstrip(".") role_graph = _clean(role_graph).rstrip(".")
hard_item = _clean(hard_item).rstrip(".") hard_item = _clean(hard_item).rstrip(".")
role_graph = re.sub( role_graph = re.sub(
@@ -730,8 +908,11 @@ def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str
role_graph, role_graph,
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
is_climax = _is_climax_text(role_graph, hard_item, composition, _axis_values_text(axis_values))
if is_climax:
role_graph = _climax_role_graph(role_graph, hard_item, axis_values)
detail = _hardcore_item_detail(hard_item) detail = _hardcore_item_detail(hard_item)
anchor = _hardcore_pose_anchor(role_graph, hard_item, composition) anchor = _hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
if "double-penetration" in anchor.lower() and "toy" in role_graph.lower(): if "double-penetration" in anchor.lower() and "toy" in role_graph.lower():
role_graph = re.sub( role_graph = re.sub(
r"\s+while a toy adds (?:the|a) second penetration point\b", r"\s+while a toy adds (?:the|a) second penetration point\b",
@@ -739,8 +920,12 @@ def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str
role_graph, role_graph,
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail if is_climax:
arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition) anchor = ""
detail = _dedupe_climax_detail(detail, role_graph)
else:
detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail
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:
anchor_phrase = f"{anchor_phrase} {arrangement}" anchor_phrase = f"{anchor_phrase} {arrangement}"
@@ -926,7 +1111,8 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
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)
action = _hardcore_action_sentence(role_graph, item, composition) axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {}
action = _hardcore_action_sentence(role_graph, item, composition, axis_values)
parts = [ parts = [
action, action,
cast_prose, cast_prose,
@@ -1020,7 +1206,8 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
hard_role_graph = _sanitize_scene_text_for_cast(hard.get("role_graph"), hard_labels) hard_role_graph = _sanitize_scene_text_for_cast(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_action = _hardcore_action_sentence(hard_role_graph, hard_item, hard_composition) hard_axis_values = hard.get("item_axis_values") if isinstance(hard.get("item_axis_values"), dict) else {}
hard_action = _hardcore_action_sentence(hard_role_graph, hard_item, hard_composition, hard_axis_values)
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
soft_cast_presence = ( soft_cast_presence = (
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"
+55 -4
View File
@@ -2477,6 +2477,57 @@ def _role_graph(
] ]
return f" {extra} {rng.choice(actions)}." return f" {extra} {rng.choice(actions)}."
def mentions_ass(text: str) -> bool:
return bool(
re.search(
r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and",
text,
)
)
def climax_position_graph(woman: str, man: str, third: str = "") -> str:
if "lying between two partners" in item_text and third:
return f"{woman} lies between {man} and {third}, with {man} under her hips and {third} positioned above her torso as the climax lands on her body."
if "held between front-and-back partners" in item_text and third:
return f"{woman} is held between {man} behind her and {third} in front of her as the climax lands across her body."
if "kneeling between standing partners" in item_text and third:
return f"{woman} kneels between {man} and {third} while both stand close around her face and torso for the climax."
if "side-lying with thighs parted" in item_text:
return f"{woman} lies on her side with thighs parted while {man} kneels beside her hips as the climax lands across her thighs and pussy."
if "sitting on the edge of the bed" in item_text:
return f"{woman} sits on the edge of the bed with knees spread while {man} stands close between her legs as the climax lands across her body."
if "lying at the bed edge with thighs open" in item_text:
return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs as the climax lands across her pussy and thighs."
if "reclining with thighs open" in item_text or "lying on the back with legs spread" in item_text:
return f"{woman} lies on her back with thighs open while {man} kneels between her legs as the climax lands across her pussy and thighs."
if "on all fours with hips raised" in item_text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her as the climax lands across her ass, thighs, and lower back."
if "face-down ass-up" in item_text:
return f"{woman} lies face-down with ass raised while {man} is positioned behind her as the climax lands across her lower back and ass."
if "bent over with ass raised" in item_text or "bent over" in item_text:
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, climax visible across her lower back, ass, and thighs."
if "kneeling with mouth open" in item_text:
return f"{woman} kneels in front of {man} at hip height while {man} climaxes toward her face, lips, and chest."
if "kneeling in front of a standing partner" in item_text:
return f"{woman} kneels in front of {man} at hip height while {man} stands over her for the climax."
if "standing with cum on the body" in item_text:
return f"{woman} stands braced in front of {man} while he stays close at hip level and the climax lands across her body."
if "squatting on top of a partner" in item_text:
return f"{woman} squats over {man}'s hips while {man} lies on his back under her as the climax lands on her body."
if "reverse cowgirl over a partner's hips" in item_text:
return f"{woman} straddles {man}'s hips facing away while {man} lies on his back under her as the climax lands on her body."
if any(term in item_text for term in ("straddling a partner", "straddling a partner's hips", "shared climax after penetration", "orgasm during penetration")):
return f"{woman} straddles {man}'s hips while {man} lies on his back under her, their bodies still aligned from penetration as the climax lands on her body."
if "seated in a partner's lap facing them" in item_text:
return f"{woman} sits in {man}'s lap facing him, legs wrapped around his hips as the climax lands across her body."
if any(term in item_text for term in ("lower back", "cum dripping from ass", "cum on lower back")) or mentions_ass(item_text):
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, climax visible across her lower back, ass, and thighs."
if any(term in item_text for term in ("cum on face", "cum on tongue", "cum on lips", "cum on face and lips", "cum on tongue and chin")):
if third:
return f"{woman} kneels in the center while {man} and {third} stand close around her face and torso for the climax."
return f"{woman} kneels in front of {man} at hip height while {man} climaxes toward her face, lips, and chest."
return f"{woman} lies on her back with thighs open while {man} kneels between her legs as the climax lands on her body."
if people_count == 1: if people_count == 1:
solo = people[0] solo = people[0]
if women_count == 1: if women_count == 1:
@@ -2494,7 +2545,7 @@ def _role_graph(
if "oral" in slug: if "oral" in slug:
graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy." graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy."
elif "anal" in slug or "double" in slug: elif "anal" in slug or "double" in slug:
graph = f"{a} uses a strap-on or toy on {b} while keeping her hips held open." graph = f"{a} uses a strap-on on {b} while keeping her hips held open."
elif "threesome" in slug or "group" in slug or "orgy" in slug: elif "threesome" in slug or "group" in slug or "orgy" in slug:
helper = c or any_woman({a}) helper = c or any_woman({a})
graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies." graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies."
@@ -2502,7 +2553,7 @@ def _role_graph(
elif "cumshot" in slug or "climax" in slug: elif "cumshot" in slug or "climax" in slug:
graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets." graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets."
else: else:
graph = f"{a} uses a strap-on or toy on {b} while their bodies stay pressed together." graph = f"{a} uses a strap-on on {b} while their bodies stay pressed together."
return graph + support_sentence(used) return graph + support_sentence(used)
if men_count > 0 and women_count == 0: if men_count > 0 and women_count == 0:
@@ -2555,11 +2606,11 @@ def _role_graph(
else: else:
graph = f"{man} thrusts his penis into {woman}'s ass while keeping her hips held open." graph = f"{man} thrusts his penis into {woman}'s ass while keeping her hips held open."
elif "threesome" in slug: elif "threesome" in slug:
graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth or hands on the exposed body." graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body."
elif "group" in slug or "orgy" in slug: elif "group" in slug or "orgy" in slug:
graph = f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs." graph = f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs."
elif "cumshot" in slug or "climax" in slug: elif "cumshot" in slug or "climax" in slug:
graph = f"{man} climaxes on {woman}'s body while {woman} stays posed with thighs open and direct eye contact." graph = climax_position_graph(woman, man, third)
else: else:
graph = f"{man}'s penis thrusts into {woman} with penetration and body contact visible." graph = f"{man}'s penis thrusts into {woman} with penetration and body contact visible."
return graph + support_sentence({woman, man, third} if third else {woman, man}) return graph + support_sentence({woman, man, third} if third else {woman, man})