Add hardcore action family metadata
This commit is contained in:
@@ -107,6 +107,9 @@ Already isolated:
|
|||||||
- shared hardcore environment-anchor cleanup lives in
|
- shared hardcore environment-anchor cleanup lives in
|
||||||
`hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
|
`hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
|
||||||
reaches formatter routes.
|
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
|
### Pair / Adapter Layer
|
||||||
|
|
||||||
@@ -148,6 +151,8 @@ Already isolated:
|
|||||||
- `krea_action_context.py` owns shared action-family predicates, axis context
|
- `krea_action_context.py` owns shared action-family predicates, axis context
|
||||||
text, climax detection, and detail-density normalization used by action and
|
text, climax detection, and detail-density normalization used by action and
|
||||||
POV formatter routes.
|
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
|
- `krea_pov.py` owns POV labels, POV label filtering, and POV camera/composition
|
||||||
support text.
|
support text.
|
||||||
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
|
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
|
||||||
@@ -166,8 +171,8 @@ Already isolated:
|
|||||||
|
|
||||||
Improve later:
|
Improve later:
|
||||||
|
|
||||||
- add metadata fields such as `action_family` / `position_family` to reduce
|
- extend SDXL and caption routes to optionally consume `action_family` /
|
||||||
keyword guessing in hardcore formatter dispatch;
|
`position_family` when ordering tags or caption clauses;
|
||||||
- add route-level smoke fixtures for representative metadata rows;
|
- add route-level smoke fixtures for representative metadata rows;
|
||||||
|
|
||||||
### SDXL Formatter Path
|
### SDXL Formatter Path
|
||||||
@@ -346,8 +351,8 @@ Medium-term:
|
|||||||
|
|
||||||
## Recommended Next Passes
|
## Recommended Next Passes
|
||||||
|
|
||||||
1. Add metadata fields such as `action_family` / `position_family` to reduce
|
1. Extend SDXL and caption routes to optionally consume `action_family` /
|
||||||
keyword guessing in hardcore filters and formatter dispatch.
|
`position_family` when ordering tags or caption clauses.
|
||||||
2. Split `__init__.py` node classes by family after behavior is covered by smoke
|
2. Split `__init__.py` node classes by family after behavior is covered by smoke
|
||||||
checks.
|
checks.
|
||||||
3. Add route-level smoke fixtures for representative Krea/SDXL/caption metadata
|
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. |
|
| `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` | `_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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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`,
|
- Cast descriptor naturalization: `krea_cast.cast_prose`,
|
||||||
`krea_cast.natural_label_text`.
|
`krea_cast.natural_label_text`.
|
||||||
|
- Shared action-family metadata: `hardcore_action_metadata.py`.
|
||||||
- Action context and family predicates: `krea_action_context.py`.
|
- Action context and family predicates: `krea_action_context.py`.
|
||||||
- Non-POV pose anchors and arrangements: `krea_action_positions.py`.
|
- Non-POV pose anchors and arrangements: `krea_action_positions.py`.
|
||||||
- Non-climax item/detail cleanup: `krea_action_details.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
|
### Hardcore action keeps selecting the same family
|
||||||
|
|
||||||
1. Check metadata `main_category`, `subcategory`, `content_seed_axis`,
|
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
|
2. If `hardcore_position_config` disabled most families, the repeated action may
|
||||||
be the only compatible pool left.
|
be the only compatible pool left.
|
||||||
3. Inspect `categories/sexual_poses.json` for the selected subcategory,
|
3. Inspect `categories/sexual_poses.json` for the selected subcategory,
|
||||||
`item_templates`, `axes`, and `weight`.
|
`item_templates`, `axes`, and `weight`.
|
||||||
4. If raw `item` differs but Krea output looks identical, inspect
|
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_positions.py` pose anchors/arrangements,
|
||||||
`krea_action_details.py` item/detail cleanup, `krea_action_climax.py`
|
`krea_action_details.py` item/detail cleanup, `krea_action_climax.py`
|
||||||
climax cleanup, `krea_action_dispatch.py` family routing, and
|
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 (
|
from .krea_action_context import (
|
||||||
axis_values_text,
|
axis_values_text,
|
||||||
is_climax_text,
|
is_climax_text,
|
||||||
is_foreplay_text,
|
|
||||||
is_oral_text,
|
|
||||||
is_outercourse_text,
|
|
||||||
is_toy_assisted_double_text,
|
is_toy_assisted_double_text,
|
||||||
is_vaginal_penetration_text,
|
|
||||||
normalize_hardcore_detail_density,
|
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_detail import limit_detail_for_density
|
||||||
from .krea_action_positions import hardcore_pose_anchor
|
from .krea_action_positions import hardcore_pose_anchor
|
||||||
from .krea_action_details import (
|
from .krea_action_details import (
|
||||||
@@ -31,13 +37,19 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
from krea_action_context import (
|
from krea_action_context import (
|
||||||
axis_values_text,
|
axis_values_text,
|
||||||
is_climax_text,
|
is_climax_text,
|
||||||
is_foreplay_text,
|
|
||||||
is_oral_text,
|
|
||||||
is_outercourse_text,
|
|
||||||
is_toy_assisted_double_text,
|
is_toy_assisted_double_text,
|
||||||
is_vaginal_penetration_text,
|
|
||||||
normalize_hardcore_detail_density,
|
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_detail import limit_detail_for_density
|
||||||
from krea_action_positions import hardcore_pose_anchor
|
from krea_action_positions import hardcore_pose_anchor
|
||||||
from krea_action_details import (
|
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
|
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)
|
@dataclass(frozen=True)
|
||||||
class HardcoreActionParts:
|
class HardcoreActionParts:
|
||||||
family: str
|
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(
|
def action_detail_for_family(
|
||||||
family: str,
|
family: str,
|
||||||
detail: str,
|
detail: str,
|
||||||
@@ -194,18 +171,20 @@ def resolve_hardcore_action_parts(
|
|||||||
composition: str = "",
|
composition: str = "",
|
||||||
axis_values: Any = None,
|
axis_values: Any = None,
|
||||||
detail_density: str = "balanced",
|
detail_density: str = "balanced",
|
||||||
|
action_family: Any = "",
|
||||||
) -> HardcoreActionParts:
|
) -> HardcoreActionParts:
|
||||||
detail_density = normalize_hardcore_detail_density(detail_density)
|
detail_density = normalize_hardcore_detail_density(detail_density)
|
||||||
role_graph = normalize_hardcore_role_graph(role_graph)
|
role_graph = normalize_hardcore_role_graph(role_graph)
|
||||||
hard_item = _clean(hard_item).rstrip(".")
|
hard_item = _clean(hard_item).rstrip(".")
|
||||||
axis_text = axis_values_text(axis_values)
|
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:
|
if is_climax:
|
||||||
role_graph = climax_role_graph(role_graph, hard_item, axis_values)
|
role_graph = climax_role_graph(role_graph, hard_item, axis_values)
|
||||||
|
|
||||||
detail = hardcore_item_detail(hard_item)
|
detail = hardcore_item_detail(hard_item)
|
||||||
anchor = hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
|
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):
|
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_text):
|
||||||
role_graph = normalize_toy_double_role_graph(role_graph)
|
role_graph = normalize_toy_double_role_graph(role_graph)
|
||||||
|
|||||||
+2
-1
@@ -44,8 +44,9 @@ def hardcore_action_sentence(
|
|||||||
composition: str = "",
|
composition: str = "",
|
||||||
axis_values: Any = None,
|
axis_values: Any = None,
|
||||||
detail_density: str = "balanced",
|
detail_density: str = "balanced",
|
||||||
|
action_family: Any = "",
|
||||||
) -> str:
|
) -> 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
|
role_graph = parts.role_graph
|
||||||
hard_item = parts.hard_item
|
hard_item = parts.hard_item
|
||||||
detail = parts.detail
|
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)
|
item = _natural_label_text(item, cast_labels)
|
||||||
axis_values = _sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
axis_values = _sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
||||||
detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
|
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)
|
action = _pov_action_phrase(action, pov_labels, role_graph, item, source_composition, axis_values, detail_density)
|
||||||
output_composition = _pov_composition_text(composition, pov_labels)
|
output_composition = _pov_composition_text(composition, pov_labels)
|
||||||
parts = [
|
parts = [
|
||||||
@@ -633,6 +640,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
|
|||||||
hard_source_composition,
|
hard_source_composition,
|
||||||
hard_axis_values,
|
hard_axis_values,
|
||||||
hard_detail_density,
|
hard_detail_density,
|
||||||
|
hard.get("action_family") or row.get("action_family"),
|
||||||
)
|
)
|
||||||
hard_action = _pov_action_phrase(
|
hard_action = _pov_action_phrase(
|
||||||
hard_action,
|
hard_action,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ try:
|
|||||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||||
)
|
)
|
||||||
|
from .hardcore_action_metadata import source_hardcore_action_family
|
||||||
from .prompt_hygiene import (
|
from .prompt_hygiene import (
|
||||||
sanitize_caption_text,
|
sanitize_caption_text,
|
||||||
sanitize_negative_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_axis_values as _sanitize_hardcore_axis_values,
|
||||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||||
)
|
)
|
||||||
|
from hardcore_action_metadata import source_hardcore_action_family
|
||||||
from prompt_hygiene import (
|
from prompt_hygiene import (
|
||||||
sanitize_caption_text,
|
sanitize_caption_text,
|
||||||
sanitize_negative_text,
|
sanitize_negative_text,
|
||||||
@@ -490,6 +492,49 @@ HARDCORE_POSITION_AXIS_KEYS = {
|
|||||||
"aftercare_act",
|
"aftercare_act",
|
||||||
"cleanup_detail",
|
"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 = [
|
CAMERA_ORBIT_FRAMING_CHOICES = [
|
||||||
"from_zoom",
|
"from_zoom",
|
||||||
"wide",
|
"wide",
|
||||||
@@ -7055,6 +7100,27 @@ def _build_custom_row(
|
|||||||
if is_pose_category:
|
if is_pose_category:
|
||||||
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
|
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
|
||||||
composition = _pov_composition_prompt(source_composition, pov_character_labels)
|
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))
|
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))
|
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 {},
|
"composition_config": parsed_composition_config if _composition_config_active(parsed_composition_config) else {},
|
||||||
"role_graph": role_graph,
|
"role_graph": role_graph,
|
||||||
"source_role_graph": source_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_character_labels": pov_character_labels,
|
||||||
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
|
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
|
||||||
"cast_descriptors": cast_descriptor_text,
|
"cast_descriptors": cast_descriptor_text,
|
||||||
@@ -7170,6 +7240,10 @@ def _build_custom_row(
|
|||||||
"content_seed_axis": content_axis,
|
"content_seed_axis": content_axis,
|
||||||
"role_graph": role_graph,
|
"role_graph": role_graph,
|
||||||
"source_role_graph": source_role_graph,
|
"source_role_graph": source_role_graph,
|
||||||
|
"action_family": action_family,
|
||||||
|
"position_family": position_family,
|
||||||
|
"position_key": position_key,
|
||||||
|
"position_keys": position_keys,
|
||||||
"source_composition": source_composition,
|
"source_composition": source_composition,
|
||||||
"pov_character_labels": pov_character_labels,
|
"pov_character_labels": pov_character_labels,
|
||||||
"pov_prompt_directive": _pov_prompt_directive(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:
|
def smoke_hardcore_category_routes() -> None:
|
||||||
cast = _character_cast()
|
cast = _character_cast()
|
||||||
cases = [
|
cases = [
|
||||||
("hardcore_penetration", "Penetrative sex", "penetration_only"),
|
("hardcore_penetration", "Penetrative sex", "penetration_only", "penetrative", {"penetration", "default"}),
|
||||||
("hardcore_oral", "Oral sex", "oral_only"),
|
("hardcore_oral", "Oral sex", "oral_only", "oral", {"oral"}),
|
||||||
("hardcore_manual", "Manual stimulation", "manual_only"),
|
("hardcore_manual", "Manual stimulation", "manual_only", "manual", {"foreplay", "outercourse"}),
|
||||||
("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only"),
|
("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only", "outercourse", {"outercourse"}),
|
||||||
("hardcore_foreplay", "Foreplay and teasing", "foreplay_only"),
|
("hardcore_foreplay", "Foreplay and teasing", "foreplay_only", "foreplay", {"foreplay"}),
|
||||||
("hardcore_aftercare", "Aftercare and cleanup", "interaction_only"),
|
("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(
|
row = _prompt_row(
|
||||||
name=name,
|
name=name,
|
||||||
category="Hardcore sexual poses",
|
category="Hardcore sexual poses",
|
||||||
@@ -384,6 +384,9 @@ def smoke_hardcore_category_routes() -> None:
|
|||||||
)
|
)
|
||||||
_expect_custom_row(row, name)
|
_expect_custom_row(row, name)
|
||||||
_expect(row.get("subject_type") == "configured_cast", f"{name} should use configured cast")
|
_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")
|
_expect_formatter_outputs(row, name, target="single")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user