Extract Krea action climax helpers
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user