Files
ComfyUI-Ethanfel-Prompt-Bui…/hardcore_position_config.py
T

522 lines
19 KiB
Python

from __future__ import annotations
import json
import re
from typing import Any
HARDCORE_POSITION_FAMILY_CHOICES = [
"any",
"penetrative",
"foreplay",
"interaction",
"manual",
"oral",
"outercourse",
"anal",
"climax",
"threesome",
"group",
]
HARDCORE_POSITION_FOCUS_CHOICES = [
"keep_pool",
"penetration_only",
"foreplay_only",
"interaction_only",
"manual_only",
"oral_only",
"outercourse_only",
"anal_only",
"climax_only",
"threesome_only",
"group_only",
]
HARDCORE_POSITION_KEY_CHOICES = [
"missionary",
"cowgirl",
"reverse_cowgirl",
"doggy",
"bent_over",
"face_down_ass_up",
"standing",
"side_lying",
"edge_supported",
"kneeling",
"lotus_lap",
"face_sitting",
"sixty_nine",
"reclining_oral",
"straddled_oral",
"spread_leg_oral",
"chair_oral",
"kissing",
"caressing",
"breast_touch",
"face_touch",
"undressing",
"body_worship",
"nipple_play",
"ass_grab",
"thigh_kissing",
"hair_holding",
"wrist_pinning",
"dirty_talk",
"position_transition",
"guided_positioning",
"camera_showing",
"watching",
"aftercare",
"cleanup",
"fingering",
"clit_rubbing",
"mutual_masturbation",
"boobjob",
"testicle_sucking",
"penis_licking",
"handjob",
"footjob",
"open_thighs",
"front_back",
]
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"any": [
"penetrative_sex",
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"manual_stimulation",
"oral_sex",
"outercourse_sex",
"anal_double_penetration",
"threesomes",
"group_coordination",
"group_sex_orgy",
"cumshot_climax",
"aftercare_cleanup",
],
"penetrative": ["penetrative_sex"],
"foreplay": ["foreplay_teasing"],
"interaction": [
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"group_coordination",
"aftercare_cleanup",
],
"manual": ["manual_stimulation"],
"oral": ["oral_sex"],
"outercourse": ["outercourse_sex", "manual_stimulation"],
"anal": ["anal_double_penetration"],
"climax": ["cumshot_climax"],
"threesome": ["threesomes"],
"group": ["group_sex_orgy"],
}
HARDCORE_POSITION_KEY_MATCHES = {
"missionary": ("missionary", "above her", "under her"),
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
"bent_over": ("bent-over", "bent over", "hips raised"),
"face_down_ass_up": ("face-down", "ass-up"),
"standing": ("standing", "stands", "braced standing"),
"side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
"edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
"kneeling": ("kneeling", "kneels", "kneeling center"),
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
"face_sitting": ("face-sitting", "face sitting"),
"sixty_nine": ("sixty-nine", "69"),
"reclining_oral": ("reclining cunnilingus",),
"straddled_oral": ("straddled oral",),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"chair_oral": ("chair oral",),
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
"handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
}
HARDCORE_POSITION_AXIS_KEYS = {
"position",
"body_position",
"body_arrangement",
"arrangement",
"tease_act",
"touch_detail",
"manual_act",
"manual_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
}
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
"penetrative_sex": "penetrative",
"foreplay_teasing": "foreplay",
"body_worship_touching": "interaction",
"clothing_position_transitions": "interaction",
"dominant_guidance": "interaction",
"camera_performance": "interaction",
"manual_stimulation": "manual",
"oral_sex": "oral",
"outercourse_sex": "outercourse",
"anal_double_penetration": "anal",
"threesomes": "threesome",
"group_coordination": "interaction",
"group_sex_orgy": "group",
"cumshot_climax": "climax",
"aftercare_cleanup": "interaction",
}
FOCUS_FAMILY_BY_KEY = {
"penetration_only": "penetrative",
"foreplay_only": "foreplay",
"interaction_only": "interaction",
"manual_only": "manual",
"oral_only": "oral",
"outercourse_only": "outercourse",
"anal_only": "anal",
"climax_only": "climax",
"threesome_only": "threesome",
"group_only": "group",
}
def _is_false(value: Any) -> bool:
if isinstance(value, bool):
return value is False
if isinstance(value, str):
return value.strip().lower() in ("false", "0", "no", "off")
return False
def _list_from(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def hardcore_position_family_choices() -> list[str]:
return list(HARDCORE_POSITION_FAMILY_CHOICES)
def hardcore_position_focus_choices() -> list[str]:
return list(HARDCORE_POSITION_FOCUS_CHOICES)
def hardcore_position_key_choices() -> list[str]:
return list(HARDCORE_POSITION_KEY_CHOICES)
def normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
text = str(value or default).strip()
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
def normalize_hardcore_position_values(values: Any) -> list[str]:
raw_values = _list_from(values)
selected: list[str] = []
for value in raw_values:
text = str(value or "").strip()
if not text or text == "any":
continue
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
selected.append(normalized)
return selected
def empty_hardcore_position_config() -> dict[str, Any]:
return {
"config_type": "hardcore_position",
"enabled": False,
"family": "any",
"positions": [],
"require_position": False,
"allow_toys": True,
"allow_double": True,
"allow_penetration": True,
"allow_foreplay": True,
"allow_interaction": True,
"allow_manual": True,
"allow_oral": True,
"allow_outercourse": True,
"allow_anal": True,
"allow_climax": True,
}
def parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return empty_hardcore_position_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return empty_hardcore_position_config()
if not isinstance(raw, dict):
return empty_hardcore_position_config()
parsed = {**empty_hardcore_position_config(), **raw}
parsed["enabled"] = bool(parsed.get("enabled", True))
parsed["family"] = normalize_hardcore_position_family(parsed.get("family"))
parsed["positions"] = normalize_hardcore_position_values(parsed.get("positions"))
parsed["require_position"] = not _is_false(parsed.get("require_position", False))
for key in (
"allow_toys",
"allow_double",
"allow_penetration",
"allow_foreplay",
"allow_interaction",
"allow_manual",
"allow_oral",
"allow_outercourse",
"allow_anal",
"allow_climax",
):
parsed[key] = not _is_false(parsed.get(key, True))
return parsed
def hardcore_position_summary(config: dict[str, Any]) -> str:
if not config.get("enabled"):
return "hardcore position unrestricted"
parts = [f"family={config.get('family', 'any')}"]
positions = config.get("positions") or []
if positions:
parts.append("positions=" + ",".join(positions))
elif config.get("require_position"):
parts.append("position_templates=required")
disabled = [
label
for key, label in (
("allow_toys", "toys"),
("allow_double", "double"),
("allow_penetration", "penetration"),
("allow_foreplay", "foreplay"),
("allow_interaction", "interaction"),
("allow_manual", "manual"),
("allow_oral", "oral"),
("allow_outercourse", "outercourse"),
("allow_anal", "anal"),
("allow_climax", "climax"),
)
if not config.get(key, True)
]
if disabled:
parts.append("blocked=" + ",".join(disabled))
return "; ".join(parts)
def build_hardcore_position_pool_json(
hardcore_position_config: str | dict[str, Any] | None = "",
combine_mode: str = "replace",
family: str = "any",
selected_positions: list[str] | tuple[str, ...] | str | None = None,
) -> str:
base = parse_hardcore_position_config(hardcore_position_config)
if combine_mode == "replace":
base = {**empty_hardcore_position_config(), "enabled": True}
else:
base["enabled"] = True
base["family"] = normalize_hardcore_position_family(family, base.get("family", "any"))
selected = normalize_hardcore_position_values(selected_positions)
if combine_mode == "add":
existing = list(base.get("positions") or [])
for value in selected:
if value not in existing:
existing.append(value)
base["positions"] = existing
else:
base["positions"] = selected
base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
base["summary"] = hardcore_position_summary(base)
return json.dumps(base, ensure_ascii=True, sort_keys=True)
def build_hardcore_action_filter_json(
hardcore_position_config: str | dict[str, Any] | None = "",
focus: str = "keep_pool",
allow_toys: bool = False,
allow_double: bool = False,
allow_penetration: bool = True,
allow_foreplay: bool = True,
allow_interaction: bool = True,
allow_manual: bool = True,
allow_oral: bool = True,
allow_outercourse: bool = True,
allow_anal: bool = True,
allow_climax: bool = True,
) -> str:
config = parse_hardcore_position_config(hardcore_position_config)
config["enabled"] = True
focus = str(focus or "keep_pool").strip()
focus_family = FOCUS_FAMILY_BY_KEY.get(focus)
if focus_family:
config["family"] = focus_family
config["allow_toys"] = bool(allow_toys)
config["allow_double"] = bool(allow_double)
config["allow_penetration"] = bool(allow_penetration)
config["allow_foreplay"] = bool(allow_foreplay)
config["allow_interaction"] = bool(allow_interaction)
config["allow_manual"] = bool(allow_manual)
config["allow_oral"] = bool(allow_oral)
config["allow_outercourse"] = bool(allow_outercourse)
config["allow_anal"] = bool(allow_anal)
config["allow_climax"] = bool(allow_climax)
if not focus_family and config["family"] != "any":
enabled_action_families = {
family
for enabled, family in (
(config["allow_penetration"], "penetrative"),
(config["allow_foreplay"], "foreplay"),
(config["allow_interaction"], "interaction"),
(config["allow_manual"], "manual"),
(config["allow_oral"], "oral"),
(config["allow_outercourse"], "outercourse"),
(config["allow_anal"], "anal"),
(config["allow_climax"], "climax"),
)
if enabled
}
if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
config["family"] = "any"
if focus == "foreplay_only":
config["allow_foreplay"] = True
config["allow_interaction"] = True
elif focus == "interaction_only":
config["allow_interaction"] = True
config["allow_foreplay"] = True
elif focus == "manual_only":
config["allow_manual"] = True
elif focus == "oral_only":
config["allow_oral"] = True
config["allow_penetration"] = False
elif focus == "outercourse_only":
config["allow_outercourse"] = True
config["allow_oral"] = False
config["allow_penetration"] = False
elif focus == "anal_only":
config["allow_anal"] = True
config["allow_penetration"] = True
elif focus == "climax_only":
config["allow_climax"] = True
config["summary"] = hardcore_position_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def hardcore_position_config_active(config: dict[str, Any]) -> bool:
return bool(config.get("enabled"))
def hardcore_position_template_required(config: dict[str, Any]) -> bool:
if not hardcore_position_config_active(config):
return False
return (
bool(config.get("require_position"))
or bool(config.get("positions"))
or normalize_hardcore_position_family(config.get("family")) != "any"
)
def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
family = normalize_hardcore_position_family(config.get("family"))
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
if not config.get("allow_penetration", True):
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
if not config.get("allow_foreplay", True):
allowed.discard("foreplay_teasing")
if not config.get("allow_interaction", True):
allowed.difference_update(
{
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"group_coordination",
"aftercare_cleanup",
}
)
if not config.get("allow_manual", True):
allowed.discard("manual_stimulation")
if not config.get("allow_oral", True):
allowed.discard("oral_sex")
if not config.get("allow_outercourse", True):
allowed.discard("outercourse_sex")
if not config.get("allow_anal", True):
allowed.discard("anal_double_penetration")
if not config.get("allow_climax", True):
allowed.discard("cumshot_climax")
if not config.get("allow_double", True) and family == "anal":
allowed.add("anal_double_penetration")
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
if family:
return family
config_family = normalize_hardcore_position_family((config or {}).get("family"), "")
return "" if config_family == "any" else config_family
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
if isinstance(axis_values, dict):
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
text = " ".join(text_parts).lower()
if not text:
return []
keys: list[str] = []
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
if any(token in text for token in tokens):
keys.append(key)
return keys
_normalize_hardcore_position_family = normalize_hardcore_position_family
_normalize_hardcore_position_values = normalize_hardcore_position_values
_empty_hardcore_position_config = empty_hardcore_position_config
_parse_hardcore_position_config = parse_hardcore_position_config
_hardcore_position_summary = hardcore_position_summary
_hardcore_position_config_active = hardcore_position_config_active
_hardcore_position_template_required = hardcore_position_template_required
_hardcore_allowed_subcategory_slugs = hardcore_allowed_subcategory_slugs
_hardcore_source_position_family = hardcore_source_position_family
_hardcore_position_keys = hardcore_position_keys