Extract Krea clothing cleanup

This commit is contained in:
2026-06-26 15:31:09 +02:00
parent 92469daf03
commit 031223255d
5 changed files with 115 additions and 66 deletions
+4 -3
View File
@@ -137,7 +137,6 @@ Keep here:
- Krea prose style; - Krea prose style;
- hardcore action sentence rewriting; - hardcore action sentence rewriting;
- POV sentence rewriting; - POV sentence rewriting;
- clothing naturalization;
- camera-scene preservation; - camera-scene preservation;
- fallback text parsing. - fallback text parsing.
@@ -145,11 +144,13 @@ Already isolated:
- `krea_cast.py` owns cast descriptor parsing, cast prose, label joining, and - `krea_cast.py` owns cast descriptor parsing, cast prose, label joining, and
natural label replacement for formatter routes. natural label replacement for formatter routes.
- `krea_clothing.py` owns clothing-state cleanup and action-aware body-access
wording for formatter routes.
Improve later: Improve later:
- split semantic blocks into modules: - split semantic blocks into modules:
`krea_actions.py`, `krea_pov.py`, `krea_clothing.py`; `krea_actions.py`, `krea_pov.py`;
- add route-level smoke fixtures for representative metadata rows; - add route-level smoke fixtures for representative metadata rows;
- make `_hardcore_action_sentence` dispatch by action family instead of long - make `_hardcore_action_sentence` dispatch by action family instead of long
conditional chains. conditional chains.
@@ -330,7 +331,7 @@ Medium-term:
## Recommended Next Passes ## Recommended Next Passes
1. Split Krea action/POV/clothing helpers into separate modules, using 1. Split Krea action/POV helpers into separate modules, using
`krea_cast.py` as the pattern for stable import aliases and smoke coverage. `krea_cast.py` as the pattern for stable import aliases and smoke coverage.
2. Split `__init__.py` node classes by family after behavior is covered by smoke 2. Split `__init__.py` node classes by family after behavior is covered by smoke
checks. checks.
+6 -5
View File
@@ -545,10 +545,11 @@ Important POV rule:
Key Krea2 ownership: Key Krea2 ownership:
- Cast descriptor naturalization: `_cast_prose`, `_natural_label_text`. - Cast descriptor naturalization: `krea_cast.cast_prose`,
`krea_cast.natural_label_text`.
- Hardcore action sentence: `_hardcore_action_sentence`. - Hardcore action sentence: `_hardcore_action_sentence`.
- POV hardcore sentence: `_pov_hardcore_pose_sentence`, `_pov_action_phrase`. - POV hardcore sentence: `_pov_hardcore_pose_sentence`, `_pov_action_phrase`.
- Clothing state cleanup: `_natural_clothing_state`. - Clothing state cleanup: `krea_clothing.natural_clothing_state`.
- Camera scene preservation: `_camera_scene_phrase`. - Camera scene preservation: `_camera_scene_phrase`.
Krea2 field consumption: Krea2 field consumption:
@@ -558,7 +559,7 @@ Krea2 field consumption:
| Normal single row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `_normal_row_to_krea` | | Normal single row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `_normal_row_to_krea` |
| Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase` | | Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase` |
| Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `_insta_pair_to_krea` | | Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `_insta_pair_to_krea` |
| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase`, `_natural_clothing_state` | | Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase`, `krea_clothing.natural_clothing_state` |
| Plain text fallback | `source_text` only | `_fallback_text_to_krea` | | Plain text fallback | `source_text` only | `_fallback_text_to_krea` |
If metadata is connected and `method` says `text(fallback)`, the formatter did If metadata is connected and `method` says `text(fallback)`, the formatter did
@@ -700,7 +701,7 @@ pair metadata through the core Python APIs, then verifies:
| --- | --- | | --- | --- |
| Wrong main category/subcategory frequency | Category node config, `load_category_library`, category JSON weights. | | Wrong main category/subcategory frequency | Category node config, `load_category_library`, category JSON weights. |
| Wrong outfit/clothing item | Relevant category JSON, `INSTA_OF_SOFTCORE_OUTFITS`, `SxCP Character Clothing`. | | Wrong outfit/clothing item | Relevant category JSON, `INSTA_OF_SOFTCORE_OUTFITS`, `SxCP Character Clothing`. |
| Nude/clothing state confusing Krea2 | `build_insta_of_pair` clothing state helpers, then `_natural_clothing_state`. | | Nude/clothing state confusing Krea2 | `build_insta_of_pair` clothing state helpers, then `krea_clothing.natural_clothing_state`. |
| Wrong location | `categories/location_pools.json`, category `scene_pool`, `_scene_pool`. | | Wrong location | `categories/location_pools.json`, category `scene_pool`, `_scene_pool`. |
| Location good but camera/location layout wrong | `_camera_scene_directive_for_context`, coworking adapter functions. | | Location good but camera/location layout wrong | `_camera_scene_directive_for_context`, coworking adapter functions. |
| Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. | | Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. |
@@ -763,7 +764,7 @@ Use these traces to narrow a problem in one pass.
1. Check pair root `hardcore_clothing_state`. 1. Check pair root `hardcore_clothing_state`.
2. Check hard row `item` and `source_role_graph` for access flags. 2. Check hard row `item` and `source_role_graph` for access flags.
3. Character slot `hardcore_clothing` overrides pair fallback clothing. 3. Character slot `hardcore_clothing` overrides pair fallback clothing.
4. For Krea wording, inspect `_natural_clothing_state`. 4. For Krea wording, inspect `krea_clothing.natural_clothing_state`.
5. For generation wording, inspect `_insta_of_hardcore_clothing_state`, 5. For generation wording, inspect `_insta_of_hardcore_clothing_state`,
`_hardcore_row_access_flags`, and `character_hardcore_clothing_values`. `_hardcore_row_access_flags`, and `character_hardcore_clothing_values`.
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
import re
from typing import Any
try:
from .krea_cast import natural_label_text
except ImportError: # Allows local smoke tests with `python -c`.
from krea_cast import natural_label_text
def _clean(value: Any) -> str:
text = "" if value is None else str(value)
text = text.replace("\n", " ")
text = re.sub(r"\s+", " ", text).strip()
text = re.sub(r"\s+([,.;:])", r"\1", text)
return text
def clothing_access_phrase(action_text: Any) -> str:
text = _clean(action_text).lower()
if any(term in text for term in ("cumshot", "ejaculat", "semen", "cum on", "cum across", "post-orgasm", "aftermath")):
return "leaving the body exposed for visible semen and aftermath"
if any(term in text for term in ("boobjob", "titjob", "breast sex", "handjob", "hand job", "footjob", "testicle", "balls", "penis licking", "non-penetrative")):
return "leaving the contact point unobstructed"
if any(term in text for term in ("oral", "blowjob", "fellatio", "mouth", "tongue")):
return "leaving the oral contact unobstructed"
if any(term in text for term in ("penetrat", "thrust", "penis entering", "vaginal", "anal")):
return "leaving the penetration point unobstructed"
return "leaving skin and body contact readable"
def natural_clothing_state(text: Any, action_text: Any = "") -> str:
text = _clean(text)
if not text:
return ""
text = re.sub(r"^Clothing state:\s*", "", text, flags=re.IGNORECASE)
if re.search(r";\s*(?=(?:Woman|Man) [A-Z]\b)", text):
parts = [
natural_clothing_state(part, action_text).rstrip(".")
for part in re.split(r";\s*(?=(?:Woman|Man) [A-Z]\b)", text)
if _clean(part)
]
return ". ".join(part for part in parts if part)
body_exposure = re.match(r"^Body exposure:\s*(.*?)\.?$", text, flags=re.IGNORECASE)
if body_exposure:
return _clean(body_exposure.group(1)).rstrip(".")
if re.search(r"\bfully nude\b|\bbody is fully exposed\b|\bno clothing covering\b", text, flags=re.IGNORECASE):
owner = "the woman"
owner_match = re.match(r"^\s*((?:Woman|Man) [A-Z])\b", text)
if owner_match:
owner = natural_label_text(owner_match.group(1), ["Woman A", "Man A"]) or owner
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
match = re.match(
r"^(.*?)\b(?:softcore|teaser) outfit is (.*?)(?: for the (?:hardcore|sex) scene)?;\s*(?:softcore visual reference|teaser outfit detail):\s*(.*?)\.?$",
text,
flags=re.IGNORECASE,
)
if match:
owner = natural_label_text(match.group(1).strip(" 's"), ["Woman A", "Man A"]).strip() or "the woman"
state = _clean(match.group(2)).lower()
outfit = _clean(match.group(3)).rstrip(".")
if "fully nude" in state or "fully exposed" in state or "no clothing covering" in state:
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
if "nude-adjacent" in state:
return f"{owner.capitalize()}'s body is partly exposed"
if "partially removed" in state or "pushed aside" in state:
return f"{owner.capitalize()}'s {outfit} is pushed aside or partly removed where needed, {clothing_access_phrase(action_text)}"
if "keeps" in state:
return f"{owner.capitalize()} keeps the {outfit} on while {clothing_access_phrase(action_text)}"
text = re.sub(r";\s*(?:softcore visual reference|teaser outfit detail):\s*", ". Visual clothing state: ", text, flags=re.IGNORECASE)
text = text.replace("softcore outfit", "outfit")
text = text.replace("teaser outfit", "outfit")
text = text.replace("hardcore scene", "sex scene")
return text
+2 -58
View File
@@ -16,6 +16,7 @@ try:
natural_label_text as _natural_label_text, natural_label_text as _natural_label_text,
prompt_cast_descriptors as _prompt_cast_descriptors, prompt_cast_descriptors as _prompt_cast_descriptors,
) )
from .krea_clothing import natural_clothing_state as _natural_clothing_state
from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
from hardcore_text_cleanup import ( from hardcore_text_cleanup import (
@@ -29,6 +30,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
natural_label_text as _natural_label_text, natural_label_text as _natural_label_text,
prompt_cast_descriptors as _prompt_cast_descriptors, prompt_cast_descriptors as _prompt_cast_descriptors,
) )
from krea_clothing import natural_clothing_state as _natural_clothing_state
from prompt_hygiene import sanitize_negative_text, sanitize_prose_text from prompt_hygiene import sanitize_negative_text, sanitize_prose_text
@@ -593,64 +595,6 @@ def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
return text return text
def _clothing_access_phrase(action_text: Any) -> str:
text = _clean(action_text).lower()
if any(term in text for term in ("cumshot", "ejaculat", "semen", "cum on", "cum across", "post-orgasm", "aftermath")):
return "leaving the body exposed for visible semen and aftermath"
if any(term in text for term in ("boobjob", "titjob", "breast sex", "handjob", "hand job", "footjob", "testicle", "balls", "penis licking", "non-penetrative")):
return "leaving the contact point unobstructed"
if any(term in text for term in ("oral", "blowjob", "fellatio", "mouth", "tongue")):
return "leaving the oral contact unobstructed"
if any(term in text for term in ("penetrat", "thrust", "penis entering", "vaginal", "anal")):
return "leaving the penetration point unobstructed"
return "leaving skin and body contact readable"
def _natural_clothing_state(text: Any, action_text: Any = "") -> str:
text = _clean(text)
if not text:
return ""
text = re.sub(r"^Clothing state:\s*", "", text, flags=re.IGNORECASE)
if re.search(r";\s*(?=(?:Woman|Man) [A-Z]\b)", text):
parts = [
_natural_clothing_state(part, action_text).rstrip(".")
for part in re.split(r";\s*(?=(?:Woman|Man) [A-Z]\b)", text)
if _clean(part)
]
return ". ".join(part for part in parts if part)
body_exposure = re.match(r"^Body exposure:\s*(.*?)\.?$", text, flags=re.IGNORECASE)
if body_exposure:
return _clean(body_exposure.group(1)).rstrip(".")
if re.search(r"\bfully nude\b|\bbody is fully exposed\b|\bno clothing covering\b", text, flags=re.IGNORECASE):
owner = "the woman"
owner_match = re.match(r"^\s*((?:Woman|Man) [A-Z])\b", text)
if owner_match:
owner = _natural_label_text(owner_match.group(1), ["Woman A", "Man A"]) or owner
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
match = re.match(
r"^(.*?)\b(?:softcore|teaser) outfit is (.*?)(?: for the (?:hardcore|sex) scene)?;\s*(?:softcore visual reference|teaser outfit detail):\s*(.*?)\.?$",
text,
flags=re.IGNORECASE,
)
if match:
owner = _natural_label_text(match.group(1).strip(" 's"), ["Woman A", "Man A"]).strip() or "the woman"
state = _clean(match.group(2)).lower()
outfit = _clean(match.group(3)).rstrip(".")
if "fully nude" in state or "fully exposed" in state or "no clothing covering" in state:
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
if "nude-adjacent" in state:
return f"{owner.capitalize()}'s body is partly exposed"
if "partially removed" in state or "pushed aside" in state:
return f"{owner.capitalize()}'s {outfit} is pushed aside or partly removed where needed, {_clothing_access_phrase(action_text)}"
if "keeps" in state:
return f"{owner.capitalize()} keeps the {outfit} on while {_clothing_access_phrase(action_text)}"
text = re.sub(r";\s*(?:softcore visual reference|teaser outfit detail):\s*", ". Visual clothing state: ", text, flags=re.IGNORECASE)
text = text.replace("softcore outfit", "outfit")
text = text.replace("teaser outfit", "outfit")
text = text.replace("hardcore scene", "sex scene")
return text
def _axis_values_text(axis_values: Any) -> str: def _axis_values_text(axis_values: Any) -> str:
if not isinstance(axis_values, dict): if not isinstance(axis_values, dict):
return "" return ""
+28
View File
@@ -468,6 +468,33 @@ def smoke_insta_pair() -> None:
_expect("teaser outfit detail" not in clothing_state, "explicit nude pair should not repeat softcore outfit detail") _expect("teaser outfit detail" not in clothing_state, "explicit nude pair should not repeat softcore outfit detail")
def smoke_krea_pair_clothing_state() -> None:
pair = pb.build_insta_of_pair(
row_number=1,
start_index=1,
seed=3511,
ethnicity="any",
figure="random",
no_plus_women=False,
no_black=False,
trigger=Trigger,
prepend_trigger_to_prompt=True,
options_json=_insta_options(hardcore_clothing_continuity="partially_removed"),
character_cast=_character_cast(),
hardcore_position_config=_action_filter("penetration_only"),
)
_expect_pair(pair, "krea_pair_clothing_state")
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()
_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")
_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")
def smoke_insta_pair_pov() -> None: def smoke_insta_pair_pov() -> None:
pair = pb.build_insta_of_pair( pair = pb.build_insta_of_pair(
row_number=1, row_number=1,
@@ -654,6 +681,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("hardcore_category_routes", smoke_hardcore_category_routes), ("hardcore_category_routes", smoke_hardcore_category_routes),
("krea_close_foreplay_route", smoke_krea_close_foreplay_route), ("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
("insta_pair_same_cast", smoke_insta_pair), ("insta_pair_same_cast", smoke_insta_pair),
("krea_pair_clothing_state", smoke_krea_pair_clothing_state),
("insta_pair_pov_man", smoke_insta_pair_pov), ("insta_pair_pov_man", smoke_insta_pair_pov),
("insta_pair_camera_split", smoke_insta_pair_camera_split), ("insta_pair_camera_split", smoke_insta_pair_camera_split),
("pov_camera_scene", smoke_pov_camera_scene), ("pov_camera_scene", smoke_pov_camera_scene),