From e042960466686889cd95afbdd42fdf6a1fbe7bf5 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 24 Jun 2026 18:19:20 +0200 Subject: [PATCH] Add character-level InstaOF overrides --- README.md | 23 ++++++-- __init__.py | 36 +++++++++++++ krea_formatter.py | 17 +++++- prompt_builder.py | 133 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 194 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b325471..591e0bf 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,17 @@ character-driven. For configured casts, matching enabled slots emit per-character expression text such as `Woman A has ...; Man A has ...`; Krea formatting naturalizes those labels in pair prompts. +For Insta/OF pairs, slots also expose character-level overrides: + +- `softcore_expression_intensity` and `hardcore_expression_intensity`: override + the option-node expression fallback for that character and that output half. +- `softcore_outfit`: overrides the character's softcore clothing. For `Woman A` + this replaces the generated teaser outfit; for partners it replaces random + partner styling. +- `hardcore_clothing`: adds direct character clothing/nudity wording in the + hardcore output. A `Woman A` hardcore clothing override replaces the global + `hardcore_clothing_continuity` text to avoid contradictory clothing prompts. + Slots are chainable through the `character_cast` input/output. In automatic label mode, the slot closest to the final generator becomes `A` for its gender, the next upstream slot becomes `B`, then `C`, and so on. Example: @@ -311,11 +322,12 @@ Options: microwear, or shirtless partner styling. `explicit_nude` is available when you want visible nude creator-shot framing without a sex act. - `hardcore_level`: `explicit` or `hardcore`. -- `softcore_expression_intensity`: `0.0` is mild/controlled, `0.5` is sensual, +- `softcore_expression_intensity`: fallback when no connected character slot + sets `softcore_expression_intensity`. `0.0` is mild/controlled, `0.5` is sensual, `1.0` strongly favors more heated softcore faces. -- `hardcore_expression_intensity`: `0.0` is controlled, `0.5` is balanced - hardcore, `1.0` strongly favors ahegao-style, drooling, fucked-out, climax, - and messy orgasm expressions. +- `hardcore_expression_intensity`: fallback when no connected character slot + sets `hardcore_expression_intensity`. `0.0` is controlled, `0.5` is balanced + hardcore, `1.0` strongly favors stronger hardcore expressions. - `softcore_expression_enabled` and `hardcore_expression_enabled`: disable the expression sentence for that half of the Insta/OF pair. The intensity values are fallbacks; `SxCP Woman Slot` / `SxCP Man Slot` `expression_intensity` @@ -328,7 +340,8 @@ Options: - `hardcore_clothing_continuity`: `none`, `same_outfit`, `partially_removed`, `implied_nude`, or `explicit_nude`. This controls whether the hardcore prompt references the softcore outfit, uses it displaced/removed, or makes Woman A - explicitly nude. + explicitly nude. It is a fallback for Woman A; `hardcore_clothing` on + `SxCP Woman Slot` or `SxCP Man Slot` takes priority for that character. - `softcore_camera_mode`: base camera mode for the softcore output. - `hardcore_camera_mode`: `from_camera_config`, `same_as_softcore`, or a separate base camera mode for the hardcore output. `from_camera_config` is diff --git a/__init__.py b/__init__.py index e48192e..63a76e4 100644 --- a/__init__.py +++ b/__init__.py @@ -614,6 +614,10 @@ class SxCPCharacterSlot: "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "presence_mode": (character_presence_choices(), {"default": "visible"}), + "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "softcore_outfit": ("STRING", {"default": ""}), + "hardcore_clothing": ("STRING", {"default": ""}), }, "optional": { "character_cast": ("STRING", {"default": "", "multiline": True}), @@ -644,6 +648,10 @@ class SxCPCharacterSlot: expression_enabled=True, expression_intensity=-1.0, presence_mode="visible", + softcore_expression_intensity=-1.0, + hardcore_expression_intensity=-1.0, + softcore_outfit="", + hardcore_clothing="", character_cast="", ): result = build_character_slot_json( @@ -663,6 +671,10 @@ class SxCPCharacterSlot: expression_enabled=expression_enabled, expression_intensity=expression_intensity, presence_mode=presence_mode, + softcore_expression_intensity=softcore_expression_intensity, + hardcore_expression_intensity=hardcore_expression_intensity, + softcore_outfit=softcore_outfit, + hardcore_clothing=hardcore_clothing, enabled=enabled, character_cast=character_cast or "", ) @@ -689,6 +701,10 @@ class SxCPWomanSlot: "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "softcore_outfit": ("STRING", {"default": ""}), + "hardcore_clothing": ("STRING", {"default": ""}), }, "optional": { "character_cast": ("STRING", {"default": "", "multiline": True}), @@ -717,6 +733,10 @@ class SxCPWomanSlot: descriptor_detail="auto", expression_enabled=True, expression_intensity=-1.0, + softcore_expression_intensity=-1.0, + hardcore_expression_intensity=-1.0, + softcore_outfit="", + hardcore_clothing="", character_cast="", ): result = build_character_slot_json( @@ -735,6 +755,10 @@ class SxCPWomanSlot: descriptor_detail=descriptor_detail, expression_enabled=expression_enabled, expression_intensity=expression_intensity, + softcore_expression_intensity=softcore_expression_intensity, + hardcore_expression_intensity=hardcore_expression_intensity, + softcore_outfit=softcore_outfit, + hardcore_clothing=hardcore_clothing, enabled=enabled, character_cast=character_cast or "", ) @@ -761,6 +785,10 @@ class SxCPManSlot: "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "presence_mode": (character_presence_choices(), {"default": "visible"}), + "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "softcore_outfit": ("STRING", {"default": ""}), + "hardcore_clothing": ("STRING", {"default": ""}), }, "optional": { "character_cast": ("STRING", {"default": "", "multiline": True}), @@ -789,6 +817,10 @@ class SxCPManSlot: expression_enabled=True, expression_intensity=-1.0, presence_mode="visible", + softcore_expression_intensity=-1.0, + hardcore_expression_intensity=-1.0, + softcore_outfit="", + hardcore_clothing="", character_cast="", ): result = build_character_slot_json( @@ -808,6 +840,10 @@ class SxCPManSlot: expression_enabled=expression_enabled, expression_intensity=expression_intensity, presence_mode=presence_mode, + softcore_expression_intensity=softcore_expression_intensity, + hardcore_expression_intensity=hardcore_expression_intensity, + softcore_outfit=softcore_outfit, + hardcore_clothing=hardcore_clothing, enabled=enabled, character_cast=character_cast or "", ) diff --git a/krea_formatter.py b/krea_formatter.py index 0efbe66..b50fb02 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -307,6 +307,18 @@ def _pov_action_phrase(action: Any, pov_labels: list[str]) -> str: rendered = re.sub(r"\bhe\b", "the POV viewer", rendered, flags=re.IGNORECASE) rendered = re.sub(r"\bhim\b", "the POV viewer", rendered, flags=re.IGNORECASE) rendered = re.sub(r"\bhis\b", "the POV viewer's", rendered, flags=re.IGNORECASE) + rendered = re.sub( + r"\bthe POV viewer lies on the POV viewer's back under her\b", + "the POV viewer reclines underneath her", + rendered, + flags=re.IGNORECASE, + ) + rendered = re.sub( + r"\bthe POV viewer lies on the POV viewer's back\b", + "the POV viewer reclines", + rendered, + flags=re.IGNORECASE, + ) rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE) return rendered @@ -1595,7 +1607,10 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) hard_parts = [ hard_action, _pov_camera_phrase(pov_labels), - _natural_clothing_state(row.get("hardcore_clothing_state")), + _natural_label_text( + _filter_pov_labeled_clauses(_natural_clothing_state(row.get("hardcore_clothing_state")), pov_labels), + hard_labels, + ), hard_cast_prose, f"set in {hard_scene}" if hard_scene else "", _expression_phrase(hard_expression), diff --git a/prompt_builder.py b/prompt_builder.py index deefff3..474c807 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -1906,6 +1906,17 @@ def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None: return intensity if intensity >= 0 else None +def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None: + if not slot or not _slot_expression_enabled(slot): + return None + phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else "" + if phase_key: + intensity = _normalize_slot_expression_intensity(slot.get(phase_key)) + if intensity >= 0: + return intensity + return _slot_expression_intensity(slot) + + def _mean(values: list[float]) -> float: return sum(values) / len(values) @@ -1915,6 +1926,7 @@ def _cast_expression_intensity_override( label_map: dict[str, dict[str, Any]], women_count: int, men_count: int, + expression_phase: str = "", ) -> tuple[float | None, str]: groups: list[tuple[str, list[str]]] = [ ("women", [f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))]), @@ -1931,7 +1943,7 @@ def _cast_expression_intensity_override( continue if slot: matching_slots.append(slot) - value = _slot_expression_intensity(slot) + value = _slot_expression_intensity_for_phase(slot, expression_phase) if value is not None: values.append(value) value_labels.append(label) @@ -1954,6 +1966,7 @@ def _character_expression_entries( label_map: dict[str, dict[str, Any]], women_count: int, men_count: int, + expression_phase: str = "", ) -> list[str]: labels = [ *[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))], @@ -1969,7 +1982,7 @@ def _character_expression_entries( continue if not _slot_expression_enabled(slot): continue - intensity = _slot_expression_intensity(slot) + intensity = _slot_expression_intensity_for_phase(slot, expression_phase) if intensity is None: intensity = fallback_intensity entries = _compatible_entries( @@ -2075,8 +2088,12 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]: "eyes": _slot_value(slot.get("eyes")), "descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")), "presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type), + "softcore_outfit": _slot_value(slot.get("softcore_outfit")), + "hardcore_clothing": _slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit")), "expression_enabled": not _is_false(slot.get("expression_enabled", True)), "expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")), + "softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")), + "hardcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")), } normalized["summary"] = _character_slot_summary(normalized) return normalized @@ -2129,6 +2146,16 @@ def _character_slot_summary(slot: dict[str, Any]) -> str: expression_intensity = _slot_expression_intensity(slot) if expression_intensity is not None: parts.append(f"expression={expression_intensity:.2f}") + softcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "softcore") + hardcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "hardcore") + if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity: + parts.append(f"soft_expr={softcore_expression_intensity:.2f}") + if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity: + parts.append(f"hard_expr={hardcore_expression_intensity:.2f}") + if slot.get("softcore_outfit"): + parts.append(f"soft_outfit={slot['softcore_outfit']}") + if slot.get("hardcore_clothing"): + parts.append(f"hard_clothing={slot['hardcore_clothing']}") for key in ("body_phrase", "skin", "hair", "eyes"): value = slot.get(key) if value: @@ -2155,6 +2182,10 @@ def build_character_slot_json( enabled: bool = True, character_cast: str | dict[str, Any] | list[Any] | None = "", presence_mode: str = "visible", + softcore_expression_intensity: float = -1.0, + hardcore_expression_intensity: float = -1.0, + softcore_outfit: str = "", + hardcore_clothing: str = "", ) -> dict[str, str]: existing_slots = _parse_character_cast(character_cast) slot = _normalize_character_slot( @@ -2173,8 +2204,12 @@ def build_character_slot_json( "eyes": eyes, "descriptor_detail": descriptor_detail, "presence_mode": presence_mode, + "softcore_outfit": softcore_outfit, + "hardcore_clothing": hardcore_clothing, "expression_enabled": expression_enabled, "expression_intensity": expression_intensity, + "softcore_expression_intensity": softcore_expression_intensity, + "hardcore_expression_intensity": hardcore_expression_intensity, } ) slots = existing_slots + ([slot] if enabled else []) @@ -2271,6 +2306,58 @@ def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str: 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 "" + + +def _slot_hardcore_clothing(slot: dict[str, Any] | None) -> str: + return _slot_value(slot.get("hardcore_clothing")) if slot else "" + + +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(("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}" + + +def _character_hardcore_clothing_entries( + label_map: dict[str, dict[str, Any]], + women_count: int, + men_count: int, + pov_labels: list[str] | 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)) + 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], @@ -3303,6 +3390,7 @@ def _build_custom_row( expression_intensity: float, character_profile: str | dict[str, Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None, + expression_phase: str = "", ) -> dict[str, Any]: categories = load_category_library() category_rng = _axis_rng(seed_config, "category", seed, row_number) @@ -3382,7 +3470,7 @@ def _build_custom_row( expression_disabled = True expression_intensity_source = f"character_slot:{slot_label}:disabled" else: - slot_expression_intensity = _slot_expression_intensity(applied_slot) + slot_expression_intensity = _slot_expression_intensity_for_phase(applied_slot, expression_phase) if slot_expression_intensity is not None: expression_intensity = slot_expression_intensity expression_intensity_source = f"character_slot:{slot_label}" @@ -3392,6 +3480,7 @@ def _build_custom_row( character_slot_map, women_count, men_count, + expression_phase, ) if expression_intensity is None: expression_disabled = True @@ -3439,6 +3528,7 @@ def _build_custom_row( character_slot_map, women_count, men_count, + expression_phase, ) character_expression_text = "; ".join(character_expressions) if character_expression_text: @@ -3616,6 +3706,7 @@ def build_prompt( character_profile: str | dict[str, Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None, expression_enabled: bool = True, + expression_phase: str = "", ) -> dict[str, Any]: apply_pool_extensions() row_number = max(1, int(row_number)) @@ -3683,6 +3774,7 @@ def build_prompt( expression_intensity, character_profile, character_cast, + expression_phase, ) if not expression_enabled: @@ -4155,6 +4247,7 @@ def _insta_of_partner_styling( women_count: int, men_count: int, pov_labels: list[str] | None = None, + label_map: dict[str, dict[str, Any]] | None = None, ) -> dict[str, Any]: content_rng = _axis_rng(seed_config, "content", seed, row_number + 421) pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421) @@ -4162,12 +4255,20 @@ def _insta_of_partner_styling( outfits: list[str] = [] for index in range(max(0, women_count - 1)): label = chr(ord("B") + index) - outfits.append(f"Woman {label} wears {g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)}") + full_label = f"Woman {label}" + outfit = _slot_softcore_outfit((label_map or {}).get(full_label)) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS) + sentence = _softcore_outfit_sentence(full_label, outfit) + if sentence: + outfits.append(sentence) for index in range(max(0, men_count)): label = chr(ord("A") + index) - if f"Man {label}" in pov_set: + full_label = f"Man {label}" + if full_label in pov_set: continue - outfits.append(f"Man {label} wears {g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)}") + outfit = _slot_softcore_outfit((label_map or {}).get(full_label)) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS) + sentence = _softcore_outfit_sentence(full_label, outfit) + if sentence: + outfits.append(sentence) return { "outfits": outfits, "pose": g.choose(pose_rng, SOFTCORE_CAST_POSES), @@ -4225,6 +4326,7 @@ def build_insta_of_pair( character_slot_map, soft_expression_women_count, soft_expression_men_count, + "softcore", ) if soft_expression_intensity is None: soft_expression_enabled = False @@ -4277,11 +4379,12 @@ def build_insta_of_pair( soft_row["character_slot_status"] = "applied:Woman A" if not soft_expression_enabled: soft_row = _disable_row_expression(soft_row, soft_expression_intensity_source) - soft_row["item"] = _insta_of_softcore_outfit(soft_content_rng, softcore_level_key) + 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["custom_item"] = "insta_of_softcore_outfit" - soft_row["softcore_outfit_policy"] = "insta_of_safe_softcore" + soft_row["softcore_outfit_policy"] = "character_slot:Woman A" if primary_softcore_outfit else "insta_of_safe_softcore" soft_row["pov_character_labels"] = ( pov_character_labels if options["softcore_cast"] == "same_as_hardcore" @@ -4319,6 +4422,7 @@ def build_insta_of_pair( expression_enabled=options["hardcore_expression_enabled"], expression_intensity=options["hardcore_expression_intensity"], character_cast=character_cast or "", + expression_phase="hardcore", ) hard_row["hardcore_detail_density"] = options["hardcore_detail_density"] hard_row["pov_character_labels"] = pov_character_labels @@ -4351,6 +4455,7 @@ def build_insta_of_pair( hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1, hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0, pov_character_labels if options["softcore_cast"] == "same_as_hardcore" else [], + character_slot_map, ) if options["softcore_cast"] != "same_as_hardcore": soft_partner_styling = {"outfits": [], "pose": ""} @@ -4394,10 +4499,19 @@ def build_insta_of_pair( else "" ) hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count) - hard_clothing_state = _insta_of_hardcore_clothing_state( + character_hardcore_clothing_entries = _character_hardcore_clothing_entries( + character_slot_map, + hard_women_count, + hard_men_count, + pov_character_labels, + ) + 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"], ) + hard_clothing_parts = [part for part in (fallback_hard_clothing_state, *character_hardcore_clothing_entries) if part] + hard_clothing_state = " ".join(hard_clothing_parts) hard_detail_density = options["hardcore_detail_density"] hard_detail_directive = { "compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ", @@ -4482,6 +4596,7 @@ def build_insta_of_pair( "pov_character_labels": pov_character_labels, "pov_prompt_directive": pov_directive, "softcore_partner_styling": soft_partner_styling, + "character_hardcore_clothing": character_hardcore_clothing_entries, "hardcore_clothing_state": hard_clothing_state, "hardcore_detail_density": hard_detail_density, "softcore_prompt": soft_prompt,