Add character-level InstaOF overrides

This commit is contained in:
2026-06-24 18:19:20 +02:00
parent 89e499537e
commit e042960466
4 changed files with 194 additions and 15 deletions
+18 -5
View File
@@ -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
+36
View File
@@ -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 "",
)
+16 -1
View File
@@ -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),
+124 -9
View File
@@ -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,