From 659a730169c35a3f85726177af202dfcd2542cd6 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 15:36:43 +0200 Subject: [PATCH] Extract Krea action context helpers --- docs/prompt-architecture-improvement-plan.md | 3 + docs/prompt-pool-routing-map.md | 2 + krea_action_context.py | 301 ++++++++++++++++++ krea_formatter.py | 312 ++----------------- 4 files changed, 330 insertions(+), 288 deletions(-) create mode 100644 krea_action_context.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 9368d1f..61e9d02 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -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: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index eee7dd7..3c86abf 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -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`. diff --git a/krea_action_context.py b/krea_action_context.py new file mode 100644 index 0000000..c9f5de1 --- /dev/null +++ b/krea_action_context.py @@ -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", + ) + ) diff --git a/krea_formatter.py b/krea_formatter.py index 30566b0..327df53 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -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)