From 0e49aed8aca4c59d6616211e4741f9108065ce2f Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 16:26:28 +0200 Subject: [PATCH] Extract Krea action family dispatch --- docs/prompt-architecture-improvement-plan.md | 17 +- docs/prompt-pool-routing-map.md | 11 +- krea_action_dispatch.py | 230 +++++++++++++++++++ krea_actions.py | 141 +----------- 4 files changed, 254 insertions(+), 145 deletions(-) create mode 100644 krea_action_dispatch.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 2aa1a21..8212770 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -158,14 +158,16 @@ Already isolated: outercourse, oral, penetration, toy/double-contact, and anchor dedupe paths. - `krea_action_climax.py` owns climax-specific role/detail cleanup and aftermath view dedupe. -- `krea_actions.py` owns non-POV hardcore action sentence dispatch. +- `krea_action_dispatch.py` owns non-POV role normalization, action-family + classification, and family-specific detail cleanup. +- `krea_actions.py` owns final non-POV hardcore action sentence assembly. - `krea_pov_actions.py` owns POV hardcore action sentence rewriting and first-person body geometry. Improve later: -- make `krea_actions.hardcore_action_sentence` dispatch by action family instead - of long conditional chains; +- add metadata fields such as `action_family` / `position_family` to reduce + keyword guessing in hardcore formatter dispatch; - add route-level smoke fixtures for representative metadata rows; ### SDXL Formatter Path @@ -344,10 +346,9 @@ Medium-term: ## Recommended Next Passes -1. Split `krea_actions.hardcore_action_sentence` into action-family dispatch - helpers, using `krea_cast.py` as the pattern for stable import aliases and - smoke coverage. +1. Add metadata fields such as `action_family` / `position_family` to reduce + keyword guessing in hardcore filters and formatter dispatch. 2. Split `__init__.py` node classes by family after behavior is covered by smoke checks. -3. Add metadata fields such as `action_family` / `position_family` to reduce - keyword guessing in hardcore filters and formatter dispatch. +3. Add route-level smoke fixtures for representative Krea/SDXL/caption metadata + rows. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 2f5c84e..0f33897 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -279,7 +279,8 @@ Edit targets: - Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`. - Krea2 non-climax item/detail cleanup: `krea_action_details.py`. - Krea2 climax role/detail cleanup: `krea_action_climax.py`. -- Krea2 non-POV action rewrite: `krea_actions.py`. +- Krea2 non-POV action-family routing: `krea_action_dispatch.py`. +- Krea2 non-POV action sentence assembly: `krea_actions.py`. - Krea2 POV position rewrite: `krea_pov_actions.py`. ### Composition @@ -474,7 +475,9 @@ What each part owns: details. - `krea_action_climax.py`: rewrites climax role graphs and dedupes aftermath detail/view clauses. -- `krea_actions.py`: dispatches non-POV hardcore action sentence rewriting. +- `krea_action_dispatch.py`: normalizes non-POV role graphs, classifies action + families, and applies the matching detail cleanup. +- `krea_actions.py`: assembles the final non-POV hardcore action sentence. - `krea_pov_actions.py`: rewrites POV variants with first-person geometry. Current broad hardcore families: @@ -565,6 +568,7 @@ Key Krea2 ownership: - Non-POV pose anchors and arrangements: `krea_action_positions.py`. - Non-climax item/detail cleanup: `krea_action_details.py`. - Climax role/detail cleanup: `krea_action_climax.py`. +- Non-POV action-family routing: `krea_action_dispatch.py`. - Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`. - POV labels, filtering, and camera/composition support: `krea_pov.py`. - Detail clause splitting and density limiting: `krea_detail.py`. @@ -756,7 +760,8 @@ Use these traces to narrow a problem in one pass. `krea_action_context.py` family predicates first, then `krea_action_positions.py` pose anchors/arrangements, `krea_action_details.py` item/detail cleanup, `krea_action_climax.py` - climax cleanup, and `krea_actions.py` action sentence dispatch. + climax cleanup, `krea_action_dispatch.py` family routing, and + `krea_actions.py` action sentence assembly. ### POV position is spatially wrong diff --git a/krea_action_dispatch.py b/krea_action_dispatch.py new file mode 100644 index 0000000..db6642b --- /dev/null +++ b/krea_action_dispatch.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any + +try: + from .krea_action_context import ( + axis_values_text, + is_climax_text, + is_foreplay_text, + is_oral_text, + is_outercourse_text, + is_toy_assisted_double_text, + is_vaginal_penetration_text, + normalize_hardcore_detail_density, + ) + from .krea_detail import limit_detail_for_density + from .krea_action_positions import hardcore_pose_anchor + from .krea_action_details import ( + dedupe_anchor_detail, + dedupe_oral_detail, + dedupe_outercourse_detail, + dedupe_penetration_detail, + dedupe_toy_double_detail, + hardcore_item_detail, + sanitize_foreplay_detail, + ) + from .krea_action_climax import climax_role_graph, dedupe_climax_detail +except ImportError: # Allows local smoke tests with `python -c`. + from krea_action_context import ( + axis_values_text, + is_climax_text, + is_foreplay_text, + is_oral_text, + is_outercourse_text, + is_toy_assisted_double_text, + is_vaginal_penetration_text, + normalize_hardcore_detail_density, + ) + from krea_detail import limit_detail_for_density + from krea_action_positions import hardcore_pose_anchor + from krea_action_details import ( + dedupe_anchor_detail, + dedupe_oral_detail, + dedupe_outercourse_detail, + dedupe_penetration_detail, + dedupe_toy_double_detail, + hardcore_item_detail, + sanitize_foreplay_detail, + ) + from krea_action_climax import climax_role_graph, dedupe_climax_detail + + +ACTION_CLIMAX = "climax" +ACTION_FOREPLAY = "foreplay" +ACTION_OUTERCOURSE = "outercourse" +ACTION_ORAL = "oral" +ACTION_PENETRATION = "penetration" +ACTION_TOY_DOUBLE = "toy_double" +ACTION_DEFAULT = "default" + + +@dataclass(frozen=True) +class HardcoreActionParts: + family: str + role_graph: str + hard_item: str + detail: str + anchor: str + detail_density: str + + +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_role_graph(role_graph: str) -> str: + role_graph = _clean(role_graph).rstrip(".") + replacements = ( + ( + r"\bthe man penetrates the woman while a toy adds a second point of contact\b", + "the man's penis thrusts into the woman while a toy is positioned at the second penetration point", + ), + ( + r"\bthe man thrusts his penis into the woman while a toy adds a second penetration point\b", + "the man's penis thrusts into the woman while a toy is positioned at the second penetration point", + ), + ( + r"\bthe man thrusts his penis into the woman\b", + "the man's penis thrusts into the woman", + ), + ( + r"\bthe man penetrates the woman anally\b", + "the man's penis thrusts into the woman's ass", + ), + ( + r"\bthe man thrusts his penis into the woman's ass\b", + "the man's penis thrusts into the woman's ass", + ), + ( + r"\bthe man penetrates the woman\b", + "the man's penis thrusts into the woman", + ), + ( + r"\bthe woman and the man are in mutual oral contact with mouth-to-genital contact visible\b", + "the woman has the man's penis in her mouth while the man uses his mouth on her pussy", + ), + ( + r"\bthe woman gives oral to the man\b", + "the woman takes the man's penis in her mouth", + ), + ) + for pattern, replacement in replacements: + role_graph = re.sub(pattern, replacement, role_graph, flags=re.IGNORECASE) + return role_graph + + +def normalize_toy_double_role_graph(role_graph: str) -> str: + return re.sub( + r"\s+while a toy adds (?:the|a) second penetration point\b", + " while a toy is positioned at the second penetration point", + role_graph, + flags=re.IGNORECASE, + ) + + +def hardcore_action_family( + role_graph: str, + hard_item: str, + composition: str = "", + axis_values: Any = None, + *, + is_climax: bool | None = None, +) -> str: + axis_text = axis_values_text(axis_values) + if is_climax is None: + is_climax = is_climax_text(role_graph, hard_item, composition, axis_text) + if is_climax: + return ACTION_CLIMAX + if is_foreplay_text(role_graph, hard_item, composition, axis_text): + return ACTION_FOREPLAY + if is_outercourse_text(role_graph, hard_item, composition, axis_text): + return ACTION_OUTERCOURSE + if is_oral_text(role_graph, hard_item, composition, axis_text): + return ACTION_ORAL + if is_vaginal_penetration_text(role_graph, hard_item, composition, axis_text): + return ACTION_PENETRATION + if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text): + return ACTION_TOY_DOUBLE + return ACTION_DEFAULT + + +def action_detail_for_family( + family: str, + detail: str, + role_graph: str, + hard_item: str, + composition: str = "", + axis_values: Any = None, + *, + anchor: str = "", + detail_density: str = "balanced", +) -> tuple[str, str]: + if family == ACTION_CLIMAX: + return "", dedupe_climax_detail(detail, role_graph, detail_density) + if family == ACTION_FOREPLAY: + detail = sanitize_foreplay_detail(detail, role_graph, composition) + return "", limit_detail_for_density(detail, detail_density, False) + if family == ACTION_OUTERCOURSE: + detail = dedupe_outercourse_detail(detail, role_graph, hard_item, axis_values) + return "", limit_detail_for_density(detail, detail_density, False) + if family == ACTION_ORAL and role_graph: + detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values) + return "", limit_detail_for_density(detail, detail_density, False) + if family == ACTION_PENETRATION and role_graph: + detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values) + return "", limit_detail_for_density(detail, detail_density, False) + + if anchor: + detail = dedupe_anchor_detail(detail, anchor) + if family == ACTION_TOY_DOUBLE: + detail = dedupe_toy_double_detail(detail) + return anchor, limit_detail_for_density(detail, detail_density, False) + + +def resolve_hardcore_action_parts( + role_graph: str, + hard_item: str, + composition: str = "", + axis_values: Any = None, + detail_density: str = "balanced", +) -> HardcoreActionParts: + detail_density = normalize_hardcore_detail_density(detail_density) + role_graph = normalize_hardcore_role_graph(role_graph) + hard_item = _clean(hard_item).rstrip(".") + axis_text = axis_values_text(axis_values) + is_climax = is_climax_text(role_graph, hard_item, composition, axis_text) + if is_climax: + role_graph = climax_role_graph(role_graph, hard_item, axis_values) + + detail = hardcore_item_detail(hard_item) + anchor = hardcore_pose_anchor(role_graph, hard_item, composition, axis_values) + family = hardcore_action_family(role_graph, hard_item, composition, axis_values, is_climax=is_climax) + + if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text): + role_graph = normalize_toy_double_role_graph(role_graph) + + anchor, detail = action_detail_for_family( + family, + detail, + role_graph, + hard_item, + composition, + axis_values, + anchor=anchor, + detail_density=detail_density, + ) + return HardcoreActionParts( + family=family, + role_graph=role_graph, + hard_item=hard_item, + detail=detail, + anchor=anchor, + detail_density=detail_density, + ) diff --git a/krea_actions.py b/krea_actions.py index b685ec5..ab80cd1 100644 --- a/krea_actions.py +++ b/krea_actions.py @@ -4,59 +4,17 @@ import re from typing import Any try: - from .krea_action_context import ( - axis_values_text, - is_climax_text, - is_foreplay_text, - is_oral_text, - is_outercourse_text, - is_toy_assisted_double_text, - is_vaginal_penetration_text, - normalize_hardcore_detail_density, - ) - from .krea_detail import limit_detail_for_density from .krea_action_positions import ( arrangement_duplicates_role, - hardcore_pose_anchor, hardcore_pose_arrangement, ) - from .krea_action_details import ( - dedupe_anchor_detail, - dedupe_oral_detail, - dedupe_outercourse_detail, - dedupe_penetration_detail, - dedupe_toy_double_detail, - hardcore_item_detail, - sanitize_foreplay_detail, - ) - from .krea_action_climax import climax_role_graph, dedupe_climax_detail + from .krea_action_dispatch import resolve_hardcore_action_parts except ImportError: # Allows local smoke tests with `python -c`. - from krea_action_context import ( - axis_values_text, - is_climax_text, - is_foreplay_text, - is_oral_text, - is_outercourse_text, - is_toy_assisted_double_text, - is_vaginal_penetration_text, - normalize_hardcore_detail_density, - ) - from krea_detail import limit_detail_for_density from krea_action_positions import ( arrangement_duplicates_role, - hardcore_pose_anchor, hardcore_pose_arrangement, ) - from krea_action_details import ( - dedupe_anchor_detail, - dedupe_oral_detail, - dedupe_outercourse_detail, - dedupe_penetration_detail, - dedupe_toy_double_detail, - hardcore_item_detail, - sanitize_foreplay_detail, - ) - from krea_action_climax import climax_role_graph, dedupe_climax_detail + from krea_action_dispatch import resolve_hardcore_action_parts def _clean(value: Any) -> str: @@ -87,96 +45,11 @@ def hardcore_action_sentence( axis_values: Any = None, detail_density: str = "balanced", ) -> str: - detail_density = normalize_hardcore_detail_density(detail_density) - role_graph = _clean(role_graph).rstrip(".") - hard_item = _clean(hard_item).rstrip(".") - role_graph = re.sub( - r"\bthe man penetrates the woman while a toy adds a second point of contact\b", - "the man's penis thrusts into the woman while a toy is positioned at the second penetration point", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe man thrusts his penis into the woman while a toy adds a second penetration point\b", - "the man's penis thrusts into the woman while a toy is positioned at the second penetration point", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe man thrusts his penis into the woman\b", - "the man's penis thrusts into the woman", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe man penetrates the woman anally\b", - "the man's penis thrusts into the woman's ass", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe man thrusts his penis into the woman's ass\b", - "the man's penis thrusts into the woman's ass", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe man penetrates the woman\b", - "the man's penis thrusts into the woman", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe woman and the man are in mutual oral contact with mouth-to-genital contact visible\b", - "the woman has the man's penis in her mouth while the man uses his mouth on her pussy", - role_graph, - flags=re.IGNORECASE, - ) - role_graph = re.sub( - r"\bthe woman gives oral to the man\b", - "the woman takes the man's penis in her mouth", - role_graph, - flags=re.IGNORECASE, - ) - is_climax = is_climax_text(role_graph, hard_item, composition, axis_values_text(axis_values)) - if is_climax: - role_graph = climax_role_graph(role_graph, hard_item, axis_values) - detail = hardcore_item_detail(hard_item) - 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_oral = is_oral_text(role_graph, hard_item, composition, axis_values_text(axis_values)) - is_penetrative = is_vaginal_penetration_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( - r"\s+while a toy adds (?:the|a) second penetration point\b", - " while a toy is positioned at the second penetration point", - role_graph, - flags=re.IGNORECASE, - ) - if is_climax: - anchor = "" - detail = dedupe_climax_detail(detail, role_graph, detail_density) - elif is_foreplay_text(role_graph, hard_item, composition, axis_values_text(axis_values)): - anchor = "" - detail = sanitize_foreplay_detail(detail, role_graph, composition) - detail = limit_detail_for_density(detail, detail_density, False) - elif is_outercourse: - anchor = "" - detail = dedupe_outercourse_detail(detail, role_graph, hard_item, axis_values) - 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) - elif is_penetrative and role_graph: - anchor = "" - detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values) - detail = limit_detail_for_density(detail, detail_density, False) - else: - detail = dedupe_anchor_detail(detail, anchor) if anchor else detail - if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)): - detail = dedupe_toy_double_detail(detail) - detail = limit_detail_for_density(detail, detail_density, False) + parts = resolve_hardcore_action_parts(role_graph, hard_item, composition, axis_values, detail_density) + role_graph = parts.role_graph + hard_item = parts.hard_item + detail = parts.detail + anchor = parts.anchor arrangement = hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values) anchor_phrase = _with_indefinite_article(anchor) if anchor else "" if arrangement and anchor_phrase and not arrangement_duplicates_role(arrangement, role_graph):