diff --git a/caption_metadata_routes.py b/caption_metadata_routes.py index 0c7e896..917a2fb 100644 --- a/caption_metadata_routes.py +++ b/caption_metadata_routes.py @@ -5,8 +5,10 @@ from dataclasses import dataclass from typing import Any, Callable try: + from . import formatter_input as input_policy from . import formatter_target as target_policy except ImportError: # pragma: no cover - plain-script smoke tests + import formatter_input as input_policy import formatter_target as target_policy @@ -308,7 +310,7 @@ def insta_of_pair_from_row_result( keep_style = request.keep_style pair_target = target_policy.pair_policy(request.target) target = pair_target.pair_target - if deps.clean_text(row.get("mode")).lower() != "insta/of": + if not input_policy.is_pair_metadata(row): return None soft_row = row.get("softcore_row") hard_row = row.get("hardcore_row") diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index d3daf26..82e7ecd 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -698,6 +698,13 @@ Important POV rule: ## Formatter Routes +Formatter metadata input is normalized by `formatter_input.py`. Pair routing is +structural: metadata with both a softcore side and a hardcore side +(`softcore_row`/`hardcore_row` or root soft/hard prompt/caption fields) is +treated as pair metadata even if the UI `mode` label is absent. Krea2, SDXL, and +caption routes share this detection to avoid hidden drift between formatter +paths. + ### Krea2 `format_krea2_prompt` chooses between three roads: diff --git a/formatter_input.py b/formatter_input.py index a602eef..4f3426d 100644 --- a/formatter_input.py +++ b/formatter_input.py @@ -81,11 +81,27 @@ def maybe_json(text: Any) -> dict[str, Any] | None: def normalize_input_metadata(row: dict[str, Any]) -> dict[str, Any]: row = dict(row) trigger = str(row.get("trigger") or "").strip() - if row.get("mode") == "Insta/OF": + if is_pair_metadata(row): return row_normalization_policy.normalize_pair_metadata(row, active_trigger=trigger) return row_normalization_policy.sanitize_metadata_row_text(row, active_trigger=trigger) +def is_pair_metadata(row: Any) -> bool: + if not isinstance(row, dict): + return False + soft_side = ( + isinstance(row.get("softcore_row"), dict) + or bool(clean_text(row.get("softcore_prompt"))) + or bool(clean_text(row.get("softcore_caption"))) + ) + hard_side = ( + isinstance(row.get("hardcore_row"), dict) + or bool(clean_text(row.get("hardcore_prompt"))) + or bool(clean_text(row.get("hardcore_caption"))) + ) + return soft_side and hard_side + + def normalize_input_hint(value: Any, *, text_hint: str = INPUT_HINT_PROMPT) -> str: hint = clean_text(value).lower().replace("-", "_") hint = _INPUT_HINT_ALIASES.get(hint, hint) diff --git a/krea_format_route.py b/krea_format_route.py index 65d9f43..fbc591b 100644 --- a/krea_format_route.py +++ b/krea_format_route.py @@ -5,9 +5,11 @@ from typing import Any, Callable try: from . import formatter_detail as detail_policy + from . import formatter_input as input_policy from . import formatter_target as target_policy except ImportError: # pragma: no cover - plain-script smoke tests import formatter_detail as detail_policy + import formatter_input as input_policy import formatter_target as target_policy @@ -68,7 +70,7 @@ def format_krea2_prompt_result(request: KreaFormatRequest, deps: KreaFormatDepen target = target_policy.normalize_target(request.target) row, method = deps.row_from_inputs(request.source_text, request.metadata_json, request.input_hint) - if row and row.get("mode") == "Insta/OF": + if row and input_policy.is_pair_metadata(row): pair_target = target_policy.pair_policy(target) soft_prompt, soft_negative, hard_prompt, hard_negative = deps.insta_pair_to_krea( row, diff --git a/sdxl_format_route.py b/sdxl_format_route.py index 6ff37c5..a281ac6 100644 --- a/sdxl_format_route.py +++ b/sdxl_format_route.py @@ -4,8 +4,10 @@ from dataclasses import dataclass from typing import Any, Callable try: + from . import formatter_input as input_policy from . import formatter_target as target_policy except ImportError: # pragma: no cover - plain-script smoke tests + import formatter_input as input_policy import formatter_target as target_policy @@ -65,7 +67,7 @@ def format_sdxl_prompt_result(request: SDXLFormatRequest, deps: SDXLFormatDepend 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": + if row and input_policy.is_pair_metadata(row): pair_target = target_policy.pair_policy(target) 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 {} diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index b2ea47f..b78e7b2 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -2932,7 +2932,6 @@ def smoke_formatter_input_policy() -> None: row, method = formatter_input.row_from_inputs(source_json, "", "prompt") _expect(row is None and method == "text", "Formatter input parser should not parse source JSON in explicit prompt mode") pair_metadata = { - "mode": "Insta/OF", "trigger": Trigger, "softcore_row": { "prompt": f"{Trigger}, {Trigger}, embedded-only soft.", @@ -2952,8 +2951,10 @@ def smoke_formatter_input_policy() -> None: "camera_scene_directive": "Row hard scene camera layout.", }, } + _expect(formatter_input.is_pair_metadata(pair_metadata), "Formatter input policy should detect structural pair metadata") parsed_pair, pair_method = formatter_input.row_from_inputs("", _json(pair_metadata), "metadata_json") _expect(pair_method == "metadata_json", "Formatter input parser should read pair metadata JSON") + _expect(formatter_input.is_pair_metadata(parsed_pair), "Formatter input parser should preserve structural pair metadata") _expect_trigger_once("formatter_input.pair.soft_prompt", parsed_pair.get("softcore_prompt"), Trigger) _expect( parsed_pair.get("softcore_partner_styling") == parsed_pair["softcore_row"].get("softcore_partner_styling"), @@ -6107,7 +6108,6 @@ def smoke_formatter_metadata_fixtures() -> None: _expect("sdxl route tag" not in caption_text, "Caption naturalizer leaked SDXL formatter hint") external_pair = { - "mode": "Insta/OF", "trigger": Trigger, "shared_descriptor": "25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes", "shared_cast_descriptors": [