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
|
remains available for compatibility and manual mixed use, but the gendered
|
||||||
slots are the cleaner default.
|
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
|
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:
|
||||||
|
|||||||
+11
@@ -32,6 +32,7 @@ try:
|
|||||||
category_choices,
|
category_choices,
|
||||||
character_age_choices,
|
character_age_choices,
|
||||||
character_body_choices,
|
character_body_choices,
|
||||||
|
character_descriptor_detail_choices,
|
||||||
character_ethnicity_choices,
|
character_ethnicity_choices,
|
||||||
character_figure_choices,
|
character_figure_choices,
|
||||||
character_label_choices,
|
character_label_choices,
|
||||||
@@ -75,6 +76,7 @@ except ImportError:
|
|||||||
category_choices,
|
category_choices,
|
||||||
character_age_choices,
|
character_age_choices,
|
||||||
character_body_choices,
|
character_body_choices,
|
||||||
|
character_descriptor_detail_choices,
|
||||||
character_ethnicity_choices,
|
character_ethnicity_choices,
|
||||||
character_figure_choices,
|
character_figure_choices,
|
||||||
character_label_choices,
|
character_label_choices,
|
||||||
@@ -597,6 +599,7 @@ class SxCPCharacterSlot:
|
|||||||
"skin": ("STRING", {"default": ""}),
|
"skin": ("STRING", {"default": ""}),
|
||||||
"hair": ("STRING", {"default": ""}),
|
"hair": ("STRING", {"default": ""}),
|
||||||
"eyes": ("STRING", {"default": ""}),
|
"eyes": ("STRING", {"default": ""}),
|
||||||
|
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||||
@@ -623,6 +626,7 @@ class SxCPCharacterSlot:
|
|||||||
skin,
|
skin,
|
||||||
hair,
|
hair,
|
||||||
eyes,
|
eyes,
|
||||||
|
descriptor_detail="auto",
|
||||||
character_cast="",
|
character_cast="",
|
||||||
):
|
):
|
||||||
result = build_character_slot_json(
|
result = build_character_slot_json(
|
||||||
@@ -638,6 +642,7 @@ class SxCPCharacterSlot:
|
|||||||
skin=skin,
|
skin=skin,
|
||||||
hair=hair,
|
hair=hair,
|
||||||
eyes=eyes,
|
eyes=eyes,
|
||||||
|
descriptor_detail=descriptor_detail,
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
character_cast=character_cast or "",
|
character_cast=character_cast or "",
|
||||||
)
|
)
|
||||||
@@ -661,6 +666,7 @@ class SxCPWomanSlot:
|
|||||||
"skin": ("STRING", {"default": ""}),
|
"skin": ("STRING", {"default": ""}),
|
||||||
"hair": ("STRING", {"default": ""}),
|
"hair": ("STRING", {"default": ""}),
|
||||||
"eyes": ("STRING", {"default": ""}),
|
"eyes": ("STRING", {"default": ""}),
|
||||||
|
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||||
@@ -686,6 +692,7 @@ class SxCPWomanSlot:
|
|||||||
skin,
|
skin,
|
||||||
hair,
|
hair,
|
||||||
eyes,
|
eyes,
|
||||||
|
descriptor_detail="auto",
|
||||||
character_cast="",
|
character_cast="",
|
||||||
):
|
):
|
||||||
result = build_character_slot_json(
|
result = build_character_slot_json(
|
||||||
@@ -701,6 +708,7 @@ class SxCPWomanSlot:
|
|||||||
skin=skin,
|
skin=skin,
|
||||||
hair=hair,
|
hair=hair,
|
||||||
eyes=eyes,
|
eyes=eyes,
|
||||||
|
descriptor_detail=descriptor_detail,
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
character_cast=character_cast or "",
|
character_cast=character_cast or "",
|
||||||
)
|
)
|
||||||
@@ -723,6 +731,7 @@ class SxCPManSlot:
|
|||||||
"skin": ("STRING", {"default": ""}),
|
"skin": ("STRING", {"default": ""}),
|
||||||
"hair": ("STRING", {"default": ""}),
|
"hair": ("STRING", {"default": ""}),
|
||||||
"eyes": ("STRING", {"default": ""}),
|
"eyes": ("STRING", {"default": ""}),
|
||||||
|
"descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
"character_cast": ("STRING", {"default": "", "multiline": True}),
|
||||||
@@ -747,6 +756,7 @@ class SxCPManSlot:
|
|||||||
skin,
|
skin,
|
||||||
hair,
|
hair,
|
||||||
eyes,
|
eyes,
|
||||||
|
descriptor_detail="compact",
|
||||||
character_cast="",
|
character_cast="",
|
||||||
):
|
):
|
||||||
result = build_character_slot_json(
|
result = build_character_slot_json(
|
||||||
@@ -762,6 +772,7 @@ class SxCPManSlot:
|
|||||||
skin=skin,
|
skin=skin,
|
||||||
hair=hair,
|
hair=hair,
|
||||||
eyes=eyes,
|
eyes=eyes,
|
||||||
|
descriptor_detail=descriptor_detail,
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
character_cast=character_cast or "",
|
character_cast=character_cast or "",
|
||||||
)
|
)
|
||||||
|
|||||||
+92
-23
@@ -174,6 +174,7 @@ CHARACTER_MAN_BODY_CHOICES = [
|
|||||||
"heavyset",
|
"heavyset",
|
||||||
"fat",
|
"fat",
|
||||||
]
|
]
|
||||||
|
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
|
||||||
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
||||||
|
|
||||||
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||||
@@ -1366,6 +1367,10 @@ def character_man_body_choices() -> list[str]:
|
|||||||
return list(CHARACTER_MAN_BODY_CHOICES)
|
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]:
|
def character_ethnicity_choices() -> list[str]:
|
||||||
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
|
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
|
||||||
|
|
||||||
@@ -1791,6 +1796,44 @@ def _slot_value(value: Any) -> str:
|
|||||||
return text
|
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:
|
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
||||||
choice = str(choice or "").strip()
|
choice = str(choice or "").strip()
|
||||||
manual_value = str(manual_value 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")),
|
"skin": _slot_value(slot.get("skin")),
|
||||||
"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")),
|
||||||
}
|
}
|
||||||
normalized["summary"] = _character_slot_summary(normalized)
|
normalized["summary"] = _character_slot_summary(normalized)
|
||||||
return normalized
|
return normalized
|
||||||
@@ -1881,6 +1925,7 @@ def _character_slot_summary(slot: dict[str, Any]) -> str:
|
|||||||
f"ethnicity={slot.get('ethnicity', 'random')}",
|
f"ethnicity={slot.get('ethnicity', 'random')}",
|
||||||
f"figure={slot.get('figure', 'random')}",
|
f"figure={slot.get('figure', 'random')}",
|
||||||
f"body={slot.get('body', 'random')}",
|
f"body={slot.get('body', 'random')}",
|
||||||
|
f"detail={slot.get('descriptor_detail', 'auto')}",
|
||||||
]
|
]
|
||||||
for key in ("body_phrase", "skin", "hair", "eyes"):
|
for key in ("body_phrase", "skin", "hair", "eyes"):
|
||||||
value = slot.get(key)
|
value = slot.get(key)
|
||||||
@@ -1902,6 +1947,7 @@ def build_character_slot_json(
|
|||||||
skin: str = "",
|
skin: str = "",
|
||||||
hair: str = "",
|
hair: str = "",
|
||||||
eyes: str = "",
|
eyes: str = "",
|
||||||
|
descriptor_detail: str = "auto",
|
||||||
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]:
|
||||||
@@ -1920,6 +1966,7 @@ def build_character_slot_json(
|
|||||||
"skin": skin,
|
"skin": skin,
|
||||||
"hair": hair,
|
"hair": hair,
|
||||||
"eyes": eyes,
|
"eyes": eyes,
|
||||||
|
"descriptor_detail": descriptor_detail,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
slots = existing_slots + ([slot] if enabled else [])
|
slots = existing_slots + ([slot] if enabled else [])
|
||||||
@@ -2001,6 +2048,7 @@ def _context_from_character_slot(
|
|||||||
value = _slot_value(slot.get(key))
|
value = _slot_value(slot.get(key))
|
||||||
if value:
|
if value:
|
||||||
context[key] = value
|
context[key] = value
|
||||||
|
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
|
||||||
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
|
||||||
@@ -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]:
|
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)
|
value = context.get(key)
|
||||||
if value:
|
if value:
|
||||||
row[key] = 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:
|
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||||
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
|
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
|
||||||
age = str(profile.get("age") or "").strip()
|
return _descriptor_from_parts(
|
||||||
age = age.removesuffix(" adults").removesuffix(" adult").strip()
|
subject,
|
||||||
subject_phrase = f"{age} adult {subject}".strip() if age else f"adult {subject}"
|
profile.get("age"),
|
||||||
pieces = [
|
|
||||||
subject_phrase,
|
|
||||||
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
|
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
|
||||||
profile.get("skin"),
|
profile.get("skin"),
|
||||||
profile.get("hair"),
|
profile.get("hair"),
|
||||||
profile.get("eyes"),
|
profile.get("eyes"),
|
||||||
]
|
profile.get("descriptor_detail"),
|
||||||
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
|
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(),
|
"hair": str(profile.get("hair") or "").strip(),
|
||||||
"eyes": str(profile.get("eyes") or "").strip(),
|
"eyes": str(profile.get("eyes") or "").strip(),
|
||||||
"figure": figure,
|
"figure": figure,
|
||||||
|
"descriptor_detail": _normalize_descriptor_detail(profile.get("descriptor_detail")),
|
||||||
}
|
}
|
||||||
normalized["descriptor"] = _character_profile_descriptor(normalized)
|
normalized["descriptor"] = _character_profile_descriptor(normalized)
|
||||||
return normalized
|
return normalized
|
||||||
@@ -2137,6 +2196,7 @@ def build_character_profile_json(
|
|||||||
"hair": row.get("hair") or hair,
|
"hair": row.get("hair") or hair,
|
||||||
"eyes": row.get("eyes") or eyes,
|
"eyes": row.get("eyes") or eyes,
|
||||||
"figure": row.get("figure") or figure,
|
"figure": row.get("figure") or figure,
|
||||||
|
"descriptor_detail": row.get("descriptor_detail") or "auto",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raw_profile = {
|
raw_profile = {
|
||||||
@@ -2149,6 +2209,7 @@ def build_character_profile_json(
|
|||||||
"hair": hair,
|
"hair": hair,
|
||||||
"eyes": eyes,
|
"eyes": eyes,
|
||||||
"figure": figure,
|
"figure": figure,
|
||||||
|
"descriptor_detail": "auto",
|
||||||
}
|
}
|
||||||
profile = _normalize_character_profile(raw_profile, profile_name)
|
profile = _normalize_character_profile(raw_profile, profile_name)
|
||||||
saved_path = ""
|
saved_path = ""
|
||||||
@@ -2257,7 +2318,19 @@ def _apply_character_profile_to_context(
|
|||||||
if profile["subject_type"] != context.get("subject_type"):
|
if profile["subject_type"] != context.get("subject_type"):
|
||||||
return context, profile, "skipped_subject_mismatch"
|
return context, profile, "skipped_subject_mismatch"
|
||||||
updated = dict(context)
|
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)
|
value = profile.get(key)
|
||||||
if value:
|
if value:
|
||||||
updated[key] = 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:
|
def _insta_of_descriptor(row: dict[str, Any]) -> str:
|
||||||
age = str(row.get("age_band") or row.get("age") or "").strip()
|
return _descriptor_from_parts(
|
||||||
age = " ".join(age.split())
|
"woman",
|
||||||
age = age.removesuffix(" adults").removesuffix(" adult").strip()
|
row.get("age_band") or row.get("age"),
|
||||||
pieces = [
|
|
||||||
f"{age} adult woman" if age else "adult woman",
|
|
||||||
row.get("body_phrase"),
|
row.get("body_phrase"),
|
||||||
row.get("skin"),
|
row.get("skin"),
|
||||||
row.get("hair"),
|
row.get("hair"),
|
||||||
row.get("eyes"),
|
row.get("eyes"),
|
||||||
]
|
row.get("descriptor_detail"),
|
||||||
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
)
|
||||||
|
|
||||||
|
|
||||||
def _insta_of_descriptor_from_context(context: dict[str, Any]) -> str:
|
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()
|
subject = str(context.get("subject") or context.get("subject_type") or "person").strip()
|
||||||
pieces = [
|
return _descriptor_from_parts(
|
||||||
f"{age} adult {subject}".strip(),
|
subject,
|
||||||
|
context.get("age"),
|
||||||
context.get("body_phrase"),
|
context.get("body_phrase"),
|
||||||
context.get("skin"),
|
context.get("skin"),
|
||||||
context.get("hair"),
|
context.get("hair"),
|
||||||
context.get("eyes"),
|
context.get("eyes"),
|
||||||
]
|
context.get("descriptor_detail"),
|
||||||
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
)
|
||||||
|
|
||||||
|
|
||||||
def _insta_of_cast_descriptors(
|
def _insta_of_cast_descriptors(
|
||||||
|
|||||||
Reference in New Issue
Block a user