Sanitize hard pair scene continuity

This commit is contained in:
2026-06-27 15:36:57 +02:00
parent bb7df8ad77
commit d0f2670d9c
5 changed files with 61 additions and 5 deletions
+2 -2
View File
@@ -320,9 +320,9 @@ def insta_of_pair_from_row_result(
hard_row_for_text = dict(hard_row) hard_row_for_text = dict(hard_row)
options = row.get("options") options = row.get("options")
if isinstance(options, dict) and options.get("continuity") == "same_creator_same_room": 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"] 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"] hard_row_for_text["composition"] = soft_row["composition"]
include_soft = pair_target.include_softcore include_soft = pair_target.include_softcore
+5
View File
@@ -56,6 +56,11 @@ flowchart TD
The config nodes mostly emit JSON. The final builder nodes parse that JSON and The config nodes mostly emit JSON. The final builder nodes parse that JSON and
call the same core generation functions. 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 ## Main Entry Points
| ComfyUI node | Python entry | What it owns | | ComfyUI node | Python entry | What it owns |
+1 -1
View File
@@ -76,7 +76,7 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma
soft_level = deps.clean(options.get("softcore_level")).replace("_", " ") soft_level = deps.clean(options.get("softcore_level")).replace("_", " ")
hard_level = deps.clean(options.get("hardcore_level")).replace("_", " ") hard_level = deps.clean(options.get("hardcore_level")).replace("_", " ")
same_room = options.get("continuity") == "same_creator_same_room" 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_composition = deps.sanitize_hardcore_environment_anchors(hard.get("composition"))
hard_source_composition = deps.sanitize_hardcore_environment_anchors(hard.get("source_composition") or hard_composition) hard_source_composition = deps.sanitize_hardcore_environment_anchors(hard.get("source_composition") or hard_composition)
pov_labels = deps.merge_labels( pov_labels = deps.merge_labels(
+17 -2
View File
@@ -169,6 +169,10 @@ def body_exposure_scene_text(scene: Any) -> str:
(r",?\s*\blingerie visible nearby\b", ""), (r",?\s*\blingerie visible nearby\b", ""),
(r"\boutfit racks\b", "mirror shelves"), (r"\boutfit racks\b", "mirror shelves"),
(r"\bcostume 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"\bhanging outfits\b", "hanging fabric"),
(r"\bclothing hooks\b", "wall hooks"), (r"\bclothing hooks\b", "wall hooks"),
(r"\boutfit-check\b", "creator-shot"), (r"\boutfit-check\b", "creator-shot"),
@@ -433,6 +437,17 @@ def resolve_hardcore_pair_clothing_result(
if str(part or "").strip() if str(part or "").strip()
] ]
hard_clothing_state = "; ".join(hard_clothing_parts) 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( return HardcorePairClothingRoute(
access_flags=access_flags, access_flags=access_flags,
woman_access=woman_access, woman_access=woman_access,
@@ -440,8 +455,8 @@ def resolve_hardcore_pair_clothing_result(
hardcore_clothing_state=hard_clothing_state, hardcore_clothing_state=hard_clothing_state,
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "", hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
requires_body_exposure_scene=( requires_body_exposure_scene=(
"body is fully exposed" in hard_clothing_state.lower() any(access_flags.values())
or "bare skin unobstructed" in hard_clothing_state.lower() or any(term in hard_clothing_lower for term in scene_cleanup_terms)
), ),
) )
+36
View File
@@ -4711,6 +4711,12 @@ def smoke_pair_options_policy() -> None:
"creator-shot" in pair_clothing.body_exposure_scene_text("outfit-check framing"), "creator-shot" in pair_clothing.body_exposure_scene_text("outfit-check framing"),
"Pair clothing body exposure scene cleanup should replace outfit-check wording", "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( _expect(
pair_clothing.softcore_outfit_sentence("Man A", "wears hoodie and joggers") pair_clothing.softcore_outfit_sentence("Man A", "wears hoodie and joggers")
== "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.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.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") _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: def smoke_pair_builder_policy() -> None:
@@ -5138,6 +5157,12 @@ def smoke_insta_pair() -> None:
def smoke_krea_pair_clothing_state() -> 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( pair = pb.build_insta_of_pair(
row_number=1, row_number=1,
start_index=1, start_index=1,
@@ -5151,6 +5176,7 @@ def smoke_krea_pair_clothing_state() -> None:
options_json=_insta_options(hardcore_clothing_continuity="partially_removed"), options_json=_insta_options(hardcore_clothing_continuity="partially_removed"),
character_cast=_character_cast(), character_cast=_character_cast(),
hardcore_position_config=_action_filter("penetration_only"), hardcore_position_config=_action_filter("penetration_only"),
location_config=forced_location,
) )
_expect_pair(pair, "krea_pair_clothing_state") _expect_pair(pair, "krea_pair_clothing_state")
typed_route = krea_pair_formatter.format_insta_pair_result( 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("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("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("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: def smoke_insta_pair_pov() -> None: