Extract Krea action context helpers
This commit is contained in:
@@ -146,6 +146,9 @@ Already isolated:
|
||||
natural label replacement for formatter routes.
|
||||
- `krea_clothing.py` owns clothing-state cleanup and action-aware body-access
|
||||
wording for formatter routes.
|
||||
- `krea_action_context.py` owns shared action-family predicates, axis context
|
||||
text, climax detection, and detail-density normalization used by action and
|
||||
POV formatter routes.
|
||||
|
||||
Improve later:
|
||||
|
||||
|
||||
@@ -547,6 +547,7 @@ Key Krea2 ownership:
|
||||
|
||||
- Cast descriptor naturalization: `krea_cast.cast_prose`,
|
||||
`krea_cast.natural_label_text`.
|
||||
- Action context and family predicates: `krea_action_context.py`.
|
||||
- Hardcore action sentence: `_hardcore_action_sentence`.
|
||||
- POV hardcore sentence: `_pov_hardcore_pose_sentence`, `_pov_action_phrase`.
|
||||
- Clothing state cleanup: `krea_clothing.natural_clothing_state`.
|
||||
@@ -733,6 +734,7 @@ Use these traces to narrow a problem in one pass.
|
||||
3. Inspect `categories/sexual_poses.json` for the selected subcategory,
|
||||
`item_templates`, `axes`, and `weight`.
|
||||
4. If raw `item` differs but Krea output looks identical, inspect
|
||||
`krea_action_context.py` family predicates first, then
|
||||
`_hardcore_pose_anchor`, `_hardcore_pose_arrangement`,
|
||||
`_hardcore_item_detail`, and `_hardcore_action_sentence`.
|
||||
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"}
|
||||
|
||||
|
||||
def _clean(value: Any) -> str:
|
||||
text = "" if value is None else str(value)
|
||||
text = text.replace("\n", " ")
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||
return text
|
||||
|
||||
|
||||
def normalize_hardcore_detail_density(value: Any) -> str:
|
||||
text = _clean(value).lower()
|
||||
return text if text in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
|
||||
|
||||
|
||||
def axis_values_text(axis_values: Any) -> str:
|
||||
if not isinstance(axis_values, dict):
|
||||
return ""
|
||||
priority = (
|
||||
"position",
|
||||
"body_position",
|
||||
"body_arrangement",
|
||||
"arrangement",
|
||||
"angle",
|
||||
"surface",
|
||||
"body_contact",
|
||||
"leg_detail",
|
||||
"oral_act",
|
||||
"oral_detail",
|
||||
"penetration_act",
|
||||
"penetration_detail",
|
||||
"anal_act",
|
||||
"double_act",
|
||||
"threesome_act",
|
||||
"group_act",
|
||||
)
|
||||
parts = [_clean(axis_values.get(key)) for key in priority if _clean(axis_values.get(key))]
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
||||
return " ".join(
|
||||
_clean(part).lower()
|
||||
for part in (role_graph, hard_item, composition, axis_values_text(axis_values))
|
||||
if _clean(part)
|
||||
)
|
||||
|
||||
|
||||
def is_outercourse_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"outercourse",
|
||||
"non-penetrative",
|
||||
"boobjob",
|
||||
"titjob",
|
||||
"breast sex",
|
||||
"breast-sex",
|
||||
"testicle",
|
||||
"balls",
|
||||
"balls licking",
|
||||
"balls-licking",
|
||||
"breasts tightly around",
|
||||
"breasts around",
|
||||
"penis licking",
|
||||
"penis-licking",
|
||||
"tongue along",
|
||||
"tongue runs along",
|
||||
"tongue running along",
|
||||
"handjob",
|
||||
"hand job",
|
||||
"hand wrapped",
|
||||
"hand stroking",
|
||||
"hand wraps around",
|
||||
"manual stimulation",
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"fingers in pussy",
|
||||
"hand on pussy",
|
||||
"fingers on pussy",
|
||||
"fingers sliding against the pussy",
|
||||
"open-thigh manual",
|
||||
"clit rubbing",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"mutual masturbation",
|
||||
"footjob",
|
||||
"soles wrap around",
|
||||
"soles",
|
||||
"toes curled",
|
||||
"feet stroking",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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_foreplay_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text:
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"foreplay",
|
||||
"pre-sex",
|
||||
"before sex",
|
||||
"before penetration",
|
||||
"kissing",
|
||||
"deep kiss",
|
||||
"mouth-to-mouth",
|
||||
"lips pressed",
|
||||
"caressing",
|
||||
"hands roaming",
|
||||
"stroking skin",
|
||||
"touching breasts",
|
||||
"cupping a breast",
|
||||
"hand on the cheek",
|
||||
"cheek and jaw",
|
||||
"fingers under the chin",
|
||||
"undressing",
|
||||
"removing clothing",
|
||||
"removing clothes",
|
||||
"pulling clothing",
|
||||
"sliding straps",
|
||||
"unbuttoning",
|
||||
"body worship",
|
||||
"nipple",
|
||||
"mouth on skin",
|
||||
"kissing down",
|
||||
"ass grabbing",
|
||||
"gripping the ass",
|
||||
"thigh kissing",
|
||||
"inner thighs",
|
||||
"hair held",
|
||||
"holding hair",
|
||||
"hair pulled back",
|
||||
"wrist",
|
||||
"wrists",
|
||||
"pinning",
|
||||
"guided",
|
||||
"guiding",
|
||||
"turning the body",
|
||||
"position transition",
|
||||
"pulling onto the bed",
|
||||
"lifting and spreading",
|
||||
"spreading thighs",
|
||||
"dirty talk",
|
||||
"whispering",
|
||||
"camera performance",
|
||||
"presented directly to the camera",
|
||||
"present her body",
|
||||
"showing to camera",
|
||||
"spread open for the camera",
|
||||
"watching partner",
|
||||
"waiting turn",
|
||||
"group coordination",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"wiping",
|
||||
"towel",
|
||||
"post-sex",
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"hand on pussy",
|
||||
"fingers on pussy",
|
||||
"clit rubbing",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"manual stimulation",
|
||||
"mutual masturbation",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_close_foreplay_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text or not is_foreplay_text(text):
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"stand close",
|
||||
"stand face-to-face",
|
||||
"press their bodies",
|
||||
"bodies pressed close",
|
||||
"hips pressed close",
|
||||
"mouth-to-mouth",
|
||||
"deep kissing",
|
||||
"heated kiss",
|
||||
"hands pull clothing",
|
||||
"pull clothing aside",
|
||||
"clothing being removed",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_vaginal_penetration_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text or is_outercourse_text(text) or is_oral_text(text) or is_foreplay_text(text):
|
||||
return False
|
||||
if any(term in text for term in ("anal", "double penetration", "double-penetration", "toy-assisted", "strap-on")):
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"vaginal penetration",
|
||||
"deep vaginal sex",
|
||||
"explicit penetrative sex",
|
||||
"penetrative sex",
|
||||
"penis entering pussy",
|
||||
"penis thrusts into her pussy",
|
||||
"penis thrusts into the woman",
|
||||
"pussy stretched around a penis",
|
||||
"hardcore vaginal thrusting",
|
||||
"full-body penetrative sex",
|
||||
"close-contact vaginal sex",
|
||||
"missionary position",
|
||||
"cowgirl position",
|
||||
"reverse cowgirl position",
|
||||
"doggy style position",
|
||||
"standing sex position",
|
||||
"spooning sex position",
|
||||
"edge-of-bed position",
|
||||
"kneeling straddle position",
|
||||
"lotus sex position",
|
||||
"bent-over position",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_toy_assisted_double_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if "toy" not in text:
|
||||
return False
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"double penetration",
|
||||
"double-penetration",
|
||||
"vaginal and anal penetration",
|
||||
"second penetration point",
|
||||
"second point of contact",
|
||||
"second contact",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_climax_text(*parts: str) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"cumshot",
|
||||
"ejaculation",
|
||||
"post-orgasm",
|
||||
"post-climax",
|
||||
"orgasm aftermath",
|
||||
"orgasm scene",
|
||||
"orgasm during",
|
||||
"shared climax",
|
||||
"hardcore climax",
|
||||
"external cumshot",
|
||||
"visible external ejaculation",
|
||||
"climaxes on",
|
||||
"climax lands",
|
||||
)
|
||||
)
|
||||
+24
-288
@@ -5,6 +5,18 @@ import re
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_action_context import (
|
||||
axis_values_text as _axis_values_text,
|
||||
is_climax_text as _is_climax_text,
|
||||
is_close_foreplay_text as _is_close_foreplay_text,
|
||||
is_foreplay_text as _is_foreplay_text,
|
||||
is_oral_text as _is_oral_text,
|
||||
is_outercourse_text as _is_outercourse_text,
|
||||
is_toy_assisted_double_text as _is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text as _is_vaginal_penetration_text,
|
||||
normalize_hardcore_detail_density as _normalize_hardcore_detail_density,
|
||||
position_context_text as _position_context_text,
|
||||
)
|
||||
from .hardcore_text_cleanup import (
|
||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||
@@ -19,6 +31,18 @@ try:
|
||||
from .krea_clothing import natural_clothing_state as _natural_clothing_state
|
||||
from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_context import (
|
||||
axis_values_text as _axis_values_text,
|
||||
is_climax_text as _is_climax_text,
|
||||
is_close_foreplay_text as _is_close_foreplay_text,
|
||||
is_foreplay_text as _is_foreplay_text,
|
||||
is_oral_text as _is_oral_text,
|
||||
is_outercourse_text as _is_outercourse_text,
|
||||
is_toy_assisted_double_text as _is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text as _is_vaginal_penetration_text,
|
||||
normalize_hardcore_detail_density as _normalize_hardcore_detail_density,
|
||||
position_context_text as _position_context_text,
|
||||
)
|
||||
from hardcore_text_cleanup import (
|
||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||
@@ -38,8 +62,6 @@ TRIGGER_CANDIDATES = (
|
||||
"sxcpinup_coloredpencil",
|
||||
"sxcppnl7",
|
||||
)
|
||||
HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"}
|
||||
|
||||
PROMPT_FIELD_LABELS = (
|
||||
"Ages",
|
||||
"Body types",
|
||||
@@ -82,11 +104,6 @@ def _expression_disabled(row: dict[str, Any]) -> bool:
|
||||
return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True))
|
||||
|
||||
|
||||
def _normalize_hardcore_detail_density(value: Any) -> str:
|
||||
text = _clean(value).lower()
|
||||
return text if text in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
|
||||
|
||||
|
||||
def _sentence(text: str) -> str:
|
||||
text = _clean(text).strip(" ,;")
|
||||
if not text:
|
||||
@@ -595,214 +612,6 @@ def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def _axis_values_text(axis_values: Any) -> str:
|
||||
if not isinstance(axis_values, dict):
|
||||
return ""
|
||||
priority = (
|
||||
"position",
|
||||
"body_position",
|
||||
"body_arrangement",
|
||||
"arrangement",
|
||||
"angle",
|
||||
"surface",
|
||||
"body_contact",
|
||||
"leg_detail",
|
||||
"oral_act",
|
||||
"oral_detail",
|
||||
"penetration_act",
|
||||
"penetration_detail",
|
||||
"anal_act",
|
||||
"double_act",
|
||||
"threesome_act",
|
||||
"group_act",
|
||||
)
|
||||
parts = [_clean(axis_values.get(key)) for key in priority if _clean(axis_values.get(key))]
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
||||
return " ".join(
|
||||
_clean(part).lower()
|
||||
for part in (role_graph, hard_item, composition, _axis_values_text(axis_values))
|
||||
if _clean(part)
|
||||
)
|
||||
|
||||
|
||||
def _is_outercourse_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"outercourse",
|
||||
"non-penetrative",
|
||||
"boobjob",
|
||||
"titjob",
|
||||
"breast sex",
|
||||
"breast-sex",
|
||||
"testicle",
|
||||
"balls",
|
||||
"balls licking",
|
||||
"balls-licking",
|
||||
"breasts tightly around",
|
||||
"breasts around",
|
||||
"penis licking",
|
||||
"penis-licking",
|
||||
"tongue along",
|
||||
"tongue runs along",
|
||||
"tongue running along",
|
||||
"handjob",
|
||||
"hand job",
|
||||
"hand wrapped",
|
||||
"hand stroking",
|
||||
"hand wraps around",
|
||||
"manual stimulation",
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"fingers in pussy",
|
||||
"hand on pussy",
|
||||
"fingers on pussy",
|
||||
"fingers sliding against the pussy",
|
||||
"open-thigh manual",
|
||||
"clit rubbing",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"mutual masturbation",
|
||||
"footjob",
|
||||
"soles wrap around",
|
||||
"soles",
|
||||
"toes curled",
|
||||
"feet stroking",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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_foreplay_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text:
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"foreplay",
|
||||
"pre-sex",
|
||||
"before sex",
|
||||
"before penetration",
|
||||
"kissing",
|
||||
"deep kiss",
|
||||
"mouth-to-mouth",
|
||||
"lips pressed",
|
||||
"caressing",
|
||||
"hands roaming",
|
||||
"stroking skin",
|
||||
"touching breasts",
|
||||
"cupping a breast",
|
||||
"hand on the cheek",
|
||||
"cheek and jaw",
|
||||
"fingers under the chin",
|
||||
"undressing",
|
||||
"removing clothing",
|
||||
"removing clothes",
|
||||
"pulling clothing",
|
||||
"sliding straps",
|
||||
"unbuttoning",
|
||||
"body worship",
|
||||
"nipple",
|
||||
"mouth on skin",
|
||||
"kissing down",
|
||||
"ass grabbing",
|
||||
"gripping the ass",
|
||||
"thigh kissing",
|
||||
"inner thighs",
|
||||
"hair held",
|
||||
"holding hair",
|
||||
"hair pulled back",
|
||||
"wrist",
|
||||
"wrists",
|
||||
"pinning",
|
||||
"guided",
|
||||
"guiding",
|
||||
"turning the body",
|
||||
"position transition",
|
||||
"pulling onto the bed",
|
||||
"lifting and spreading",
|
||||
"spreading thighs",
|
||||
"dirty talk",
|
||||
"whispering",
|
||||
"camera performance",
|
||||
"presented directly to the camera",
|
||||
"present her body",
|
||||
"showing to camera",
|
||||
"spread open for the camera",
|
||||
"watching partner",
|
||||
"waiting turn",
|
||||
"group coordination",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"wiping",
|
||||
"towel",
|
||||
"post-sex",
|
||||
"fingering",
|
||||
"fingers inside",
|
||||
"hand on pussy",
|
||||
"fingers on pussy",
|
||||
"clit rubbing",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"manual stimulation",
|
||||
"mutual masturbation",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _is_close_foreplay_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text or not _is_foreplay_text(text):
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"stand close",
|
||||
"stand face-to-face",
|
||||
"press their bodies",
|
||||
"bodies pressed close",
|
||||
"hips pressed close",
|
||||
"mouth-to-mouth",
|
||||
"deep kissing",
|
||||
"heated kiss",
|
||||
"hands pull clothing",
|
||||
"pull clothing aside",
|
||||
"clothing being removed",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _sanitize_foreplay_detail(detail: str, role_graph: str = "", composition: str = "") -> str:
|
||||
detail = _clean(detail)
|
||||
if not detail:
|
||||
@@ -832,57 +641,6 @@ def _sanitize_foreplay_detail(detail: str, role_graph: str = "", composition: st
|
||||
return _clean(detail)
|
||||
|
||||
|
||||
def _is_vaginal_penetration_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if not text or _is_outercourse_text(text) or _is_oral_text(text) or _is_foreplay_text(text):
|
||||
return False
|
||||
if any(term in text for term in ("anal", "double penetration", "double-penetration", "toy-assisted", "strap-on")):
|
||||
return False
|
||||
return any(
|
||||
term in text
|
||||
for term in (
|
||||
"vaginal penetration",
|
||||
"deep vaginal sex",
|
||||
"explicit penetrative sex",
|
||||
"penetrative sex",
|
||||
"penis entering pussy",
|
||||
"penis thrusts into her pussy",
|
||||
"penis thrusts into the woman",
|
||||
"pussy stretched around a penis",
|
||||
"hardcore vaginal thrusting",
|
||||
"full-body penetrative sex",
|
||||
"close-contact vaginal sex",
|
||||
"missionary position",
|
||||
"cowgirl position",
|
||||
"reverse cowgirl position",
|
||||
"doggy style position",
|
||||
"standing sex position",
|
||||
"spooning sex position",
|
||||
"edge-of-bed position",
|
||||
"kneeling straddle position",
|
||||
"lotus sex position",
|
||||
"bent-over position",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _is_toy_assisted_double_text(*parts: Any) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
if "toy" not in text:
|
||||
return False
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"double penetration",
|
||||
"double-penetration",
|
||||
"vaginal and anal penetration",
|
||||
"second penetration point",
|
||||
"second point of contact",
|
||||
"second contact",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _mentions_ass(text: str) -> bool:
|
||||
return bool(
|
||||
re.search(
|
||||
@@ -1729,28 +1487,6 @@ def _limit_detail_for_density(detail: str, density: str, is_climax: bool) -> str
|
||||
return _join_detail_clauses(clauses[:limit])
|
||||
|
||||
|
||||
def _is_climax_text(*parts: str) -> bool:
|
||||
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
||||
return any(
|
||||
token in text
|
||||
for token in (
|
||||
"cumshot",
|
||||
"ejaculation",
|
||||
"post-orgasm",
|
||||
"post-climax",
|
||||
"orgasm aftermath",
|
||||
"orgasm scene",
|
||||
"orgasm during",
|
||||
"shared climax",
|
||||
"hardcore climax",
|
||||
"external cumshot",
|
||||
"visible external ejaculation",
|
||||
"climaxes on",
|
||||
"climax lands",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) -> str:
|
||||
role_graph = _clean(role_graph).rstrip(".")
|
||||
text = " ".join(part.lower() for part in (role_graph, _clean(hard_item), _axis_values_text(axis_values)) if part)
|
||||
|
||||
Reference in New Issue
Block a user