Extract builder config route

This commit is contained in:
2026-06-27 12:09:41 +02:00
parent f1567118b4
commit 9a5809deaa
5 changed files with 196 additions and 35 deletions
+101
View File
@@ -0,0 +1,101 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
@dataclass(frozen=True)
class PromptFromConfigsRequest:
row_number: int
start_index: int
seed: int
category_config: str | dict[str, Any] | None = ""
cast_config: str | dict[str, Any] | None = ""
generation_profile: str | dict[str, Any] | None = ""
filter_config: str | dict[str, Any] | None = ""
seed_config: str | dict[str, Any] | None = ""
camera_config: str | dict[str, Any] | None = ""
character_profile: str | dict[str, Any] | None = ""
character_cast: str | dict[str, Any] | list[Any] | None = ""
hardcore_position_config: str | dict[str, Any] | None = ""
location_config: str | dict[str, Any] | None = ""
composition_config: str | dict[str, Any] | None = ""
extra_positive: str = ""
extra_negative: str = ""
@dataclass(frozen=True)
class PromptFromConfigsRoute:
row: dict[str, Any]
category: str
subcategory: str
cast: dict[str, Any]
profile: dict[str, Any]
filters: dict[str, Any]
build_kwargs: dict[str, Any]
@dataclass(frozen=True)
class PromptFromConfigsDependencies:
parse_category_config: Callable[[str | dict[str, Any] | None], tuple[str, str]]
parse_cast_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
parse_generation_profile: Callable[[str | dict[str, Any] | None], dict[str, Any]]
parse_filter_config: Callable[[str | dict[str, Any] | None], dict[str, Any]]
build_prompt: Callable[..., dict[str, Any]]
def build_prompt_from_configs_result(
request: PromptFromConfigsRequest,
deps: PromptFromConfigsDependencies,
) -> PromptFromConfigsRoute:
category, subcategory = deps.parse_category_config(request.category_config)
cast = deps.parse_cast_config(request.cast_config)
profile = deps.parse_generation_profile(request.generation_profile)
filters = deps.parse_filter_config(request.filter_config)
build_kwargs: dict[str, Any] = {
"category": category,
"subcategory": subcategory,
"row_number": request.row_number,
"start_index": request.start_index,
"seed": request.seed,
"clothing": profile["clothing"],
"ethnicity": filters["ethnicity"],
"poses": profile["poses"],
"expression_enabled": profile["expression_enabled"],
"expression_intensity": profile["expression_intensity"],
"backside_bias": profile["backside_bias"],
"figure": filters["figure"],
"no_plus_women": filters["no_plus_women"],
"no_black": filters["no_black"],
"women_count": int(cast["women_count"]),
"men_count": int(cast["men_count"]),
"minimal_clothing_ratio": profile["minimal_clothing_ratio"],
"standard_pose_ratio": profile["standard_pose_ratio"],
"trigger": profile["trigger"],
"prepend_trigger_to_prompt": profile["prepend_trigger_to_prompt"],
"extra_positive": request.extra_positive or "",
"extra_negative": request.extra_negative or "",
"seed_config": request.seed_config or "",
"camera_config": request.camera_config or "",
"character_profile": request.character_profile or "",
"character_cast": request.character_cast or "",
"hardcore_position_config": request.hardcore_position_config or "",
"location_config": request.location_config or "",
"composition_config": request.composition_config or "",
}
return PromptFromConfigsRoute(
row=deps.build_prompt(**build_kwargs),
category=category,
subcategory=subcategory,
cast=dict(cast),
profile=dict(profile),
filters=dict(filters),
build_kwargs=build_kwargs,
)
def build_prompt_from_configs(
request: PromptFromConfigsRequest,
deps: PromptFromConfigsDependencies,
) -> dict[str, Any]:
return build_prompt_from_configs_result(request, deps).row
@@ -120,6 +120,9 @@ Move or isolate later:
Already isolated: Already isolated:
- config-driven prompt-builder request parsing, helper-node config mapping, and
direct `build_prompt` kwarg assembly live in `builder_config_route.py`;
`prompt_builder.py` keeps the public wrapper.
- JSON category loading, subcategory normalization, named scene/expression/ - JSON category loading, subcategory normalization, named scene/expression/
composition pool loading, cast compatibility filtering, exact subcategory composition pool loading, cast compatibility filtering, exact subcategory
lookup, and inheritance-based pool merging live in `category_library.py`. lookup, and inheritance-based pool merging live in `category_library.py`.
+2 -1
View File
@@ -61,7 +61,7 @@ call the same core generation functions.
| ComfyUI node | Python entry | What it owns | | ComfyUI node | Python entry | What it owns |
| --- | --- | --- | | --- | --- | --- |
| `SxCP Prompt Builder` | `build_prompt` | Direct single prompt generation. Can use built-in categories or JSON categories. | | `SxCP Prompt Builder` | `build_prompt` | Direct single prompt generation. Can use built-in categories or JSON categories. |
| `SxCP Prompt Builder From Configs` | `build_prompt_from_configs` -> `build_prompt` | Same generator, but inputs come from category/cast/profile/filter helper nodes. | | `SxCP Prompt Builder From Configs` | `build_prompt_from_configs` -> `builder_config_route.py` -> `build_prompt` | Same generator, but inputs come from category/cast/profile/filter helper nodes. |
| `SxCP Insta/OF Prompt Pair` | `build_insta_of_pair` | Builds a softcore row and hardcore row with shared cast/continuity options. | | `SxCP Insta/OF Prompt Pair` | `build_insta_of_pair` | Builds a softcore row and hardcore row with shared cast/continuity options. |
| `SxCP Krea2 Formatter` | `format_krea2_prompt` | Converts metadata rows or pair metadata into Krea2-friendly prose. | | `SxCP Krea2 Formatter` | `format_krea2_prompt` | Converts metadata rows or pair metadata into Krea2-friendly prose. |
| `SxCP SDXL Formatter` | `format_sdxl_prompt` | Converts metadata rows or pair metadata into SDXL/tag style prompts. | | `SxCP SDXL Formatter` | `format_sdxl_prompt` | Converts metadata rows or pair metadata into SDXL/tag style prompts. |
@@ -72,6 +72,7 @@ Core helper ownership:
| Python module | What it owns | | Python module | What it owns |
| --- | --- | | --- | --- |
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. | | `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
| `builder_config_route.py` | Config-driven prompt-builder request parsing, category/cast/profile/filter helper-node mapping, and direct `build_prompt` kwarg assembly. |
| `category_extensions.py` | JSON `pool_extensions`, legacy pool patching, built-in category choice lists, and category/subcategory UI choices. | | `category_extensions.py` | JSON `pool_extensions`, legacy pool patching, built-in category choice lists, and category/subcategory UI choices. |
| `category_template_metadata.py` | Object-style item-template metadata extraction, action/position family normalization, position-key normalization, key merging, and audit validation errors. | | `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_item.py` | Row item selection, weighted item/pair choice, item-template axis filling, and oral/outercourse axis compatibility filters. |
+32 -34
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from typing import Any from typing import Any
try: try:
from . import builder_config_route as builder_config_route_policy
from .category_library import ( from .category_library import (
compatible_entries as _compatible_entries, compatible_entries as _compatible_entries,
compatible_entry as _compatible_entry, compatible_entry as _compatible_entry,
@@ -51,6 +52,7 @@ try:
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
) )
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
import builder_config_route as builder_config_route_policy
from category_library import ( from category_library import (
compatible_entries as _compatible_entries, compatible_entries as _compatible_entries,
compatible_entry as _compatible_entry, compatible_entry as _compatible_entry,
@@ -2609,6 +2611,16 @@ def build_prompt(
return row return row
def _prompt_from_configs_dependencies() -> builder_config_route_policy.PromptFromConfigsDependencies:
return builder_config_route_policy.PromptFromConfigsDependencies(
parse_category_config=_parse_category_config,
parse_cast_config=_parse_cast_config,
parse_generation_profile=_parse_generation_profile,
parse_filter_config=_parse_filter_config,
build_prompt=build_prompt,
)
def build_prompt_from_configs( def build_prompt_from_configs(
row_number: int, row_number: int,
start_index: int, start_index: int,
@@ -2627,40 +2639,26 @@ def build_prompt_from_configs(
extra_positive: str = "", extra_positive: str = "",
extra_negative: str = "", extra_negative: str = "",
) -> dict[str, Any]: ) -> dict[str, Any]:
category, subcategory = _parse_category_config(category_config) return builder_config_route_policy.build_prompt_from_configs(
cast = _parse_cast_config(cast_config) builder_config_route_policy.PromptFromConfigsRequest(
profile = _parse_generation_profile(generation_profile) row_number=row_number,
filters = _parse_filter_config(filter_config) start_index=start_index,
return build_prompt( seed=seed,
category=category, category_config=category_config,
subcategory=subcategory, cast_config=cast_config,
row_number=row_number, generation_profile=generation_profile,
start_index=start_index, filter_config=filter_config,
seed=seed, seed_config=seed_config,
clothing=profile["clothing"], camera_config=camera_config,
ethnicity=filters["ethnicity"], character_profile=character_profile,
poses=profile["poses"], character_cast=character_cast,
expression_enabled=profile["expression_enabled"], hardcore_position_config=hardcore_position_config,
expression_intensity=profile["expression_intensity"], location_config=location_config,
backside_bias=profile["backside_bias"], composition_config=composition_config,
figure=filters["figure"], extra_positive=extra_positive,
no_plus_women=filters["no_plus_women"], extra_negative=extra_negative,
no_black=filters["no_black"], ),
women_count=int(cast["women_count"]), _prompt_from_configs_dependencies(),
men_count=int(cast["men_count"]),
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
standard_pose_ratio=profile["standard_pose_ratio"],
trigger=profile["trigger"],
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
extra_positive=extra_positive or "",
extra_negative=extra_negative or "",
seed_config=seed_config or "",
camera_config=camera_config or "",
character_profile=character_profile or "",
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
) )
+58
View File
@@ -28,6 +28,7 @@ import caption_naturalizer # noqa: E402
import caption_metadata_routes # noqa: E402 import caption_metadata_routes # noqa: E402
import caption_policy # noqa: E402 import caption_policy # noqa: E402
import caption_text_policy # noqa: E402 import caption_text_policy # noqa: E402
import builder_config_route # noqa: E402
import cast_context # noqa: E402 import cast_context # noqa: E402
import category_extensions # noqa: E402 import category_extensions # noqa: E402
import category_template_metadata # noqa: E402 import category_template_metadata # noqa: E402
@@ -604,6 +605,62 @@ def smoke_config_route_location_theme() -> None:
_expect_formatter_outputs(row, "config_route_location_theme", target="single") _expect_formatter_outputs(row, "config_route_location_theme", target="single")
def smoke_builder_config_route_policy() -> None:
category_config = pb.build_category_config_json("women_casual", "Casual clothes / Smart casual")
cast_config = pb.build_cast_config_json("solo_woman")
generation_profile = pb.build_generation_profile_json(
profile="casual_clean",
trigger_policy="prepend_trigger",
)
filter_config = pb.build_filter_config_json(
ethnicity="french_european",
figure="balanced",
)
seed_config_json = pb.build_seed_lock_config_json(base_seed=3401, reroll_axis="scene", reroll_seed=3402)
request = builder_config_route.PromptFromConfigsRequest(
row_number=2,
start_index=5,
seed=3401,
category_config=category_config,
cast_config=cast_config,
generation_profile=generation_profile,
filter_config=filter_config,
seed_config=seed_config_json,
extra_positive="clean route marker",
extra_negative="bad route marker",
)
typed_route = builder_config_route.build_prompt_from_configs_result(
request,
pb._prompt_from_configs_dependencies(),
)
legacy_row = pb.build_prompt_from_configs(
row_number=request.row_number,
start_index=request.start_index,
seed=request.seed,
category_config=category_config,
cast_config=cast_config,
generation_profile=generation_profile,
filter_config=filter_config,
seed_config=seed_config_json,
extra_positive=request.extra_positive,
extra_negative=request.extra_negative,
)
_expect(typed_route.row == legacy_row, "Prompt Builder From Configs route should match public wrapper output")
_expect(typed_route.category == "Casual clothes", "Config route lost category preset")
_expect(typed_route.subcategory == "Casual clothes / Smart casual", "Config route lost requested subcategory")
_expect(typed_route.cast["women_count"] == 1 and typed_route.cast["men_count"] == 0, "Config route lost cast preset")
_expect(typed_route.profile["trigger"] == "sxcpinup_coloredpencil", "Config route lost generation profile trigger")
_expect(typed_route.filters["ethnicity"] == "french_european", "Config route lost filter ethnicity")
kwargs = typed_route.build_kwargs
_expect(kwargs["category"] == typed_route.category, "Config route build kwargs category drifted")
_expect(kwargs["subcategory"] == typed_route.subcategory, "Config route build kwargs subcategory drifted")
_expect(kwargs["women_count"] == 1 and kwargs["men_count"] == 0, "Config route build kwargs cast counts drifted")
_expect(kwargs["seed_config"] == seed_config_json, "Config route build kwargs seed config drifted")
_expect(kwargs["extra_positive"] == "clean route marker", "Config route build kwargs extra positive drifted")
_expect("clean route marker" in typed_route.row.get("prompt", ""), "Config route row lost extra positive")
_expect("bad route marker" in typed_route.row.get("negative_prompt", ""), "Config route row lost extra negative")
def smoke_krea_normal_row_routes() -> None: def smoke_krea_normal_row_routes() -> None:
single = { single = {
"subject_type": "woman", "subject_type": "woman",
@@ -5365,6 +5422,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("camera_scene_single", smoke_camera_scene_single), ("camera_scene_single", smoke_camera_scene_single),
("row_camera_policy", smoke_row_camera_policy), ("row_camera_policy", smoke_row_camera_policy),
("config_route_location_theme", smoke_config_route_location_theme), ("config_route_location_theme", smoke_config_route_location_theme),
("builder_config_route_policy", smoke_builder_config_route_policy),
("krea_normal_row_routes", smoke_krea_normal_row_routes), ("krea_normal_row_routes", smoke_krea_normal_row_routes),
("krea_row_fields_policy", smoke_krea_row_fields_policy), ("krea_row_fields_policy", smoke_krea_row_fields_policy),
("location_config_policy", smoke_location_config_policy), ("location_config_policy", smoke_location_config_policy),