1119 lines
54 KiB
Python
1119 lines
54 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
try:
|
|
from .krea_action_context import (
|
|
axis_values_text,
|
|
is_climax_text,
|
|
is_close_foreplay_text,
|
|
is_foreplay_text,
|
|
is_oral_text,
|
|
is_outercourse_text,
|
|
is_toy_assisted_double_text,
|
|
is_vaginal_penetration_text,
|
|
normalize_hardcore_detail_density,
|
|
position_context_text,
|
|
)
|
|
from .krea_detail import detail_clauses, join_detail_clauses, limit_detail_for_density
|
|
except ImportError: # Allows local smoke tests with `python -c`.
|
|
from krea_action_context import (
|
|
axis_values_text,
|
|
is_climax_text,
|
|
is_close_foreplay_text,
|
|
is_foreplay_text,
|
|
is_oral_text,
|
|
is_outercourse_text,
|
|
is_toy_assisted_double_text,
|
|
is_vaginal_penetration_text,
|
|
normalize_hardcore_detail_density,
|
|
position_context_text,
|
|
)
|
|
from krea_detail import detail_clauses, join_detail_clauses, limit_detail_for_density
|
|
|
|
|
|
def _clean(value: Any) -> str:
|
|
text = "" if value is None else str(value)
|
|
text = text.replace("\n", " ")
|
|
text = re.sub(r"\s+", " ", text).strip()
|
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
|
return text
|
|
|
|
|
|
def _lowercase_for_inline_join(text: str) -> str:
|
|
text = _clean(text)
|
|
return text[:1].lower() + text[1:] if text else text
|
|
|
|
|
|
def _with_indefinite_article(text: str) -> str:
|
|
text = _clean(text)
|
|
if not text or text.lower().startswith(("a ", "an ")):
|
|
return text
|
|
article = "an" if text[:1].lower() in "aeiou" else "a"
|
|
return f"{article} {text}"
|
|
|
|
|
|
def _sanitize_foreplay_detail(detail: str, role_graph: str = "", composition: str = "") -> str:
|
|
detail = _clean(detail)
|
|
if not detail:
|
|
return ""
|
|
if not is_close_foreplay_text(role_graph, detail, composition):
|
|
return detail
|
|
detail = re.sub(
|
|
r"\b(?:raised edge|edge-supported|edge-of-bed|bed-edge)\s+undressing position\s+(?:featuring|while|with)\s+",
|
|
"",
|
|
detail,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
detail = re.sub(
|
|
r"\b(?:standing kissing|wall-pressed kissing|mirror undressing)\s+position\s+(?:featuring|while|with)\s+",
|
|
"",
|
|
detail,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
detail = re.sub(
|
|
r"\b(?:raised edge|edge-supported|edge-of-bed|bed-edge)\s+undressing position\b",
|
|
"close standing undressing",
|
|
detail,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
detail = re.sub(r"\braised-edge open-thigh position\b", "close-body first-person position", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"\s*,\s*", ", ", detail).strip(" ,;")
|
|
return _clean(detail)
|
|
|
|
|
|
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)
|
|
position_text = ""
|
|
if isinstance(axis_values, dict):
|
|
position_text = _clean(axis_values.get("position", "")).lower()
|
|
if not text:
|
|
return ""
|
|
if is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
|
return ""
|
|
if is_outercourse_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
|
if any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex")):
|
|
return "breast-sex outercourse pose"
|
|
if any(term in text for term in ("testicle", "balls licking", "balls-licking", "balls and mouth")):
|
|
return "testicle-sucking outercourse pose"
|
|
if any(term in text for term in ("penis licking", "penis-licking", "tongue along", "tongue licking")):
|
|
return "penis-licking outercourse pose"
|
|
if any(term in text for term in ("handjob", "hand job", "hand wrapped", "hand stroking", "manual stimulation")):
|
|
return "handjob outercourse pose"
|
|
if any(term in text for term in ("footjob", "soles", "toes curled", "feet stroking")):
|
|
return "footjob outercourse pose"
|
|
return "non-penetrative outercourse pose"
|
|
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
|
if "face-down ass-up" in text or "face-down" in text:
|
|
return "toy-assisted face-down rear-entry double-penetration pose"
|
|
if "doggy style" in text or "doggy-style" in text or "all fours" in text or "rear-entry" in text:
|
|
return "toy-assisted rear-entry double-penetration pose"
|
|
if "bent-over" in text or "bent forward" in text:
|
|
return "toy-assisted bent-over double-penetration pose"
|
|
if "spooning anal" in text or "side-lying anal" in text or "side-lying" in text:
|
|
return "toy-assisted side-lying double-penetration pose"
|
|
if "edge-supported" in text or "bed-edge" in text or "edge-of-bed" in text:
|
|
return "toy-assisted edge-supported double-penetration pose"
|
|
if "standing anal" in text or "standing supported" in text or "standing" in text:
|
|
return "toy-assisted standing double-penetration pose"
|
|
if "kneeling anal" in text or "kneeling" in text:
|
|
return "toy-assisted kneeling rear-entry double-penetration pose"
|
|
return "toy-assisted rear-entry double-penetration pose"
|
|
if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text:
|
|
if "face-down ass-up" in text:
|
|
return "face-down rear-entry double-penetration pose"
|
|
if "doggy style" in text or "doggy-style" in text:
|
|
return "doggy-style double-penetration pose"
|
|
if "bent-over" in text:
|
|
return "bent-over double-penetration pose"
|
|
if "spooning anal" in text or "side-lying anal" in text:
|
|
return "side-lying double-penetration pose"
|
|
if "bed-edge" in text or "edge-of-bed" in text:
|
|
return "bed-edge front-and-back double-penetration pose"
|
|
if "standing anal" in text or "standing supported" in text:
|
|
return "standing supported front-and-back double-penetration pose"
|
|
if "kneeling anal" in text:
|
|
return "kneeling rear-entry double-penetration pose"
|
|
if "standing supported" in text:
|
|
return "standing supported front-and-back double-penetration pose"
|
|
if "kneeling" in text:
|
|
return "kneeling front-and-back double-penetration pose"
|
|
return "front-and-back double-penetration pose"
|
|
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
|
|
return "sixty-nine oral pose"
|
|
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
|
|
return "face-sitting oral pose"
|
|
if "side-lying oral" in position_text or (("side-lying oral position" in item_text or "side-lying oral" in text) and not position_text):
|
|
return "side-lying oral pose"
|
|
if (
|
|
"edge-of-bed oral" in position_text
|
|
or "edge-supported oral" in position_text
|
|
or (("edge-of-bed oral position" in item_text or "edge-of-bed oral" in text or "edge-supported oral" in text) and not position_text)
|
|
):
|
|
return "edge-supported oral pose"
|
|
if "standing oral" in position_text or (("standing oral position" in item_text or "standing oral" in text) and not position_text):
|
|
return "standing oral pose"
|
|
if "chair oral" in position_text or (("chair oral position" in item_text or "chair oral" in text) and not position_text):
|
|
return "chair oral pose"
|
|
if "kneeling oral" in position_text or (("kneeling oral position" in item_text or "kneeling oral" in text) and not position_text):
|
|
return "kneeling oral pose"
|
|
if "straddled oral" in position_text or (("straddled oral position" in item_text or "straddled oral" in text) and not position_text):
|
|
return "straddled cunnilingus pose"
|
|
if "reclining cunnilingus" in position_text or (("reclining cunnilingus position" in item_text or "reclining cunnilingus" in text) and not position_text):
|
|
return "reclining cunnilingus pose"
|
|
if "spread-leg oral" in position_text or (("spread-leg oral position" in item_text or "spread-leg oral" in text) and not position_text):
|
|
return "spread-leg oral pose"
|
|
if "cunnilingus" in text or "pussy licking" in text or "mouth on her pussy" in text:
|
|
if "reclining" in text:
|
|
return "reclining cunnilingus pose"
|
|
if "straddled" in text:
|
|
return "straddled cunnilingus pose"
|
|
return "open-thigh cunnilingus pose"
|
|
if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text:
|
|
if "side-lying oral position" in item_text:
|
|
return "side-lying oral pose"
|
|
if "spread-leg oral position" in item_text:
|
|
return "spread-leg oral pose"
|
|
if "edge-of-bed oral position" in item_text:
|
|
return "edge-supported oral pose"
|
|
if "standing oral position" in item_text:
|
|
return "standing oral pose"
|
|
if "chair oral position" in item_text:
|
|
return "chair oral pose"
|
|
if "kneeling oral position" in item_text or "kneeling" in text:
|
|
return "kneeling oral pose"
|
|
if "standing" in text:
|
|
return "standing oral pose"
|
|
if "side-lying" in text:
|
|
return "side-lying oral pose"
|
|
if "edge-of-bed" in text or "bed-edge" in text:
|
|
return "edge-supported oral pose"
|
|
if "spread-leg" in text:
|
|
return "spread-leg oral pose"
|
|
if "chair oral" in text:
|
|
return "chair oral pose"
|
|
return "mouth-to-genitals oral pose"
|
|
if "anal" in text or _mentions_rear_entry(text) or "rear-entry" in text:
|
|
if "face-down ass-up" in text:
|
|
return "face-down ass-up rear-entry anal pose"
|
|
if "doggy style" in text or "doggy-style" in text:
|
|
return "doggy-style anal pose"
|
|
if "bed-edge" in text or "edge-of-bed" in text:
|
|
return "bed-edge rear-entry anal pose"
|
|
if "bent-over" in text:
|
|
return "bent-over rear-entry anal pose"
|
|
if "spooning anal" in text or "side-lying anal" in text:
|
|
return "side-lying rear-entry anal pose"
|
|
if "kneeling anal" in text:
|
|
return "kneeling rear-entry anal pose"
|
|
if "standing anal" in text:
|
|
return "standing rear-entry anal pose"
|
|
if "doggy" in text:
|
|
return "doggy-style anal pose"
|
|
return "rear-entry anal pose"
|
|
if "edge-supported" in text or "raised edge" in text or "edge-of-bed" in text or "bed-edge" in text:
|
|
return "edge-supported penetrative sex pose"
|
|
positions = (
|
|
"missionary",
|
|
"reverse cowgirl",
|
|
"cowgirl",
|
|
"doggy style",
|
|
"standing sex",
|
|
"spooning sex",
|
|
"edge-of-bed",
|
|
"kneeling straddle",
|
|
"lotus",
|
|
"bent-over",
|
|
)
|
|
for position in positions:
|
|
if position in text:
|
|
return f"{position.replace('doggy style', 'doggy-style')} pose"
|
|
if "threesome" in text or "three-body" in text:
|
|
return "three-body explicit sex pose"
|
|
if "group" in text or "orgy" in text:
|
|
return "multi-body explicit sex pose"
|
|
if re.search(r"(?<!non-)penetrat|thrust", text):
|
|
return "hip-aligned penetrative sex pose"
|
|
return ""
|
|
|
|
|
|
def hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
|
text = position_context_text(anchor, f"{role_graph} {hard_item}", composition, axis_values)
|
|
position_text = ""
|
|
if isinstance(axis_values, dict):
|
|
position_text = _clean(axis_values.get("position", "")).lower()
|
|
if not text:
|
|
return ""
|
|
mixed_woman_man = "the woman" in text and "the man" in text
|
|
is_double = "double-penetration" in text or "double penetration" in text
|
|
|
|
def cast_phrase(mixed: str, generic: str) -> str:
|
|
return mixed if mixed_woman_man else generic
|
|
|
|
def double_tail() -> str:
|
|
return "" if "toy" in text else ", with the second penetration point aligned"
|
|
|
|
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
|
|
return cast_phrase(
|
|
"with the woman and man inverted head-to-hips so both mouths align with genitals",
|
|
"with both bodies inverted head-to-hips so both mouths align with genitals",
|
|
)
|
|
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
|
|
return cast_phrase(
|
|
"with the man lying back while the woman straddles his face",
|
|
"with one partner lying back while the other straddles the face",
|
|
)
|
|
if (
|
|
"reclining cunnilingus" in position_text
|
|
or "spread-leg oral" in position_text
|
|
or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text)
|
|
):
|
|
if "takes the man's penis" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the man seated with legs apart and the woman positioned at his hips",
|
|
"with the receiver seated with legs apart and the giver positioned at the hips",
|
|
)
|
|
return cast_phrase(
|
|
"with the woman lying back, thighs spread, and the man positioned between her legs",
|
|
"with the receiving partner lying back, thighs spread, and the giver positioned between the legs",
|
|
)
|
|
if (
|
|
"straddled oral" in position_text
|
|
or (("straddled cunnilingus" in text or "straddled oral" in text) and not position_text)
|
|
):
|
|
return cast_phrase(
|
|
"with the woman straddling above the man's mouth and her thighs framing his face",
|
|
"with the receiver straddling above the giver's mouth",
|
|
)
|
|
if (
|
|
"edge-of-bed oral" in position_text
|
|
or "edge-supported oral" in position_text
|
|
or ("edge-of-bed oral" in text and not position_text)
|
|
or ("edge-supported oral" in text and not position_text)
|
|
):
|
|
if "takes the man's penis" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the man at a raised edge and the woman kneeling at his hips",
|
|
"with the receiver at a raised edge and the giver positioned at hip height",
|
|
)
|
|
return cast_phrase(
|
|
"with the woman lying at a raised edge and the man positioned between her open thighs",
|
|
"with the receiver lying at a raised edge and the giver positioned between open thighs",
|
|
)
|
|
if "standing oral" in position_text or ("standing oral" in text and not position_text):
|
|
if "takes the man's penis" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the man standing and the woman kneeling in front of his hips",
|
|
"with the receiver standing and the giver kneeling at hip height",
|
|
)
|
|
return cast_phrase(
|
|
"with the woman standing braced and the man kneeling between her thighs",
|
|
"with the receiver standing braced and the giver kneeling between the thighs",
|
|
)
|
|
if "chair oral" in position_text or ("chair oral" in text and not position_text):
|
|
if "takes the man's penis" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the man seated in the chair and the woman kneeling between his legs at hip level",
|
|
"with the receiver seated in the chair and the giver kneeling between the legs at hip level",
|
|
)
|
|
return cast_phrase(
|
|
"with one partner seated in a chair and the other kneeling between the open thighs",
|
|
"with the receiver seated in a chair and the giver kneeling between the open thighs",
|
|
)
|
|
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
|
|
return "with both bodies lying on their sides and mouth aligned to genitals"
|
|
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
|
|
if "takes the man's penis" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the woman kneeling in front of the man's hips, her mouth at penis level",
|
|
"with the giver kneeling in front of the receiver's hips",
|
|
)
|
|
if "mouth on her pussy" in text or "uses his mouth on" in text:
|
|
return cast_phrase(
|
|
"with the man kneeling between the woman's open thighs, his mouth at her pussy",
|
|
"with the giver kneeling between the receiver's open thighs",
|
|
)
|
|
return "with the giver kneeling at the receiver's hips"
|
|
if "reverse cowgirl" in text:
|
|
return cast_phrase(
|
|
"with the man lying on his back under the woman while she straddles his hips facing away",
|
|
"with the lower partner lying on their back while the upper partner straddles them facing away",
|
|
)
|
|
if "cowgirl" in text:
|
|
return cast_phrase(
|
|
"with the man lying on his back under the woman while she straddles his hips on top",
|
|
"with the lower partner lying on their back while the upper partner straddles their hips on top",
|
|
)
|
|
if "missionary" in text:
|
|
return cast_phrase(
|
|
"with the woman lying on her back under the man, legs open around his hips",
|
|
"with the receiving partner lying on their back under the penetrating partner, legs open around the hips",
|
|
)
|
|
if "lotus" in text:
|
|
return cast_phrase(
|
|
"with the man seated upright and the woman seated in his lap facing him, legs wrapped around his hips",
|
|
"with one partner seated upright and the other seated in their lap facing them, legs wrapped around the hips",
|
|
)
|
|
if "kneeling straddle" in text:
|
|
return cast_phrase(
|
|
"with the woman straddling the man's kneeling lap, both torsos upright and hips pressed together",
|
|
"with one partner straddling the other's kneeling lap, torsos upright and hips pressed together",
|
|
)
|
|
if "doggy-style" in text:
|
|
return cast_phrase(
|
|
f"with the woman on all fours and the man positioned behind her at hip level{double_tail() if is_double else ''}",
|
|
f"with the receiving partner on all fours and the penetrating partner positioned behind at hip level{double_tail() if is_double else ''}",
|
|
)
|
|
if "face-down" in text:
|
|
return cast_phrase(
|
|
f"with the woman face-down, hips raised, and the man positioned behind her{double_tail() if is_double else ''}",
|
|
f"with the receiving partner face-down, hips raised, and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "bent-over" in text:
|
|
return cast_phrase(
|
|
f"with the woman bent forward at the waist and the man positioned behind her{double_tail() if is_double else ''}",
|
|
f"with the receiving partner bent forward at the waist and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "spooning" in text or ("side-lying" in text and "oral" not in text):
|
|
return cast_phrase(
|
|
f"with both lying on their sides and the man positioned behind the woman{double_tail() if is_double else ''}",
|
|
f"with both bodies lying on their sides and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "edge-of-bed" in text or "bed-edge" in text:
|
|
return cast_phrase(
|
|
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 kneeling between the legs{double_tail() if is_double else ''}",
|
|
)
|
|
if "standing" in text:
|
|
return cast_phrase(
|
|
f"with the woman braced standing and the man aligned at her hips{double_tail() if is_double else ''}",
|
|
f"with both partners standing and the penetrating partner aligned at the receiver's hips{double_tail() if is_double else ''}",
|
|
)
|
|
if "kneeling" in text and ("anal" in text or "rear-entry" in text):
|
|
return cast_phrase(
|
|
f"with the woman kneeling forward and the man positioned behind her{double_tail() if is_double else ''}",
|
|
f"with the receiving partner kneeling forward and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "double-penetration" in text or "double penetration" in text:
|
|
if "toy" in text:
|
|
return cast_phrase(
|
|
"with the woman on all fours and the man positioned behind her at hip level",
|
|
"with the receiving body on all fours and the penetrating partner positioned behind at hip level",
|
|
)
|
|
if "from the front" in text:
|
|
return cast_phrase(
|
|
"with the woman held between the man behind her and a second partner in front",
|
|
"with the receiving body held between one partner behind and a second partner in front",
|
|
)
|
|
return cast_phrase(
|
|
"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",
|
|
)
|
|
if "anal" in text or _mentions_rear_entry(text) or "rear-entry" in text:
|
|
return cast_phrase(
|
|
"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",
|
|
)
|
|
if "cunnilingus" in text or "mouth on her pussy" in text or "pussy licking" in text:
|
|
return cast_phrase(
|
|
"with the woman's thighs open and the man's mouth pressed to her pussy",
|
|
"with the receiver's thighs open and the giver's mouth pressed to genitals",
|
|
)
|
|
if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text:
|
|
if "takes the man's penis in her mouth" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the woman's mouth at the man's hips",
|
|
"with the giver's mouth positioned at the receiver's hips",
|
|
)
|
|
return "with mouth and genitals aligned clearly"
|
|
if "threesome" in text or "three-body" in text:
|
|
return "with all three adult bodies clearly placed around the central subject"
|
|
if "group" in text or "orgy" in text:
|
|
return "with each adult body readable in the shared sex act"
|
|
if re.search(r"(?<!non-)penetrat|thrust", text):
|
|
return "with hips aligned and legs open around the contact point"
|
|
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:
|
|
return ""
|
|
text = re.sub(r"^hardcore\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"^(?: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)
|
|
act_patterns = (
|
|
r"(?:penis and toy|toy and strap-on|toy-assisted|front-and-back|hardcore|deep|kneeling|standing supported)?\s*double penetration",
|
|
r"toy-assisted vaginal and anal penetration at the same time",
|
|
r"vaginal and anal penetration at the same time",
|
|
r"one penis in pussy and one penis in ass",
|
|
r"anal penetration with visible genital contact",
|
|
r"rear-entry anal penetration",
|
|
r"anal sex with spread cheeks",
|
|
r"ass stretched around a penis",
|
|
r"penis entering ass",
|
|
r"deep anal sex",
|
|
r"bent-over anal sex",
|
|
r"hardcore anal thrusting",
|
|
r"vaginal penetration with visible genital contact",
|
|
r"penis entering pussy",
|
|
r"pussy stretched around a penis",
|
|
r"deep vaginal sex",
|
|
r"explicit penetrative sex",
|
|
r"penetrative sex",
|
|
r"hardcore vaginal thrusting",
|
|
r"full-body penetrative sex",
|
|
r"close-contact vaginal sex",
|
|
r"fellatio with penis in mouth",
|
|
r"deepthroat blowjob",
|
|
r"blowjob",
|
|
r"penis sucking with visible saliva",
|
|
r"cunnilingus with tongue on pussy",
|
|
r"face-sitting cunnilingus",
|
|
r"pussy licking with thighs spread",
|
|
r"oral sex with tongue and fingers",
|
|
r"oral contact with mouth on the visible genitals",
|
|
r"sixty-nine oral sex",
|
|
)
|
|
act_pattern = "|".join(act_patterns)
|
|
position_pattern = (
|
|
r"missionary position|cowgirl position|reverse cowgirl position|doggy style position|"
|
|
r"standing sex position|spooning sex position|edge-of-bed position|kneeling straddle position|"
|
|
r"lotus sex position|bent-over position|kneeling oral position|face-sitting position|"
|
|
r"sixty-nine position|edge-of-bed oral position|edge-supported oral position|standing oral position|reclining cunnilingus position|"
|
|
r"straddled oral position|side-lying oral position|spread-leg oral position|chair oral position"
|
|
)
|
|
text = re.sub(
|
|
rf"^({position_pattern})\s+(?:while|with|featuring)\s+(?:{act_pattern})\s*,?\s*",
|
|
r"\1, ",
|
|
text,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
text = re.sub(
|
|
rf"^(?:{act_pattern})\s*(?:in|from|on|with|while|featuring)?\s*",
|
|
"",
|
|
text,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
text = re.sub(r"^(?:position|pose)\s+", "", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"^with\s+", "", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\bwith with\b", "with", text, flags=re.IGNORECASE)
|
|
text = re.sub(r",\s*with\s+", ", ", text, flags=re.IGNORECASE)
|
|
text = re.sub(r",\s+and\s+", ", ", text)
|
|
text = re.sub(r"\s*,\s*", ", ", text).strip(" ,;")
|
|
return _clean(text)
|
|
|
|
|
|
def _dedupe_hardcore_detail(detail: str, anchor: str) -> str:
|
|
detail = _clean(detail)
|
|
anchor_lower = anchor.lower()
|
|
duplicate_phrases = {
|
|
"front-and-back": (r"front-and-back contact",),
|
|
"side-lying oral": (r"side-lying oral position",),
|
|
"kneeling oral": (r"kneeling oral position",),
|
|
"face-sitting": (r"face-sitting position",),
|
|
"sixty-nine": (
|
|
r"sixty-nine position",
|
|
r"sixty-nine oral sex",
|
|
r"kneeling oral position",
|
|
r"face-sitting position",
|
|
r"edge-of-bed oral position",
|
|
r"standing oral position",
|
|
r"reclining cunnilingus position",
|
|
r"straddled oral position",
|
|
r"side-lying oral position",
|
|
r"spread-leg oral position",
|
|
r"chair oral position",
|
|
),
|
|
"edge-supported oral": (r"edge-of-bed oral position", r"edge-supported oral position"),
|
|
"edge-of-bed oral": (r"edge-of-bed oral position", r"edge-supported oral position"),
|
|
"standing oral": (r"standing oral position",),
|
|
"spread-leg oral": (r"spread-leg oral position",),
|
|
"chair oral": (r"chair oral position",),
|
|
"reclining cunnilingus": (r"reclining cunnilingus position",),
|
|
"straddled cunnilingus": (r"straddled oral position", r"straddled cunnilingus position"),
|
|
"open-thigh cunnilingus": (r"reclining cunnilingus position", r"straddled cunnilingus position"),
|
|
"bent-over": (r"bent-over position",),
|
|
"face-down": (r"face-down ass-up position",),
|
|
"missionary": (r"missionary position",),
|
|
"reverse cowgirl": (r"reverse cowgirl position",),
|
|
"cowgirl": (r"cowgirl position",),
|
|
"doggy-style": (r"doggy style position",),
|
|
"edge-supported": (r"edge-of-bed position", r"edge-supported position", r"raised edge position"),
|
|
"edge-of-bed": (r"edge-of-bed position", r"edge-supported 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():
|
|
if anchor_token in anchor_lower:
|
|
for phrase in phrases:
|
|
detail = re.sub(rf"\b{phrase}\b,?\s*", "", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"^\s*,\s*", "", detail)
|
|
detail = re.sub(r",\s*,", ",", detail)
|
|
return _clean(detail).strip(" ,;")
|
|
|
|
|
|
def _dedupe_toy_double_detail(detail: str) -> str:
|
|
detail = _clean(detail)
|
|
if not detail:
|
|
return ""
|
|
angle_view = (
|
|
r"(?:rear-view|side-profile|low-angle|mirror-reflected|overhead|close-up|wide full-body|front-facing with hips turned)"
|
|
)
|
|
toy_act = (
|
|
r"(?:penis and toy double penetration|toy-assisted vaginal and anal penetration at the same time|toy and strap-on double penetration)"
|
|
)
|
|
detail = re.sub(
|
|
rf"\b({angle_view}\s+view of\s+){toy_act}\b",
|
|
r"\1the rear-entry contact",
|
|
detail,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
detail = re.sub(rf",?\s*\b{toy_act}\b", "", detail, flags=re.IGNORECASE)
|
|
duplicate_phrases = (
|
|
"toy-assisted second contact aligned behind the body",
|
|
"toy aligned for a second penetration point",
|
|
"rear-entry body alignment",
|
|
"close body alignment",
|
|
"stacked bodies in close contact",
|
|
"one body between two partners",
|
|
"one partner behind and one partner in front",
|
|
"two partners penetrating at once",
|
|
"one partner held between two bodies",
|
|
"front-and-back contact",
|
|
"three bodies locked together",
|
|
"kneeling center partner",
|
|
)
|
|
for phrase in duplicate_phrases:
|
|
detail = re.sub(rf",?\s*\b{re.escape(phrase)}\b", "", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"^\s*,\s*", "", detail)
|
|
detail = re.sub(r",\s*,", ",", detail)
|
|
return _clean(detail).strip(" ,;")
|
|
|
|
|
|
def _dedupe_outercourse_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
|
detail = _clean(detail)
|
|
if not detail:
|
|
return ""
|
|
context = position_context_text(role_graph, hard_item, "", axis_values)
|
|
context_lower = context.lower()
|
|
breast_sex = any(term in context_lower for term in ("boobjob", "titjob", "breast sex", "breast-sex"))
|
|
clauses: list[str] = []
|
|
for clause in detail_clauses(detail):
|
|
lower = clause.lower()
|
|
if breast_sex:
|
|
if lower in ("penis", "breasts", "mouth clearly visible"):
|
|
continue
|
|
if any(
|
|
term in lower
|
|
for term in (
|
|
"boobjob",
|
|
"titjob",
|
|
"breast-sex",
|
|
"breast sex",
|
|
"seated titjob position",
|
|
"kneeling boobjob position",
|
|
"tight close-up breast-sex position",
|
|
"penis shaft compressed between breasts",
|
|
"penis squeezed between both breasts",
|
|
"hands pressing the breasts tightly",
|
|
"hands pressing breasts firmly together",
|
|
"fingers spreading the breasts around the penis shaft",
|
|
"soft flesh squeezed around the penis shaft",
|
|
"hand wrapped around the penis shaft",
|
|
"glans near the mouth",
|
|
"glans visible",
|
|
"penis, breasts, and mouth clearly visible",
|
|
)
|
|
):
|
|
continue
|
|
clauses.append(clause)
|
|
return join_detail_clauses(clauses)
|
|
|
|
|
|
def _dedupe_oral_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
|
detail = _clean(detail)
|
|
if not detail:
|
|
return ""
|
|
context = position_context_text(role_graph, hard_item, "", axis_values)
|
|
woman_gives = any(
|
|
term in context
|
|
for term in (
|
|
"takes the man's penis",
|
|
"takes his penis",
|
|
"penis in her mouth",
|
|
"mouth at penis level",
|
|
"mouth on his penis",
|
|
"fellatio",
|
|
"blowjob",
|
|
"deepthroat",
|
|
"penis sucking",
|
|
)
|
|
)
|
|
clauses: list[str] = []
|
|
for clause in detail_clauses(detail):
|
|
lower = clause.lower()
|
|
if any(
|
|
term in lower
|
|
for term in (
|
|
"kneeling oral position",
|
|
"standing oral position",
|
|
"edge-of-bed oral position",
|
|
"side-lying oral position",
|
|
"chair oral position",
|
|
"reclining cunnilingus position",
|
|
"face-sitting position",
|
|
"sixty-nine position",
|
|
"fellatio with penis in mouth",
|
|
"deepthroat blowjob",
|
|
"penis sucking with visible saliva",
|
|
"cunnilingus with tongue on pussy",
|
|
"oral sex with tongue and fingers",
|
|
"oral contact with mouth on the visible genitals",
|
|
"bodies stacked close together",
|
|
"body angle keeps the penis and face readable",
|
|
)
|
|
):
|
|
continue
|
|
if woman_gives and lower == "wet shine on genitals":
|
|
clause = "saliva dripping on the penis"
|
|
clauses.append(clause)
|
|
return join_detail_clauses(clauses)
|
|
|
|
|
|
def _dedupe_penetration_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
|
|
detail = _clean(detail)
|
|
if not detail:
|
|
return ""
|
|
role_lower = _clean(role_graph).lower()
|
|
detail = re.sub(
|
|
r"\b(?:front-facing|side-profile|rear-view|overhead|mirror-reflected|low-angle|close-up|wide full-body)\s+view of\s+"
|
|
r"(?:vaginal penetration with visible genital contact|deep vaginal sex|explicit penetrative sex|penetrative sex|"
|
|
r"penis entering pussy|pussy stretched around a penis|hardcore vaginal thrusting|full-body penetrative sex|"
|
|
r"close-contact vaginal sex)\b,?\s*",
|
|
"",
|
|
detail,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
act_terms = (
|
|
"vaginal penetration with visible genital contact",
|
|
"deep vaginal sex",
|
|
"explicit penetrative sex",
|
|
"penetrative sex",
|
|
"penis entering pussy",
|
|
"pussy stretched around a penis",
|
|
"hardcore vaginal thrusting",
|
|
"full-body penetrative sex",
|
|
"close-contact vaginal sex",
|
|
"missionary position",
|
|
"cowgirl position",
|
|
"reverse cowgirl position",
|
|
"doggy style position",
|
|
"standing sex position",
|
|
"spooning sex position",
|
|
"edge-of-bed position",
|
|
"kneeling straddle position",
|
|
"lotus sex position",
|
|
"bent-over position",
|
|
)
|
|
clauses: list[str] = []
|
|
for clause in detail_clauses(detail):
|
|
lower = clause.lower()
|
|
if any(term in lower for term in act_terms):
|
|
continue
|
|
if lower in (
|
|
"tongues visible while kissing",
|
|
"deep kissing",
|
|
"mouth close to the ear",
|
|
"neck kissing",
|
|
"explicit genital contact visible",
|
|
"genitals clearly visible",
|
|
"anatomically clear penetration",
|
|
"pussy and penis visible",
|
|
"wetness visible between the thighs",
|
|
):
|
|
continue
|
|
if lower in ("legs spread wide", "thighs open toward the viewer") and any(
|
|
term in role_lower for term in ("legs spread wide", "thighs open", "open thighs")
|
|
):
|
|
continue
|
|
if lower == "one body pinned under another" and "lies under" in role_lower:
|
|
continue
|
|
if lower in ("hips locked tightly together", "hips aligned") and "hips" in role_lower:
|
|
continue
|
|
if lower in ("hands gripping hips", "hands spreading the thighs") and any(
|
|
term in role_lower for term in ("hips", "thighs", "legs")
|
|
):
|
|
continue
|
|
clauses.append(clause)
|
|
return join_detail_clauses(clauses)
|
|
|
|
|
|
def action_position_phrase(action: str) -> str:
|
|
action = _clean(action).lower()
|
|
if is_close_foreplay_text(action):
|
|
return "single-frame close-body first-person position"
|
|
if "pov reverse cowgirl" in action:
|
|
return "reverse-cowgirl first-person position"
|
|
if "pov cowgirl" in action:
|
|
return "cowgirl first-person position"
|
|
if "pov missionary" in action:
|
|
return "missionary first-person position"
|
|
if "pov raised-edge" in action or "raised edge" in action:
|
|
return "raised-edge open-thigh position"
|
|
if "pov doggy" in action or "on all fours" in action:
|
|
return "all-fours rear-entry position"
|
|
if "pov bent-over" in action or "bent forward" in action:
|
|
return "bent-over rear-entry position"
|
|
if "pov face-down" in action:
|
|
return "face-down rear-entry position"
|
|
if "pov standing" in action:
|
|
return "standing rear-entry position"
|
|
if "pov side-lying" in action:
|
|
return "side-lying position"
|
|
if "pov lotus" in action:
|
|
return "lap-straddling position"
|
|
if "face-down" in action and "ass raised" in action:
|
|
return "face-down raised-hip position"
|
|
if "on all fours" in action:
|
|
return "all-fours raised-hip position"
|
|
if "bends forward" in action or "bent forward" in action:
|
|
return "bent-over raised-hip position"
|
|
if "lies on her back" in action and ("thighs open" in action or "legs open" in action):
|
|
return "open-thigh reclined position"
|
|
if "lies at the bed edge" in action or "bed edge" in action:
|
|
return "bed-edge position"
|
|
if "lies on her side" in action:
|
|
return "side-lying position"
|
|
if "kneels in front" in action:
|
|
return "kneeling-at-hip-height position"
|
|
if "straddles" in action or "squats over" in action:
|
|
return "straddling position"
|
|
if "sits in the man's lap" in action:
|
|
return "lap-straddling position"
|
|
if "stands braced" in action:
|
|
return "standing braced position"
|
|
if "held between" in action or "front-and-back" in action:
|
|
return "front-and-back position"
|
|
if "lies between" in action:
|
|
return "between-partners position"
|
|
return ""
|
|
|
|
|
|
def _normalize_climax_view_clause(clause: str, role_graph: str) -> str:
|
|
lower = clause.lower()
|
|
if "view" not in lower and "frame" not in lower:
|
|
return clause
|
|
angle_match = re.search(
|
|
r"\b(front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\b",
|
|
lower,
|
|
)
|
|
if not angle_match:
|
|
return clause
|
|
angle = angle_match.group(1)
|
|
if angle == "wide":
|
|
angle = "wide full-body"
|
|
position = action_position_phrase(role_graph)
|
|
if position:
|
|
return f"{angle} aftermath view with the {position} readable"
|
|
return f"{angle} aftermath view"
|
|
|
|
|
|
def _climax_clause_duplicates_role(clause: str, role_graph: str) -> bool:
|
|
clause_lower = clause.lower()
|
|
role_lower = role_graph.lower()
|
|
role_has_ejaculation = any(token in role_lower for token in ("ejaculates semen", "visible semen", "semen lands"))
|
|
if role_has_ejaculation and re.search(
|
|
r"\b(?:cum clearly visible|explicit semen aftermath visible|hardcore ejaculation detail visible|"
|
|
r"post-ejaculation fluids anatomically clear|sexual fluids and body contact visible|"
|
|
r"visible external ejaculation|hardcore ejaculation scene|visible orgasm aftermath)\b",
|
|
clause_lower,
|
|
):
|
|
return True
|
|
duplicate_pairs = (
|
|
(("lower back", "ass"), ("lower back", "ass")),
|
|
(("ass",), ("ass",)),
|
|
(("pussy", "thigh"), ("pussy", "thigh")),
|
|
(("face", "lips"), ("face", "lips")),
|
|
(("tongue", "chin"), ("face", "lips", "mouth", "tongue")),
|
|
(("breast",), ("breast", "chest")),
|
|
(("belly",), ("belly", "torso")),
|
|
(("body",), ("body",)),
|
|
)
|
|
if any(token in clause_lower for token in ("cum", "semen", "fluid")):
|
|
for clause_tokens, role_tokens in duplicate_pairs:
|
|
if any(token in clause_lower for token in clause_tokens) and any(token in role_lower for token in role_tokens):
|
|
return True
|
|
return False
|
|
|
|
|
|
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 visible semen 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 visible semen lands across her body"
|
|
if "kneeling between standing partners" in text:
|
|
return "the woman kneels between standing partners gathered around her face and torso for visible ejaculation"
|
|
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 and ejaculates semen 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 and ejaculates semen 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 and ejaculates semen 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 and ejaculates semen 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 and ejaculates semen across her ass, thighs, and lower back"
|
|
if "face-down ass-up" in text or "lies face-down" in text or "face down" in text:
|
|
return "the woman lies face-down with ass raised while the man is positioned behind her and ejaculates semen 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 with visible semen 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 he ejaculates semen onto 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 visible ejaculation"
|
|
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 and ejaculates semen 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 and ejaculates semen onto 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 and ejaculates semen onto 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 and ejaculates semen onto 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 he ejaculates semen 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 with visible semen 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 he ejaculates semen onto 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 and ejaculates semen 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 and ejaculates semen across her body"
|
|
|
|
|
|
def _dedupe_climax_detail(detail: str, role_graph: str, density: str = "balanced") -> 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)
|
|
if not any(token in lower for token in ("face", "mouth", "lips", "tongue")):
|
|
detail = re.sub(r"\bsaliva and cum mixed on the mouth\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"\bcum on tongue and chin\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"\bcum on face and lips\b", "visible semen on skin", 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)
|
|
clauses: list[str] = []
|
|
for clause in detail_clauses(detail):
|
|
normalized = _normalize_climax_view_clause(clause, role_graph)
|
|
if _climax_clause_duplicates_role(normalized, role_graph):
|
|
continue
|
|
if density != "dense" and normalized.lower() in ("orgasm during penetration", "post-orgasm visible release"):
|
|
continue
|
|
clauses.append(normalized)
|
|
return limit_detail_for_density(join_detail_clauses(clauses), density, True)
|
|
|
|
|
|
def hardcore_action_sentence(
|
|
role_graph: str,
|
|
hard_item: str,
|
|
composition: str = "",
|
|
axis_values: Any = None,
|
|
detail_density: str = "balanced",
|
|
) -> str:
|
|
detail_density = normalize_hardcore_detail_density(detail_density)
|
|
role_graph = _clean(role_graph).rstrip(".")
|
|
hard_item = _clean(hard_item).rstrip(".")
|
|
role_graph = re.sub(
|
|
r"\bthe man penetrates the woman while a toy adds a second point of contact\b",
|
|
"the man's penis thrusts into the woman while a toy is positioned at the second penetration point",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe man thrusts his penis into the woman while a toy adds a second penetration point\b",
|
|
"the man's penis thrusts into the woman while a toy is positioned at the second penetration point",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe man thrusts his penis into the woman\b",
|
|
"the man's penis thrusts into the woman",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe man penetrates the woman anally\b",
|
|
"the man's penis thrusts into the woman's ass",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe man thrusts his penis into the woman's ass\b",
|
|
"the man's penis thrusts into the woman's ass",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe man penetrates the woman\b",
|
|
"the man's penis thrusts into the woman",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe woman and the man are in mutual oral contact with mouth-to-genital contact visible\b",
|
|
"the woman has the man's penis in her mouth while the man uses his mouth on her pussy",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe woman gives oral to the man\b",
|
|
"the woman takes the man's penis in her mouth",
|
|
role_graph,
|
|
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)
|
|
anchor = hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
|
|
is_outercourse = is_outercourse_text(role_graph, hard_item, composition, axis_values_text(axis_values))
|
|
is_oral = is_oral_text(role_graph, hard_item, composition, axis_values_text(axis_values))
|
|
is_penetrative = is_vaginal_penetration_text(role_graph, hard_item, composition, axis_values_text(axis_values))
|
|
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
|
role_graph = re.sub(
|
|
r"\s+while a toy adds (?:the|a) second penetration point\b",
|
|
" while a toy is positioned at the second penetration point",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
if is_climax:
|
|
anchor = ""
|
|
detail = _dedupe_climax_detail(detail, role_graph, detail_density)
|
|
elif is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
|
anchor = ""
|
|
detail = _sanitize_foreplay_detail(detail, role_graph, composition)
|
|
detail = limit_detail_for_density(detail, detail_density, False)
|
|
elif is_outercourse:
|
|
anchor = ""
|
|
detail = _dedupe_outercourse_detail(detail, role_graph, hard_item, axis_values)
|
|
detail = limit_detail_for_density(detail, detail_density, False)
|
|
elif is_oral and role_graph:
|
|
anchor = ""
|
|
detail = _dedupe_oral_detail(detail, role_graph, hard_item, axis_values)
|
|
detail = limit_detail_for_density(detail, detail_density, False)
|
|
elif is_penetrative and role_graph:
|
|
anchor = ""
|
|
detail = _dedupe_penetration_detail(detail, role_graph, hard_item, axis_values)
|
|
detail = limit_detail_for_density(detail, detail_density, False)
|
|
else:
|
|
detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail
|
|
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
|
|
detail = _dedupe_toy_double_detail(detail)
|
|
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 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}, {_lowercase_for_inline_join(role_graph)}"
|
|
elif role_graph:
|
|
sentence = role_graph
|
|
elif detail and anchor_phrase:
|
|
sentence = f"In {anchor_phrase}, {detail}"
|
|
detail = ""
|
|
else:
|
|
sentence = detail or hard_item
|
|
detail = ""
|
|
if detail:
|
|
sentence = f"{sentence}; {detail}"
|
|
return sentence
|