From f27ba23a625e41b2a3ac7a50c7910fb522f9c371 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 16:57:08 +0200 Subject: [PATCH] Extract hardcore role graph builder --- docs/prompt-architecture-improvement-plan.md | 9 +- docs/prompt-pool-routing-map.md | 9 + hardcore_role_graphs.py | 576 +++++++++++++++++++ prompt_builder.py | 576 +------------------ 4 files changed, 593 insertions(+), 577 deletions(-) create mode 100644 hardcore_role_graphs.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index d6d153d..b8d69b5 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -95,12 +95,13 @@ Keep here: Move or isolate later: -- role graph generation for hardcore interaction categories into a dedicated - module, for example `hardcore_role_graphs.py`; - category-library loading and inheritance helpers into `category_library.py`. 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. - camera-scene prose and coworking composition adaptation live in `scene_camera_adapters.py`; `prompt_builder.py` still owns camera config parsing and row mutation. @@ -357,7 +358,7 @@ Medium-term: 1. Split `__init__.py` node classes by family after behavior is covered by smoke checks. -2. Extract hardcore role graph generation from `prompt_builder.py` into a - dedicated `hardcore_role_graphs.py` module. +2. Split the internals of `hardcore_role_graphs.py` by action family once more + generated edge cases are covered by smoke fixtures. 3. Add more route-level smoke fixtures for generated edge cases that are not covered by the current static Krea/SDXL/caption metadata fixtures. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 6db8963..ecf369c 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -60,6 +60,15 @@ call the same core generation functions. | `SxCP SDXL Formatter` | `format_sdxl_prompt` | Converts metadata rows or pair metadata into SDXL/tag style prompts. | | `SxCP Caption Naturalizer` | `naturalize_caption` | Converts rows into more natural sentence captions. | +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_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. | + ## Node IO Map Use this when wiring or debugging a workflow. If the formatter can receive diff --git a/hardcore_role_graphs.py b/hardcore_role_graphs.py new file mode 100644 index 0000000..6d45ce1 --- /dev/null +++ b/hardcore_role_graphs.py @@ -0,0 +1,576 @@ +from __future__ import annotations + +import random +import re +from typing import Any + + +def _lettered(prefix: str, count: int) -> list[str]: + letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + return [f"{prefix.capitalize()} {letters[index]}" for index in range(max(0, count))] + + +def _pick_distinct(rng: random.Random, items: list[str], count: int) -> list[str]: + if not items: + return [] + if len(items) >= count: + return rng.sample(items, count) + picked = list(items) + while len(picked) < count: + picked.append(items[rng.randrange(len(items))]) + return picked + + +def _participant_context(women_count: int, men_count: int) -> dict[str, list[str]]: + women = _lettered("woman", women_count) + men = _lettered("man", men_count) + return {"women": women, "men": men, "people": women + men} + + +def build_hardcore_role_graph( + rng: random.Random, + subcategory: dict[str, Any], + context: dict[str, Any], + item_axis_values: dict[str, Any] | None = None, + pov_labels: list[str] | None = None, +) -> str: + if context.get("subject_type") != "configured_cast": + return "" + women_count = int(context.get("women_count") or 0) + men_count = int(context.get("men_count") or 0) + people_count = women_count + men_count + if people_count <= 0: + return "" + + participants = _participant_context(women_count, men_count) + women = participants["women"] + men = participants["men"] + people = participants["people"] + slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower() + item_text = " ".join((item_axis_values or {}).values()).lower() + pov_set = set(pov_labels or []) + + def any_person(exclude: set[str] | None = None) -> str: + exclude = exclude or set() + pool = [person for person in people if person not in exclude] or people + return rng.choice(pool) + + def any_woman(exclude: set[str] | None = None) -> str: + exclude = exclude or set() + pool = [person for person in women if person not in exclude] or [person for person in people if person not in exclude] or people + return rng.choice(pool) + + def any_man(exclude: set[str] | None = None) -> str: + exclude = exclude or set() + pool = [person for person in men if person not in exclude] or [person for person in people if person not in exclude] or people + return rng.choice(pool) + + def support_sentence(exclude: set[str]) -> str: + extras = [person for person in people if person not in exclude] + if not extras: + return "" + extra = rng.choice(extras) + actions = [ + "kisses and grips the nearest body", + "holds hips open for the camera", + "touches breasts, thighs, and stomach", + "keeps one hand on a partner's ass", + "watches close and joins the body contact", + "presses in from the side with hands on skin", + ] + 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." + + 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." + + 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() + 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." + + def outercourse_position_graph(woman: str, man: str) -> str: + position_text = str((item_axis_values or {}).get("position") or "").lower() + text = " ".join( + str(part or "").lower() + for part in ( + item_text, + *((item_axis_values or {}).values()), + ) + ) + man_is_pov = man in pov_set + if any(term in text for term in ("boobjob", "titjob", "breast-sex", "breast sex")): + if man_is_pov: + return ( + f"{woman} kneels between the POV viewer's open thighs with her torso bent forward over his pelvis and shoulders low, " + "both hands lifting and pressing her breasts tightly around the POV viewer's penis shaft while the glans sits just below her lips." + ) + return ( + f"{woman} kneels between {man}'s open thighs with her torso bent forward over his pelvis and shoulders low while {man} sits with legs apart, " + f"{woman}'s hands lifting and pressing her breasts tightly around {man}'s penis shaft while the glans sits just below her lips." + ) + if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")): + if man_is_pov: + return ( + f"{woman} kneels very low between the POV viewer's open thighs with her torso bent forward and shoulders between his knees, " + "head tucked under the penis shaft at the base of the penis, mouth and tongue on the POV viewer's balls while his penis points upward above her face." + ) + return ( + f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, " + f"head tucked under the penis shaft at the base of his penis, mouth and tongue on his balls while {man}'s penis points upward above her face." + ) + if "penis-licking" in position_text or "penis licking" in text or "tongue along" in text or "tongue licking" in text: + if man_is_pov: + return ( + f"{woman} bends forward between the POV viewer's open thighs, head low under the POV viewer's penis with her face directly under the penis, " + "tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis." + ) + return ( + f"{woman} bends forward between {man}'s open thighs, head low under {man}'s penis with her face directly under the penis, " + f"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis." + ) + if "handjob" in position_text or "handjob" in text or "hand job" in text or "hand wrapped" in text: + if man_is_pov: + return ( + f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the penis shaft, " + "one hand wrapped around the POV viewer's penis shaft while the other hand steadies the base of the penis as she strokes toward the glans." + ) + return ( + f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind the penis shaft, " + f"one hand wrapped around {man}'s penis shaft while the other hand steadies the base of the penis as she strokes toward the glans." + ) + if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text: + if man_is_pov: + return ( + f"{woman} faces the POV viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera, " + "both soles wrapped around the POV viewer's penis shaft in the lower foreground." + ) + return ( + f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, " + f"wrapping both soles around {man}'s penis shaft while the contact stays centered." + ) + if man_is_pov: + return ( + f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, " + "with her mouth, hands, breasts, or feet visibly working around the penis shaft." + ) + return ( + f"{woman} kneels close to {man}'s hips and keeps {man}'s penis centered in clear non-penetrative contact, " + "with her mouth, hands, breasts, or feet visibly working around the penis shaft." + ) + + def oral_position_graph(woman: str, man: str) -> str: + position_text = str((item_axis_values or {}).get("position") or "").lower() + text = " ".join( + str(part or "").lower() + for part in ( + item_text, + *((item_axis_values or {}).values()), + ) + ) + man_is_pov = man in pov_set + woman_gives = any( + term in text + for term in ( + "fellatio", + "blowjob", + "deepthroat", + "penis sucking", + "penis in mouth", + "penis in her mouth", + "mouth stretched around a penis", + "lips wrapped", + ) + ) + man_gives = any( + term in text + for term in ( + "cunnilingus", + "pussy licking", + "tongue on pussy", + "mouth on pussy", + "pussy and tongue", + "face-sitting", + "tongue contact clearly visible", + ) + ) + if "mouth on genitals" in text and not woman_gives and not man_gives: + if any(term in text for term in ("face-sitting", "reclining", "straddled", "spread-leg", "open thighs")): + man_gives = True + else: + woman_gives = True + + if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text): + return f"{woman} and {man} lie head-to-hips in a sixty-nine position, with {woman}'s mouth on {man}'s penis and {man}'s mouth on {woman}'s pussy." + if "face-sitting" in position_text or ("face-sitting" in text and not position_text): + if man_is_pov: + return ( + f"{woman} is above the POV camera, straddling the POV viewer's face with thighs on both sides of his head, " + "pussy directly over the POV viewer's mouth for close first-person underview tongue contact." + ) + return f"{man} lies on his back while {woman} straddles his face with her thighs around his head and {man}'s mouth pressed to her pussy." + if "straddled oral" in position_text or ("straddled oral" in text and not position_text): + if woman_gives and not man_gives: + return f"{man} straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis." + return f"{woman} straddles above {man}'s face with her thighs framing his head while {man}'s mouth stays pressed to her pussy." + if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text): + if woman_gives and not man_gives: + return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth." + return f"{woman} lies on her side with her top thigh lifted while {man} lies beside her hips with his mouth pressed to her pussy." + if ( + "edge-of-bed oral" in position_text + or "edge of bed oral" in position_text + or "edge-supported oral" in position_text + or (("edge-of-bed oral" in text or "edge of bed oral" in text or "edge-supported oral" in text) and not position_text) + ): + if woman_gives and not man_gives: + return f"{man} sits at a raised edge with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." + return f"{woman} lies at a raised edge with thighs open while {man} kneels between her legs with his mouth on her pussy." + if "standing oral" in position_text or ("standing oral" in text and not position_text): + if man_gives and not woman_gives: + return f"{woman} stands braced with one thigh lifted while {man} kneels between her legs with his mouth on her pussy." + return f"{man} stands with hips forward while {woman} kneels in front of him at hip height and takes his penis in her mouth." + if "chair oral" in position_text or ("chair oral" in text and not position_text): + if man_gives and not woman_gives: + return f"{woman} sits in a chair with thighs open while {man} kneels between her legs with his mouth pressed to her pussy." + return f"{man} sits in a chair with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." + if ( + "reclining cunnilingus" in position_text + or "spread-leg oral" in position_text + or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text) + ): + if woman_gives and not man_gives: + return f"{man} reclines with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." + return f"{woman} reclines on her back with thighs spread while {man} kneels between her legs with his mouth on her pussy." + if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text): + if man_gives and not woman_gives: + return f"{woman} kneels with thighs parted and hips angled forward while {man} kneels in front of her with his mouth on her pussy." + return ( + f"{woman} kneels in front of {man}'s penis while {man} stands over her; " + f"{woman} takes {man}'s penis in her mouth with saliva dripping on the penis as {man} looks down toward her." + ) + if man_gives and not woman_gives: + return f"{woman} lies on her back with thighs open while {man} kneels between her legs with his mouth pressed to her pussy." + return f"{woman} kneels in front of {man}'s hips and takes his penis in her mouth while {man} keeps his hips aligned with her face." + + if people_count == 1: + solo = people[0] + if women_count == 1: + if "manual_stimulation" in slug: + return manual_position_graph(solo) + 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: + return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets." + return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness." + if "cumshot" in slug or "climax" in slug: + return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible." + return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing." + + if women_count > 0 and men_count == 0: + a, b = _pick_distinct(rng, women, 2) + c = any_woman({a, b}) if len(women) >= 3 else "" + used = {a, b} + if "manual_stimulation" in slug: + graph = manual_position_graph(a, b) + elif "group_coordination" in slug and c: + graph = group_coordination_graph(a, b, c) + 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) + if c and "camera_performance" in slug: + used.add(c) + elif "foreplay" in slug: + graph = foreplay_position_graph(a, b) + 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: + graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy." + elif "anal" in slug or "double" in slug: + graph = f"{a} uses a strap-on on {b} while keeping her hips held open." + elif "threesome" in slug or "group" in slug or "orgy" in slug: + helper = c or any_woman({a}) + graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies." + used.add(helper) + elif "cumshot" in slug or "climax" in slug: + graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets." + else: + graph = f"{a} uses a strap-on on {b} while their bodies stay pressed together." + return graph + support_sentence(used) + + if men_count > 0 and women_count == 0: + a, b = _pick_distinct(rng, men, 2) + c = any_man({a, b}) if len(men) >= 3 else "" + used = {a, b} + 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) + 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." + elif "foreplay" in slug: + graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside." + elif "outercourse" in slug: + graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet." + elif "oral" in slug: + graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips." + elif "anal" in slug or "double" in slug or "penetrative" in slug: + graph = f"{a} penetrates {b} anally while {b}'s hips are held open." + elif "threesome" in slug or "group" in slug or "orgy" in slug: + helper = c or any_man({a}) + graph = f"{a} penetrates {b} anally while {helper} gives oral contact from the front." + used.add(helper) + elif "cumshot" in slug or "climax" in slug: + graph = f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis." + else: + graph = f"{a} and {b} keep explicit penis and anal contact visible." + return graph + support_sentence(used) + + woman = any_woman() + man = any_man() + third = any_person({woman, man}) if people_count >= 3 else "" + if "manual_stimulation" in slug: + graph = manual_position_graph(woman, man) + elif "group_coordination" in slug: + graph = group_coordination_graph(woman, man, third) + 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) + elif "foreplay" in slug: + graph = foreplay_position_graph(woman, man) + elif "outercourse" in slug: + graph = outercourse_position_graph(woman, man) + elif "oral" in slug: + graph = oral_position_graph(woman, man) + 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) + 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: + 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) + else: + graph = penetration_position_graph(woman, man) + return graph + support_sentence({woman, man, third} if third else {woman, man}) diff --git a/prompt_builder.py b/prompt_builder.py index 04c72eb..4f5cf6e 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -16,6 +16,7 @@ try: sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, ) from .hardcore_action_metadata import source_hardcore_action_family + from .hardcore_role_graphs import build_hardcore_role_graph from .prompt_hygiene import ( sanitize_caption_text, sanitize_negative_text, @@ -29,6 +30,7 @@ except ImportError: # Allows local smoke tests with `python -c`. sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, ) from hardcore_action_metadata import source_hardcore_action_family + from hardcore_role_graphs import build_hardcore_role_graph from prompt_hygiene import ( sanitize_caption_text, sanitize_negative_text, @@ -5904,578 +5906,6 @@ def _couple_type_from_counts( return primary_subject, subject_phrase, pose, 1, 1 -def _lettered(prefix: str, count: int) -> list[str]: - letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - return [f"{prefix.capitalize()} {letters[index]}" for index in range(max(0, count))] - - -def _pick_distinct(rng: random.Random, items: list[str], count: int) -> list[str]: - if not items: - return [] - if len(items) >= count: - return rng.sample(items, count) - picked = list(items) - while len(picked) < count: - picked.append(items[rng.randrange(len(items))]) - return picked - - -def _participant_context(women_count: int, men_count: int) -> dict[str, list[str]]: - women = _lettered("woman", women_count) - men = _lettered("man", men_count) - return {"women": women, "men": men, "people": women + men} - - -def _role_graph( - rng: random.Random, - subcategory: dict[str, Any], - context: dict[str, str], - item_axis_values: dict[str, str] | None = None, - pov_labels: list[str] | None = None, -) -> str: - if context.get("subject_type") != "configured_cast": - return "" - women_count = int(context.get("women_count") or 0) - men_count = int(context.get("men_count") or 0) - people_count = women_count + men_count - if people_count <= 0: - return "" - - participants = _participant_context(women_count, men_count) - women = participants["women"] - men = participants["men"] - people = participants["people"] - slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower() - item_text = " ".join((item_axis_values or {}).values()).lower() - pov_set = set(pov_labels or []) - - def any_person(exclude: set[str] | None = None) -> str: - exclude = exclude or set() - pool = [person for person in people if person not in exclude] or people - return rng.choice(pool) - - def any_woman(exclude: set[str] | None = None) -> str: - exclude = exclude or set() - pool = [person for person in women if person not in exclude] or [person for person in people if person not in exclude] or people - return rng.choice(pool) - - def any_man(exclude: set[str] | None = None) -> str: - exclude = exclude or set() - pool = [person for person in men if person not in exclude] or [person for person in people if person not in exclude] or people - return rng.choice(pool) - - def support_sentence(exclude: set[str]) -> str: - extras = [person for person in people if person not in exclude] - if not extras: - return "" - extra = rng.choice(extras) - actions = [ - "kisses and grips the nearest body", - "holds hips open for the camera", - "touches breasts, thighs, and stomach", - "keeps one hand on a partner's ass", - "watches close and joins the body contact", - "presses in from the side with hands on skin", - ] - 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." - - 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." - - 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() - 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." - - def outercourse_position_graph(woman: str, man: str) -> str: - position_text = str((item_axis_values or {}).get("position") or "").lower() - text = " ".join( - str(part or "").lower() - for part in ( - item_text, - *((item_axis_values or {}).values()), - ) - ) - man_is_pov = man in pov_set - if any(term in text for term in ("boobjob", "titjob", "breast-sex", "breast sex")): - if man_is_pov: - return ( - f"{woman} kneels between the POV viewer's open thighs with her torso bent forward over his pelvis and shoulders low, " - "both hands lifting and pressing her breasts tightly around the POV viewer's penis shaft while the glans sits just below her lips." - ) - return ( - f"{woman} kneels between {man}'s open thighs with her torso bent forward over his pelvis and shoulders low while {man} sits with legs apart, " - f"{woman}'s hands lifting and pressing her breasts tightly around {man}'s penis shaft while the glans sits just below her lips." - ) - if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")): - if man_is_pov: - return ( - f"{woman} kneels very low between the POV viewer's open thighs with her torso bent forward and shoulders between his knees, " - "head tucked under the penis shaft at the base of the penis, mouth and tongue on the POV viewer's balls while his penis points upward above her face." - ) - return ( - f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, " - f"head tucked under the penis shaft at the base of his penis, mouth and tongue on his balls while {man}'s penis points upward above her face." - ) - if "penis-licking" in position_text or "penis licking" in text or "tongue along" in text or "tongue licking" in text: - if man_is_pov: - return ( - f"{woman} bends forward between the POV viewer's open thighs, head low under the POV viewer's penis with her face directly under the penis, " - "tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis." - ) - return ( - f"{woman} bends forward between {man}'s open thighs, head low under {man}'s penis with her face directly under the penis, " - f"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis." - ) - if "handjob" in position_text or "handjob" in text or "hand job" in text or "hand wrapped" in text: - if man_is_pov: - return ( - f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the penis shaft, " - "one hand wrapped around the POV viewer's penis shaft while the other hand steadies the base of the penis as she strokes toward the glans." - ) - return ( - f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind the penis shaft, " - f"one hand wrapped around {man}'s penis shaft while the other hand steadies the base of the penis as she strokes toward the glans." - ) - if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text: - if man_is_pov: - return ( - f"{woman} faces the POV viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera, " - "both soles wrapped around the POV viewer's penis shaft in the lower foreground." - ) - return ( - f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, " - f"wrapping both soles around {man}'s penis shaft while the contact stays centered." - ) - if man_is_pov: - return ( - f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, " - "with her mouth, hands, breasts, or feet visibly working around the penis shaft." - ) - return ( - f"{woman} kneels close to {man}'s hips and keeps {man}'s penis centered in clear non-penetrative contact, " - "with her mouth, hands, breasts, or feet visibly working around the penis shaft." - ) - - def oral_position_graph(woman: str, man: str) -> str: - position_text = str((item_axis_values or {}).get("position") or "").lower() - text = " ".join( - str(part or "").lower() - for part in ( - item_text, - *((item_axis_values or {}).values()), - ) - ) - man_is_pov = man in pov_set - woman_gives = any( - term in text - for term in ( - "fellatio", - "blowjob", - "deepthroat", - "penis sucking", - "penis in mouth", - "penis in her mouth", - "mouth stretched around a penis", - "lips wrapped", - ) - ) - man_gives = any( - term in text - for term in ( - "cunnilingus", - "pussy licking", - "tongue on pussy", - "mouth on pussy", - "pussy and tongue", - "face-sitting", - "tongue contact clearly visible", - ) - ) - if "mouth on genitals" in text and not woman_gives and not man_gives: - if any(term in text for term in ("face-sitting", "reclining", "straddled", "spread-leg", "open thighs")): - man_gives = True - else: - woman_gives = True - - if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text): - return f"{woman} and {man} lie head-to-hips in a sixty-nine position, with {woman}'s mouth on {man}'s penis and {man}'s mouth on {woman}'s pussy." - if "face-sitting" in position_text or ("face-sitting" in text and not position_text): - if man_is_pov: - return ( - f"{woman} is above the POV camera, straddling the POV viewer's face with thighs on both sides of his head, " - "pussy directly over the POV viewer's mouth for close first-person underview tongue contact." - ) - return f"{man} lies on his back while {woman} straddles his face with her thighs around his head and {man}'s mouth pressed to her pussy." - if "straddled oral" in position_text or ("straddled oral" in text and not position_text): - if woman_gives and not man_gives: - return f"{man} straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis." - return f"{woman} straddles above {man}'s face with her thighs framing his head while {man}'s mouth stays pressed to her pussy." - if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text): - if woman_gives and not man_gives: - return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth." - return f"{woman} lies on her side with her top thigh lifted while {man} lies beside her hips with his mouth pressed to her pussy." - if ( - "edge-of-bed oral" in position_text - or "edge of bed oral" in position_text - or "edge-supported oral" in position_text - or (("edge-of-bed oral" in text or "edge of bed oral" in text or "edge-supported oral" in text) and not position_text) - ): - if woman_gives and not man_gives: - return f"{man} sits at a raised edge with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." - return f"{woman} lies at a raised edge with thighs open while {man} kneels between her legs with his mouth on her pussy." - if "standing oral" in position_text or ("standing oral" in text and not position_text): - if man_gives and not woman_gives: - return f"{woman} stands braced with one thigh lifted while {man} kneels between her legs with his mouth on her pussy." - return f"{man} stands with hips forward while {woman} kneels in front of him at hip height and takes his penis in her mouth." - if "chair oral" in position_text or ("chair oral" in text and not position_text): - if man_gives and not woman_gives: - return f"{woman} sits in a chair with thighs open while {man} kneels between her legs with his mouth pressed to her pussy." - return f"{man} sits in a chair with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." - if ( - "reclining cunnilingus" in position_text - or "spread-leg oral" in position_text - or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text) - ): - if woman_gives and not man_gives: - return f"{man} reclines with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." - return f"{woman} reclines on her back with thighs spread while {man} kneels between her legs with his mouth on her pussy." - if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text): - if man_gives and not woman_gives: - return f"{woman} kneels with thighs parted and hips angled forward while {man} kneels in front of her with his mouth on her pussy." - return ( - f"{woman} kneels in front of {man}'s penis while {man} stands over her; " - f"{woman} takes {man}'s penis in her mouth with saliva dripping on the penis as {man} looks down toward her." - ) - if man_gives and not woman_gives: - return f"{woman} lies on her back with thighs open while {man} kneels between her legs with his mouth pressed to her pussy." - return f"{woman} kneels in front of {man}'s hips and takes his penis in her mouth while {man} keeps his hips aligned with her face." - - if people_count == 1: - solo = people[0] - if women_count == 1: - if "manual_stimulation" in slug: - return manual_position_graph(solo) - 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: - return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets." - return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness." - if "cumshot" in slug or "climax" in slug: - return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible." - return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing." - - if women_count > 0 and men_count == 0: - a, b = _pick_distinct(rng, women, 2) - c = any_woman({a, b}) if len(women) >= 3 else "" - used = {a, b} - if "manual_stimulation" in slug: - graph = manual_position_graph(a, b) - elif "group_coordination" in slug and c: - graph = group_coordination_graph(a, b, c) - 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) - if c and "camera_performance" in slug: - used.add(c) - elif "foreplay" in slug: - graph = foreplay_position_graph(a, b) - 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: - graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy." - elif "anal" in slug or "double" in slug: - graph = f"{a} uses a strap-on on {b} while keeping her hips held open." - elif "threesome" in slug or "group" in slug or "orgy" in slug: - helper = c or any_woman({a}) - graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies." - used.add(helper) - elif "cumshot" in slug or "climax" in slug: - graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets." - else: - graph = f"{a} uses a strap-on on {b} while their bodies stay pressed together." - return graph + support_sentence(used) - - if men_count > 0 and women_count == 0: - a, b = _pick_distinct(rng, men, 2) - c = any_man({a, b}) if len(men) >= 3 else "" - used = {a, b} - 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) - 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." - elif "foreplay" in slug: - graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside." - elif "outercourse" in slug: - graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet." - elif "oral" in slug: - graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips." - elif "anal" in slug or "double" in slug or "penetrative" in slug: - graph = f"{a} penetrates {b} anally while {b}'s hips are held open." - elif "threesome" in slug or "group" in slug or "orgy" in slug: - helper = c or any_man({a}) - graph = f"{a} penetrates {b} anally while {helper} gives oral contact from the front." - used.add(helper) - elif "cumshot" in slug or "climax" in slug: - graph = f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis." - else: - graph = f"{a} and {b} keep explicit penis and anal contact visible." - return graph + support_sentence(used) - - # Mixed cast. - woman = any_woman() - man = any_man() - third = any_person({woman, man}) if people_count >= 3 else "" - if "manual_stimulation" in slug: - graph = manual_position_graph(woman, man) - elif "group_coordination" in slug: - graph = group_coordination_graph(woman, man, third) - 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) - elif "foreplay" in slug: - graph = foreplay_position_graph(woman, man) - elif "outercourse" in slug: - graph = outercourse_position_graph(woman, man) - elif "oral" in slug: - graph = oral_position_graph(woman, man) - 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) - 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: - 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) - else: - graph = penetration_position_graph(woman, man) - return graph + support_sentence({woman, man, third} if third else {woman, man}) - - def _subject_context( rng: random.Random, subject_type: str, @@ -6994,7 +6424,7 @@ def _build_custom_row( if subject_type == "configured_cast" else [] ) - source_role_graph = _role_graph(role_rng, subcategory, context, item_axis_values, pov_character_labels) + source_role_graph = build_hardcore_role_graph(role_rng, subcategory, context, item_axis_values, pov_character_labels) if is_pose_category: source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph) role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)