Files
ComfyUI-Ethanfel-Prompt-Bui…/krea_actions.py
T

691 lines
31 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
from .krea_action_positions import (
action_position_phrase,
arrangement_duplicates_role,
hardcore_pose_anchor,
hardcore_pose_arrangement,
mentions_rear_entry,
)
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
from krea_action_positions import (
action_position_phrase,
arrangement_duplicates_role,
hardcore_pose_anchor,
hardcore_pose_arrangement,
mentions_rear_entry,
)
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 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 _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