Extract Krea action detail helpers

This commit is contained in:
2026-06-26 16:12:48 +02:00
parent ce90fb7593
commit c08af2c14a
4 changed files with 413 additions and 366 deletions
+7 -5
View File
@@ -154,14 +154,16 @@ Already isolated:
density limiting for Krea action prose.
- `krea_action_positions.py` owns non-POV pose anchors, body-arrangement text,
rear-entry detection, and action-position phrasing.
- `krea_actions.py` owns non-POV hardcore action sentence rewriting, item/detail
cleanup, and climax detail cleanup.
- `krea_action_details.py` owns non-climax item/detail cleanup for foreplay,
outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths.
- `krea_actions.py` owns non-POV hardcore action sentence dispatch and
climax-specific role/detail cleanup.
- `krea_pov_actions.py` owns POV hardcore action sentence rewriting and
first-person body geometry.
Improve later:
- split `krea_actions.py` by action family once the extracted module is stable;
- split remaining climax-specific cleanup out of `krea_actions.py`;
- add route-level smoke fixtures for representative metadata rows;
- make `krea_actions.hardcore_action_sentence` dispatch by action family instead
of long conditional chains.
@@ -342,8 +344,8 @@ Medium-term:
## Recommended Next Passes
1. Split `krea_actions.py` by action family, using `krea_cast.py` as the
pattern for stable import aliases and smoke coverage.
1. Split climax-specific role/detail cleanup out of `krea_actions.py`, using
`krea_cast.py` as the pattern for stable import aliases and smoke coverage.
2. Split `__init__.py` node classes by family after behavior is covered by smoke
checks.
3. Add metadata fields such as `action_family` / `position_family` to reduce
+10 -4
View File
@@ -277,6 +277,7 @@ Edit targets:
`build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`.
- Krea2 action rewrite orchestration: `krea_formatter.py`.
- Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`.
- Krea2 non-climax item/detail cleanup: `krea_action_details.py`.
- Krea2 non-POV action rewrite: `krea_actions.py`.
- Krea2 POV position rewrite: `krea_pov_actions.py`.
@@ -467,8 +468,11 @@ What each part owns:
model-readable prose.
- `krea_action_positions.py`: resolves non-POV pose anchors, body-arrangement
text, duplicate arrangement checks, and action-position phrases.
- `krea_actions.py`: rewrites non-POV hardcore action sentences, item/detail
dedupe, and climax cleanup.
- `krea_action_details.py`: normalizes non-climax item/detail text and dedupes
foreplay, outercourse, oral, penetration, toy/double-contact, and anchor
details.
- `krea_actions.py`: rewrites non-POV hardcore action sentences and handles
climax-specific role/detail cleanup.
- `krea_pov_actions.py`: rewrites POV variants with first-person geometry.
Current broad hardcore families:
@@ -557,6 +561,7 @@ Key Krea2 ownership:
`krea_cast.natural_label_text`.
- Action context and family predicates: `krea_action_context.py`.
- Non-POV pose anchors and arrangements: `krea_action_positions.py`.
- Non-climax item/detail cleanup: `krea_action_details.py`.
- Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`.
- POV labels, filtering, and camera/composition support: `krea_pov.py`.
- Detail clause splitting and density limiting: `krea_detail.py`.
@@ -746,8 +751,9 @@ Use these traces to narrow a problem in one pass.
`item_templates`, `axes`, and `weight`.
4. If raw `item` differs but Krea output looks identical, inspect
`krea_action_context.py` family predicates first, then
`krea_action_positions.py` pose anchors/arrangements and `krea_actions.py`
item detail cleanup/action sentence dispatch.
`krea_action_positions.py` pose anchors/arrangements,
`krea_action_details.py` item/detail cleanup, and `krea_actions.py` action
sentence dispatch.
### POV position is spatially wrong
+372
View File
@@ -0,0 +1,372 @@
from __future__ import annotations
import re
from typing import Any
try:
from .krea_action_context import (
is_close_foreplay_text,
position_context_text,
)
from .krea_detail import detail_clauses, join_detail_clauses
except ImportError: # Allows local smoke tests with `python -c`.
from krea_action_context import (
is_close_foreplay_text,
position_context_text,
)
from krea_detail import detail_clauses, join_detail_clauses
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 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_anchor_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)
+24 -357
View File
@@ -7,14 +7,12 @@ 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 (
@@ -24,18 +22,25 @@ try:
hardcore_pose_arrangement,
mentions_rear_entry,
)
from .krea_action_details import (
dedupe_anchor_detail,
dedupe_oral_detail,
dedupe_outercourse_detail,
dedupe_penetration_detail,
dedupe_toy_double_detail,
hardcore_item_detail,
sanitize_foreplay_detail,
)
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 (
@@ -45,6 +50,15 @@ except ImportError: # Allows local smoke tests with `python -c`.
hardcore_pose_arrangement,
mentions_rear_entry,
)
from krea_action_details import (
dedupe_anchor_detail,
dedupe_oral_detail,
dedupe_outercourse_detail,
dedupe_penetration_detail,
dedupe_toy_double_detail,
hardcore_item_detail,
sanitize_foreplay_detail,
)
def _clean(value: Any) -> str:
@@ -68,353 +82,6 @@ def _with_indefinite_article(text: str) -> str:
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:
@@ -652,24 +319,24 @@ def hardcore_action_sentence(
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 = 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 = 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 = 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 = 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
detail = dedupe_anchor_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 = 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 ""