from __future__ import annotations import re from typing import Any try: from . import item_axis_policy except ImportError: # Allows local smoke tests with top-level imports. import item_axis_policy 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: return item_axis_policy.action_context_text(axis_values) 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)) return any( token in text for token in ( "double penetration", "double-penetration", "front-and-back double", "vaginal and anal penetration", "pussy and ass filled", "one penis in pussy and one penis in ass", "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", ) )