Extract Krea action climax helpers

This commit is contained in:
2026-06-26 16:18:56 +02:00
parent c08af2c14a
commit f6d6dfffb4
4 changed files with 200 additions and 177 deletions
+8 -7
View File
@@ -156,17 +156,17 @@ Already isolated:
rear-entry detection, and action-position phrasing. rear-entry detection, and action-position phrasing.
- `krea_action_details.py` owns non-climax item/detail cleanup for foreplay, - `krea_action_details.py` owns non-climax item/detail cleanup for foreplay,
outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths. outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths.
- `krea_actions.py` owns non-POV hardcore action sentence dispatch and - `krea_action_climax.py` owns climax-specific role/detail cleanup and aftermath
climax-specific role/detail cleanup. view dedupe.
- `krea_actions.py` owns non-POV hardcore action sentence dispatch.
- `krea_pov_actions.py` owns POV hardcore action sentence rewriting and - `krea_pov_actions.py` owns POV hardcore action sentence rewriting and
first-person body geometry. first-person body geometry.
Improve later: Improve later:
- split remaining climax-specific cleanup out of `krea_actions.py`;
- add route-level smoke fixtures for representative metadata rows;
- make `krea_actions.hardcore_action_sentence` dispatch by action family instead - make `krea_actions.hardcore_action_sentence` dispatch by action family instead
of long conditional chains. of long conditional chains;
- add route-level smoke fixtures for representative metadata rows;
### SDXL Formatter Path ### SDXL Formatter Path
@@ -344,8 +344,9 @@ Medium-term:
## Recommended Next Passes ## Recommended Next Passes
1. Split climax-specific role/detail cleanup out of `krea_actions.py`, using 1. Split `krea_actions.hardcore_action_sentence` into action-family dispatch
`krea_cast.py` as the pattern for stable import aliases and smoke coverage. helpers, 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 2. Split `__init__.py` node classes by family after behavior is covered by smoke
checks. checks.
3. Add metadata fields such as `action_family` / `position_family` to reduce 3. Add metadata fields such as `action_family` / `position_family` to reduce
+7 -4
View File
@@ -278,6 +278,7 @@ Edit targets:
- Krea2 action rewrite orchestration: `krea_formatter.py`. - Krea2 action rewrite orchestration: `krea_formatter.py`.
- Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`. - Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`.
- Krea2 non-climax item/detail cleanup: `krea_action_details.py`. - Krea2 non-climax item/detail cleanup: `krea_action_details.py`.
- Krea2 climax role/detail cleanup: `krea_action_climax.py`.
- Krea2 non-POV action rewrite: `krea_actions.py`. - Krea2 non-POV action rewrite: `krea_actions.py`.
- Krea2 POV position rewrite: `krea_pov_actions.py`. - Krea2 POV position rewrite: `krea_pov_actions.py`.
@@ -471,8 +472,9 @@ What each part owns:
- `krea_action_details.py`: normalizes non-climax item/detail text and dedupes - `krea_action_details.py`: normalizes non-climax item/detail text and dedupes
foreplay, outercourse, oral, penetration, toy/double-contact, and anchor foreplay, outercourse, oral, penetration, toy/double-contact, and anchor
details. details.
- `krea_actions.py`: rewrites non-POV hardcore action sentences and handles - `krea_action_climax.py`: rewrites climax role graphs and dedupes aftermath
climax-specific role/detail cleanup. detail/view clauses.
- `krea_actions.py`: dispatches non-POV hardcore action sentence rewriting.
- `krea_pov_actions.py`: rewrites POV variants with first-person geometry. - `krea_pov_actions.py`: rewrites POV variants with first-person geometry.
Current broad hardcore families: Current broad hardcore families:
@@ -562,6 +564,7 @@ Key Krea2 ownership:
- Action context and family predicates: `krea_action_context.py`. - Action context and family predicates: `krea_action_context.py`.
- Non-POV pose anchors and arrangements: `krea_action_positions.py`. - Non-POV pose anchors and arrangements: `krea_action_positions.py`.
- Non-climax item/detail cleanup: `krea_action_details.py`. - Non-climax item/detail cleanup: `krea_action_details.py`.
- Climax role/detail cleanup: `krea_action_climax.py`.
- Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`. - Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`.
- POV labels, filtering, and camera/composition support: `krea_pov.py`. - POV labels, filtering, and camera/composition support: `krea_pov.py`.
- Detail clause splitting and density limiting: `krea_detail.py`. - Detail clause splitting and density limiting: `krea_detail.py`.
@@ -752,8 +755,8 @@ Use these traces to narrow a problem in one pass.
4. If raw `item` differs but Krea output looks identical, inspect 4. If raw `item` differs but Krea output looks identical, inspect
`krea_action_context.py` family predicates first, then `krea_action_context.py` family predicates first, then
`krea_action_positions.py` pose anchors/arrangements, `krea_action_positions.py` pose anchors/arrangements,
`krea_action_details.py` item/detail cleanup, and `krea_actions.py` action `krea_action_details.py` item/detail cleanup, `krea_action_climax.py`
sentence dispatch. climax cleanup, and `krea_actions.py` action sentence dispatch.
### POV position is spatially wrong ### POV position is spatially wrong
+180
View File
@@ -0,0 +1,180 @@
from __future__ import annotations
import re
from typing import Any
try:
from .krea_action_context import axis_values_text
from .krea_action_positions import action_position_phrase, mentions_rear_entry
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
from krea_action_positions import action_position_phrase, mentions_rear_entry
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 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)
+5 -166
View File
@@ -14,13 +14,11 @@ try:
is_vaginal_penetration_text, is_vaginal_penetration_text,
normalize_hardcore_detail_density, normalize_hardcore_detail_density,
) )
from .krea_detail import detail_clauses, join_detail_clauses, limit_detail_for_density from .krea_detail import limit_detail_for_density
from .krea_action_positions import ( from .krea_action_positions import (
action_position_phrase,
arrangement_duplicates_role, arrangement_duplicates_role,
hardcore_pose_anchor, hardcore_pose_anchor,
hardcore_pose_arrangement, hardcore_pose_arrangement,
mentions_rear_entry,
) )
from .krea_action_details import ( from .krea_action_details import (
dedupe_anchor_detail, dedupe_anchor_detail,
@@ -31,6 +29,7 @@ try:
hardcore_item_detail, hardcore_item_detail,
sanitize_foreplay_detail, sanitize_foreplay_detail,
) )
from .krea_action_climax import climax_role_graph, dedupe_climax_detail
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
from krea_action_context import ( from krea_action_context import (
axis_values_text, axis_values_text,
@@ -42,13 +41,11 @@ except ImportError: # Allows local smoke tests with `python -c`.
is_vaginal_penetration_text, is_vaginal_penetration_text,
normalize_hardcore_detail_density, normalize_hardcore_detail_density,
) )
from krea_detail import detail_clauses, join_detail_clauses, limit_detail_for_density from krea_detail import limit_detail_for_density
from krea_action_positions import ( from krea_action_positions import (
action_position_phrase,
arrangement_duplicates_role, arrangement_duplicates_role,
hardcore_pose_anchor, hardcore_pose_anchor,
hardcore_pose_arrangement, hardcore_pose_arrangement,
mentions_rear_entry,
) )
from krea_action_details import ( from krea_action_details import (
dedupe_anchor_detail, dedupe_anchor_detail,
@@ -59,6 +56,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
hardcore_item_detail, hardcore_item_detail,
sanitize_foreplay_detail, sanitize_foreplay_detail,
) )
from krea_action_climax import climax_role_graph, dedupe_climax_detail
def _clean(value: Any) -> str: def _clean(value: Any) -> str:
@@ -82,165 +80,6 @@ def _with_indefinite_article(text: str) -> str:
return f"{article} {text}" return f"{article} {text}"
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( def hardcore_action_sentence(
role_graph: str, role_graph: str,
hard_item: str, hard_item: str,
@@ -316,7 +155,7 @@ def hardcore_action_sentence(
) )
if is_climax: if is_climax:
anchor = "" anchor = ""
detail = _dedupe_climax_detail(detail, role_graph, detail_density) detail = dedupe_climax_detail(detail, role_graph, detail_density)
elif is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)): elif is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)):
anchor = "" anchor = ""
detail = sanitize_foreplay_detail(detail, role_graph, composition) detail = sanitize_foreplay_detail(detail, role_graph, composition)