diff --git a/krea_formatter.py b/krea_formatter.py index b50fb02..5728581 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -375,6 +375,15 @@ def _natural_clothing_state(text: Any) -> str: if not text: return "" text = re.sub(r"^Clothing state:\s*", "", text, flags=re.IGNORECASE) + 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, @@ -384,10 +393,10 @@ def _natural_clothing_state(text: Any) -> str: 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: - return f"{owner.capitalize()} is fully nude, with the removed {outfit} visible nearby" + 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()} is partly nude, with the {outfit} slipping off and no abstract clothing-reference wording" + 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 and partly removed, exposing the sexual contact clearly" if "keeps" in state: @@ -1589,6 +1598,11 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) hard_expression_source, hard_labels, ) + soft_item = _clean(soft.get("item")) + soft_item_label = _clean(soft.get("softcore_item_prompt_label")) + soft_item_phrase = "" + if soft_item: + soft_item_phrase = f"body exposure: {soft_item}" if soft_item_label == "Body exposure" else f"wearing {soft_item}" soft_parts = [ soft_cast_prose, @@ -1596,7 +1610,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) partner_outfit_text, partner_pose, _pov_camera_phrase(pov_labels, softcore=True) if same_soft_cast else "", - f"wearing {soft.get('item')}" if soft.get("item") else "", + soft_item_phrase, f"{soft.get('pose')}" if soft.get("pose") else "", _expression_phrase(soft_expression), f"in {soft.get('scene_text')}" if soft.get("scene_text") else "", diff --git a/prompt_builder.py b/prompt_builder.py index 474c807..0dada48 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -2306,6 +2306,32 @@ def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str: return _clean_prompt_punctuation(text) +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) -> str: return _slot_value(slot.get("softcore_outfit")) if slot else "" @@ -2329,10 +2355,12 @@ def _hardcore_clothing_sentence(label: str, clothing: str) -> str: 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}" - if lower.startswith(("fully nude", "nude", "partly nude")): - return f"{label} is {clothing}" return f"{label}'s clothing: {clothing}" @@ -3863,8 +3891,8 @@ INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = { "none": "", "same_outfit": "Woman A keeps her teaser outfit on, with sexual contact still clearly visible", "partially_removed": "Woman A's teaser outfit is pushed aside and partly removed, exposing the sexual contact clearly", - "implied_nude": "Woman A is partly nude, with the teaser outfit slipping off or covering only part of the body", - "explicit_nude": "Woman A is fully nude, with the removed teaser outfit visible nearby", + "implied_nude": "Woman A's body is partly exposed, with fabric slipping off or covering only part of the body", + "explicit_nude": "Woman A's body is fully exposed, bare skin unobstructed", } INSTA_OF_NEGATIVE = ( @@ -3924,12 +3952,12 @@ INSTA_OF_SOFTCORE_OUTFITS = { "strappy lingerie set with covered cups and high-waisted bottoms, styled as a stronger solo teaser", ], "explicit_nude": [ - "fully nude creator styling with jewelry, heels, and direct adult selfie confidence", - "fully nude mirror-selfie styling with jewelry only and bold creator-shot framing", - "nude-on-sheets creator pose with lingerie discarded nearby and direct eye contact", - "fully nude vanity-mirror pose with heels, necklace, and premium adult teaser styling", - "nude shower-afterglow creator pose with wet hair, skin highlights, and phone-shot framing", - "fully nude bedroom creator pose with one hand holding the phone and lingerie visible nearby", + "body fully exposed with jewelry accents and direct adult selfie confidence", + "mirror-selfie body exposure with jewelry accents and bold creator-shot framing", + "body fully exposed on soft sheets with direct eye contact", + "vanity-mirror body exposure with necklace detail and premium creator-shot styling", + "shower-afterglow body exposure with wet hair, skin highlights, and phone-shot framing", + "bedroom body exposure with one hand holding the phone and direct camera awareness", ], } @@ -3967,12 +3995,12 @@ INSTA_OF_SOFTCORE_POSES = { "arching subtly in a solo adult tease while the styling keeps explicit anatomy obscured", ], "explicit_nude": [ - "taking a bold nude mirror selfie with direct eye contact and the body clearly framed", - "posing fully nude on the bed with jewelry and heels as the only styling", - "standing at the vanity fully nude in a premium creator-shot pose", - "reclining fully nude on soft sheets with the phone held close", - "turning slightly in a nude mirror pose with the body framed head-to-thigh", - "kneeling fully nude in a controlled adult teaser pose with direct phone-camera awareness", + "taking a bold mirror selfie with direct eye contact and the body clearly framed", + "posing on the bed with body fully exposed and jewelry accents as styling", + "standing at the vanity with body fully exposed in a premium creator-shot pose", + "reclining on soft sheets with body fully exposed and the phone held close", + "turning slightly in a mirror pose with the body framed head-to-thigh", + "kneeling in a controlled adult teaser pose with body fully exposed and direct phone-camera awareness", ], } @@ -4226,6 +4254,10 @@ def _insta_of_softcore_outfit(rng: random.Random, level: str) -> str: return g.choose(rng, pool) +def _insta_of_softcore_item_prompt_label(level: str) -> str: + return "Body exposure" if level == "explicit_nude" else "Outfit" + + def _insta_of_softcore_pose(rng: random.Random, level: str) -> str: pool = INSTA_OF_SOFTCORE_POSES.get(level, INSTA_OF_SOFTCORE_POSES["lingerie_tease"]) return g.choose(rng, pool) @@ -4237,6 +4269,10 @@ def _insta_of_hardcore_clothing_state(mode: str, softcore_outfit: str) -> str: 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}." return f"Clothing state: {base}; teaser outfit detail: {outfit}." @@ -4382,9 +4418,13 @@ def build_insta_of_pair( primary_softcore_outfit = _slot_softcore_outfit(primary_slot) soft_row["item"] = primary_softcore_outfit or _insta_of_softcore_outfit(soft_content_rng, softcore_level_key) soft_row["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key) - soft_row["item_label"] = "Insta/OF softcore outfit" + soft_row["item_label"] = "Insta/OF softcore body exposure" if softcore_level_key == "explicit_nude" else "Insta/OF softcore outfit" + soft_row["softcore_item_prompt_label"] = _insta_of_softcore_item_prompt_label(softcore_level_key) soft_row["custom_item"] = "insta_of_softcore_outfit" 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["pov_character_labels"] = ( pov_character_labels if options["softcore_cast"] == "same_as_hardcore" @@ -4512,6 +4552,10 @@ def build_insta_of_pair( ) hard_clothing_parts = [part for part in (fallback_hard_clothing_state, *character_hardcore_clothing_entries) if part] hard_clothing_state = " ".join(hard_clothing_parts) + if "body is fully exposed" in hard_clothing_state.lower() or "bare skin unobstructed" in hard_clothing_state.lower(): + 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"] = _body_exposure_scene_text(hard_row.get("scene_text", "")) hard_detail_density = options["hardcore_detail_density"] hard_detail_directive = { "compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ", @@ -4531,7 +4575,7 @@ def build_insta_of_pair( f"Softcore setup: {soft_level}. Cast: {soft_cast}. " f"{soft_cast_presence}" f"{soft_cast_styling_sentence}" - f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " + f"{soft_row['softcore_item_prompt_label']}: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}" f"Composition: {soft_row['composition']}. " f"{soft_camera_sentence}"