Move hardcore position filtering policy

This commit is contained in:
2026-06-27 03:02:23 +02:00
parent 1cc65e35b5
commit d4d3be5789
5 changed files with 343 additions and 246 deletions
+2 -2
View File
@@ -150,8 +150,8 @@ Already isolated:
to rows.
- hardcore position/action-filter choices, selected-position normalization,
config JSON builders/parsers, focus-policy toggles, subcategory allow-list
policy, and position-key detection live in `hardcore_position_config.py`;
`prompt_builder.py` still applies the config to category rows.
policy, position-key detection, category filtering, and item-template/axis
filtering live in `hardcore_position_config.py`.
- hardcore configured-cast role graph generation lives in
`hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and
then asks that module for the source role graph.
+7 -8
View File
@@ -77,7 +77,7 @@ Core helper ownership:
| `generation_profile_config.py` | Generation profile presets, profile option overrides, trigger policy, expression/pose/clothing config normalization, and profile config parsing. |
| `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. |
| `location_config.py` | Location/composition preset schemas, themed location packs, custom location/composition parsing, pool merge behavior, and location/composition config parsing. |
| `hardcore_position_config.py` | Hardcore position/action-filter choices, selected-position normalization, config JSON builders/parsers, focus-policy toggles, subcategory allow-list policy, and position-key detection. |
| `hardcore_position_config.py` | Hardcore position/action-filter choices, selected-position normalization, config JSON builders/parsers, focus-policy toggles, subcategory allow-list policy, position-key detection, and category/template/axis filtering. |
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, and hardcore cast count policy. |
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
| `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. |
@@ -126,7 +126,7 @@ These recipes identify the intended road before editing prompt text.
| Request | Preferred node route | Critical settings | If wrong, inspect |
| --- | --- | --- | --- |
| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, `krea_actions.hardcore_action_sentence` |
| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `krea_actions.hardcore_action_sentence` |
| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `hardcore_position_config.apply_hardcore_position_config_to_subcategory`, `krea_actions.hardcore_action_sentence` |
| Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `krea_pov_actions.py`, `krea_pov.py`, `krea_cast.cast_prose` omit-label handling |
| Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, `krea_action_context.is_foreplay_text` / `krea_actions.hardcore_action_sentence` |
| Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields |
@@ -325,9 +325,8 @@ Edit targets:
`foreplay_teasing`, `manual_stimulation`, `body_worship_touching`,
`clothing_position_transitions`, `dominant_guidance`,
`camera_performance`, `group_coordination`, and `aftercare_cleanup`.
- Position filtering UI/config: builders and parsers live in
`hardcore_position_config.py`; `prompt_builder._apply_hardcore_position_config_to_subcategory`
applies the config to category rows.
- Position filtering UI/config and category/template/axis filter policy live in
`hardcore_position_config.py`.
- Krea2 action rewrite orchestration: `krea_formatter.py`.
- Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`.
- Krea2 non-climax item/detail cleanup: `krea_action_details.py`.
@@ -517,8 +516,8 @@ plain prompt text. When debugging, inspect these fields before editing pools.
flowchart TD
A[Hardcore Position Pool] --> C[hardcore_position_config]
B[Hardcore Action Filter] --> C
C --> D[_filter_hardcore_categories_for_position]
C --> E[_apply_hardcore_position_config_to_subcategory]
C --> D[hardcore_position_config.filter_hardcore_categories_for_position]
C --> E[hardcore_position_config.apply_hardcore_position_config_to_subcategory]
E --> F[item_templates / axes in sexual_poses.json]
F --> G[role_graph + item + axis_values]
G --> H[Krea2 action sentence and POV rewrite]
@@ -831,7 +830,7 @@ pair metadata through the core Python APIs, then verifies:
| Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. |
| Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. |
| Expression appears when disabled | `_disable_row_expression`, formatter expression extraction. |
| Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `_apply_hardcore_position_config_to_subcategory`. |
| Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `hardcore_position_config.apply_hardcore_position_config_to_subcategory`. |
| Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and `krea_action_context.is_foreplay_text` / `krea_action_positions.hardcore_pose_anchor`. |
| Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. |
| Krea2 hardcore prompt position is vague | `krea_actions.hardcore_action_sentence` or `krea_pov_actions.py`. |
+262 -1
View File
@@ -2,7 +2,8 @@ from __future__ import annotations
import json
import re
from typing import Any
from string import Formatter
from typing import Any, Callable
HARDCORE_POSITION_FAMILY_CHOICES = [
@@ -226,6 +227,19 @@ def _list_from(value: Any) -> list[Any]:
return [value]
def _entry_text(item: Any) -> str:
if isinstance(item, dict):
return str(
item.get("template")
or item.get("prompt")
or item.get("text")
or item.get("description")
or item.get("name")
or ""
).strip()
return str(item).strip()
def hardcore_position_family_choices() -> list[str]:
return list(HARDCORE_POSITION_FAMILY_CHOICES)
@@ -486,6 +500,253 @@ def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
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_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", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
):
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_outercourse", True) and (
axis_name in ("outer_act", "contact_detail", "texture_detail")
or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
):
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_foreplay", True) and (
axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
or any(
term in text
for term in (
"kiss",
"kissing",
"mouth-to-mouth",
"caress",
"caressing",
"stroking skin",
"hands roaming",
"touching breasts",
"cupping breasts",
"hand on the cheek",
"fingers under the chin",
"undressing",
"removing clothing",
"removing clothes",
"pulling clothing",
"sliding straps",
"unbuttoning",
)
)
):
return True
if not config.get("allow_interaction", True) and (
axis_name
in (
"tease_act",
"touch_detail",
"clothing_detail",
"foreplay_detail",
"face_detail",
"body_contact",
"mood_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
)
or any(
term in text
for term in (
"kiss",
"kissing",
"caress",
"body worship",
"nipple",
"ass grab",
"thigh",
"hair holding",
"wrists",
"dirty talk",
"whispering",
"undressing",
"position transition",
"guided",
"camera",
"watching",
"aftercare",
"cleanup",
"wiping",
)
)
):
return True
if not config.get("allow_manual", True) and (
axis_name in ("manual_act", "manual_detail")
or any(
term in text
for term in (
"fingering",
"fingers inside",
"clit",
"clitoris",
"manual stimulation",
"mutual masturbation",
"masturbating together",
"fingers on pussy",
"fingers on clit",
)
)
):
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 hardcore_position_template_required(config):
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 = hardcore_position_template_required(config) 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 filter_hardcore_categories_for_position(
categories: list[dict[str, Any]],
config: dict[str, Any],
women_count: int,
men_count: int,
compatible_entry: Callable[[dict[str, Any], int, int], bool],
) -> 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_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, "")
+9 -235
View File
@@ -135,8 +135,6 @@ HARDCORE_POSITION_FAMILY_CHOICES = hardcore_position_policy.HARDCORE_POSITION_FA
HARDCORE_POSITION_FOCUS_CHOICES = hardcore_position_policy.HARDCORE_POSITION_FOCUS_CHOICES
HARDCORE_POSITION_KEY_CHOICES = hardcore_position_policy.HARDCORE_POSITION_KEY_CHOICES
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = hardcore_position_policy.HARDCORE_POSITION_FAMILY_SUBCATEGORIES
HARDCORE_POSITION_KEY_MATCHES = hardcore_position_policy.HARDCORE_POSITION_KEY_MATCHES
HARDCORE_POSITION_AXIS_KEYS = hardcore_position_policy.HARDCORE_POSITION_AXIS_KEYS
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = hardcore_position_policy.HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY
@@ -997,16 +995,8 @@ def _hardcore_position_config_active(config: dict[str, Any]) -> bool:
return hardcore_position_policy.hardcore_position_config_active(config)
def _hardcore_position_template_required(config: dict[str, Any]) -> bool:
return hardcore_position_policy.hardcore_position_template_required(config)
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]:
return hardcore_position_policy.hardcore_allowed_subcategory_slugs(config)
return hardcore_position_policy.is_hardcore_sexual_category(category)
def _filter_hardcore_categories_for_position(
@@ -1015,236 +1005,20 @@ def _filter_hardcore_categories_for_position(
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", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
):
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_outercourse", True) and (
axis_name in ("outer_act", "contact_detail", "texture_detail")
or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
):
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_foreplay", True) and (
axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
or any(
term in text
for term in (
"kiss",
"kissing",
"mouth-to-mouth",
"caress",
"caressing",
"stroking skin",
"hands roaming",
"touching breasts",
"cupping breasts",
"hand on the cheek",
"fingers under the chin",
"undressing",
"removing clothing",
"removing clothes",
"pulling clothing",
"sliding straps",
"unbuttoning",
)
)
):
return True
if not config.get("allow_interaction", True) and (
axis_name
in (
"tease_act",
"touch_detail",
"clothing_detail",
"foreplay_detail",
"face_detail",
"body_contact",
"mood_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
)
or any(
term in text
for term in (
"kiss",
"kissing",
"caress",
"body worship",
"nipple",
"ass grab",
"thigh",
"hair holding",
"wrists",
"dirty talk",
"whispering",
"undressing",
"position transition",
"guided",
"camera",
"watching",
"aftercare",
"cleanup",
"wiping",
)
)
):
return True
if not config.get("allow_manual", True) and (
axis_name in ("manual_act", "manual_detail")
or any(
term in text
for term in (
"fingering",
"fingers inside",
"clit",
"clitoris",
"manual stimulation",
"mutual masturbation",
"masturbating together",
"fingers on pussy",
"fingers on clit",
)
)
):
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 _hardcore_position_template_required(config):
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 = _hardcore_position_template_required(config) 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
return hardcore_position_policy.filter_hardcore_categories_for_position(
categories,
config,
women_count,
men_count,
_compatible_entry,
)
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
return hardcore_position_policy.apply_hardcore_position_config_to_subcategory(subcategory, config)
def _ratio_or_none(value: float) -> float | None:
+63
View File
@@ -1242,6 +1242,69 @@ def smoke_hardcore_position_config_policy() -> None:
_expect(filtered.get("allow_penetration") is False, "Hardcore outercourse focus should disable penetration")
_expect("outercourse_sex" in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories lost outercourse")
_expect("oral_sex" not in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories should exclude oral")
action_only = json.loads(
hardcore_position_config.build_hardcore_action_filter_json(
focus="outercourse_only",
allow_toys=False,
allow_double=False,
allow_penetration=True,
allow_foreplay=True,
allow_interaction=True,
allow_manual=True,
allow_oral=True,
allow_outercourse=True,
allow_anal=True,
allow_climax=True,
)
)
action_axis = hardcore_position_config.filter_hardcore_axis(
"outer_act",
["boobjob body contact", "blowjob oral sex", "vaginal penetration"],
action_only,
)
_expect(action_axis == ["boobjob body contact"], "Hardcore action filter policy did not block disabled oral/penetration text")
position_filtered = hardcore_position_config.apply_hardcore_position_config_to_subcategory(
{
"slug": "oral_sex",
"item_templates": [
{"template": "oral contact in {position}"},
{"template": "oral sex without a position axis"},
{"template": "unsupported static template"},
],
"item_axes": {
"position": ["standing oral position", "kneeling oral position"],
"oral_act": ["blowjob", "cunnilingus"],
},
},
base,
)
_expect(
position_filtered["item_templates"] == [{"template": "oral contact in {position}"}],
"Hardcore position policy did not filter templates by selected position requirements",
)
_expect(
position_filtered["item_axes"]["position"] == ["standing oral position"],
"Hardcore position policy did not filter position axes by selected keys",
)
filtered_categories = hardcore_position_config.filter_hardcore_categories_for_position(
[
{
"name": "Hardcore sexual poses",
"slug": "hardcore_sexual_poses",
"subcategories": [{"slug": "oral_sex"}, {"slug": "outercourse_sex"}],
},
{"name": "Casual clothes", "slug": "casual_clothes", "subcategories": [{"slug": "tops"}]},
],
filtered,
1,
1,
lambda _entry, _women, _men: True,
)
_expect(
[entry["slug"] for entry in filtered_categories[0]["subcategories"]] == ["outercourse_sex"],
"Hardcore category filter policy did not remove disallowed subcategories",
)
_expect(filtered_categories[1]["slug"] == "casual_clothes", "Hardcore category filter should preserve non-hardcore categories")
keys = pb._hardcore_position_keys("woman on all fours from behind", axis_values={"position": "doggy"})
_expect(keys == ["doggy"], "Hardcore position key detection changed")