diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index ee2adc7..34ea25c 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -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 diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 214b069..dae6dad 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -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 diff --git a/krea_action_details.py b/krea_action_details.py new file mode 100644 index 0000000..8a879a7 --- /dev/null +++ b/krea_action_details.py @@ -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) diff --git a/krea_actions.py b/krea_actions.py index 986cfcc..746fcd9 100644 --- a/krea_actions.py +++ b/krea_actions.py @@ -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 ""