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
+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: