Add hardcore position control nodes
This commit is contained in:
+107
@@ -21,6 +21,7 @@ SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
|
||||
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
|
||||
SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE"
|
||||
SXCP_INSTA_OF_OPTIONS = "SXCP_INSTA_OF_OPTIONS"
|
||||
SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
|
||||
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
|
||||
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
|
||||
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
|
||||
@@ -41,6 +42,8 @@ try:
|
||||
build_filter_config_json,
|
||||
build_generation_profile_json,
|
||||
build_hair_config_json,
|
||||
build_hardcore_action_filter_json,
|
||||
build_hardcore_position_pool_json,
|
||||
build_insta_of_options_json,
|
||||
build_insta_of_pair,
|
||||
build_prompt,
|
||||
@@ -81,6 +84,9 @@ try:
|
||||
character_woman_body_choices,
|
||||
ethnicity_choices,
|
||||
generation_profile_choices,
|
||||
hardcore_position_family_choices,
|
||||
hardcore_position_focus_choices,
|
||||
hardcore_position_key_choices,
|
||||
hardcore_detail_density_choices,
|
||||
load_character_profile_json,
|
||||
save_character_profile_payload,
|
||||
@@ -105,6 +111,8 @@ except ImportError:
|
||||
build_filter_config_json,
|
||||
build_generation_profile_json,
|
||||
build_hair_config_json,
|
||||
build_hardcore_action_filter_json,
|
||||
build_hardcore_position_pool_json,
|
||||
build_insta_of_options_json,
|
||||
build_insta_of_pair,
|
||||
build_prompt,
|
||||
@@ -145,6 +153,9 @@ except ImportError:
|
||||
character_woman_body_choices,
|
||||
ethnicity_choices,
|
||||
generation_profile_choices,
|
||||
hardcore_position_family_choices,
|
||||
hardcore_position_focus_choices,
|
||||
hardcore_position_key_choices,
|
||||
hardcore_detail_density_choices,
|
||||
load_character_profile_json,
|
||||
save_character_profile_payload,
|
||||
@@ -199,6 +210,7 @@ class SxCPPromptBuilder:
|
||||
"camera_config": (SXCP_CAMERA_CONFIG,),
|
||||
"character_profile": (SXCP_CHARACTER_PROFILE,),
|
||||
"character_cast": (SXCP_CHARACTER_CAST,),
|
||||
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
|
||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
@@ -233,6 +245,7 @@ class SxCPPromptBuilder:
|
||||
camera_config="",
|
||||
character_profile="",
|
||||
character_cast="",
|
||||
hardcore_position_config="",
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
no_plus_women=False,
|
||||
@@ -266,6 +279,7 @@ class SxCPPromptBuilder:
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
hardcore_position_config=hardcore_position_config or "",
|
||||
)
|
||||
return (
|
||||
row["prompt"],
|
||||
@@ -1077,6 +1091,89 @@ class SxCPCharacterClothing:
|
||||
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:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -1150,6 +1247,7 @@ class SxCPPromptBuilderFromConfigs:
|
||||
"camera_config": (SXCP_CAMERA_CONFIG,),
|
||||
"character_profile": (SXCP_CHARACTER_PROFILE,),
|
||||
"character_cast": (SXCP_CHARACTER_CAST,),
|
||||
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
|
||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
@@ -1174,6 +1272,7 @@ class SxCPPromptBuilderFromConfigs:
|
||||
camera_config="",
|
||||
character_profile="",
|
||||
character_cast="",
|
||||
hardcore_position_config="",
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
):
|
||||
@@ -1189,6 +1288,7 @@ class SxCPPromptBuilderFromConfigs:
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
hardcore_position_config=hardcore_position_config or "",
|
||||
extra_positive=extra_positive or "",
|
||||
extra_negative=extra_negative or "",
|
||||
)
|
||||
@@ -1818,6 +1918,7 @@ class SxCPInstaOFPromptPair:
|
||||
"hardcore_camera_config": (SXCP_CAMERA_CONFIG,),
|
||||
"character_profile": (SXCP_CHARACTER_PROFILE,),
|
||||
"character_cast": (SXCP_CHARACTER_CAST,),
|
||||
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
|
||||
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
@@ -1855,6 +1956,7 @@ class SxCPInstaOFPromptPair:
|
||||
hardcore_camera_config="",
|
||||
character_profile="",
|
||||
character_cast="",
|
||||
hardcore_position_config="",
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
no_plus_women=False,
|
||||
@@ -1878,6 +1980,7 @@ class SxCPInstaOFPromptPair:
|
||||
hardcore_camera_config=hardcore_camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
character_cast=character_cast or "",
|
||||
hardcore_position_config=hardcore_position_config or "",
|
||||
extra_positive=extra_positive or "",
|
||||
extra_negative=extra_negative or "",
|
||||
)
|
||||
@@ -1914,6 +2017,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SxCPManBodyPool": SxCPManBodyPool,
|
||||
"SxCPEyeColorPool": SxCPEyeColorPool,
|
||||
"SxCPCharacterClothing": SxCPCharacterClothing,
|
||||
"SxCPHardcorePositionPool": SxCPHardcorePositionPool,
|
||||
"SxCPHardcoreActionFilter": SxCPHardcoreActionFilter,
|
||||
"SxCPCharacterManualDetails": SxCPCharacterManualDetails,
|
||||
"SxCPAdvancedFilters": SxCPAdvancedFilters,
|
||||
"SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs,
|
||||
@@ -1950,6 +2055,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SxCPManBodyPool": "SxCP Man Body Pool",
|
||||
"SxCPEyeColorPool": "SxCP Eye Color Pool",
|
||||
"SxCPCharacterClothing": "SxCP Character Clothing",
|
||||
"SxCPHardcorePositionPool": "SxCP Hardcore Position Pool",
|
||||
"SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter",
|
||||
"SxCPCharacterManualDetails": "SxCP Character Manual Details",
|
||||
"SxCPAdvancedFilters": "SxCP Advanced Filters",
|
||||
"SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs",
|
||||
|
||||
+443
-1
@@ -295,6 +295,77 @@ CHARACTER_EYE_COLOR_CHOICES = [
|
||||
|
||||
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||
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 = [
|
||||
"from_zoom",
|
||||
"wide",
|
||||
@@ -1517,6 +1588,319 @@ def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str
|
||||
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:
|
||||
try:
|
||||
ratio = float(value)
|
||||
@@ -1802,6 +2186,18 @@ def hardcore_detail_density_choices() -> list[str]:
|
||||
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]:
|
||||
return [
|
||||
"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} 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:
|
||||
solo = people[0]
|
||||
if women_count == 1:
|
||||
@@ -4546,7 +4966,7 @@ def _role_graph(
|
||||
elif people_count >= 3:
|
||||
graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front."
|
||||
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:
|
||||
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:
|
||||
@@ -4871,6 +5291,7 @@ def _build_custom_row(
|
||||
character_profile: str | dict[str, Any] | None = None,
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = None,
|
||||
expression_phase: str = "",
|
||||
hardcore_position_config: str | dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
categories = load_category_library()
|
||||
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)
|
||||
expression_rng = _axis_rng(seed_config, "expression", 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_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(
|
||||
categories,
|
||||
category_choice,
|
||||
@@ -4901,6 +5329,8 @@ def _build_custom_row(
|
||||
"effective_women_count": women_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_rng = _axis_rng(seed_config, content_axis, seed, row_number)
|
||||
items = _list_from(subcategory.get("items", [subcategory["name"]]))
|
||||
@@ -5133,6 +5563,11 @@ def _build_custom_row(
|
||||
"scene_text": scene,
|
||||
"pose": pose,
|
||||
"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,
|
||||
"role_graph": 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,
|
||||
expression_enabled: bool = True,
|
||||
expression_phase: str = "",
|
||||
hardcore_position_config: str | dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
apply_pool_extensions()
|
||||
row_number = max(1, int(row_number))
|
||||
@@ -5265,6 +5701,7 @@ def build_prompt(
|
||||
character_profile,
|
||||
character_cast,
|
||||
expression_phase,
|
||||
hardcore_position_config,
|
||||
)
|
||||
|
||||
if not expression_enabled:
|
||||
@@ -5293,6 +5730,7 @@ def build_prompt_from_configs(
|
||||
camera_config: str | dict[str, Any] | None = "",
|
||||
character_profile: str | dict[str, Any] | None = "",
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||
hardcore_position_config: str | dict[str, Any] | None = "",
|
||||
extra_positive: str = "",
|
||||
extra_negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
@@ -5327,6 +5765,7 @@ def build_prompt_from_configs(
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile 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,
|
||||
character_profile: str | dict[str, Any] | None = "",
|
||||
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
||||
hardcore_position_config: str | dict[str, Any] | None = "",
|
||||
extra_positive: str = "",
|
||||
extra_negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
@@ -5970,6 +6410,7 @@ def build_insta_of_pair(
|
||||
expression_intensity=options["hardcore_expression_intensity"],
|
||||
character_cast=character_cast or "",
|
||||
expression_phase="hardcore",
|
||||
hardcore_position_config=hardcore_position_config or "",
|
||||
)
|
||||
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
|
||||
hard_row["pov_character_labels"] = pov_character_labels
|
||||
@@ -6154,6 +6595,7 @@ def build_insta_of_pair(
|
||||
"character_hardcore_clothing": character_hardcore_clothing_entries,
|
||||
"hardcore_clothing_state": hard_clothing_state,
|
||||
"hardcore_detail_density": hard_detail_density,
|
||||
"hardcore_position_config": hard_row.get("hardcore_position_config", {}),
|
||||
"softcore_prompt": soft_prompt,
|
||||
"hardcore_prompt": hard_prompt,
|
||||
"softcore_negative_prompt": soft_negative,
|
||||
|
||||
Reference in New Issue
Block a user