Clean up outercourse prompt wording

This commit is contained in:
2026-06-25 17:41:00 +02:00
parent 1f851bf1cc
commit 5f439dc579
4 changed files with 134 additions and 9 deletions
@@ -235,6 +235,20 @@
"wet lips pressed around a partner", "wet lips pressed around a partner",
"shameless oral-service stare" "shameless oral-service stare"
], ],
"hardcore_outercourse_expressions": [
"focused downward gaze at the contact point",
"eyes looking up from the contact point",
"mouth open in focused concentration",
"heavy-lidded teasing eye contact",
"controlled hungry expression",
"soft flushed concentration",
"messy lips with steady eye contact",
"breathless focused pleasure face",
"intense close-camera stare",
"wet lips close to the contact point",
"calm concentrated pleasure expression",
"playful shameless eye contact"
],
"hardcore_anal_dp_expressions": [ "hardcore_anal_dp_expressions": [
"controlled braced expression during anal sex", "controlled braced expression during anal sex",
"focused eye contact while hips are held open", "focused eye contact while hips are held open",
+3 -3
View File
@@ -400,12 +400,12 @@
"inherit_compositions": false, "inherit_compositions": false,
"weight": 1.0, "weight": 1.0,
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"], "scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
"expression_pools": ["hardcore_oral_expressions", "hardcore_messy_expressions"], "expression_pools": ["hardcore_outercourse_expressions"],
"compositions": [ "compositions": [
{"text": "tight contact close-up with penis, hands, and face readable", "min_people": 2, "max_people": 3}, {"text": "tight contact close-up with penis, hands, and face readable", "min_people": 2, "max_people": 3},
{"text": "side-profile non-penetrative contact frame with body geometry clear", "min_people": 2, "max_people": 3}, {"text": "side-profile non-penetrative contact frame with body geometry clear", "min_people": 2, "max_people": 3},
{"text": "POV-aligned close-up from the man's hips with the visible partner centered", "min_people": 2, "max_people": 2}, {"text": "POV-aligned close-up from the man's hips with the visible partner centered", "min_people": 2, "max_people": 2},
{"text": "low-angle close crop on breasts or feet around the penis", "min_people": 2, "max_people": 3}, {"text": "low-angle close crop around the penis with the active contact readable", "min_people": 2, "max_people": 3},
{"text": "front-facing kneeling contact composition with eyes, mouth, and hands visible", "min_people": 2, "max_people": 3}, {"text": "front-facing kneeling contact composition with eyes, mouth, and hands visible", "min_people": 2, "max_people": 3},
{"text": "mirror-reflected non-penetrative sex composition with contact point readable", "min_people": 2, "max_people": 3}, {"text": "mirror-reflected non-penetrative sex composition with contact point readable", "min_people": 2, "max_people": 3},
{"text": "waist-level explicit contact frame with natural skin compression visible", "min_people": 2, "max_people": 3}, {"text": "waist-level explicit contact frame with natural skin compression visible", "min_people": 2, "max_people": 3},
@@ -413,7 +413,7 @@
], ],
"item_templates": [ "item_templates": [
"{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}", "{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}",
"{position} featuring {outer_act}, {body_contact}, {texture_detail}, and {angle}", "{position} featuring {outer_act}, {body_contact}, {texture_detail}, seen from a {angle} view",
"{angle} view of {outer_act}, with {visibility}, {contact_detail}, and {expression_detail}", "{angle} view of {outer_act}, with {visibility}, {contact_detail}, and {expression_detail}",
"explicit non-penetrative sex pose: {outer_act}, {position}, {contact_detail}, and {visibility}", "explicit non-penetrative sex pose: {outer_act}, {position}, {contact_detail}, and {visibility}",
"{outer_act} on {surface}, with {hand_detail}, {body_contact}, and {texture_detail}", "{outer_act} on {surface}, with {hand_detail}, {body_contact}, and {texture_detail}",
+45
View File
@@ -1404,6 +1404,46 @@ def _dedupe_toy_double_detail(detail: str) -> str:
return _clean(detail).strip(" ,;") return _clean(detail).strip(" ,;")
def _dedupe_outercourse_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)
context_lower = context.lower()
breast_sex = any(term in context_lower for term in ("boobjob", "titjob", "breast sex", "breast-sex"))
clauses: list[str] = []
for clause in _detail_clauses(detail):
lower = clause.lower()
if breast_sex:
if lower in ("penis", "breasts", "mouth clearly visible"):
continue
if any(
term in lower
for term in (
"boobjob",
"titjob",
"breast-sex",
"breast sex",
"seated titjob position",
"kneeling boobjob position",
"tight close-up breast-sex position",
"penis shaft compressed between breasts",
"penis squeezed between both breasts",
"hands pressing the breasts tightly",
"hands pressing breasts firmly together",
"fingers spreading the breasts around the penis shaft",
"soft flesh squeezed around the penis shaft",
"hand wrapped around the penis shaft",
"glans near the mouth",
"glans visible",
"penis, breasts, and mouth clearly visible",
)
):
continue
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(" ,;")]
@@ -1727,6 +1767,7 @@ def _hardcore_action_sentence(
role_graph = _climax_role_graph(role_graph, hard_item, axis_values) role_graph = _climax_role_graph(role_graph, hard_item, axis_values)
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))
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",
@@ -1737,6 +1778,10 @@ def _hardcore_action_sentence(
if is_climax: if is_climax:
anchor = "" anchor = ""
detail = _dedupe_climax_detail(detail, role_graph, detail_density) detail = _dedupe_climax_detail(detail, role_graph, detail_density)
elif is_outercourse:
anchor = ""
detail = _dedupe_outercourse_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)):
+72 -6
View File
@@ -1044,19 +1044,34 @@ def _outercourse_axis_values_for_position(values: list[Any], position: str, axis
def value_text(value: Any) -> str: def value_text(value: Any) -> str:
return _entry_text(value).lower() return _entry_text(value).lower()
def filtered(terms: tuple[str, ...]) -> list[Any]: 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)] 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 return matches or values
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")): if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
by_axis = { by_axis = {
"contact_detail": ("compressed", "glans", "shaft", "skin", "fingers"), "contact_detail": ("compressed", "glans", "breast", "breasts", "soft tissue", "skin visibly"),
"hand_detail": ("breast", "breasts", "fingers"), "hand_detail": ("breast", "breasts", "fingers"),
"texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"), "texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"),
"visibility": ("breast", "breasts", "glans", "shaft"), "visibility": ("breast", "breasts", "glans", "shaft"),
"body_contact": ("torso", "body angled", "shoulders", "hips"), "body_contact": ("torso", "body angled", "shoulders", "hips"),
} }
return filtered(by_axis.get(axis_name, ("breast", "breasts", "shaft"))) excluded_by_axis = {
"contact_detail": ("hand wrapped", "fingers and palm", "soles", "toes", "balls", "tongue"),
"hand_detail": ("base of the penis", "penis shaft", "balls", "thigh", "ankles", "stroking"),
"texture_detail": ("toes", "soles", "tongue"),
"visibility": ("balls", "soles", "toes", "hand"),
"body_contact": ("head tucked", "face directly", "base of the penis"),
}
return filtered(
by_axis.get(axis_name, ("breast", "breasts", "shaft")),
excluded_by_axis.get(axis_name, ()),
)
if any(term in position_text for term in ("testicle", "balls")): if any(term in position_text for term in ("testicle", "balls")):
by_axis = { by_axis = {
"contact_detail": ("balls", "lips", "tongue", "wet"), "contact_detail": ("balls", "lips", "tongue", "wet"),
@@ -3581,6 +3596,50 @@ def _character_expression_entries(
return expressions return expressions
def _sanitize_character_expression_text_for_action(
expression_text: str,
role_graph: Any,
item: Any,
axis_values: Any = None,
) -> str:
text = str(expression_text or "").strip()
if not text:
return ""
context = " ".join(
str(part or "").lower()
for part in (
role_graph,
item,
*((axis_values or {}).values() if isinstance(axis_values, dict) else ()),
)
)
woman_active_outercourse = (
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 (
"boobjob",
"titjob",
"breast sex",
"breasts tightly",
"testicle",
"balls-licking",
"balls licking",
"penis-licking",
"penis licking",
"handjob",
"hand job",
"footjob",
)
)
)
clauses = [clause.strip() for clause in text.split(";") if clause.strip()]
if woman_active_outercourse:
clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)]
return "; ".join(clauses)
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str: def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
detail = _normalize_descriptor_detail(descriptor_detail) detail = _normalize_descriptor_detail(descriptor_detail)
if detail != "auto": if detail != "auto":
@@ -5136,8 +5195,8 @@ def _role_graph(
"both hands lifting and pressing her breasts tightly around the POV viewer's penis shaft while the glans sits just below her lips." "both hands lifting and pressing her breasts tightly around the POV viewer's penis shaft while the glans sits just below her lips."
) )
return ( return (
f"{man} sits with legs apart while {woman} kneels between his open thighs with her torso bent forward over his pelvis and shoulders low, " f"{woman} kneels between {man}'s open thighs with her torso bent forward over his pelvis and shoulders low while {man} sits with legs apart, "
f"both hands lifting and pressing her breasts tightly around {man}'s penis shaft while the glans sits just below her lips." f"{woman}'s hands lifting and pressing her breasts tightly around {man}'s penis shaft while the glans sits just below her lips."
) )
if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")): if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")):
if man_is_pov: if man_is_pov:
@@ -5840,6 +5899,13 @@ def _build_custom_row(
expression_phase, expression_phase,
) )
character_expression_text = "; ".join(character_expressions) character_expression_text = "; ".join(character_expressions)
character_expression_text = _sanitize_character_expression_text_for_action(
character_expression_text,
source_role_graph,
item,
item_axis_values,
)
character_expressions = [part.strip() for part in character_expression_text.split(";") if part.strip()]
if character_expression_text: if character_expression_text:
expression = character_expression_text expression = character_expression_text
source_composition = _choose_text( source_composition = _choose_text(