From f6d6dfffb469bc3612af2a344a53883e45086d20 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 16:18:56 +0200 Subject: [PATCH] Extract Krea action climax helpers --- docs/prompt-architecture-improvement-plan.md | 15 +- docs/prompt-pool-routing-map.md | 11 +- krea_action_climax.py | 180 +++++++++++++++++++ krea_actions.py | 171 +----------------- 4 files changed, 200 insertions(+), 177 deletions(-) create mode 100644 krea_action_climax.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 34ea25c..2aa1a21 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -156,17 +156,17 @@ Already isolated: rear-entry detection, and action-position phrasing. - `krea_action_details.py` owns non-climax item/detail cleanup for foreplay, outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths. -- `krea_actions.py` owns non-POV hardcore action sentence dispatch and - climax-specific role/detail cleanup. +- `krea_action_climax.py` owns climax-specific role/detail cleanup and aftermath + view dedupe. +- `krea_actions.py` owns non-POV hardcore action sentence dispatch. - `krea_pov_actions.py` owns POV hardcore action sentence rewriting and first-person body geometry. 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 - of long conditional chains. + of long conditional chains; +- add route-level smoke fixtures for representative metadata rows; ### SDXL Formatter Path @@ -344,8 +344,9 @@ Medium-term: ## Recommended Next Passes -1. Split climax-specific role/detail cleanup out of `krea_actions.py`, using - `krea_cast.py` as the pattern for stable import aliases and smoke coverage. +1. Split `krea_actions.hardcore_action_sentence` into action-family dispatch + 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 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 dae6dad..2f5c84e 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -278,6 +278,7 @@ Edit targets: - Krea2 action rewrite orchestration: `krea_formatter.py`. - Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`. - Krea2 non-climax item/detail cleanup: `krea_action_details.py`. +- Krea2 climax role/detail cleanup: `krea_action_climax.py`. - Krea2 non-POV action rewrite: `krea_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 foreplay, outercourse, oral, penetration, toy/double-contact, and anchor details. -- `krea_actions.py`: rewrites non-POV hardcore action sentences and handles - climax-specific role/detail cleanup. +- `krea_action_climax.py`: rewrites climax role graphs and dedupes aftermath + detail/view clauses. +- `krea_actions.py`: dispatches non-POV hardcore action sentence rewriting. - `krea_pov_actions.py`: rewrites POV variants with first-person geometry. Current broad hardcore families: @@ -562,6 +564,7 @@ Key Krea2 ownership: - Action context and family predicates: `krea_action_context.py`. - Non-POV pose anchors and arrangements: `krea_action_positions.py`. - Non-climax item/detail cleanup: `krea_action_details.py`. +- Climax role/detail cleanup: `krea_action_climax.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`. @@ -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 `krea_action_context.py` family predicates first, then `krea_action_positions.py` pose anchors/arrangements, - `krea_action_details.py` item/detail cleanup, and `krea_actions.py` action - sentence dispatch. + `krea_action_details.py` item/detail cleanup, `krea_action_climax.py` + climax cleanup, and `krea_actions.py` action sentence dispatch. ### POV position is spatially wrong diff --git a/krea_action_climax.py b/krea_action_climax.py new file mode 100644 index 0000000..ca70fa8 --- /dev/null +++ b/krea_action_climax.py @@ -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) diff --git a/krea_actions.py b/krea_actions.py index 746fcd9..b685ec5 100644 --- a/krea_actions.py +++ b/krea_actions.py @@ -14,13 +14,11 @@ try: is_vaginal_penetration_text, 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 ( - action_position_phrase, arrangement_duplicates_role, hardcore_pose_anchor, hardcore_pose_arrangement, - mentions_rear_entry, ) from .krea_action_details import ( dedupe_anchor_detail, @@ -31,6 +29,7 @@ try: hardcore_item_detail, sanitize_foreplay_detail, ) + from .krea_action_climax import climax_role_graph, dedupe_climax_detail except ImportError: # Allows local smoke tests with `python -c`. from krea_action_context import ( axis_values_text, @@ -42,13 +41,11 @@ except ImportError: # Allows local smoke tests with `python -c`. is_vaginal_penetration_text, 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 ( - action_position_phrase, arrangement_duplicates_role, hardcore_pose_anchor, hardcore_pose_arrangement, - mentions_rear_entry, ) from krea_action_details import ( dedupe_anchor_detail, @@ -59,6 +56,7 @@ except ImportError: # Allows local smoke tests with `python -c`. hardcore_item_detail, sanitize_foreplay_detail, ) + from krea_action_climax import climax_role_graph, dedupe_climax_detail def _clean(value: Any) -> str: @@ -82,165 +80,6 @@ def _with_indefinite_article(text: str) -> str: 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( role_graph: str, hard_item: str, @@ -316,7 +155,7 @@ def hardcore_action_sentence( ) if is_climax: 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)): anchor = "" detail = sanitize_foreplay_detail(detail, role_graph, composition)