Extract Krea clothing cleanup
This commit is contained in:
@@ -137,7 +137,6 @@ Keep here:
|
||||
- Krea prose style;
|
||||
- hardcore action sentence rewriting;
|
||||
- POV sentence rewriting;
|
||||
- clothing naturalization;
|
||||
- camera-scene preservation;
|
||||
- fallback text parsing.
|
||||
|
||||
@@ -145,11 +144,13 @@ Already isolated:
|
||||
|
||||
- `krea_cast.py` owns cast descriptor parsing, cast prose, label joining, and
|
||||
natural label replacement for formatter routes.
|
||||
- `krea_clothing.py` owns clothing-state cleanup and action-aware body-access
|
||||
wording for formatter routes.
|
||||
|
||||
Improve later:
|
||||
|
||||
- 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;
|
||||
- make `_hardcore_action_sentence` dispatch by action family instead of long
|
||||
conditional chains.
|
||||
@@ -330,7 +331,7 @@ Medium-term:
|
||||
|
||||
## 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.
|
||||
2. Split `__init__.py` node classes by family after behavior is covered by smoke
|
||||
checks.
|
||||
|
||||
@@ -545,10 +545,11 @@ Important POV rule:
|
||||
|
||||
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`.
|
||||
- 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`.
|
||||
|
||||
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 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 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` |
|
||||
|
||||
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 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`. |
|
||||
| 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. |
|
||||
@@ -763,7 +764,7 @@ Use these traces to narrow a problem in one pass.
|
||||
1. Check pair root `hardcore_clothing_state`.
|
||||
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 `_natural_clothing_state`.
|
||||
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`.
|
||||
|
||||
|
||||
@@ -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
@@ -16,6 +16,7 @@ try:
|
||||
natural_label_text as _natural_label_text,
|
||||
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
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
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,
|
||||
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
|
||||
|
||||
|
||||
@@ -593,64 +595,6 @@ def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
|
||||
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:
|
||||
if not isinstance(axis_values, dict):
|
||||
return ""
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
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:
|
||||
pair = pb.build_insta_of_pair(
|
||||
row_number=1,
|
||||
@@ -654,6 +681,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||
("hardcore_category_routes", smoke_hardcore_category_routes),
|
||||
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
|
||||
("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_camera_split", smoke_insta_pair_camera_split),
|
||||
("pov_camera_scene", smoke_pov_camera_scene),
|
||||
|
||||
Reference in New Issue
Block a user