Move hardcore position filtering policy
This commit is contained in:
@@ -150,8 +150,8 @@ Already isolated:
|
|||||||
to rows.
|
to rows.
|
||||||
- hardcore position/action-filter choices, selected-position normalization,
|
- hardcore position/action-filter choices, selected-position normalization,
|
||||||
config JSON builders/parsers, focus-policy toggles, subcategory allow-list
|
config JSON builders/parsers, focus-policy toggles, subcategory allow-list
|
||||||
policy, and position-key detection live in `hardcore_position_config.py`;
|
policy, position-key detection, category filtering, and item-template/axis
|
||||||
`prompt_builder.py` still applies the config to category rows.
|
filtering live in `hardcore_position_config.py`.
|
||||||
- hardcore configured-cast role graph generation lives in
|
- hardcore configured-cast role graph generation lives in
|
||||||
`hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and
|
`hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and
|
||||||
then asks that module for the source role graph.
|
then asks that module for the source role graph.
|
||||||
|
|||||||
@@ -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. |
|
| `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. |
|
| `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. |
|
| `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_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_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. |
|
| `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 |
|
| 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` |
|
| 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 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` |
|
| 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 |
|
| 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`,
|
`foreplay_teasing`, `manual_stimulation`, `body_worship_touching`,
|
||||||
`clothing_position_transitions`, `dominant_guidance`,
|
`clothing_position_transitions`, `dominant_guidance`,
|
||||||
`camera_performance`, `group_coordination`, and `aftercare_cleanup`.
|
`camera_performance`, `group_coordination`, and `aftercare_cleanup`.
|
||||||
- Position filtering UI/config: builders and parsers live in
|
- Position filtering UI/config and category/template/axis filter policy live in
|
||||||
`hardcore_position_config.py`; `prompt_builder._apply_hardcore_position_config_to_subcategory`
|
`hardcore_position_config.py`.
|
||||||
applies the config to category rows.
|
|
||||||
- Krea2 action rewrite orchestration: `krea_formatter.py`.
|
- Krea2 action rewrite orchestration: `krea_formatter.py`.
|
||||||
- Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`.
|
- Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`.
|
||||||
- Krea2 non-climax item/detail cleanup: `krea_action_details.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
|
flowchart TD
|
||||||
A[Hardcore Position Pool] --> C[hardcore_position_config]
|
A[Hardcore Position Pool] --> C[hardcore_position_config]
|
||||||
B[Hardcore Action Filter] --> C
|
B[Hardcore Action Filter] --> C
|
||||||
C --> D[_filter_hardcore_categories_for_position]
|
C --> D[hardcore_position_config.filter_hardcore_categories_for_position]
|
||||||
C --> E[_apply_hardcore_position_config_to_subcategory]
|
C --> E[hardcore_position_config.apply_hardcore_position_config_to_subcategory]
|
||||||
E --> F[item_templates / axes in sexual_poses.json]
|
E --> F[item_templates / axes in sexual_poses.json]
|
||||||
F --> G[role_graph + item + axis_values]
|
F --> G[role_graph + item + axis_values]
|
||||||
G --> H[Krea2 action sentence and POV rewrite]
|
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. |
|
| Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. |
|
||||||
| Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. |
|
| Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. |
|
||||||
| Expression appears when disabled | `_disable_row_expression`, formatter expression extraction. |
|
| 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`. |
|
| 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. |
|
| 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`. |
|
| Krea2 hardcore prompt position is vague | `krea_actions.hardcore_action_sentence` or `krea_pov_actions.py`. |
|
||||||
|
|||||||
+262
-1
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from string import Formatter
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
HARDCORE_POSITION_FAMILY_CHOICES = [
|
HARDCORE_POSITION_FAMILY_CHOICES = [
|
||||||
@@ -226,6 +227,19 @@ def _list_from(value: Any) -> list[Any]:
|
|||||||
return [value]
|
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]:
|
def hardcore_position_family_choices() -> list[str]:
|
||||||
return list(HARDCORE_POSITION_FAMILY_CHOICES)
|
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"])
|
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:
|
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()
|
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
|
||||||
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
|
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
|
||||||
|
|||||||
+8
-234
@@ -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_FOCUS_CHOICES = hardcore_position_policy.HARDCORE_POSITION_FOCUS_CHOICES
|
||||||
HARDCORE_POSITION_KEY_CHOICES = hardcore_position_policy.HARDCORE_POSITION_KEY_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_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
|
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)
|
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:
|
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"
|
return hardcore_position_policy.is_hardcore_sexual_category(category)
|
||||||
|
|
||||||
|
|
||||||
def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
|
||||||
return hardcore_position_policy.hardcore_allowed_subcategory_slugs(config)
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_hardcore_categories_for_position(
|
def _filter_hardcore_categories_for_position(
|
||||||
@@ -1015,236 +1005,20 @@ def _filter_hardcore_categories_for_position(
|
|||||||
women_count: int,
|
women_count: int,
|
||||||
men_count: int,
|
men_count: int,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
if not _hardcore_position_config_active(config):
|
return hardcore_position_policy.filter_hardcore_categories_for_position(
|
||||||
return categories
|
categories,
|
||||||
allowed = _hardcore_allowed_subcategory_slugs(config)
|
config,
|
||||||
filtered_categories: list[dict[str, Any]] = []
|
women_count,
|
||||||
for category in categories:
|
men_count,
|
||||||
if not _is_hardcore_sexual_category(category):
|
_compatible_entry,
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_hardcore_position_config_to_subcategory(
|
def _apply_hardcore_position_config_to_subcategory(
|
||||||
subcategory: dict[str, Any],
|
subcategory: dict[str, Any],
|
||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if not _hardcore_position_config_active(config):
|
return hardcore_position_policy.apply_hardcore_position_config_to_subcategory(subcategory, 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:
|
||||||
|
|||||||
@@ -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(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("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")
|
_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"})
|
keys = pb._hardcore_position_keys("woman on all fours from behind", axis_values={"position": "doggy"})
|
||||||
_expect(keys == ["doggy"], "Hardcore position key detection changed")
|
_expect(keys == ["doggy"], "Hardcore position key detection changed")
|
||||||
|
|||||||
Reference in New Issue
Block a user