from __future__ import annotations import re from typing import Any OUTERCOURSE_BOOBJOB = "boobjob" OUTERCOURSE_TESTICLE = "testicle_sucking" OUTERCOURSE_PENIS_LICKING = "penis_licking" OUTERCOURSE_HANDJOB = "handjob" OUTERCOURSE_FOOTJOB = "footjob" OUTERCOURSE_GENERIC = "generic" OUTERCOURSE_ACTION_KIND_CHOICES = { OUTERCOURSE_BOOBJOB, OUTERCOURSE_TESTICLE, OUTERCOURSE_PENIS_LICKING, OUTERCOURSE_HANDJOB, OUTERCOURSE_FOOTJOB, OUTERCOURSE_GENERIC, } 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_outercourse_action_kind(value: Any, default: str = OUTERCOURSE_GENERIC) -> str: text = re.sub(r"[^a-z0-9]+", "_", _clean(value).lower()).strip("_") aliases = { "breast_sex": OUTERCOURSE_BOOBJOB, "titjob": OUTERCOURSE_BOOBJOB, "tit_job": OUTERCOURSE_BOOBJOB, "testicle": OUTERCOURSE_TESTICLE, "testicles": OUTERCOURSE_TESTICLE, "ball_licking": OUTERCOURSE_TESTICLE, "balls_licking": OUTERCOURSE_TESTICLE, "balls": OUTERCOURSE_TESTICLE, "penis_lick": OUTERCOURSE_PENIS_LICKING, "penis_tongue": OUTERCOURSE_PENIS_LICKING, "hand_job": OUTERCOURSE_HANDJOB, "two_handed_handjob": OUTERCOURSE_HANDJOB, "foot_job": OUTERCOURSE_FOOTJOB, "feet_job": OUTERCOURSE_FOOTJOB, } text = aliases.get(text, text) return text if text in OUTERCOURSE_ACTION_KIND_CHOICES else default def infer_outercourse_action_kind(*parts: Any) -> str: text = " ".join(_clean(part).lower() for part in parts if _clean(part)) if not text: return OUTERCOURSE_GENERIC if any( term in text for term in ( "boobjob", "titjob", "tit job", "breast sex", "breast-sex", "breasts tightly around", "breasts around", "breasts firmly together", "penis squeezed between both breasts", "penis shaft compressed between breasts", "soft flesh squeezed around the penis", ) ): return OUTERCOURSE_BOOBJOB if any( term in text for term in ( "testicle", "balls licking", "balls-licking", "balls held", "balls close", "balls and mouth", "mouth and tongue on the viewer's balls", "mouth and tongue on the pov viewer's balls", "mouth and tongue licking the viewer's balls", "mouth and tongue licking the pov viewer's balls", ) ): return OUTERCOURSE_TESTICLE if any( term in text for term in ( "penis licking", "penis-licking", "tongue along", "tongue runs along", "tongue running along", "tongue licking", "underside of the penis", ) ): return OUTERCOURSE_PENIS_LICKING if any( term in text for term in ( "handjob", "hand job", "hand wrapped", "hand wraps around", "hand stroking", "both hands stroking", "two-handed", "one hand grips", "one hand wrapped around", ) ): return OUTERCOURSE_HANDJOB if any( term in text for term in ( "footjob", "foot job", "soles", "toes curled", "feet stroking", "feet and penis", "both feet", ) ): return OUTERCOURSE_FOOTJOB return OUTERCOURSE_GENERIC def outercourse_context_text(*parts: Any) -> str: return " ".join(_clean(part).lower() for part in parts if _clean(part))