295 lines
17 KiB
Python
295 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import random
|
|
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_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_oral import build_oral_role_graph
|
|
from hardcore_role_outercourse import build_outercourse_role_graph
|
|
from hardcore_role_penetration import build_penetration_role_graph
|
|
|
|
|
|
def _lettered(prefix: str, count: int) -> list[str]:
|
|
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()
|
|
|
|
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."
|
|
|
|
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 = build_outercourse_role_graph(woman, man, item_text, item_axis_values, pov_labels)
|
|
elif "oral" in slug:
|
|
graph = build_oral_role_graph(woman, man, item_text, item_axis_values, pov_labels)
|
|
elif "anal" in slug or "double" in slug:
|
|
graph = build_anal_or_double_role_graph(woman, man, third, people_count, item_text, item_axis_values)
|
|
elif "threesome" in slug:
|
|
graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body."
|
|
elif "group" in slug or "orgy" in slug:
|
|
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 = build_climax_role_graph(woman, man, third, item_text, item_axis_values)
|
|
else:
|
|
graph = build_penetration_role_graph(woman, man, item_text, item_axis_values)
|
|
return graph + support_sentence({woman, man, third} if third else {woman, man})
|