diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 620eba7..aec5912 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -195,6 +195,10 @@ Already isolated: filtering, expression-disabled handling, per-character expression promotion, POV composition adaptation, and pose-category environment sanitizing live in `row_prompt_axes.py`; `prompt_builder.py` keeps a public delegate wrapper. +- row prompt/caption text-field resolution, prompt/caption template selection, + safe formatting, configured-cast descriptor insertion, and POV directive + insertion live in `row_rendering.py`; `prompt_builder.py` keeps public + delegate wrappers. - row expression text cleanup, expression route resolution, expression intensity weighting, character-slot/cast expression override resolution, and per-character expression picking plus action-aware character-expression diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 6d9d595..3c50eea 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -72,7 +72,7 @@ Core helper ownership: | `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_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 text-field resolution, template selection, safe formatting, default prompt templates, configured-cast descriptor insertion, and POV directive insertion. | | `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. | diff --git a/prompt_builder.py b/prompt_builder.py index 12b4b28..d6911c7 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -812,6 +812,14 @@ def _format(template: str, context: dict[str, Any]) -> str: return row_rendering_policy.format_template(template, context) +def _row_text_fields( + category: dict[str, Any], + subcategory: dict[str, Any], + item: Any, +) -> row_rendering_policy.RowTextFields: + return row_rendering_policy.resolve_row_text_fields(category, subcategory, item) + + def _clean_prompt_punctuation(text: str) -> str: return row_expression_policy.clean_prompt_punctuation(text) @@ -2269,18 +2277,7 @@ def _build_custom_row( position_key = str(action_route.get("position_key") or "") action_family = str(action_route.get("action_family") or "") - negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT)) - positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX)) - style = str( - _merged_field( - category, - subcategory, - item, - "style", - "sexy but tasteful adult pin-up coloured-pencil comic illustration", - ) - ) - item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"])) + text_fields = _row_text_fields(category, subcategory, item) assembly_request = row_assembly_policy.CustomRowAssemblyRequest( row_number=row_number, @@ -2295,10 +2292,10 @@ def _build_custom_row( 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, + item_label=text_fields.item_label, + style=text_fields.style, + positive_suffix=text_fields.positive_suffix, + negative_prompt=text_fields.negative_prompt, scene_slug=scene_slug, scene=scene, pose=pose, diff --git a/row_rendering.py b/row_rendering.py index 1f13a00..a48b586 100644 --- a/row_rendering.py +++ b/row_rendering.py @@ -1,11 +1,16 @@ from __future__ import annotations +from dataclasses import dataclass from string import Formatter from typing import Any try: + from . import category_library as category_policy + from . import generate_prompt_batches as g from . import row_camera as row_camera_policy except ImportError: # Allows local smoke tests from the repository root. + import category_library as category_policy + import generate_prompt_batches as g import row_camera as row_camera_policy @@ -14,6 +19,17 @@ GENERIC_POSITIVE_SUFFIX = ( "pastel skin tones, muted blues and pinks, warm sensual lighting, and tactile textured paper." ) +DEFAULT_STYLE = "sexy but tasteful adult pin-up coloured-pencil comic illustration" + + +@dataclass(frozen=True) +class RowTextFields: + negative_prompt: str + positive_suffix: str + style: str + item_label: str + + SINGLE_TEMPLATE = ( "A {subject}: {style}, {age}, {body_phrase}, {skin}, {hair}, {eyes}. " "{item_label}: {item}. Scene: {scene}. Pose: {pose}. Facial expression: {expression}. " @@ -56,6 +72,19 @@ def format_template(template: str, context: dict[str, Any]) -> str: return template.format_map(safe_context) +def resolve_row_text_fields(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> RowTextFields: + return RowTextFields( + negative_prompt=str( + category_policy.merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT) + ), + positive_suffix=str( + category_policy.merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX) + ), + style=str(category_policy.merged_field(category, subcategory, item, "style", DEFAULT_STYLE)), + item_label=str(category_policy.merged_field(category, subcategory, item, "item_label", category["name"])), + ) + + def default_prompt_template(subject_type: str) -> str: if subject_type in ("woman", "man"): return SINGLE_TEMPLATE diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index f9f9403..38b9d61 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -1714,6 +1714,37 @@ def smoke_row_rendering_policy() -> None: row_rendering.prompt_template_for({}, {}, {}, "group") == row_rendering.GROUP_TEMPLATE, "Row rendering default group template changed", ) + category_text = { + "name": "Category Label", + "negative_prompt": "category negative", + "positive_suffix": "category suffix", + "style": "category style", + "item_label": "Category Item", + } + subcategory_text = { + "negative_prompt": "subcategory negative", + "positive_suffix": "subcategory suffix", + "style": "subcategory style", + } + item_text = { + "negative_prompt": "item negative", + "style": "item style", + } + _expect( + pb._row_text_fields(category_text, subcategory_text, item_text) + == row_rendering.resolve_row_text_fields(category_text, subcategory_text, item_text), + "Prompt builder row text field wrapper should delegate to row_rendering", + ) + text_fields = row_rendering.resolve_row_text_fields(category_text, subcategory_text, item_text) + _expect(text_fields.negative_prompt == "item negative", "Row text fields did not prefer item negative prompt") + _expect(text_fields.positive_suffix == "subcategory suffix", "Row text fields did not prefer subcategory suffix") + _expect(text_fields.style == "item style", "Row text fields did not prefer item style") + _expect(text_fields.item_label == "Category Item", "Row text fields did not fall back to category item label") + default_text_fields = row_rendering.resolve_row_text_fields({"name": "Default Category"}, {}, {}) + _expect(default_text_fields.negative_prompt, "Row text fields lost default negative prompt") + _expect(default_text_fields.positive_suffix == row_rendering.GENERIC_POSITIVE_SUFFIX, "Row text fields lost suffix default") + _expect(default_text_fields.style == row_rendering.DEFAULT_STYLE, "Row text fields lost style default") + _expect(default_text_fields.item_label == "Default Category", "Row text fields lost category-name label default") context = { "trigger": Trigger,