Use shared item axis context in role routes
This commit is contained in:
@@ -5,6 +5,11 @@ import re
|
|||||||
from string import Formatter
|
from string import Formatter
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
HARDCORE_POSITION_FAMILY_CHOICES = [
|
HARDCORE_POSITION_FAMILY_CHOICES = [
|
||||||
"any",
|
"any",
|
||||||
@@ -847,10 +852,7 @@ def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[st
|
|||||||
|
|
||||||
|
|
||||||
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
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()]
|
text = item_axis_policy.context_text(*parts, axis_values=axis_values)
|
||||||
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:
|
if not text:
|
||||||
return []
|
return []
|
||||||
keys: list[str] = []
|
keys: list[str] = []
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _anal_position_graph(woman: str, man: str, context: str) -> str:
|
def _anal_position_graph(woman: str, man: str, context: str) -> str:
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _mentions_ass(text: str) -> bool:
|
def _mentions_ass(text: str) -> bool:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import random
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
from .hardcore_role_anal import build_anal_or_double_role_graph
|
from .hardcore_role_anal import build_anal_or_double_role_graph
|
||||||
from .hardcore_role_climax import build_climax_role_graph
|
from .hardcore_role_climax import build_climax_role_graph
|
||||||
from .hardcore_role_fallback import (
|
from .hardcore_role_fallback import (
|
||||||
@@ -23,6 +24,7 @@ try:
|
|||||||
from .hardcore_role_outercourse import build_outercourse_role_graph
|
from .hardcore_role_outercourse import build_outercourse_role_graph
|
||||||
from .hardcore_role_penetration import build_penetration_role_graph
|
from .hardcore_role_penetration import build_penetration_role_graph
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
import item_axis_policy
|
||||||
from hardcore_role_anal import build_anal_or_double_role_graph
|
from hardcore_role_anal import build_anal_or_double_role_graph
|
||||||
from hardcore_role_climax import build_climax_role_graph
|
from hardcore_role_climax import build_climax_role_graph
|
||||||
from hardcore_role_fallback import (
|
from hardcore_role_fallback import (
|
||||||
@@ -85,7 +87,7 @@ def build_hardcore_role_graph(
|
|||||||
men = participants["men"]
|
men = participants["men"]
|
||||||
people = participants["people"]
|
people = participants["people"]
|
||||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
||||||
item_text = " ".join((item_axis_values or {}).values()).lower()
|
item_text = item_axis_policy.context_text(axis_values=item_axis_values)
|
||||||
|
|
||||||
def any_person(exclude: set[str] | None = None) -> str:
|
def any_person(exclude: set[str] | None = None) -> str:
|
||||||
exclude = exclude or set()
|
exclude = exclude or set()
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_foreplay_role_graph(
|
def build_foreplay_role_graph(
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _oral_direction(text: str) -> tuple[bool, bool]:
|
def _oral_direction(text: str) -> tuple[bool, bool]:
|
||||||
@@ -54,7 +53,7 @@ def build_oral_role_graph(
|
|||||||
item_axis_values: dict[str, Any] | None = None,
|
item_axis_values: dict[str, Any] | None = None,
|
||||||
pov_labels: list[str] | None = None,
|
pov_labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
position_text = item_axis_policy.key_text(item_axis_values, "position")
|
||||||
text = _context_text(item_text, item_axis_values)
|
text = _context_text(item_text, item_axis_values)
|
||||||
man_is_pov = man in set(pov_labels or [])
|
man_is_pov = man in set(pov_labels or [])
|
||||||
woman_gives, man_gives = _oral_direction(text)
|
woman_gives, man_gives = _oral_direction(text)
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
from . import outercourse_action_policy as outercourse_policy
|
from . import outercourse_action_policy as outercourse_policy
|
||||||
except ImportError: # Allows local smoke tests with top-level imports.
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
import outercourse_action_policy as outercourse_policy
|
import outercourse_action_policy as outercourse_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_outercourse_role_graph(
|
def build_outercourse_role_graph(
|
||||||
@@ -25,7 +21,7 @@ def build_outercourse_role_graph(
|
|||||||
item_axis_values: dict[str, Any] | None = None,
|
item_axis_values: dict[str, Any] | None = None,
|
||||||
pov_labels: list[str] | None = None,
|
pov_labels: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
position_text = str((item_axis_values or {}).get("position") or "").lower()
|
position_text = item_axis_policy.key_text(item_axis_values, "position")
|
||||||
text = _context_text(item_text, item_axis_values)
|
text = _context_text(item_text, item_axis_values)
|
||||||
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
|
action_kind = outercourse_policy.infer_outercourse_action_kind(position_text)
|
||||||
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
if action_kind == outercourse_policy.OUTERCOURSE_GENERIC:
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
|
||||||
return " ".join(
|
return item_axis_policy.context_text(item_text, axis_values=item_axis_values)
|
||||||
str(part or "").lower()
|
|
||||||
for part in (
|
|
||||||
item_text,
|
|
||||||
*((item_axis_values or {}).values()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_penetration_role_graph(
|
def build_penetration_role_graph(
|
||||||
|
|||||||
@@ -108,6 +108,19 @@ def action_context_text(axis_values: Any) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def context_text(*parts: Any, axis_values: Any = None) -> str:
|
||||||
|
text_parts = [clean_text(part) for part in parts if clean_text(part)]
|
||||||
|
text_parts.extend(axis_value_texts(axis_values))
|
||||||
|
return " ".join(part.lower() for part in text_parts if part)
|
||||||
|
|
||||||
|
|
||||||
|
def key_text(axis_values: Any, key: str) -> str:
|
||||||
|
if not isinstance(axis_values, dict):
|
||||||
|
return ""
|
||||||
|
values = value_texts(axis_values.get(key))
|
||||||
|
return values[0].lower() if values else ""
|
||||||
|
|
||||||
|
|
||||||
def row_axis_value_texts(
|
def row_axis_value_texts(
|
||||||
row: dict[str, Any],
|
row: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
|
|||||||
+9
-3
@@ -4,6 +4,11 @@ from dataclasses import dataclass
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import item_axis_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import item_axis_policy
|
||||||
|
|
||||||
|
|
||||||
WOMAN_LOWER_ACCESS_TERMS = (
|
WOMAN_LOWER_ACCESS_TERMS = (
|
||||||
"penetrat",
|
"penetrat",
|
||||||
@@ -237,7 +242,7 @@ def character_hardcore_clothing_entries(
|
|||||||
|
|
||||||
def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
|
def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
|
||||||
axis_values = row.get("item_axis_values")
|
axis_values = row.get("item_axis_values")
|
||||||
axis_text = " ".join(str(value) for value in axis_values.values()) if isinstance(axis_values, dict) else ""
|
axis_text = item_axis_policy.context_text(axis_values=axis_values)
|
||||||
role_text = " ".join(
|
role_text = " ".join(
|
||||||
str(part or "")
|
str(part or "")
|
||||||
for part in (
|
for part in (
|
||||||
@@ -255,10 +260,11 @@ def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
|
|||||||
)
|
)
|
||||||
).lower()
|
).lower()
|
||||||
full_text = f"{role_text} {detail_text}"
|
full_text = f"{role_text} {detail_text}"
|
||||||
|
lower_access_text = f"{role_text} {axis_text}"
|
||||||
return {
|
return {
|
||||||
"woman_lower": any(term in role_text for term in WOMAN_LOWER_ACCESS_TERMS),
|
"woman_lower": any(term in lower_access_text for term in WOMAN_LOWER_ACCESS_TERMS),
|
||||||
"woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS),
|
"woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS),
|
||||||
"man_lower": any(term in role_text for term in MAN_LOWER_ACCESS_TERMS),
|
"man_lower": any(term in lower_access_text for term in MAN_LOWER_ACCESS_TERMS),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3439,6 +3439,22 @@ def smoke_row_role_graph_policy() -> None:
|
|||||||
pov_route.role_graph.startswith("First-person POV from Man A;"),
|
pov_route.role_graph.startswith("First-person POV from Man A;"),
|
||||||
"Role graph route did not prepend POV role graph directive",
|
"Role graph route did not prepend POV role graph directive",
|
||||||
)
|
)
|
||||||
|
structured_axis_route = row_role_graph.resolve_role_graph_route(
|
||||||
|
rng=random.Random(54),
|
||||||
|
subcategory={"slug": "oral", "name": "Oral"},
|
||||||
|
context=context,
|
||||||
|
item_axis_values={
|
||||||
|
"position": {"text": "standing oral position"},
|
||||||
|
"oral_act": ["blowjob", "auto"],
|
||||||
|
"contact_detail": {"unused": "mouth contact at hip height"},
|
||||||
|
},
|
||||||
|
pov_character_labels=[],
|
||||||
|
is_pose_category=False,
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
"kneels in front" in structured_axis_route.source_role_graph,
|
||||||
|
"Role graph route should read structured/list axis values through item_axis_policy",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def smoke_row_assembly_policy() -> None:
|
def smoke_row_assembly_policy() -> None:
|
||||||
@@ -5876,6 +5892,24 @@ def smoke_pair_route_policy() -> None:
|
|||||||
partial_common = {**clothing_common, "mode": "partially_removed"}
|
partial_common = {**clothing_common, "mode": "partially_removed"}
|
||||||
partial_route = pair_clothing.resolve_hardcore_pair_clothing_result(**partial_common)
|
partial_route = pair_clothing.resolve_hardcore_pair_clothing_result(**partial_common)
|
||||||
_expect(partial_route.requires_body_exposure_scene is True, "Partial lower-access clothing should request scene cleanup")
|
_expect(partial_route.requires_body_exposure_scene is True, "Partial lower-access clothing should request scene cleanup")
|
||||||
|
structured_axis_clothing = pair_clothing.resolve_hardcore_pair_clothing_result(
|
||||||
|
**{
|
||||||
|
**clothing_common,
|
||||||
|
"hard_row": {
|
||||||
|
"role_graph": "generic adult action",
|
||||||
|
"item": "generic configured action",
|
||||||
|
"item_axis_values": {
|
||||||
|
"position": {"text": "edge-supported penetration"},
|
||||||
|
"leg_detail": ["thighs open", "auto"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mode": "partially_removed",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
structured_axis_clothing.woman_access == "lower",
|
||||||
|
"Pair clothing should read structured/list axis values for lower-access detection",
|
||||||
|
)
|
||||||
oral_common = {
|
oral_common = {
|
||||||
**clothing_common,
|
**clothing_common,
|
||||||
"hard_row": {
|
"hard_row": {
|
||||||
|
|||||||
Reference in New Issue
Block a user