From 2b9e880b1143945af1f2dc22e90fcd1282b8c2d0 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 17:46:15 +0200 Subject: [PATCH] Specialize Krea POV oral positioning --- krea_pov_actions.py | 208 ++++++++++++++++++++++++++++++++++++++---- tools/prompt_smoke.py | 18 ++-- 2 files changed, 202 insertions(+), 24 deletions(-) diff --git a/krea_pov_actions.py b/krea_pov_actions.py index 87f7ca6..87f0121 100644 --- a/krea_pov_actions.py +++ b/krea_pov_actions.py @@ -8,6 +8,7 @@ try: from .krea_action_context import ( axis_values_text, is_climax_text, + is_oral_text, is_outercourse_text, is_toy_assisted_double_text, position_context_text, @@ -18,6 +19,7 @@ except ImportError: # Allows local smoke tests with `python -c`. from krea_action_context import ( axis_values_text, is_climax_text, + is_oral_text, is_outercourse_text, is_toy_assisted_double_text, position_context_text, @@ -76,12 +78,21 @@ def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str: detail = re.sub(r"\bthe POV viewer\b", "the viewer", detail, flags=re.IGNORECASE) detail = re.sub(r"\bthe man's\b", "the viewer's", detail, flags=re.IGNORECASE) detail = re.sub(r"\bthe man\b", "the viewer", detail, flags=re.IGNORECASE) + detail = re.sub(r"\bhe\b", "the viewer", detail, flags=re.IGNORECASE) + detail = re.sub(r"\bhis\b", "the viewer's", detail, flags=re.IGNORECASE) + detail = re.sub(r"\bhim\b", "the viewer", detail, flags=re.IGNORECASE) detail = re.sub( r"^(?:missionary|cowgirl|reverse cowgirl|doggy style|standing sex|spooning sex|edge-supported|edge-of-bed|raised edge|kneeling straddle|lotus sex|bent-over|face-down ass-up|side-lying|kneeling rear-entry)\s+(?:position|pose)\s+(?:featuring|with|while|,)?\s*", "", detail, flags=re.IGNORECASE, ) + detail = re.sub( + r"^(?:kneeling oral|standing oral|chair oral|side-lying oral|sixty-nine|edge-supported oral|edge-of-bed oral|reclining cunnilingus|straddled oral|spread-leg oral)\s+(?:position|pose),?\s*", + "", + detail, + flags=re.IGNORECASE, + ) detail = re.sub(r"^(?:featuring|with)\s+", "", detail, flags=re.IGNORECASE) detail = re.sub( r"^(?:full-body|explicit|close-contact|deep|hardcore|vaginal|anal)?\s*(?:penetrative sex|vaginal sex|anal sex|penetration with visible genital contact|hardcore vaginal thrusting|hardcore anal thrusting),?\s*", @@ -127,6 +138,26 @@ def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str: return limit_detail_for_density(detail, detail_density, is_climax_text(context, detail)) +def pov_clean_oral_detail(detail: Any, context: str, detail_density: str) -> str: + detail = pov_clean_detail(detail, context, detail_density) + if not detail: + return "" + duplicate_patterns = ( + r"\bthe woman takes the viewer's penis in her mouth with\s+", + r"\bthe woman takes the viewer's penis in her mouth\b,?\s*", + r"\bher mouth on the viewer's penis\b,?\s*", + r"\bthe viewer's mouth on the woman's pussy\b,?\s*", + r"\bmouth on the viewer's penis\b,?\s*", + r"\bmouth on the woman's pussy\b,?\s*", + ) + for pattern in duplicate_patterns: + detail = re.sub(pattern, "", detail, flags=re.IGNORECASE) + detail = re.sub(r"\bwith\s+(?=[,;.]|$)", "", detail, flags=re.IGNORECASE) + detail = re.sub(r"\s*,\s*", ", ", detail) + detail = re.sub(r",\s*,", ",", detail) + return _clean(detail).strip(" ,;") + + def pov_hardcore_pose_sentence( action: Any, role_graph: Any, @@ -154,6 +185,66 @@ def pov_hardcore_pose_sentence( def outercourse_sentence(base: str) -> str: return _clean(base).rstrip(".") + def oral_sentence(base: str) -> str: + details = "" + if ";" in action_text: + details = pov_clean_oral_detail(action_text.split(";", 1)[1], f"{context} {base}", detail_density) + return _clean(f"{base}; {details}" if details else base).rstrip(".") + + def oral_direction() -> tuple[bool, bool]: + oral_context = f"{context} {action_lower}" + woman_gives = any( + token in oral_context + for token in ( + "fellatio", + "blowjob", + "deepthroat", + "penis sucking", + "penis in her mouth", + "mouth on the viewer's penis", + "mouth on viewer's penis", + "takes the viewer's penis", + "takes the man's penis", + "mouth at penis level", + "lips wrapped", + ) + ) + man_gives = any( + token in oral_context + for token in ( + "cunnilingus", + "pussy licking", + "mouth on the woman's pussy", + "mouth on her pussy", + "mouth pressed to her pussy", + "tongue on pussy", + "face-sitting", + "straddles his face", + "straddling the viewer's face", + ) + ) + if "sixty-nine" in oral_context: + return True, True + return woman_gives, man_gives + + penetrative_tokens = ( + "penetrat", + "thrust", + "anal", + "cowgirl", + "missionary", + "doggy", + "rear-entry", + "spooning", + "bent-over", + "face-down", + "ejaculat", + "semen", + "cumshot", + "climax", + ) + has_penetrative_context = any(token in context or token in action_lower for token in penetrative_tokens) + if ( "face-sitting" in context or "face sitting" in context @@ -199,24 +290,105 @@ def pov_hardcore_pose_sentence( return outercourse_sentence( "The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact" ) - penetrative_tokens = ( - "penetrat", - "thrust", - "anal", - "cowgirl", - "missionary", - "doggy", - "rear-entry", - "spooning", - "side-lying", - "bent-over", - "face-down", - "ejaculat", - "semen", - "cumshot", - "climax", - ) - if not any(token in context or token in action_lower for token in penetrative_tokens): + + if is_oral_text(context, action_lower) and not has_penetrative_context: + woman_gives, man_gives = oral_direction() + if "sixty-nine" in position_context: + return oral_sentence( + "POV sixty-nine oral position: the woman lies head-to-hips over the viewer, her pelvis close to his face and her head lowered toward his hips; " + "her mouth on the viewer's penis and the viewer's mouth on the woman's pussy, with her torso, hips, mouth, and the viewer's lower-foreground body cues aligned in one first-person frame" + ) + if "side-lying oral" in position_context or "side lying oral" in position_context: + if woman_gives and not man_gives: + return oral_sentence( + "POV side-lying oral position: the viewer lies on his side with hips angled toward the woman while she lies beside his thighs; " + "her head stays at penis height with her mouth on the viewer's penis, shoulders and hands close to his pelvis in the lower foreground" + ) + return oral_sentence( + "POV side-lying cunnilingus position: the woman lies on her side with her top thigh lifted while the viewer lies beside her hips; " + "his face is at pussy height, with her thigh, hip, and torso forming a clear side-profile first-person frame" + ) + if ( + "edge-supported oral" in position_context + or "edge-of-bed oral" in position_context + or "edge of bed oral" in position_context + or "raised edge" in position_context + ): + if woman_gives and not man_gives: + return oral_sentence( + "POV raised-edge oral position: the viewer sits at the raised edge with legs apart while the woman kneels directly between his thighs; " + "her head is at penis height, mouth on the viewer's penis, with his thighs framing her shoulders in the lower foreground" + ) + return oral_sentence( + "POV raised-edge cunnilingus position: the woman reclines at the raised edge with thighs open toward the viewer; " + "the viewer kneels between her legs with his face at pussy height, her hips and open thighs framing the first-person view" + ) + if "chair oral" in position_context: + if woman_gives and not man_gives: + return oral_sentence( + "POV chair oral position: the viewer sits in a chair with legs apart while the woman kneels between the viewer's thighs; " + "her head is low at his pelvis, mouth on the viewer's penis, with chair seat, thighs, and hands anchoring the lower foreground" + ) + return oral_sentence( + "POV chair cunnilingus position: the woman sits in the chair with thighs open while the viewer kneels between her legs; " + "his face is at pussy height and her hips, knees, and chair seat define the first-person geometry" + ) + if "standing oral" in position_context: + if man_gives and not woman_gives: + return oral_sentence( + "POV standing cunnilingus position: the woman stands braced with one thigh lifted while the viewer kneels in front of her; " + "his face is at pussy height, with her raised thigh, hips, and standing leg clearly framing the view" + ) + return oral_sentence( + "POV standing oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him at hip height; " + "her head is tilted up at penis level, mouth on the viewer's penis, with his thighs and hands in the lower foreground" + ) + if ( + "reclining cunnilingus" in position_context + or "spread-leg oral" in position_context + or "open-thigh" in position_context + or "open thigh" in position_context + ): + if woman_gives and not man_gives: + return oral_sentence( + "POV reclining oral position: the viewer reclines with thighs apart while the woman kneels low between his legs; " + "her face stays at penis height with her mouth on the viewer's penis and his thighs framing the first-person view" + ) + return oral_sentence( + "POV open-thigh cunnilingus position: the woman reclines on her back with thighs spread toward the viewer; " + "the viewer kneels between her legs with his face at pussy height, her knees, hips, and torso aligned toward the camera" + ) + if "straddled oral" in position_context: + if woman_gives and not man_gives: + return oral_sentence( + "POV straddled oral position: the viewer leans forward near the woman's face while she kneels below his pelvis; " + "her mouth stays on the viewer's penis with her head tilted upward and his thighs framing the lower foreground" + ) + return oral_sentence( + "POV straddled cunnilingus position: the woman straddles above the viewer's face with her thighs framing his head; " + "her pussy stays directly over the viewer's mouth in a close first-person oral frame" + ) + if "kneeling oral" in position_context or "kneeling" in position_context: + if man_gives and not woman_gives: + return oral_sentence( + "POV kneeling cunnilingus position: the woman kneels with thighs parted and hips angled forward while the viewer kneels in front of her; " + "his face is at pussy height, with her knees, hips, and torso readable from the first-person angle" + ) + return oral_sentence( + "POV kneeling oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him; " + "her head is at penis height, mouth on the viewer's penis, shoulders below his hips and his thighs framing the lower foreground" + ) + if man_gives and not woman_gives: + return oral_sentence( + "POV cunnilingus position: the woman lies back with thighs open toward the viewer while he kneels between her legs; " + "his face is at pussy height, with her knees, hips, and torso forming the first-person frame" + ) + return oral_sentence( + "POV oral position: the woman kneels close at the viewer's pelvis with her head at penis height; " + "her mouth is on the viewer's penis, shoulders between his thighs, and the viewer's hands or thighs anchor the lower foreground" + ) + + if not has_penetrative_context: return "" oral_only = any(token in context for token in ("oral", "blowjob", "cunnilingus", "mouth on", "penis in her mouth")) if oral_only and not any(token in context for token in ("penetrat", "thrust", "anal", "ejaculat", "semen", "cumshot", "climax")): diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index f3f73f6..256a932 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -6199,37 +6199,37 @@ def smoke_pov_oral_position_routes() -> None: "pov_oral_kneeling", "kneeling", ("viewer's penis", "takes the viewer's penis in her mouth"), - ("takes the viewer's penis in her mouth", "viewer stands over her"), + ("pov kneeling oral position", "viewer stands over her", "head is at penis height", "thighs framing"), ), ( "pov_oral_face_sitting", "face_sitting", ("straddling the viewer's face", "pussy directly over the viewer's mouth"), - ("straddling the viewer's face", "tongue contact visible"), + ("close first-person underview", "straddling the viewer's face", "tongue contact visible"), ), ( "pov_oral_sixty_nine", "sixty_nine", ("head-to-hips", "viewer's mouth on woman a's pussy"), - ("head-to-hips", "viewer's mouth on the woman's pussy"), + ("pov sixty-nine oral position", "head-to-hips", "viewer's mouth on the woman's pussy", "lower-foreground body cues aligned"), ), ( "pov_oral_edge_supported", "edge_supported", ("raised edge with thighs open", "viewer kneels between her legs"), - ("raised edge with thighs open", "viewer kneels between her legs"), + ("pov raised-edge cunnilingus position", "raised edge with thighs open", "face at pussy height"), ), ( "pov_oral_side_lying", "side_lying", ("woman a lies on her side", "viewer lies beside her hips"), - ("woman lies on her side", "viewer lies beside her hips"), + ("pov side-lying cunnilingus position", "woman lies on her side", "top thigh lifted", "viewer lies beside her hips"), ), ( "pov_oral_chair", "chair_oral", ("viewer sits in a chair", "kneels between his thighs"), - ("viewer sits in a chair", "kneels between the viewer's thighs"), + ("pov chair oral position", "viewer sits in a chair", "kneels between the viewer's thighs", "head is low at his pelvis"), ), ] for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3701): @@ -6272,6 +6272,12 @@ def smoke_pov_oral_position_routes() -> None: _expect("viewer" in prompt and "first-person" in prompt, f"{name} Krea prompt lost POV wording") _expect("viewer lies on the viewer" not in prompt, f"{name} Krea prompt kept recursive POV wording: {prompt}") _expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive") + _expect("as he looks down" not in prompt, f"{name} Krea prompt kept unresolved male pronoun: {prompt}") + _expect( + "the woman takes the viewer's penis in her mouth with" not in prompt, + f"{name} Krea prompt repeated oral contact detail after POV rewrite: {prompt}", + ) + _expect("edge-supported oral position;" not in prompt, f"{name} Krea prompt kept source oral-position scaffold: {prompt}") for term in krea_terms: _expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")