From 1c661b3c9d333fcfa3f382920a6fa83e3c7bdb76 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 16:00:11 +0200 Subject: [PATCH] Extract Krea hardcore action helpers --- docs/prompt-architecture-improvement-plan.md | 13 +- docs/prompt-pool-routing-map.md | 24 +- krea_actions.py | 1118 ++++++++++++++++++ krea_formatter.py | 1101 +---------------- 4 files changed, 1144 insertions(+), 1112 deletions(-) create mode 100644 krea_actions.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index b6bce1d..69b1172 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -136,7 +136,6 @@ Keep here: - Krea prose style; - Krea route orchestration; -- remaining hardcore action sentence dispatch; - camera-scene preservation; - fallback text parsing. @@ -153,15 +152,17 @@ Already isolated: support text. - `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and density limiting for Krea action prose. +- `krea_actions.py` owns non-POV hardcore action sentence rewriting, pose + anchors, arrangements, climax detail cleanup, and action-position phrasing. - `krea_pov_actions.py` owns POV hardcore action sentence rewriting and first-person body geometry. Improve later: -- split the remaining non-POV hardcore action dispatcher into `krea_actions.py`; +- split `krea_actions.py` by action family once the extracted module is stable; - add route-level smoke fixtures for representative metadata rows; -- make `_hardcore_action_sentence` dispatch by action family instead of long - conditional chains. +- make `krea_actions.hardcore_action_sentence` dispatch by action family instead + of long conditional chains. ### SDXL Formatter Path @@ -339,8 +340,8 @@ Medium-term: ## Recommended Next Passes -1. Split the remaining Krea action dispatcher into `krea_actions.py`, using - `krea_cast.py` as the pattern for stable import aliases and smoke coverage. +1. Split `krea_actions.py` by action family, 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 d66310d..136bc77 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -82,10 +82,10 @@ These recipes identify the intended road before editing prompt text. | Request | Preferred node route | Critical settings | If wrong, inspect | | --- | --- | --- | --- | -| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, Krea `_hardcore_action_sentence` | -| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `_hardcore_action_sentence` | +| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, `krea_actions.hardcore_action_sentence` | +| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `krea_actions.hardcore_action_sentence` | | Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `krea_pov_actions.py`, `krea_pov.py`, `krea_cast.cast_prose` omit-label handling | -| Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, Krea `_is_foreplay_text` / `_hardcore_action_sentence` | +| Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, `krea_action_context.is_foreplay_text` / `krea_actions.hardcore_action_sentence` | | Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields | | Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch | | Change only outfit/clothing | Character clothing or category content route | Keep `person_seed`, `scene_seed`, `pose_seed`; change `content_seed`; slot `softcore_outfit` overrides Insta/OF outfit | `SxCP Character Clothing`, `INSTA_OF_SOFTCORE_OUTFITS`, category item templates | @@ -276,6 +276,7 @@ Edit targets: - Position filtering UI: `build_hardcore_position_pool_json`, `build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`. - Krea2 action rewrite orchestration: `krea_formatter.py`. +- Krea2 non-POV action rewrite: `krea_actions.py`. - Krea2 POV position rewrite: `krea_pov_actions.py`. ### Composition @@ -463,6 +464,8 @@ What each part owns: - `prompt_builder.py`: filters which templates/axes remain available. - `krea_formatter.py`: orchestrates the selected action rewrite into model-readable prose. +- `krea_actions.py`: rewrites non-POV hardcore action sentences, pose anchors, + arrangements, detail dedupe, and climax cleanup. - `krea_pov_actions.py`: rewrites POV variants with first-person geometry. Current broad hardcore families: @@ -550,9 +553,9 @@ Key Krea2 ownership: - Cast descriptor naturalization: `krea_cast.cast_prose`, `krea_cast.natural_label_text`. - Action context and family predicates: `krea_action_context.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`. -- Hardcore action sentence: `_hardcore_action_sentence`. - POV hardcore sentence: `krea_pov_actions.pov_action_phrase`. - Clothing state cleanup: `krea_clothing.natural_clothing_state`. - Camera scene preservation: `_camera_scene_phrase`. @@ -562,9 +565,9 @@ Krea2 field consumption: | Branch | Reads most from | Key functions | | --- | --- | --- | | Normal single row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `_normal_row_to_krea` | -| Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `_hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase` | +| Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `krea_actions.hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase` | | Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `_insta_pair_to_krea` | -| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `_hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase`, `krea_clothing.natural_clothing_state` | +| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `krea_actions.hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase`, `krea_clothing.natural_clothing_state` | | Plain text fallback | `source_text` only | `_fallback_text_to_krea` | If metadata is connected and `method` says `text(fallback)`, the formatter did @@ -713,9 +716,9 @@ pair metadata through the core Python APIs, then verifies: | Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. | | Expression appears when disabled | `_disable_row_expression`, formatter expression extraction. | | Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `_apply_hardcore_position_config_to_subcategory`. | -| Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and Krea `_is_foreplay_text` / `_hardcore_pose_anchor`. | +| Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and `krea_action_context.is_foreplay_text` / `krea_actions.hardcore_pose_anchor`. | | Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. | -| Krea2 hardcore prompt position is vague | `_hardcore_action_sentence` or `krea_pov_actions.py`. | +| Krea2 hardcore prompt position is vague | `krea_actions.hardcore_action_sentence` or `krea_pov_actions.py`. | | Man appears described in POV | POV labels, `krea_cast.cast_prose` omit labels, `krea_pov_actions.pov_action_phrase`. | | Camera prompt missing from Krea2 | Row `camera_directive` / `camera_scene_directive`, then Krea `_camera_phrase`. | | Trigger missing in Krea2 fallback | `format_krea2_prompt` preserve-trigger fallback behavior. | @@ -738,9 +741,8 @@ Use these traces to narrow a problem in one pass. 3. Inspect `categories/sexual_poses.json` for the selected subcategory, `item_templates`, `axes`, and `weight`. 4. If raw `item` differs but Krea output looks identical, inspect - `krea_action_context.py` family predicates first, then - `_hardcore_pose_anchor`, `_hardcore_pose_arrangement`, - `_hardcore_item_detail`, and `_hardcore_action_sentence`. + `krea_action_context.py` family predicates first, then `krea_actions.py` + pose anchors, arrangements, item detail cleanup, and action sentence dispatch. ### POV position is spatially wrong diff --git a/krea_actions.py b/krea_actions.py new file mode 100644 index 0000000..a5daa9d --- /dev/null +++ b/krea_actions.py @@ -0,0 +1,1118 @@ +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"(? 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"(? 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 diff --git a/krea_formatter.py b/krea_formatter.py index 3a62b83..ba02621 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -6,16 +6,9 @@ from typing import Any try: from .krea_action_context import ( - axis_values_text as _axis_values_text, - is_climax_text as _is_climax_text, is_close_foreplay_text as _is_close_foreplay_text, - is_foreplay_text as _is_foreplay_text, - is_oral_text as _is_oral_text, is_outercourse_text as _is_outercourse_text, - is_toy_assisted_double_text as _is_toy_assisted_double_text, - is_vaginal_penetration_text as _is_vaginal_penetration_text, normalize_hardcore_detail_density as _normalize_hardcore_detail_density, - position_context_text as _position_context_text, ) from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -29,10 +22,9 @@ try: prompt_cast_descriptors as _prompt_cast_descriptors, ) from .krea_clothing import natural_clothing_state as _natural_clothing_state - from .krea_detail import ( - detail_clauses as _detail_clauses, - join_detail_clauses as _join_detail_clauses, - limit_detail_for_density as _limit_detail_for_density, + from .krea_actions import ( + action_position_phrase as _action_position_phrase, + hardcore_action_sentence as _hardcore_action_sentence, ) from .krea_pov import ( filter_pov_labeled_clauses as _filter_pov_labeled_clauses, @@ -45,16 +37,9 @@ try: from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text except ImportError: # Allows local smoke tests with `python -c`. from krea_action_context import ( - axis_values_text as _axis_values_text, - is_climax_text as _is_climax_text, is_close_foreplay_text as _is_close_foreplay_text, - is_foreplay_text as _is_foreplay_text, - is_oral_text as _is_oral_text, is_outercourse_text as _is_outercourse_text, - is_toy_assisted_double_text as _is_toy_assisted_double_text, - is_vaginal_penetration_text as _is_vaginal_penetration_text, normalize_hardcore_detail_density as _normalize_hardcore_detail_density, - position_context_text as _position_context_text, ) from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -68,10 +53,9 @@ except ImportError: # Allows local smoke tests with `python -c`. prompt_cast_descriptors as _prompt_cast_descriptors, ) from krea_clothing import natural_clothing_state as _natural_clothing_state - from krea_detail import ( - detail_clauses as _detail_clauses, - join_detail_clauses as _join_detail_clauses, - limit_detail_for_density as _limit_detail_for_density, + from krea_actions import ( + action_position_phrase as _action_position_phrase, + hardcore_action_sentence as _hardcore_action_sentence, ) from krea_pov import ( filter_pov_labeled_clauses as _filter_pov_labeled_clauses, @@ -275,1079 +259,6 @@ def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str: 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 _mentions_ass(text: str) -> bool: - return bool( - re.search( - r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and", - text, - ) - ) - - -def _mentions_rear_entry(text: str) -> bool: - return bool( - re.search( - r"ass[- ](?:up|raised|exposed|lifted|stretched)|penis entering ass|cum (?:on|dripping from) ass|spread cheeks|lower back and ass|pussy, ass|rear[- ]entry", - text, - ) - ) - - -def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str: - text = _position_context_text(role_graph, hard_item, composition, axis_values) - item_text = " ".join(part for part in (_clean(hard_item).lower(), _axis_values_text(axis_values).lower()) if part) - 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"(? 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"(? 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 - - def _composition_phrase( composition: Any, action: str = "",