Improve kneeling oral prompt geometry

This commit is contained in:
2026-06-25 18:00:56 +02:00
parent 5f439dc579
commit 7dc4333746
2 changed files with 202 additions and 1 deletions
+82
View File
@@ -829,6 +829,33 @@ def _is_outercourse_text(*parts: Any) -> bool:
) )
def _is_oral_text(*parts: Any) -> bool:
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
return any(
term in text
for term in (
"oral",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"penis in her mouth",
"penis in mouth",
"takes the man's penis",
"takes his penis",
"mouth at penis level",
"mouth on his penis",
"lips wrapped",
"cunnilingus",
"pussy licking",
"mouth on her pussy",
"mouth pressed to her pussy",
"face-sitting",
"sixty-nine",
)
)
def _is_toy_assisted_double_text(*parts: Any) -> bool: def _is_toy_assisted_double_text(*parts: Any) -> bool:
text = " ".join(_clean(part).lower() for part in parts if _clean(part)) text = " ".join(_clean(part).lower() for part in parts if _clean(part))
if "toy" not in text: if "toy" not in text:
@@ -1444,6 +1471,56 @@ def _dedupe_outercourse_detail(detail: str, role_graph: str, hard_item: str = ""
return _join_detail_clauses(clauses) return _join_detail_clauses(clauses)
def _dedupe_oral_detail(detail: str, role_graph: str, hard_item: str = "", axis_values: Any = None) -> str:
detail = _clean(detail)
if not detail:
return ""
context = _position_context_text(role_graph, hard_item, "", axis_values)
woman_gives = any(
term in context
for term in (
"takes the man's penis",
"takes his penis",
"penis in her mouth",
"mouth at penis level",
"mouth on his penis",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
)
)
clauses: list[str] = []
for clause in _detail_clauses(detail):
lower = clause.lower()
if any(
term in lower
for term in (
"kneeling oral position",
"standing oral position",
"edge-of-bed oral position",
"side-lying oral position",
"chair oral position",
"reclining cunnilingus position",
"face-sitting position",
"sixty-nine position",
"fellatio with penis in mouth",
"deepthroat blowjob",
"penis sucking with visible saliva",
"cunnilingus with tongue on pussy",
"oral sex with tongue and fingers",
"oral contact with mouth on the visible genitals",
"bodies stacked close together",
"body angle keeps the penis and face readable",
)
):
continue
if woman_gives and lower == "wet shine on genitals":
clause = "saliva dripping on the penis"
clauses.append(clause)
return _join_detail_clauses(clauses)
def _detail_clauses(detail: str) -> list[str]: def _detail_clauses(detail: str) -> list[str]:
return [part.strip(" ,;") for part in re.split(r",\s*(?:and\s+)?", _clean(detail)) if part.strip(" ,;")] return [part.strip(" ,;") for part in re.split(r",\s*(?:and\s+)?", _clean(detail)) if part.strip(" ,;")]
@@ -1768,6 +1845,7 @@ def _hardcore_action_sentence(
detail = _hardcore_item_detail(hard_item) detail = _hardcore_item_detail(hard_item)
anchor = _hardcore_pose_anchor(role_graph, hard_item, composition, axis_values) anchor = _hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
is_outercourse = _is_outercourse_text(role_graph, hard_item, composition, _axis_values_text(axis_values)) is_outercourse = _is_outercourse_text(role_graph, hard_item, composition, _axis_values_text(axis_values))
is_oral = _is_oral_text(role_graph, hard_item, composition, _axis_values_text(axis_values))
if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)): if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
role_graph = re.sub( role_graph = re.sub(
r"\s+while a toy adds (?:the|a) second penetration point\b", r"\s+while a toy adds (?:the|a) second penetration point\b",
@@ -1782,6 +1860,10 @@ def _hardcore_action_sentence(
anchor = "" anchor = ""
detail = _dedupe_outercourse_detail(detail, role_graph, hard_item, axis_values) detail = _dedupe_outercourse_detail(detail, role_graph, hard_item, axis_values)
detail = _limit_detail_for_density(detail, detail_density, False) detail = _limit_detail_for_density(detail, detail_density, False)
elif is_oral and role_graph:
anchor = ""
detail = _dedupe_oral_detail(detail, role_graph, hard_item, axis_values)
detail = _limit_detail_for_density(detail, detail_density, False)
else: else:
detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail
if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)): if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
+120 -1
View File
@@ -999,6 +999,8 @@ def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
return filtered(lambda text: "sixty-nine" in text) return filtered(lambda text: "sixty-nine" in text)
if "face-sitting" in position_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)) return filtered(lambda text: "face-sitting" in text or any(term in text for term in cunnilingus_terms))
if "kneeling oral" in position_text:
return filtered(lambda text: any(term in text for term in penis_terms))
if "straddled oral" in position_text or "reclining cunnilingus" in position_text: 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)) 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: if "spread-leg oral" in position_text:
@@ -1008,6 +1010,62 @@ def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
return values return values
def _oral_axis_values_for_context(values: list[Any], position: str, oral_act: str, axis_name: str) -> list[Any]:
axis_name = str(axis_name or "").lower()
if axis_name not in {"body_contact", "hand_detail", "mouth_detail", "saliva_detail", "climax_hint", "visibility"}:
return values
position_text = str(position or "").lower()
act_text = str(oral_act or "").lower()
woman_gives = any(
term in act_text
for term in ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth")
)
man_gives = any(
term in act_text
for term in ("cunnilingus", "pussy licking", "tongue on pussy")
)
if not (woman_gives or man_gives):
return values
def value_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(terms: tuple[str, ...], excluded_terms: tuple[str, ...] = ()) -> list[Any]:
matches = [
value
for value in values
if any(term in value_text(value) for term in terms)
and not any(term in value_text(value) for term in excluded_terms)
]
return matches or values
if woman_gives:
by_axis = {
"body_contact": ("hips pushed", "fingers tangled", "bodies stacked", "hands on thighs"),
"hand_detail": ("hips", "penis", "head", "hair"),
"mouth_detail": ("lips", "mouth", "deep mouth", "saliva"),
"saliva_detail": ("saliva", "wet lips", "slick wet mouth", "drool", "mouth"),
"climax_hint": ("mouth", "lips", "tongue", "breasts", "belly", "sexual fluids"),
"visibility": ("mouth", "penis", "oral"),
}
excluded = {
"body_contact": ("legs held open", "spread legs", "ass lifted", "chest pressed to thighs"),
"hand_detail": ("spreading thighs", "sheets", "cupping breasts", "pressing into thighs", "holding the ass"),
}
return filtered(by_axis.get(axis_name, ("mouth", "penis")), excluded.get(axis_name, ()))
if man_gives and ("kneeling oral" in position_text or "standing oral" in position_text):
by_axis = {
"body_contact": ("legs held open", "one body kneeling", "chest pressed", "ass lifted", "hands on thighs"),
"hand_detail": ("thigh", "hips", "head", "ass"),
"mouth_detail": ("tongue", "wet lips", "deep mouth", "genitals"),
"saliva_detail": ("saliva", "wet lips", "tongue", "drool"),
"climax_hint": ("sexual fluids", "orgasmic tension"),
"visibility": ("mouth", "pussy", "oral", "genital"),
}
return filtered(by_axis.get(axis_name, ("mouth", "pussy", "tongue")), ("penis", "breasts"))
return values
def _outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]: def _outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]:
position_text = str(position or "").lower() position_text = str(position or "").lower()
if not position_text: if not position_text:
@@ -1136,6 +1194,13 @@ def _compose_item(
values = _compatible_entries(axes[name], women_count, men_count) values = _compatible_entries(axes[name], women_count, men_count)
if subcategory_slug == "oral_sex" and name == "oral_act": if subcategory_slug == "oral_sex" and name == "oral_act":
values = _oral_acts_for_position(values, axis_values.get("position", "")) values = _oral_acts_for_position(values, axis_values.get("position", ""))
elif subcategory_slug == "oral_sex":
values = _oral_axis_values_for_context(
values,
axis_values.get("position", ""),
axis_values.get("oral_act", ""),
name,
)
if subcategory_slug == "outercourse_sex" and name == "outer_act": if subcategory_slug == "outercourse_sex" and name == "outer_act":
values = _outercourse_acts_for_position(values, axis_values.get("position", "")) values = _outercourse_acts_for_position(values, axis_values.get("position", ""))
if subcategory_slug == "outercourse_sex": if subcategory_slug == "outercourse_sex":
@@ -3634,9 +3699,60 @@ def _sanitize_character_expression_text_for_action(
) )
) )
) )
woman_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"takes man",
"penis in her mouth",
"mouth at penis level",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"lips wrapped",
)
)
)
man_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"mouth on her pussy",
"mouth on woman",
"mouth pressed to her pussy",
"cunnilingus",
"pussy licking",
"tongue on pussy",
)
)
)
mouth_expression_terms = ("mouth", "oral", "tongue", "lips", "gagging", "saliva")
clauses = [clause.strip() for clause in text.split(";") if clause.strip()] clauses = [clause.strip() for clause in text.split(";") if clause.strip()]
if woman_active_outercourse: if woman_active_outercourse:
clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)] clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)]
if woman_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Man [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
if man_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Woman [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
return "; ".join(clauses) return "; ".join(clauses)
@@ -5334,7 +5450,10 @@ def _role_graph(
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text): if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
if man_gives and not woman_gives: 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 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 close above her." return (
f"{woman} kneels in front of {man}'s penis while {man} stands over her; "
f"{woman} takes {man}'s penis in her mouth with saliva dripping on the penis as {man} looks down toward her."
)
if man_gives and not woman_gives: 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} 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." 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."