Add character descriptor detail controls

This commit is contained in:
2026-06-24 16:56:01 +02:00
parent 253b343c90
commit 5750175eea
3 changed files with 117 additions and 24 deletions
+13
View File
@@ -85,6 +85,19 @@ body choices and omits figure bias. The older generic `SxCP Character Slot`
remains available for compatibility and manual mixed use, but the gendered
slots are the cleaner default.
Each slot also has `descriptor_detail`, which controls how much appearance text
is emitted into named-cast descriptors:
- `auto`: women use `full`; men use `compact`.
- `full`: age, body, skin, hair, and eyes.
- `medium`: age, body, skin, and hair.
- `compact`: age, body, and skin.
- `minimal`: age and body only.
`SxCP Man Slot` defaults to `compact`, which keeps men readable in Krea-style
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.
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:
+11
View File
@@ -32,6 +32,7 @@ try:
category_choices,
character_age_choices,
character_body_choices,
character_descriptor_detail_choices,
character_ethnicity_choices,
character_figure_choices,
character_label_choices,
@@ -75,6 +76,7 @@ except ImportError:
category_choices,
character_age_choices,
character_body_choices,
character_descriptor_detail_choices,
character_ethnicity_choices,
character_figure_choices,
character_label_choices,
@@ -597,6 +599,7 @@ class SxCPCharacterSlot:
"skin": ("STRING", {"default": ""}),
"hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
},
"optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -623,6 +626,7 @@ class SxCPCharacterSlot:
skin,
hair,
eyes,
descriptor_detail="auto",
character_cast="",
):
result = build_character_slot_json(
@@ -638,6 +642,7 @@ class SxCPCharacterSlot:
skin=skin,
hair=hair,
eyes=eyes,
descriptor_detail=descriptor_detail,
enabled=enabled,
character_cast=character_cast or "",
)
@@ -661,6 +666,7 @@ class SxCPWomanSlot:
"skin": ("STRING", {"default": ""}),
"hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
},
"optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -686,6 +692,7 @@ class SxCPWomanSlot:
skin,
hair,
eyes,
descriptor_detail="auto",
character_cast="",
):
result = build_character_slot_json(
@@ -701,6 +708,7 @@ class SxCPWomanSlot:
skin=skin,
hair=hair,
eyes=eyes,
descriptor_detail=descriptor_detail,
enabled=enabled,
character_cast=character_cast or "",
)
@@ -723,6 +731,7 @@ class SxCPManSlot:
"skin": ("STRING", {"default": ""}),
"hair": ("STRING", {"default": ""}),
"eyes": ("STRING", {"default": ""}),
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}),
},
"optional": {
"character_cast": ("STRING", {"default": "", "multiline": True}),
@@ -747,6 +756,7 @@ class SxCPManSlot:
skin,
hair,
eyes,
descriptor_detail="compact",
character_cast="",
):
result = build_character_slot_json(
@@ -762,6 +772,7 @@ class SxCPManSlot:
skin=skin,
hair=hair,
eyes=eyes,
descriptor_detail=descriptor_detail,
enabled=enabled,
character_cast=character_cast or "",
)
+93 -24
View File
@@ -174,6 +174,7 @@ CHARACTER_MAN_BODY_CHOICES = [
"heavyset",
"fat",
]
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
@@ -1366,6 +1367,10 @@ def character_man_body_choices() -> list[str]:
return list(CHARACTER_MAN_BODY_CHOICES)
def character_descriptor_detail_choices() -> list[str]:
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
def character_ethnicity_choices() -> list[str]:
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
@@ -1791,6 +1796,44 @@ def _slot_value(value: Any) -> str:
return text
def _normalize_descriptor_detail(value: Any) -> str:
text = str(value or "auto").strip()
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
detail = _normalize_descriptor_detail(descriptor_detail)
if detail != "auto":
return detail
return "compact" if str(subject or "").strip().lower() == "man" else "full"
def _descriptor_from_parts(
subject: Any,
age: Any,
body_phrase: Any,
skin: Any,
hair: Any,
eyes: Any,
descriptor_detail: Any = "auto",
) -> str:
subject = str(subject or "person").strip() or "person"
age_text = " ".join(str(age or "").strip().split())
age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
if age_text in ("adult", "adults"):
age_text = ""
subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
detail = _descriptor_detail_for_subject(subject, descriptor_detail)
detail_map = {
"minimal": (body_phrase,),
"compact": (body_phrase, skin),
"medium": (body_phrase, skin, hair),
"full": (body_phrase, skin, hair, eyes),
}
pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
choice = str(choice or "").strip()
manual_value = str(manual_value or "").strip()
@@ -1839,6 +1882,7 @@ def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
"skin": _slot_value(slot.get("skin")),
"hair": _slot_value(slot.get("hair")),
"eyes": _slot_value(slot.get("eyes")),
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
}
normalized["summary"] = _character_slot_summary(normalized)
return normalized
@@ -1881,6 +1925,7 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
f"ethnicity={slot.get('ethnicity', 'random')}",
f"figure={slot.get('figure', 'random')}",
f"body={slot.get('body', 'random')}",
f"detail={slot.get('descriptor_detail', 'auto')}",
]
for key in ("body_phrase", "skin", "hair", "eyes"):
value = slot.get(key)
@@ -1902,6 +1947,7 @@ def build_character_slot_json(
skin: str = "",
hair: str = "",
eyes: str = "",
descriptor_detail: str = "auto",
enabled: bool = True,
character_cast: str | dict[str, Any] | list[Any] | None = "",
) -> dict[str, str]:
@@ -1920,6 +1966,7 @@ def build_character_slot_json(
"skin": skin,
"hair": hair,
"eyes": eyes,
"descriptor_detail": descriptor_detail,
}
)
slots = existing_slots + ([slot] if enabled else [])
@@ -2001,6 +2048,7 @@ def _context_from_character_slot(
value = _slot_value(slot.get(key))
if value:
context[key] = value
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
context["subject_type"] = subject_type
context["subject"] = subject_type
context["subject_phrase"] = subject_type
@@ -2024,7 +2072,19 @@ def _character_context_for_label(
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
for key in ("subject_type", "subject", "subject_phrase", "age", "body", "body_phrase", "skin", "hair", "eyes", "figure"):
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
):
value = context.get(key)
if value:
row[key] = value
@@ -2073,17 +2133,15 @@ def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> di
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
age = str(profile.get("age") or "").strip()
age = age.removesuffix(" adults").removesuffix(" adult").strip()
subject_phrase = f"{age} adult {subject}".strip() if age else f"adult {subject}"
pieces = [
subject_phrase,
return _descriptor_from_parts(
subject,
profile.get("age"),
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
profile.get("skin"),
profile.get("hair"),
profile.get("eyes"),
]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
profile.get("descriptor_detail"),
)
def _normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
@@ -2106,6 +2164,7 @@ def _normalize_character_profile(profile: dict[str, Any], profile_name: str = ""
"hair": str(profile.get("hair") or "").strip(),
"eyes": str(profile.get("eyes") or "").strip(),
"figure": figure,
"descriptor_detail": _normalize_descriptor_detail(profile.get("descriptor_detail")),
}
normalized["descriptor"] = _character_profile_descriptor(normalized)
return normalized
@@ -2137,6 +2196,7 @@ def build_character_profile_json(
"hair": row.get("hair") or hair,
"eyes": row.get("eyes") or eyes,
"figure": row.get("figure") or figure,
"descriptor_detail": row.get("descriptor_detail") or "auto",
}
else:
raw_profile = {
@@ -2149,7 +2209,8 @@ def build_character_profile_json(
"hair": hair,
"eyes": eyes,
"figure": figure,
}
"descriptor_detail": "auto",
}
profile = _normalize_character_profile(raw_profile, profile_name)
saved_path = ""
status = "not_saved"
@@ -2257,7 +2318,19 @@ def _apply_character_profile_to_context(
if profile["subject_type"] != context.get("subject_type"):
return context, profile, "skipped_subject_mismatch"
updated = dict(context)
for key in ("subject_type", "subject", "subject_phrase", "age", "body", "body_phrase", "skin", "hair", "eyes", "figure"):
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
):
value = profile.get(key)
if value:
updated[key] = value
@@ -3567,32 +3640,28 @@ def _insta_of_hardcore_counts(options: dict[str, Any]) -> tuple[int, int]:
def _insta_of_descriptor(row: dict[str, Any]) -> str:
age = str(row.get("age_band") or row.get("age") or "").strip()
age = " ".join(age.split())
age = age.removesuffix(" adults").removesuffix(" adult").strip()
pieces = [
f"{age} adult woman" if age else "adult woman",
return _descriptor_from_parts(
"woman",
row.get("age_band") or row.get("age"),
row.get("body_phrase"),
row.get("skin"),
row.get("hair"),
row.get("eyes"),
]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
row.get("descriptor_detail"),
)
def _insta_of_descriptor_from_context(context: dict[str, Any]) -> str:
age = str(context.get("age") or "").strip()
age = " ".join(age.split())
age = age.removesuffix(" adults").removesuffix(" adult").strip()
subject = str(context.get("subject") or context.get("subject_type") or "person").strip()
pieces = [
f"{age} adult {subject}".strip(),
return _descriptor_from_parts(
subject,
context.get("age"),
context.get("body_phrase"),
context.get("skin"),
context.get("hair"),
context.get("eyes"),
]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
context.get("descriptor_detail"),
)
def _insta_of_cast_descriptors(