From 8bff345cf70e3e2cc5f21e64d9fe1ad67173c770 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 20:03:36 +0200 Subject: [PATCH] Extract Insta pair clothing routing --- docs/prompt-architecture-improvement-plan.md | 7 +- docs/prompt-pool-routing-map.md | 5 +- pair_clothing.py | 345 +++++++++++++++++++ prompt_builder.py | 327 +----------------- tools/prompt_smoke.py | 3 + 5 files changed, 373 insertions(+), 314 deletions(-) create mode 100644 pair_clothing.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index e150345..f46ba9d 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -154,8 +154,7 @@ Improve later: - make a single pair metadata sanitizer that normalizes `softcore_row`, `hardcore_row`, pair prompts, negatives, captions, and camera fields; - split pair assembly into small functions by phase: - `build_soft_row`, `build_hard_row`, `resolve_pair_clothing`, - `assemble_pair_metadata`. + `build_soft_row`, `build_hard_row`, `assemble_pair_metadata`. Already isolated: @@ -163,6 +162,10 @@ Already isolated: camera config selection, same-as-softcore mode, camera-detail override, same-room hard scene continuity, camera-aware composition mutation, POV camera suppression, and row/root camera metadata synchronization. +- pair-level hardcore clothing continuity lives in `pair_clothing.py`, + including action-aware body-access flags, conflicting outfit-piece cleanup, + default visible-men clothing, character-clothing override handling, and final + root clothing-state assembly. ### Krea2 Formatter Path diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index c7dd49d..f0f5e4e 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -66,6 +66,7 @@ Core helper ownership: | --- | --- | | `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. | | `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. | +| `pair_clothing.py` | Insta/OF hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, default visible-men clothing, and final root clothing-state assembly. | | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | | `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. | | `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. | @@ -842,8 +843,8 @@ Use these traces to narrow a problem in one pass. 2. Check hard row `item` and `source_role_graph` for access flags. 3. Character slot `hardcore_clothing` overrides pair fallback clothing. 4. For Krea wording, inspect `krea_clothing.natural_clothing_state`. -5. For generation wording, inspect `_insta_of_hardcore_clothing_state`, - `_hardcore_row_access_flags`, and `character_hardcore_clothing_values`. +5. For generation wording, inspect `pair_clothing.py` and + `character_hardcore_clothing_values`. ### Softcore contains strange no-contact or bed/action leakage diff --git a/pair_clothing.py b/pair_clothing.py new file mode 100644 index 0000000..9f33844 --- /dev/null +++ b/pair_clothing.py @@ -0,0 +1,345 @@ +from __future__ import annotations + +import re +from typing import Any, Callable + + +WOMAN_LOWER_ACCESS_TERMS = ( + "penetrat", + "thrust", + "vaginal", + "anal", + "rear-entry", + "rear entry", + "front-and-back", + "front and back", + "double", + "doggy", + "missionary", + "cowgirl", + "straddles", + "hips aligned", + "penis into", + "penis inside", + "penis entering", + "mouth on her pussy", + "mouth pressed to her pussy", + "pussy licking", + "cunnilingus", + "thighs spread", + "thighs open", + "legs spread", + "legs open", + "cum on pussy", + "cum across her pussy", + "cum dripping from pussy", + "cum dripping from ass", + "cum on belly", + "cum on thighs", + "cum across her ass", + "cum across her lower back", + "toy aligned", + "second penetration point", +) + +WOMAN_UPPER_ACCESS_TERMS = ( + "boobjob", + "titjob", + "breast sex", + "breasts around", + "breasts tightly", + "hands pressing both breasts", + "breasts together", + "cum on breasts", + "cum across her breasts", + "cum on chest", +) + +MAN_LOWER_ACCESS_TERMS = ( + "penis", + "glans", + "testicle", + "balls", + "cumshot", + "ejaculat", + "semen", + "boobjob", + "titjob", + "breast sex", + "footjob", + "handjob", + "hand job", + "hand wrapped", + "hand stroking", + "blowjob", + "fellatio", + "penis sucking", + "penis in mouth", + "mouth on penis", + "penis licking", +) + +LOWER_BODY_CLOTHING_TERMS = ( + "panty", + "panties", + "brief", + "briefs", + "thong", + "bottom", + "bottoms", + "bodysuit", + "teddy", + "dress", + "skirt", + "shorts", + "jeans", + "trousers", + "pants", + "bikini", + "towel", + "sheet", + "blanket", +) + +UPPER_BODY_CLOTHING_TERMS = ( + "bra", + "cup", + "cups", + "corset", + "bodysuit", + "bustier", + "top", + "camisole", + "shirt", + "blouse", + "bodice", + "dress", + "robe", + "jacket", + "sweater", + "harness", + "chest", + "cleavage", + "panel", + "panels", +) + +INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS = [ + "wears an open button shirt with jeans lowered below the hips for genital access", + "wears a fitted tee pushed up with trousers lowered below the hips", + "keeps a dark shirt on while pants and underwear are pulled down below the hips", + "wears an open overshirt with jeans pushed down at the thighs", + "wears a hoodie lifted at the waist with sweatpants lowered below the hips", + "wears gym shorts pulled down below the hips with his shirt still on", + "keeps a casual shirt on with belt open and pants lowered below the hips", + "wears a half-open shirt with lower garments pushed down below the hips", +] + +INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE = [ + "wears an open button shirt with jeans unfastened", + "wears a fitted tee with pants opened at the waist", + "keeps a dark shirt on with trousers loosened", + "wears an open overshirt with jeans partly lowered", + "wears gym shorts loose at the waist with a towel nearby", + "wears a hoodie lifted at the waist with sweatpants loosened", + "wears a casual shirt with belt open and pants partly lowered", + "wears a half-open shirt with dark trousers", +] + + +def hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]: + axis_values = row.get("item_axis_values") + axis_text = " ".join(str(value) for value in axis_values.values()) if isinstance(axis_values, dict) else "" + role_text = " ".join( + str(part or "") + for part in ( + row.get("source_role_graph"), + row.get("role_graph"), + ) + ).lower() + detail_text = " ".join( + str(part or "") + for part in ( + row.get("item"), + row.get("source_composition"), + row.get("composition"), + axis_text, + ) + ).lower() + full_text = f"{role_text} {detail_text}" + return { + "woman_lower": any(term in role_text for term in WOMAN_LOWER_ACCESS_TERMS), + "woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS), + "man_lower": any(term in role_text for term in MAN_LOWER_ACCESS_TERMS), + } + + +def _outfit_without_lower_body_blockers(outfit: str) -> str: + text = str(outfit or "").strip() + if not text: + return "" + text = re.sub(r"\blingerie set\b", "lingerie top details", text, flags=re.IGNORECASE) + text = re.sub(r"\bbrief set\b", "bra set", text, flags=re.IGNORECASE) + text = re.sub(r"\bbodysuit with\b", "upper bodysuit detail with", text, flags=re.IGNORECASE) + fragments = re.split(r"\s*,\s*|\s+\band\b\s+|\s+\bwith\b\s+|\s+\bunder\b\s+|\s+\bover\b\s+", text) + kept = [] + for fragment in fragments: + fragment = fragment.strip(" ,.;") + fragment = re.sub(r"^(?:and|with|under|over)\s+", "", fragment, flags=re.IGNORECASE) + if not fragment: + continue + lower = fragment.lower() + if any(term in lower for term in LOWER_BODY_CLOTHING_TERMS): + continue + kept.append(fragment) + if not kept: + return "" + deduped = [] + seen = set() + for fragment in kept: + key = re.sub(r"\W+", " ", fragment.lower()).strip() + if key and key not in seen: + deduped.append(fragment) + seen.add(key) + return ", ".join(deduped) + + +def _outfit_without_upper_body_blockers(outfit: str) -> str: + text = str(outfit or "").strip() + if not text: + return "" + text = re.sub(r"\blingerie set\b", "lingerie styling", text, flags=re.IGNORECASE) + text = re.sub(r"\bbalconette bra and brief set\b", "briefs and garter styling", text, flags=re.IGNORECASE) + fragments = re.split(r"\s*,\s*|\s+\band\s+|\s+\bwith\s+|\s+\bunder\s+|\s+\bover\s+", text) + kept = [] + for fragment in fragments: + fragment = fragment.strip(" ,.;") + fragment = re.sub(r"^(?:and|with|under|over)\s+", "", fragment, flags=re.IGNORECASE) + if not fragment: + continue + lower = fragment.lower() + if any(term in lower for term in UPPER_BODY_CLOTHING_TERMS): + continue + kept.append(fragment) + if not kept: + return "" + deduped = [] + seen = set() + for fragment in kept: + key = re.sub(r"\W+", " ", fragment.lower()).strip() + if key and key not in seen: + deduped.append(fragment) + seen.add(key) + return ", ".join(deduped) + + +def hardcore_clothing_state( + mode: str, + softcore_outfit: str, + continuity_map: dict[str, str], + woman_access: str = "", +) -> str: + mode = mode if mode in continuity_map else "none" + outfit = str(softcore_outfit or "").strip() + if mode == "none" or not outfit: + return "" + base = continuity_map[mode] + if mode == "explicit_nude": + return f"Body exposure: {base}." + if mode == "implied_nude": + return f"Body exposure: {base}." + if mode == "partially_removed" and woman_access == "lower": + detail = _outfit_without_lower_body_blockers(outfit) + base = "Woman A's lower body is clear; any lower garment is pulled aside or removed below the hips" + if detail: + return f"Clothing state: {base}; visible remaining styling: {detail}." + return f"Clothing state: {base}." + if mode == "partially_removed" and woman_access == "upper": + detail = _outfit_without_upper_body_blockers(outfit) + base = "Woman A's breasts and upper body are clear; any bra cup, bodice, or top panel is pulled aside or removed" + if detail: + return f"Clothing state: {base}; visible remaining styling: {detail}." + return f"Clothing state: {base}." + if mode == "partially_removed": + return f"Clothing state: Woman A keeps the outfit mostly on; teaser outfit detail: {outfit}." + return f"Clothing state: {base}; teaser outfit detail: {outfit}." + + +def default_man_hardcore_clothing_entries( + men_count: int, + pov_labels: list[str] | None, + configured_entries: list[str], + rng: Any, + needs_lower_access: bool, + choose: Callable[[Any, list[str]], str], + sentence_builder: Callable[[str, str], str], +) -> list[str]: + pov_set = set(pov_labels or []) + configured_labels = { + match.group(1) + for entry in configured_entries + for match in [re.match(r"^\s*(Man [A-Z])\b", str(entry or ""))] + if match + } + pool = INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS if needs_lower_access else INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE + entries = [] + for index in range(max(0, int(men_count))): + label = f"Man {chr(ord('A') + index)}" + if label in pov_set or label in configured_labels: + continue + entries.append(sentence_builder(label, choose(rng, pool))) + return entries + + +def resolve_hardcore_pair_clothing( + *, + hard_row: dict[str, Any], + mode: str, + softcore_outfit: str, + character_hardcore_clothing_entries: list[str], + men_count: int, + pov_labels: list[str] | None, + rng: Any, + continuity_map: dict[str, str], + choose: Callable[[Any, list[str]], str], + sentence_builder: Callable[[str, str], str], +) -> dict[str, Any]: + access_flags = hardcore_row_access_flags(hard_row) + woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else "" + default_man_entries = default_man_hardcore_clothing_entries( + men_count, + pov_labels, + character_hardcore_clothing_entries, + rng, + access_flags["man_lower"], + choose, + sentence_builder, + ) + has_primary_hardcore_clothing = any(entry.startswith("Woman A") for entry in character_hardcore_clothing_entries) + fallback_state = "" if has_primary_hardcore_clothing else hardcore_clothing_state( + mode, + softcore_outfit, + continuity_map, + woman_access=woman_access, + ) + hard_clothing_parts = [ + part.strip().rstrip(".") + for part in ( + fallback_state, + *character_hardcore_clothing_entries, + *default_man_entries, + ) + if str(part or "").strip() + ] + hard_clothing_state = "; ".join(hard_clothing_parts) + return { + "access_flags": access_flags, + "woman_access": woman_access, + "default_man_hardcore_clothing": default_man_entries, + "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() + ), + } diff --git a/prompt_builder.py b/prompt_builder.py index 251da4d..9089a12 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -25,6 +25,7 @@ try: template_list as _template_list, ) from . import generate_prompt_batches as g + from . import pair_clothing from . import pair_camera from . import scene_camera_adapters from .hardcore_text_cleanup import ( @@ -55,6 +56,7 @@ except ImportError: # Allows local smoke tests with `python -c`. template_list as _template_list, ) import generate_prompt_batches as g + import pair_clothing import pair_camera import scene_camera_adapters from hardcore_text_cleanup import ( @@ -6922,290 +6924,6 @@ def _insta_of_softcore_pose(rng: random.Random, level: str) -> str: return g.choose(rng, pool) -WOMAN_LOWER_ACCESS_TERMS = ( - "penetrat", - "thrust", - "vaginal", - "anal", - "rear-entry", - "rear entry", - "front-and-back", - "front and back", - "double", - "doggy", - "missionary", - "cowgirl", - "straddles", - "hips aligned", - "penis into", - "penis inside", - "penis entering", - "mouth on her pussy", - "mouth pressed to her pussy", - "pussy licking", - "cunnilingus", - "thighs spread", - "thighs open", - "legs spread", - "legs open", - "cum on pussy", - "cum across her pussy", - "cum dripping from pussy", - "cum dripping from ass", - "cum on belly", - "cum on thighs", - "cum across her ass", - "cum across her lower back", - "toy aligned", - "second penetration point", -) - -WOMAN_UPPER_ACCESS_TERMS = ( - "boobjob", - "titjob", - "breast sex", - "breasts around", - "breasts tightly", - "hands pressing both breasts", - "breasts together", - "cum on breasts", - "cum across her breasts", - "cum on chest", -) - -MAN_LOWER_ACCESS_TERMS = ( - "penis", - "glans", - "testicle", - "balls", - "cumshot", - "ejaculat", - "semen", - "boobjob", - "titjob", - "breast sex", - "footjob", - "handjob", - "hand job", - "hand wrapped", - "hand stroking", - "blowjob", - "fellatio", - "penis sucking", - "penis in mouth", - "mouth on penis", - "penis licking", -) - -LOWER_BODY_CLOTHING_TERMS = ( - "panty", - "panties", - "brief", - "briefs", - "thong", - "bottom", - "bottoms", - "bodysuit", - "teddy", - "dress", - "skirt", - "shorts", - "jeans", - "trousers", - "pants", - "bikini", - "towel", - "sheet", - "blanket", -) - -UPPER_BODY_CLOTHING_TERMS = ( - "bra", - "cup", - "cups", - "corset", - "bodysuit", - "bustier", - "top", - "camisole", - "shirt", - "blouse", - "bodice", - "dress", - "robe", - "jacket", - "sweater", - "harness", - "chest", - "cleavage", - "panel", - "panels", -) - -INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS = [ - "wears an open button shirt with jeans lowered below the hips for genital access", - "wears a fitted tee pushed up with trousers lowered below the hips", - "keeps a dark shirt on while pants and underwear are pulled down below the hips", - "wears an open overshirt with jeans pushed down at the thighs", - "wears a hoodie lifted at the waist with sweatpants lowered below the hips", - "wears gym shorts pulled down below the hips with his shirt still on", - "keeps a casual shirt on with belt open and pants lowered below the hips", - "wears a half-open shirt with lower garments pushed down below the hips", -] - -INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE = [ - "wears an open button shirt with jeans unfastened", - "wears a fitted tee with pants opened at the waist", - "keeps a dark shirt on with trousers loosened", - "wears an open overshirt with jeans partly lowered", - "wears gym shorts loose at the waist with a towel nearby", - "wears a hoodie lifted at the waist with sweatpants loosened", - "wears a casual shirt with belt open and pants partly lowered", - "wears a half-open shirt with dark trousers", -] - - -def _hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]: - axis_values = row.get("item_axis_values") - axis_text = " ".join(str(value) for value in axis_values.values()) if isinstance(axis_values, dict) else "" - role_text = " ".join( - str(part or "") - for part in ( - row.get("source_role_graph"), - row.get("role_graph"), - ) - ).lower() - detail_text = " ".join( - str(part or "") - for part in ( - row.get("item"), - row.get("source_composition"), - row.get("composition"), - axis_text, - ) - ).lower() - full_text = f"{role_text} {detail_text}" - return { - "woman_lower": any(term in role_text for term in WOMAN_LOWER_ACCESS_TERMS), - "woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS), - "man_lower": any(term in role_text for term in MAN_LOWER_ACCESS_TERMS), - } - - -def _outfit_without_lower_body_blockers(outfit: str) -> str: - text = str(outfit or "").strip() - if not text: - return "" - text = re.sub(r"\blingerie set\b", "lingerie top details", text, flags=re.IGNORECASE) - text = re.sub(r"\bbrief set\b", "bra set", text, flags=re.IGNORECASE) - text = re.sub(r"\bbodysuit with\b", "upper bodysuit detail with", text, flags=re.IGNORECASE) - fragments = re.split(r"\s*,\s*|\s+\band\b\s+|\s+\bwith\b\s+|\s+\bunder\b\s+|\s+\bover\b\s+", text) - kept = [] - for fragment in fragments: - fragment = fragment.strip(" ,.;") - fragment = re.sub(r"^(?:and|with|under|over)\s+", "", fragment, flags=re.IGNORECASE) - if not fragment: - continue - lower = fragment.lower() - if any(term in lower for term in LOWER_BODY_CLOTHING_TERMS): - continue - kept.append(fragment) - if not kept: - return "" - deduped = [] - seen = set() - for fragment in kept: - key = re.sub(r"\W+", " ", fragment.lower()).strip() - if key and key not in seen: - deduped.append(fragment) - seen.add(key) - return ", ".join(deduped) - - -def _outfit_without_upper_body_blockers(outfit: str) -> str: - text = str(outfit or "").strip() - if not text: - return "" - text = re.sub(r"\blingerie set\b", "lingerie styling", text, flags=re.IGNORECASE) - text = re.sub(r"\bbalconette bra and brief set\b", "briefs and garter styling", text, flags=re.IGNORECASE) - fragments = re.split(r"\s*,\s*|\s+\band\s+|\s+\bwith\s+|\s+\bunder\s+|\s+\bover\s+", text) - kept = [] - for fragment in fragments: - fragment = fragment.strip(" ,.;") - fragment = re.sub(r"^(?:and|with|under|over)\s+", "", fragment, flags=re.IGNORECASE) - if not fragment: - continue - lower = fragment.lower() - if any(term in lower for term in UPPER_BODY_CLOTHING_TERMS): - continue - kept.append(fragment) - if not kept: - return "" - deduped = [] - seen = set() - for fragment in kept: - key = re.sub(r"\W+", " ", fragment.lower()).strip() - if key and key not in seen: - deduped.append(fragment) - seen.add(key) - return ", ".join(deduped) - - -def _insta_of_hardcore_clothing_state(mode: str, softcore_outfit: str, woman_access: str = "") -> str: - mode = mode if mode in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY else "none" - outfit = str(softcore_outfit or "").strip() - if mode == "none" or not outfit: - return "" - base = INSTA_OF_HARDCORE_CLOTHING_CONTINUITY[mode] - if mode == "explicit_nude": - return f"Body exposure: {base}." - if mode == "implied_nude": - return f"Body exposure: {base}." - if mode == "partially_removed" and woman_access == "lower": - detail = _outfit_without_lower_body_blockers(outfit) - base = ( - "Woman A's lower body is clear; any lower garment is pulled aside or removed below the hips" - ) - if detail: - return f"Clothing state: {base}; visible remaining styling: {detail}." - return f"Clothing state: {base}." - if mode == "partially_removed" and woman_access == "upper": - detail = _outfit_without_upper_body_blockers(outfit) - base = ( - "Woman A's breasts and upper body are clear; any bra cup, bodice, or top panel is pulled aside or removed" - ) - if detail: - return f"Clothing state: {base}; visible remaining styling: {detail}." - return f"Clothing state: {base}." - if mode == "partially_removed": - return f"Clothing state: Woman A keeps the outfit mostly on; teaser outfit detail: {outfit}." - return f"Clothing state: {base}; teaser outfit detail: {outfit}." - - -def _default_man_hardcore_clothing_entries( - men_count: int, - pov_labels: list[str] | None, - configured_entries: list[str], - rng: random.Random, - needs_lower_access: bool, -) -> list[str]: - pov_set = set(pov_labels or []) - configured_labels = { - match.group(1) - for entry in configured_entries - for match in [re.match(r"^\s*(Man [A-Z])\b", str(entry or ""))] - if match - } - pool = INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS if needs_lower_access else INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE - entries = [] - for index in range(max(0, int(men_count))): - label = f"Man {chr(ord('A') + index)}" - if label in pov_set or label in configured_labels: - continue - entries.append(_hardcore_clothing_sentence(label, g.choose(rng, pool))) - return entries - - def _insta_of_partner_styling( seed_config: dict[str, int], seed: int, @@ -7506,33 +7224,22 @@ def build_insta_of_pair( pov_character_labels, hard_content_rng, ) - access_flags = _hardcore_row_access_flags(hard_row) - woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else "" - default_man_hardcore_clothing_entries = _default_man_hardcore_clothing_entries( - hard_men_count, - pov_character_labels, - character_hardcore_clothing_entries, - hard_content_rng, - access_flags["man_lower"], + clothing_route = pair_clothing.resolve_hardcore_pair_clothing( + hard_row=hard_row, + mode=options["hardcore_clothing_continuity"], + softcore_outfit=soft_row["item"], + character_hardcore_clothing_entries=character_hardcore_clothing_entries, + men_count=hard_men_count, + pov_labels=pov_character_labels, + rng=hard_content_rng, + continuity_map=INSTA_OF_HARDCORE_CLOTHING_CONTINUITY, + choose=g.choose, + sentence_builder=_hardcore_clothing_sentence, ) - has_primary_hardcore_clothing = any(entry.startswith("Woman A") for entry in character_hardcore_clothing_entries) - fallback_hard_clothing_state = "" if has_primary_hardcore_clothing else _insta_of_hardcore_clothing_state( - options["hardcore_clothing_continuity"], - soft_row["item"], - woman_access=woman_access, - ) - hard_clothing_parts = [ - part.strip().rstrip(".") - for part in ( - fallback_hard_clothing_state, - *character_hardcore_clothing_entries, - *default_man_hardcore_clothing_entries, - ) - if str(part or "").strip() - ] - hard_clothing_state = "; ".join(hard_clothing_parts) - hard_clothing_sentence = f"{hard_clothing_state}. " if hard_clothing_state else "" - if "body is fully exposed" in hard_clothing_state.lower() or "bare skin unobstructed" in hard_clothing_state.lower(): + default_man_hardcore_clothing_entries = clothing_route["default_man_hardcore_clothing"] + hard_clothing_state = clothing_route["hardcore_clothing_state"] + hard_clothing_sentence = clothing_route["hardcore_clothing_sentence"] + if clothing_route["requires_body_exposure_scene"]: hard_scene = _body_exposure_scene_text(hard_scene) hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "") hard_row["scene_text"] = hard_scene diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index aefec4d..f0525a0 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -679,6 +679,9 @@ def smoke_krea_pair_clothing_state() -> None: krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore") prompt = _expect_text("krea_pair_clothing_state.krea_prompt", krea.get("krea_prompt"), 60) lower = prompt.lower() + root_clothing = _clean_key(pair.get("hardcore_clothing_state")) + _expect("lower body is clear" in root_clothing, "pair root clothing state lost lower-body access wording") + _expect(pair.get("default_man_hardcore_clothing"), "pair root default man hardcore clothing is missing") _expect("metadata" in krea.get("method", ""), "pair clothing route did not use metadata") _expect("clothing state:" not in lower, "Krea clothing route leaked raw clothing label") _expect("visual clothing state" not in lower, "Krea clothing route fell back to visual clothing state label")