From 2c978c7eabed8faec993bd37fe69ec62c1da3459 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 10:39:45 +0200 Subject: [PATCH] Add typed category route metadata --- docs/prompt-architecture-improvement-plan.md | 10 ++- docs/prompt-pool-routing-map.md | 2 +- prompt_builder.py | 53 ++++++++--- row_category_route.py | 94 ++++++++++++++++---- tools/prompt_smoke.py | 23 +++++ 5 files changed, 146 insertions(+), 36 deletions(-) diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 835b268..cbfbc9c 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -131,10 +131,12 @@ Already isolated: - row item selection, weighted item/pair choice, item-template axis filling, and oral/outercourse axis compatibility filters live in `row_item.py`; `prompt_builder.py` keeps public delegate wrappers. -- 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 live in - `row_category_route.py`; `prompt_builder.py` keeps public delegate wrappers. +- row category/subcategory/item route resolution lives in + `row_category_route.py` behind `CategoryItemRoute`, covering hardcore + position-category filtering, cast-count adjustment, pose-vs-content seed-axis + choice, item metadata collection, legacy dict compatibility, and + pose-category item sanitizing; `prompt_builder.py` keeps public delegate + wrappers. - 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. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 376373a..3c906a5 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -71,7 +71,7 @@ Core helper ownership: | `category_extensions.py` | JSON `pool_extensions`, legacy pool patching, built-in category choice lists, and category/subcategory UI choices. | | `category_template_metadata.py` | Object-style item-template metadata extraction, action/position family normalization, position-key normalization, key merging, and audit validation errors. | | `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 behind `CategoryItemRoute`, hardcore position-category filtering, cast-count adjustment, pose-vs-content seed-axis choice, item metadata collection, legacy dict compatibility, 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. | diff --git a/prompt_builder.py b/prompt_builder.py index fc92f94..78bb74b 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -833,6 +833,31 @@ def _select_category_item_route( ) +def _select_category_item_route_result( + *, + category_choice: str, + subcategory_choice: str, + seed_config: dict[str, int], + seed: int, + row_number: int, + women_count: int, + men_count: int, + hardcore_position_config: dict[str, Any] | None = None, + categories: list[dict[str, Any]] | None = None, +) -> row_category_route_policy.CategoryItemRoute: + return row_category_route_policy.select_category_item_route_result( + category_choice=category_choice, + subcategory_choice=subcategory_choice, + seed_config=seed_config, + seed=seed, + row_number=row_number, + women_count=women_count, + men_count=men_count, + hardcore_position_config=hardcore_position_config, + categories=categories, + ) + + def _format(template: str, context: dict[str, Any]) -> str: return row_rendering_policy.format_template(template, context) @@ -2255,7 +2280,7 @@ def _build_custom_row( parsed_location_config = _parse_location_config(location_config) parsed_composition_config = _parse_composition_config(composition_config) - category_route = _select_category_item_route( + category_route = _select_category_item_route_result( category_choice=category_choice, subcategory_choice=subcategory_choice, seed_config=seed_config, @@ -2265,19 +2290,19 @@ def _build_custom_row( men_count=men_count, hardcore_position_config=parsed_hardcore_position_config, ) - category = category_route["category"] - subcategory = category_route["subcategory"] - women_count = int(category_route["women_count"]) - men_count = int(category_route["men_count"]) - count_adjustment = dict(category_route.get("count_adjustment") or {}) - content_axis = str(category_route.get("content_axis") or "content") - item = category_route["item"] - item_text = str(category_route.get("item_text") or "") - item_name = str(category_route.get("item_name") or "") - item_axis_values = dict(category_route.get("item_axis_values") or {}) - item_template_metadata = dict(category_route.get("item_template_metadata") or {}) - item_formatter_hints = dict(category_route.get("formatter_hints") or {}) - is_pose_category = bool(category_route.get("is_pose_category")) + category = category_route.category + subcategory = category_route.subcategory + women_count = category_route.women_count + men_count = category_route.men_count + count_adjustment = dict(category_route.count_adjustment) + content_axis = category_route.content_axis + item = category_route.item + item_text = category_route.item_text + item_name = category_route.item_name + item_axis_values = dict(category_route.item_axis_values) + item_template_metadata = dict(category_route.item_template_metadata) + item_formatter_hints = dict(category_route.formatter_hints) + is_pose_category = category_route.is_pose_category subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any")) subject_route = _subject_route( subject_type=subject_type, diff --git a/row_category_route.py b/row_category_route.py index 6985a19..15e4b65 100644 --- a/row_category_route.py +++ b/row_category_route.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any try: @@ -63,7 +64,41 @@ def cast_count_adjustment( } -def select_category_item_route( +@dataclass(frozen=True) +class CategoryItemRoute: + category: dict[str, Any] + subcategory: dict[str, Any] + women_count: int + men_count: int + count_adjustment: dict[str, int] + content_axis: str + item: Any + item_text: str + item_name: str + item_axis_values: dict[str, Any] + item_template_metadata: dict[str, Any] + formatter_hints: dict[str, Any] + is_pose_category: bool + + def as_dict(self) -> dict[str, Any]: + return { + "category": self.category, + "subcategory": self.subcategory, + "women_count": self.women_count, + "men_count": self.men_count, + "count_adjustment": dict(self.count_adjustment), + "content_axis": self.content_axis, + "item": self.item, + "item_text": self.item_text, + "item_name": self.item_name, + "item_axis_values": dict(self.item_axis_values), + "item_template_metadata": dict(self.item_template_metadata), + "formatter_hints": dict(self.formatter_hints), + "is_pose_category": self.is_pose_category, + } + + +def select_category_item_route_result( *, category_choice: str, subcategory_choice: str, @@ -74,7 +109,7 @@ def select_category_item_route( men_count: int, hardcore_position_config: dict[str, Any] | None = None, categories: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: +) -> CategoryItemRoute: source_categories = category_policy.load_category_library() if categories is None else categories parsed_hardcore_position_config = hardcore_position_config or {} requested_women_count = women_count @@ -126,18 +161,43 @@ def select_category_item_route( item_text = sanitize_hardcore_environment_anchors(item_text) item_axis_values = sanitize_hardcore_axis_values(item_axis_values) - return { - "category": category, - "subcategory": subcategory, - "women_count": women_count, - "men_count": men_count, - "count_adjustment": count_adjustment, - "content_axis": content_axis, - "item": item, - "item_text": item_text, - "item_name": item_name, - "item_axis_values": item_axis_values, - "item_template_metadata": item_template_metadata, - "formatter_hints": template_policy.formatter_hints(item_template_metadata), - "is_pose_category": is_pose_category, - } + return CategoryItemRoute( + category=category, + subcategory=subcategory, + women_count=women_count, + men_count=men_count, + count_adjustment=count_adjustment, + content_axis=content_axis, + item=item, + item_text=item_text, + item_name=item_name, + item_axis_values=item_axis_values, + item_template_metadata=item_template_metadata, + formatter_hints=template_policy.formatter_hints(item_template_metadata), + is_pose_category=is_pose_category, + ) + + +def select_category_item_route( + *, + category_choice: str, + subcategory_choice: str, + seed_config: dict[str, int], + seed: int, + row_number: int, + women_count: int, + men_count: int, + hardcore_position_config: dict[str, Any] | None = None, + categories: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + return select_category_item_route_result( + category_choice=category_choice, + subcategory_choice=subcategory_choice, + seed_config=seed_config, + seed=seed, + row_number=row_number, + women_count=women_count, + men_count=men_count, + hardcore_position_config=hardcore_position_config, + categories=categories, + ).as_dict() diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 4427fb6..4caf472 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -1027,6 +1027,16 @@ def smoke_row_category_route_policy() -> None: men_count=1, hardcore_position_config=hard_config, ) + route_result = row_category_route.select_category_item_route_result( + category_choice="custom_random", + subcategory_choice="Hardcore sexual poses / Oral sex", + seed_config=seed_cfg, + seed=2301, + row_number=1, + women_count=1, + men_count=1, + hardcore_position_config=hard_config, + ) delegated = pb._select_category_item_route( category_choice="custom_random", subcategory_choice="Hardcore sexual poses / Oral sex", @@ -1037,7 +1047,20 @@ def smoke_row_category_route_policy() -> None: men_count=1, hardcore_position_config=hard_config, ) + typed_delegated = pb._select_category_item_route_result( + category_choice="custom_random", + subcategory_choice="Hardcore sexual poses / Oral sex", + seed_config=seed_cfg, + seed=2301, + row_number=1, + women_count=1, + men_count=1, + hardcore_position_config=hard_config, + ) _expect(delegated == route, "Prompt builder category/item route should delegate to row_category_route") + _expect(route_result.as_dict() == route, "Typed category/item route should match legacy dict route") + _expect(typed_delegated == route_result, "Prompt builder typed category/item route should delegate") + _expect(route_result.content_axis == "pose", "Typed category/item route lost content seed axis") _expect(route["category"]["slug"] == "hardcore_sexual_poses", "Row category route selected wrong hardcore category") _expect(route["subcategory"]["slug"] == "oral_sex", "Row category route selected wrong hardcore subcategory") _expect(route["content_axis"] == "pose", "Hardcore pose category should use pose seed axis")