diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 64623a4..951c7e2 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -138,9 +138,11 @@ Already isolated: - row prompt/caption template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion live in `row_rendering.py`; `prompt_builder.py` keeps compatibility aliases. -- row action/position route metadata resolution, template metadata precedence, - inferred position-key merging, and source action-family fallback live in - `row_route_metadata.py`; `prompt_builder.py` keeps a public delegate wrapper. +- row action/position route metadata resolution lives in + `row_route_metadata.py` behind `ActionPositionRoute`, covering template + metadata precedence, inferred position-key merging, legacy dict + compatibility, and source action-family fallback; `prompt_builder.py` keeps + public delegate wrappers. - built-in legacy row generation, auto-weighted/auto-full selection, row mode randomization, ratio clamps, and expression-intensity randomization live in `row_generation.py`; `prompt_builder.py` keeps public delegate wrappers. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index a5940c9..9c6952c 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -75,7 +75,7 @@ Core helper ownership: | `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_route_metadata.py` | Row action/position route metadata resolution behind `ActionPositionRoute`, template metadata precedence, inferred position-key merging, legacy dict compatibility, 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. | | `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. | | `cast_context.py` | Generation-time cast count phrases, configured-cast context metadata, character-slot label assignment, cast-summary wording, scene-kind labels, and couple count normalization. | diff --git a/prompt_builder.py b/prompt_builder.py index cbec4f6..388aee6 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -267,6 +267,31 @@ def _action_position_route_metadata( ) +def _action_position_route( + *, + is_pose_category: bool, + subcategory: dict[str, Any], + hardcore_position_config: dict[str, Any] | None, + item_template_metadata: dict[str, Any] | None, + item_text: Any, + source_role_graph: Any, + source_composition: Any, + pose: Any, + item_axis_values: dict[str, Any] | None = None, +) -> row_route_policy.ActionPositionRoute: + return row_route_policy.resolve_action_position_route_result( + is_pose_category=is_pose_category, + subcategory=subcategory, + hardcore_position_config=hardcore_position_config, + item_template_metadata=item_template_metadata, + item_text=item_text, + source_role_graph=source_role_graph, + source_composition=source_composition, + pose=pose, + item_axis_values=item_axis_values, + ) + + def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]: return row_item_policy.oral_acts_for_position(values, position) @@ -2286,7 +2311,7 @@ def _build_custom_row( character_expression_text = str(prompt_axes.get("character_expression_text") or "") source_composition = str(prompt_axes.get("source_composition") or "") composition = str(prompt_axes.get("composition") or "") - action_route = _action_position_route_metadata( + action_route = _action_position_route( is_pose_category=is_pose_category, subcategory=subcategory, hardcore_position_config=parsed_hardcore_position_config, @@ -2297,10 +2322,10 @@ def _build_custom_row( pose=pose, item_axis_values=item_axis_values, ) - position_family = str(action_route.get("position_family") or "") - position_keys = list(action_route.get("position_keys") or []) - position_key = str(action_route.get("position_key") or "") - action_family = str(action_route.get("action_family") or "") + position_family = action_route.position_family + position_keys = list(action_route.position_keys) + position_key = action_route.position_key + action_family = action_route.action_family text_fields = _row_text_fields(category, subcategory, item) diff --git a/row_route_metadata.py b/row_route_metadata.py index 0b11abd..9e5c475 100644 --- a/row_route_metadata.py +++ b/row_route_metadata.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any try: @@ -20,16 +21,36 @@ EMPTY_ACTION_POSITION_ROUTE = { } +@dataclass(frozen=True) +class ActionPositionRoute: + position_family: str + position_keys: list[str] + position_key: str + action_family: str + + def as_dict(self) -> dict[str, Any]: + return { + "position_family": self.position_family, + "position_keys": list(self.position_keys), + "position_key": self.position_key, + "action_family": self.action_family, + } + + +def empty_action_position_route_result() -> ActionPositionRoute: + return ActionPositionRoute( + position_family="", + position_keys=[], + position_key="", + action_family="", + ) + + def empty_action_position_route() -> dict[str, Any]: - return { - "position_family": "", - "position_keys": [], - "position_key": "", - "action_family": "", - } + return empty_action_position_route_result().as_dict() -def resolve_action_position_route( +def resolve_action_position_route_result( *, is_pose_category: bool, subcategory: dict[str, Any], @@ -40,9 +61,9 @@ def resolve_action_position_route( source_composition: Any, pose: Any, item_axis_values: dict[str, Any] | None = None, -) -> dict[str, Any]: +) -> ActionPositionRoute: if not is_pose_category: - return empty_action_position_route() + return empty_action_position_route_result() metadata = item_template_metadata or {} position_family = template_policy.template_position_family( @@ -72,9 +93,34 @@ def resolve_action_position_route( item_axis_values, ) - return { - "position_family": position_family, - "position_keys": position_keys, - "position_key": position_keys[0] if position_keys else "", - "action_family": action_family, - } + return ActionPositionRoute( + position_family=position_family, + position_keys=position_keys, + position_key=position_keys[0] if position_keys else "", + action_family=action_family, + ) + + +def resolve_action_position_route( + *, + is_pose_category: bool, + subcategory: dict[str, Any], + hardcore_position_config: dict[str, Any] | None, + item_template_metadata: dict[str, Any] | None, + item_text: Any, + source_role_graph: Any, + source_composition: Any, + pose: Any, + item_axis_values: dict[str, Any] | None = None, +) -> dict[str, Any]: + return resolve_action_position_route_result( + is_pose_category=is_pose_category, + subcategory=subcategory, + hardcore_position_config=hardcore_position_config, + item_template_metadata=item_template_metadata, + item_text=item_text, + source_role_graph=source_role_graph, + source_composition=source_composition, + pose=pose, + item_axis_values=item_axis_values, + ).as_dict() diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 0a2f522..be6eacf 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -2445,6 +2445,19 @@ def smoke_row_route_metadata_policy() -> None: _expect(route["position_family"] == "oral", "Route policy lost template position family") _expect(route["position_key"] == "kneeling", "Route policy did not preserve first template position key") _expect(route["position_keys"] == ["kneeling", "open_thighs"], "Route policy changed template position-key precedence") + route_result = row_route_metadata.resolve_action_position_route_result( + is_pose_category=True, + subcategory={"slug": "oral_sex"}, + hardcore_position_config={}, + item_template_metadata=template_metadata, + item_text="mouth contact in kneeling oral position", + source_role_graph="the woman kneels in front of the man", + source_composition="close kneeling oral composition", + pose="kneeling pose", + item_axis_values={"position": "kneeling oral position"}, + ) + _expect(route_result.as_dict() == route, "Typed action/position route should match legacy dict route") + _expect(route_result.position_key == "kneeling", "Typed action/position route lost first position key") delegated = pb._action_position_route_metadata( is_pose_category=True, @@ -2458,6 +2471,18 @@ def smoke_row_route_metadata_policy() -> None: item_axis_values={"position": "kneeling oral position"}, ) _expect(delegated == route, "Prompt builder route wrapper should delegate to row_route_metadata") + typed_delegated = pb._action_position_route( + is_pose_category=True, + subcategory={"slug": "oral_sex"}, + hardcore_position_config={}, + item_template_metadata=template_metadata, + item_text="mouth contact in kneeling oral position", + source_role_graph="the woman kneels in front of the man", + source_composition="close kneeling oral composition", + pose="kneeling pose", + item_axis_values={"position": "kneeling oral position"}, + ) + _expect(typed_delegated == route_result, "Prompt builder typed route wrapper should delegate to row_route_metadata") fallback = row_route_metadata.resolve_action_position_route( is_pose_category=True, @@ -2485,6 +2510,10 @@ def smoke_row_route_metadata_policy() -> None: pose="standing pose", ) _expect(empty == row_route_metadata.empty_action_position_route(), "Non-pose route should return empty route metadata") + _expect( + row_route_metadata.empty_action_position_route_result().as_dict() == empty, + "Typed empty action/position route should match legacy dict route", + ) def smoke_category_library_route() -> None: