Improve oral hardcore pose variation

This commit is contained in:
2026-06-25 01:35:31 +02:00
parent 8ecb1a65c5
commit 0e27f2e5f4
2 changed files with 201 additions and 46 deletions
+59 -20
View File
@@ -55,7 +55,7 @@ HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = (
(r"\bsitting on the edge of the bed\b", "sitting on a raised edge"),
(r"\blying at the bed edge with thighs open\b", "lying near a raised edge with thighs open"),
(r"\bedge[- ]of[- ]bed\b", "edge-supported"),
(r"\bbed[- ]edge\b", "edge-supported"),
(r"\bbed[- ]edge\b", "raised edge"),
(r"\bedge of (?:the )?bed\b", "raised edge"),
(r"\bbed edge\b", "raised edge"),
(r"\bhands? braced on the bed\b", "hands braced beside the body"),
@@ -544,6 +544,9 @@ def _mentions_rear_entry(text: str) -> bool:
def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
text = _position_context_text(role_graph, hard_item, composition, axis_values)
item_text = " ".join(part for part in (_clean(hard_item).lower(), _axis_values_text(axis_values).lower()) if part)
position_text = ""
if isinstance(axis_values, dict):
position_text = _clean(axis_values.get("position", "")).lower()
if not text:
return ""
if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
@@ -582,10 +585,30 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
if "kneeling" in text:
return "kneeling front-and-back double-penetration pose"
return "front-and-back double-penetration pose"
if "sixty-nine" in text:
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
return "sixty-nine oral pose"
if "face-sitting" in text:
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
return "face-sitting oral pose"
if "side-lying oral" in position_text or (("side-lying oral position" in item_text or "side-lying oral" in text) and not position_text):
return "side-lying oral pose"
if (
"edge-of-bed oral" in position_text
or "edge-supported oral" in position_text
or (("edge-of-bed oral position" in item_text or "edge-of-bed oral" in text or "edge-supported oral" in text) and not position_text)
):
return "edge-supported oral pose"
if "standing oral" in position_text or (("standing oral position" in item_text or "standing oral" in text) and not position_text):
return "standing oral pose"
if "chair oral" in position_text or (("chair oral position" in item_text or "chair oral" in text) and not position_text):
return "chair oral pose"
if "kneeling oral" in position_text or (("kneeling oral position" in item_text or "kneeling oral" in text) and not position_text):
return "kneeling oral pose"
if "straddled oral" in position_text or (("straddled oral position" in item_text or "straddled oral" in text) and not position_text):
return "straddled cunnilingus pose"
if "reclining cunnilingus" in position_text or (("reclining cunnilingus position" in item_text or "reclining cunnilingus" in text) and not position_text):
return "reclining cunnilingus pose"
if "spread-leg oral" in position_text or (("spread-leg oral position" in item_text or "spread-leg oral" in text) and not position_text):
return "spread-leg oral pose"
if "cunnilingus" in text or "pussy licking" in text or "mouth on her pussy" in text:
if "reclining" in text:
return "reclining cunnilingus pose"
@@ -598,7 +621,7 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
if "spread-leg oral position" in item_text:
return "spread-leg oral pose"
if "edge-of-bed oral position" in item_text:
return "edge-of-bed oral pose"
return "edge-supported oral pose"
if "standing oral position" in item_text:
return "standing oral pose"
if "chair oral position" in item_text:
@@ -610,7 +633,7 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
if "side-lying" in text:
return "side-lying oral pose"
if "edge-of-bed" in text or "bed-edge" in text:
return "edge-of-bed oral pose"
return "edge-supported oral pose"
if "spread-leg" in text:
return "spread-leg oral pose"
if "chair oral" in text:
@@ -660,6 +683,9 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = ""
def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
text = _position_context_text(anchor, f"{role_graph} {hard_item}", composition, axis_values)
position_text = ""
if isinstance(axis_values, dict):
position_text = _clean(axis_values.get("position", "")).lower()
if not text:
return ""
mixed_woman_man = "the woman" in text and "the man" in text
@@ -671,17 +697,21 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
def double_tail() -> str:
return "" if "toy" in text else ", with the second penetration point aligned"
if "sixty-nine" in text:
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
return cast_phrase(
"with the woman and man inverted head-to-hips so both mouths align with genitals",
"with both bodies inverted head-to-hips so both mouths align with genitals",
)
if "face-sitting" in text:
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
return cast_phrase(
"with the man lying back while the woman straddles his face",
"with one partner lying back while the other straddles the face",
)
if "reclining cunnilingus" in text or "spread-leg oral" in text:
if (
"reclining cunnilingus" in position_text
or "spread-leg oral" in position_text
or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text)
):
if "takes the man's penis" in text or "penis in her mouth" in text:
return cast_phrase(
"with the man seated with legs apart and the woman positioned at his hips",
@@ -691,22 +721,30 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
"with the woman lying back, thighs spread, and the man positioned between her legs",
"with the receiving partner lying back, thighs spread, and the giver positioned between the legs",
)
if "straddled cunnilingus" in text or "straddled oral" in text:
if (
"straddled oral" in position_text
or (("straddled cunnilingus" in text or "straddled oral" in text) and not position_text)
):
return cast_phrase(
"with the woman straddling above the man's mouth and her thighs framing his face",
"with the receiver straddling above the giver's mouth",
)
if "edge-of-bed oral" in text:
if (
"edge-of-bed oral" in position_text
or "edge-supported oral" in position_text
or ("edge-of-bed oral" in text and not position_text)
or ("edge-supported oral" in text and not position_text)
):
if "takes the man's penis" in text or "penis in her mouth" in text:
return cast_phrase(
"with the man at the bed edge and the woman kneeling at his hips",
"with the receiver at the bed edge and the giver positioned at hip height",
"with the man at a raised edge and the woman kneeling at his hips",
"with the receiver at a raised edge and the giver positioned at hip height",
)
return cast_phrase(
"with the woman lying at the bed edge and the man positioned between her open thighs",
"with the receiver lying at the bed edge and the giver positioned between open thighs",
"with the woman lying at a raised edge and the man positioned between her open thighs",
"with the receiver lying at a raised edge and the giver positioned between open thighs",
)
if "standing oral" in text:
if "standing oral" in position_text or ("standing oral" in text and not position_text):
if "takes the man's penis" in text or "penis in her mouth" in text:
return cast_phrase(
"with the man standing and the woman kneeling in front of his hips",
@@ -716,7 +754,7 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
"with the woman standing braced and the man kneeling between her thighs",
"with the receiver standing braced and the giver kneeling between the thighs",
)
if "chair oral" in text:
if "chair oral" in position_text or ("chair oral" in text and not position_text):
if "takes the man's penis" in text or "penis in her mouth" in text:
return cast_phrase(
"with the man seated in the chair and the woman kneeling between his legs at hip level",
@@ -726,9 +764,9 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com
"with one partner seated in a chair and the other kneeling between the open thighs",
"with the receiver seated in a chair and the giver kneeling between the open thighs",
)
if "side-lying oral" in text:
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
return "with both bodies lying on their sides and mouth aligned to genitals"
if "kneeling oral" in text:
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
if "takes the man's penis" in text or "penis in her mouth" in text:
return cast_phrase(
"with the woman kneeling in front of the man's hips, her mouth at penis level",
@@ -917,7 +955,7 @@ def _hardcore_item_detail(hard_item: str) -> str:
r"missionary position|cowgirl position|reverse cowgirl position|doggy style position|"
r"standing sex position|spooning sex position|edge-of-bed position|kneeling straddle position|"
r"lotus sex position|bent-over position|kneeling oral position|face-sitting position|"
r"sixty-nine position|edge-of-bed oral position|standing oral position|reclining cunnilingus position|"
r"sixty-nine position|edge-of-bed oral position|edge-supported oral position|standing oral position|reclining cunnilingus position|"
r"straddled oral position|side-lying oral position|spread-leg oral position|chair oral position"
)
text = re.sub(
@@ -962,7 +1000,8 @@ def _dedupe_hardcore_detail(detail: str, anchor: str) -> str:
r"spread-leg oral position",
r"chair oral position",
),
"edge-of-bed oral": (r"edge-of-bed oral position",),
"edge-supported oral": (r"edge-of-bed oral position", r"edge-supported oral position"),
"edge-of-bed oral": (r"edge-of-bed oral position", r"edge-supported oral position"),
"standing oral": (r"standing oral position",),
"spread-leg oral": (r"spread-leg oral position",),
"chair oral": (r"chair oral position",),
+142 -26
View File
@@ -6,7 +6,7 @@ import random
import re
from pathlib import Path
from string import Formatter
from typing import Any
from typing import Any, Callable
try:
from . import generate_prompt_batches as g
@@ -327,7 +327,10 @@ HARDCORE_POSITION_KEY_CHOICES = [
"lotus_lap",
"face_sitting",
"sixty_nine",
"reclining_oral",
"straddled_oral",
"spread_leg_oral",
"chair_oral",
"open_thighs",
"front_back",
]
@@ -361,7 +364,10 @@ HARDCORE_POSITION_KEY_MATCHES = {
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
"face_sitting": ("face-sitting", "face sitting"),
"sixty_nine": ("sixty-nine", "69"),
"reclining_oral": ("reclining cunnilingus",),
"straddled_oral": ("straddled oral",),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"chair_oral": ("chair oral",),
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
}
@@ -863,7 +869,7 @@ HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = (
(r"\bsitting on the edge of the bed\b", "sitting on a raised edge"),
(r"\blying at the bed edge with thighs open\b", "lying near a raised edge with thighs open"),
(r"\bedge[- ]of[- ]bed\b", "edge-supported"),
(r"\bbed[- ]edge\b", "edge-supported"),
(r"\bbed[- ]edge\b", "raised edge"),
(r"\bedge of (?:the )?bed\b", "raised edge"),
(r"\bbed edge\b", "raised edge"),
(r"\bhands? braced on the bed\b", "hands braced beside the body"),
@@ -960,6 +966,33 @@ def _merged_axes(category: dict[str, Any], subcategory: dict[str, Any], item: An
return axes
def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
def act_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(predicate: Callable[[str], bool]) -> list[Any]:
matches = [value for value in values if predicate(act_text(value))]
return matches or values
penis_terms = ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth")
cunnilingus_terms = ("cunnilingus", "pussy licking", "tongue on pussy", "oral sex with tongue and fingers", "mouth on genitals")
if "sixty-nine" in position_text:
return filtered(lambda text: "sixty-nine" in text)
if "face-sitting" in position_text:
return filtered(lambda text: "face-sitting" in text or any(term in text for term in cunnilingus_terms))
if "straddled oral" in position_text or "reclining cunnilingus" in position_text:
return filtered(lambda text: "sixty-nine" not in text and not any(term in text for term in penis_terms))
if "spread-leg oral" in position_text:
return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text)
if any(term in position_text for term in ("standing oral", "kneeling oral", "edge-of-bed oral", "chair oral", "side-lying oral")):
return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text)
return values
def _compose_item(
rng: random.Random,
category: dict[str, Any],
@@ -972,12 +1005,19 @@ def _compose_item(
axes = _merged_axes(category, subcategory, item)
if templates and axes:
template = _entry_text(_weighted_choice(rng, _compatible_entries(templates, women_count, men_count)))
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
axis_values = {
name: _entry_text(_weighted_choice(rng, _compatible_entries(axes[name], women_count, men_count)))
for name in fields
if name in axes and axes[name]
}
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
unique_fields = list(dict.fromkeys(fields))
axis_values: dict[str, str] = {}
if str(subcategory.get("slug") or "").lower() == "oral_sex" and "position" in unique_fields and axes.get("position"):
position_values = _compatible_entries(axes["position"], women_count, men_count)
axis_values["position"] = _entry_text(_weighted_choice(rng, position_values))
for name in unique_fields:
if name in axis_values or name not in axes or not axes[name]:
continue
values = _compatible_entries(axes[name], women_count, men_count)
if str(subcategory.get("slug") or "").lower() == "oral_sex" and name == "oral_act":
values = _oral_acts_for_position(values, axis_values.get("position", ""))
axis_values[name] = _entry_text(_weighted_choice(rng, values))
item_text = _format(template, axis_values).strip()
item_name = _item_name(item) or subcategory["name"]
return item_text, item_name, axis_values
@@ -1736,6 +1776,12 @@ def _hardcore_position_config_active(config: dict[str, Any]) -> bool:
return bool(config.get("enabled"))
def _hardcore_position_template_required(config: dict[str, Any]) -> bool:
if not _hardcore_position_config_active(config):
return False
return bool(config.get("positions")) or _normalize_hardcore_position_family(config.get("family")) != "any"
def _is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
return str(category.get("slug") or "").strip() == "hardcore_sexual_poses" or str(category.get("name") or "").strip().lower() == "hardcore sexual poses"
@@ -1841,7 +1887,7 @@ def _hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bo
def _hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
if not config.get("positions"):
if not _hardcore_position_template_required(config):
return True
axes = subcategory.get("item_axes")
if not isinstance(axes, dict):
@@ -1875,7 +1921,7 @@ def _filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) ->
for template in templates:
text = _entry_text(template)
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
blocked = bool(config.get("positions")) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
blocked = _hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
blocked = blocked or any(_hardcore_text_blocked_by_action(text, field, config) for field in fields | {""})
if not blocked:
filtered.append(template)
@@ -4881,6 +4927,91 @@ def _role_graph(
return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass."
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
def oral_position_graph(woman: str, man: str) -> str:
position_text = str((item_axis_values or {}).get("position") or "").lower()
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
woman_gives = any(
term in text
for term in (
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"penis in mouth",
"penis in her mouth",
"mouth stretched around a penis",
"lips wrapped",
)
)
man_gives = any(
term in text
for term in (
"cunnilingus",
"pussy licking",
"tongue on pussy",
"mouth on pussy",
"pussy and tongue",
"face-sitting",
"tongue contact clearly visible",
)
)
if "mouth on genitals" in text and not woman_gives and not man_gives:
if any(term in text for term in ("face-sitting", "reclining", "straddled", "spread-leg", "open thighs")):
man_gives = True
else:
woman_gives = True
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
return f"{woman} and {man} lie head-to-hips in a sixty-nine position, with {woman}'s mouth on {man}'s penis and {man}'s mouth on {woman}'s pussy."
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
return f"{man} lies on his back while {woman} straddles his face with her thighs around his head and {man}'s mouth pressed to her pussy."
if "straddled oral" in position_text or ("straddled oral" in text and not position_text):
if woman_gives and not man_gives:
return f"{man} straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis."
return f"{woman} straddles above {man}'s face with her thighs framing his head while {man}'s mouth stays pressed to her pussy."
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
if woman_gives and not man_gives:
return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth."
return f"{woman} lies on her side with her top thigh lifted while {man} lies beside her hips with his mouth pressed to her pussy."
if (
"edge-of-bed oral" in position_text
or "edge of bed oral" in position_text
or "edge-supported oral" in position_text
or (("edge-of-bed oral" in text or "edge of bed oral" in text or "edge-supported oral" in text) and not position_text)
):
if woman_gives and not man_gives:
return f"{man} sits at a raised edge with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
return f"{woman} lies at a raised edge with thighs open while {man} kneels between her legs with his mouth on her pussy."
if "standing oral" in position_text or ("standing oral" in text and not position_text):
if man_gives and not woman_gives:
return f"{woman} stands braced with one thigh lifted while {man} kneels between her legs with his mouth on her pussy."
return f"{man} stands with hips forward while {woman} kneels in front of him at hip height and takes his penis in her mouth."
if "chair oral" in position_text or ("chair oral" in text and not position_text):
if man_gives and not woman_gives:
return f"{woman} sits in a chair with thighs open while {man} kneels between her legs with his mouth pressed to her pussy."
return f"{man} sits in a chair with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
if (
"reclining cunnilingus" in position_text
or "spread-leg oral" in position_text
or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text)
):
if woman_gives and not man_gives:
return f"{man} reclines with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
return f"{woman} reclines on her back with thighs spread while {man} kneels between her legs with his mouth on her pussy."
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
if man_gives and not woman_gives:
return f"{woman} kneels with thighs parted and hips angled forward while {man} kneels in front of her with his mouth on her pussy."
return f"{woman} kneels in front of {man}'s hips with her mouth at penis level while {man} stands or sits close above her."
if man_gives and not woman_gives:
return f"{woman} lies on her back with thighs open while {man} kneels between her legs with his mouth pressed to her pussy."
return f"{woman} kneels in front of {man}'s hips and takes his penis in her mouth while {man} keeps his hips aligned with her face."
if people_count == 1:
solo = people[0]
if women_count == 1:
@@ -4932,22 +5063,7 @@ def _role_graph(
man = any_man()
third = any_person({woman, man}) if people_count >= 3 else ""
if "oral" in slug:
if "sixty-nine" in item_text or ("blowjob" in item_text and ("cunnilingus" in item_text or "pussy" in item_text)):
graph = f"{woman} has {man}'s penis in her mouth while {man} uses his mouth on {woman}'s pussy, with both mouths pressed to genitals."
elif any(
term in item_text
for term in (
"cunnilingus",
"pussy licking",
"tongue on pussy",
"mouth on pussy",
"pussy and tongue",
"tongue contact",
)
) or ("pussy" in item_text and "penis" not in item_text):
graph = f"{man} gives oral to {woman}, mouth on her pussy while {woman}'s thighs are held open for the camera."
else:
graph = f"{woman} takes {man}'s penis in her mouth while {man} holds her hair and hips."
graph = oral_position_graph(woman, man)
elif "anal" in slug or "double" in slug:
if "double" in item_text or "toy" in item_text:
if people_count >= 3: