diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index ec8d84b..2dbce08 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -253,6 +253,10 @@ 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. ### Pair / Adapter Layer diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 40c9ad4..07d8421 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 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_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 0493b25..ffc860b 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -34,6 +34,7 @@ try: from . import pair_options from . import pov_policy from . import row_normalization as row_policy + from . import row_assembly as row_assembly_policy from . import row_camera as row_camera_policy from . import row_category_route as row_category_route_policy from . import row_expression as row_expression_policy @@ -82,6 +83,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import pair_options import pov_policy import row_normalization as row_policy + import row_assembly as row_assembly_policy import row_camera as row_camera_policy import row_category_route as row_category_route_policy import row_expression as row_expression_policy @@ -2084,6 +2086,10 @@ def _prompt_axes_route( ) +def _assemble_custom_row(**kwargs: Any) -> dict[str, Any]: + return row_assembly_policy.assemble_custom_row(**kwargs) + + def _build_custom_row( category_choice: str, subcategory_choice: str, @@ -2258,130 +2264,60 @@ def _build_custom_row( ) item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"])) - 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": _composition_prompt(composition), - "composition_config": parsed_composition_config if _composition_config_active(parsed_composition_config) else {}, - "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, - "pov_prompt_directive": _pov_prompt_directive(pov_character_labels), - "cast_descriptors": cast_descriptor_text, - "positive_suffix": positive_suffix, - "negative_prompt": negative_prompt, - } - ) - - rendered = row_rendering_policy.render_prompt_caption( - item=item, - subcategory=subcategory, + return _assemble_custom_row( + row_number=row_number, + start_index=start_index, category=category, - subject_type=subject_type, + subcategory=subcategory, + item=item, context=context, + subject_type=subject_type, + item_text=item_text, + item_name=item_name, + item_axis_values=item_axis_values, + item_template_metadata=item_template_metadata, + formatter_hints=item_formatter_hints, + item_label=item_label, + style=style, + positive_suffix=positive_suffix, + negative_prompt=negative_prompt, + scene_slug=scene_slug, + scene=scene, + pose=pose, + expression=expression, + shared_expression=shared_expression, + character_expressions=character_expressions, + character_expression_text=character_expression_text, + expression_disabled=expression_disabled, + expression_intensity=expression_intensity, + expression_intensity_source=expression_intensity_source, + composition=composition, + source_composition=source_composition, + 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, + cast_descriptors=cast_descriptors, cast_descriptor_text=cast_descriptor_text, - pov_prompt_directive=_pov_prompt_directive(pov_character_labels) if pov_character_labels else "", + seed_config=seed_config, + hardcore_position_config=( + parsed_hardcore_position_config + if _hardcore_position_config_active(parsed_hardcore_position_config) + else {} + ), + location_config=parsed_location_config if _location_config_active(parsed_location_config) else {}, + composition_config=parsed_composition_config if _composition_config_active(parsed_composition_config) else {}, + content_seed_axis=content_axis, + count_adjustment=count_adjustment, + applied_profile=applied_profile, + profile_status=profile_status, + applied_slot=applied_slot, + slot_status=slot_status, + character_slots=character_slots, ) - prompt = rendered["prompt"] - caption = rendered["caption"] - batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1) - index = start_index + row_number - 1 - row = g.row_base(index, batch, context["subject"], context["age"], context["body"], scene_slug, composition) - row.update( - { - "prompt": prompt, - "caption": 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, - "subject_phrase": context.get("subject_phrase", ""), - "body_phrase": context.get("body_phrase", ""), - "skin": context.get("skin", ""), - "hair": context.get("hair", ""), - "eyes": 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": item_formatter_hints, - "scene_text": scene, - "location_config": parsed_location_config if _location_config_active(parsed_location_config) else {}, - "pose": pose, - "seed_config": seed_config, - "hardcore_position_config": ( - parsed_hardcore_position_config - if _hardcore_position_config_active(parsed_hardcore_position_config) - else {} - ), - "content_seed_axis": content_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, - "pov_prompt_directive": _pov_prompt_directive(pov_character_labels), - "shared_expression": shared_expression, - "character_expressions": character_expressions, - "character_expression_text": character_expression_text, - "expression_enabled": not expression_disabled, - "expression_disabled": expression_disabled, - "cast_summary": context.get("cast_summary", ""), - "cast_descriptors": cast_descriptors, - "cast_descriptor_text": cast_descriptor_text, - "scene_kind": context.get("scene_kind", ""), - "women_count": context.get("women_count", ""), - "men_count": context.get("men_count", ""), - "person_count": context.get("person_count", ""), - "cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {}, - "character_profile": applied_profile, - "character_profile_status": profile_status, - "character_slot": applied_slot, - "character_slot_status": slot_status, - "character_cast_slots": character_slots, - "expression_intensity": expression_intensity, - "expression_intensity_source": expression_intensity_source, - "source": "json_category", - } - ) - if context.get("figure"): - row["figure"] = context["figure"] - if expression_disabled: - row = _disable_row_expression(row, expression_intensity_source) - return row def build_prompt( diff --git a/row_assembly.py b/row_assembly.py new file mode 100644 index 0000000..ca8bbd2 --- /dev/null +++ b/row_assembly.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from typing import Any + +try: + from . import generate_prompt_batches as g + from . import pov_policy + from . import row_camera as row_camera_policy + from . import row_expression as row_expression_policy + from . import row_rendering as row_rendering_policy +except ImportError: # Allows local smoke tests from the repository root. + import generate_prompt_batches as g + import pov_policy + import row_camera as row_camera_policy + import row_expression as row_expression_policy + 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) + 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, + "pov_prompt_directive": pov_prompt_directive, + "cast_descriptors": cast_descriptor_text, + "positive_suffix": positive_suffix, + "negative_prompt": negative_prompt, + } + ) + rendered = row_rendering_policy.render_prompt_caption( + item=item, + subcategory=subcategory, + category=category, + subject_type=subject_type, + context=render_context, + cast_descriptor_text=cast_descriptor_text, + pov_prompt_directive=pov_prompt_directive if pov_character_labels else "", + ) + batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1) + index = start_index + row_number - 1 + row = g.row_base( + index, + batch, + render_context["subject"], + render_context["age"], + render_context["body"], + scene_slug, + 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, + "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, + "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, + "cast_summary": render_context.get("cast_summary", ""), + "cast_descriptors": cast_descriptors, + "cast_descriptor_text": 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, + "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) + return row diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 35a4025..54cd0ee 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -50,6 +50,7 @@ import pair_clothing # noqa: E402 import prompt_builder as pb # noqa: E402 import pov_policy # noqa: E402 import row_normalization # noqa: E402 +import row_assembly # noqa: E402 import route_metadata # noqa: E402 import row_camera # noqa: E402 import row_category_route # noqa: E402 @@ -1687,6 +1688,99 @@ def smoke_row_rendering_policy() -> None: ) +def smoke_row_assembly_policy() -> None: + context = { + "subject": "configured cast", + "subject_phrase": "configured adult cast", + "age": "21+ adults", + "body": "varied adult builds", + "body_phrase": "varied adult builds", + "figure": "balanced cast", + "skin": "warm skin tones", + "hair": "dark hair", + "eyes": "brown eyes", + "cast_summary": "one woman and one man", + "scene_kind": "configured_cast", + "women_count": "1", + "men_count": "1", + "person_count": "2", + } + count_adjustment = {"requested_women_count": 1, "requested_men_count": 1} + kwargs = { + "row_number": 2, + "start_index": 10, + "category": {"name": "Axis Test", "slug": "axis_test"}, + "subcategory": { + "name": "Custom Scene", + "slug": "custom_scene", + "prompt_template": "Scene: {item}. Composition: {composition_prompt}. Avoid: {negative_prompt}.", + "caption_template": "{trigger}, {item}, {scene}", + }, + "item": {"text": "shared structured action"}, + "context": context, + "subject_type": "configured_cast", + "item_text": "shared structured action", + "item_name": "shared_action", + "item_axis_values": {"action_family": "test_action"}, + "item_template_metadata": {"position_key": "test_position"}, + "formatter_hints": {"krea": ["test_hint"]}, + "item_label": "Scene", + "style": "clean test style", + "positive_suffix": "clear readable composition.", + "negative_prompt": "bad anatomy", + "scene_slug": "test_room", + "scene": "warm test room", + "pose": "standing close", + "expression": "focused look", + "shared_expression": "focused look", + "character_expressions": ["Woman A has focused look"], + "character_expression_text": "Woman A has focused look", + "expression_disabled": True, + "expression_intensity": 0.7, + "expression_intensity_source": "disabled", + "composition": "centered frame", + "source_composition": "centered frame", + "role_graph": "the visible partner stays centered", + "source_role_graph": "Man A stays centered", + "action_family": "test_action", + "position_family": "standing", + "position_key": "test_position", + "position_keys": ["test_position"], + "pov_character_labels": ["Man A"], + "cast_descriptors": ["Woman A: adult woman", "Man A: adult man"], + "cast_descriptor_text": "Woman A: adult woman; Man A: adult man", + "seed_config": {"content_seed": 123}, + "hardcore_position_config": {"family": "standing"}, + "location_config": {"location": "test_room"}, + "composition_config": {"composition": "centered"}, + "content_seed_axis": "pose", + "count_adjustment": count_adjustment, + "applied_profile": {"name": "profile_a"}, + "profile_status": "applied", + "applied_slot": {"label": "Woman A"}, + "slot_status": "applied", + "character_slots": [{"label": "Woman A"}, {"label": "Man A"}], + } + row = row_assembly.assemble_custom_row(**kwargs) + delegated = pb._assemble_custom_row(**kwargs) + + _expect(row == delegated, "Prompt builder row assembly wrapper should delegate without changing output") + _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") + _expect(row["figure"] == "balanced cast", "Row assembly lost figure metadata") + _expect(row["formatter_hints"] == {"krea": ["test_hint"]}, "Row assembly lost formatter hints") + _expect(row["cast_count_adjustment"] == count_adjustment, "Row assembly lost configured-cast count adjustment") + _expect(row["content_seed_axis"] == "pose", "Row assembly lost content seed axis") + _expect("POV participant: Man A" in row["prompt"], "Row assembly lost POV prompt directive") + _expect("Characters: Woman A: adult woman; Man A: adult man." in row["prompt"], "Row assembly lost cast descriptor insertion") + _expect(row["caption"].endswith("Woman A: adult woman; Man A: adult man"), "Row assembly lost caption descriptor append") + _expect(row["expression"] == "", "Disabled expression should clear row expression") + _expect(row["expression_enabled"] is False, "Disabled expression should mark expression disabled") + _expect(row["expression_intensity"] is None, "Disabled expression should clear intensity") + _expect(row["expression_intensity_source"] == "disabled", "Disabled expression should preserve disabled source") + + def smoke_formatter_input_policy() -> None: source_row = { "prompt": "A simple adult portrait. Setting: quiet studio. Pose: standing calmly. Avoid: low quality.", @@ -4486,6 +4580,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_assembly_policy", smoke_row_assembly_policy), ("formatter_input_policy", smoke_formatter_input_policy), ("formatter_cast_policy", smoke_formatter_cast_policy), ("caption_policy", smoke_caption_policy),