From 5ec17df1a42c39e6854951a3331945c3b3eea231 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 11:16:08 +0200 Subject: [PATCH] Extract Krea configured cast formatter route --- docs/prompt-architecture-improvement-plan.md | 5 + docs/prompt-pool-routing-map.md | 7 +- krea_configured_cast_formatter.py | 134 ++++++++++++++++ krea_formatter.py | 152 ++++++++++++------- tools/prompt_smoke.py | 10 ++ 5 files changed, 249 insertions(+), 59 deletions(-) create mode 100644 krea_configured_cast_formatter.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 7497091..26bcfa4 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -335,6 +335,11 @@ Keep here: Already isolated: +- `krea_configured_cast_formatter.py` owns normal metadata configured-cast + Krea prose assembly behind `KreaConfiguredCastRequest`, + `KreaConfiguredCastDependencies`, and `KreaConfiguredCastPrompt`; + `krea_formatter.py` keeps configured-cast detection and compatibility + wrapper helpers. - `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 8badcb8..d86fbaf 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -650,7 +650,10 @@ Important POV rule: - Pair metadata: `krea_pair_formatter.format_insta_pair_result` through the `_insta_pair_to_krea` compatibility wrapper. -- Normal metadata row: `_normal_row_to_krea`. +- Normal configured-cast metadata row: + `krea_configured_cast_formatter.format_configured_cast_result` through the + `_normal_row_to_krea` compatibility wrapper. +- Other normal metadata rows: `_normal_row_to_krea`. - Plain text fallback: `_fallback_text_to_krea`. Key Krea2 ownership: @@ -666,6 +669,8 @@ Key Krea2 ownership: - Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`. - Insta/OF pair soft/hard Krea prose assembly: `krea_pair_formatter.format_insta_pair_result`. +- Normal configured-cast Krea prose assembly: + `krea_configured_cast_formatter.format_configured_cast_result`. - Shared POV labels/filtering/composition cleanup: `pov_policy.py`. - Krea POV camera support: `krea_pov.py`. - Detail clause splitting and density limiting: `krea_detail.py`. diff --git a/krea_configured_cast_formatter.py b/krea_configured_cast_formatter.py new file mode 100644 index 0000000..58b0180 --- /dev/null +++ b/krea_configured_cast_formatter.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable + + +@dataclass(frozen=True) +class KreaConfiguredCastRequest: + row: dict[str, Any] + detail_level: str + style_mode: str + primary: str + item: str + scene: str + expression: str + composition: str + source_composition: str + camera: str + camera_scene: str + style: str + + +@dataclass(frozen=True) +class KreaConfiguredCastPrompt: + prompt: str + method: str = "metadata(configured_cast)" + + def as_tuple(self) -> tuple[str, str]: + return self.prompt, self.method + + +@dataclass(frozen=True) +class KreaConfiguredCastDependencies: + clean: Callable[[Any], str] + prompt_field: Callable[[str, str], str] + sanitize_hardcore_environment_anchors: Callable[[Any], str] + sanitize_hardcore_axis_values: Callable[[Any], Any] + sanitize_scene_text_for_cast: Callable[[Any, list[str]], str] + normalize_hardcore_detail_density: Callable[[Any], str] + row_action_family: Callable[[Any], str] + hardcore_action_sentence: Callable[[str, str, str, Any, str, str], str] + pov_action_phrase: Callable[[str, list[str], str, str, str, Any, str], str] + pov_labels_from_value: Callable[[Any], list[str]] + merge_labels: Callable[..., list[str]] + cast_prose_omit: Callable[[str, list[str]], tuple[str, list[str]]] + filter_pov_labeled_clauses: Callable[[Any, list[str]], str] + natural_label_text: Callable[[Any, list[str]], str] + pov_composition_text: Callable[[Any, list[str]], str] + pov_camera_phrase: Callable[[list[str]], str] + expression_phrase: Callable[[Any], str] + composition_phrase: Callable[..., str] + paragraph: Callable[[list[str]], str] + + +def format_configured_cast_result( + request: KreaConfiguredCastRequest, + deps: KreaConfiguredCastDependencies, +) -> KreaConfiguredCastPrompt: + row = request.row + subject = deps.clean(row.get("subject_phrase") or request.primary or "adult sexual scene") + cast = deps.clean(row.get("cast_summary")) + try: + women_count = int(row.get("women_count") or 0) + men_count = int(row.get("men_count") or 0) + except (TypeError, ValueError): + women_count = men_count = 0 + cast_descriptor_text = ( + deps.clean(row.get("cast_descriptor_text")) + or deps.prompt_field(deps.clean(row.get("prompt")), "Characters") + or deps.prompt_field(deps.clean(row.get("prompt")), "Cast descriptors") + ) + pov_labels = deps.pov_labels_from_value(row.get("pov_character_labels")) + camera = request.camera + if pov_labels: + camera = "" + cast_prose, cast_labels = deps.cast_prose_omit(cast_descriptor_text, pov_labels) + if not cast_labels and women_count == 1 and men_count == 1: + cast_labels = ["Woman A", "Man A"] + cast_labels = deps.merge_labels(cast_labels, pov_labels) + expression = deps.filter_pov_labeled_clauses(request.expression, pov_labels) + expression = deps.natural_label_text(expression, cast_labels) + composition = deps.sanitize_hardcore_environment_anchors(request.composition) + source_composition = deps.sanitize_hardcore_environment_anchors(request.source_composition) + role_graph = deps.sanitize_scene_text_for_cast( + deps.sanitize_hardcore_environment_anchors(row.get("source_role_graph") or row.get("role_graph")), + cast_labels, + ) + item = deps.sanitize_scene_text_for_cast( + deps.sanitize_hardcore_environment_anchors(request.item), + cast_labels, + ) + role_graph = deps.natural_label_text(role_graph, cast_labels) + item = deps.natural_label_text(item, cast_labels) + axis_values = deps.sanitize_hardcore_axis_values(row.get("item_axis_values")) + detail_density = deps.normalize_hardcore_detail_density(row.get("hardcore_detail_density")) + action = deps.hardcore_action_sentence( + role_graph, + item, + source_composition, + axis_values, + detail_density, + deps.row_action_family(row), + ) + action = deps.pov_action_phrase( + action, + pov_labels, + role_graph, + item, + source_composition, + axis_values, + detail_density, + ) + output_composition = deps.pov_composition_text(composition, pov_labels) + parts = [ + action, + deps.pov_camera_phrase(pov_labels), + cast_prose, + f"A consensual explicit adult scene with {subject}" if not action else "", + f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", + f"The setting is {request.scene}" if request.scene else "", + request.camera_scene, + deps.expression_phrase(expression), + deps.composition_phrase(output_composition, action, "The image is framed as", detail_density), + camera, + request.style if request.detail_level != "concise" else "", + ] + return KreaConfiguredCastPrompt(deps.paragraph(parts)) + + +def format_configured_cast( + request: KreaConfiguredCastRequest, + deps: KreaConfiguredCastDependencies, +) -> tuple[str, str]: + return format_configured_cast_result(request, deps).as_tuple() diff --git a/krea_formatter.py b/krea_formatter.py index fa99900..3cf5fbd 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -11,6 +11,7 @@ try: is_outercourse_text as _is_outercourse_text, normalize_hardcore_detail_density as _normalize_hardcore_detail_density, ) + from . import krea_configured_cast_formatter from . import krea_pair_formatter from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -43,6 +44,7 @@ except ImportError: # Allows local smoke tests with `python -c`. is_outercourse_text as _is_outercourse_text, normalize_hardcore_detail_density as _normalize_hardcore_detail_density, ) + import krea_configured_cast_formatter import krea_pair_formatter from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -407,13 +409,69 @@ def _couple_clothing_phrase(item: str) -> str: return f"The couple wears {item}" -def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str]: - subject_type = _clean(row.get("subject_type")) +def _krea_configured_cast_dependencies() -> krea_configured_cast_formatter.KreaConfiguredCastDependencies: + return krea_configured_cast_formatter.KreaConfiguredCastDependencies( + clean=_clean, + prompt_field=_prompt_field, + sanitize_hardcore_environment_anchors=_sanitize_hardcore_environment_anchors, + sanitize_hardcore_axis_values=_sanitize_hardcore_axis_values, + sanitize_scene_text_for_cast=_sanitize_scene_text_for_cast, + normalize_hardcore_detail_density=_normalize_hardcore_detail_density, + row_action_family=route_metadata_policy.row_action_family, + hardcore_action_sentence=_hardcore_action_sentence, + pov_action_phrase=_pov_action_phrase, + pov_labels_from_value=_pov_labels_from_value, + merge_labels=_merge_labels, + cast_prose_omit=lambda text, omit_labels: _cast_prose(text, omit_labels=omit_labels), + filter_pov_labeled_clauses=_filter_pov_labeled_clauses, + natural_label_text=_natural_label_text, + pov_composition_text=_pov_composition_text, + pov_camera_phrase=lambda labels: _pov_camera_phrase(labels), + expression_phrase=_expression_phrase, + composition_phrase=_composition_phrase, + paragraph=_paragraph, + ) + + +def _krea_configured_cast_request( + row: dict[str, Any], + detail_level: str, + style_mode: str, + primary: str, + item: str, + scene: str, + expression: str, + composition: str, + source_composition: str, + camera: str, + camera_scene: str, + style: str, +) -> krea_configured_cast_formatter.KreaConfiguredCastRequest: + return krea_configured_cast_formatter.KreaConfiguredCastRequest( + row=row, + detail_level=detail_level, + style_mode=style_mode, + primary=primary, + item=item, + scene=scene, + expression=expression, + composition=composition, + source_composition=source_composition, + camera=camera, + camera_scene=camera_scene, + style=style, + ) + + +def _krea_configured_cast_request_from_row( + row: dict[str, Any], + 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")) - 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")) @@ -427,64 +485,42 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) 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, + ) + +def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str]: + subject_type = _clean(row.get("subject_type")) if subject_type == "configured_cast" or _clean(row.get("cast_summary")): - subject = _clean(row.get("subject_phrase") or primary or "adult sexual scene") - cast = _clean(row.get("cast_summary")) - try: - women_count = int(row.get("women_count") or 0) - men_count = int(row.get("men_count") or 0) - except (TypeError, ValueError): - women_count = men_count = 0 - cast_descriptor_text = ( - _clean(row.get("cast_descriptor_text")) - or _prompt_field(_clean(row.get("prompt")), "Characters") - or _prompt_field(_clean(row.get("prompt")), "Cast descriptors") + return krea_configured_cast_formatter.format_configured_cast( + _krea_configured_cast_request_from_row(row, detail_level, style_mode), + _krea_configured_cast_dependencies(), ) - pov_labels = _pov_labels_from_value(row.get("pov_character_labels")) - if pov_labels: - camera = "" - cast_prose, cast_labels = _cast_prose(cast_descriptor_text, omit_labels=pov_labels) - if not cast_labels and women_count == 1 and men_count == 1: - cast_labels = ["Woman A", "Man A"] - cast_labels = _merge_labels(cast_labels, pov_labels) - expression = _filter_pov_labeled_clauses(expression, pov_labels) - expression = _natural_label_text(expression, cast_labels) - composition = _sanitize_hardcore_environment_anchors(composition) - source_composition = _sanitize_hardcore_environment_anchors(source_composition) - role_graph = _sanitize_scene_text_for_cast( - _sanitize_hardcore_environment_anchors(row.get("source_role_graph") or row.get("role_graph")), - cast_labels, - ) - item = _sanitize_scene_text_for_cast(_sanitize_hardcore_environment_anchors(item), cast_labels) - role_graph = _natural_label_text(role_graph, cast_labels) - item = _natural_label_text(item, cast_labels) - axis_values = _sanitize_hardcore_axis_values(row.get("item_axis_values")) - detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density")) - action = _hardcore_action_sentence( - role_graph, - item, - source_composition, - axis_values, - detail_density, - route_metadata_policy.row_action_family(row), - ) - action = _pov_action_phrase(action, pov_labels, role_graph, item, source_composition, axis_values, detail_density) - output_composition = _pov_composition_text(composition, pov_labels) - parts = [ - action, - _pov_camera_phrase(pov_labels), - cast_prose, - f"A consensual explicit adult scene with {subject}" if not action else "", - f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", - f"The setting is {scene}" if scene else "", - camera_scene, - _expression_phrase(expression), - _composition_phrase(output_composition, action, "The image is framed as", detail_density), - camera, - style if detail_level != "concise" else "", - ] - return _paragraph(parts), "metadata(configured_cast)" + + 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) if primary in ("woman", "man") or subject_type in ("woman", "man", "single_any"): subject = _age_subject(row, "adult woman") diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index e50e0d9..8b196a3 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -42,6 +42,7 @@ import __init__ as sxcp_nodes # noqa: E402 import generation_profile_config # noqa: E402 import index_switch_policy # noqa: E402 import krea_cast # noqa: E402 +import krea_configured_cast_formatter # noqa: E402 import krea_formatter # noqa: E402 import krea_pair_formatter # noqa: E402 import location_config # noqa: E402 @@ -566,6 +567,15 @@ def smoke_config_route_location_theme() -> None: _expect(seed_config.get("role_seed") == 3302, "seed lock did not reroll role axis") _expect(row.get("trigger") == "sxcpinup_coloredpencil", "generation profile trigger did not apply") _expect_trigger_once("config_route_location_theme.prompt", row.get("prompt"), "sxcpinup_coloredpencil") + typed_route = krea_configured_cast_formatter.format_configured_cast_result( + krea_formatter._krea_configured_cast_request_from_row(row, "balanced", "preserve"), + krea_formatter._krea_configured_cast_dependencies(), + ) + legacy_route = krea_formatter._normal_row_to_krea(row, "balanced", "preserve") + _expect( + typed_route.as_tuple() == legacy_route, + "Typed Krea configured-cast formatter route should match legacy wrapper output", + ) krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single") prompt = krea.get("krea_prompt") or "" _expect("library" in prompt.lower() or "bookshelves" in prompt.lower(), "Krea config route lost theme scene")