Extract Krea action context helpers

This commit is contained in:
2026-06-26 15:36:43 +02:00
parent 031223255d
commit 659a730169
4 changed files with 330 additions and 288 deletions
@@ -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:
+2
View File
@@ -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`.
+301
View File
@@ -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
View File
@@ -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)