diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index fddc0fc..03a4507 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -102,6 +102,9 @@ Already isolated: - hardcore configured-cast role graph generation lives in `hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and then asks that module for the source role graph. +- interaction-style role graph wording lives in `hardcore_role_interaction.py`, + covering foreplay, manual stimulation, body worship, clothing transitions, + dominant guidance, camera performance, aftercare, and group coordination. - outercourse-specific role graph wording has started moving into action-family modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking, penis-licking, handjob, and footjob body geometry. @@ -318,6 +321,8 @@ Near-term: 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. +- Cover generated interaction routes through Krea, SDXL, and natural caption + outputs so source contact/guidance/presentation wording stays metadata-driven. Medium-term: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 76375d2..9c2cfad 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -65,6 +65,7 @@ Core helper ownership: | Python module | What it owns | | --- | --- | | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | +| `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. | | `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. | @@ -752,6 +753,9 @@ pair metadata through the core Python APIs, then verifies: - 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; +- interaction routes keep source role graphs, Krea prose, SDXL tags, and + training captions synchronized for manual, clothing transition, body worship, + camera-performance, aftercare, and group-coordination rows; - 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 b61920f..3c2614b 100644 --- a/hardcore_role_graphs.py +++ b/hardcore_role_graphs.py @@ -6,12 +6,24 @@ 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_interaction import ( + build_foreplay_role_graph, + build_group_coordination_role_graph, + build_interaction_role_graph, + build_manual_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_interaction import ( + build_foreplay_role_graph, + build_group_coordination_role_graph, + build_interaction_role_graph, + build_manual_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 @@ -91,112 +103,11 @@ def build_hardcore_role_graph( ] return f" {extra} {rng.choice(actions)}." - def foreplay_position_graph(primary: str, partner: str) -> str: - text = " ".join( - str(part or "").lower() - for part in ( - item_text, - *((item_axis_values or {}).values()), - ) - ) - if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")): - return ( - f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; " - f"{primary}'s exposed skin and the clothing being removed stay clearly visible." - ) - if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")): - return ( - f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; " - f"their faces stay close and the breast-touching gesture is clear." - ) - if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")): - return ( - f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; " - f"their lips are close and the face-touching gesture is clear." - ) - if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")): - return ( - f"{primary} and {partner} press their bodies together and kiss deeply, " - f"with hands on each other's face, waist, and hips." - ) - return ( - f"{primary} and {partner} are pressed close in a heated foreplay setup, " - f"hands caressing skin while clothing is pulled aside." - ) - - def interaction_text() -> str: - return " ".join( - str(part or "").lower() - for part in ( - item_text, - *((item_axis_values or {}).values()), - ) - ) - - def manual_position_graph(primary: str, partner: str = "") -> str: - text = interaction_text() - if not partner: - if "mutual" in text: - return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing." - return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy." - if "mutual" in text: - return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible." - if "clit" in text or "clitoris" in text: - return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers rubbing her clit as her hips tilt toward the touch." - if "toy" in text or "vibrator" in text: - return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open." - return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy." - - def interaction_position_graph(primary: str, partner: str, third: str = "") -> str: - text = interaction_text() - if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")): - if "cleanup" in text or "wiping" in text or "towel" in text: - return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible." - return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle." - if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")): - if third: - return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer." - return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal." - if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")): - if "ass" in text: - return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin." - if "thigh" in text: - return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place." - if "nipple" in text or "breast" in text: - return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast." - return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs." - if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")): - if "turn" in text or "rear-facing" in text: - return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose." - if "legs" in text or "thigh" in text: - return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible." - return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose." - if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")): - if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text: - return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close." - if "wrist" in text or "wrists" in text: - return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible." - if "hair" in text: - return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible." - if "thigh" in text or "spread" in text: - return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera." - return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable." - return foreplay_position_graph(primary, partner) - - def group_coordination_graph(primary: str, partner: str, third: str) -> str: - observer = third or any_person({primary, partner}) - text = interaction_text() - if "camera" in text or "hold" in text or "present" in text: - return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible." - if "watch" in text or "waiting" in text: - 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." - if people_count == 1: solo = people[0] if women_count == 1: if "manual_stimulation" in slug: - return manual_position_graph(solo) + return build_manual_role_graph(solo, item_text=item_text, item_axis_values=item_axis_values) if "camera_performance" in slug: return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose." if "cumshot" in slug or "climax" in slug: @@ -211,16 +122,16 @@ def build_hardcore_role_graph( c = any_woman({a, b}) if len(women) >= 3 else "" used = {a, b} if "manual_stimulation" in slug: - graph = manual_position_graph(a, b) + graph = build_manual_role_graph(a, b, item_text, item_axis_values) elif "group_coordination" in slug and c: - graph = group_coordination_graph(a, b, c) + graph = build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values) used.add(c) elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): - graph = interaction_position_graph(a, b, c) + graph = build_interaction_role_graph(a, b, c, slug, item_text, item_axis_values) if c and "camera_performance" in slug: used.add(c) elif "foreplay" in slug: - graph = foreplay_position_graph(a, b) + graph = build_foreplay_role_graph(a, b, item_text, item_axis_values) elif "outercourse" in slug: graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact." elif "oral" in slug: @@ -244,7 +155,7 @@ def build_hardcore_role_graph( if "manual_stimulation" in slug: graph = f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup." elif "group_coordination" in slug and c: - graph = group_coordination_graph(a, b, c) + graph = build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values) used.add(c) elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside." @@ -270,13 +181,20 @@ def build_hardcore_role_graph( man = any_man() third = any_person({woman, man}) if people_count >= 3 else "" if "manual_stimulation" in slug: - graph = manual_position_graph(woman, man) + graph = build_manual_role_graph(woman, man, item_text, item_axis_values) elif "group_coordination" in slug: - graph = group_coordination_graph(woman, man, third) + graph = build_group_coordination_role_graph( + woman, + man, + third, + any_person({woman, man}) if not third else "", + item_text, + item_axis_values, + ) elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): - graph = interaction_position_graph(woman, man, third) + graph = build_interaction_role_graph(woman, man, third, slug, item_text, item_axis_values) elif "foreplay" in slug: - graph = foreplay_position_graph(woman, man) + graph = build_foreplay_role_graph(woman, man, item_text, item_axis_values) elif "outercourse" in slug: graph = build_outercourse_role_graph(woman, man, item_text, item_axis_values, pov_labels) elif "oral" in slug: diff --git a/hardcore_role_interaction.py b/hardcore_role_interaction.py new file mode 100644 index 0000000..bb5359a --- /dev/null +++ b/hardcore_role_interaction.py @@ -0,0 +1,127 @@ +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_foreplay_role_graph( + primary: str, + partner: str, + item_text: str, + item_axis_values: dict[str, Any] | None = None, +) -> str: + text = _context_text(item_text, item_axis_values) + if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")): + return ( + f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; " + f"{primary}'s exposed skin and the clothing being removed stay clearly visible." + ) + if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")): + return ( + f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; " + f"their faces stay close and the breast-touching gesture is clear." + ) + if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")): + return ( + f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; " + f"their lips are close and the face-touching gesture is clear." + ) + if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")): + return ( + f"{primary} and {partner} press their bodies together and kiss deeply, " + f"with hands on each other's face, waist, and hips." + ) + return ( + f"{primary} and {partner} are pressed close in a heated foreplay setup, " + f"hands caressing skin while clothing is pulled aside." + ) + + +def build_manual_role_graph( + primary: str, + partner: str = "", + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> str: + text = _context_text(item_text, item_axis_values) + if not partner: + if "mutual" in text: + return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing." + return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy." + if "mutual" in text: + return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible." + if "clit" in text or "clitoris" in text: + return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers rubbing her clit as her hips tilt toward the touch." + if "toy" in text or "vibrator" in text: + return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open." + return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy." + + +def build_interaction_role_graph( + primary: str, + partner: str, + third: str = "", + slug: str = "", + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> str: + text = _context_text(item_text, item_axis_values) + if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")): + if "cleanup" in text or "wiping" in text or "towel" in text: + return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible." + return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle." + if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")): + if third: + return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer." + return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal." + if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")): + if "ass" in text: + return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin." + if "thigh" in text: + return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place." + if "nipple" in text or "breast" in text: + return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast." + return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs." + if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")): + if "turn" in text or "rear-facing" in text: + return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose." + if "legs" in text or "thigh" in text: + return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible." + return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose." + if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")): + if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text: + return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close." + if "wrist" in text or "wrists" in text: + return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible." + if "hair" in text: + return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible." + if "thigh" in text or "spread" in text: + return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera." + return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable." + return build_foreplay_role_graph(primary, partner, item_text, item_axis_values) + + +def build_group_coordination_role_graph( + primary: str, + partner: str, + third: str = "", + fallback_observer: str = "", + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> str: + observer = third or fallback_observer or partner + text = _context_text(item_text, item_axis_values) + if "camera" in text or "hold" in text or "present" in text: + return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible." + if "watch" in text or "waiting" in text: + 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." diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index ae4193a..90ad946 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -1217,6 +1217,114 @@ def smoke_climax_position_routes() -> None: _expect_formatter_outputs(row, name, target="single") +def smoke_interaction_role_graph_routes() -> None: + cases = [ + ( + "interaction_manual", + "Manual stimulation", + "manual_only", + "manual", + "fingering", + 4301, + _character_cast(), + 1, + 1, + ("reclines with thighs open", "fingers visibly stimulating"), + ("fingers visibly stimulating", "between her legs"), + ), + ( + "interaction_clothing_transition", + "Clothing and position transitions", + "interaction_only", + "interaction", + "position_transition", + 4302, + _character_cast(), + 1, + 1, + ("mid-transition", "moving clothing aside"), + ("mid-transition", "guiding the woman's hips"), + ), + ( + "interaction_body_worship", + "Body worship and touching", + "interaction_only", + "interaction", + "body_worship", + 4301, + _character_cast(), + 1, + 1, + ("kisses down her body", "hands tracing"), + ("kisses down her body", "hands tracing"), + ), + ( + "interaction_camera_performance", + "Camera performance", + "interaction_only", + "interaction", + "camera_showing", + 4301, + _character_cast(), + 1, + 1, + ("faces the camera", "creator-shot reveal"), + ("faces the camera", "creator-shot reveal"), + ), + ( + "interaction_aftercare", + "Aftercare and cleanup", + "interaction_only", + "interaction", + "aftercare", + 4301, + _character_cast(), + 1, + 1, + ("lie close together after sex", "post-sex cuddle"), + ("lie close together after sex", "post-sex cuddle"), + ), + ( + "interaction_group_coordination", + "Group coordination", + "interaction_only", + "interaction", + "watching", + 4301, + _character_cast_two_men(), + 1, + 2, + ("is centered", "hold and present"), + ("is centered", "each role clearly visible"), + ), + ] + for name, subcategory, focus, family, position_key, seed, cast, women_count, men_count, role_terms, krea_terms in cases: + row = _prompt_row( + name=name, + category="Hardcore sexual poses", + subcategory=subcategory, + seed=seed, + character_cast=cast, + women_count=women_count, + men_count=men_count, + hardcore_position_config=_position_filter(focus, family, [position_key]), + ) + _expect_custom_row(row, name) + _expect(row.get("action_family") == "foreplay", f"{name} action_family should stay formatter foreplay") + _expect(row.get("position_family") == family, f"{name} position_family mismatch: {row.get('position_family')}") + _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}") + _expect_formatter_outputs(row, name, target="single") + + def smoke_no_expression_fallback() -> None: cast = pb.build_character_slot_json( subject_type="woman", @@ -1354,6 +1462,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("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), + ("interaction_role_graph_routes", smoke_interaction_role_graph_routes), ("expression_disabled", smoke_no_expression_fallback), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ]