From 1ee0b6e91a7e4430689a459aeb47ea150e7739ed Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 12:26:00 +0200 Subject: [PATCH] Extract SDXL format dispatch route --- docs/prompt-architecture-improvement-plan.md | 4 + docs/prompt-pool-routing-map.md | 3 +- sdxl_format_route.py | 168 +++++++++++++++++++ sdxl_formatter.py | 123 +++++--------- tools/prompt_smoke.py | 122 ++++++++++++++ 5 files changed, 338 insertions(+), 82 deletions(-) create mode 100644 sdxl_format_route.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 2ccd013..80b5239 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -415,6 +415,10 @@ Keep here: Already isolated: +- `sdxl_format_route.py` owns top-level SDXL dispatch, including formatter + profile application, target and nude-weight normalization, metadata-vs-text + input selection, single-vs-pair branching, final prompt/negative output + shape, and fallback routing; `sdxl_formatter.py` keeps the public wrapper. - `sdxl_tag_routes.py` owns normal metadata row tags and Insta/OF pair soft/hard tag extraction behind `SDXLRowTagRequest`, `SDXLPairTagRequest`, `SDXLTagRouteDependencies`, and `SDXLTagRoute`; `sdxl_formatter.py` keeps diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index a508b73..3231091 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -64,7 +64,7 @@ call the same core generation functions. | `SxCP Prompt Builder From Configs` | `build_prompt_from_configs` -> `builder_config_route.py` -> `build_prompt` -> `builder_prompt_route.py` | 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 Krea2 Formatter` | `format_krea2_prompt` -> `krea_format_route.py` | 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` -> `sdxl_format_route.py` | Converts metadata rows or pair metadata into SDXL/tag style prompts. | | `SxCP Caption Naturalizer` | `naturalize_caption` | Converts rows into more natural sentence captions. | Core helper ownership: @@ -130,6 +130,7 @@ Core helper ownership: | `node_tooltips.py` | Node input tooltip inventory, node-specific overrides, dynamic-input fallback rules, and tooltip injection installer used by `__init__.py`. | | `server_routes.py` | Pure payload handlers for profile-save and accumulator server endpoints, used by ComfyUI routes and smoke tests without importing ComfyUI. | | `sdxl_presets.py` | SDXL formatter profiles, style presets, quality presets, default negative prompt, and metadata-family tag hints used by the SDXL formatter and node choice lists. | +| `sdxl_format_route.py` | Top-level SDXL dispatch, formatter profile application, target and nude-weight normalization, metadata-vs-text input selection, single-vs-pair branching, final prompt/negative output shape, and fallback routing. | | `sdxl_tag_policy.py` | SDXL tag splitting, tag-key dedupe, count inference, character descriptor tags, metadata-family/camera/explicit helper tags, and route dependency assembly used by `sdxl_formatter.py` and `sdxl_tag_routes.py`. | | `caption_policy.py` | Caption naturalizer policy data and helpers: caption profiles, style tails, item labels, metadata-family caption labels, detail/style-policy normalization, clothing cleanup, and composition cleanup. | | `caption_text_policy.py` | Caption sentence helpers, trigger wrapping, formatter-hint append, row-value fallback wrappers, cast text wrappers, single-caption front parsing, and metadata-route dependency assembly used by `caption_naturalizer.py` and `caption_metadata_routes.py`. | diff --git a/sdxl_format_route.py b/sdxl_format_route.py new file mode 100644 index 0000000..b06a165 --- /dev/null +++ b/sdxl_format_route.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable + + +@dataclass(frozen=True) +class SDXLFormatRequest: + source_text: str + metadata_json: str = "" + negative_prompt: str = "" + input_hint: str = "auto" + target: str = "auto" + style_preset: str = "flat_vector_pony" + quality_preset: str = "pony_high" + trigger: str = "mythp0rt" + prepend_trigger: bool = True + preserve_trigger: bool = False + nude_weight: float = 1.29 + custom_style: str = "" + custom_quality: str = "" + extra_positive: str = "" + extra_negative: str = "" + formatter_profile: str = "manual_controls" + + +@dataclass(frozen=True) +class SDXLFormatRoute: + output: dict[str, str] + branch: str + method: str + target: str + style_preset: str + quality_preset: str + nude_weight: float + + +@dataclass(frozen=True) +class SDXLFormatDependencies: + default_negative: str + apply_formatter_profile: Callable[[str, str, str], tuple[str, str]] + clean: Callable[[Any], str] + row_from_inputs: Callable[[str, str, str], tuple[dict[str, Any] | None, str]] + row_core_tags: Callable[[dict[str, Any], float], list[str]] + soft_tags: Callable[[dict[str, Any], dict[str, Any], float], str] + hard_tags: Callable[[dict[str, Any], dict[str, Any], float], str] + fallback_text_to_sdxl: Callable[[str, bool, float], tuple[str, str, str]] + assemble_prompt: Callable[[str, str, str, str, bool, str, str, str], str] + combine_negative: Callable[..., str] + sanitize_negative_text: Callable[[str], str] + + +def format_sdxl_prompt_result(request: SDXLFormatRequest, deps: SDXLFormatDependencies) -> SDXLFormatRoute: + style_preset, quality_preset = deps.apply_formatter_profile( + request.formatter_profile, + request.style_preset, + request.quality_preset, + ) + target = request.target if request.target in ("auto", "single", "softcore", "hardcore") else "auto" + nude_weight = max(0.1, min(3.0, float(request.nude_weight))) + row, method = deps.row_from_inputs(request.source_text, request.metadata_json, request.input_hint) + + if row and row.get("mode") == "Insta/OF": + soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {} + hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {} + soft_body = deps.soft_tags(soft_row, row, nude_weight) + hard_body = deps.hard_tags(hard_row, row, nude_weight) + soft_prompt = deps.assemble_prompt( + soft_body, + style_preset, + quality_preset, + request.trigger, + request.prepend_trigger, + request.custom_style, + request.custom_quality, + request.extra_positive, + ) + hard_prompt = deps.assemble_prompt( + hard_body, + style_preset, + quality_preset, + request.trigger, + request.prepend_trigger, + request.custom_style, + request.custom_quality, + request.extra_positive, + ) + selected = hard_prompt if target == "hardcore" else soft_prompt + selected_negative = ( + row.get("hardcore_negative_prompt") if target == "hardcore" else row.get("softcore_negative_prompt") + ) + output = { + "sdxl_prompt": selected, + "negative_prompt": deps.sanitize_negative_text( + deps.combine_negative( + deps.default_negative, + selected_negative, + request.negative_prompt, + request.extra_negative, + ) + ), + "sdxl_softcore_prompt": soft_prompt, + "sdxl_hardcore_prompt": hard_prompt, + "softcore_negative_prompt": deps.sanitize_negative_text( + deps.combine_negative(deps.default_negative, row.get("softcore_negative_prompt"), request.extra_negative) + ), + "hardcore_negative_prompt": deps.sanitize_negative_text( + deps.combine_negative(deps.default_negative, row.get("hardcore_negative_prompt"), request.extra_negative) + ), + "method": f"{method}:sdxl(insta_of_pair)", + } + return SDXLFormatRoute( + output=output, + branch="insta_of_pair", + method=output["method"], + target=target, + style_preset=style_preset, + quality_preset=quality_preset, + nude_weight=nude_weight, + ) + + if row: + body = ", ".join(deps.row_core_tags(row, nude_weight)) + extracted_negative = deps.clean(row.get("negative_prompt")) + method = f"{method}:sdxl(metadata)" + branch = "metadata" + else: + body, extracted_negative, method = deps.fallback_text_to_sdxl( + request.source_text, + request.preserve_trigger, + nude_weight, + ) + branch = "fallback" + + prompt = deps.assemble_prompt( + body, + style_preset, + quality_preset, + request.trigger, + request.prepend_trigger, + request.custom_style, + request.custom_quality, + request.extra_positive, + ) + output = { + "sdxl_prompt": prompt, + "negative_prompt": deps.sanitize_negative_text( + deps.combine_negative(deps.default_negative, extracted_negative, request.negative_prompt, request.extra_negative) + ), + "sdxl_softcore_prompt": "", + "sdxl_hardcore_prompt": "", + "softcore_negative_prompt": "", + "hardcore_negative_prompt": "", + "method": method, + } + return SDXLFormatRoute( + output=output, + branch=branch, + method=method, + target=target, + style_preset=style_preset, + quality_preset=quality_preset, + nude_weight=nude_weight, + ) + + +def format_sdxl_prompt(request: SDXLFormatRequest, deps: SDXLFormatDependencies) -> dict[str, str]: + return format_sdxl_prompt_result(request, deps).output diff --git a/sdxl_formatter.py b/sdxl_formatter.py index b0e0b85..9d54c08 100644 --- a/sdxl_formatter.py +++ b/sdxl_formatter.py @@ -4,12 +4,14 @@ from typing import Any try: from . import formatter_input as input_policy + from . import sdxl_format_route from . import sdxl_tag_policy from . import sdxl_tag_routes from . import sdxl_presets as sdxl_policy from .prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt except ImportError: # Allows local smoke tests with `python -c`. import formatter_input as input_policy + import sdxl_format_route import sdxl_tag_policy import sdxl_tag_routes import sdxl_presets as sdxl_policy @@ -203,6 +205,26 @@ def _fallback_text_to_sdxl( return tags, negative, "text(fallback)" +def _sdxl_format_dependencies() -> sdxl_format_route.SDXLFormatDependencies: + return sdxl_format_route.SDXLFormatDependencies( + default_negative=SDXL_DEFAULT_NEGATIVE, + apply_formatter_profile=lambda profile, style, quality: sdxl_policy.apply_formatter_profile( + profile, + style_preset=style, + quality_preset=quality, + ), + clean=_clean, + row_from_inputs=_row_from_inputs, + row_core_tags=_row_core_tags, + soft_tags=_soft_tags, + hard_tags=_hard_tags, + fallback_text_to_sdxl=_fallback_text_to_sdxl, + assemble_prompt=_assemble_prompt, + combine_negative=_combine_negative, + sanitize_negative_text=sanitize_negative_text, + ) + + def format_sdxl_prompt( source_text: str, metadata_json: str = "", @@ -221,85 +243,24 @@ def format_sdxl_prompt( extra_negative: str = "", formatter_profile: str = "manual_controls", ) -> dict[str, str]: - style_preset, quality_preset = sdxl_policy.apply_formatter_profile( - formatter_profile, - style_preset=style_preset, - quality_preset=quality_preset, - ) - target = target if target in ("auto", "single", "softcore", "hardcore") else "auto" - nude_weight = max(0.1, min(3.0, float(nude_weight))) - row, method = _row_from_inputs(source_text, metadata_json, input_hint) - - if row and row.get("mode") == "Insta/OF": - soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {} - hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {} - soft_body = _soft_tags(soft_row, row, nude_weight) - hard_body = _hard_tags(hard_row, row, nude_weight) - soft_prompt = _assemble_prompt( - soft_body, - style_preset, - quality_preset, - trigger, - prepend_trigger, - custom_style, - custom_quality, - extra_positive, - ) - hard_prompt = _assemble_prompt( - hard_body, - style_preset, - quality_preset, - trigger, - prepend_trigger, - custom_style, - custom_quality, - extra_positive, - ) - selected = hard_prompt if target == "hardcore" else soft_prompt - selected_negative = ( - row.get("hardcore_negative_prompt") if target == "hardcore" else row.get("softcore_negative_prompt") - ) - return { - "sdxl_prompt": selected, - "negative_prompt": sanitize_negative_text( - _combine_negative(SDXL_DEFAULT_NEGATIVE, selected_negative, negative_prompt, extra_negative) - ), - "sdxl_softcore_prompt": soft_prompt, - "sdxl_hardcore_prompt": hard_prompt, - "softcore_negative_prompt": sanitize_negative_text( - _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("softcore_negative_prompt"), extra_negative) - ), - "hardcore_negative_prompt": sanitize_negative_text( - _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("hardcore_negative_prompt"), extra_negative) - ), - "method": f"{method}:sdxl(insta_of_pair)", - } - - if row: - body = ", ".join(_row_core_tags(row, nude_weight)) - extracted_negative = _clean(row.get("negative_prompt")) - method = f"{method}:sdxl(metadata)" - else: - body, extracted_negative, method = _fallback_text_to_sdxl(source_text, preserve_trigger, nude_weight) - - prompt = _assemble_prompt( - body, - style_preset, - quality_preset, - trigger, - prepend_trigger, - custom_style, - custom_quality, - extra_positive, - ) - return { - "sdxl_prompt": prompt, - "negative_prompt": sanitize_negative_text( - _combine_negative(SDXL_DEFAULT_NEGATIVE, extracted_negative, negative_prompt, extra_negative) + return sdxl_format_route.format_sdxl_prompt( + sdxl_format_route.SDXLFormatRequest( + source_text=source_text, + metadata_json=metadata_json, + negative_prompt=negative_prompt, + input_hint=input_hint, + target=target, + style_preset=style_preset, + quality_preset=quality_preset, + trigger=trigger, + prepend_trigger=prepend_trigger, + preserve_trigger=preserve_trigger, + nude_weight=nude_weight, + custom_style=custom_style, + custom_quality=custom_quality, + extra_positive=extra_positive, + extra_negative=extra_negative, + formatter_profile=formatter_profile, ), - "sdxl_softcore_prompt": "", - "sdxl_hardcore_prompt": "", - "softcore_negative_prompt": "", - "hardcore_negative_prompt": "", - "method": method, - } + _sdxl_format_dependencies(), + ) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index af3d0b3..711c367 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -79,6 +79,7 @@ import row_route_metadata # noqa: E402 import row_subject_route # noqa: E402 import server_routes # noqa: E402 import sdxl_formatter # noqa: E402 +import sdxl_format_route # noqa: E402 import sdxl_presets # noqa: E402 import sdxl_tag_policy # noqa: E402 import sdxl_tag_routes # noqa: E402 @@ -2763,6 +2764,126 @@ def smoke_sdxl_presets_policy() -> None: _expect("score_9" not in profiled_prompt, "SDXL photo profile should switch away from Pony score quality tail") +def smoke_sdxl_format_route_policy() -> None: + row = _prompt_row( + name="sdxl_format_route_single", + category="woman", + subcategory="random", + seed=3701, + men_count=0, + camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5), + ) + single_request = sdxl_format_route.SDXLFormatRequest( + source_text="", + metadata_json=_json(row), + target="single", + style_preset="flat_vector_pony", + quality_preset="pony_high", + trigger=SdxlTrigger, + prepend_trigger=True, + nude_weight=9.0, + extra_positive="sdxl route marker", + extra_negative="sdxl route negative", + formatter_profile="sdxl_photo", + ) + typed_single = sdxl_format_route.format_sdxl_prompt_result( + single_request, + sdxl_formatter._sdxl_format_dependencies(), + ) + public_single = sdxl_formatter.format_sdxl_prompt( + "", + metadata_json=single_request.metadata_json, + target=single_request.target, + style_preset=single_request.style_preset, + quality_preset=single_request.quality_preset, + trigger=single_request.trigger, + prepend_trigger=single_request.prepend_trigger, + nude_weight=single_request.nude_weight, + extra_positive=single_request.extra_positive, + extra_negative=single_request.extra_negative, + formatter_profile=single_request.formatter_profile, + ) + _expect(typed_single.output == public_single, "Typed SDXL format route should match public single formatter output") + _expect(typed_single.branch == "metadata", "Typed SDXL format route changed single branch") + _expect(typed_single.target == "single", "Typed SDXL format route lost target normalization") + _expect(typed_single.nude_weight == 3.0, "Typed SDXL format route should clamp high nude weight") + _expect(typed_single.style_preset == "photographic", "Typed SDXL format route lost profile style override") + _expect("sdxl route marker" in typed_single.output.get("sdxl_prompt", ""), "Typed SDXL route lost extra positive") + _expect("sdxl route negative" in typed_single.output.get("negative_prompt", ""), "Typed SDXL route lost extra negative") + + pair = pb.build_insta_of_pair( + row_number=1, + start_index=1, + seed=3702, + ethnicity="any", + figure="random", + no_plus_women=False, + no_black=False, + trigger=Trigger, + prepend_trigger_to_prompt=True, + options_json=_insta_options(), + character_cast=_character_cast(), + hardcore_position_config=_action_filter("penetration_only"), + ) + pair_request = sdxl_format_route.SDXLFormatRequest( + source_text="", + metadata_json=_json(pair), + target="hardcore", + trigger=SdxlTrigger, + prepend_trigger=True, + extra_positive="pair sdxl route marker", + extra_negative="pair sdxl route negative", + ) + typed_pair = sdxl_format_route.format_sdxl_prompt_result( + pair_request, + sdxl_formatter._sdxl_format_dependencies(), + ) + public_pair = sdxl_formatter.format_sdxl_prompt( + "", + metadata_json=pair_request.metadata_json, + target=pair_request.target, + trigger=pair_request.trigger, + prepend_trigger=pair_request.prepend_trigger, + extra_positive=pair_request.extra_positive, + extra_negative=pair_request.extra_negative, + ) + _expect(typed_pair.output == public_pair, "Typed SDXL format route should match public pair formatter output") + _expect(typed_pair.branch == "insta_of_pair", "Typed SDXL format route changed pair branch") + _expect_text("sdxl_format_route_policy.hard_prompt", typed_pair.output.get("sdxl_hardcore_prompt"), 40) + _expect("pair sdxl route marker" in typed_pair.output.get("sdxl_prompt", ""), "Typed SDXL pair route lost extra positive") + + fallback_request = sdxl_format_route.SDXLFormatRequest( + source_text="Characters: woman. Erotic outfit: sheer dress. Camera: side view. Avoid: blur", + input_hint="prompt", + target="weird", + trigger=SdxlTrigger, + prepend_trigger=False, + nude_weight=0.01, + style_preset="none", + quality_preset="none", + ) + typed_fallback = sdxl_format_route.format_sdxl_prompt_result( + fallback_request, + sdxl_formatter._sdxl_format_dependencies(), + ) + public_fallback = sdxl_formatter.format_sdxl_prompt( + fallback_request.source_text, + input_hint=fallback_request.input_hint, + target=fallback_request.target, + trigger=fallback_request.trigger, + prepend_trigger=fallback_request.prepend_trigger, + nude_weight=fallback_request.nude_weight, + style_preset=fallback_request.style_preset, + quality_preset=fallback_request.quality_preset, + ) + _expect(typed_fallback.output == public_fallback, "Typed SDXL format route should match public fallback output") + _expect(typed_fallback.branch == "fallback", "Typed SDXL format route changed fallback branch") + _expect(typed_fallback.target == "auto", "Typed SDXL format route should normalize invalid target") + _expect(typed_fallback.nude_weight == 0.1, "Typed SDXL format route should clamp low nude weight") + _expect("Characters:" not in typed_fallback.output.get("sdxl_prompt", ""), "Typed SDXL fallback leaked Characters label") + _expect("blur" in typed_fallback.output.get("negative_prompt", ""), "Typed SDXL fallback route lost Avoid negative") + + def smoke_sdxl_tag_policy() -> None: row = _fixture_hardcore_row( action_family="oral", @@ -5628,6 +5749,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("caption_text_policy", smoke_caption_text_policy), ("caption_metadata_routes", smoke_caption_metadata_routes), ("sdxl_presets_policy", smoke_sdxl_presets_policy), + ("sdxl_format_route_policy", smoke_sdxl_format_route_policy), ("sdxl_tag_policy", smoke_sdxl_tag_policy), ("sdxl_tag_routes", smoke_sdxl_tag_routes), ("hardcore_position_config_policy", smoke_hardcore_position_config_policy),