302 lines
8.4 KiB
Python
302 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
|
|
HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"}
|
|
|
|
|
|
def _clean(value: Any) -> str:
|
|
text = "" if value is None else str(value)
|
|
text = text.replace("\n", " ")
|
|
text = re.sub(r"\s+", " ", text).strip()
|
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
|
return text
|
|
|
|
|
|
def normalize_hardcore_detail_density(value: Any) -> str:
|
|
text = _clean(value).lower()
|
|
return text if text in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
|
|
|
|
|
|
def axis_values_text(axis_values: Any) -> str:
|
|
if not isinstance(axis_values, dict):
|
|
return ""
|
|
priority = (
|
|
"position",
|
|
"body_position",
|
|
"body_arrangement",
|
|
"arrangement",
|
|
"angle",
|
|
"surface",
|
|
"body_contact",
|
|
"leg_detail",
|
|
"oral_act",
|
|
"oral_detail",
|
|
"penetration_act",
|
|
"penetration_detail",
|
|
"anal_act",
|
|
"double_act",
|
|
"threesome_act",
|
|
"group_act",
|
|
)
|
|
parts = [_clean(axis_values.get(key)) for key in priority if _clean(axis_values.get(key))]
|
|
return " ".join(parts)
|
|
|
|
|
|
def position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
|
return " ".join(
|
|
_clean(part).lower()
|
|
for part in (role_graph, hard_item, composition, axis_values_text(axis_values))
|
|
if _clean(part)
|
|
)
|
|
|
|
|
|
def is_outercourse_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
return any(
|
|
term in text
|
|
for term in (
|
|
"outercourse",
|
|
"non-penetrative",
|
|
"boobjob",
|
|
"titjob",
|
|
"breast sex",
|
|
"breast-sex",
|
|
"testicle",
|
|
"balls",
|
|
"balls licking",
|
|
"balls-licking",
|
|
"breasts tightly around",
|
|
"breasts around",
|
|
"penis licking",
|
|
"penis-licking",
|
|
"tongue along",
|
|
"tongue runs along",
|
|
"tongue running along",
|
|
"handjob",
|
|
"hand job",
|
|
"hand wrapped",
|
|
"hand stroking",
|
|
"hand wraps around",
|
|
"manual stimulation",
|
|
"fingering",
|
|
"fingers inside",
|
|
"fingers in pussy",
|
|
"hand on pussy",
|
|
"fingers on pussy",
|
|
"fingers sliding against the pussy",
|
|
"open-thigh manual",
|
|
"clit rubbing",
|
|
"clit",
|
|
"clitoris",
|
|
"mutual masturbation",
|
|
"footjob",
|
|
"soles wrap around",
|
|
"soles",
|
|
"toes curled",
|
|
"feet stroking",
|
|
)
|
|
)
|
|
|
|
|
|
def is_oral_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
return any(
|
|
term in text
|
|
for term in (
|
|
"oral",
|
|
"fellatio",
|
|
"blowjob",
|
|
"deepthroat",
|
|
"penis sucking",
|
|
"penis in her mouth",
|
|
"penis in mouth",
|
|
"takes the man's penis",
|
|
"takes his penis",
|
|
"mouth at penis level",
|
|
"mouth on his penis",
|
|
"lips wrapped",
|
|
"cunnilingus",
|
|
"pussy licking",
|
|
"mouth on her pussy",
|
|
"mouth pressed to her pussy",
|
|
"face-sitting",
|
|
"sixty-nine",
|
|
)
|
|
)
|
|
|
|
|
|
def is_foreplay_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
if not text:
|
|
return False
|
|
return any(
|
|
term in text
|
|
for term in (
|
|
"foreplay",
|
|
"pre-sex",
|
|
"before sex",
|
|
"before penetration",
|
|
"kissing",
|
|
"deep kiss",
|
|
"mouth-to-mouth",
|
|
"lips pressed",
|
|
"caressing",
|
|
"hands roaming",
|
|
"stroking skin",
|
|
"touching breasts",
|
|
"cupping a breast",
|
|
"hand on the cheek",
|
|
"cheek and jaw",
|
|
"fingers under the chin",
|
|
"undressing",
|
|
"removing clothing",
|
|
"removing clothes",
|
|
"pulling clothing",
|
|
"sliding straps",
|
|
"unbuttoning",
|
|
"body worship",
|
|
"nipple",
|
|
"mouth on skin",
|
|
"kissing down",
|
|
"ass grabbing",
|
|
"gripping the ass",
|
|
"thigh kissing",
|
|
"inner thighs",
|
|
"hair held",
|
|
"holding hair",
|
|
"hair pulled back",
|
|
"wrist",
|
|
"wrists",
|
|
"pinning",
|
|
"guided",
|
|
"guiding",
|
|
"turning the body",
|
|
"position transition",
|
|
"pulling onto the bed",
|
|
"lifting and spreading",
|
|
"spreading thighs",
|
|
"dirty talk",
|
|
"whispering",
|
|
"camera performance",
|
|
"presented directly to the camera",
|
|
"present her body",
|
|
"showing to camera",
|
|
"spread open for the camera",
|
|
"watching partner",
|
|
"waiting turn",
|
|
"group coordination",
|
|
"aftercare",
|
|
"cleanup",
|
|
"wiping",
|
|
"towel",
|
|
"post-sex",
|
|
"fingering",
|
|
"fingers inside",
|
|
"hand on pussy",
|
|
"fingers on pussy",
|
|
"clit rubbing",
|
|
"clit",
|
|
"clitoris",
|
|
"manual stimulation",
|
|
"mutual masturbation",
|
|
)
|
|
)
|
|
|
|
|
|
def is_close_foreplay_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
if not text or not is_foreplay_text(text):
|
|
return False
|
|
return any(
|
|
term in text
|
|
for term in (
|
|
"stand close",
|
|
"stand face-to-face",
|
|
"press their bodies",
|
|
"bodies pressed close",
|
|
"hips pressed close",
|
|
"mouth-to-mouth",
|
|
"deep kissing",
|
|
"heated kiss",
|
|
"hands pull clothing",
|
|
"pull clothing aside",
|
|
"clothing being removed",
|
|
)
|
|
)
|
|
|
|
|
|
def is_vaginal_penetration_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
if not text or is_outercourse_text(text) or is_oral_text(text) or is_foreplay_text(text):
|
|
return False
|
|
if any(term in text for term in ("anal", "double penetration", "double-penetration", "toy-assisted", "strap-on")):
|
|
return False
|
|
return any(
|
|
term in text
|
|
for term in (
|
|
"vaginal penetration",
|
|
"deep vaginal sex",
|
|
"explicit penetrative sex",
|
|
"penetrative sex",
|
|
"penis entering pussy",
|
|
"penis thrusts into her pussy",
|
|
"penis thrusts into the woman",
|
|
"pussy stretched around a penis",
|
|
"hardcore vaginal thrusting",
|
|
"full-body penetrative sex",
|
|
"close-contact vaginal sex",
|
|
"missionary position",
|
|
"cowgirl position",
|
|
"reverse cowgirl position",
|
|
"doggy style position",
|
|
"standing sex position",
|
|
"spooning sex position",
|
|
"edge-of-bed position",
|
|
"kneeling straddle position",
|
|
"lotus sex position",
|
|
"bent-over position",
|
|
)
|
|
)
|
|
|
|
|
|
def is_toy_assisted_double_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
if "toy" not in text:
|
|
return False
|
|
return any(
|
|
token in text
|
|
for token in (
|
|
"double penetration",
|
|
"double-penetration",
|
|
"vaginal and anal penetration",
|
|
"second penetration point",
|
|
"second point of contact",
|
|
"second contact",
|
|
)
|
|
)
|
|
|
|
|
|
def is_climax_text(*parts: str) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
return any(
|
|
token in text
|
|
for token in (
|
|
"cumshot",
|
|
"ejaculation",
|
|
"post-orgasm",
|
|
"post-climax",
|
|
"orgasm aftermath",
|
|
"orgasm scene",
|
|
"orgasm during",
|
|
"shared climax",
|
|
"hardcore climax",
|
|
"external cumshot",
|
|
"visible external ejaculation",
|
|
"climaxes on",
|
|
"climax lands",
|
|
)
|
|
)
|