From ab2a13ecde57d297baf25bdb201c869ca4a07a18 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 02:32:38 +0200 Subject: [PATCH] Synchronize pair side metadata --- docs/prompt-architecture-improvement-plan.md | 10 ++++-- docs/prompt-pool-routing-map.md | 4 +-- row_normalization.py | 24 +++++++++++++++ tools/prompt_smoke.py | 32 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 990290a..aef4b10 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -195,7 +195,9 @@ Already isolated: - final row and pair text normalization lives in `row_normalization.py`, covering trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output synchronization, and row - sanitation before metadata leaves generation. + sanitation before metadata leaves generation. It also copies side-specific + pair metadata, such as soft partner styling and hardcore clothing/detail + state, onto the embedded soft/hard rows. ### Pair / Adapter Layer @@ -232,7 +234,9 @@ Already isolated: shape; the final cleanup step is delegated to `row_normalization.py`. Embedded soft/hard rows are synchronized to the final pair prompt, caption, and negative outputs during normalization so serialized pair metadata does - not carry stale standalone row text. + not carry stale standalone row text. Side-specific structured fields are + synchronized there too, including soft partner styling and hardcore clothing + continuity metadata. ### Krea2 Formatter Path @@ -441,7 +445,7 @@ Medium-term: Near-term: - Normalize pair metadata with one helper, including embedded row prompt, - caption, and negative synchronization. + caption, negative, and side-specific metadata synchronization. - Confirm pair prompts, captions, and soft/hard rows carry the same sanitized scene/camera/clothing fields. - Keep same-room pair continuity synchronized in both assembled prompt text and diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 7707e76..640bc49 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -96,7 +96,7 @@ Core helper ownership: | `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. | | `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 synchronization, and embedded row sanitation. | +| `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. | | `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. | | `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. | | `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. | @@ -494,7 +494,7 @@ plain prompt text. When debugging, inspect these fields before editing pools. | `options` | `SxCP Insta/OF Options` | Formatters/debug | Soft/hard level, cast mode, continuity, camera modes, expression settings. | | `shared_descriptor` | `pair_cast.py` | Pair formatters | Primary creator descriptor. | | `shared_cast_descriptors` | `pair_cast.py` | Pair formatters | Full cast descriptor list. | -| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side; their prompt, caption, and negative fields are synchronized to the final pair outputs during pair normalization. | +| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side; their prompt, caption, negative, and side-specific metadata fields are synchronized to the final pair outputs/root fields during pair normalization. | | `softcore_prompt`, `hardcore_prompt` | `pair_output.py` | Direct output/fallback | Raw pair prompts before formatter rewrite. | | `softcore_negative_prompt`, `hardcore_negative_prompt` | `pair_output.py` | Formatter negatives | Separate negatives for each side. | | `softcore_partner_styling` | `pair_cast.py` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. | diff --git a/row_normalization.py b/row_normalization.py index aed8c4d..18bd6b3 100644 --- a/row_normalization.py +++ b/row_normalization.py @@ -119,10 +119,34 @@ def synchronize_pair_row_outputs(pair: dict[str, Any]) -> dict[str, Any]: return pair +def synchronize_pair_side_metadata(pair: dict[str, Any]) -> dict[str, Any]: + side_keys = { + "softcore_row": ( + "softcore_partner_styling", + ), + "hardcore_row": ( + "hardcore_clothing_state", + "character_hardcore_clothing", + "default_man_hardcore_clothing", + "hardcore_detail_density", + "hardcore_position_config", + ), + } + for row_key, keys in side_keys.items(): + row = pair.get(row_key) + if not isinstance(row, dict): + continue + for key in keys: + if key in pair: + row[key] = pair.get(key) + return pair + + def normalize_pair_metadata(pair: dict[str, Any], *, active_trigger: str = "") -> dict[str, Any]: trigger = str(active_trigger or "").strip() triggers = _trigger_tuple(trigger) synchronize_pair_row_outputs(pair) + synchronize_pair_side_metadata(pair) for key in ("softcore_prompt", "hardcore_prompt"): if key in pair: pair[key] = sanitize_prompt_text(pair.get(key, ""), triggers=triggers) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 55ac18d..3acffd1 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -835,6 +835,12 @@ def smoke_row_normalization_policy() -> None: "hardcore_caption": f"{Trigger}, {Trigger}, hard caption.", "softcore_negative_prompt": "bad anatomy, bad anatomy", "hardcore_negative_prompt": "bad anatomy, low quality, bad anatomy", + "softcore_partner_styling": {"outfits": ["partner outfit"], "pose": "partner pose"}, + "hardcore_clothing_state": "structured hard clothing state", + "character_hardcore_clothing": ["Woman A custom hard clothing"], + "default_man_hardcore_clothing": ["Man A default hard clothing"], + "hardcore_detail_density": "dense", + "hardcore_position_config": {"family": "oral"}, "softcore_row": { "prompt": f"{Trigger}, {Trigger}, embedded soft.", "caption": f"{Trigger}, {Trigger}, embedded soft caption.", @@ -860,6 +866,18 @@ def smoke_row_normalization_policy() -> None: pair["hardcore_row"].get("caption") == pair.get("hardcore_caption"), "Pair normalization left stale hard row caption text", ) + _expect( + pair["softcore_row"].get("softcore_partner_styling") == pair.get("softcore_partner_styling"), + "Pair normalization left stale soft side metadata", + ) + _expect( + pair["hardcore_row"].get("hardcore_clothing_state") == pair.get("hardcore_clothing_state"), + "Pair normalization left stale hard clothing metadata", + ) + _expect( + pair["hardcore_row"].get("default_man_hardcore_clothing") == pair.get("default_man_hardcore_clothing"), + "Pair normalization left stale hard default clothing metadata", + ) _expect_no_duplicate_comma_items("row_normalization.pair.soft_negative", pair.get("softcore_negative_prompt")) _expect_no_duplicate_comma_items("row_normalization.pair.hard_row_negative", pair["hardcore_row"].get("negative_prompt")) @@ -1511,6 +1529,20 @@ def _expect_pair(pair: dict[str, Any], name: str) -> None: pair["hardcore_row"].get("negative_prompt") == pair.get("hardcore_negative_prompt"), f"{name}.hardcore_row negative drifted from pair negative", ) + if "softcore_partner_styling" in pair: + _expect( + pair["softcore_row"].get("softcore_partner_styling") == pair.get("softcore_partner_styling"), + f"{name}.softcore_row partner styling drifted from pair root", + ) + for key in ( + "hardcore_clothing_state", + "character_hardcore_clothing", + "default_man_hardcore_clothing", + "hardcore_detail_density", + "hardcore_position_config", + ): + if key in pair: + _expect(pair["hardcore_row"].get(key) == pair.get(key), f"{name}.hardcore_row {key} drifted from pair root") _expect_no_duplicate_comma_items(f"{name}.softcore_negative", pair.get("softcore_negative_prompt")) _expect_no_duplicate_comma_items(f"{name}.hardcore_negative", pair.get("hardcore_negative_prompt")) _expect_formatter_outputs(pair, name, target="softcore")