Add hardcore action family metadata
This commit is contained in:
@@ -107,6 +107,9 @@ Already isolated:
|
||||
- shared hardcore environment-anchor cleanup lives in
|
||||
`hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
|
||||
reaches formatter routes.
|
||||
- shared hardcore action metadata lives in `hardcore_action_metadata.py`; custom
|
||||
rows now emit `action_family`, `position_family`, `position_key`, and
|
||||
`position_keys` so formatter routing and debugging do less keyword guessing.
|
||||
|
||||
### Pair / Adapter Layer
|
||||
|
||||
@@ -148,6 +151,8 @@ Already isolated:
|
||||
- `krea_action_context.py` owns shared action-family predicates, axis context
|
||||
text, climax detection, and detail-density normalization used by action and
|
||||
POV formatter routes.
|
||||
- `hardcore_action_metadata.py` owns shared action-family constants,
|
||||
normalization, and inference used by the builder and Krea formatter route.
|
||||
- `krea_pov.py` owns POV labels, POV label filtering, and POV camera/composition
|
||||
support text.
|
||||
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
|
||||
@@ -166,8 +171,8 @@ Already isolated:
|
||||
|
||||
Improve later:
|
||||
|
||||
- add metadata fields such as `action_family` / `position_family` to reduce
|
||||
keyword guessing in hardcore formatter dispatch;
|
||||
- extend SDXL and caption routes to optionally consume `action_family` /
|
||||
`position_family` when ordering tags or caption clauses;
|
||||
- add route-level smoke fixtures for representative metadata rows;
|
||||
|
||||
### SDXL Formatter Path
|
||||
@@ -346,8 +351,8 @@ Medium-term:
|
||||
|
||||
## Recommended Next Passes
|
||||
|
||||
1. Add metadata fields such as `action_family` / `position_family` to reduce
|
||||
keyword guessing in hardcore filters and formatter dispatch.
|
||||
1. Extend SDXL and caption routes to optionally consume `action_family` /
|
||||
`position_family` when ordering tags or caption clauses.
|
||||
2. Split `__init__.py` node classes by family after behavior is covered by smoke
|
||||
checks.
|
||||
3. Add route-level smoke fixtures for representative Krea/SDXL/caption metadata
|
||||
|
||||
@@ -397,6 +397,9 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
||||
| `content_seed_axis` | `_build_custom_row` | Debug | Shows whether the item/action was driven by `content` or `pose`. Critical for hardcore pose categories. |
|
||||
| `item` | `_compose_item` or Insta override | Krea/SDXL/Naturalizer | Clothing item, category item, or sexual scene/action text. |
|
||||
| `item_axis_values` | `_compose_item` | Krea hardcore rewrite, SDXL tags | Filled template axes such as position/action/detail values. |
|
||||
| `action_family` | `hardcore_action_metadata.source_hardcore_action_family` | Krea hardcore rewrite, debug | Source-aware formatter semantic family such as `foreplay`, `outercourse`, `oral`, `penetration`, `toy_double`, or `climax`. |
|
||||
| `position_family` | `_hardcore_source_position_family` | Debug/filtering | Source/UI hardcore family selected by subcategory, such as `manual`, `interaction`, `oral`, `anal`, or `climax`. |
|
||||
| `position_key`, `position_keys` | `_hardcore_position_keys` | Debug/future filters | Concrete position tokens inferred from axes and role text, such as `kneeling`, `doggy`, `boobjob`, or `open_thighs`. |
|
||||
| `custom_item`, `item_label` | Category/pair route | Formatters and debug | Label/name for item route. |
|
||||
| `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. |
|
||||
| `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. |
|
||||
@@ -564,6 +567,7 @@ Key Krea2 ownership:
|
||||
|
||||
- Cast descriptor naturalization: `krea_cast.cast_prose`,
|
||||
`krea_cast.natural_label_text`.
|
||||
- Shared action-family metadata: `hardcore_action_metadata.py`.
|
||||
- Action context and family predicates: `krea_action_context.py`.
|
||||
- Non-POV pose anchors and arrangements: `krea_action_positions.py`.
|
||||
- Non-climax item/detail cleanup: `krea_action_details.py`.
|
||||
@@ -751,13 +755,15 @@ Use these traces to narrow a problem in one pass.
|
||||
### Hardcore action keeps selecting the same family
|
||||
|
||||
1. Check metadata `main_category`, `subcategory`, `content_seed_axis`,
|
||||
`hardcore_position_config`, `item`, `role_graph`, and `item_axis_values`.
|
||||
`action_family`, `position_family`, `position_key`, `hardcore_position_config`,
|
||||
`item`, `role_graph`, and `item_axis_values`.
|
||||
2. If `hardcore_position_config` disabled most families, the repeated action may
|
||||
be the only compatible pool left.
|
||||
3. Inspect `categories/sexual_poses.json` for the selected subcategory,
|
||||
`item_templates`, `axes`, and `weight`.
|
||||
4. If raw `item` differs but Krea output looks identical, inspect
|
||||
`krea_action_context.py` family predicates first, then
|
||||
`hardcore_action_metadata.py` action-family metadata first, then
|
||||
`krea_action_context.py` family predicates,
|
||||
`krea_action_positions.py` pose anchors/arrangements,
|
||||
`krea_action_details.py` item/detail cleanup, `krea_action_climax.py`
|
||||
climax cleanup, `krea_action_dispatch.py` family routing, and
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_foreplay_text,
|
||||
is_oral_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text,
|
||||
)
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_foreplay_text,
|
||||
is_oral_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text,
|
||||
)
|
||||
|
||||
|
||||
ACTION_CLIMAX = "climax"
|
||||
ACTION_FOREPLAY = "foreplay"
|
||||
ACTION_OUTERCOURSE = "outercourse"
|
||||
ACTION_ORAL = "oral"
|
||||
ACTION_PENETRATION = "penetration"
|
||||
ACTION_TOY_DOUBLE = "toy_double"
|
||||
ACTION_DEFAULT = "default"
|
||||
|
||||
HARDCORE_ACTION_FAMILY_CHOICES = {
|
||||
ACTION_CLIMAX,
|
||||
ACTION_FOREPLAY,
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_ORAL,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_TOY_DOUBLE,
|
||||
ACTION_DEFAULT,
|
||||
}
|
||||
|
||||
|
||||
def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
if text == "penetrative":
|
||||
text = ACTION_PENETRATION
|
||||
return text if text in HARDCORE_ACTION_FAMILY_CHOICES else default
|
||||
|
||||
|
||||
def infer_hardcore_action_family(
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
*,
|
||||
is_climax: bool | None = None,
|
||||
) -> str:
|
||||
axis_text = axis_values_text(axis_values)
|
||||
if is_climax is None:
|
||||
is_climax = is_climax_text(role_graph, hard_item, composition, axis_text)
|
||||
if is_climax:
|
||||
return ACTION_CLIMAX
|
||||
if is_foreplay_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_FOREPLAY
|
||||
if is_outercourse_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_OUTERCOURSE
|
||||
if is_oral_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_ORAL
|
||||
if is_vaginal_penetration_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_PENETRATION
|
||||
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_TOY_DOUBLE
|
||||
return ACTION_DEFAULT
|
||||
|
||||
|
||||
def source_hardcore_action_family(
|
||||
source_family: Any,
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
) -> str:
|
||||
inferred = infer_hardcore_action_family(role_graph, hard_item, composition, axis_values)
|
||||
if inferred in (ACTION_CLIMAX, ACTION_TOY_DOUBLE):
|
||||
return inferred
|
||||
family = str(source_family or "").strip().lower()
|
||||
source_mapping = {
|
||||
"penetrative": ACTION_PENETRATION,
|
||||
"foreplay": ACTION_FOREPLAY,
|
||||
"interaction": ACTION_FOREPLAY,
|
||||
"manual": ACTION_FOREPLAY,
|
||||
"oral": ACTION_ORAL,
|
||||
"outercourse": ACTION_OUTERCOURSE,
|
||||
"climax": ACTION_CLIMAX,
|
||||
}
|
||||
if family == "anal":
|
||||
return ACTION_DEFAULT
|
||||
return source_mapping.get(family, inferred)
|
||||
+24
-45
@@ -8,13 +8,19 @@ try:
|
||||
from .krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_foreplay_text,
|
||||
is_oral_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text,
|
||||
normalize_hardcore_detail_density,
|
||||
)
|
||||
from .hardcore_action_metadata import (
|
||||
ACTION_CLIMAX,
|
||||
ACTION_FOREPLAY,
|
||||
ACTION_ORAL,
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_TOY_DOUBLE,
|
||||
infer_hardcore_action_family,
|
||||
normalize_hardcore_action_family,
|
||||
)
|
||||
from .krea_detail import limit_detail_for_density
|
||||
from .krea_action_positions import hardcore_pose_anchor
|
||||
from .krea_action_details import (
|
||||
@@ -31,13 +37,19 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_context import (
|
||||
axis_values_text,
|
||||
is_climax_text,
|
||||
is_foreplay_text,
|
||||
is_oral_text,
|
||||
is_outercourse_text,
|
||||
is_toy_assisted_double_text,
|
||||
is_vaginal_penetration_text,
|
||||
normalize_hardcore_detail_density,
|
||||
)
|
||||
from hardcore_action_metadata import (
|
||||
ACTION_CLIMAX,
|
||||
ACTION_FOREPLAY,
|
||||
ACTION_ORAL,
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_TOY_DOUBLE,
|
||||
infer_hardcore_action_family,
|
||||
normalize_hardcore_action_family,
|
||||
)
|
||||
from krea_detail import limit_detail_for_density
|
||||
from krea_action_positions import hardcore_pose_anchor
|
||||
from krea_action_details import (
|
||||
@@ -52,15 +64,6 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from krea_action_climax import climax_role_graph, dedupe_climax_detail
|
||||
|
||||
|
||||
ACTION_CLIMAX = "climax"
|
||||
ACTION_FOREPLAY = "foreplay"
|
||||
ACTION_OUTERCOURSE = "outercourse"
|
||||
ACTION_ORAL = "oral"
|
||||
ACTION_PENETRATION = "penetration"
|
||||
ACTION_TOY_DOUBLE = "toy_double"
|
||||
ACTION_DEFAULT = "default"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HardcoreActionParts:
|
||||
family: str
|
||||
@@ -129,32 +132,6 @@ def normalize_toy_double_role_graph(role_graph: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def hardcore_action_family(
|
||||
role_graph: str,
|
||||
hard_item: str,
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
*,
|
||||
is_climax: bool | None = None,
|
||||
) -> str:
|
||||
axis_text = axis_values_text(axis_values)
|
||||
if is_climax is None:
|
||||
is_climax = is_climax_text(role_graph, hard_item, composition, axis_text)
|
||||
if is_climax:
|
||||
return ACTION_CLIMAX
|
||||
if is_foreplay_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_FOREPLAY
|
||||
if is_outercourse_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_OUTERCOURSE
|
||||
if is_oral_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_ORAL
|
||||
if is_vaginal_penetration_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_PENETRATION
|
||||
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text):
|
||||
return ACTION_TOY_DOUBLE
|
||||
return ACTION_DEFAULT
|
||||
|
||||
|
||||
def action_detail_for_family(
|
||||
family: str,
|
||||
detail: str,
|
||||
@@ -194,18 +171,20 @@ def resolve_hardcore_action_parts(
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
detail_density: str = "balanced",
|
||||
action_family: Any = "",
|
||||
) -> HardcoreActionParts:
|
||||
detail_density = normalize_hardcore_detail_density(detail_density)
|
||||
role_graph = normalize_hardcore_role_graph(role_graph)
|
||||
hard_item = _clean(hard_item).rstrip(".")
|
||||
axis_text = axis_values_text(axis_values)
|
||||
is_climax = is_climax_text(role_graph, hard_item, composition, axis_text)
|
||||
forced_family = normalize_hardcore_action_family(action_family)
|
||||
is_climax = forced_family == ACTION_CLIMAX or is_climax_text(role_graph, hard_item, composition, axis_text)
|
||||
if is_climax:
|
||||
role_graph = climax_role_graph(role_graph, hard_item, axis_values)
|
||||
|
||||
detail = hardcore_item_detail(hard_item)
|
||||
anchor = hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
|
||||
family = hardcore_action_family(role_graph, hard_item, composition, axis_values, is_climax=is_climax)
|
||||
family = forced_family or infer_hardcore_action_family(role_graph, hard_item, composition, axis_values, is_climax=is_climax)
|
||||
|
||||
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text):
|
||||
role_graph = normalize_toy_double_role_graph(role_graph)
|
||||
|
||||
+2
-1
@@ -44,8 +44,9 @@ def hardcore_action_sentence(
|
||||
composition: str = "",
|
||||
axis_values: Any = None,
|
||||
detail_density: str = "balanced",
|
||||
action_family: Any = "",
|
||||
) -> str:
|
||||
parts = resolve_hardcore_action_parts(role_graph, hard_item, composition, axis_values, detail_density)
|
||||
parts = resolve_hardcore_action_parts(role_graph, hard_item, composition, axis_values, detail_density, action_family)
|
||||
role_graph = parts.role_graph
|
||||
hard_item = parts.hard_item
|
||||
detail = parts.detail
|
||||
|
||||
+9
-1
@@ -502,7 +502,14 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
||||
item = _natural_label_text(item, cast_labels)
|
||||
axis_values = _sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
||||
detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
|
||||
action = _hardcore_action_sentence(role_graph, item, source_composition, axis_values, detail_density)
|
||||
action = _hardcore_action_sentence(
|
||||
role_graph,
|
||||
item,
|
||||
source_composition,
|
||||
axis_values,
|
||||
detail_density,
|
||||
row.get("action_family"),
|
||||
)
|
||||
action = _pov_action_phrase(action, pov_labels, role_graph, item, source_composition, axis_values, detail_density)
|
||||
output_composition = _pov_composition_text(composition, pov_labels)
|
||||
parts = [
|
||||
@@ -633,6 +640,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
||||
hard_source_composition,
|
||||
hard_axis_values,
|
||||
hard_detail_density,
|
||||
hard.get("action_family") or row.get("action_family"),
|
||||
)
|
||||
hard_action = _pov_action_phrase(
|
||||
hard_action,
|
||||
|
||||
@@ -15,6 +15,7 @@ try:
|
||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||
)
|
||||
from .hardcore_action_metadata import source_hardcore_action_family
|
||||
from .prompt_hygiene import (
|
||||
sanitize_caption_text,
|
||||
sanitize_negative_text,
|
||||
@@ -27,6 +28,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||
)
|
||||
from hardcore_action_metadata import source_hardcore_action_family
|
||||
from prompt_hygiene import (
|
||||
sanitize_caption_text,
|
||||
sanitize_negative_text,
|
||||
@@ -490,6 +492,49 @@ HARDCORE_POSITION_AXIS_KEYS = {
|
||||
"aftercare_act",
|
||||
"cleanup_detail",
|
||||
}
|
||||
|
||||
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
|
||||
"penetrative_sex": "penetrative",
|
||||
"foreplay_teasing": "foreplay",
|
||||
"body_worship_touching": "interaction",
|
||||
"clothing_position_transitions": "interaction",
|
||||
"dominant_guidance": "interaction",
|
||||
"camera_performance": "interaction",
|
||||
"manual_stimulation": "manual",
|
||||
"oral_sex": "oral",
|
||||
"outercourse_sex": "outercourse",
|
||||
"anal_double_penetration": "anal",
|
||||
"threesomes": "threesome",
|
||||
"group_coordination": "interaction",
|
||||
"group_sex_orgy": "group",
|
||||
"cumshot_climax": "climax",
|
||||
"aftercare_cleanup": "interaction",
|
||||
}
|
||||
|
||||
|
||||
def _hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
|
||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
|
||||
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
|
||||
if family:
|
||||
return family
|
||||
config_family = _normalize_hardcore_position_family((config or {}).get("family"), "")
|
||||
return "" if config_family == "any" else config_family
|
||||
|
||||
|
||||
def _hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
||||
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
|
||||
if isinstance(axis_values, dict):
|
||||
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
|
||||
text = " ".join(text_parts).lower()
|
||||
if not text:
|
||||
return []
|
||||
keys: list[str] = []
|
||||
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
|
||||
if any(token in text for token in tokens):
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
|
||||
CAMERA_ORBIT_FRAMING_CHOICES = [
|
||||
"from_zoom",
|
||||
"wide",
|
||||
@@ -7055,6 +7100,27 @@ def _build_custom_row(
|
||||
if is_pose_category:
|
||||
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
|
||||
composition = _pov_composition_prompt(source_composition, pov_character_labels)
|
||||
position_family = ""
|
||||
position_keys: list[str] = []
|
||||
position_key = ""
|
||||
action_family = ""
|
||||
if is_pose_category:
|
||||
position_family = _hardcore_source_position_family(subcategory, parsed_hardcore_position_config)
|
||||
position_keys = _hardcore_position_keys(
|
||||
item_text,
|
||||
source_role_graph,
|
||||
source_composition,
|
||||
pose,
|
||||
axis_values=item_axis_values,
|
||||
)
|
||||
position_key = position_keys[0] if position_keys else ""
|
||||
action_family = source_hardcore_action_family(
|
||||
position_family,
|
||||
source_role_graph,
|
||||
item_text,
|
||||
source_composition,
|
||||
item_axis_values,
|
||||
)
|
||||
|
||||
negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
|
||||
positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
|
||||
@@ -7096,6 +7162,10 @@ def _build_custom_row(
|
||||
"composition_config": parsed_composition_config if _composition_config_active(parsed_composition_config) else {},
|
||||
"role_graph": role_graph,
|
||||
"source_role_graph": source_role_graph,
|
||||
"action_family": action_family,
|
||||
"position_family": position_family,
|
||||
"position_key": position_key,
|
||||
"position_keys": position_keys,
|
||||
"pov_character_labels": pov_character_labels,
|
||||
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
|
||||
"cast_descriptors": cast_descriptor_text,
|
||||
@@ -7170,6 +7240,10 @@ def _build_custom_row(
|
||||
"content_seed_axis": content_axis,
|
||||
"role_graph": role_graph,
|
||||
"source_role_graph": source_role_graph,
|
||||
"action_family": action_family,
|
||||
"position_family": position_family,
|
||||
"position_key": position_key,
|
||||
"position_keys": position_keys,
|
||||
"source_composition": source_composition,
|
||||
"pov_character_labels": pov_character_labels,
|
||||
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
|
||||
|
||||
+10
-7
@@ -364,14 +364,14 @@ def smoke_config_route_location_theme() -> None:
|
||||
def smoke_hardcore_category_routes() -> None:
|
||||
cast = _character_cast()
|
||||
cases = [
|
||||
("hardcore_penetration", "Penetrative sex", "penetration_only"),
|
||||
("hardcore_oral", "Oral sex", "oral_only"),
|
||||
("hardcore_manual", "Manual stimulation", "manual_only"),
|
||||
("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only"),
|
||||
("hardcore_foreplay", "Foreplay and teasing", "foreplay_only"),
|
||||
("hardcore_aftercare", "Aftercare and cleanup", "interaction_only"),
|
||||
("hardcore_penetration", "Penetrative sex", "penetration_only", "penetrative", {"penetration", "default"}),
|
||||
("hardcore_oral", "Oral sex", "oral_only", "oral", {"oral"}),
|
||||
("hardcore_manual", "Manual stimulation", "manual_only", "manual", {"foreplay", "outercourse"}),
|
||||
("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only", "outercourse", {"outercourse"}),
|
||||
("hardcore_foreplay", "Foreplay and teasing", "foreplay_only", "foreplay", {"foreplay"}),
|
||||
("hardcore_aftercare", "Aftercare and cleanup", "interaction_only", "interaction", {"foreplay"}),
|
||||
]
|
||||
for index, (name, subcategory, focus) in enumerate(cases, start=1101):
|
||||
for index, (name, subcategory, focus, position_family, action_families) in enumerate(cases, start=1101):
|
||||
row = _prompt_row(
|
||||
name=name,
|
||||
category="Hardcore sexual poses",
|
||||
@@ -384,6 +384,9 @@ def smoke_hardcore_category_routes() -> None:
|
||||
)
|
||||
_expect_custom_row(row, name)
|
||||
_expect(row.get("subject_type") == "configured_cast", f"{name} should use configured cast")
|
||||
_expect(row.get("position_family") == position_family, f"{name} position_family mismatch: {row.get('position_family')}")
|
||||
_expect(row.get("action_family") in action_families, f"{name} action_family mismatch: {row.get('action_family')}")
|
||||
_expect(isinstance(row.get("position_keys"), list), f"{name} position_keys missing")
|
||||
_expect_formatter_outputs(row, name, target="single")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user