From ee62e2215d5fb7a134a312f0c2408ca0fef075fc Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 17:32:04 +0200 Subject: [PATCH] Extract anal role graph wording --- docs/prompt-architecture-improvement-plan.md | 10 +- docs/prompt-pool-routing-map.md | 8 + hardcore_role_anal.py | 61 +++++++ hardcore_role_graphs.py | 45 +---- krea_pov_actions.py | 71 ++++---- tools/prompt_smoke.py | 170 +++++++++++++++++++ 6 files changed, 290 insertions(+), 75 deletions(-) create mode 100644 hardcore_role_anal.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index d25bdc1..85eca9c 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -111,6 +111,9 @@ Already isolated: - penetration-specific role graph wording lives in `hardcore_role_penetration.py`, covering the main vaginal penetration position families while Krea POV rewriting keeps first-person geometry stable. +- anal/double-contact role graph wording lives in `hardcore_role_anal.py`, + covering rear-entry anal variants and front/back double-contact source + geometry. - camera-scene prose and coworking composition adaptation live in `scene_camera_adapters.py`; `prompt_builder.py` still owns camera config parsing and row mutation. @@ -177,8 +180,9 @@ Already isolated: - `krea_action_dispatch.py` owns non-POV role normalization, action-family classification, and family-specific detail cleanup. - `krea_actions.py` owns final non-POV hardcore action sentence assembly. -- `krea_pov_actions.py` owns POV hardcore action sentence rewriting and - first-person body geometry. +- `krea_pov_actions.py` owns POV hardcore action sentence rewriting, + first-person body geometry, and selected-position-axis priority before loose + context fallback. Improve later: @@ -307,6 +311,8 @@ Near-term: - Cover close foreplay and POV penetration Krea routes so raw labels, invalid surface grammar, normal third-person camera text, and composition punctuation drift are caught. +- Cover POV outercourse, oral, penetration, anal, and front/back double-contact + Krea routes so selected position geometry stays synchronized with metadata. Medium-term: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 25ec64f..e6c469e 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -68,6 +68,7 @@ Core helper ownership: | `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral 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_anal.py` | Anal and double-contact role graph wording for rear-entry, raised-edge, kneeling, side-lying, and front/back double-position geometry. | | `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. | @@ -481,6 +482,8 @@ What each part owns: - `sexual_poses.json`: available positions, families, action templates, role graph templates, interaction templates, and action-specific pool references. - `prompt_builder.py`: filters which templates/axes remain available. +- `hardcore_role_graphs.py` and action-family helper modules: turn selected + item axes into source role graphs before formatter-specific rewrites. - `krea_formatter.py`: orchestrates the selected action rewrite into model-readable prose. - `krea_action_positions.py`: resolves non-POV pose anchors, body-arrangement @@ -740,6 +743,11 @@ pair metadata through the core Python APIs, then verifies: without recursive viewer wording; - POV penetration routes keep constrained missionary, cowgirl, reverse-cowgirl, doggy, raised-edge, and lotus geometry through Krea formatting; +- POV anal routes keep constrained doggy, bent-over, face-down, standing, + side-lying, raised-edge, and kneeling rear-entry geometry through Krea + formatting; +- front/back double-contact routes keep source role graph metadata and Krea + front/back position wording synchronized; - 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_anal.py b/hardcore_role_anal.py new file mode 100644 index 0000000..20cc461 --- /dev/null +++ b/hardcore_role_anal.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +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 _anal_position_graph(woman: str, man: str, context: str) -> str: + if "bent-over" in context or "bent over" in context: + return f"{woman} is bent forward with hips raised while {man} stands behind her and thrusts his penis into her ass." + if "face-down" in context: + return f"{woman} lies face-down with ass raised while {man} is positioned behind her and thrusts his penis into her ass." + if "doggy" in context or "rear-entry" in context: + return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + if "standing" in context: + return f"{woman} stands braced with hips angled back while {man} stands behind her and thrusts his penis into her ass." + if "spooning" in context or "side-lying" in context: + return f"{woman} lies on her side with thighs parted while {man} presses behind her and thrusts his penis into her ass." + if "edge-of-bed" in context or "edge of bed" in context or "bed edge" in context or "edge-supported" in context: + return f"{woman} lies near a raised edge with hips exposed while {man} kneels behind her and thrusts his penis into her ass." + if "kneeling" in context: + return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass." + return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + + +def _two_person_double_graph(woman: str, man: str, context: str) -> str: + if "bent-over" in context or "bent over" in context: + return f"{woman} is bent forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + if "face-down" in context: + return f"{woman} lies face-down with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + if "standing" in context: + return f"{woman} stands braced with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + if "kneeling" in context: + return f"{woman} kneels forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + + +def build_anal_or_double_role_graph( + woman: str, + man: str, + third: str, + people_count: int, + item_text: str, + item_axis_values: dict[str, Any] | None = None, +) -> str: + context = _context_text(item_text, item_axis_values) + if "double" in context or "toy" in context: + if people_count >= 3 and third: + return f"{man} thrusts his penis into {woman} while {third} adds a second penetration point from the front." + return _two_person_double_graph(woman, man, context) + if people_count >= 3 and third: + return f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front." + return _anal_position_graph(woman, man, context) diff --git a/hardcore_role_graphs.py b/hardcore_role_graphs.py index a98378e..efe845b 100644 --- a/hardcore_role_graphs.py +++ b/hardcore_role_graphs.py @@ -5,10 +5,12 @@ import re from typing import Any try: + from .hardcore_role_anal import build_anal_or_double_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_oral import build_oral_role_graph from hardcore_role_outercourse import build_outercourse_role_graph from hardcore_role_penetration import build_penetration_role_graph @@ -240,30 +242,6 @@ def build_hardcore_role_graph( 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." - def anal_position_graph(woman: str, man: str) -> str: - text = " ".join( - str(part or "").lower() - for part in ( - item_text, - *((item_axis_values or {}).values()), - ) - ) - if "bent-over" in text or "bent over" in text: - return f"{woman} is bent forward with hips raised while {man} stands behind her and thrusts his penis into her ass." - if "face-down" in text: - return f"{woman} lies face-down with ass raised while {man} is positioned behind her and thrusts his penis into her ass." - if "doggy" in text or "rear-entry" in text: - return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - if "standing" in text: - return f"{woman} stands braced with hips angled back while {man} stands behind her and thrusts his penis into her ass." - if "spooning" in text or "side-lying" in text: - return f"{woman} lies on her side with thighs parted while {man} presses behind her and thrusts his penis into her ass." - if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text: - return f"{woman} lies near a raised edge with hips exposed while {man} kneels behind her and thrusts his penis into her ass." - if "kneeling" in text: - return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass." - return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - if people_count == 1: solo = people[0] if women_count == 1: @@ -354,24 +332,7 @@ def build_hardcore_role_graph( elif "oral" in slug: graph = build_oral_role_graph(woman, man, item_text, item_axis_values, pov_labels) elif "anal" in slug or "double" in slug: - if "double" in item_text or "toy" in item_text: - if people_count >= 3: - graph = f"{man} thrusts his penis into {woman} while {third} adds a second penetration point from the front." - else: - if "bent-over" in item_text or "bent over" in item_text: - graph = f"{woman} is bent forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - elif "face-down" in item_text: - graph = f"{woman} lies face-down with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - elif "standing" in item_text: - graph = f"{woman} stands braced with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - elif "kneeling" in item_text: - graph = f"{woman} kneels forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - else: - graph = f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." - elif people_count >= 3: - graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front." - else: - graph = anal_position_graph(woman, man) + graph = build_anal_or_double_role_graph(woman, man, third, people_count, item_text, item_axis_values) elif "threesome" in slug: graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body." elif "group" in slug or "orgy" in slug: diff --git a/krea_pov_actions.py b/krea_pov_actions.py index 9ad91d8..4d99a13 100644 --- a/krea_pov_actions.py +++ b/krea_pov_actions.py @@ -138,6 +138,10 @@ def pov_hardcore_pose_sentence( action_lower = action_text.lower() if not context: context = action_lower + position_text = "" + if isinstance(axis_values, dict): + position_text = _clean(axis_values.get("position", "")).lower() + position_context = position_text or context def sentence(base: str) -> str: details = "" @@ -212,63 +216,68 @@ def pov_hardcore_pose_sentence( contact = pov_contact_clause(action, role_graph, hard_item, axis_values, context) - if "reverse cowgirl" in context: + if "reverse cowgirl" in position_context: return sentence( "POV reverse cowgirl position: the viewer lies on his back while the woman straddles his hips facing away; " f"her back, ass, thighs, and the viewer's foreground legs are visible {contact}" ) - if "cowgirl" in context or "straddling a partner" in context or "squatting on top" in context: + if "cowgirl" in position_context or "straddling a partner" in position_context or "squatting on top" in position_context: return sentence( "POV cowgirl position: the viewer lies on his back while the woman straddles his hips facing him; " f"her torso, hips, and open thighs fill the frame from below {contact}" ) - if "lotus" in context or "seated in a partner's lap" in context: + if "lotus" in position_context or "seated in a partner's lap" in position_context: return sentence( "POV lotus position: the viewer sits upright while the woman sits in his lap facing him with her legs around his hips; " f"her torso and hips stay close to the viewer {contact}" ) - if "kneeling straddle" in context: + if "kneeling straddle" in position_context: return sentence( "POV kneeling straddle position: the viewer kneels upright while the woman straddles his hips facing him; " f"both torsos are upright and her hips press directly against him {contact}" ) - if "face-down" in context or "face down" in context: + if "face-down" in position_context or "face down" in position_context: return sentence( "The woman is seen from behind with her ass raised toward the POV viewer, lying face-down with hips lifted; " f"the viewer looks down at her raised ass with foreground hands on her hips {contact}" ) - if "bent-over" in context or "bent over" in context or "bent forward" in context: - return sentence( - "The woman is seen from behind with her ass raised toward the POV viewer, bent forward at the waist with hips lifted and head turned back; " - f"the viewer looks down at her raised ass from behind with foreground hands near her hips {contact}" - ) - if "doggy" in context or "all fours" in context or "rear-entry" in context: - return sentence( - "The woman is seen from behind with her ass raised toward the POV viewer, on all fours directly in front of him with hips high and back arched; " - f"the viewer looks down at her raised ass with his hands on her hips in the foreground {contact}" - ) - if "standing" in context: - return sentence( - "POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; " - f"the viewer stands behind her at hip level {contact}" - ) - if "spooning" in context or "side-lying" in context or "lies on her side" in context: - return sentence( - "POV side-lying sex position: the woman lies on her side in front of the viewer with thighs parted; " - f"the viewer is behind her along the same body line {contact}" - ) if ( - "edge-supported" in context - or "raised edge" in context - or "edge of bed" in context - or "bed edge" in context - or "kneels between her legs" in context + "edge-supported" in position_context + or "raised edge" in position_context + or "edge of bed" in position_context + or "bed edge" in position_context + or (not position_text and "kneels between her legs" in context) ): return sentence( "POV raised-edge penetration position: the woman reclines at the raised edge with thighs open toward the viewer; " f"the viewer kneels between her legs with his hands near her hips {contact}" ) - if "missionary" in context or ("lies on her back" in context and ("legs open" in context or "thighs open" in context)): + if "standing" in position_context: + return sentence( + "POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; " + f"the viewer stands behind her at hip level {contact}" + ) + if "spooning" in position_context or "side-lying" in position_context or "lies on her side" in position_context: + return sentence( + "POV side-lying sex position: the woman lies on her side in front of the viewer with thighs parted; " + f"the viewer is behind her along the same body line {contact}" + ) + if "doggy" in position_context or "all fours" in position_context or "rear-entry" in position_context: + return sentence( + "The woman is seen from behind with her ass raised toward the POV viewer, on all fours directly in front of him with hips high and back arched; " + f"the viewer looks down at her raised ass with his hands on her hips in the foreground {contact}" + ) + if "kneeling" in position_context: + return sentence( + "POV kneeling rear-entry position: the woman kneels forward in front of the viewer with hips raised and thighs apart; " + f"the viewer kneels behind her at hip level with foreground hands near her waist {contact}" + ) + if "bent-over" in position_context or "bent over" in position_context or "bent forward" in position_context: + return sentence( + "The woman is seen from behind with her ass raised toward the POV viewer, bent forward at the waist with hips lifted and head turned back; " + f"the viewer looks down at her raised ass from behind with foreground hands near her hips {contact}" + ) + if "missionary" in position_context or (not position_text and "lies on her back" in context and ("legs open" in context or "thighs open" in context)): return sentence( "POV missionary position: the woman lies on her back with legs open around the viewer's hips; " f"the viewer is above her with foreground arms braced beside her body {contact}" diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 19a8e1c..eeda5c6 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -163,6 +163,23 @@ def _character_cast(*, pov_man: bool = False) -> str: )["character_cast"] +def _character_cast_two_men(*, pov_first_man: bool = False) -> str: + cast = _character_cast(pov_man=pov_first_man) + return pb.build_character_slot_json( + subject_type="man", + label="B", + age="41-year-old adult", + ethnicity="western_european", + figure="balanced", + body="average", + descriptor_detail="compact", + expression_intensity=0.55, + softcore_expression_intensity=0.35, + hardcore_expression_intensity=0.75, + character_cast=cast, + )["character_cast"] + + def _action_filter(focus: str, hardcore_position_config: str | dict[str, Any] | None = "") -> str: kwargs = { "allow_toys": False, @@ -192,6 +209,28 @@ def _position_filter(focus: str, family: str, positions: list[str] | tuple[str, return _action_filter(focus, position_config) +def _anal_double_filter(positions: list[str] | tuple[str, ...] | str) -> str: + position_config = pb.build_hardcore_position_pool_json( + combine_mode="replace", + family="anal", + selected_positions=positions, + ) + return pb.build_hardcore_action_filter_json( + hardcore_position_config=position_config, + focus="anal_only", + allow_toys=True, + allow_double=True, + allow_penetration=True, + allow_foreplay=False, + allow_interaction=False, + allow_manual=False, + allow_oral=False, + allow_outercourse=False, + allow_anal=True, + allow_climax=False, + ) + + def _coworking_location_config() -> str: return pb.build_location_pool_json( enabled=True, @@ -967,6 +1006,135 @@ def smoke_pov_penetration_position_routes() -> None: _expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}") +def smoke_pov_anal_position_routes() -> None: + cases = [ + ( + "pov_anal_doggy", + "doggy", + ("on all fours", "positioned behind her"), + ("on all fours directly in front", "penetrates her ass"), + ), + ( + "pov_anal_bent_over", + "bent_over", + ("bent forward", "stands behind her"), + ("bent forward at the waist", "penetrates her ass"), + ), + ( + "pov_anal_face_down", + "face_down_ass_up", + ("lies face-down", "ass raised"), + ("lying face-down", "penetrates her ass"), + ), + ( + "pov_anal_standing", + "standing", + ("stands braced", "stands behind her"), + ("pov standing rear-entry position", "viewer stands behind her"), + ), + ( + "pov_anal_side_lying", + "side_lying", + ("lies on her side", "presses behind her"), + ("pov side-lying sex position", "viewer is behind her"), + ), + ( + "pov_anal_edge_supported", + "edge_supported", + ("raised edge", "kneels behind her"), + ("pov raised-edge penetration position", "viewer kneels between her legs"), + ), + ( + "pov_anal_kneeling", + "kneeling", + ("kneels forward", "kneels behind her"), + ("pov kneeling rear-entry position", "viewer kneels behind her"), + ), + ] + for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3901): + pair = pb.build_insta_of_pair( + row_number=1, + start_index=1, + seed=offset, + ethnicity="any", + figure="random", + no_plus_women=False, + no_black=False, + trigger=Trigger, + prepend_trigger_to_prompt=True, + options_json=_insta_options( + softcore_camera_mode="from_camera_config", + hardcore_camera_mode="from_camera_config", + camera_detail="compact", + ), + character_cast=_character_cast(pov_man=True), + hardcore_position_config=_position_filter("anal_only", "anal", [position_key]), + location_config=_coworking_location_config(), + hardcore_camera_config=_orbit_camera( + horizontal_angle=45, + vertical_angle=0, + zoom=7.5, + subject_focus="action", + ), + ) + _expect_pair(pair, name) + hard_row = pair.get("hardcore_row") or {} + _expect(hard_row.get("position_family") == "anal", f"{name} position_family should be anal") + _expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}") + role_graph = _expect_text(f"{name}.source_role_graph", hard_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(pair), target="hardcore") + 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("viewer" in prompt and "first-person" in prompt, f"{name} Krea prompt lost POV wording") + _expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive") + for term in krea_terms: + _expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}") + + +def smoke_double_front_back_route() -> None: + pair = pb.build_insta_of_pair( + row_number=1, + start_index=1, + seed=3911, + ethnicity="any", + figure="random", + no_plus_women=False, + no_black=False, + trigger=Trigger, + prepend_trigger_to_prompt=True, + options_json=_insta_options( + hardcore_cast="mixed_group", + hardcore_men_count=2, + softcore_camera_mode="from_camera_config", + hardcore_camera_mode="from_camera_config", + camera_detail="compact", + ), + character_cast=_character_cast_two_men(), + hardcore_position_config=_anal_double_filter(["front_back"]), + location_config=_coworking_location_config(), + hardcore_camera_config=_orbit_camera( + horizontal_angle=45, + vertical_angle=0, + zoom=7.5, + subject_focus="action", + ), + ) + _expect_pair(pair, "double_front_back_route") + hard_row = pair.get("hardcore_row") or {} + _expect(hard_row.get("position_family") == "anal", "double route position_family should be anal") + _expect("front_back" in (hard_row.get("position_keys") or []), "double route lost front_back key") + role_graph = _expect_text("double_front_back_route.source_role_graph", hard_row.get("source_role_graph"), 40).lower() + _expect("second penetration point from the front" in role_graph, f"double route role graph lost front/back placement: {role_graph}") + krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore") + prompt = _expect_text("double_front_back_route.krea_prompt", krea.get("krea_prompt"), 60).lower() + _expect("metadata" in krea.get("method", ""), "double route Krea did not use metadata") + _expect("front-and-back" in prompt, "double route Krea lost front/back position wording") + _expect("second penetration point" in prompt, "double route Krea lost second-contact wording") + _expect("role graph:" not in prompt and "sexual scene:" not in prompt, "double route Krea leaked raw labels") + + def smoke_no_expression_fallback() -> None: cast = pb.build_character_slot_json( subject_type="woman", @@ -1101,6 +1269,8 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("pov_outercourse_position_routes", smoke_pov_outercourse_position_routes), ("pov_oral_position_routes", smoke_pov_oral_position_routes), ("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), ("expression_disabled", smoke_no_expression_fallback), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ]