Extract role graph route policy

This commit is contained in:
2026-06-27 10:23:10 +02:00
parent 09eaafc8f6
commit 2b221463ee
5 changed files with 137 additions and 9 deletions
+6 -2
View File
@@ -199,6 +199,10 @@ Already isolated:
safe formatting, configured-cast descriptor insertion, and POV directive safe formatting, configured-cast descriptor insertion, and POV directive
insertion live in `row_rendering.py`; `prompt_builder.py` keeps public insertion live in `row_rendering.py`; `prompt_builder.py` keeps public
delegate wrappers. delegate wrappers.
- row role-graph route sequencing lives in `row_role_graph.py`, covering
hardcore source role graph construction, pose-category environment-anchor
cleanup, and POV role-graph rewriting before prompt axes and formatter
metadata consume the graph.
- row expression text cleanup, expression route resolution, expression - row expression text cleanup, expression route resolution, expression
intensity weighting, character-slot/cast expression override resolution, and intensity weighting, character-slot/cast expression override resolution, and
per-character expression picking plus action-aware character-expression per-character expression picking plus action-aware character-expression
@@ -209,8 +213,8 @@ Already isolated:
policy, position-key detection, category filtering, and item-template/axis policy, position-key detection, category filtering, and item-template/axis
filtering live in `hardcore_position_config.py`. 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`; row generation reaches it through
then asks that module for the source role graph. `row_role_graph.py` after item/axis metadata is selected.
- fallback role graph wording lives in `hardcore_role_fallback.py`, covering - fallback role graph wording lives in `hardcore_role_fallback.py`, covering
solo rows, women-only rows, men-only rows, mixed group fallbacks, and support solo rows, women-only rows, men-only rows, mixed group fallbacks, and support
partner sentences. partner sentences.
+2 -1
View File
@@ -73,6 +73,7 @@ Core helper ownership:
| `row_item.py` | Row item selection, weighted item/pair choice, item-template axis filling, and oral/outercourse axis compatibility filters. | | `row_item.py` | Row item selection, weighted item/pair choice, item-template axis filling, and oral/outercourse axis compatibility filters. |
| `row_category_route.py` | Row category/subcategory/item route resolution, hardcore position-category filtering, cast-count adjustment, pose-vs-content seed-axis choice, item metadata collection, and pose-category item sanitizing. | | `row_category_route.py` | Row category/subcategory/item route resolution, hardcore position-category filtering, cast-count adjustment, pose-vs-content seed-axis choice, item metadata collection, and pose-category item sanitizing. |
| `row_rendering.py` | Row prompt/caption text-field resolution, template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. | | `row_rendering.py` | Row prompt/caption text-field resolution, template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. |
| `row_role_graph.py` | Row role-graph route sequencing, including hardcore source graph construction, pose-category environment-anchor cleanup, and POV role-graph rewriting. |
| `row_assembly.py` | Final custom-row dictionary assembly behind `CustomRowAssemblyRequest`, render-context metadata population, prompt/caption rendering delegation, row-base indexing, cast/profile/slot metadata copying, and disabled-expression cleanup. | | `row_assembly.py` | Final custom-row dictionary assembly behind `CustomRowAssemblyRequest`, render-context metadata population, prompt/caption rendering delegation, row-base indexing, cast/profile/slot metadata copying, and disabled-expression cleanup. |
| `row_route_metadata.py` | Row action/position route metadata resolution, template metadata precedence, inferred position-key merging, and source action-family fallback. | | `row_route_metadata.py` | Row action/position route metadata resolution, template metadata precedence, inferred position-key merging, and source action-family fallback. |
| `row_generation.py` | Built-in legacy row generation, auto-weighted/auto-full selection, row mode randomization, ratio clamps, and expression-intensity randomization. | | `row_generation.py` | Built-in legacy row generation, auto-weighted/auto-full selection, row mode randomization, ratio clamps, and expression-intensity randomization. |
@@ -100,7 +101,7 @@ Core helper ownership:
| `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. |
| `pair_clothing.py` | Insta/OF clothing sentence formatting, body-exposure scene cleanup, hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, and final root clothing-state assembly. | | `pair_clothing.py` | Insta/OF clothing sentence formatting, body-exposure scene cleanup, hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, and final root clothing-state assembly. |
| `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. | | `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. |
| `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry, called through `row_role_graph.py` for row generation. |
| `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. | | `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. |
| `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. | | `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. |
| `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. | | `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. |
+31 -6
View File
@@ -44,6 +44,7 @@ try:
from . import row_prompt_axes as row_prompt_axes_policy from . import row_prompt_axes as row_prompt_axes_policy
from . import row_pools as row_pool_policy from . import row_pools as row_pool_policy
from . import row_rendering as row_rendering_policy from . import row_rendering as row_rendering_policy
from . import row_role_graph as row_role_graph_policy
from . import row_route_metadata as row_route_policy from . import row_route_metadata as row_route_policy
from . import row_subject_route as row_subject_route_policy from . import row_subject_route as row_subject_route_policy
from . import seed_config as seed_policy from . import seed_config as seed_policy
@@ -52,7 +53,6 @@ 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_role_graphs import build_hardcore_role_graph
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
from category_library import ( from category_library import (
compatible_entries as _compatible_entries, compatible_entries as _compatible_entries,
@@ -93,6 +93,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
import row_prompt_axes as row_prompt_axes_policy import row_prompt_axes as row_prompt_axes_policy
import row_pools as row_pool_policy import row_pools as row_pool_policy
import row_rendering as row_rendering_policy import row_rendering as row_rendering_policy
import row_role_graph as row_role_graph_policy
import row_route_metadata as row_route_policy import row_route_metadata as row_route_policy
import row_subject_route as row_subject_route_policy import row_subject_route as row_subject_route_policy
import seed_config as seed_policy import seed_config as seed_policy
@@ -101,7 +102,6 @@ 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_role_graphs import build_hardcore_role_graph
ROOT_DIR = Path(__file__).resolve().parent ROOT_DIR = Path(__file__).resolve().parent
@@ -2121,6 +2121,25 @@ def _prompt_axes_route(
) )
def _role_graph_route(
*,
rng: random.Random,
subcategory: dict[str, Any],
context: dict[str, Any],
item_axis_values: dict[str, Any],
pov_character_labels: list[str],
is_pose_category: bool,
) -> row_role_graph_policy.RoleGraphRoute:
return row_role_graph_policy.resolve_role_graph_route(
rng=rng,
subcategory=subcategory,
context=context,
item_axis_values=item_axis_values,
pov_character_labels=pov_character_labels,
is_pose_category=is_pose_category,
)
def _assemble_custom_row(request: row_assembly_policy.CustomRowAssemblyRequest) -> dict[str, Any]: def _assemble_custom_row(request: row_assembly_policy.CustomRowAssemblyRequest) -> dict[str, Any]:
return row_assembly_policy.assemble_custom_row(request) return row_assembly_policy.assemble_custom_row(request)
@@ -2207,10 +2226,16 @@ def _build_custom_row(
pov_character_labels = list(subject_route.get("pov_character_labels") or []) pov_character_labels = list(subject_route.get("pov_character_labels") or [])
cast_descriptors = list(subject_route.get("cast_descriptors") or []) cast_descriptors = list(subject_route.get("cast_descriptors") or [])
cast_descriptor_text = str(subject_route.get("cast_descriptor_text") or "") cast_descriptor_text = str(subject_route.get("cast_descriptor_text") or "")
source_role_graph = build_hardcore_role_graph(role_rng, subcategory, context, item_axis_values, pov_character_labels) role_graph_route = _role_graph_route(
if is_pose_category: rng=role_rng,
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph) subcategory=subcategory,
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels) context=context,
item_axis_values=item_axis_values,
pov_character_labels=pov_character_labels,
is_pose_category=is_pose_category,
)
source_role_graph = role_graph_route.source_role_graph
role_graph = role_graph_route.role_graph
expression_route = _resolve_expression_route( expression_route = _resolve_expression_route(
expression_enabled=expression_enabled, expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import Any
try:
from . import hardcore_role_graphs
from . import hardcore_text_cleanup
from . import pov_policy
except ImportError: # Allows local smoke tests from the repository root.
import hardcore_role_graphs
import hardcore_text_cleanup
import pov_policy
@dataclass(frozen=True)
class RoleGraphRoute:
source_role_graph: str
role_graph: str
def resolve_role_graph_route(
*,
rng: random.Random,
subcategory: dict[str, Any],
context: dict[str, Any],
item_axis_values: dict[str, Any],
pov_character_labels: list[str],
is_pose_category: bool,
) -> RoleGraphRoute:
source_role_graph = hardcore_role_graphs.build_hardcore_role_graph(
rng,
subcategory,
context,
item_axis_values,
pov_character_labels,
)
if is_pose_category:
source_role_graph = hardcore_text_cleanup.sanitize_hardcore_environment_anchors(source_role_graph)
role_graph = pov_policy.pov_role_graph_prompt(source_role_graph, pov_character_labels)
return RoleGraphRoute(source_role_graph=source_role_graph, role_graph=role_graph)
+56
View File
@@ -61,6 +61,7 @@ import row_location # noqa: E402
import row_pools # noqa: E402 import row_pools # noqa: E402
import row_prompt_axes # noqa: E402 import row_prompt_axes # noqa: E402
import row_rendering # noqa: E402 import row_rendering # noqa: E402
import row_role_graph # noqa: E402
import row_route_metadata # noqa: E402 import row_route_metadata # noqa: E402
import row_subject_route # noqa: E402 import row_subject_route # noqa: E402
import server_routes # noqa: E402 import server_routes # noqa: E402
@@ -1793,6 +1794,60 @@ def smoke_row_rendering_policy() -> None:
) )
def smoke_row_role_graph_policy() -> None:
empty_route = row_role_graph.resolve_role_graph_route(
rng=random.Random(51),
subcategory={"slug": "penetration"},
context={"subject_type": "woman"},
item_axis_values={"position": "missionary"},
pov_character_labels=[],
is_pose_category=True,
)
_expect(empty_route == row_role_graph.RoleGraphRoute("", ""), "Role graph route should stay empty outside configured cast")
context = {
"subject_type": "configured_cast",
"women_count": "1",
"men_count": "1",
}
subcategory = {"slug": "cumshot_climax", "name": "Cumshot and climax"}
axis_values = {"position": "lying at the bed edge with thighs open"}
route = row_role_graph.resolve_role_graph_route(
rng=random.Random(52),
subcategory=subcategory,
context=context,
item_axis_values=axis_values,
pov_character_labels=[],
is_pose_category=True,
)
delegated = pb._role_graph_route(
rng=random.Random(52),
subcategory=subcategory,
context=context,
item_axis_values=axis_values,
pov_character_labels=[],
is_pose_category=True,
)
_expect(route == delegated, "Prompt builder role graph route wrapper should delegate to row_role_graph")
_expect("raised edge" in route.source_role_graph, "Role graph route did not sanitize bed-edge environment anchor")
_expect("bed edge" not in route.source_role_graph.lower(), "Role graph route leaked bed-edge environment anchor")
_expect(route.role_graph == route.source_role_graph, "Role graph route changed non-POV role graph text")
pov_route = row_role_graph.resolve_role_graph_route(
rng=random.Random(53),
subcategory={"slug": "oral", "name": "Oral"},
context=context,
item_axis_values={"position": "standing oral", "act": "blowjob"},
pov_character_labels=["Man A"],
is_pose_category=False,
)
_expect(pov_route.source_role_graph, "Role graph route lost POV source role graph")
_expect(
pov_route.role_graph.startswith("First-person POV from Man A;"),
"Role graph route did not prepend POV role graph directive",
)
def smoke_row_assembly_policy() -> None: def smoke_row_assembly_policy() -> None:
context = { context = {
"subject": "configured cast", "subject": "configured cast",
@@ -4687,6 +4742,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("character_profile_policy", smoke_character_profile_policy), ("character_profile_policy", smoke_character_profile_policy),
("row_normalization_policy", smoke_row_normalization_policy), ("row_normalization_policy", smoke_row_normalization_policy),
("row_rendering_policy", smoke_row_rendering_policy), ("row_rendering_policy", smoke_row_rendering_policy),
("row_role_graph_policy", smoke_row_role_graph_policy),
("row_assembly_policy", smoke_row_assembly_policy), ("row_assembly_policy", smoke_row_assembly_policy),
("formatter_input_policy", smoke_formatter_input_policy), ("formatter_input_policy", smoke_formatter_input_policy),
("formatter_cast_policy", smoke_formatter_cast_policy), ("formatter_cast_policy", smoke_formatter_cast_policy),