Extract climax role graph wording

This commit is contained in:
2026-06-26 17:38:51 +02:00
parent ee62e2215d
commit 3ebbb09d63
6 changed files with 171 additions and 53 deletions
@@ -114,6 +114,9 @@ Already isolated:
- anal/double-contact role graph wording lives in `hardcore_role_anal.py`, - anal/double-contact role graph wording lives in `hardcore_role_anal.py`,
covering rear-entry anal variants and front/back double-contact source covering rear-entry anal variants and front/back double-contact source
geometry. 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 - camera-scene prose and coworking composition adaptation live in
`scene_camera_adapters.py`; `prompt_builder.py` still owns camera config `scene_camera_adapters.py`; `prompt_builder.py` still owns camera config
parsing and row mutation. parsing and row mutation.
@@ -313,6 +316,8 @@ Near-term:
drift are caught. drift are caught.
- Cover POV outercourse, oral, penetration, anal, and front/back double-contact - Cover POV outercourse, oral, penetration, anal, and front/back double-contact
Krea routes so selected position geometry stays synchronized with metadata. 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: Medium-term:
+4
View File
@@ -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_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_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_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. | | `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. | | `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. | | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. |
@@ -748,6 +749,9 @@ pair metadata through the core Python APIs, then verifies:
formatting; formatting;
- front/back double-contact routes keep source role graph metadata and Krea - front/back double-contact routes keep source role graph metadata and Krea
front/back position wording synchronized; 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. - expression-disabled rows do not fall back to generated expression text.
- static formatter metadata fixtures keep source-provided action families - static formatter metadata fixtures keep source-provided action families
stable across Krea2 prose, SDXL tags, and natural captions even when raw item stable across Krea2 prose, SDXL tags, and natural captions even when raw item
+74
View File
@@ -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."
+3 -53
View File
@@ -1,16 +1,17 @@
from __future__ import annotations from __future__ import annotations
import random import random
import re
from typing import Any from typing import Any
try: try:
from .hardcore_role_anal import build_anal_or_double_role_graph 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_oral import build_oral_role_graph
from .hardcore_role_outercourse import build_outercourse_role_graph from .hardcore_role_outercourse import build_outercourse_role_graph
from .hardcore_role_penetration import build_penetration_role_graph from .hardcore_role_penetration import build_penetration_role_graph
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
from hardcore_role_anal import build_anal_or_double_role_graph 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_oral import build_oral_role_graph
from hardcore_role_outercourse import build_outercourse_role_graph from hardcore_role_outercourse import build_outercourse_role_graph
from hardcore_role_penetration import build_penetration_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} 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." 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: if people_count == 1:
solo = people[0] solo = people[0]
if women_count == 1: if women_count == 1:
@@ -338,7 +288,7 @@ def build_hardcore_role_graph(
elif "group" in slug or "orgy" in slug: 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." 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: 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: else:
graph = build_penetration_role_graph(woman, man, item_text, item_axis_values) graph = build_penetration_role_graph(woman, man, item_text, item_axis_values)
return graph + support_sentence({woman, man, third} if third else {woman, man}) return graph + support_sentence({woman, man, third} if third else {woman, man})
+2
View File
@@ -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") patterns.append(r"face-down ass-up on the mattress")
if "lies on her side" in lower: if "lies on her side" in lower:
patterns.append(r"side-lying with thighs parted") 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: if "sits on the edge" in lower:
patterns.append(r"sitting on the edge of the bed") patterns.append(r"sitting on the edge of the bed")
if "bed edge" in lower: if "bed edge" in lower:
+83
View File
@@ -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") _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: def smoke_no_expression_fallback() -> None:
cast = pb.build_character_slot_json( cast = pb.build_character_slot_json(
subject_type="woman", subject_type="woman",
@@ -1271,6 +1353,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("pov_penetration_position_routes", smoke_pov_penetration_position_routes), ("pov_penetration_position_routes", smoke_pov_penetration_position_routes),
("pov_anal_position_routes", smoke_pov_anal_position_routes), ("pov_anal_position_routes", smoke_pov_anal_position_routes),
("double_front_back_route", smoke_double_front_back_route), ("double_front_back_route", smoke_double_front_back_route),
("climax_position_routes", smoke_climax_position_routes),
("expression_disabled", smoke_no_expression_fallback), ("expression_disabled", smoke_no_expression_fallback),
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
] ]