From d0f2670d9ca356a4fdcdcff6e9e4591c648d44f0 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 15:36:57 +0200 Subject: [PATCH] Sanitize hard pair scene continuity --- caption_metadata_routes.py | 4 ++-- docs/prompt-pool-routing-map.md | 5 +++++ krea_pair_formatter.py | 2 +- pair_clothing.py | 19 +++++++++++++++-- tools/prompt_smoke.py | 36 +++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/caption_metadata_routes.py b/caption_metadata_routes.py index 917a2fb..1fca3cd 100644 --- a/caption_metadata_routes.py +++ b/caption_metadata_routes.py @@ -320,9 +320,9 @@ def insta_of_pair_from_row_result( hard_row_for_text = dict(hard_row) options = row.get("options") if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room": - if soft_row.get("scene_text"): + if not hard_row_for_text.get("scene_text") and soft_row.get("scene_text"): hard_row_for_text["scene_text"] = soft_row["scene_text"] - if soft_row.get("composition"): + if not hard_row_for_text.get("composition") and soft_row.get("composition"): hard_row_for_text["composition"] = soft_row["composition"] include_soft = pair_target.include_softcore diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 79b3e66..16bb5c3 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -56,6 +56,11 @@ flowchart TD The config nodes mostly emit JSON. The final builder nodes parse that JSON and call the same core generation functions. +For Insta/OF pair formatting, the embedded `hardcore_row` is authoritative for +hardcore scene/composition text. Same-room continuity may copy the soft scene +into that row earlier, but formatter routes should not bypass later hard-row +cleanup such as clothing/body-access scene sanitization. + ## Main Entry Points | ComfyUI node | Python entry | What it owns | diff --git a/krea_pair_formatter.py b/krea_pair_formatter.py index eabd3e5..f9c3f81 100644 --- a/krea_pair_formatter.py +++ b/krea_pair_formatter.py @@ -76,7 +76,7 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma soft_level = deps.clean(options.get("softcore_level")).replace("_", " ") hard_level = deps.clean(options.get("hardcore_level")).replace("_", " ") same_room = options.get("continuity") == "same_creator_same_room" - hard_scene = soft.get("scene_text") if same_room and soft.get("scene_text") else hard.get("scene_text") + hard_scene = hard.get("scene_text") or (soft.get("scene_text") if same_room else "") hard_composition = deps.sanitize_hardcore_environment_anchors(hard.get("composition")) hard_source_composition = deps.sanitize_hardcore_environment_anchors(hard.get("source_composition") or hard_composition) pov_labels = deps.merge_labels( diff --git a/pair_clothing.py b/pair_clothing.py index 2541e49..18d9d0d 100644 --- a/pair_clothing.py +++ b/pair_clothing.py @@ -169,6 +169,10 @@ def body_exposure_scene_text(scene: Any) -> str: (r",?\s*\blingerie visible nearby\b", ""), (r"\boutfit racks\b", "mirror shelves"), (r"\bcostume racks\b", "mirror shelves"), + (r"\bshoe shelves\b", "side shelves"), + (r"\bshoes visible\b", "body placement visible"), + (r"\bbag and shoes visible\b", "nearby floor edge visible"), + (r"\bshoes and bag visible\b", "nearby floor edge visible"), (r"\bhanging outfits\b", "hanging fabric"), (r"\bclothing hooks\b", "wall hooks"), (r"\boutfit-check\b", "creator-shot"), @@ -433,6 +437,17 @@ def resolve_hardcore_pair_clothing_result( if str(part or "").strip() ] hard_clothing_state = "; ".join(hard_clothing_parts) + scene_cleanup_terms = ( + "body is fully exposed", + "bare skin unobstructed", + "body is partly exposed", + "lower body is clear", + "upper body are clear", + "pulled aside", + "removed below the hips", + "pants and underwear are pulled down", + ) + hard_clothing_lower = hard_clothing_state.lower() return HardcorePairClothingRoute( access_flags=access_flags, woman_access=woman_access, @@ -440,8 +455,8 @@ def resolve_hardcore_pair_clothing_result( hardcore_clothing_state=hard_clothing_state, hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "", requires_body_exposure_scene=( - "body is fully exposed" in hard_clothing_state.lower() - or "bare skin unobstructed" in hard_clothing_state.lower() + any(access_flags.values()) + or any(term in hard_clothing_lower for term in scene_cleanup_terms) ), ) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index ffc721f..08ec8d5 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -4711,6 +4711,12 @@ def smoke_pair_options_policy() -> None: "creator-shot" in pair_clothing.body_exposure_scene_text("outfit-check framing"), "Pair clothing body exposure scene cleanup should replace outfit-check wording", ) + sanitized_closet_scene = pair_clothing.body_exposure_scene_text( + "full-length closet mirror with outfit racks, shoe shelves, and soft boutique lighting" + ).lower() + _expect("outfit racks" not in sanitized_closet_scene, "Pair clothing body exposure scene cleanup should remove outfit racks") + _expect("shoe shelves" not in sanitized_closet_scene, "Pair clothing body exposure scene cleanup should remove shoe shelves") + _expect("mirror shelves" in sanitized_closet_scene, "Pair clothing body exposure scene cleanup should keep neutral mirror detail") _expect( pair_clothing.softcore_outfit_sentence("Man A", "wears hoodie and joggers") == "Man A wears hoodie and joggers", @@ -5012,6 +5018,19 @@ def smoke_pair_route_policy() -> None: _expect(clothing_route.as_dict() == clothing_legacy, "Typed pair clothing route should match legacy dict route") _expect(clothing_route.woman_access == "lower", "Typed pair clothing route lost lower-access detection") _expect(clothing_route.requires_body_exposure_scene is True, "Typed pair clothing route lost exposure-scene flag") + partial_common = {**clothing_common, "mode": "partially_removed"} + partial_route = pair_clothing.resolve_hardcore_pair_clothing_result(**partial_common) + _expect(partial_route.requires_body_exposure_scene is True, "Partial lower-access clothing should request scene cleanup") + oral_common = { + **clothing_common, + "hard_row": { + "role_graph": "the woman takes the man's penis in her mouth", + "item": "standing oral test item", + }, + "mode": "partially_removed", + } + oral_route = pair_clothing.resolve_hardcore_pair_clothing_result(**oral_common) + _expect(oral_route.requires_body_exposure_scene is True, "Man lower-access clothing should request scene cleanup") def smoke_pair_builder_policy() -> None: @@ -5138,6 +5157,12 @@ def smoke_insta_pair() -> None: def smoke_krea_pair_clothing_state() -> None: + forced_location = pb.build_location_pool_json( + enabled=True, + combine_mode="replace", + preset="custom_only", + custom_locations="closet_scene: full-length closet mirror with outfit racks, shoe shelves, and soft boutique lighting", + ) pair = pb.build_insta_of_pair( row_number=1, start_index=1, @@ -5151,6 +5176,7 @@ def smoke_krea_pair_clothing_state() -> None: options_json=_insta_options(hardcore_clothing_continuity="partially_removed"), character_cast=_character_cast(), hardcore_position_config=_action_filter("penetration_only"), + location_config=forced_location, ) _expect_pair(pair, "krea_pair_clothing_state") typed_route = krea_pair_formatter.format_insta_pair_result( @@ -5174,6 +5200,16 @@ def smoke_krea_pair_clothing_state() -> None: _expect("softcore outfit" not in lower and "teaser outfit" not in lower, "Krea clothing route leaked softcore outfit label") _expect("lower body is clear" in lower, "Krea clothing route lost generated clothing continuity") _expect("the man keeps" in lower, "Krea clothing route lost partner clothing continuity") + _expect("outfit racks" not in lower and "shoe shelves" not in lower, "Krea pair formatter leaked unsanitized hard scene") + hard_scene = _clean_key(pair["hardcore_row"].get("scene_text")) + _expect("outfit racks" not in hard_scene and "shoe shelves" not in hard_scene, "Pair builder leaked outfit-check scene clutter into hardcore row") + _expect("mirror shelves" in hard_scene, "Pair builder lost neutralized scene anchor") + caption_text, _caption_method = caption_naturalizer.naturalize_caption("", metadata_json=_json(pair), target="hardcore") + caption_lower = caption_text.lower() + _expect("outfit racks" not in caption_lower and "shoe shelves" not in caption_lower, "Caption pair formatter leaked unsanitized hard scene") + sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=_json(pair), target="hardcore") + sdxl_lower = sdxl.get("sdxl_prompt", "").lower() + _expect("outfit racks" not in sdxl_lower and "shoe shelves" not in sdxl_lower, "SDXL pair formatter leaked unsanitized hard scene") def smoke_insta_pair_pov() -> None: