diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index aec5912..64623a4 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -199,6 +199,10 @@ Already isolated: safe formatting, configured-cast descriptor insertion, and POV directive insertion live in `row_rendering.py`; `prompt_builder.py` keeps public 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 intensity weighting, character-slot/cast expression override resolution, and 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 filtering live in `hardcore_position_config.py`. - hardcore configured-cast role graph generation lives in - `hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and - then asks that module for the source role graph. + `hardcore_role_graphs.py`; row generation reaches it through + `row_role_graph.py` after item/axis metadata is selected. - fallback role graph wording lives in `hardcore_role_fallback.py`, covering solo rows, women-only rows, men-only rows, mixed group fallbacks, and support partner sentences. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 3c50eea..a5940c9 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -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_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_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_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. | @@ -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_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. | -| `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_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. | diff --git a/prompt_builder.py b/prompt_builder.py index d6911c7..cbec4f6 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -44,6 +44,7 @@ try: from . import row_prompt_axes as row_prompt_axes_policy from . import row_pools as row_pool_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_subject_route as row_subject_route_policy from . import seed_config as seed_policy @@ -52,7 +53,6 @@ try: sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, 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`. from category_library import ( 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_pools as row_pool_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_subject_route as row_subject_route_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_environment_anchors as _sanitize_hardcore_environment_anchors, ) - from hardcore_role_graphs import build_hardcore_role_graph 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]: 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 []) cast_descriptors = list(subject_route.get("cast_descriptors") 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) - if is_pose_category: - source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph) - role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels) + role_graph_route = _role_graph_route( + rng=role_rng, + subcategory=subcategory, + 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_enabled=expression_enabled, expression_intensity=expression_intensity, diff --git a/row_role_graph.py b/row_role_graph.py new file mode 100644 index 0000000..a73da10 --- /dev/null +++ b/row_role_graph.py @@ -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) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 38b9d61..0a2f522 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -61,6 +61,7 @@ import row_location # noqa: E402 import row_pools # noqa: E402 import row_prompt_axes # noqa: E402 import row_rendering # noqa: E402 +import row_role_graph # noqa: E402 import row_route_metadata # noqa: E402 import row_subject_route # 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: context = { "subject": "configured cast", @@ -4687,6 +4742,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("character_profile_policy", smoke_character_profile_policy), ("row_normalization_policy", smoke_row_normalization_policy), ("row_rendering_policy", smoke_row_rendering_policy), + ("row_role_graph_policy", smoke_row_role_graph_policy), ("row_assembly_policy", smoke_row_assembly_policy), ("formatter_input_policy", smoke_formatter_input_policy), ("formatter_cast_policy", smoke_formatter_cast_policy),