From 22df4f754feed032d47e89d9afc5301a8f6c0f04 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 12:02:26 +0200 Subject: [PATCH] Add hardcore foreplay action pool --- __init__.py | 4 + categories/expression_composition_pools.json | 28 +++++ categories/sexual_poses.json | 119 +++++++++++++++++++ krea_formatter.py | 37 +++++- prompt_builder.py | 108 ++++++++++++++++- 5 files changed, 290 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index 17f3920..535d339 100644 --- a/__init__.py +++ b/__init__.py @@ -265,6 +265,7 @@ NODE_INPUT_TOOLTIPS = { "allow_toys": "Allow toy/strap-on wording in hardcore actions.", "allow_double": "Allow double-penetration or second-contact wording.", "allow_penetration": "Allow vaginal/penetrative sex subcategories.", + "allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.", "allow_oral": "Allow oral sex subcategories.", "allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.", "allow_anal": "Allow anal subcategories.", @@ -1939,6 +1940,7 @@ class SxCPHardcoreActionFilter: "allow_toys": ("BOOLEAN", {"default": False}), "allow_double": ("BOOLEAN", {"default": False}), "allow_penetration": ("BOOLEAN", {"default": True}), + "allow_foreplay": ("BOOLEAN", {"default": True}), "allow_oral": ("BOOLEAN", {"default": True}), "allow_outercourse": ("BOOLEAN", {"default": True}), "allow_anal": ("BOOLEAN", {"default": True}), @@ -1960,6 +1962,7 @@ class SxCPHardcoreActionFilter: allow_toys, allow_double, allow_penetration, + allow_foreplay, allow_oral, allow_outercourse, allow_anal, @@ -1972,6 +1975,7 @@ class SxCPHardcoreActionFilter: allow_toys=allow_toys, allow_double=allow_double, allow_penetration=allow_penetration, + allow_foreplay=allow_foreplay, allow_oral=allow_oral, allow_outercourse=allow_outercourse, allow_anal=allow_anal, diff --git a/categories/expression_composition_pools.json b/categories/expression_composition_pools.json index 9390aed..e6c7459 100644 --- a/categories/expression_composition_pools.json +++ b/categories/expression_composition_pools.json @@ -195,6 +195,22 @@ "intense shameless eye contact", "spent satisfied expression" ], + "hardcore_foreplay_expressions": [ + "heavy-lidded teasing stare", + "breathless close-range expression", + "soft moan while being touched", + "heated kissing expression", + "hungry eye contact during undressing", + "parted lips before a kiss", + "flushed anticipation", + "controlled teasing smile", + "dazed aroused stare", + "eyes half-closed during caressing", + "mouth open against a kiss", + "shameless pre-sex grin", + "focused stare while clothing is pulled aside", + "close-range lustful eye contact" + ], "hardcore_penetration_expressions": [ "controlled eye contact during penetration", "focused adult pleasure face during thrusting", @@ -455,6 +471,18 @@ "dramatic overhead studio crop", "tight vertical frame with props and body centered" ], + "foreplay_compositions": [ + {"text": "close body-contact frame centered on kissing, hands, and exposed skin", "min_people": 2, "max_people": 3}, + {"text": "standing close-range foreplay frame with faces and hand placement readable", "min_people": 2, "max_people": 3}, + {"text": "bed-edge undressing composition with clothing movement visible", "min_people": 2, "max_people": 3}, + {"text": "mirror-view undressing frame with faces, hands, and exposed skin visible", "min_people": 2, "max_people": 3}, + {"text": "tight crop on hands touching breasts, face, waist, and clothing", "min_people": 2, "max_people": 3}, + {"text": "couch foreplay composition with bodies pressed close", "min_people": 2, "max_people": 3}, + {"text": "wall-pressed kissing composition with hands on face and hips", "min_people": 2, "max_people": 3}, + {"text": "seated lap teasing composition with clothing being pulled aside", "min_people": 2, "max_people": 3}, + {"text": "close candid creator-shot frame centered on undressing and body contact", "min_people": 2, "max_people": 3}, + {"text": "full-body pre-sex foreplay frame with faces, hands, and clothes readable", "min_people": 2, "max_people": 3} + ], "hardcore_explicit_compositions": [ {"text": "full-body explicit sex frame with all adult bodies visible", "min_people": 2}, {"text": "bed-level camera angle focused on genital contact and faces", "min_people": 2, "max_people": 3}, diff --git a/categories/sexual_poses.json b/categories/sexual_poses.json index f290062..47150bf 100644 --- a/categories/sexual_poses.json +++ b/categories/sexual_poses.json @@ -84,6 +84,125 @@ } ], "subcategories": [ + { + "name": "Foreplay and teasing", + "slug": "foreplay_teasing", + "min_people": 2, + "min_women": 1, + "inherit_expressions": false, + "inherit_compositions": false, + "weight": 0.75, + "item_label": "Foreplay action", + "positive_suffix": "Use clear adult body contact, readable hands and faces, visible undressing, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.", + "prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Foreplay action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body contact, kissing, caressing, undressing, visible arousal, exposed skin, and readable hand placement. {positive_suffix} Avoid: {negative_prompt}.", + "caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, foreplay teasing action: {item}, {scene}, {composition}, explicit consensual adult erotic foreplay illustration", + "scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"], + "expression_pools": ["hardcore_foreplay_expressions"], + "composition_pools": ["foreplay_compositions"], + "item_templates": [ + "{tease_act} in {position}, with {touch_detail}, {clothing_detail}, and {mood_detail}", + "{position} featuring {tease_act}, {body_contact}, {touch_detail}, and {visibility}", + "hardcore foreplay setup: {tease_act}, {clothing_detail}, {face_detail}, and {body_contact}", + "{tease_act} on {surface}, with {touch_detail}, {mood_detail}, and {visibility}", + "{position} while {tease_act}, with {face_detail}, {clothing_detail}, and {touch_detail}" + ], + "item_axes": { + "position": [ + "standing kissing position", + "bed-edge undressing position", + "seated lap teasing position", + "kneeling clothing-removal position", + "side-lying caressing position", + "mirror undressing position", + "couch foreplay position", + "wall-pressed kissing position" + ], + "tease_act": [ + "deep kissing before sex", + "mouth-to-mouth kissing with bodies pressed close", + "slow caressing over bare skin", + "hands roaming over the body before sex", + "touching and squeezing breasts during foreplay", + "one hand cupping a breast while kissing", + "hand on the cheek and jaw during a heated kiss", + "fingers under the chin guiding the face closer", + "pulling clothing aside before sex", + "slowly removing clothing with visible skin revealed", + "sliding straps off shoulders and exposing the chest", + "unbuttoning clothing while bodies press together" + ], + "touch_detail": [ + "hands cupping breasts", + "thumbs brushing nipples through parted clothing", + "fingers tracing the waist and hips", + "one hand gripping the ass", + "one hand on the cheek and jaw", + "fingers under the chin", + "hands sliding under fabric", + "hands pulling bodies closer", + "fingers tangled in hair", + "hands on the lower back" + ], + "clothing_detail": [ + "clothing partly removed with fabric bunched at the waist", + "shirt pulled open and straps sliding off shoulders", + "bra or top pushed aside with chest exposed", + "lower garments loosened while bodies stay pressed close", + "clothing being pulled off one shoulder", + "open shirt exposing bare chest and stomach", + "fabric lifted to reveal skin while kissing", + "discarded clothing visible nearby" + ], + "face_detail": [ + "foreheads close and lips nearly touching", + "mouths pressed together in a deep kiss", + "cheek held in one hand", + "chin lifted by fingertips", + "heavy eye contact at close range", + "breathless parted lips", + "face turned toward the partner's hand", + "hair pulled back gently from the face" + ], + "body_contact": [ + "chests pressed together", + "hips pressed close", + "bodies leaning into each other", + "one body pinned gently against the wall", + "torso arched into the touch", + "thighs pressed together", + "standing close with bodies aligned", + "seated close with legs interlaced" + ], + "mood_detail": [ + "slow teasing buildup", + "heated pre-sex tension", + "intimate close-contact anticipation", + "urgent hands and heavy breathing", + "consensual teasing before the explicit act", + "breathless pause before sex", + "visible arousal before sex", + "adult foreplay tension" + ], + "visibility": [ + "hands, faces, and exposed skin clearly readable", + "clothing movement and body contact visible", + "breasts, hands, and faces centered", + "face-touching and undressing action readable", + "body contact visible before sex", + "teasing hand placement clearly visible" + ], + "surface": [ + "rumpled bed sheets", + "a wide couch", + "against a wall", + "beside a mirror", + "a hotel bed", + "floor cushions", + "a dressing-room bench", + "a low mattress" + ] + } + }, { "name": "Penetrative sex", "slug": "penetrative_sex", diff --git a/krea_formatter.py b/krea_formatter.py index 61ad856..e75697c 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -875,9 +875,42 @@ def _is_oral_text(*parts: Any) -> bool: ) +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", + ) + ) + + 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): + 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 @@ -952,6 +985,8 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "" position_text = _clean(axis_values.get("position", "")).lower() if not text: return "" + if _is_foreplay_text(role_graph, hard_item, composition, _axis_values_text(axis_values)): + return "" if _is_outercourse_text(role_graph, hard_item, composition, _axis_values_text(axis_values)): if any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex")): return "breast-sex outercourse pose" diff --git a/prompt_builder.py b/prompt_builder.py index 546461b..63ee53c 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -299,6 +299,7 @@ HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"] HARDCORE_POSITION_FAMILY_CHOICES = [ "any", "penetrative", + "foreplay", "oral", "outercourse", "anal", @@ -309,6 +310,7 @@ HARDCORE_POSITION_FAMILY_CHOICES = [ HARDCORE_POSITION_FOCUS_CHOICES = [ "keep_pool", "penetration_only", + "foreplay_only", "oral_only", "outercourse_only", "anal_only", @@ -334,6 +336,11 @@ HARDCORE_POSITION_KEY_CHOICES = [ "straddled_oral", "spread_leg_oral", "chair_oral", + "kissing", + "caressing", + "breast_touch", + "face_touch", + "undressing", "boobjob", "testicle_sucking", "penis_licking", @@ -345,6 +352,7 @@ HARDCORE_POSITION_KEY_CHOICES = [ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = { "any": [ "penetrative_sex", + "foreplay_teasing", "oral_sex", "outercourse_sex", "anal_double_penetration", @@ -353,6 +361,7 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = { "cumshot_climax", ], "penetrative": ["penetrative_sex"], + "foreplay": ["foreplay_teasing"], "oral": ["oral_sex"], "outercourse": ["outercourse_sex"], "anal": ["anal_double_penetration"], @@ -378,6 +387,11 @@ HARDCORE_POSITION_KEY_MATCHES = { "straddled_oral": ("straddled oral",), "spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"), "chair_oral": ("chair oral",), + "kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"), + "caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"), + "breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"), + "face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"), + "undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"), "boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"), "testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"), "penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"), @@ -2332,6 +2346,7 @@ def _empty_hardcore_position_config() -> dict[str, Any]: "allow_toys": True, "allow_double": True, "allow_penetration": True, + "allow_foreplay": True, "allow_oral": True, "allow_outercourse": True, "allow_anal": True, @@ -2356,7 +2371,16 @@ def _parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[ parsed["family"] = _normalize_hardcore_position_family(parsed.get("family")) parsed["positions"] = _normalize_hardcore_position_values(parsed.get("positions")) parsed["require_position"] = not _is_false(parsed.get("require_position", False)) - for key in ("allow_toys", "allow_double", "allow_penetration", "allow_oral", "allow_outercourse", "allow_anal", "allow_climax"): + for key in ( + "allow_toys", + "allow_double", + "allow_penetration", + "allow_foreplay", + "allow_oral", + "allow_outercourse", + "allow_anal", + "allow_climax", + ): parsed[key] = not _is_false(parsed.get(key, True)) return parsed @@ -2376,6 +2400,7 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str: ("allow_toys", "toys"), ("allow_double", "double"), ("allow_penetration", "penetration"), + ("allow_foreplay", "foreplay"), ("allow_oral", "oral"), ("allow_outercourse", "outercourse"), ("allow_anal", "anal"), @@ -2420,6 +2445,7 @@ def build_hardcore_action_filter_json( allow_toys: bool = False, allow_double: bool = False, allow_penetration: bool = True, + allow_foreplay: bool = True, allow_oral: bool = True, allow_outercourse: bool = True, allow_anal: bool = True, @@ -2430,6 +2456,7 @@ def build_hardcore_action_filter_json( focus = str(focus or "keep_pool").strip() focus_family = { "penetration_only": "penetrative", + "foreplay_only": "foreplay", "oral_only": "oral", "outercourse_only": "outercourse", "anal_only": "anal", @@ -2442,6 +2469,7 @@ def build_hardcore_action_filter_json( config["allow_toys"] = bool(allow_toys) config["allow_double"] = bool(allow_double) config["allow_penetration"] = bool(allow_penetration) + config["allow_foreplay"] = bool(allow_foreplay) config["allow_oral"] = bool(allow_oral) config["allow_outercourse"] = bool(allow_outercourse) config["allow_anal"] = bool(allow_anal) @@ -2452,6 +2480,7 @@ def build_hardcore_action_filter_json( family for enabled, family in ( (config["allow_penetration"], "penetrative"), + (config["allow_foreplay"], "foreplay"), (config["allow_oral"], "oral"), (config["allow_outercourse"], "outercourse"), (config["allow_anal"], "anal"), @@ -2462,7 +2491,9 @@ def build_hardcore_action_filter_json( if config["family"] in enabled_action_families and len(enabled_action_families) > 1: config["family"] = "any" - if focus == "oral_only": + if focus == "foreplay_only": + config["allow_foreplay"] = True + elif focus == "oral_only": config["allow_oral"] = True config["allow_penetration"] = False elif focus == "outercourse_only": @@ -2497,6 +2528,8 @@ def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]: allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])) if not config.get("allow_penetration", True): allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"}) + if not config.get("allow_foreplay", True): + allowed.discard("foreplay_teasing") if not config.get("allow_oral", True): allowed.discard("oral_sex") if not config.get("allow_outercourse", True): @@ -2567,6 +2600,32 @@ def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex")) ): return True + if not config.get("allow_foreplay", True) and ( + axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail") + or any( + term in text + for term in ( + "kiss", + "kissing", + "mouth-to-mouth", + "caress", + "caressing", + "stroking skin", + "hands roaming", + "touching breasts", + "cupping breasts", + "hand on the cheek", + "fingers under the chin", + "undressing", + "removing clothing", + "removing clothes", + "pulling clothing", + "sliding straps", + "unbuttoning", + ) + ) + ): + return True if not config.get("allow_climax", True) and ( axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location") or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration")) @@ -5975,6 +6034,39 @@ def _role_graph( ] 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 mentions_ass(text: str) -> bool: return bool( re.search( @@ -6265,7 +6357,9 @@ def _role_graph( a, b = _pick_distinct(rng, women, 2) c = any_woman({a, b}) if len(women) >= 3 else "" used = {a, b} - if "outercourse" in slug: + if "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." @@ -6285,7 +6379,9 @@ def _role_graph( a, b = _pick_distinct(rng, men, 2) c = any_man({a, b}) if len(men) >= 3 else "" used = {a, b} - if "outercourse" in slug: + if "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." @@ -6305,7 +6401,9 @@ def _role_graph( woman = any_woman() man = any_man() third = any_person({woman, man}) if people_count >= 3 else "" - if "outercourse" in slug: + if "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)