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, )