Move pair clothing wording policy

This commit is contained in:
2026-06-27 03:28:44 +02:00
parent 61535cc60d
commit 049f2c6e87
6 changed files with 136 additions and 89 deletions
+4 -3
View File
@@ -231,9 +231,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
- pair-level clothing policy lives in `pair_clothing.py`, including clothing
sentence formatting, body-exposure scene cleanup, action-aware body-access
flags, conflicting outfit-piece cleanup, default visible-men clothing,
character-clothing override handling, hardcore clothing continuity, and final
root clothing-state assembly.
- final pair output assembly lives in `pair_output.py`, including soft/hard
prompt strings, trigger preservation, negatives, captions, and root metadata
+1 -1
View File
@@ -82,7 +82,7 @@ Core helper ownership:
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, hardcore cast count policy, and hardcore detail-density directives. |
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
| `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. |
| `pair_clothing.py` | Insta/OF clothing sentence formatting, body-exposure scene cleanup, hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, and final root clothing-state assembly. |
| `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata 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. |
+84 -4
View File
@@ -147,6 +147,89 @@ INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE = [
]
def _clean_pair_punctuation(text: Any) -> str:
text = re.sub(r"\s+", " ", str(text or "")).strip()
text = re.sub(r"\s+([,.;:])", r"\1", text)
text = re.sub(r"(?:,\s*){2,}", ", ", text)
text = re.sub(r"\.\s*\.", ".", text)
text = re.sub(r":\s*\.", ".", text)
return text.strip()
def body_exposure_scene_text(scene: Any) -> str:
text = str(scene or "").strip()
if not text:
return ""
replacements = (
(r",?\s*\bscattered (?:clothes|clothing)\b", ""),
(r",?\s*\bfloor clothes\b", ""),
(r"\bclothes scattered\b", "soft floor shadows"),
(r",?\s*\bscattered lingerie\b", ""),
(r",?\s*\blingerie visible nearby\b", ""),
(r"\boutfit racks\b", "mirror shelves"),
(r"\bcostume racks\b", "mirror shelves"),
(r"\bhanging outfits\b", "hanging fabric"),
(r"\bclothing hooks\b", "wall hooks"),
(r"\boutfit-check\b", "creator-shot"),
(r"\boutfit framing\b", "body framing"),
(r"\bfull outfits\b", "full bodies"),
(r"\bcoordinated outfits\b", "coordinated posing"),
)
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
text = re.sub(r"\bwith,\s*", "with ", text, flags=re.IGNORECASE)
text = re.sub(r",\s*,", ",", text)
return _clean_pair_punctuation(text)
def softcore_outfit_sentence(label: str, outfit: str) -> str:
outfit = str(outfit or "").strip()
if not outfit:
return ""
lower = outfit.lower()
if lower.startswith(("wears ", "wearing ", "in ")):
return f"{label} {outfit}"
return f"{label} wears {outfit}"
def hardcore_clothing_sentence(label: str, clothing: str) -> str:
clothing = str(clothing or "").strip().rstrip(".")
if not clothing:
return ""
lower = clothing.lower()
if lower.startswith(("fully nude", "nude")):
return f"{label}'s body is fully exposed, bare skin unobstructed"
if lower.startswith("partly nude"):
return f"{label}'s body is partly exposed"
if lower.startswith(("is ", "wears ", "wearing ", "keeps ", "has ", "with ")):
return f"{label} {clothing}"
return f"{label}'s clothing: {clothing}"
def character_hardcore_clothing_entries(
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
pov_labels: list[str] | None,
rng: Any,
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str],
) -> list[str]:
pov_set = set(pov_labels or [])
labels = [
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
]
entries: list[str] = []
for label in labels:
if label in pov_set:
continue
clothing = slot_hardcore_clothing(label_map.get(label), rng)
sentence = hardcore_clothing_sentence(label, clothing)
if sentence:
entries.append(sentence)
return entries
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 ""
@@ -272,7 +355,6 @@ def default_man_hardcore_clothing_entries(
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 = {
@@ -287,7 +369,7 @@ def default_man_hardcore_clothing_entries(
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)))
entries.append(hardcore_clothing_sentence(label, choose(rng, pool)))
return entries
@@ -302,7 +384,6 @@ def resolve_hardcore_pair_clothing(
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 ""
@@ -313,7 +394,6 @@ def resolve_hardcore_pair_clothing(
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(
+6 -2
View File
@@ -2,6 +2,11 @@ from __future__ import annotations
from typing import Any, Callable
try:
from . import pair_clothing
except ImportError: # Allows local smoke tests with top-level imports.
import pair_clothing
BuildPrompt = Callable[..., dict[str, Any]]
AxisRng = Callable[[dict[str, int], str, int, int], Any]
@@ -45,7 +50,6 @@ def build_insta_pair_rows(
softcore_outfit: Callable[[Any, str], str],
softcore_pose: Callable[[Any, str], str],
softcore_item_prompt_label: Callable[[str], str],
body_exposure_scene_text: Callable[[Any], str],
pov_prompt_directive: Callable[[list[str]], str],
pov_composition_prompt: Callable[[Any, list[str]], str],
) -> dict[str, Any]:
@@ -134,7 +138,7 @@ def build_insta_pair_rows(
soft_row["softcore_outfit_policy"] = "character_slot:Woman A" if primary_softcore_outfit else "insta_of_safe_softcore"
if softcore_level_key == "explicit_nude":
soft_row["source_scene_text"] = soft_row.get("source_scene_text") or soft_row.get("scene_text", "")
soft_row["scene_text"] = body_exposure_scene_text(soft_row.get("scene_text", ""))
soft_row["scene_text"] = pair_clothing.body_exposure_scene_text(soft_row.get("scene_text", ""))
soft_row["pov_character_labels"] = (
pov_character_labels
if options["softcore_cast"] == "same_as_hardcore"
+5 -79
View File
@@ -2476,32 +2476,6 @@ def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
return pov_policy.pov_composition_prompt(composition, pov_labels)
def _body_exposure_scene_text(scene: Any) -> str:
text = str(scene or "").strip()
if not text:
return ""
replacements = (
(r",?\s*\bscattered (?:clothes|clothing)\b", ""),
(r",?\s*\bfloor clothes\b", ""),
(r"\bclothes scattered\b", "soft floor shadows"),
(r",?\s*\bscattered lingerie\b", ""),
(r",?\s*\blingerie visible nearby\b", ""),
(r"\boutfit racks\b", "mirror shelves"),
(r"\bcostume racks\b", "mirror shelves"),
(r"\bhanging outfits\b", "hanging fabric"),
(r"\bclothing hooks\b", "wall hooks"),
(r"\boutfit-check\b", "creator-shot"),
(r"\boutfit framing\b", "body framing"),
(r"\bfull outfits\b", "full bodies"),
(r"\bcoordinated outfits\b", "coordinated posing"),
)
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
text = re.sub(r"\bwith,\s*", "with ", text, flags=re.IGNORECASE)
text = re.sub(r",\s*,", ",", text)
return _clean_prompt_punctuation(text)
def _slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
@@ -2524,53 +2498,6 @@ def _slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | No
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "hardcore_clothing", rng)
def _softcore_outfit_sentence(label: str, outfit: str) -> str:
outfit = str(outfit or "").strip()
if not outfit:
return ""
lower = outfit.lower()
if lower.startswith(("wears ", "wearing ", "in ")):
return f"{label} {outfit}"
return f"{label} wears {outfit}"
def _hardcore_clothing_sentence(label: str, clothing: str) -> str:
clothing = str(clothing or "").strip().rstrip(".")
if not clothing:
return ""
lower = clothing.lower()
if lower.startswith(("fully nude", "nude")):
return f"{label}'s body is fully exposed, bare skin unobstructed"
if lower.startswith("partly nude"):
return f"{label}'s body is partly exposed"
if lower.startswith(("is ", "wears ", "wearing ", "keeps ", "has ", "with ")):
return f"{label} {clothing}"
return f"{label}'s clothing: {clothing}"
def _character_hardcore_clothing_entries(
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
pov_labels: list[str] | None = None,
rng: random.Random | None = None,
) -> list[str]:
pov_set = set(pov_labels or [])
labels = [
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
]
entries: list[str] = []
for label in labels:
if label in pov_set:
continue
clothing = _slot_hardcore_clothing(label_map.get(label), rng)
sentence = _hardcore_clothing_sentence(label, clothing)
if sentence:
entries.append(sentence)
return entries
def _context_from_character_slot(
rng: random.Random,
slot: dict[str, Any],
@@ -4052,7 +3979,7 @@ def _insta_of_partner_styling(
label = chr(ord("B") + index)
full_label = f"Woman {label}"
outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
sentence = _softcore_outfit_sentence(full_label, outfit)
sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit)
if sentence:
outfits.append(sentence)
for index in range(max(0, men_count)):
@@ -4061,7 +3988,7 @@ def _insta_of_partner_styling(
if full_label in pov_set:
continue
outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
sentence = _softcore_outfit_sentence(full_label, outfit)
sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit)
if sentence:
outfits.append(sentence)
return {
@@ -4143,7 +4070,6 @@ def build_insta_of_pair(
softcore_outfit=_insta_of_softcore_outfit,
softcore_pose=_insta_of_softcore_pose,
softcore_item_prompt_label=_insta_of_softcore_item_prompt_label,
body_exposure_scene_text=_body_exposure_scene_text,
pov_prompt_directive=_pov_prompt_directive,
pov_composition_prompt=_pov_composition_prompt,
)
@@ -4219,12 +4145,13 @@ def build_insta_of_pair(
soft_cast_presence = cast_context["soft_cast_presence"]
soft_cast_styling_sentence = cast_context["soft_cast_styling_sentence"]
hard_cast = cast_context["hard_cast"]
character_hardcore_clothing_entries = _character_hardcore_clothing_entries(
character_hardcore_clothing_entries = pair_clothing.character_hardcore_clothing_entries(
character_slot_map,
hard_women_count,
hard_men_count,
pov_character_labels,
hard_content_rng,
_slot_hardcore_clothing,
)
clothing_route = pair_clothing.resolve_hardcore_pair_clothing(
hard_row=hard_row,
@@ -4236,13 +4163,12 @@ def build_insta_of_pair(
rng=hard_content_rng,
continuity_map=INSTA_OF_HARDCORE_CLOTHING_CONTINUITY,
choose=g.choose,
sentence_builder=_hardcore_clothing_sentence,
)
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_scene = pair_clothing.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
hard_detail_density = options["hardcore_detail_density"]
+36
View File
@@ -41,6 +41,7 @@ import krea_cast # noqa: E402
import krea_formatter # noqa: E402
import location_config # noqa: E402
import loop_nodes # noqa: E402
import pair_clothing # noqa: E402
import prompt_builder as pb # noqa: E402
import pov_policy # noqa: E402
import row_normalization # noqa: E402
@@ -1645,6 +1646,41 @@ def smoke_pair_options_policy() -> None:
pb.pair_options.hardcore_detail_directive("bad") == "",
"invalid hardcore detail density directive should be empty",
)
_expect(
"scattered clothes" not in pair_clothing.body_exposure_scene_text(
"mirror corner, scattered clothes, outfit-check framing"
),
"Pair clothing body exposure scene cleanup should remove clothing clutter",
)
_expect(
"creator-shot" in pair_clothing.body_exposure_scene_text("outfit-check framing"),
"Pair clothing body exposure scene cleanup should replace outfit-check wording",
)
_expect(
pair_clothing.softcore_outfit_sentence("Man A", "wears hoodie and joggers")
== "Man A wears hoodie and joggers",
"Pair clothing softcore outfit sentence formatting changed",
)
_expect(
pair_clothing.hardcore_clothing_sentence("Woman A", "fully nude")
== "Woman A's body is fully exposed, bare skin unobstructed",
"Pair clothing hardcore fully nude sentence formatting changed",
)
_expect(
pair_clothing.character_hardcore_clothing_entries(
{
"Woman A": {"hardcore_clothing": "fully nude"},
"Man A": {"hardcore_clothing": "wears jeans"},
},
1,
1,
["Man A"],
random.Random(1),
lambda slot, _rng: str((slot or {}).get("hardcore_clothing") or ""),
)
== ["Woman A's body is fully exposed, bare skin unobstructed"],
"Pair clothing character entries should skip POV labels",
)
options = json.loads(
pb.build_insta_of_options_json(
softcore_expression_enabled="false",