Add hardcore position control nodes

This commit is contained in:
2026-06-25 01:05:58 +02:00
parent 91d5049774
commit 8ecb1a65c5
2 changed files with 550 additions and 1 deletions
+107
View File
@@ -21,6 +21,7 @@ SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG" SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE" SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE"
SXCP_INSTA_OF_OPTIONS = "SXCP_INSTA_OF_OPTIONS" SXCP_INSTA_OF_OPTIONS = "SXCP_INSTA_OF_OPTIONS"
SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT" SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE" SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
@@ -41,6 +42,8 @@ try:
build_filter_config_json, build_filter_config_json,
build_generation_profile_json, build_generation_profile_json,
build_hair_config_json, build_hair_config_json,
build_hardcore_action_filter_json,
build_hardcore_position_pool_json,
build_insta_of_options_json, build_insta_of_options_json,
build_insta_of_pair, build_insta_of_pair,
build_prompt, build_prompt,
@@ -81,6 +84,9 @@ try:
character_woman_body_choices, character_woman_body_choices,
ethnicity_choices, ethnicity_choices,
generation_profile_choices, generation_profile_choices,
hardcore_position_family_choices,
hardcore_position_focus_choices,
hardcore_position_key_choices,
hardcore_detail_density_choices, hardcore_detail_density_choices,
load_character_profile_json, load_character_profile_json,
save_character_profile_payload, save_character_profile_payload,
@@ -105,6 +111,8 @@ except ImportError:
build_filter_config_json, build_filter_config_json,
build_generation_profile_json, build_generation_profile_json,
build_hair_config_json, build_hair_config_json,
build_hardcore_action_filter_json,
build_hardcore_position_pool_json,
build_insta_of_options_json, build_insta_of_options_json,
build_insta_of_pair, build_insta_of_pair,
build_prompt, build_prompt,
@@ -145,6 +153,9 @@ except ImportError:
character_woman_body_choices, character_woman_body_choices,
ethnicity_choices, ethnicity_choices,
generation_profile_choices, generation_profile_choices,
hardcore_position_family_choices,
hardcore_position_focus_choices,
hardcore_position_key_choices,
hardcore_detail_density_choices, hardcore_detail_density_choices,
load_character_profile_json, load_character_profile_json,
save_character_profile_payload, save_character_profile_payload,
@@ -199,6 +210,7 @@ class SxCPPromptBuilder:
"camera_config": (SXCP_CAMERA_CONFIG,), "camera_config": (SXCP_CAMERA_CONFIG,),
"character_profile": (SXCP_CHARACTER_PROFILE,), "character_profile": (SXCP_CHARACTER_PROFILE,),
"character_cast": (SXCP_CHARACTER_CAST,), "character_cast": (SXCP_CHARACTER_CAST,),
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
"extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_positive": ("STRING", {"default": "", "multiline": True}),
"extra_negative": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}),
}, },
@@ -233,6 +245,7 @@ class SxCPPromptBuilder:
camera_config="", camera_config="",
character_profile="", character_profile="",
character_cast="", character_cast="",
hardcore_position_config="",
extra_positive="", extra_positive="",
extra_negative="", extra_negative="",
no_plus_women=False, no_plus_women=False,
@@ -266,6 +279,7 @@ class SxCPPromptBuilder:
camera_config=camera_config or "", camera_config=camera_config or "",
character_profile=character_profile or "", character_profile=character_profile or "",
character_cast=character_cast or "", character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
) )
return ( return (
row["prompt"], row["prompt"],
@@ -1077,6 +1091,89 @@ class SxCPCharacterClothing:
return config, json.loads(config).get("summary", "") return config, json.loads(config).get("summary", "")
class SxCPHardcorePositionPool:
@classmethod
def INPUT_TYPES(cls):
required = {
"combine_mode": (["replace", "add"], {"default": "replace"}),
"family": (hardcore_position_family_choices(), {"default": "any"}),
}
for choice in hardcore_position_key_choices():
required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False})
return {
"required": required,
"optional": {
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
},
}
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING")
RETURN_NAMES = ("hardcore_position_config", "summary")
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(self, combine_mode="replace", family="any", hardcore_position_config="", **kwargs):
selected = [
choice
for choice in hardcore_position_key_choices()
if bool(kwargs.get(_choice_input_key("include", choice), False))
]
config = build_hardcore_position_pool_json(
hardcore_position_config=hardcore_position_config or "",
combine_mode=combine_mode,
family=family,
selected_positions=selected,
)
return config, json.loads(config).get("summary", "")
class SxCPHardcoreActionFilter:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"focus": (hardcore_position_focus_choices(), {"default": "keep_pool"}),
"allow_toys": ("BOOLEAN", {"default": False}),
"allow_double": ("BOOLEAN", {"default": False}),
"allow_penetration": ("BOOLEAN", {"default": True}),
"allow_oral": ("BOOLEAN", {"default": True}),
"allow_anal": ("BOOLEAN", {"default": True}),
"allow_climax": ("BOOLEAN", {"default": True}),
},
"optional": {
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
},
}
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING")
RETURN_NAMES = ("hardcore_position_config", "summary")
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(
self,
focus,
allow_toys,
allow_double,
allow_penetration,
allow_oral,
allow_anal,
allow_climax,
hardcore_position_config="",
):
config = build_hardcore_action_filter_json(
hardcore_position_config=hardcore_position_config or "",
focus=focus,
allow_toys=allow_toys,
allow_double=allow_double,
allow_penetration=allow_penetration,
allow_oral=allow_oral,
allow_anal=allow_anal,
allow_climax=allow_climax,
)
return config, json.loads(config).get("summary", "")
class SxCPCharacterManualDetails: class SxCPCharacterManualDetails:
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
@@ -1150,6 +1247,7 @@ class SxCPPromptBuilderFromConfigs:
"camera_config": (SXCP_CAMERA_CONFIG,), "camera_config": (SXCP_CAMERA_CONFIG,),
"character_profile": (SXCP_CHARACTER_PROFILE,), "character_profile": (SXCP_CHARACTER_PROFILE,),
"character_cast": (SXCP_CHARACTER_CAST,), "character_cast": (SXCP_CHARACTER_CAST,),
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
"extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_positive": ("STRING", {"default": "", "multiline": True}),
"extra_negative": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}),
}, },
@@ -1174,6 +1272,7 @@ class SxCPPromptBuilderFromConfigs:
camera_config="", camera_config="",
character_profile="", character_profile="",
character_cast="", character_cast="",
hardcore_position_config="",
extra_positive="", extra_positive="",
extra_negative="", extra_negative="",
): ):
@@ -1189,6 +1288,7 @@ class SxCPPromptBuilderFromConfigs:
camera_config=camera_config or "", camera_config=camera_config or "",
character_profile=character_profile or "", character_profile=character_profile or "",
character_cast=character_cast or "", character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
extra_positive=extra_positive or "", extra_positive=extra_positive or "",
extra_negative=extra_negative or "", extra_negative=extra_negative or "",
) )
@@ -1818,6 +1918,7 @@ class SxCPInstaOFPromptPair:
"hardcore_camera_config": (SXCP_CAMERA_CONFIG,), "hardcore_camera_config": (SXCP_CAMERA_CONFIG,),
"character_profile": (SXCP_CHARACTER_PROFILE,), "character_profile": (SXCP_CHARACTER_PROFILE,),
"character_cast": (SXCP_CHARACTER_CAST,), "character_cast": (SXCP_CHARACTER_CAST,),
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
"extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_positive": ("STRING", {"default": "", "multiline": True}),
"extra_negative": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}),
}, },
@@ -1855,6 +1956,7 @@ class SxCPInstaOFPromptPair:
hardcore_camera_config="", hardcore_camera_config="",
character_profile="", character_profile="",
character_cast="", character_cast="",
hardcore_position_config="",
extra_positive="", extra_positive="",
extra_negative="", extra_negative="",
no_plus_women=False, no_plus_women=False,
@@ -1878,6 +1980,7 @@ class SxCPInstaOFPromptPair:
hardcore_camera_config=hardcore_camera_config or "", hardcore_camera_config=hardcore_camera_config or "",
character_profile=character_profile or "", character_profile=character_profile or "",
character_cast=character_cast or "", character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
extra_positive=extra_positive or "", extra_positive=extra_positive or "",
extra_negative=extra_negative or "", extra_negative=extra_negative or "",
) )
@@ -1914,6 +2017,8 @@ NODE_CLASS_MAPPINGS = {
"SxCPManBodyPool": SxCPManBodyPool, "SxCPManBodyPool": SxCPManBodyPool,
"SxCPEyeColorPool": SxCPEyeColorPool, "SxCPEyeColorPool": SxCPEyeColorPool,
"SxCPCharacterClothing": SxCPCharacterClothing, "SxCPCharacterClothing": SxCPCharacterClothing,
"SxCPHardcorePositionPool": SxCPHardcorePositionPool,
"SxCPHardcoreActionFilter": SxCPHardcoreActionFilter,
"SxCPCharacterManualDetails": SxCPCharacterManualDetails, "SxCPCharacterManualDetails": SxCPCharacterManualDetails,
"SxCPAdvancedFilters": SxCPAdvancedFilters, "SxCPAdvancedFilters": SxCPAdvancedFilters,
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs, "SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
@@ -1950,6 +2055,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SxCPManBodyPool": "SxCP Man Body Pool", "SxCPManBodyPool": "SxCP Man Body Pool",
"SxCPEyeColorPool": "SxCP Eye Color Pool", "SxCPEyeColorPool": "SxCP Eye Color Pool",
"SxCPCharacterClothing": "SxCP Character Clothing", "SxCPCharacterClothing": "SxCP Character Clothing",
"SxCPHardcorePositionPool": "SxCP Hardcore Position Pool",
"SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter",
"SxCPCharacterManualDetails": "SxCP Character Manual Details", "SxCPCharacterManualDetails": "SxCP Character Manual Details",
"SxCPAdvancedFilters": "SxCP Advanced Filters", "SxCPAdvancedFilters": "SxCP Advanced Filters",
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs", "SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
+443 -1
View File
@@ -295,6 +295,77 @@ CHARACTER_EYE_COLOR_CHOICES = [
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"] CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"] HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
HARDCORE_POSITION_FAMILY_CHOICES = [
"any",
"penetrative",
"oral",
"anal",
"climax",
"threesome",
"group",
]
HARDCORE_POSITION_FOCUS_CHOICES = [
"keep_pool",
"penetration_only",
"oral_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",
"spread_leg_oral",
"open_thighs",
"front_back",
]
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"any": [
"penetrative_sex",
"oral_sex",
"anal_double_penetration",
"threesomes",
"group_sex_orgy",
"cumshot_climax",
],
"penetrative": ["penetrative_sex"],
"oral": ["oral_sex"],
"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"),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"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"}
CAMERA_ORBIT_FRAMING_CHOICES = [ CAMERA_ORBIT_FRAMING_CHOICES = [
"from_zoom", "from_zoom",
"wide", "wide",
@@ -1517,6 +1588,319 @@ def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str
return parsed return parsed
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": [],
"allow_toys": True,
"allow_double": True,
"allow_penetration": True,
"allow_oral": 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"))
for key in ("allow_toys", "allow_double", "allow_penetration", "allow_oral", "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))
disabled = [
label
for key, label in (
("allow_toys", "toys"),
("allow_double", "double"),
("allow_penetration", "penetration"),
("allow_oral", "oral"),
("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["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_oral: 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 = {
"penetration_only": "penetrative",
"oral_only": "oral",
"anal_only": "anal",
"climax_only": "climax",
"threesome_only": "threesome",
"group_only": "group",
}.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_oral"] = bool(allow_oral)
config["allow_anal"] = bool(allow_anal)
config["allow_climax"] = bool(allow_climax)
if config["family"] == "oral":
config["allow_oral"] = True
config["allow_penetration"] = False
elif config["family"] == "anal":
config["allow_anal"] = True
config["allow_penetration"] = True
elif config["family"] == "climax":
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 _is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
return str(category.get("slug") or "").strip() == "hardcore_sexual_poses" or str(category.get("name") or "").strip().lower() == "hardcore sexual poses"
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_oral", True):
allowed.discard("oral_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 _filter_hardcore_categories_for_position(
categories: list[dict[str, Any]],
config: dict[str, Any],
women_count: int,
men_count: int,
) -> list[dict[str, Any]]:
if not _hardcore_position_config_active(config):
return categories
allowed = _hardcore_allowed_subcategory_slugs(config)
filtered_categories: list[dict[str, Any]] = []
for category in categories:
if not _is_hardcore_sexual_category(category):
filtered_categories.append(category)
continue
category_copy = dict(category)
subcategories = [
subcategory
for subcategory in category.get("subcategories", [])
if str(subcategory.get("slug") or "") in allowed and _compatible_entry(subcategory, women_count, men_count)
and _hardcore_subcategory_supports_positions(subcategory, config)
]
if subcategories:
category_copy["subcategories"] = subcategories
filtered_categories.append(category_copy)
return filtered_categories
def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str, Any]) -> bool:
text = str(text or "").lower()
axis_name = str(axis_name or "").lower()
if not config.get("allow_toys", True) and any(term in text for term in ("toy", "dildo", "strap-on", "strap on")):
return True
if not config.get("allow_double", True) and (
axis_name == "double_act"
or any(term in text for term in ("double penetration", "double-penetration", "front-and-back", "front and back", "second penetration", "both sides", "two partners penetrating", "multiple penetrations"))
):
return True
if not config.get("allow_anal", True) and (
axis_name == "anal_act"
or any(term in text for term in (" anal", "ass", "rear-entry anal"))
):
return True
if not config.get("allow_oral", True) and (
axis_name in ("oral_act", "oral_detail")
or any(term in text for term in ("oral sex", "mouth on genitals", "mouth on pussy", "blowjob", "cunnilingus", "tongue on pussy", "deepthroat", "fellatio"))
):
return True
if not config.get("allow_penetration", True) and (
axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
):
return True
if not config.get("allow_climax", True) and (
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
):
return True
return False
def _hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
positions = config.get("positions") or []
if not positions:
return True
text = _entry_text(entry).lower()
for position in positions:
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
return True
return False
def _hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bool:
selected = set(config.get("positions") or [])
if not selected:
return False
text = _entry_text(entry).lower()
matched = {
position
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
if any(term in text for term in terms)
}
return bool(matched) and not bool(matched & selected)
def _hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
if not config.get("positions"):
return True
axes = subcategory.get("item_axes")
if not isinstance(axes, dict):
return True
for axis_name, values in axes.items():
if str(axis_name) in HARDCORE_POSITION_AXIS_KEYS and any(
_hardcore_position_entry_matches(value, config)
for value in _list_from(values)
):
return True
return False
def _filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, Any]) -> list[Any]:
if not _hardcore_position_config_active(config):
return values
filtered = [
value
for value in values
if not _hardcore_text_blocked_by_action(_entry_text(value), axis_name, config)
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and _hardcore_position_entry_conflicts(value, config))
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or _hardcore_position_entry_matches(value, config))
]
return filtered or values
def _filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> list[Any]:
if not _hardcore_position_config_active(config):
return templates
filtered: list[Any] = []
for template in templates:
text = _entry_text(template)
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
blocked = bool(config.get("positions")) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
blocked = blocked or any(_hardcore_text_blocked_by_action(text, field, config) for field in fields | {""})
if not blocked:
filtered.append(template)
return filtered or templates
def _apply_hardcore_position_config_to_subcategory(
subcategory: dict[str, Any],
config: dict[str, Any],
) -> dict[str, Any]:
if not _hardcore_position_config_active(config):
return subcategory
subcategory_copy = dict(subcategory)
if "item_templates" in subcategory_copy:
subcategory_copy["item_templates"] = _filter_hardcore_templates(_list_from(subcategory_copy["item_templates"]), config)
raw_axes = subcategory_copy.get("item_axes")
if isinstance(raw_axes, dict):
axes = {}
for axis_name, values in raw_axes.items():
axes[axis_name] = _filter_hardcore_axis(str(axis_name), _list_from(values), config)
subcategory_copy["item_axes"] = axes
subcategory_copy["hardcore_position_config"] = config
return subcategory_copy
def _ratio_or_none(value: float) -> float | None: def _ratio_or_none(value: float) -> float | None:
try: try:
ratio = float(value) ratio = float(value)
@@ -1802,6 +2186,18 @@ def hardcore_detail_density_choices() -> list[str]:
return list(HARDCORE_DETAIL_DENSITY_CHOICES) return list(HARDCORE_DETAIL_DENSITY_CHOICES)
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 character_softcore_outfit_source_choices() -> list[str]: def character_softcore_outfit_source_choices() -> list[str]:
return [ return [
"no_change", "no_change",
@@ -4461,6 +4857,30 @@ def _role_graph(
return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her." return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her."
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and {man}'s penis thrusts into her." return f"{woman} lies on her back with thighs open while {man} kneels between her legs and {man}'s penis thrusts into her."
def anal_position_graph(woman: str, man: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if "bent-over" in text or "bent over" in text:
return f"{woman} is bent forward with hips raised while {man} stands behind her and thrusts his penis into her ass."
if "face-down" in text:
return f"{woman} lies face-down with ass raised while {man} is positioned behind her and thrusts his penis into her ass."
if "doggy" in text or "rear-entry" in text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
if "standing" in text:
return f"{woman} stands braced with hips angled back while {man} stands behind her and thrusts his penis into her ass."
if "spooning" in text or "side-lying" in text:
return f"{woman} lies on her side with thighs parted while {man} presses behind her and thrusts his penis into her ass."
if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text:
return f"{woman} lies near a raised edge with hips exposed while {man} kneels behind her and thrusts his penis into her ass."
if "kneeling" in text:
return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass."
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
if people_count == 1: if people_count == 1:
solo = people[0] solo = people[0]
if women_count == 1: if women_count == 1:
@@ -4546,7 +4966,7 @@ def _role_graph(
elif people_count >= 3: elif people_count >= 3:
graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front." graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front."
else: else:
graph = f"{man} thrusts his penis into {woman}'s ass while keeping her hips held open." graph = anal_position_graph(woman, man)
elif "threesome" in slug: elif "threesome" in slug:
graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body." graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body."
elif "group" in slug or "orgy" in slug: elif "group" in slug or "orgy" in slug:
@@ -4871,6 +5291,7 @@ def _build_custom_row(
character_profile: str | dict[str, Any] | None = None, character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_phase: str = "", expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
categories = load_category_library() categories = load_category_library()
category_rng = _axis_rng(seed_config, "category", seed, row_number) category_rng = _axis_rng(seed_config, "category", seed, row_number)
@@ -4881,9 +5302,16 @@ def _build_custom_row(
role_rng = _axis_rng(seed_config, "role", seed, row_number) role_rng = _axis_rng(seed_config, "role", seed, row_number)
expression_rng = _axis_rng(seed_config, "expression", seed, row_number) expression_rng = _axis_rng(seed_config, "expression", seed, row_number)
composition_rng = _axis_rng(seed_config, "composition", seed, row_number) composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
parsed_hardcore_position_config = _parse_hardcore_position_config(hardcore_position_config)
requested_women_count = women_count requested_women_count = women_count
requested_men_count = men_count requested_men_count = men_count
categories = _filter_hardcore_categories_for_position(
categories,
parsed_hardcore_position_config,
women_count,
men_count,
)
category, subcategory, women_count, men_count = _find_subcategory( category, subcategory, women_count, men_count = _find_subcategory(
categories, categories,
category_choice, category_choice,
@@ -4901,6 +5329,8 @@ def _build_custom_row(
"effective_women_count": women_count, "effective_women_count": women_count,
"effective_men_count": men_count, "effective_men_count": men_count,
} }
if _is_hardcore_sexual_category(category):
subcategory = _apply_hardcore_position_config_to_subcategory(subcategory, parsed_hardcore_position_config)
content_axis = "pose" if _is_pose_content_category(category, subcategory) else "content" content_axis = "pose" if _is_pose_content_category(category, subcategory) else "content"
content_rng = _axis_rng(seed_config, content_axis, seed, row_number) content_rng = _axis_rng(seed_config, content_axis, seed, row_number)
items = _list_from(subcategory.get("items", [subcategory["name"]])) items = _list_from(subcategory.get("items", [subcategory["name"]]))
@@ -5133,6 +5563,11 @@ def _build_custom_row(
"scene_text": scene, "scene_text": scene,
"pose": pose, "pose": pose,
"seed_config": seed_config, "seed_config": seed_config,
"hardcore_position_config": (
parsed_hardcore_position_config
if _hardcore_position_config_active(parsed_hardcore_position_config)
else {}
),
"content_seed_axis": content_axis, "content_seed_axis": content_axis,
"role_graph": role_graph, "role_graph": role_graph,
"source_role_graph": source_role_graph, "source_role_graph": source_role_graph,
@@ -5197,6 +5632,7 @@ def build_prompt(
character_cast: str | dict[str, Any] | list[Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_enabled: bool = True, expression_enabled: bool = True,
expression_phase: str = "", expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
apply_pool_extensions() apply_pool_extensions()
row_number = max(1, int(row_number)) row_number = max(1, int(row_number))
@@ -5265,6 +5701,7 @@ def build_prompt(
character_profile, character_profile,
character_cast, character_cast,
expression_phase, expression_phase,
hardcore_position_config,
) )
if not expression_enabled: if not expression_enabled:
@@ -5293,6 +5730,7 @@ def build_prompt_from_configs(
camera_config: str | dict[str, Any] | None = "", camera_config: str | dict[str, Any] | None = "",
character_profile: str | dict[str, Any] | None = "", character_profile: str | dict[str, Any] | None = "",
character_cast: str | dict[str, Any] | list[Any] | None = "", character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
extra_positive: str = "", extra_positive: str = "",
extra_negative: str = "", extra_negative: str = "",
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -5327,6 +5765,7 @@ def build_prompt_from_configs(
camera_config=camera_config or "", camera_config=camera_config or "",
character_profile=character_profile or "", character_profile=character_profile or "",
character_cast=character_cast or "", character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
) )
@@ -5837,6 +6276,7 @@ def build_insta_of_pair(
hardcore_camera_config: str | dict[str, Any] | None = None, hardcore_camera_config: str | dict[str, Any] | None = None,
character_profile: str | dict[str, Any] | None = "", character_profile: str | dict[str, Any] | None = "",
character_cast: str | dict[str, Any] | list[Any] | None = "", character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
extra_positive: str = "", extra_positive: str = "",
extra_negative: str = "", extra_negative: str = "",
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -5970,6 +6410,7 @@ def build_insta_of_pair(
expression_intensity=options["hardcore_expression_intensity"], expression_intensity=options["hardcore_expression_intensity"],
character_cast=character_cast or "", character_cast=character_cast or "",
expression_phase="hardcore", expression_phase="hardcore",
hardcore_position_config=hardcore_position_config or "",
) )
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"] hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
hard_row["pov_character_labels"] = pov_character_labels hard_row["pov_character_labels"] = pov_character_labels
@@ -6154,6 +6595,7 @@ def build_insta_of_pair(
"character_hardcore_clothing": character_hardcore_clothing_entries, "character_hardcore_clothing": character_hardcore_clothing_entries,
"hardcore_clothing_state": hard_clothing_state, "hardcore_clothing_state": hard_clothing_state,
"hardcore_detail_density": hard_detail_density, "hardcore_detail_density": hard_detail_density,
"hardcore_position_config": hard_row.get("hardcore_position_config", {}),
"softcore_prompt": soft_prompt, "softcore_prompt": soft_prompt,
"hardcore_prompt": hard_prompt, "hardcore_prompt": hard_prompt,
"softcore_negative_prompt": soft_negative, "softcore_negative_prompt": soft_negative,