Add expression enable controls

This commit is contained in:
2026-06-24 17:27:56 +02:00
parent c105926a6b
commit e2bdff6075
6 changed files with 405 additions and 28 deletions
+14
View File
@@ -98,6 +98,14 @@ is emitted into named-cast descriptors:
couple/group prompts without turning every partner into a fully detailed primary couple/group prompts without turning every partner into a fully detailed primary
character. Set a man slot to `full` when the partner needs exact hair/eye detail. character. Set a man slot to `full` when the partner needs exact hair/eye detail.
Slots also expose `expression_enabled` and `expression_intensity`. Disable
`expression_enabled` when that character should not receive a face/expression
directive. Leave `expression_intensity` at `-1` to use the generator or Insta/OF
option fallback. Set it from `0.0` to `1.0` to make expression selection
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.
Slots are chainable through the `character_cast` input/output. In automatic 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, 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: the next upstream slot becomes `B`, then `C`, and so on. Example:
@@ -293,6 +301,11 @@ Options:
- `hardcore_expression_intensity`: `0.0` is controlled, `0.5` is balanced - `hardcore_expression_intensity`: `0.0` is controlled, `0.5` is balanced
hardcore, `1.0` strongly favors ahegao-style, drooling, fucked-out, climax, hardcore, `1.0` strongly favors ahegao-style, drooling, fucked-out, climax,
and messy orgasm expressions. and messy orgasm 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`
overrides them when character slots are connected, and a disabled slot omits
that character's expression.
- `platform_style`: `hybrid`, `instagram`, or `onlyfans`. - `platform_style`: `hybrid`, `instagram`, or `onlyfans`.
- `continuity`: `same_creator_same_room` keeps the scene aligned while each - `continuity`: `same_creator_same_room` keeps the scene aligned while each
output keeps its own pose/composition; `same_creator_new_scene` keeps the same output keeps its own pose/composition; `same_creator_new_scene` keeps the same
@@ -320,6 +333,7 @@ The node keeps the original generator controls:
`indigenous`, `mixed`, `asian`, or `white_asian`. Combined filter strings `indigenous`, `mixed`, `asian`, or `white_asian`. Combined filter strings
such as `latina+south_asian` are also accepted in config JSON. such as `latina+south_asian` are also accepted in config JSON.
- `poses`: `standard` or `evocative`. - `poses`: `standard` or `evocative`.
- `expression_enabled`: disable facial-expression text entirely for this row.
- `expression_intensity`: `0.0` favors mild, neutral, controlled expressions; - `expression_intensity`: `0.0` favors mild, neutral, controlled expressions;
`0.5` favors balanced category expressions; `1.0` strongly favors the most `0.5` favors balanced category expressions; `1.0` strongly favors the most
intense expressions available in the selected category. This affects custom intense expressions available in the selected category. This affects custom
+32 -1
View File
@@ -106,6 +106,7 @@ class SxCPPromptBuilder:
"clothing": (["full", "minimal"], {"default": "full"}), "clothing": (["full", "minimal"], {"default": "full"}),
"ethnicity": (ethnicity_choices(), {"default": "any"}), "ethnicity": (ethnicity_choices(), {"default": "any"}),
"poses": (["standard", "evocative"], {"default": "standard"}), "poses": (["standard", "evocative"], {"default": "standard"}),
"expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), "expression_intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"backside_bias": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "backside_bias": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}), "figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}),
@@ -141,6 +142,7 @@ class SxCPPromptBuilder:
clothing, clothing,
ethnicity, ethnicity,
poses, poses,
expression_enabled,
expression_intensity, expression_intensity,
backside_bias, backside_bias,
figure, figure,
@@ -168,6 +170,7 @@ class SxCPPromptBuilder:
clothing=clothing, clothing=clothing,
ethnicity=ethnicity, ethnicity=ethnicity,
poses=poses, poses=poses,
expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
backside_bias=backside_bias, backside_bias=backside_bias,
figure=figure, figure=figure,
@@ -418,6 +421,7 @@ class SxCPGenerationProfile:
"profile": (generation_profile_choices(), {"default": "balanced"}), "profile": (generation_profile_choices(), {"default": "balanced"}),
"clothing_override": (["profile_default", "full", "minimal"], {"default": "profile_default"}), "clothing_override": (["profile_default", "full", "minimal"], {"default": "profile_default"}),
"poses_override": (["profile_default", "standard", "evocative"], {"default": "profile_default"}), "poses_override": (["profile_default", "standard", "evocative"], {"default": "profile_default"}),
"expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"backside_bias": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "backside_bias": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
"minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
@@ -436,6 +440,7 @@ class SxCPGenerationProfile:
profile, profile,
clothing_override, clothing_override,
poses_override, poses_override,
expression_enabled,
expression_intensity, expression_intensity,
backside_bias, backside_bias,
minimal_clothing_ratio, minimal_clothing_ratio,
@@ -446,6 +451,7 @@ class SxCPGenerationProfile:
profile=profile, profile=profile,
clothing_override=clothing_override, clothing_override=clothing_override,
poses_override=poses_override, poses_override=poses_override,
expression_enabled=expression_enabled,
expression_intensity=expression_intensity, expression_intensity=expression_intensity,
backside_bias=backside_bias, backside_bias=backside_bias,
minimal_clothing_ratio=minimal_clothing_ratio, minimal_clothing_ratio=minimal_clothing_ratio,
@@ -453,7 +459,8 @@ class SxCPGenerationProfile:
trigger_policy=trigger_policy, trigger_policy=trigger_policy,
) )
parsed = json.loads(config) parsed = json.loads(config)
summary = f"{parsed['profile']}: {parsed['clothing']}, {parsed['poses']}, expression {parsed['expression_intensity']}" expression_summary = "expression disabled" if not parsed.get("expression_enabled", True) else f"expression {parsed['expression_intensity']}"
summary = f"{parsed['profile']}: {parsed['clothing']}, {parsed['poses']}, {expression_summary}"
return config, summary return config, summary
@@ -600,6 +607,8 @@ class SxCPCharacterSlot:
"hair": ("STRING", {"default": ""}), "hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}), "eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "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}),
}, },
"optional": { "optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -627,6 +636,8 @@ class SxCPCharacterSlot:
hair, hair,
eyes, eyes,
descriptor_detail="auto", descriptor_detail="auto",
expression_enabled=True,
expression_intensity=-1.0,
character_cast="", character_cast="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
@@ -643,6 +654,8 @@ class SxCPCharacterSlot:
hair=hair, hair=hair,
eyes=eyes, eyes=eyes,
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled,
expression_intensity=expression_intensity,
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -667,6 +680,8 @@ class SxCPWomanSlot:
"hair": ("STRING", {"default": ""}), "hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}), "eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "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}),
}, },
"optional": { "optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -693,6 +708,8 @@ class SxCPWomanSlot:
hair, hair,
eyes, eyes,
descriptor_detail="auto", descriptor_detail="auto",
expression_enabled=True,
expression_intensity=-1.0,
character_cast="", character_cast="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
@@ -709,6 +726,8 @@ class SxCPWomanSlot:
hair=hair, hair=hair,
eyes=eyes, eyes=eyes,
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled,
expression_intensity=expression_intensity,
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -732,6 +751,8 @@ class SxCPManSlot:
"hair": ("STRING", {"default": ""}), "hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}), "eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}),
"expression_enabled": ("BOOLEAN", {"default": True}),
"expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
}, },
"optional": { "optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}), "character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -757,6 +778,8 @@ class SxCPManSlot:
hair, hair,
eyes, eyes,
descriptor_detail="compact", descriptor_detail="compact",
expression_enabled=True,
expression_intensity=-1.0,
character_cast="", character_cast="",
): ):
result = build_character_slot_json( result = build_character_slot_json(
@@ -773,6 +796,8 @@ class SxCPManSlot:
hair=hair, hair=hair,
eyes=eyes, eyes=eyes,
descriptor_detail=descriptor_detail, descriptor_detail=descriptor_detail,
expression_enabled=expression_enabled,
expression_intensity=expression_intensity,
enabled=enabled, enabled=enabled,
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -1005,6 +1030,8 @@ class SxCPInstaOFOptions:
"hardcore_men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "hardcore_men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
"softcore_level": (["social_tease", "lingerie_tease", "implied_nude", "explicit_tease", "explicit_nude"], {"default": "lingerie_tease"}), "softcore_level": (["social_tease", "lingerie_tease", "implied_nude", "explicit_tease", "explicit_nude"], {"default": "lingerie_tease"}),
"hardcore_level": (["explicit", "hardcore"], {"default": "hardcore"}), "hardcore_level": (["explicit", "hardcore"], {"default": "hardcore"}),
"softcore_expression_enabled": ("BOOLEAN", {"default": True}),
"hardcore_expression_enabled": ("BOOLEAN", {"default": True}),
"softcore_expression_intensity": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01}), "softcore_expression_intensity": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01}),
"hardcore_expression_intensity": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}),
"platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}), "platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}),
@@ -1029,6 +1056,8 @@ class SxCPInstaOFOptions:
hardcore_men_count, hardcore_men_count,
softcore_level, softcore_level,
hardcore_level, hardcore_level,
softcore_expression_enabled,
hardcore_expression_enabled,
softcore_expression_intensity, softcore_expression_intensity,
hardcore_expression_intensity, hardcore_expression_intensity,
platform_style, platform_style,
@@ -1046,6 +1075,8 @@ class SxCPInstaOFOptions:
hardcore_men_count=hardcore_men_count, hardcore_men_count=hardcore_men_count,
softcore_level=softcore_level, softcore_level=softcore_level,
hardcore_level=hardcore_level, hardcore_level=hardcore_level,
softcore_expression_enabled=softcore_expression_enabled,
hardcore_expression_enabled=hardcore_expression_enabled,
softcore_expression_intensity=softcore_expression_intensity, softcore_expression_intensity=softcore_expression_intensity,
hardcore_expression_intensity=hardcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity,
platform_style=platform_style, platform_style=platform_style,
+22 -4
View File
@@ -49,6 +49,18 @@ def _clean_text(value: Any) -> str:
return text return text
def _is_false(value: Any) -> bool:
if isinstance(value, bool):
return value is False
if isinstance(value, str):
return value.strip().lower() in ("false", "0", "no", "off")
return False
def _expression_disabled(row: dict[str, Any]) -> bool:
return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True))
def _cap_first(text: str) -> str: def _cap_first(text: str) -> str:
text = _clean_text(text).strip(" ,") text = _clean_text(text).strip(" ,")
return text[:1].upper() + text[1:] if text else "" return text[:1].upper() + text[1:] if text else ""
@@ -362,7 +374,7 @@ def _single_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -
item = _clean_clothing(_row_value(row, "clothing", ("Clothing", "Erotic outfit"))) item = _clean_clothing(_row_value(row, "clothing", ("Clothing", "Erotic outfit")))
scene = _row_value(row, "scene_text", ("Scene", "Setting")) scene = _row_value(row, "scene_text", ("Scene", "Setting"))
pose = _row_value(row, "pose", ("Pose",)) pose = _row_value(row, "pose", ("Pose",))
expression = _row_value(row, "expression", ("Facial expression", "Facial expressions")) expression = "" if _expression_disabled(row) else _row_value(row, "expression", ("Facial expression", "Facial expressions"))
composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
prop = _row_value(row, "prop", ("Prop/detail",)) prop = _row_value(row, "prop", ("Prop/detail",))
style = _row_value(row, "style") if keep_style else "" style = _row_value(row, "style") if keep_style else ""
@@ -431,7 +443,9 @@ def _couple_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -
pose = pose.replace(", affectionate and flirtatious but non-explicit", "") pose = pose.replace(", affectionate and flirtatious but non-explicit", "")
clothing = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",))) clothing = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",)))
scene = _row_value(row, "scene_text", ("Scene", "Setting")) scene = _row_value(row, "scene_text", ("Scene", "Setting"))
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) expression = ""
if not _expression_disabled(row):
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
style = _row_value(row, "style") if keep_style else "" style = _row_value(row, "style") if keep_style else ""
@@ -466,7 +480,9 @@ def _configured_cast_from_row(row: dict[str, Any], detail_level: str, keep_style
role_graph = _row_value(row, "role_graph", ("Role graph",)) role_graph = _row_value(row, "role_graph", ("Role graph",))
item = _row_value(row, "item", ITEM_LABELS) item = _row_value(row, "item", ITEM_LABELS)
scene = _row_value(row, "scene_text", ("Setting", "Scene")) scene = _row_value(row, "scene_text", ("Setting", "Scene"))
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) expression = ""
if not _expression_disabled(row):
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
cast_descriptor_text = _row_value(row, "cast_descriptor_text", ("Characters", "Cast descriptors")) cast_descriptor_text = _row_value(row, "cast_descriptor_text", ("Characters", "Cast descriptors"))
scene_kind = _row_value(row, "scene_kind") or "explicit adult sex scene" scene_kind = _row_value(row, "scene_kind") or "explicit adult sex scene"
@@ -504,7 +520,9 @@ def _group_or_layout_from_row(row: dict[str, Any], detail_level: str, keep_style
age = _row_value(row, "age", ("Ages",)) or _clean_text(row.get("age_band")) age = _row_value(row, "age", ("Ages",)) or _clean_text(row.get("age_band"))
item = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",))) item = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",)))
scene = _row_value(row, "scene_text", ("Scene", "Setting")) scene = _row_value(row, "scene_text", ("Scene", "Setting"))
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) expression = ""
if not _expression_disabled(row):
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) composition = _normalize_composition(_row_value(row, "composition", ("Composition",)))
style = _row_value(row, "style") if keep_style else "" style = _row_value(row, "style") if keep_style else ""
+4 -4
View File
@@ -41,7 +41,7 @@
"id": 3, "id": 3,
"type": "SxCPGenerationProfile", "type": "SxCPGenerationProfile",
"pos": [-1220, -300], "pos": [-1220, -300],
"size": [320, 226], "size": [320, 250],
"flags": {}, "flags": {},
"order": 2, "order": 2,
"mode": 0, "mode": 0,
@@ -51,7 +51,7 @@
{"name": "summary", "type": "STRING", "links": null, "slot_index": 1} {"name": "summary", "type": "STRING", "links": null, "slot_index": 1}
], ],
"properties": {"Node name for S&R": "SxCPGenerationProfile"}, "properties": {"Node name for S&R": "SxCPGenerationProfile"},
"widgets_values": ["casual_clean", "profile_default", "profile_default", -1.0, -1.0, -1.0, -1.0, "profile_default"] "widgets_values": ["casual_clean", "profile_default", "profile_default", true, -1.0, -1.0, -1.0, -1.0, "profile_default"]
}, },
{ {
"id": 4, "id": 4,
@@ -210,7 +210,7 @@
"id": 12, "id": 12,
"type": "SxCPInstaOFOptions", "type": "SxCPInstaOFOptions",
"pos": [-1220, 290], "pos": [-1220, 290],
"size": [360, 318], "size": [360, 366],
"flags": {}, "flags": {},
"order": 11, "order": 11,
"mode": 0, "mode": 0,
@@ -219,7 +219,7 @@
{"name": "options_json", "type": "STRING", "links": [10], "slot_index": 0} {"name": "options_json", "type": "STRING", "links": [10], "slot_index": 0}
], ],
"properties": {"Node name for S&R": "SxCPInstaOFOptions"}, "properties": {"Node name for S&R": "SxCPInstaOFOptions"},
"widgets_values": ["same_as_hardcore", "couple", 1, 1, "lingerie_tease", "hardcore", 0.45, 0.85, "hybrid", "same_creator_same_room", "partially_removed", "handheld_selfie", "from_camera_config", "compact"] "widgets_values": ["same_as_hardcore", "couple", 1, 1, "lingerie_tease", "hardcore", true, true, 0.45, 0.85, "hybrid", "same_creator_same_room", "partially_removed", "handheld_selfie", "from_camera_config", "compact"]
}, },
{ {
"id": 13, "id": 13,
+41 -4
View File
@@ -40,6 +40,18 @@ def _clean(value: Any) -> str:
return text return text
def _is_false(value: Any) -> bool:
if isinstance(value, bool):
return value is False
if isinstance(value, str):
return value.strip().lower() in ("false", "0", "no", "off")
return False
def _expression_disabled(row: dict[str, Any]) -> bool:
return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True))
def _sentence(text: str) -> str: def _sentence(text: str) -> str:
text = _clean(text).strip(" ,;") text = _clean(text).strip(" ,;")
if not text: if not text:
@@ -1009,6 +1021,15 @@ def _appearance_phrase(row: dict[str, Any]) -> str:
return ", ".join(_clean(part) for part in parts if _clean(part)) return ", ".join(_clean(part) for part in parts if _clean(part))
def _expression_phrase(expression: Any) -> str:
expression = _clean(expression)
if not expression:
return ""
if ";" in expression or re.search(r"\b(?:Woman|Man) [A-Z] has\b|\bthe (?:woman|man) has\b", expression):
return f"Expressions: {expression}"
return f"with {expression}"
def _camera_phrase(row: dict[str, Any]) -> str: def _camera_phrase(row: dict[str, Any]) -> str:
directive = _clean(row.get("camera_directive")) directive = _clean(row.get("camera_directive"))
if directive: if directive:
@@ -1090,7 +1111,9 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
item = re.sub(r",?\s*(fashion editorial|resort) styling$", "", item, flags=re.IGNORECASE) item = re.sub(r",?\s*(fashion editorial|resort) styling$", "", item, flags=re.IGNORECASE)
scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene")) scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene"))
pose = _row_value(row, "pose", ("Sexual pose", "Pose")) pose = _row_value(row, "pose", ("Sexual pose", "Pose"))
expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) expression = ""
if not _expression_disabled(row):
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE) composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE)
camera = _camera_phrase(row) camera = _camera_phrase(row)
style = _style_phrase(row, style_mode) style = _style_phrase(row, style_mode)
@@ -1111,6 +1134,7 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
cast_prose, cast_labels = _cast_prose(cast_descriptor_text) cast_prose, cast_labels = _cast_prose(cast_descriptor_text)
if not cast_labels and women_count == 1 and men_count == 1: if not cast_labels and women_count == 1 and men_count == 1:
cast_labels = ["Woman A", "Man A"] cast_labels = ["Woman A", "Man A"]
expression = _natural_label_text(expression, cast_labels)
role_graph = _sanitize_scene_text_for_cast(row.get("role_graph"), cast_labels) role_graph = _sanitize_scene_text_for_cast(row.get("role_graph"), cast_labels)
item = _sanitize_scene_text_for_cast(item, cast_labels) item = _sanitize_scene_text_for_cast(item, cast_labels)
role_graph = _natural_label_text(role_graph, cast_labels) role_graph = _natural_label_text(role_graph, cast_labels)
@@ -1123,7 +1147,7 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
f"A consensual explicit adult scene with {subject}" if not action else "", f"A consensual explicit adult scene with {subject}" if not action else "",
f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "",
f"The setting is {scene}" if scene else "", f"The setting is {scene}" if scene else "",
f"Facial expressions are {expression}" if expression else "", _expression_phrase(expression),
_composition_phrase(composition, action, "The image is framed as"), _composition_phrase(composition, action, "The image is framed as"),
camera, camera,
style if detail_level != "concise" else "", style if detail_level != "concise" else "",
@@ -1228,6 +1252,19 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
partner_pose = "" partner_pose = ""
partner_outfit_text = _natural_label_text(partner_outfit_text, soft_labels) partner_outfit_text = _natural_label_text(partner_outfit_text, soft_labels)
soft_expression = ""
if not _expression_disabled(soft):
soft_expression = _natural_label_text(
_clean(soft.get("character_expression_text")) or _clean(soft.get("expression")),
soft_labels,
)
hard_expression = ""
if not _expression_disabled(hard):
hard_expression = _natural_label_text(
_clean(hard.get("character_expression_text")) or _clean(hard.get("expression")),
hard_labels,
)
soft_parts = [ soft_parts = [
soft_cast_prose, soft_cast_prose,
soft_cast_presence, soft_cast_presence,
@@ -1235,7 +1272,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
partner_pose, partner_pose,
f"wearing {soft.get('item')}" if soft.get("item") else "", f"wearing {soft.get('item')}" if soft.get("item") else "",
f"{soft.get('pose')}" if soft.get("pose") else "", f"{soft.get('pose')}" if soft.get("pose") else "",
f"with {soft.get('expression')}" if soft.get("expression") else "", _expression_phrase(soft_expression),
f"in {soft.get('scene_text')}" if soft.get("scene_text") else "", f"in {soft.get('scene_text')}" if soft.get("scene_text") else "",
f"framed as {soft.get('composition')}" if soft.get("composition") else "", f"framed as {soft.get('composition')}" if soft.get("composition") else "",
soft_camera, soft_camera,
@@ -1246,7 +1283,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
_natural_clothing_state(row.get("hardcore_clothing_state")), _natural_clothing_state(row.get("hardcore_clothing_state")),
hard_cast_prose, hard_cast_prose,
f"set in {hard_scene}" if hard_scene else "", f"set in {hard_scene}" if hard_scene else "",
f"with {hard.get('expression')}" if hard.get("expression") else "", _expression_phrase(hard_expression),
_composition_phrase(hard_composition, hard_action), _composition_phrase(hard_composition, hard_action),
hard_camera, hard_camera,
hard_style if detail_level != "concise" else "", hard_style if detail_level != "concise" else "",
+283 -6
View File
@@ -890,6 +890,7 @@ GENERATION_PROFILE_PRESETS = {
"balanced": { "balanced": {
"clothing": "full", "clothing": "full",
"poses": "standard", "poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.5, "expression_intensity": 0.5,
"backside_bias": 0.0, "backside_bias": 0.0,
"minimal_clothing_ratio": -1.0, "minimal_clothing_ratio": -1.0,
@@ -900,6 +901,7 @@ GENERATION_PROFILE_PRESETS = {
"casual_clean": { "casual_clean": {
"clothing": "full", "clothing": "full",
"poses": "standard", "poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.35, "expression_intensity": 0.35,
"backside_bias": 0.0, "backside_bias": 0.0,
"minimal_clothing_ratio": -1.0, "minimal_clothing_ratio": -1.0,
@@ -910,6 +912,7 @@ GENERATION_PROFILE_PRESETS = {
"evocative_softcore": { "evocative_softcore": {
"clothing": "minimal", "clothing": "minimal",
"poses": "evocative", "poses": "evocative",
"expression_enabled": True,
"expression_intensity": 0.65, "expression_intensity": 0.65,
"backside_bias": 0.2, "backside_bias": 0.2,
"minimal_clothing_ratio": -1.0, "minimal_clothing_ratio": -1.0,
@@ -920,6 +923,7 @@ GENERATION_PROFILE_PRESETS = {
"hardcore_intense": { "hardcore_intense": {
"clothing": "minimal", "clothing": "minimal",
"poses": "evocative", "poses": "evocative",
"expression_enabled": True,
"expression_intensity": 0.9, "expression_intensity": 0.9,
"backside_bias": 0.0, "backside_bias": 0.0,
"minimal_clothing_ratio": -1.0, "minimal_clothing_ratio": -1.0,
@@ -930,6 +934,7 @@ GENERATION_PROFILE_PRESETS = {
"krea2_friendly": { "krea2_friendly": {
"clothing": "full", "clothing": "full",
"poses": "standard", "poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.55, "expression_intensity": 0.55,
"backside_bias": 0.0, "backside_bias": 0.0,
"minimal_clothing_ratio": -1.0, "minimal_clothing_ratio": -1.0,
@@ -940,6 +945,7 @@ GENERATION_PROFILE_PRESETS = {
"flux_original": { "flux_original": {
"clothing": "full", "clothing": "full",
"poses": "standard", "poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.5, "expression_intensity": 0.5,
"backside_bias": 0.0, "backside_bias": 0.0,
"minimal_clothing_ratio": -1.0, "minimal_clothing_ratio": -1.0,
@@ -1039,6 +1045,7 @@ def build_generation_profile_json(
minimal_clothing_ratio: float = -1.0, minimal_clothing_ratio: float = -1.0,
standard_pose_ratio: float = -1.0, standard_pose_ratio: float = -1.0,
trigger_policy: str = "profile_default", trigger_policy: str = "profile_default",
expression_enabled: bool = True,
) -> str: ) -> str:
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced" profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
config = dict(GENERATION_PROFILE_PRESETS[profile]) config = dict(GENERATION_PROFILE_PRESETS[profile])
@@ -1046,6 +1053,7 @@ def build_generation_profile_json(
config["clothing"] = clothing_override config["clothing"] = clothing_override
if poses_override in ("standard", "evocative"): if poses_override in ("standard", "evocative"):
config["poses"] = poses_override config["poses"] = poses_override
config["expression_enabled"] = not _is_false(expression_enabled)
if float(expression_intensity) >= 0: if float(expression_intensity) >= 0:
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"]) config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
if float(backside_bias) >= 0: if float(backside_bias) >= 0:
@@ -1079,6 +1087,7 @@ def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> di
parsed.update(raw) parsed.update(raw)
parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal") else "full" parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal") else "full"
parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative") else "standard" parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative") else "standard"
parsed["expression_enabled"] = not _is_false(parsed.get("expression_enabled", True))
parsed["expression_intensity"] = _clamped_float(parsed.get("expression_intensity"), 0.5) parsed["expression_intensity"] = _clamped_float(parsed.get("expression_intensity"), 0.5)
parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0) parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0)
parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0) parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0)
@@ -1325,6 +1334,54 @@ def _format(template: str, context: dict[str, Any]) -> str:
return template.format_map(safe_context) return template.format_map(safe_context)
def _clean_prompt_punctuation(text: str) -> str:
text = re.sub(r"\s+", " ", str(text or "")).strip()
text = re.sub(r"\s+([,.;:])", r"\1", text)
text = re.sub(r"(?:,\s*){2,}", ", ", text)
text = re.sub(r"\.\s*\.", ".", text)
text = re.sub(r":\s*\.", ".", text)
return text.strip()
def _strip_expression_text(text: str, expression: Any = "") -> str:
text = str(text or "")
if not text:
return ""
text = re.sub(r"\s*Facial expressions?:\s*[^.]*\.\s*", " ", text, flags=re.IGNORECASE)
text = re.sub(r",\s*one with [^,]+ and the other with [^,]+(?=,)", "", text, flags=re.IGNORECASE)
text = re.sub(r",\s*a lively mix of expressions from [^,]+(?=,)", "", text, flags=re.IGNORECASE)
text = re.sub(r"\s+with\s+(?:an?|the)\s+[^,]*expression(?=,)", "", text, flags=re.IGNORECASE)
expression_text = str(expression or "").strip()
if expression_text:
for part in [piece.strip() for piece in expression_text.split(";") if piece.strip()]:
escaped = re.escape(part)
text = re.sub(rf",\s*{escaped}(?=,)", "", text, flags=re.IGNORECASE)
text = re.sub(rf"\s+with\s+(?:an?|the)?\s*{escaped}", "", text, flags=re.IGNORECASE)
return _clean_prompt_punctuation(text)
def _disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dict[str, Any]:
previous_expression = row.get("expression", "")
row["prompt"] = _strip_expression_text(row.get("prompt", ""), previous_expression)
row["caption"] = _strip_expression_text(row.get("caption", ""), previous_expression)
row["expression"] = ""
row["shared_expression"] = ""
row["character_expressions"] = []
row["character_expression_text"] = ""
row["expression_enabled"] = False
row["expression_disabled"] = True
row["expression_intensity"] = None
row["expression_intensity_source"] = source
return row
def _labeled_expression_sentence(label: str, expression: Any) -> str:
expression = str(expression or "").strip()
if not expression:
return ""
return f"{label}: {expression}. "
def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str: def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str:
trigger = trigger.strip() trigger = trigger.strip()
if not enabled or not trigger: if not enabled or not trigger:
@@ -1801,6 +1858,111 @@ def _normalize_descriptor_detail(value: Any) -> str:
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto" return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
def _normalize_slot_expression_intensity(value: Any) -> float:
try:
intensity = float(value)
except (TypeError, ValueError):
return -1.0
if intensity < 0:
return -1.0
return _clamped_float(intensity, 0.5)
def _slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
if not slot:
return True
return not _is_false(slot.get("expression_enabled", True))
def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
if not slot or not _slot_expression_enabled(slot):
return None
intensity = _normalize_slot_expression_intensity(slot.get("expression_intensity"))
return intensity if intensity >= 0 else None
def _mean(values: list[float]) -> float:
return sum(values) / len(values)
def _cast_expression_intensity_override(
fallback: float,
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
) -> 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))]),
("men", [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]),
]
all_values: list[float] = []
matching_slots: list[dict[str, Any]] = []
for group_name, labels in groups:
values: list[float] = []
value_labels: list[str] = []
for label in labels:
slot = label_map.get(label)
if slot:
matching_slots.append(slot)
value = _slot_expression_intensity(slot)
if value is not None:
values.append(value)
value_labels.append(label)
all_values.append(value)
if values:
if len(values) == 1:
return values[0], f"character_slot:{value_labels[0]}"
return _mean(values), f"character_slots:{group_name}"
if all_values:
return _mean(all_values), "character_slots:cast"
if matching_slots and all(not _slot_expression_enabled(slot) for slot in matching_slots):
return None, "character_slots:disabled"
return fallback, "input"
def _character_expression_entries(
rng: random.Random,
expression_pool: list[Any],
fallback_intensity: float,
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
) -> list[str]:
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))],
]
expressions: list[str] = []
used: set[str] = set()
for label in labels:
slot = label_map.get(label)
if not slot:
continue
if not _slot_expression_enabled(slot):
continue
intensity = _slot_expression_intensity(slot)
if intensity is None:
intensity = fallback_intensity
entries = _compatible_entries(
_expression_entries_for_intensity(expression_pool, intensity),
women_count,
men_count,
)
if not entries:
continue
choice = ""
for _attempt in range(5):
candidate = _choose_text(rng, entries)
if candidate not in used:
choice = candidate
break
if not choice:
choice = _choose_text(rng, entries)
used.add(choice)
expressions.append(f"{label} has {choice}")
return expressions
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str: def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
detail = _normalize_descriptor_detail(descriptor_detail) detail = _normalize_descriptor_detail(descriptor_detail)
if detail != "auto": if detail != "auto":
@@ -1883,6 +2045,8 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
"hair": _slot_value(slot.get("hair")), "hair": _slot_value(slot.get("hair")),
"eyes": _slot_value(slot.get("eyes")), "eyes": _slot_value(slot.get("eyes")),
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")), "descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
} }
normalized["summary"] = _character_slot_summary(normalized) normalized["summary"] = _character_slot_summary(normalized)
return normalized return normalized
@@ -1927,6 +2091,12 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
f"body={slot.get('body', 'random')}", f"body={slot.get('body', 'random')}",
f"detail={slot.get('descriptor_detail', 'auto')}", f"detail={slot.get('descriptor_detail', 'auto')}",
] ]
if not _slot_expression_enabled(slot):
parts.append("expression=disabled")
else:
expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None:
parts.append(f"expression={expression_intensity:.2f}")
for key in ("body_phrase", "skin", "hair", "eyes"): for key in ("body_phrase", "skin", "hair", "eyes"):
value = slot.get(key) value = slot.get(key)
if value: if value:
@@ -1948,6 +2118,8 @@ def build_character_slot_json(
hair: str = "", hair: str = "",
eyes: str = "", eyes: str = "",
descriptor_detail: str = "auto", descriptor_detail: str = "auto",
expression_enabled: bool = True,
expression_intensity: float = -1.0,
enabled: bool = True, enabled: bool = True,
character_cast: str | dict[str, Any] | list[Any] | None = "", character_cast: str | dict[str, Any] | list[Any] | None = "",
) -> dict[str, str]: ) -> dict[str, str]:
@@ -1967,6 +2139,8 @@ def build_character_slot_json(
"hair": hair, "hair": hair,
"eyes": eyes, "eyes": eyes,
"descriptor_detail": descriptor_detail, "descriptor_detail": descriptor_detail,
"expression_enabled": expression_enabled,
"expression_intensity": expression_intensity,
} }
) )
slots = existing_slots + ([slot] if enabled else []) slots = existing_slots + ([slot] if enabled else [])
@@ -2049,6 +2223,10 @@ def _context_from_character_slot(
if value: if value:
context[key] = value context[key] = value
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail")) context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
context["expression_enabled"] = _slot_expression_enabled(slot)
expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None:
context["expression_intensity"] = expression_intensity
context["subject_type"] = subject_type context["subject_type"] = subject_type
context["subject"] = subject_type context["subject"] = subject_type
context["subject_phrase"] = subject_type context["subject_phrase"] = subject_type
@@ -2084,9 +2262,11 @@ def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]
"eyes", "eyes",
"figure", "figure",
"descriptor_detail", "descriptor_detail",
"expression_enabled",
"expression_intensity",
): ):
value = context.get(key) value = context.get(key)
if value: if value is not None and value != "":
row[key] = value row[key] = value
if context.get("age"): if context.get("age"):
row["age_band"] = context["age"] row["age_band"] = context["age"]
@@ -2997,6 +3177,7 @@ def _build_custom_row(
men_count: int, men_count: int,
seed: int, seed: int,
seed_config: dict[str, int], seed_config: dict[str, int],
expression_enabled: bool,
expression_intensity: float, expression_intensity: float,
character_profile: str | dict[str, Any] | None = None, character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None,
@@ -3063,6 +3244,29 @@ def _build_custom_row(
role_graph = _role_graph(role_rng, subcategory, context, item_axis_values) role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
cast_descriptors: list[str] = [] cast_descriptors: list[str] = []
cast_descriptor_text = "" cast_descriptor_text = ""
expression_intensity_source = "input"
expression_disabled = not bool(expression_enabled)
if expression_disabled:
expression_intensity_source = "disabled"
elif subject_type in ("woman", "man") and applied_slot:
slot_label = "Woman A" if subject_type == "woman" else "Man A"
if not _slot_expression_enabled(applied_slot):
expression_disabled = True
expression_intensity_source = f"character_slot:{slot_label}:disabled"
else:
slot_expression_intensity = _slot_expression_intensity(applied_slot)
if slot_expression_intensity is not None:
expression_intensity = slot_expression_intensity
expression_intensity_source = f"character_slot:{slot_label}"
elif subject_type == "configured_cast" and character_slots:
expression_intensity, expression_intensity_source = _cast_expression_intensity_override(
expression_intensity,
character_slot_map,
women_count,
men_count,
)
if expression_intensity is None:
expression_disabled = True
if subject_type == "configured_cast" and character_slots: if subject_type == "configured_cast" and character_slots:
cast_descriptors, _descriptor_slots = _cast_descriptor_entries( cast_descriptors, _descriptor_slots = _cast_descriptor_entries(
seed_config, seed_config,
@@ -3082,8 +3286,12 @@ def _build_custom_row(
pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text( pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text(
pose_rng, _compatible_entries(_pose_pool(category, subcategory, item, subject_type, poses), women_count, men_count) pose_rng, _compatible_entries(_pose_pool(category, subcategory, item, subject_type, poses), women_count, men_count)
)) ))
expression_pool = _expression_pool(category, subcategory, item)
if expression_disabled:
expression = ""
else:
expression_entries = _compatible_entries( expression_entries = _compatible_entries(
_expression_entries_for_intensity(_expression_pool(category, subcategory, item), expression_intensity), _expression_entries_for_intensity(expression_pool, expression_intensity),
women_count, women_count,
men_count, men_count,
) )
@@ -3092,6 +3300,21 @@ def _build_custom_row(
secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression) secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression)
if secondary_expression: if secondary_expression:
expression = f"{expression}; {secondary_expression}" expression = f"{expression}; {secondary_expression}"
shared_expression = expression
character_expressions: list[str] = []
character_expression_text = ""
if not expression_disabled and subject_type == "configured_cast" and character_slots:
character_expressions = _character_expression_entries(
expression_rng,
expression_pool,
expression_intensity,
character_slot_map,
women_count,
men_count,
)
character_expression_text = "; ".join(character_expressions)
if character_expression_text:
expression = character_expression_text
composition = _choose_text( composition = _choose_text(
composition_rng, composition_rng,
_compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count), _compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count),
@@ -3124,7 +3347,13 @@ def _build_custom_row(
"scene_slug": scene_slug, "scene_slug": scene_slug,
"pose": pose, "pose": pose,
"expression": expression, "expression": expression,
"shared_expression": shared_expression,
"character_expressions": character_expressions,
"character_expression_text": character_expression_text,
"expression_enabled": not expression_disabled,
"expression_disabled": expression_disabled,
"expression_intensity": expression_intensity, "expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"composition": composition, "composition": composition,
"composition_prompt": _composition_prompt(composition), "composition_prompt": _composition_prompt(composition),
"role_graph": role_graph, "role_graph": role_graph,
@@ -3191,6 +3420,11 @@ def _build_custom_row(
"seed_config": seed_config, "seed_config": seed_config,
"content_seed_axis": content_axis, "content_seed_axis": content_axis,
"role_graph": role_graph, "role_graph": role_graph,
"shared_expression": shared_expression,
"character_expressions": character_expressions,
"character_expression_text": character_expression_text,
"expression_enabled": not expression_disabled,
"expression_disabled": expression_disabled,
"cast_summary": context.get("cast_summary", ""), "cast_summary": context.get("cast_summary", ""),
"cast_descriptors": cast_descriptors, "cast_descriptors": cast_descriptors,
"cast_descriptor_text": cast_descriptor_text, "cast_descriptor_text": cast_descriptor_text,
@@ -3204,11 +3438,15 @@ def _build_custom_row(
"character_slot": applied_slot, "character_slot": applied_slot,
"character_slot_status": slot_status, "character_slot_status": slot_status,
"character_cast_slots": character_slots, "character_cast_slots": character_slots,
"expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"source": "json_category", "source": "json_category",
} }
) )
if context.get("figure"): if context.get("figure"):
row["figure"] = context["figure"] row["figure"] = context["figure"]
if expression_disabled:
row = _disable_row_expression(row, expression_intensity_source)
return row return row
@@ -3238,6 +3476,7 @@ def build_prompt(
expression_intensity: float = 0.5, expression_intensity: float = 0.5,
character_profile: str | dict[str, Any] | None = None, character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_enabled: bool = True,
) -> dict[str, Any]: ) -> dict[str, Any]:
apply_pool_extensions() apply_pool_extensions()
row_number = max(1, int(row_number)) row_number = max(1, int(row_number))
@@ -3247,6 +3486,7 @@ def build_prompt(
ethnicity = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in str(ethnicity) else "any" ethnicity = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in str(ethnicity) else "any"
poses = poses if poses in ("standard", "evocative") else "standard" poses = poses if poses in ("standard", "evocative") else "standard"
figure = figure if figure in ("curvy", "balanced", "bombshell") else "curvy" figure = figure if figure in ("curvy", "balanced", "bombshell") else "curvy"
expression_enabled = not _is_false(expression_enabled)
minimal_ratio = _ratio_or_none(minimal_clothing_ratio) minimal_ratio = _ratio_or_none(minimal_clothing_ratio)
pose_ratio = _ratio_or_none(standard_pose_ratio) pose_ratio = _ratio_or_none(standard_pose_ratio)
expression_intensity = _clamped_float(expression_intensity, 0.5) expression_intensity = _clamped_float(expression_intensity, 0.5)
@@ -3300,11 +3540,14 @@ def build_prompt(
int(men_count), int(men_count),
seed, seed,
parsed_seed_config, parsed_seed_config,
expression_enabled,
expression_intensity, expression_intensity,
character_profile, character_profile,
character_cast, character_cast,
) )
if not expression_enabled:
row = _disable_row_expression(row, "disabled")
if extra_positive.strip(): if extra_positive.strip():
row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}" row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}"
row = _apply_camera_config(row, camera_config) row = _apply_camera_config(row, camera_config)
@@ -3312,7 +3555,8 @@ def build_prompt(
row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt)) row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt))
row["negative_prompt"] = _combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative) row["negative_prompt"] = _combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
row["trigger"] = active_trigger row["trigger"] = active_trigger
row["expression_intensity"] = expression_intensity row.setdefault("expression_intensity", expression_intensity)
row.setdefault("expression_intensity_source", "input")
return row return row
@@ -3344,6 +3588,7 @@ def build_prompt_from_configs(
clothing=profile["clothing"], clothing=profile["clothing"],
ethnicity=filters["ethnicity"], ethnicity=filters["ethnicity"],
poses=profile["poses"], poses=profile["poses"],
expression_enabled=profile["expression_enabled"],
expression_intensity=profile["expression_intensity"], expression_intensity=profile["expression_intensity"],
backside_bias=profile["backside_bias"], backside_bias=profile["backside_bias"],
figure=filters["figure"], figure=filters["figure"],
@@ -3538,6 +3783,8 @@ def build_insta_of_options_json(
camera_detail: str = "compact", camera_detail: str = "compact",
softcore_expression_intensity: float = 0.45, softcore_expression_intensity: float = 0.45,
hardcore_expression_intensity: float = 0.85, hardcore_expression_intensity: float = 0.85,
softcore_expression_enabled: bool = True,
hardcore_expression_enabled: bool = True,
) -> str: ) -> str:
return json.dumps( return json.dumps(
{ {
@@ -3553,6 +3800,8 @@ def build_insta_of_options_json(
"softcore_camera_mode": softcore_camera_mode, "softcore_camera_mode": softcore_camera_mode,
"hardcore_camera_mode": hardcore_camera_mode, "hardcore_camera_mode": hardcore_camera_mode,
"camera_detail": camera_detail, "camera_detail": camera_detail,
"softcore_expression_enabled": not _is_false(softcore_expression_enabled),
"hardcore_expression_enabled": not _is_false(hardcore_expression_enabled),
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45), "softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85), "hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
}, },
@@ -3575,6 +3824,8 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s
"softcore_camera_mode": "handheld_selfie", "softcore_camera_mode": "handheld_selfie",
"hardcore_camera_mode": "from_camera_config", "hardcore_camera_mode": "from_camera_config",
"camera_detail": "compact", "camera_detail": "compact",
"softcore_expression_enabled": True,
"hardcore_expression_enabled": True,
"softcore_expression_intensity": 0.45, "softcore_expression_intensity": 0.45,
"hardcore_expression_intensity": 0.85, "hardcore_expression_intensity": 0.85,
} }
@@ -3608,6 +3859,8 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s
): ):
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"] parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
parsed["camera_detail"] = parsed["camera_detail"] if parsed["camera_detail"] in CAMERA_DETAIL_CHOICES else defaults["camera_detail"] parsed["camera_detail"] = parsed["camera_detail"] if parsed["camera_detail"] in CAMERA_DETAIL_CHOICES else defaults["camera_detail"]
parsed["softcore_expression_enabled"] = not _is_false(parsed.get("softcore_expression_enabled", True))
parsed["hardcore_expression_enabled"] = not _is_false(parsed.get("hardcore_expression_enabled", True))
parsed["softcore_expression_intensity"] = _clamped_float( parsed["softcore_expression_intensity"] = _clamped_float(
parsed.get("softcore_expression_intensity"), parsed.get("softcore_expression_intensity"),
defaults["softcore_expression_intensity"], defaults["softcore_expression_intensity"],
@@ -3806,6 +4059,22 @@ def build_insta_of_pair(
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key) soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
soft_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 311) soft_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 311)
soft_person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number) soft_person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
soft_expression_women_count = hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1
soft_expression_men_count = hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0
soft_expression_enabled = bool(options["softcore_expression_enabled"])
soft_expression_intensity = options["softcore_expression_intensity"]
soft_expression_intensity_source = "input"
if soft_expression_enabled:
soft_expression_intensity, soft_expression_intensity_source = _cast_expression_intensity_override(
options["softcore_expression_intensity"],
character_slot_map,
soft_expression_women_count,
soft_expression_men_count,
)
if soft_expression_intensity is None:
soft_expression_enabled = False
else:
soft_expression_intensity_source = "disabled"
primary_slot_context = None primary_slot_context = None
primary_slot = character_slot_map.get("Woman A") primary_slot = character_slot_map.get("Woman A")
if primary_slot: if primary_slot:
@@ -3841,14 +4110,18 @@ def build_insta_of_pair(
seed_config=parsed_seed_config, seed_config=parsed_seed_config,
women_count=1, women_count=1,
men_count=0, men_count=0,
expression_intensity=options["softcore_expression_intensity"], expression_enabled=soft_expression_enabled,
expression_intensity=soft_expression_intensity,
character_profile="" if primary_slot else character_profile or "", character_profile="" if primary_slot else character_profile or "",
character_cast="", character_cast="",
) )
soft_row["expression_intensity_source"] = soft_expression_intensity_source
if primary_slot_context: if primary_slot_context:
soft_row = _apply_character_context_to_row(soft_row, primary_slot_context) soft_row = _apply_character_context_to_row(soft_row, primary_slot_context)
soft_row["character_slot"] = primary_slot soft_row["character_slot"] = primary_slot
soft_row["character_slot_status"] = "applied:Woman A" 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) soft_row["item"] = _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["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 outfit"
@@ -3876,6 +4149,7 @@ def build_insta_of_pair(
seed_config=parsed_seed_config, seed_config=parsed_seed_config,
women_count=hard_women_count, women_count=hard_women_count,
men_count=hard_men_count, men_count=hard_men_count,
expression_enabled=options["hardcore_expression_enabled"],
expression_intensity=options["hardcore_expression_intensity"], expression_intensity=options["hardcore_expression_intensity"],
character_cast=character_cast or "", character_cast=character_cast or "",
) )
@@ -3959,7 +4233,8 @@ def build_insta_of_pair(
f"{soft_cast_presence}" f"{soft_cast_presence}"
f"{soft_cast_styling_sentence}" f"{soft_cast_styling_sentence}"
f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
f"Facial expression: {soft_row['expression']}. Composition: {soft_row['composition']}. " f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}"
f"Composition: {soft_row['composition']}. "
f"{soft_camera_sentence}" f"{soft_camera_sentence}"
"Keep the softcore version seductive, creator-shot, and non-explicit. " "Keep the softcore version seductive, creator-shot, and non-explicit. "
f"{soft_row['positive_suffix']}." f"{soft_row['positive_suffix']}."
@@ -3971,7 +4246,9 @@ def build_insta_of_pair(
"Keep Woman A visually central. " "Keep Woman A visually central. "
f"{hard_clothing_state} " f"{hard_clothing_state} "
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. " f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
f"Setting: {hard_scene}. Facial expressions: {hard_row['expression']}. Composition: {hard_composition}. " f"Setting: {hard_scene}. "
f"{_labeled_expression_sentence('Facial expressions', hard_row.get('expression'))}"
f"Composition: {hard_composition}. "
f"{hard_camera_sentence}" f"{hard_camera_sentence}"
f"{hard_row['positive_suffix']}." f"{hard_row['positive_suffix']}."
) )