From 3ebbb09d631103e19cab8100e323b8a43f89e90b Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 17:38:51 +0200 Subject: [PATCH] Extract climax role graph wording --- docs/prompt-architecture-improvement-plan.md | 5 ++ docs/prompt-pool-routing-map.md | 4 + hardcore_role_climax.py | 74 +++++++++++++++++ hardcore_role_graphs.py | 56 +------------ krea_action_climax.py | 2 + tools/prompt_smoke.py | 83 ++++++++++++++++++++ 6 files changed, 171 insertions(+), 53 deletions(-) create mode 100644 hardcore_role_climax.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 85eca9c..fddc0fc 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -114,6 +114,9 @@ Already isolated: - anal/double-contact role graph wording lives in `hardcore_role_anal.py`, covering rear-entry anal variants and front/back double-contact source geometry. +- climax role graph wording lives in `hardcore_role_climax.py`, covering + ejaculation aftermath placement for face/body/ass, lap, open-thigh, + side-lying, and front/back group layouts. - camera-scene prose and coworking composition adaptation live in `scene_camera_adapters.py`; `prompt_builder.py` still owns camera config parsing and row mutation. @@ -313,6 +316,8 @@ Near-term: drift are caught. - Cover POV outercourse, oral, penetration, anal, and front/back double-contact Krea routes so selected position geometry stays synchronized with metadata. +- Cover generated climax routes through Krea, SDXL, and natural caption outputs + so source aftermath placement and formatter details cannot drift apart. Medium-term: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index e6c469e..76375d2 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -69,6 +69,7 @@ Core helper ownership: | `hardcore_role_outercourse.py` | Outercourse role graph wording for boobjob, testicle-sucking, penis-licking, handjob, and footjob geometry. | | `hardcore_role_penetration.py` | Penetrative-sex role graph wording for missionary, cowgirl, reverse-cowgirl, doggy, standing, side-lying, raised-edge, kneeling-straddle, and lotus geometry. | | `hardcore_role_anal.py` | Anal and double-contact role graph wording for rear-entry, raised-edge, kneeling, side-lying, and front/back double-position geometry. | +| `hardcore_role_climax.py` | Climax and ejaculation aftermath role graph wording for face/body/ass, lap, open-thigh, side-lying, and group front/back placement. | | `hardcore_action_metadata.py` | Source action-family and position-family metadata used by Krea2, SDXL, and caption routes. | | `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. | | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. | @@ -748,6 +749,9 @@ pair metadata through the core Python APIs, then verifies: formatting; - front/back double-contact routes keep source role graph metadata and Krea front/back position wording synchronized; +- climax routes keep source body position, Krea aftermath wording, SDXL family + tags, and training captions synchronized for face-down, side-lying, lap, + open-thigh, and front/back placements; - expression-disabled rows do not fall back to generated expression text. - static formatter metadata fixtures keep source-provided action families stable across Krea2 prose, SDXL tags, and natural captions even when raw item diff --git a/hardcore_role_climax.py b/hardcore_role_climax.py new file mode 100644 index 0000000..1468e94 --- /dev/null +++ b/hardcore_role_climax.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import re +from typing import Any + + +def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str: + return " ".join( + str(part or "").lower() + for part in ( + item_text, + *((item_axis_values or {}).values()), + ) + ) + + +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 build_climax_role_graph( + woman: str, + man: str, + third: str = "", + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> str: + context = _context_text(item_text, item_axis_values) + if "lying between two partners" in context and third: + return f"{woman} lies between {man} and {third}, with {man} under her hips and {third} positioned above her torso as visible semen lands on her body." + if "held between front-and-back partners" in context and third: + return f"{woman} is held between {man} behind her and {third} in front of her as visible semen lands across her body." + if "kneeling between standing partners" in context and third: + return f"{woman} kneels between {man} and {third} while both stand close around her face and torso for visible ejaculation." + if "side-lying with thighs parted" in context: + return f"{woman} lies on her side with thighs parted while {man} kneels beside her hips and ejaculates semen across her thighs and pussy." + if "sitting on the edge of the bed" in context: + return f"{woman} sits on the edge of the bed with knees spread while {man} stands close between her legs and ejaculates semen across her body." + if "lying at the bed edge with thighs open" in context: + return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs." + if "reclining with thighs open" in context or "lying on the back with legs spread" in context: + return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs." + if "on all fours with hips raised" in context: + return f"{woman} is on all fours with hips raised while {man} is positioned behind her and ejaculates semen across her ass, thighs, and lower back." + if "face-down ass-up" in context: + return f"{woman} lies face-down with ass raised while {man} is positioned behind her and ejaculates semen across her lower back and ass." + if "bent over with ass raised" in context or "bent over" in context: + return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs." + if "kneeling with mouth open" in context: + return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest." + if "kneeling in front of a standing partner" in context: + return f"{woman} kneels in front of {man} at hip height while {man} stands over her for visible ejaculation." + if "standing with cum on the body" in context: + return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body." + if "squatting on top of a partner" in context: + return f"{woman} squats over {man}'s hips while {man} lies on his back under her and ejaculates semen onto her body." + if "reverse cowgirl over a partner's hips" in context: + return f"{woman} straddles {man}'s hips facing away while {man} lies on his back under her and ejaculates semen onto her body." + if any(term in context for term in ("straddling a partner", "straddling a partner's hips", "shared climax after penetration", "orgasm during penetration")): + return f"{woman} straddles {man}'s hips while {man} lies on his back under her, their bodies still aligned from penetration as he ejaculates semen onto her body." + if "seated in a partner's lap facing them" in context: + return f"{woman} sits in {man}'s lap facing him, legs wrapped around his hips as he ejaculates semen across her body." + if any(term in context for term in ("lower back", "cum dripping from ass", "cum on lower back")) or _mentions_ass(context): + return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs." + if any(term in context for term in ("cum on face", "cum on tongue", "cum on lips", "cum on face and lips", "cum on tongue and chin")): + if third: + return f"{woman} kneels in the center while {man} and {third} stand close around her face and torso for visible ejaculation." + return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest." + return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body." diff --git a/hardcore_role_graphs.py b/hardcore_role_graphs.py index efe845b..b61920f 100644 --- a/hardcore_role_graphs.py +++ b/hardcore_role_graphs.py @@ -1,16 +1,17 @@ from __future__ import annotations import random -import re from typing import Any try: from .hardcore_role_anal import build_anal_or_double_role_graph + from .hardcore_role_climax import build_climax_role_graph from .hardcore_role_oral import build_oral_role_graph from .hardcore_role_outercourse import build_outercourse_role_graph from .hardcore_role_penetration import build_penetration_role_graph except ImportError: # Allows local smoke tests with `python -c`. from hardcore_role_anal import build_anal_or_double_role_graph + from hardcore_role_climax import build_climax_role_graph from hardcore_role_oral import build_oral_role_graph from hardcore_role_outercourse import build_outercourse_role_graph from hardcore_role_penetration import build_penetration_role_graph @@ -191,57 +192,6 @@ def build_hardcore_role_graph( return f"{primary} is centered while {partner} touches her body and {observer} watches close beside them, hands and faces readable." return f"{primary} is centered while {partner} touches her body and {observer} stays close as the watching or guiding partner." - 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 climax_position_graph(woman: str, man: str, third: str = "") -> str: - if "lying between two partners" in item_text and third: - return f"{woman} lies between {man} and {third}, with {man} under her hips and {third} positioned above her torso as visible semen lands on her body." - if "held between front-and-back partners" in item_text and third: - return f"{woman} is held between {man} behind her and {third} in front of her as visible semen lands across her body." - if "kneeling between standing partners" in item_text and third: - return f"{woman} kneels between {man} and {third} while both stand close around her face and torso for visible ejaculation." - if "side-lying with thighs parted" in item_text: - return f"{woman} lies on her side with thighs parted while {man} kneels beside her hips and ejaculates semen across her thighs and pussy." - if "sitting on the edge of the bed" in item_text: - return f"{woman} sits on the edge of the bed with knees spread while {man} stands close between her legs and ejaculates semen across her body." - if "lying at the bed edge with thighs open" in item_text: - return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs." - if "reclining with thighs open" in item_text or "lying on the back with legs spread" in item_text: - return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs." - if "on all fours with hips raised" in item_text: - return f"{woman} is on all fours with hips raised while {man} is positioned behind her and ejaculates semen across her ass, thighs, and lower back." - if "face-down ass-up" in item_text: - return f"{woman} lies face-down with ass raised while {man} is positioned behind her and ejaculates semen across her lower back and ass." - if "bent over with ass raised" in item_text or "bent over" in item_text: - return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs." - if "kneeling with mouth open" in item_text: - return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest." - if "kneeling in front of a standing partner" in item_text: - return f"{woman} kneels in front of {man} at hip height while {man} stands over her for visible ejaculation." - if "standing with cum on the body" in item_text: - return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body." - if "squatting on top of a partner" in item_text: - return f"{woman} squats over {man}'s hips while {man} lies on his back under her and ejaculates semen onto her body." - if "reverse cowgirl over a partner's hips" in item_text: - return f"{woman} straddles {man}'s hips facing away while {man} lies on his back under her and ejaculates semen onto her body." - if any(term in item_text for term in ("straddling a partner", "straddling a partner's hips", "shared climax after penetration", "orgasm during penetration")): - return f"{woman} straddles {man}'s hips while {man} lies on his back under her, their bodies still aligned from penetration as he ejaculates semen onto her body." - if "seated in a partner's lap facing them" in item_text: - return f"{woman} sits in {man}'s lap facing him, legs wrapped around his hips as he ejaculates semen across her body." - if any(term in item_text for term in ("lower back", "cum dripping from ass", "cum on lower back")) or mentions_ass(item_text): - return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs." - if any(term in item_text for term in ("cum on face", "cum on tongue", "cum on lips", "cum on face and lips", "cum on tongue and chin")): - if third: - return f"{woman} kneels in the center while {man} and {third} stand close around her face and torso for visible ejaculation." - return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest." - return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body." - if people_count == 1: solo = people[0] if women_count == 1: @@ -338,7 +288,7 @@ def build_hardcore_role_graph( elif "group" in slug or "orgy" in slug: graph = f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs." elif "cumshot" in slug or "climax" in slug: - graph = climax_position_graph(woman, man, third) + graph = build_climax_role_graph(woman, man, third, item_text, item_axis_values) else: graph = build_penetration_role_graph(woman, man, item_text, item_axis_values) return graph + support_sentence({woman, man, third} if third else {woman, man}) diff --git a/krea_action_climax.py b/krea_action_climax.py index ca70fa8..d4cb52f 100644 --- a/krea_action_climax.py +++ b/krea_action_climax.py @@ -151,6 +151,8 @@ def dedupe_climax_detail(detail: str, role_graph: str, density: str = "balanced" 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") + 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 "sits on the edge" in lower: patterns.append(r"sitting on the edge of the bed") if "bed edge" in lower: diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index eeda5c6..ae4193a 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -1135,6 +1135,88 @@ def smoke_double_front_back_route() -> None: _expect("role graph:" not in prompt and "sexual scene:" not in prompt, "double route Krea leaked raw labels") +def smoke_climax_position_routes() -> None: + cases = [ + ( + "climax_face_down", + "face_down_ass_up", + 4001, + _character_cast(), + 1, + 1, + ("lies face-down", "lower back and ass"), + ("face-down", "lower back and ass"), + ), + ( + "climax_side_lying", + "side_lying", + 4042, + _character_cast(), + 1, + 1, + ("lies on her side", "thighs and pussy"), + ("lies on her side", "thighs and pussy"), + ), + ( + "climax_lotus_lap", + "lotus_lap", + 4001, + _character_cast(), + 1, + 1, + ("sits in man a's lap", "legs wrapped"), + ("sits in the man's lap", "legs wrapped"), + ), + ( + "climax_open_thighs", + "open_thighs", + 4001, + _character_cast(), + 1, + 1, + ("lies on her back", "thighs open"), + ("lies on her back", "thighs open"), + ), + ( + "climax_front_back", + "front_back", + 4090, + _character_cast_two_men(), + 1, + 2, + ("lies between man a and man b", "man a under her hips"), + ("lies between man a and man b", "visible semen lands"), + ), + ] + for name, position_key, seed, cast, women_count, men_count, role_terms, krea_terms in cases: + row = _prompt_row( + name=name, + category="Hardcore sexual poses", + subcategory="Cumshot and climax", + seed=seed, + character_cast=cast, + women_count=women_count, + men_count=men_count, + hardcore_position_config=_position_filter("climax_only", "climax", [position_key]), + ) + _expect_custom_row(row, name) + _expect(row.get("action_family") == "climax", f"{name} action_family should be climax") + _expect(row.get("position_family") == "climax", f"{name} position_family should be climax") + _expect(position_key in (row.get("position_keys") or []), f"{name} lost position key {position_key!r}") + role_graph = _expect_text(f"{name}.source_role_graph", row.get("source_role_graph"), 40).lower() + for term in role_terms: + _expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}") + krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single") + prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower() + _expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata") + _expect("role graph:" not in prompt and "sexual scene:" not in prompt, f"{name} Krea leaked raw labels") + for term in krea_terms: + _expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}") + if position_key == "side_lying": + _expect("lower back and ass" not in prompt, f"{name} Krea kept conflicting rear-entry fluid location: {prompt}") + _expect_formatter_outputs(row, name, target="single") + + def smoke_no_expression_fallback() -> None: cast = pb.build_character_slot_json( subject_type="woman", @@ -1271,6 +1353,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("pov_penetration_position_routes", smoke_pov_penetration_position_routes), ("pov_anal_position_routes", smoke_pov_anal_position_routes), ("double_front_back_route", smoke_double_front_back_route), + ("climax_position_routes", smoke_climax_position_routes), ("expression_disabled", smoke_no_expression_fallback), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ]