Add row assembly request object

This commit is contained in:
2026-06-27 10:09:20 +02:00
parent ddf72a87dd
commit 58abbaa347
5 changed files with 159 additions and 151 deletions
+5 -4
View File
@@ -253,10 +253,11 @@ Already isolated:
sanitation before metadata leaves generation. It also copies side-specific sanitation before metadata leaves generation. It also copies side-specific
pair metadata, such as soft partner styling and hardcore clothing/detail pair metadata, such as soft partner styling and hardcore clothing/detail
state, onto the embedded soft/hard rows. state, onto the embedded soft/hard rows.
- final custom-row assembly now lives in `row_assembly.py`, covering render - final custom-row assembly now lives in `row_assembly.py` behind
context population, prompt/caption rendering delegation, row-base indexing, `CustomRowAssemblyRequest`, covering render context population,
row metadata copying, configured-cast count metadata, profile/slot metadata, prompt/caption rendering delegation, row-base indexing, row metadata copying,
and disabled-expression cleanup. configured-cast count metadata, profile/slot metadata, and
disabled-expression cleanup.
### Pair / Adapter Layer ### Pair / Adapter Layer
+1 -1
View File
@@ -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_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 template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. | | `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_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. |
| `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. | | `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. |
+4 -3
View File
@@ -2086,8 +2086,8 @@ def _prompt_axes_route(
) )
def _assemble_custom_row(**kwargs: Any) -> dict[str, Any]: def _assemble_custom_row(request: row_assembly_policy.CustomRowAssemblyRequest) -> dict[str, Any]:
return row_assembly_policy.assemble_custom_row(**kwargs) return row_assembly_policy.assemble_custom_row(request)
def _build_custom_row( def _build_custom_row(
@@ -2264,7 +2264,7 @@ def _build_custom_row(
) )
item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"])) 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, row_number=row_number,
start_index=start_index, start_index=start_index,
category=category, category=category,
@@ -2318,6 +2318,7 @@ def _build_custom_row(
slot_status=slot_status, slot_status=slot_status,
character_slots=character_slots, character_slots=character_slots,
) )
return _assemble_custom_row(assembly_request)
def build_prompt( def build_prompt(
+145 -141
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any from typing import Any
try: try:
@@ -16,181 +17,184 @@ except ImportError: # Allows local smoke tests from the repository root.
import row_rendering as row_rendering_policy import row_rendering as row_rendering_policy
def assemble_custom_row( @dataclass(frozen=True)
*, class CustomRowAssemblyRequest:
row_number: int, row_number: int
start_index: int, start_index: int
category: dict[str, Any], category: dict[str, Any]
subcategory: dict[str, Any], subcategory: dict[str, Any]
item: Any, item: Any
context: dict[str, Any], context: dict[str, Any]
subject_type: str, subject_type: str
item_text: str, item_text: str
item_name: str, item_name: str
item_axis_values: dict[str, Any], item_axis_values: dict[str, Any]
item_template_metadata: dict[str, Any], item_template_metadata: dict[str, Any]
formatter_hints: dict[str, Any], formatter_hints: dict[str, Any]
item_label: str, item_label: str
style: str, style: str
positive_suffix: str, positive_suffix: str
negative_prompt: str, negative_prompt: str
scene_slug: str, scene_slug: str
scene: str, scene: str
pose: str, pose: str
expression: str, expression: str
shared_expression: str, shared_expression: str
character_expressions: list[str], character_expressions: list[str]
character_expression_text: str, character_expression_text: str
expression_disabled: bool, expression_disabled: bool
expression_intensity: float | None, expression_intensity: float | None
expression_intensity_source: str, expression_intensity_source: str
composition: str, composition: str
source_composition: str, source_composition: str
role_graph: str, role_graph: str
source_role_graph: str, source_role_graph: str
action_family: str, action_family: str
position_family: str, position_family: str
position_key: str, position_key: str
position_keys: list[str], position_keys: list[str]
pov_character_labels: list[str], pov_character_labels: list[str]
cast_descriptors: list[str], cast_descriptors: list[str]
cast_descriptor_text: str, cast_descriptor_text: str
seed_config: dict[str, int], seed_config: dict[str, Any]
hardcore_position_config: dict[str, Any] | None = None, hardcore_position_config: dict[str, Any] | None = None
location_config: dict[str, Any] | None = None, location_config: dict[str, Any] | None = None
composition_config: dict[str, Any] | None = None, composition_config: dict[str, Any] | None = None
content_seed_axis: str = "content", content_seed_axis: str = "content"
count_adjustment: dict[str, Any] | None = None, count_adjustment: dict[str, Any] | None = None
applied_profile: dict[str, Any] | None = None, applied_profile: dict[str, Any] | None = None
profile_status: str = "none", profile_status: str = "none"
applied_slot: dict[str, Any] | None = None, applied_slot: dict[str, Any] | None = None
slot_status: str = "none", slot_status: str = "none"
character_slots: list[dict[str, Any]] | None = 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) 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( render_context.update(
{ {
"trigger": g.TRIGGER, "trigger": g.TRIGGER,
"main_category": category["name"], "main_category": r.category["name"],
"subcategory": subcategory["name"], "subcategory": r.subcategory["name"],
"category": category["name"], "category": r.category["name"],
"item": item_text, "item": r.item_text,
"item_name": item_name, "item_name": r.item_name,
"item_label": item_label, "item_label": r.item_label,
"style": style, "style": r.style,
"scene": scene, "scene": r.scene,
"scene_slug": scene_slug, "scene_slug": r.scene_slug,
"pose": pose, "pose": r.pose,
"expression": expression, "expression": r.expression,
"shared_expression": shared_expression, "shared_expression": r.shared_expression,
"character_expressions": character_expressions, "character_expressions": r.character_expressions,
"character_expression_text": character_expression_text, "character_expression_text": r.character_expression_text,
"expression_enabled": not expression_disabled, "expression_enabled": not r.expression_disabled,
"expression_disabled": expression_disabled, "expression_disabled": r.expression_disabled,
"expression_intensity": expression_intensity, "expression_intensity": r.expression_intensity,
"expression_intensity_source": expression_intensity_source, "expression_intensity_source": r.expression_intensity_source,
"composition": composition, "composition": r.composition,
"source_composition": source_composition, "source_composition": r.source_composition,
"composition_prompt": row_camera_policy.composition_prompt(composition), "composition_prompt": row_camera_policy.composition_prompt(r.composition),
"composition_config": composition_config or {}, "composition_config": r.composition_config or {},
"role_graph": role_graph, "role_graph": r.role_graph,
"source_role_graph": source_role_graph, "source_role_graph": r.source_role_graph,
"action_family": action_family, "action_family": r.action_family,
"position_family": position_family, "position_family": r.position_family,
"position_key": position_key, "position_key": r.position_key,
"position_keys": position_keys, "position_keys": r.position_keys,
"pov_character_labels": pov_character_labels, "pov_character_labels": r.pov_character_labels,
"pov_prompt_directive": pov_prompt_directive, "pov_prompt_directive": pov_prompt_directive,
"cast_descriptors": cast_descriptor_text, "cast_descriptors": r.cast_descriptor_text,
"positive_suffix": positive_suffix, "positive_suffix": r.positive_suffix,
"negative_prompt": negative_prompt, "negative_prompt": r.negative_prompt,
} }
) )
rendered = row_rendering_policy.render_prompt_caption( rendered = row_rendering_policy.render_prompt_caption(
item=item, item=r.item,
subcategory=subcategory, subcategory=r.subcategory,
category=category, category=r.category,
subject_type=subject_type, subject_type=r.subject_type,
context=render_context, context=render_context,
cast_descriptor_text=cast_descriptor_text, cast_descriptor_text=r.cast_descriptor_text,
pov_prompt_directive=pov_prompt_directive if pov_character_labels else "", pov_prompt_directive=pov_prompt_directive if r.pov_character_labels else "",
) )
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1) batch = max(1, ((r.row_number - 1) // g.BATCH_SIZE) + 1)
index = start_index + row_number - 1 index = r.start_index + r.row_number - 1
row = g.row_base( row = g.row_base(
index, index,
batch, batch,
render_context["subject"], render_context["subject"],
render_context["age"], render_context["age"],
render_context["body"], render_context["body"],
scene_slug, r.scene_slug,
composition, r.composition,
) )
row.update( row.update(
{ {
"prompt": rendered["prompt"], "prompt": rendered["prompt"],
"caption": rendered["caption"], "caption": rendered["caption"],
"negative_prompt": negative_prompt, "negative_prompt": r.negative_prompt,
"expression": expression, "expression": r.expression,
"main_category": category["name"], "main_category": r.category["name"],
"subcategory": subcategory["name"], "subcategory": r.subcategory["name"],
"category_slug": category["slug"], "category_slug": r.category["slug"],
"subcategory_slug": subcategory["slug"], "subcategory_slug": r.subcategory["slug"],
"subject_type": subject_type, "subject_type": r.subject_type,
"subject_phrase": render_context.get("subject_phrase", ""), "subject_phrase": render_context.get("subject_phrase", ""),
"body_phrase": render_context.get("body_phrase", ""), "body_phrase": render_context.get("body_phrase", ""),
"skin": render_context.get("skin", ""), "skin": render_context.get("skin", ""),
"hair": render_context.get("hair", ""), "hair": render_context.get("hair", ""),
"eyes": render_context.get("eyes", ""), "eyes": render_context.get("eyes", ""),
"style": style, "style": r.style,
"item": item_text, "item": r.item_text,
"item_label": item_label, "item_label": r.item_label,
"positive_suffix": positive_suffix, "positive_suffix": r.positive_suffix,
"custom_item": item_name, "custom_item": r.item_name,
"item_axis_values": item_axis_values, "item_axis_values": r.item_axis_values,
"item_template_metadata": item_template_metadata, "item_template_metadata": r.item_template_metadata,
"formatter_hints": formatter_hints, "formatter_hints": r.formatter_hints,
"scene_text": scene, "scene_text": r.scene,
"location_config": location_config or {}, "location_config": r.location_config or {},
"pose": pose, "pose": r.pose,
"seed_config": seed_config, "seed_config": r.seed_config,
"hardcore_position_config": hardcore_position_config or {}, "hardcore_position_config": r.hardcore_position_config or {},
"content_seed_axis": content_seed_axis, "content_seed_axis": r.content_seed_axis,
"role_graph": role_graph, "role_graph": r.role_graph,
"source_role_graph": source_role_graph, "source_role_graph": r.source_role_graph,
"action_family": action_family, "action_family": r.action_family,
"position_family": position_family, "position_family": r.position_family,
"position_key": position_key, "position_key": r.position_key,
"position_keys": position_keys, "position_keys": r.position_keys,
"source_composition": source_composition, "source_composition": r.source_composition,
"pov_character_labels": pov_character_labels, "pov_character_labels": r.pov_character_labels,
"pov_prompt_directive": pov_prompt_directive, "pov_prompt_directive": pov_prompt_directive,
"shared_expression": shared_expression, "shared_expression": r.shared_expression,
"character_expressions": character_expressions, "character_expressions": r.character_expressions,
"character_expression_text": character_expression_text, "character_expression_text": r.character_expression_text,
"expression_enabled": not expression_disabled, "expression_enabled": not r.expression_disabled,
"expression_disabled": expression_disabled, "expression_disabled": r.expression_disabled,
"cast_summary": render_context.get("cast_summary", ""), "cast_summary": render_context.get("cast_summary", ""),
"cast_descriptors": cast_descriptors, "cast_descriptors": r.cast_descriptors,
"cast_descriptor_text": cast_descriptor_text, "cast_descriptor_text": r.cast_descriptor_text,
"scene_kind": render_context.get("scene_kind", ""), "scene_kind": render_context.get("scene_kind", ""),
"women_count": render_context.get("women_count", ""), "women_count": render_context.get("women_count", ""),
"men_count": render_context.get("men_count", ""), "men_count": render_context.get("men_count", ""),
"person_count": render_context.get("person_count", ""), "person_count": render_context.get("person_count", ""),
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {}, "cast_count_adjustment": r.count_adjustment if r.subject_type == "configured_cast" else {},
"character_profile": applied_profile or {}, "character_profile": r.applied_profile or {},
"character_profile_status": profile_status, "character_profile_status": r.profile_status,
"character_slot": applied_slot or {}, "character_slot": r.applied_slot or {},
"character_slot_status": slot_status, "character_slot_status": r.slot_status,
"character_cast_slots": character_slots or [], "character_cast_slots": r.character_slots or [],
"expression_intensity": expression_intensity, "expression_intensity": r.expression_intensity,
"expression_intensity_source": expression_intensity_source, "expression_intensity_source": r.expression_intensity_source,
"source": "json_category", "source": "json_category",
} }
) )
if render_context.get("figure"): if render_context.get("figure"):
row["figure"] = render_context["figure"] row["figure"] = render_context["figure"]
if expression_disabled: if r.expression_disabled:
row = row_expression_policy.disable_row_expression(row, expression_intensity_source) row = row_expression_policy.disable_row_expression(row, r.expression_intensity_source)
return row return row
+4 -2
View File
@@ -1761,10 +1761,12 @@ def smoke_row_assembly_policy() -> None:
"slot_status": "applied", "slot_status": "applied",
"character_slots": [{"label": "Woman A"}, {"label": "Man A"}], "character_slots": [{"label": "Woman A"}, {"label": "Man A"}],
} }
row = row_assembly.assemble_custom_row(**kwargs) request = row_assembly.CustomRowAssemblyRequest(**kwargs)
delegated = pb._assemble_custom_row(**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(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["id"] == "sxcp_0011", "Row assembly changed row indexing")
_expect(row["batch"] == "batch_001", "Row assembly changed batch calculation") _expect(row["batch"] == "batch_001", "Row assembly changed batch calculation")
_expect(row["source"] == "json_category", "Row assembly lost source marker") _expect(row["source"] == "json_category", "Row assembly lost source marker")