From 8fc3abc50425035cc81d5a21db35b3109856d3ea Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 11:42:14 +0200 Subject: [PATCH] Extract Krea row field policy --- docs/prompt-architecture-improvement-plan.md | 6 +- docs/prompt-pool-routing-map.md | 1 + krea_formatter.py | 89 +++++++++----------- krea_row_fields.py | 71 ++++++++++++++++ tools/prompt_smoke.py | 36 ++++++++ 5 files changed, 153 insertions(+), 50 deletions(-) create mode 100644 krea_row_fields.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index ad8bb23..98193d9 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -342,8 +342,10 @@ Already isolated: wrapper helpers. - `krea_normal_formatter.py` owns normal metadata single/couple/generic Krea prose assembly behind `KreaNormalRowRequest`, `KreaNormalRowDependencies`, - and `KreaNormalRowPrompt`; `krea_formatter.py` keeps common row-field - extraction and route selection. + and `KreaNormalRowPrompt`; `krea_formatter.py` keeps route selection. +- `krea_row_fields.py` owns shared normal-row Krea field extraction for item, + scene, pose, expression, composition/source-composition, camera, and style so + normal and configured-cast Krea routes cannot drift independently. - `krea_pair_formatter.py` owns Insta/OF pair soft/hard Krea prose assembly behind `KreaPairFormatRequest`, `KreaPairFormatDependencies`, and `KreaPairPrompts`; `krea_formatter.py` keeps the `_insta_pair_to_krea` diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 47c2a0f..705e633 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -115,6 +115,7 @@ Core helper ownership: | `pov_policy.py` | Shared POV slot detection, POV label merging/filtering, builder POV directives, source role-graph viewer replacement, and shared POV composition cleanup used by builder and Krea2 routes. | | `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. | | `row_camera.py` | Row-level camera insertion, contextual coworking composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. | +| `krea_row_fields.py` | Shared Krea normal-row field extraction for item, scene, pose, expression, composition/source-composition, camera, and style used by normal and configured-cast routes. | | `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. | | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. | | `row_normalization.py` | Final prompt-row and pair metadata normalization: trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. | diff --git a/krea_formatter.py b/krea_formatter.py index a225756..e35dce8 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -14,6 +14,7 @@ try: from . import krea_configured_cast_formatter from . import krea_normal_formatter from . import krea_pair_formatter + from . import krea_row_fields from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, @@ -48,6 +49,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import krea_configured_cast_formatter import krea_normal_formatter import krea_pair_formatter + import krea_row_fields from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, @@ -399,6 +401,17 @@ def _style_phrase(row: dict[str, Any], style_mode: str) -> str: return style or suffix +def _krea_row_field_dependencies() -> krea_row_fields.KreaRowFieldDependencies: + return krea_row_fields.KreaRowFieldDependencies( + clean=_clean, + row_value=_row_value, + camera_phrase=_camera_phrase, + camera_scene_phrase=_camera_scene_phrase, + style_phrase=_style_phrase, + expression_disabled=_expression_disabled, + ) + + def _krea_normal_row_dependencies() -> krea_normal_formatter.KreaNormalRowDependencies: return krea_normal_formatter.KreaNormalRowDependencies( clean=_clean, @@ -416,33 +429,25 @@ def _krea_normal_row_request_from_row( detail_level: str, style_mode: str, ) -> krea_normal_formatter.KreaNormalRowRequest: - subject_type = _clean(row.get("subject_type")) - primary = _clean(row.get("primary_subject")) - item = _row_value(row, "item", ("Sexual pose", "Erotic outfit", "Clothing")) or _clean(row.get("custom_item")) - item = re.sub(r",?\s*(fashion editorial|resort) styling$", "", item, flags=re.IGNORECASE) - scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene")) - pose = _row_value(row, "pose", ("Sexual pose", "Pose")) - expression = "" - if not _expression_disabled(row): - expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression")) - composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE) - camera = _camera_phrase(row) - camera_scene = _camera_scene_phrase(row) - style = _style_phrase(row, style_mode) + fields = krea_row_fields.extract_krea_row_fields( + row, + style_mode, + _krea_row_field_dependencies(), + ) return krea_normal_formatter.KreaNormalRowRequest( row=row, detail_level=detail_level, style_mode=style_mode, - subject_type=subject_type, - primary=primary, - item=item, - scene=scene, - pose=pose, - expression=expression, - composition=composition, - camera=camera, - camera_scene=camera_scene, - style=style, + subject_type=fields.subject_type, + primary=fields.primary, + item=fields.item, + scene=fields.scene, + pose=fields.pose, + expression=fields.expression, + composition=fields.composition, + camera=fields.camera, + camera_scene=fields.camera_scene, + style=fields.style, ) @@ -505,36 +510,24 @@ def _krea_configured_cast_request_from_row( detail_level: str, style_mode: str, ) -> krea_configured_cast_formatter.KreaConfiguredCastRequest: - primary = _clean(row.get("primary_subject")) - item = _row_value(row, "item", ("Sexual pose", "Erotic outfit", "Clothing")) or _clean(row.get("custom_item")) - item = re.sub(r",?\s*(fashion editorial|resort) styling$", "", item, flags=re.IGNORECASE) - scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene")) - expression = "" - if not _expression_disabled(row): - expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression")) - composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE) - source_composition = re.sub( - r"^vertical\s+", - "", - _clean(row.get("source_composition")) or composition, - flags=re.IGNORECASE, + fields = krea_row_fields.extract_krea_row_fields( + row, + style_mode, + _krea_row_field_dependencies(), ) - camera = _camera_phrase(row) - camera_scene = _camera_scene_phrase(row) - style = _style_phrase(row, style_mode) return _krea_configured_cast_request( row, detail_level, style_mode, - primary, - item, - scene, - expression, - composition, - source_composition, - camera, - camera_scene, - style, + fields.primary, + fields.item, + fields.scene, + fields.expression, + fields.composition, + fields.source_composition, + fields.camera, + fields.camera_scene, + fields.style, ) diff --git a/krea_row_fields.py b/krea_row_fields.py new file mode 100644 index 0000000..489d72c --- /dev/null +++ b/krea_row_fields.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any, Callable + + +@dataclass(frozen=True) +class KreaRowFields: + subject_type: str + primary: str + item: str + scene: str + pose: str + expression: str + composition: str + source_composition: str + camera: str + camera_scene: str + style: str + + +@dataclass(frozen=True) +class KreaRowFieldDependencies: + clean: Callable[[Any], str] + row_value: Callable[[dict[str, Any], str, tuple[str, ...]], str] + camera_phrase: Callable[[dict[str, Any]], str] + camera_scene_phrase: Callable[[dict[str, Any]], str] + style_phrase: Callable[[dict[str, Any], str], str] + expression_disabled: Callable[[dict[str, Any]], bool] + + +def _without_vertical_prefix(text: str) -> str: + return re.sub(r"^vertical\s+", "", text, flags=re.IGNORECASE) + + +def _clean_item_suffix(text: str) -> str: + return re.sub(r",?\s*(fashion editorial|resort) styling$", "", text, flags=re.IGNORECASE) + + +def extract_krea_row_fields( + row: dict[str, Any], + style_mode: str, + deps: KreaRowFieldDependencies, +) -> KreaRowFields: + item = deps.row_value(row, "item", ("Sexual pose", "Erotic outfit", "Clothing")) or deps.clean( + row.get("custom_item") + ) + item = _clean_item_suffix(item) + expression = "" + if not deps.expression_disabled(row): + expression = deps.row_value(row, "character_expression_text", ()) or deps.row_value( + row, + "expression", + ("Facial expressions", "Facial expression"), + ) + composition = _without_vertical_prefix(deps.row_value(row, "composition", ("Composition",))) + source_composition = _without_vertical_prefix(deps.clean(row.get("source_composition")) or composition) + return KreaRowFields( + subject_type=deps.clean(row.get("subject_type")), + primary=deps.clean(row.get("primary_subject")), + item=item, + scene=deps.row_value(row, "scene_text", ("Setting", "Scene")) or deps.clean(row.get("scene")), + pose=deps.row_value(row, "pose", ("Sexual pose", "Pose")), + expression=expression, + composition=composition, + source_composition=source_composition, + camera=deps.camera_phrase(row), + camera_scene=deps.camera_scene_phrase(row), + style=deps.style_phrase(row, style_mode), + ) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 7087236..9951c91 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -48,6 +48,7 @@ import krea_configured_cast_formatter # noqa: E402 import krea_formatter # noqa: E402 import krea_normal_formatter # noqa: E402 import krea_pair_formatter # noqa: E402 +import krea_row_fields # noqa: E402 import location_config # noqa: E402 import loop_nodes # noqa: E402 import pair_builder # noqa: E402 @@ -649,6 +650,40 @@ def smoke_krea_normal_row_routes() -> None: _expect_krea_normal_route_parity(generic, "krea_normal_generic", "metadata(generic)") +def smoke_krea_row_fields_policy() -> None: + row = { + "subject_type": "configured_cast", + "primary_subject": "woman", + "cast_summary": "1 woman, 1 man", + "item": "lace bodysuit fashion editorial styling", + "pose": "standing close together", + "scene_text": "private room with warm lamps", + "expression": "soft smile", + "expression_enabled": False, + "composition": "vertical tight two-person frame", + "source_composition": "vertical source action frame", + "camera_directive": "Camera: eye-level close-up", + "camera_scene_directive": "Camera-aware scene layout.", + "style": "realistic social photo", + } + fields = krea_row_fields.extract_krea_row_fields( + row, + "preserve", + krea_formatter._krea_row_field_dependencies(), + ) + normal_request = krea_formatter._krea_normal_row_request_from_row(row, "balanced", "preserve") + cast_request = krea_formatter._krea_configured_cast_request_from_row(row, "balanced", "preserve") + _expect(fields.item == "lace bodysuit", "Krea row fields did not strip generic styling suffix") + _expect(fields.expression == "", "Krea row fields ignored expression disabled flag") + _expect(fields.composition == "tight two-person frame", "Krea row fields did not normalize composition prefix") + _expect(fields.source_composition == "source action frame", "Krea row fields did not normalize source composition") + _expect(normal_request.item == fields.item, "Normal Krea route item extraction drifted") + _expect(cast_request.item == fields.item, "Configured-cast Krea route item extraction drifted") + _expect(normal_request.expression == cast_request.expression == fields.expression, "Krea route expression extraction drifted") + _expect(normal_request.camera == cast_request.camera == fields.camera, "Krea route camera extraction drifted") + _expect(cast_request.source_composition == fields.source_composition, "Configured-cast source composition drifted") + + def smoke_location_config_policy() -> None: _expect(pb.LOCATION_POOL_PRESETS is location_config.LOCATION_POOL_PRESETS, "Prompt builder location presets are not delegated") _expect(pb.COMPOSITION_POOL_PRESETS is location_config.COMPOSITION_POOL_PRESETS, "Prompt builder composition presets are not delegated") @@ -5256,6 +5291,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("row_camera_policy", smoke_row_camera_policy), ("config_route_location_theme", smoke_config_route_location_theme), ("krea_normal_row_routes", smoke_krea_normal_row_routes), + ("krea_row_fields_policy", smoke_krea_row_fields_policy), ("location_config_policy", smoke_location_config_policy), ("row_location_policy", smoke_row_location_policy), ("row_expression_policy", smoke_row_expression_policy),