Add character descriptor detail controls
This commit is contained in:
@@ -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
@@ -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 "",
|
||||
)
|
||||
|
||||
+92
-23
@@ -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,6 +2209,7 @@ def build_character_profile_json(
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"figure": figure,
|
||||
"descriptor_detail": "auto",
|
||||
}
|
||||
profile = _normalize_character_profile(raw_profile, profile_name)
|
||||
saved_path = ""
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user