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
+292 -15
View File
@@ -890,6 +890,7 @@ GENERATION_PROFILE_PRESETS = {
"balanced": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.5,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
@@ -900,6 +901,7 @@ GENERATION_PROFILE_PRESETS = {
"casual_clean": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.35,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
@@ -910,6 +912,7 @@ GENERATION_PROFILE_PRESETS = {
"evocative_softcore": {
"clothing": "minimal",
"poses": "evocative",
"expression_enabled": True,
"expression_intensity": 0.65,
"backside_bias": 0.2,
"minimal_clothing_ratio": -1.0,
@@ -920,6 +923,7 @@ GENERATION_PROFILE_PRESETS = {
"hardcore_intense": {
"clothing": "minimal",
"poses": "evocative",
"expression_enabled": True,
"expression_intensity": 0.9,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
@@ -930,6 +934,7 @@ GENERATION_PROFILE_PRESETS = {
"krea2_friendly": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.55,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
@@ -940,6 +945,7 @@ GENERATION_PROFILE_PRESETS = {
"flux_original": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.5,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
@@ -1039,6 +1045,7 @@ def build_generation_profile_json(
minimal_clothing_ratio: float = -1.0,
standard_pose_ratio: float = -1.0,
trigger_policy: str = "profile_default",
expression_enabled: bool = True,
) -> str:
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
config = dict(GENERATION_PROFILE_PRESETS[profile])
@@ -1046,6 +1053,7 @@ def build_generation_profile_json(
config["clothing"] = clothing_override
if poses_override in ("standard", "evocative"):
config["poses"] = poses_override
config["expression_enabled"] = not _is_false(expression_enabled)
if float(expression_intensity) >= 0:
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
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["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["expression_enabled"] = not _is_false(parsed.get("expression_enabled", True))
parsed["expression_intensity"] = _clamped_float(parsed.get("expression_intensity"), 0.5)
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)
@@ -1325,6 +1334,54 @@ def _format(template: str, context: dict[str, Any]) -> str:
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:
trigger = trigger.strip()
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"
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:
detail = _normalize_descriptor_detail(descriptor_detail)
if detail != "auto":
@@ -1883,6 +2045,8 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
"hair": _slot_value(slot.get("hair")),
"eyes": _slot_value(slot.get("eyes")),
"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)
return normalized
@@ -1927,6 +2091,12 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
f"body={slot.get('body', 'random')}",
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"):
value = slot.get(key)
if value:
@@ -1948,6 +2118,8 @@ def build_character_slot_json(
hair: str = "",
eyes: str = "",
descriptor_detail: str = "auto",
expression_enabled: bool = True,
expression_intensity: float = -1.0,
enabled: bool = True,
character_cast: str | dict[str, Any] | list[Any] | None = "",
) -> dict[str, str]:
@@ -1967,6 +2139,8 @@ def build_character_slot_json(
"hair": hair,
"eyes": eyes,
"descriptor_detail": descriptor_detail,
"expression_enabled": expression_enabled,
"expression_intensity": expression_intensity,
}
)
slots = existing_slots + ([slot] if enabled else [])
@@ -2049,6 +2223,10 @@ def _context_from_character_slot(
if value:
context[key] = value
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"] = 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",
"figure",
"descriptor_detail",
"expression_enabled",
"expression_intensity",
):
value = context.get(key)
if value:
if value is not None and value != "":
row[key] = value
if context.get("age"):
row["age_band"] = context["age"]
@@ -2997,6 +3177,7 @@ def _build_custom_row(
men_count: int,
seed: int,
seed_config: dict[str, int],
expression_enabled: bool,
expression_intensity: float,
character_profile: str | dict[str, 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)
cast_descriptors: list[str] = []
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:
cast_descriptors, _descriptor_slots = _cast_descriptor_entries(
seed_config,
@@ -3082,16 +3286,35 @@ def _build_custom_row(
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)
))
expression_entries = _compatible_entries(
_expression_entries_for_intensity(_expression_pool(category, subcategory, item), expression_intensity),
women_count,
men_count,
)
expression = _choose_text(expression_rng, expression_entries)
if subject_type in ("couple", "group") and ";" not in expression:
secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression)
if secondary_expression:
expression = f"{expression}; {secondary_expression}"
expression_pool = _expression_pool(category, subcategory, item)
if expression_disabled:
expression = ""
else:
expression_entries = _compatible_entries(
_expression_entries_for_intensity(expression_pool, expression_intensity),
women_count,
men_count,
)
expression = _choose_text(expression_rng, expression_entries)
if subject_type in ("couple", "group") and ";" not in expression:
secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression)
if 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_rng,
_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,
"pose": pose,
"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_source": expression_intensity_source,
"composition": composition,
"composition_prompt": _composition_prompt(composition),
"role_graph": role_graph,
@@ -3191,6 +3420,11 @@ def _build_custom_row(
"seed_config": seed_config,
"content_seed_axis": content_axis,
"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_descriptors": cast_descriptors,
"cast_descriptor_text": cast_descriptor_text,
@@ -3204,11 +3438,15 @@ def _build_custom_row(
"character_slot": applied_slot,
"character_slot_status": slot_status,
"character_cast_slots": character_slots,
"expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"source": "json_category",
}
)
if context.get("figure"):
row["figure"] = context["figure"]
if expression_disabled:
row = _disable_row_expression(row, expression_intensity_source)
return row
@@ -3238,6 +3476,7 @@ def build_prompt(
expression_intensity: float = 0.5,
character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_enabled: bool = True,
) -> dict[str, Any]:
apply_pool_extensions()
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"
poses = poses if poses in ("standard", "evocative") else "standard"
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)
pose_ratio = _ratio_or_none(standard_pose_ratio)
expression_intensity = _clamped_float(expression_intensity, 0.5)
@@ -3300,11 +3540,14 @@ def build_prompt(
int(men_count),
seed,
parsed_seed_config,
expression_enabled,
expression_intensity,
character_profile,
character_cast,
)
if not expression_enabled:
row = _disable_row_expression(row, "disabled")
if extra_positive.strip():
row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}"
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["negative_prompt"] = _combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
row["trigger"] = active_trigger
row["expression_intensity"] = expression_intensity
row.setdefault("expression_intensity", expression_intensity)
row.setdefault("expression_intensity_source", "input")
return row
@@ -3344,6 +3588,7 @@ def build_prompt_from_configs(
clothing=profile["clothing"],
ethnicity=filters["ethnicity"],
poses=profile["poses"],
expression_enabled=profile["expression_enabled"],
expression_intensity=profile["expression_intensity"],
backside_bias=profile["backside_bias"],
figure=filters["figure"],
@@ -3538,6 +3783,8 @@ def build_insta_of_options_json(
camera_detail: str = "compact",
softcore_expression_intensity: float = 0.45,
hardcore_expression_intensity: float = 0.85,
softcore_expression_enabled: bool = True,
hardcore_expression_enabled: bool = True,
) -> str:
return json.dumps(
{
@@ -3553,6 +3800,8 @@ def build_insta_of_options_json(
"softcore_camera_mode": softcore_camera_mode,
"hardcore_camera_mode": hardcore_camera_mode,
"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),
"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",
"hardcore_camera_mode": "from_camera_config",
"camera_detail": "compact",
"softcore_expression_enabled": True,
"hardcore_expression_enabled": True,
"softcore_expression_intensity": 0.45,
"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["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.get("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_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_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 = character_slot_map.get("Woman A")
if primary_slot:
@@ -3841,14 +4110,18 @@ def build_insta_of_pair(
seed_config=parsed_seed_config,
women_count=1,
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_cast="",
)
soft_row["expression_intensity_source"] = soft_expression_intensity_source
if 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_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["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key)
soft_row["item_label"] = "Insta/OF softcore outfit"
@@ -3876,6 +4149,7 @@ def build_insta_of_pair(
seed_config=parsed_seed_config,
women_count=hard_women_count,
men_count=hard_men_count,
expression_enabled=options["hardcore_expression_enabled"],
expression_intensity=options["hardcore_expression_intensity"],
character_cast=character_cast or "",
)
@@ -3959,7 +4233,8 @@ def build_insta_of_pair(
f"{soft_cast_presence}"
f"{soft_cast_styling_sentence}"
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}"
"Keep the softcore version seductive, creator-shot, and non-explicit. "
f"{soft_row['positive_suffix']}."
@@ -3971,7 +4246,9 @@ def build_insta_of_pair(
"Keep Woman A visually central. "
f"{hard_clothing_state} "
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_row['positive_suffix']}."
)