diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 0a180aa..d25bdc1 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -108,6 +108,9 @@ Already isolated: - oral-specific role graph wording lives in `hardcore_role_oral.py`, including direct POV viewer phrasing for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral positions. +- 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. - camera-scene prose and coworking composition adaptation live in `scene_camera_adapters.py`; `prompt_builder.py` still owns camera config parsing and row mutation. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index b72e827..25ec64f 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -67,6 +67,7 @@ Core helper ownership: | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | | `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_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. | @@ -737,6 +738,8 @@ pair metadata through the core Python APIs, then verifies: - POV oral routes keep constrained kneeling, face-sitting, sixty-nine, edge-supported, side-lying, and chair oral geometry through Krea formatting without recursive viewer wording; +- POV penetration routes keep constrained missionary, cowgirl, reverse-cowgirl, + doggy, raised-edge, and lotus geometry through Krea formatting; - 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_graphs.py b/hardcore_role_graphs.py index 0563e62..a98378e 100644 --- a/hardcore_role_graphs.py +++ b/hardcore_role_graphs.py @@ -7,9 +7,11 @@ from typing import Any try: 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_oral import build_oral_role_graph from hardcore_role_outercourse import build_outercourse_role_graph + from hardcore_role_penetration import build_penetration_role_graph def _lettered(prefix: str, count: int) -> list[str]: @@ -238,43 +240,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 penetration_position_graph(woman: str, man: str) -> str: - text = " ".join( - str(part or "").lower() - for part in ( - item_text, - *((item_axis_values or {}).values()), - ) - ) - if "missionary" in text: - return ( - f"{woman} lies on her back with legs open around {man}'s hips while {man} is above her between her thighs; " - f"{man}'s hips press close and {man}'s penis thrusts into her pussy." - ) - if "reverse cowgirl" in text: - return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her pussy." - if "cowgirl" in text or "straddling" in text: - return f"{woman} straddles {man}'s hips facing him while {man} lies under her and {man}'s penis thrusts into her pussy." - if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text: - return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her pussy." - if "standing" in text: - return f"{woman} stands braced with hips angled back while {man} stands behind her and {man}'s penis thrusts into her pussy." - 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 {man}'s penis thrusts into her pussy." - if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text or "edge-supported" in text or "raised edge" in text: - return ( - f"{woman} lies back at a raised edge with hips at the edge and legs open while {man} kneels between her thighs; " - f"{man}'s hips press close and {man}'s penis thrusts into her pussy." - ) - if "kneeling straddle" in text: - return f"{woman} kneels straddling {man}'s hips while {man} supports her waist and {man}'s penis thrusts into her pussy." - if "lotus" in text: - return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her pussy." - return ( - f"{woman} lies on her back with legs spread wide and knees bent outward while {man} kneels between her open thighs facing her; " - f"{man}'s hips are pressed between her legs and {man}'s penis thrusts into her pussy." - ) - def anal_position_graph(woman: str, man: str) -> str: text = " ".join( str(part or "").lower() @@ -414,5 +379,5 @@ def build_hardcore_role_graph( elif "cumshot" in slug or "climax" in slug: graph = climax_position_graph(woman, man, third) else: - graph = penetration_position_graph(woman, man) + 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/hardcore_role_penetration.py b/hardcore_role_penetration.py new file mode 100644 index 0000000..dfe0c95 --- /dev/null +++ b/hardcore_role_penetration.py @@ -0,0 +1,50 @@ +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 build_penetration_role_graph( + woman: str, + man: str, + item_text: str, + item_axis_values: dict[str, Any] | None = None, +) -> str: + text = _context_text(item_text, item_axis_values) + if "missionary" in text: + return ( + f"{woman} lies on her back with legs open around {man}'s hips while {man} is above her between her thighs; " + f"{man}'s hips press close and {man}'s penis thrusts into her pussy." + ) + if "reverse cowgirl" in text: + return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her pussy." + if "cowgirl" in text or "straddling" in text: + return f"{woman} straddles {man}'s hips facing him while {man} lies under her and {man}'s penis thrusts into her pussy." + if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text: + return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her pussy." + if "standing" in text: + return f"{woman} stands braced with hips angled back while {man} stands behind her and {man}'s penis thrusts into her pussy." + 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 {man}'s penis thrusts into her pussy." + if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text or "edge-supported" in text or "raised edge" in text: + return ( + f"{woman} lies back at a raised edge with hips at the edge and legs open while {man} kneels between her thighs; " + f"{man}'s hips press close and {man}'s penis thrusts into her pussy." + ) + if "kneeling straddle" in text: + return f"{woman} kneels straddling {man}'s hips while {man} supports her waist and {man}'s penis thrusts into her pussy." + if "lotus" in text: + return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her pussy." + return ( + f"{woman} lies on her back with legs spread wide and knees bent outward while {man} kneels between her open thighs facing her; " + f"{man}'s hips are pressed between her legs and {man}'s penis thrusts into her pussy." + ) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index c05add3..19a8e1c 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -885,6 +885,88 @@ def smoke_pov_oral_position_routes() -> None: _expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}") +def smoke_pov_penetration_position_routes() -> None: + cases = [ + ( + "pov_penetration_missionary", + "missionary", + ("woman a lies on her back", "man a is above her between her thighs"), + ("pov missionary position", "viewer is above her", "penetrates her pussy"), + ), + ( + "pov_penetration_cowgirl", + "cowgirl", + ("woman a straddles man a's hips facing him", "man a lies under her"), + ("pov cowgirl position", "viewer lies on his back", "woman straddles his hips"), + ), + ( + "pov_penetration_reverse_cowgirl", + "reverse_cowgirl", + ("woman a straddles man a's hips facing away", "man a lies under her"), + ("pov reverse cowgirl position", "facing away", "viewer lies on his back"), + ), + ( + "pov_penetration_doggy", + "doggy", + ("woman a is on all fours", "man a is positioned behind her"), + ("ass raised toward the pov viewer", "on all fours", "penetrates her pussy"), + ), + ( + "pov_penetration_edge_supported", + "edge_supported", + ("raised edge", "man a kneels between her thighs"), + ("pov raised-edge penetration position", "viewer kneels between her legs", "penetrates her pussy"), + ), + ( + "pov_penetration_lotus", + "lotus_lap", + ("woman a sits in man a's lap", "legs around his hips"), + ("pov lotus position", "woman sits in his lap", "penetrates her pussy"), + ), + ] + for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3801): + 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("penetration_only", "penetrative", [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("action_family") == "penetration", f"{name} action_family should be penetration") + _expect(hard_row.get("position_family") == "penetrative", f"{name} position_family should be penetrative") + _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 "pov" 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_no_expression_fallback() -> None: cast = pb.build_character_slot_json( subject_type="woman", @@ -1018,6 +1100,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("krea_pov_penetration_route", smoke_krea_pov_penetration_route), ("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), ("expression_disabled", smoke_no_expression_fallback), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ]