From 58abbaa347d5085c04bb2dec0edceecbc63ef89a Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 10:09:20 +0200 Subject: [PATCH] Add row assembly request object --- docs/prompt-architecture-improvement-plan.md | 9 +- docs/prompt-pool-routing-map.md | 2 +- prompt_builder.py | 7 +- row_assembly.py | 286 ++++++++++--------- tools/prompt_smoke.py | 6 +- 5 files changed, 159 insertions(+), 151 deletions(-) diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 2dbce08..82ffca3 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -253,10 +253,11 @@ Already isolated: sanitation before metadata leaves generation. It also copies side-specific pair metadata, such as soft partner styling and hardcore clothing/detail state, onto the embedded soft/hard rows. -- final custom-row assembly now lives in `row_assembly.py`, covering render - context population, prompt/caption rendering delegation, row-base indexing, - row metadata copying, configured-cast count metadata, profile/slot metadata, - and disabled-expression cleanup. +- final custom-row assembly now lives in `row_assembly.py` behind + `CustomRowAssemblyRequest`, covering render context population, + prompt/caption rendering delegation, row-base indexing, row metadata copying, + configured-cast count metadata, profile/slot metadata, and + disabled-expression cleanup. ### Pair / Adapter Layer diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 07d8421..83ed26c 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -73,7 +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 template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. | -| `row_assembly.py` | Final custom-row dictionary assembly, 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_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. | diff --git a/prompt_builder.py b/prompt_builder.py index ffc860b..6cc6eb0 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -2086,8 +2086,8 @@ def _prompt_axes_route( ) -def _assemble_custom_row(**kwargs: Any) -> dict[str, Any]: - return row_assembly_policy.assemble_custom_row(**kwargs) +def _assemble_custom_row(request: row_assembly_policy.CustomRowAssemblyRequest) -> dict[str, Any]: + return row_assembly_policy.assemble_custom_row(request) def _build_custom_row( @@ -2264,7 +2264,7 @@ def _build_custom_row( ) item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"])) - return _assemble_custom_row( + assembly_request = row_assembly_policy.CustomRowAssemblyRequest( row_number=row_number, start_index=start_index, category=category, @@ -2318,6 +2318,7 @@ def _build_custom_row( slot_status=slot_status, character_slots=character_slots, ) + return _assemble_custom_row(assembly_request) def build_prompt( diff --git a/row_assembly.py b/row_assembly.py index ca8bbd2..3145f71 100644 --- a/row_assembly.py +++ b/row_assembly.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any try: @@ -16,181 +17,184 @@ except ImportError: # Allows local smoke tests from the repository root. import row_rendering as row_rendering_policy -def assemble_custom_row( - *, - row_number: int, - start_index: int, - category: dict[str, Any], - subcategory: dict[str, Any], - item: Any, - context: dict[str, Any], - subject_type: str, - item_text: str, - item_name: str, - item_axis_values: dict[str, Any], - item_template_metadata: dict[str, Any], - formatter_hints: dict[str, Any], - item_label: str, - style: str, - positive_suffix: str, - negative_prompt: str, - scene_slug: str, - scene: str, - pose: str, - expression: str, - shared_expression: str, - character_expressions: list[str], - character_expression_text: str, - expression_disabled: bool, - expression_intensity: float | None, - expression_intensity_source: str, - composition: str, - source_composition: str, - role_graph: str, - source_role_graph: str, - action_family: str, - position_family: str, - position_key: str, - position_keys: list[str], - pov_character_labels: list[str], - cast_descriptors: list[str], - cast_descriptor_text: str, - seed_config: dict[str, int], - hardcore_position_config: dict[str, Any] | None = None, - location_config: dict[str, Any] | None = None, - composition_config: dict[str, Any] | None = None, - content_seed_axis: str = "content", - count_adjustment: dict[str, Any] | None = None, - applied_profile: dict[str, Any] | None = None, - profile_status: str = "none", - applied_slot: dict[str, Any] | None = None, - slot_status: str = "none", - character_slots: list[dict[str, Any]] | None = None, -) -> dict[str, Any]: - render_context = dict(context) - pov_prompt_directive = pov_policy.pov_prompt_directive(pov_character_labels) +@dataclass(frozen=True) +class CustomRowAssemblyRequest: + row_number: int + start_index: int + category: dict[str, Any] + subcategory: dict[str, Any] + item: Any + context: dict[str, Any] + subject_type: str + item_text: str + item_name: str + item_axis_values: dict[str, Any] + item_template_metadata: dict[str, Any] + formatter_hints: dict[str, Any] + item_label: str + style: str + positive_suffix: str + negative_prompt: str + scene_slug: str + scene: str + pose: str + expression: str + shared_expression: str + character_expressions: list[str] + character_expression_text: str + expression_disabled: bool + expression_intensity: float | None + expression_intensity_source: str + composition: str + source_composition: str + role_graph: str + source_role_graph: str + action_family: str + position_family: str + position_key: str + position_keys: list[str] + pov_character_labels: list[str] + cast_descriptors: list[str] + cast_descriptor_text: str + seed_config: dict[str, Any] + hardcore_position_config: dict[str, Any] | None = None + location_config: dict[str, Any] | None = None + composition_config: dict[str, Any] | None = None + content_seed_axis: str = "content" + count_adjustment: dict[str, Any] | None = None + applied_profile: dict[str, Any] | None = None + profile_status: str = "none" + applied_slot: dict[str, Any] | None = None + slot_status: str = "none" + character_slots: list[dict[str, Any]] | None = None + + +def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]: + r = request + render_context = dict(r.context) + pov_prompt_directive = pov_policy.pov_prompt_directive(r.pov_character_labels) render_context.update( { "trigger": g.TRIGGER, - "main_category": category["name"], - "subcategory": subcategory["name"], - "category": category["name"], - "item": item_text, - "item_name": item_name, - "item_label": item_label, - "style": style, - "scene": scene, - "scene_slug": scene_slug, - "pose": pose, - "expression": expression, - "shared_expression": shared_expression, - "character_expressions": character_expressions, - "character_expression_text": character_expression_text, - "expression_enabled": not expression_disabled, - "expression_disabled": expression_disabled, - "expression_intensity": expression_intensity, - "expression_intensity_source": expression_intensity_source, - "composition": composition, - "source_composition": source_composition, - "composition_prompt": row_camera_policy.composition_prompt(composition), - "composition_config": composition_config or {}, - "role_graph": role_graph, - "source_role_graph": source_role_graph, - "action_family": action_family, - "position_family": position_family, - "position_key": position_key, - "position_keys": position_keys, - "pov_character_labels": pov_character_labels, + "main_category": r.category["name"], + "subcategory": r.subcategory["name"], + "category": r.category["name"], + "item": r.item_text, + "item_name": r.item_name, + "item_label": r.item_label, + "style": r.style, + "scene": r.scene, + "scene_slug": r.scene_slug, + "pose": r.pose, + "expression": r.expression, + "shared_expression": r.shared_expression, + "character_expressions": r.character_expressions, + "character_expression_text": r.character_expression_text, + "expression_enabled": not r.expression_disabled, + "expression_disabled": r.expression_disabled, + "expression_intensity": r.expression_intensity, + "expression_intensity_source": r.expression_intensity_source, + "composition": r.composition, + "source_composition": r.source_composition, + "composition_prompt": row_camera_policy.composition_prompt(r.composition), + "composition_config": r.composition_config or {}, + "role_graph": r.role_graph, + "source_role_graph": r.source_role_graph, + "action_family": r.action_family, + "position_family": r.position_family, + "position_key": r.position_key, + "position_keys": r.position_keys, + "pov_character_labels": r.pov_character_labels, "pov_prompt_directive": pov_prompt_directive, - "cast_descriptors": cast_descriptor_text, - "positive_suffix": positive_suffix, - "negative_prompt": negative_prompt, + "cast_descriptors": r.cast_descriptor_text, + "positive_suffix": r.positive_suffix, + "negative_prompt": r.negative_prompt, } ) rendered = row_rendering_policy.render_prompt_caption( - item=item, - subcategory=subcategory, - category=category, - subject_type=subject_type, + item=r.item, + subcategory=r.subcategory, + category=r.category, + subject_type=r.subject_type, context=render_context, - cast_descriptor_text=cast_descriptor_text, - pov_prompt_directive=pov_prompt_directive if pov_character_labels else "", + cast_descriptor_text=r.cast_descriptor_text, + pov_prompt_directive=pov_prompt_directive if r.pov_character_labels else "", ) - batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1) - index = start_index + row_number - 1 + batch = max(1, ((r.row_number - 1) // g.BATCH_SIZE) + 1) + index = r.start_index + r.row_number - 1 row = g.row_base( index, batch, render_context["subject"], render_context["age"], render_context["body"], - scene_slug, - composition, + r.scene_slug, + r.composition, ) row.update( { "prompt": rendered["prompt"], "caption": rendered["caption"], - "negative_prompt": negative_prompt, - "expression": expression, - "main_category": category["name"], - "subcategory": subcategory["name"], - "category_slug": category["slug"], - "subcategory_slug": subcategory["slug"], - "subject_type": subject_type, + "negative_prompt": r.negative_prompt, + "expression": r.expression, + "main_category": r.category["name"], + "subcategory": r.subcategory["name"], + "category_slug": r.category["slug"], + "subcategory_slug": r.subcategory["slug"], + "subject_type": r.subject_type, "subject_phrase": render_context.get("subject_phrase", ""), "body_phrase": render_context.get("body_phrase", ""), "skin": render_context.get("skin", ""), "hair": render_context.get("hair", ""), "eyes": render_context.get("eyes", ""), - "style": style, - "item": item_text, - "item_label": item_label, - "positive_suffix": positive_suffix, - "custom_item": item_name, - "item_axis_values": item_axis_values, - "item_template_metadata": item_template_metadata, - "formatter_hints": formatter_hints, - "scene_text": scene, - "location_config": location_config or {}, - "pose": pose, - "seed_config": seed_config, - "hardcore_position_config": hardcore_position_config or {}, - "content_seed_axis": content_seed_axis, - "role_graph": role_graph, - "source_role_graph": source_role_graph, - "action_family": action_family, - "position_family": position_family, - "position_key": position_key, - "position_keys": position_keys, - "source_composition": source_composition, - "pov_character_labels": pov_character_labels, + "style": r.style, + "item": r.item_text, + "item_label": r.item_label, + "positive_suffix": r.positive_suffix, + "custom_item": r.item_name, + "item_axis_values": r.item_axis_values, + "item_template_metadata": r.item_template_metadata, + "formatter_hints": r.formatter_hints, + "scene_text": r.scene, + "location_config": r.location_config or {}, + "pose": r.pose, + "seed_config": r.seed_config, + "hardcore_position_config": r.hardcore_position_config or {}, + "content_seed_axis": r.content_seed_axis, + "role_graph": r.role_graph, + "source_role_graph": r.source_role_graph, + "action_family": r.action_family, + "position_family": r.position_family, + "position_key": r.position_key, + "position_keys": r.position_keys, + "source_composition": r.source_composition, + "pov_character_labels": r.pov_character_labels, "pov_prompt_directive": pov_prompt_directive, - "shared_expression": shared_expression, - "character_expressions": character_expressions, - "character_expression_text": character_expression_text, - "expression_enabled": not expression_disabled, - "expression_disabled": expression_disabled, + "shared_expression": r.shared_expression, + "character_expressions": r.character_expressions, + "character_expression_text": r.character_expression_text, + "expression_enabled": not r.expression_disabled, + "expression_disabled": r.expression_disabled, "cast_summary": render_context.get("cast_summary", ""), - "cast_descriptors": cast_descriptors, - "cast_descriptor_text": cast_descriptor_text, + "cast_descriptors": r.cast_descriptors, + "cast_descriptor_text": r.cast_descriptor_text, "scene_kind": render_context.get("scene_kind", ""), "women_count": render_context.get("women_count", ""), "men_count": render_context.get("men_count", ""), "person_count": render_context.get("person_count", ""), - "cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {}, - "character_profile": applied_profile or {}, - "character_profile_status": profile_status, - "character_slot": applied_slot or {}, - "character_slot_status": slot_status, - "character_cast_slots": character_slots or [], - "expression_intensity": expression_intensity, - "expression_intensity_source": expression_intensity_source, + "cast_count_adjustment": r.count_adjustment if r.subject_type == "configured_cast" else {}, + "character_profile": r.applied_profile or {}, + "character_profile_status": r.profile_status, + "character_slot": r.applied_slot or {}, + "character_slot_status": r.slot_status, + "character_cast_slots": r.character_slots or [], + "expression_intensity": r.expression_intensity, + "expression_intensity_source": r.expression_intensity_source, "source": "json_category", } ) if render_context.get("figure"): row["figure"] = render_context["figure"] - if expression_disabled: - row = row_expression_policy.disable_row_expression(row, expression_intensity_source) + if r.expression_disabled: + row = row_expression_policy.disable_row_expression(row, r.expression_intensity_source) return row diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 54cd0ee..9ca745e 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -1761,10 +1761,12 @@ def smoke_row_assembly_policy() -> None: "slot_status": "applied", "character_slots": [{"label": "Woman A"}, {"label": "Man A"}], } - row = row_assembly.assemble_custom_row(**kwargs) - delegated = pb._assemble_custom_row(**kwargs) + request = row_assembly.CustomRowAssemblyRequest(**kwargs) + row = row_assembly.assemble_custom_row(request) + delegated = pb._assemble_custom_row(request) _expect(row == delegated, "Prompt builder row assembly wrapper should delegate without changing output") + _expect(request.content_seed_axis == "pose", "Row assembly request lost seed-axis metadata") _expect(row["id"] == "sxcp_0011", "Row assembly changed row indexing") _expect(row["batch"] == "batch_001", "Row assembly changed batch calculation") _expect(row["source"] == "json_category", "Row assembly lost source marker")