Add SDXL prompt formatter
This commit is contained in:
+96
@@ -254,6 +254,14 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"SxCPKrea2Formatter": {
|
"SxCPKrea2Formatter": {
|
||||||
"metadata_json": "Best input for Krea2 formatting because it preserves cast, camera, and hardcore action metadata.",
|
"metadata_json": "Best input for Krea2 formatting because it preserves cast, camera, and hardcore action metadata.",
|
||||||
},
|
},
|
||||||
|
"SxCPSDXLFormatter": {
|
||||||
|
"metadata_json": "Best input for SDXL tag formatting because it preserves cast, camera, outfit, and explicit action metadata.",
|
||||||
|
"style_preset": "Positive style anchor preset. flat_vector_pony matches the old SDXL tag style.",
|
||||||
|
"quality_preset": "Quality/score tag tail for SDXL or Pony-style checkpoints.",
|
||||||
|
"custom_style": "Optional replacement for the style preset. Leave empty to use style_preset.",
|
||||||
|
"custom_quality": "Optional replacement for the quality preset. Leave empty to use quality_preset.",
|
||||||
|
"nude_weight": "Weight used when explicit nude/body exposure tags are inferred.",
|
||||||
|
},
|
||||||
"SxCPCaptionNaturalizer": {
|
"SxCPCaptionNaturalizer": {
|
||||||
"style_policy": "drop_style_tail removes training/style boilerplate; keep_style_terms preserves more of it.",
|
"style_policy": "drop_style_tail removes training/style boilerplate; keep_style_terms preserves more of it.",
|
||||||
"include_trigger": "Add the naturalizer trigger to the rewritten caption.",
|
"include_trigger": "Add the naturalizer trigger to the rewritten caption.",
|
||||||
@@ -413,6 +421,7 @@ try:
|
|||||||
)
|
)
|
||||||
from .caption_naturalizer import naturalize_caption
|
from .caption_naturalizer import naturalize_caption
|
||||||
from .krea_formatter import format_krea2_prompt
|
from .krea_formatter import format_krea2_prompt
|
||||||
|
from .sdxl_formatter import format_sdxl_prompt, sdxl_quality_preset_choices, sdxl_style_preset_choices
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from loop_nodes import (
|
from loop_nodes import (
|
||||||
ANY_TYPE,
|
ANY_TYPE,
|
||||||
@@ -489,6 +498,7 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
from caption_naturalizer import naturalize_caption
|
from caption_naturalizer import naturalize_caption
|
||||||
from krea_formatter import format_krea2_prompt
|
from krea_formatter import format_krea2_prompt
|
||||||
|
from sdxl_formatter import format_sdxl_prompt, sdxl_quality_preset_choices, sdxl_style_preset_choices
|
||||||
|
|
||||||
|
|
||||||
if PromptServer is not None and web is not None:
|
if PromptServer is not None and web is not None:
|
||||||
@@ -2192,6 +2202,90 @@ class SxCPKrea2Formatter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPSDXLFormatter:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"source_text": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"input_hint": (["auto", "metadata_json", "prompt"], {"default": "auto"}),
|
||||||
|
"target": (["auto", "single", "softcore", "hardcore"], {"default": "auto"}),
|
||||||
|
"style_preset": (sdxl_style_preset_choices(), {"default": "flat_vector_pony"}),
|
||||||
|
"quality_preset": (sdxl_quality_preset_choices(), {"default": "pony_high"}),
|
||||||
|
"trigger": ("STRING", {"default": "mythp0rt", "multiline": False}),
|
||||||
|
"prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}),
|
||||||
|
"preserve_trigger": ("BOOLEAN", {"default": False}),
|
||||||
|
"nude_weight": ("FLOAT", {"default": 1.29, "min": 0.1, "max": 3.0, "step": 0.01}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"metadata_json": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"negative_prompt": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"custom_style": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"custom_quality": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"extra_positive": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
"extra_negative": ("STRING", {"default": "", "multiline": True}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = (
|
||||||
|
"sdxl_prompt",
|
||||||
|
"negative_prompt",
|
||||||
|
"sdxl_softcore_prompt",
|
||||||
|
"sdxl_hardcore_prompt",
|
||||||
|
"softcore_negative_prompt",
|
||||||
|
"hardcore_negative_prompt",
|
||||||
|
"method",
|
||||||
|
)
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
source_text,
|
||||||
|
input_hint,
|
||||||
|
target,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
trigger,
|
||||||
|
prepend_trigger_to_prompt,
|
||||||
|
preserve_trigger,
|
||||||
|
nude_weight,
|
||||||
|
metadata_json="",
|
||||||
|
negative_prompt="",
|
||||||
|
custom_style="",
|
||||||
|
custom_quality="",
|
||||||
|
extra_positive="",
|
||||||
|
extra_negative="",
|
||||||
|
):
|
||||||
|
row = format_sdxl_prompt(
|
||||||
|
source_text=source_text or "",
|
||||||
|
metadata_json=metadata_json or "",
|
||||||
|
negative_prompt=negative_prompt or "",
|
||||||
|
input_hint=input_hint,
|
||||||
|
target=target,
|
||||||
|
style_preset=style_preset,
|
||||||
|
quality_preset=quality_preset,
|
||||||
|
trigger=trigger,
|
||||||
|
prepend_trigger=prepend_trigger_to_prompt,
|
||||||
|
preserve_trigger=preserve_trigger,
|
||||||
|
nude_weight=nude_weight,
|
||||||
|
custom_style=custom_style or "",
|
||||||
|
custom_quality=custom_quality or "",
|
||||||
|
extra_positive=extra_positive or "",
|
||||||
|
extra_negative=extra_negative or "",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
row["sdxl_prompt"],
|
||||||
|
row["negative_prompt"],
|
||||||
|
row["sdxl_softcore_prompt"],
|
||||||
|
row["sdxl_hardcore_prompt"],
|
||||||
|
row["softcore_negative_prompt"],
|
||||||
|
row["hardcore_negative_prompt"],
|
||||||
|
row["method"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SxCPInstaOFOptions:
|
class SxCPInstaOFOptions:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -2399,6 +2493,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SxCPCharacterProfileLoad": SxCPCharacterProfileLoad,
|
"SxCPCharacterProfileLoad": SxCPCharacterProfileLoad,
|
||||||
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
|
||||||
"SxCPKrea2Formatter": SxCPKrea2Formatter,
|
"SxCPKrea2Formatter": SxCPKrea2Formatter,
|
||||||
|
"SxCPSDXLFormatter": SxCPSDXLFormatter,
|
||||||
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
"SxCPInstaOFOptions": SxCPInstaOFOptions,
|
||||||
"SxCPInstaOFPromptPair": SxCPInstaOFPromptPair,
|
"SxCPInstaOFPromptPair": SxCPInstaOFPromptPair,
|
||||||
}
|
}
|
||||||
@@ -2438,6 +2533,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SxCPCharacterProfileLoad": "SxCP Character Profile Load",
|
"SxCPCharacterProfileLoad": "SxCP Character Profile Load",
|
||||||
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
|
||||||
"SxCPKrea2Formatter": "SxCP Krea2 Formatter",
|
"SxCPKrea2Formatter": "SxCP Krea2 Formatter",
|
||||||
|
"SxCPSDXLFormatter": "SxCP SDXL Formatter",
|
||||||
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
|
||||||
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,543 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGER_CANDIDATES = (
|
||||||
|
"sxcpinup_coloredpencil",
|
||||||
|
"sxcppnl7",
|
||||||
|
"mythp0rt",
|
||||||
|
)
|
||||||
|
|
||||||
|
SDXL_STYLE_PRESETS = {
|
||||||
|
"flat_vector_pony": "(skindentation:1.25), (flat color:2.0), no lineart, no outline, Flat vector",
|
||||||
|
"flat_vector": "(flat color:2.0), no lineart, no outline, Flat vector",
|
||||||
|
"photographic": "realistic photo, detailed skin texture, depth of field",
|
||||||
|
"none": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
SDXL_QUALITY_PRESETS = {
|
||||||
|
"pony_high": (
|
||||||
|
"amazing quality, ultra detailed, 8k, very detailed, high detailed texture, "
|
||||||
|
"highly detailed anatomy, best quality, newest, very aesthetic, (score_9:1.1), "
|
||||||
|
"(score_8_up:1.1), (score_7_up:1.1), masterpiece, absurdres, highres"
|
||||||
|
),
|
||||||
|
"sdxl_high": "masterpiece, best quality, amazing quality, ultra detailed, 8k, absurdres, highres",
|
||||||
|
"none": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
SDXL_DEFAULT_NEGATIVE = (
|
||||||
|
"worst quality, low quality, normal quality, lowres, bad anatomy, bad hands, "
|
||||||
|
"extra fingers, missing fingers, fused fingers, deformed, disfigured, malformed body, "
|
||||||
|
"watermark, signature, text, logo, blurry, jpeg artifacts, censored, mosaic censor"
|
||||||
|
)
|
||||||
|
|
||||||
|
PROMPT_FIELD_LABELS = (
|
||||||
|
"Ages",
|
||||||
|
"Body types",
|
||||||
|
"Cast",
|
||||||
|
"Cast descriptors",
|
||||||
|
"Characters",
|
||||||
|
"Scene",
|
||||||
|
"Setting",
|
||||||
|
"Pose",
|
||||||
|
"Sexual pose",
|
||||||
|
"Sexual scene",
|
||||||
|
"Facial expression",
|
||||||
|
"Facial expressions",
|
||||||
|
"Clothing",
|
||||||
|
"Erotic outfit",
|
||||||
|
"Composition",
|
||||||
|
"Role graph",
|
||||||
|
"Camera control",
|
||||||
|
"Use",
|
||||||
|
"Avoid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sdxl_style_preset_choices() -> list[str]:
|
||||||
|
return list(SDXL_STYLE_PRESETS)
|
||||||
|
|
||||||
|
|
||||||
|
def sdxl_quality_preset_choices() -> list[str]:
|
||||||
|
return list(SDXL_QUALITY_PRESETS)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(value: Any) -> str:
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_json(text: str) -> dict[str, Any] | None:
|
||||||
|
text = _clean(text)
|
||||||
|
if not text.startswith("{"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
return value if isinstance(value, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]:
|
||||||
|
if input_hint in ("auto", "metadata_json"):
|
||||||
|
for text, method in ((metadata_json, "metadata_json"), (source_text, "source_json")):
|
||||||
|
row = _maybe_json(text)
|
||||||
|
if row is not None:
|
||||||
|
return row, method
|
||||||
|
return None, "text"
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_trigger(text: str, preserve_trigger: bool) -> str:
|
||||||
|
text = _clean(text)
|
||||||
|
if preserve_trigger:
|
||||||
|
return text
|
||||||
|
for trigger in TRIGGER_CANDIDATES:
|
||||||
|
if text.lower().startswith(trigger.lower() + ","):
|
||||||
|
return text[len(trigger) + 1 :].strip(" ,")
|
||||||
|
if text.lower().startswith(trigger.lower() + "."):
|
||||||
|
return text[len(trigger) + 1 :].strip(" ,")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _split_avoid(text: str) -> tuple[str, str]:
|
||||||
|
match = re.search(r"\bAvoid:\s*(.*)$", text)
|
||||||
|
if not match:
|
||||||
|
return text, ""
|
||||||
|
return text[: match.start()].strip(" ."), match.group(1).strip(" .")
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt_field(text: str, label: str) -> str:
|
||||||
|
text = _clean(text)
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
labels = "|".join(re.escape(name) for name in PROMPT_FIELD_LABELS)
|
||||||
|
pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)"
|
||||||
|
match = re.search(pattern, text)
|
||||||
|
if not match:
|
||||||
|
return ""
|
||||||
|
return _clean(match.group(1)).rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
|
def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str:
|
||||||
|
value = _clean(row.get(key, ""))
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
prompt = _clean(row.get("prompt", ""))
|
||||||
|
for label in labels:
|
||||||
|
value = _prompt_field(prompt, label)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _split_tag_text(text: Any) -> list[str]:
|
||||||
|
text = _clean(text)
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
text = re.sub(r"\bWoman [A-Z]'s\b", "woman's", text)
|
||||||
|
text = re.sub(r"\bMan [A-Z]'s\b", "man's", text)
|
||||||
|
text = re.sub(r"\bWoman [A-Z]\b", "woman", text)
|
||||||
|
text = re.sub(r"\bMan [A-Z]\b", "man", text)
|
||||||
|
text = re.sub(
|
||||||
|
r"\b(?:Clothing state|Visual clothing state|teaser outfit detail|softcore visual reference|Sexual scene|Role graph):\s*",
|
||||||
|
"",
|
||||||
|
text,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
text = re.sub(r"\b(?:and|with)\b", ",", text, flags=re.IGNORECASE)
|
||||||
|
parts = re.split(r"\s*[,;]\s*", text)
|
||||||
|
return [_clean(part).strip(" .") for part in parts if _clean(part).strip(" .")]
|
||||||
|
|
||||||
|
|
||||||
|
def _tag_key(tag: str) -> str:
|
||||||
|
text = _clean(tag).lower()
|
||||||
|
text = re.sub(r"^\((.*?):[0-9.]+\)$", r"\1", text)
|
||||||
|
text = text.strip("() ")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _add(tags: list[str], seen: set[str], value: Any) -> None:
|
||||||
|
for tag in _split_tag_text(value):
|
||||||
|
key = _tag_key(tag)
|
||||||
|
if key and key not in seen:
|
||||||
|
tags.append(tag)
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_one(tags: list[str], seen: set[str], tag: str) -> None:
|
||||||
|
tag = _clean(tag).strip(" ,")
|
||||||
|
key = _tag_key(tag)
|
||||||
|
if tag and key and key not in seen:
|
||||||
|
tags.append(tag)
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
|
||||||
|
def _combine_tags(*parts: Any) -> str:
|
||||||
|
tags: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for part in parts:
|
||||||
|
_add(tags, seen, part)
|
||||||
|
return ", ".join(tags)
|
||||||
|
|
||||||
|
|
||||||
|
def _combine_negative(*parts: Any) -> str:
|
||||||
|
return _combine_tags(*(part for part in parts if _clean(part)))
|
||||||
|
|
||||||
|
|
||||||
|
def _count_tag(women_count: int = 0, men_count: int = 0) -> list[str]:
|
||||||
|
tags = []
|
||||||
|
if women_count > 0:
|
||||||
|
tags.append(f"{women_count}woman" if women_count == 1 else f"{women_count}women")
|
||||||
|
if men_count > 0:
|
||||||
|
tags.append(f"{men_count}man" if men_count == 1 else f"{men_count}men")
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def _infer_counts(row: dict[str, Any]) -> tuple[int, int]:
|
||||||
|
try:
|
||||||
|
women = int(row.get("women_count") or 0)
|
||||||
|
men = int(row.get("men_count") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
women = men = 0
|
||||||
|
if women or men:
|
||||||
|
return women, men
|
||||||
|
subject = _clean(row.get("subject_type") or row.get("primary_subject")).lower()
|
||||||
|
phrase = _clean(row.get("subject_phrase")).lower()
|
||||||
|
text = f"{subject} {phrase}"
|
||||||
|
if "two women" in text:
|
||||||
|
return 2, 0
|
||||||
|
if "two men" in text:
|
||||||
|
return 0, 2
|
||||||
|
if "woman and" in text or "woman a" in text and "man a" in text:
|
||||||
|
return 1, 1
|
||||||
|
if "group" in text:
|
||||||
|
return 2, 2
|
||||||
|
if "man" in text and "woman" not in text:
|
||||||
|
return 0, 1
|
||||||
|
return 1, 0
|
||||||
|
|
||||||
|
|
||||||
|
def _character_tags_from_descriptor(descriptor: Any) -> list[str]:
|
||||||
|
text = _clean(descriptor)
|
||||||
|
text = re.sub(r"\bWoman [A-Z]\s*/\s*primary creator:\s*", "", text)
|
||||||
|
text = re.sub(r"\b(?:Woman|Man) [A-Z]:\s*", "", text)
|
||||||
|
text = re.sub(r"\balongside\b", ",", text, flags=re.IGNORECASE)
|
||||||
|
parts = _split_tag_text(text)
|
||||||
|
cleaned = []
|
||||||
|
for part in parts:
|
||||||
|
part = re.sub(r"\bfigure\b", "build", part, flags=re.IGNORECASE)
|
||||||
|
part = part.replace("adult adult", "adult")
|
||||||
|
cleaned.append(part)
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
def _normal_character_tags(row: dict[str, Any]) -> list[str]:
|
||||||
|
descriptor = (
|
||||||
|
_clean(row.get("cast_descriptor_text"))
|
||||||
|
or _prompt_field(row.get("prompt", ""), "Characters")
|
||||||
|
or _prompt_field(row.get("prompt", ""), "Cast descriptors")
|
||||||
|
)
|
||||||
|
if descriptor:
|
||||||
|
return _character_tags_from_descriptor(descriptor)
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
_clean(row.get("age") or row.get("age_band")),
|
||||||
|
_clean(row.get("subject_phrase") or row.get("subject_type") or row.get("primary_subject")),
|
||||||
|
_clean(row.get("body_phrase") or row.get("body") or row.get("body_type")),
|
||||||
|
_clean(row.get("skin")),
|
||||||
|
_clean(row.get("hair")),
|
||||||
|
_clean(row.get("eyes")),
|
||||||
|
]
|
||||||
|
return [part for part in parts if part and part not in ("woman", "man", "single_any")]
|
||||||
|
|
||||||
|
|
||||||
|
def _camera_tags_from_config(config: Any) -> list[str]:
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
return []
|
||||||
|
if _clean(config.get("camera_detail")) == "off" or _clean(config.get("camera_mode")) == "disabled":
|
||||||
|
return []
|
||||||
|
custom = _clean(config.get("custom_camera_prompt"))
|
||||||
|
tags = _split_tag_text(custom)
|
||||||
|
direction = _clean(config.get("orbit_direction"))
|
||||||
|
elevation = _clean(config.get("orbit_elevation_label"))
|
||||||
|
distance = _clean(config.get("orbit_distance_label"))
|
||||||
|
for value in (direction, elevation, distance):
|
||||||
|
if value and value != "auto":
|
||||||
|
tags.extend(_split_tag_text(value))
|
||||||
|
for key in ("angle", "shot_size", "distance", "lens", "orientation", "subject_focus"):
|
||||||
|
value = _clean(config.get(key)).replace("_", " ")
|
||||||
|
if value and value != "auto":
|
||||||
|
tags.append(value)
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def _camera_tags(row: dict[str, Any], directive: Any = "", config: Any = None) -> list[str]:
|
||||||
|
tags = _split_tag_text(directive)
|
||||||
|
tags.extend(_camera_tags_from_config(config if config is not None else row.get("camera_config")))
|
||||||
|
camera_directive = _clean(row.get("camera_directive"))
|
||||||
|
if camera_directive:
|
||||||
|
tags.extend(_split_tag_text(camera_directive))
|
||||||
|
out = []
|
||||||
|
for tag in tags:
|
||||||
|
tag = tag.replace("0-degree front view", "(front facing:1.15)")
|
||||||
|
tag = tag.replace("front view", "(front facing:1.15)")
|
||||||
|
tag = tag.replace("right side view", "side view")
|
||||||
|
tag = tag.replace("left side view", "side view")
|
||||||
|
out.append(tag)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _explicit_tags(text: str, nude_weight: float) -> list[str]:
|
||||||
|
lower = text.lower()
|
||||||
|
tags: list[str] = []
|
||||||
|
if any(token in lower for token in ("fully nude", "fully exposed", "naked", "bare skin unobstructed", "explicit_nude")):
|
||||||
|
tags.append(f"(naked:{nude_weight:.2f})")
|
||||||
|
if any(token in lower for token in ("nipples", "breasts exposed", "bare breasts", "nipple")):
|
||||||
|
tags.append("nipples")
|
||||||
|
if any(token in lower for token in ("pussy", "vulva", "genitals")):
|
||||||
|
tags.append("pussy")
|
||||||
|
if any(token in lower for token in ("penis", "cock")):
|
||||||
|
tags.append("penis")
|
||||||
|
if "penetration" in lower or "thrust" in lower:
|
||||||
|
tags.append("penetration")
|
||||||
|
if "vaginal" in lower:
|
||||||
|
tags.append("pussy")
|
||||||
|
if "oral" in lower or "mouth" in lower:
|
||||||
|
tags.append("oral sex")
|
||||||
|
if "anal" in lower:
|
||||||
|
tags.append("anal sex")
|
||||||
|
if any(token in lower for token in ("semen", "ejaculates", "cum ")):
|
||||||
|
tags.append("semen")
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def _row_core_tags(row: dict[str, Any], nude_weight: float) -> list[str]:
|
||||||
|
tags: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
women, men = _infer_counts(row)
|
||||||
|
for tag in _count_tag(women, men):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
|
||||||
|
for tag in _normal_character_tags(row):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
|
||||||
|
item = _row_value(row, "item", ("Sexual scene", "Sexual pose", "Erotic outfit", "Clothing")) or _clean(row.get("custom_item"))
|
||||||
|
pose = _row_value(row, "pose", ("Sexual pose", "Pose"))
|
||||||
|
role_graph = _clean(row.get("source_role_graph") or row.get("role_graph"))
|
||||||
|
scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene"))
|
||||||
|
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
||||||
|
composition = _row_value(row, "composition", ("Composition",))
|
||||||
|
for value in (
|
||||||
|
item,
|
||||||
|
pose,
|
||||||
|
role_graph,
|
||||||
|
scene and f"in {scene}",
|
||||||
|
expression,
|
||||||
|
composition,
|
||||||
|
):
|
||||||
|
_add(tags, seen, value)
|
||||||
|
for tag in _camera_tags(row):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
|
||||||
|
combined = " ".join(_clean(value) for value in (item, pose, role_graph, row.get("prompt", "")))
|
||||||
|
for tag in _explicit_tags(combined, nude_weight):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def _style_prefix(style_preset: str, trigger: str, prepend_trigger: bool, custom_style: str) -> str:
|
||||||
|
style = custom_style if _clean(custom_style) else SDXL_STYLE_PRESETS.get(style_preset, SDXL_STYLE_PRESETS["flat_vector_pony"])
|
||||||
|
trigger = _clean(trigger)
|
||||||
|
if prepend_trigger and trigger:
|
||||||
|
return _combine_tags(style, trigger)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
def _quality_tail(quality_preset: str, custom_quality: str) -> str:
|
||||||
|
return _clean(custom_quality) or SDXL_QUALITY_PRESETS.get(quality_preset, SDXL_QUALITY_PRESETS["pony_high"])
|
||||||
|
|
||||||
|
|
||||||
|
def _soft_tags(row: dict[str, Any], root: dict[str, Any], nude_weight: float) -> str:
|
||||||
|
tags = _row_core_tags(row, nude_weight)
|
||||||
|
seen = {_tag_key(tag) for tag in tags}
|
||||||
|
descriptor = _clean(root.get("shared_descriptor"))
|
||||||
|
if descriptor and not any("woman" in _tag_key(tag) for tag in tags):
|
||||||
|
for tag in _character_tags_from_descriptor(descriptor):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
partner = root.get("softcore_partner_styling")
|
||||||
|
if isinstance(partner, dict):
|
||||||
|
_add(tags, seen, "; ".join(_clean(item) for item in partner.get("outfits", []) if _clean(item)))
|
||||||
|
_add(tags, seen, partner.get("pose"))
|
||||||
|
_add_one(tags, seen, "sexy")
|
||||||
|
_add_one(tags, seen, "looking at viewer")
|
||||||
|
return ", ".join(tags)
|
||||||
|
|
||||||
|
|
||||||
|
def _hard_tags(row: dict[str, Any], root: dict[str, Any], nude_weight: float) -> str:
|
||||||
|
tags: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
try:
|
||||||
|
women = int(root.get("hardcore_women_count") or row.get("women_count") or 1)
|
||||||
|
men = int(root.get("hardcore_men_count") or row.get("men_count") or 1)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
women, men = 1, 1
|
||||||
|
for tag in _count_tag(women, men):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
|
||||||
|
descriptors = root.get("shared_cast_descriptors")
|
||||||
|
if isinstance(descriptors, list):
|
||||||
|
for descriptor in descriptors:
|
||||||
|
for tag in _character_tags_from_descriptor(descriptor):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
else:
|
||||||
|
for tag in _normal_character_tags(row):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
|
||||||
|
hard_scene = _clean(row.get("scene_text"))
|
||||||
|
hard_item = _clean(row.get("item"))
|
||||||
|
hard_role = _clean(row.get("source_role_graph") or row.get("role_graph"))
|
||||||
|
hard_clothing = _clean(root.get("hardcore_clothing_state"))
|
||||||
|
expression = _clean(row.get("character_expression_text") or row.get("expression"))
|
||||||
|
composition = _clean(row.get("composition"))
|
||||||
|
for value in (
|
||||||
|
hard_role,
|
||||||
|
hard_item,
|
||||||
|
hard_clothing,
|
||||||
|
hard_scene and f"in {hard_scene}",
|
||||||
|
expression,
|
||||||
|
composition,
|
||||||
|
):
|
||||||
|
_add(tags, seen, value)
|
||||||
|
for tag in _camera_tags(row, root.get("hardcore_camera_directive"), root.get("hardcore_camera_config")):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
combined = " ".join([hard_role, hard_item, hard_clothing, expression, composition, root.get("hardcore_prompt", "") or ""])
|
||||||
|
for tag in _explicit_tags(combined, nude_weight):
|
||||||
|
_add_one(tags, seen, tag)
|
||||||
|
return ", ".join(tags)
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_prompt(
|
||||||
|
body_tags: str,
|
||||||
|
style_preset: str,
|
||||||
|
quality_preset: str,
|
||||||
|
trigger: str,
|
||||||
|
prepend_trigger: bool,
|
||||||
|
custom_style: str,
|
||||||
|
custom_quality: str,
|
||||||
|
extra_positive: str,
|
||||||
|
) -> str:
|
||||||
|
return _combine_tags(
|
||||||
|
_style_prefix(style_preset, trigger, prepend_trigger, custom_style),
|
||||||
|
body_tags,
|
||||||
|
_quality_tail(quality_preset, custom_quality),
|
||||||
|
extra_positive,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fallback_text_to_sdxl(
|
||||||
|
source_text: str,
|
||||||
|
preserve_trigger: bool,
|
||||||
|
nude_weight: float,
|
||||||
|
) -> tuple[str, str, str]:
|
||||||
|
positive, negative = _split_avoid(_strip_trigger(source_text, preserve_trigger))
|
||||||
|
positive = re.sub(
|
||||||
|
r"\b(?:Scene|Setting|Pose|Sexual pose|Sexual scene|Facial expressions?|Composition|Role graph|Camera control):\s*",
|
||||||
|
"",
|
||||||
|
positive,
|
||||||
|
)
|
||||||
|
tags = _combine_tags(positive, ", ".join(_explicit_tags(positive, nude_weight)))
|
||||||
|
return tags, negative, "text(fallback)"
|
||||||
|
|
||||||
|
|
||||||
|
def format_sdxl_prompt(
|
||||||
|
source_text: str,
|
||||||
|
metadata_json: str = "",
|
||||||
|
negative_prompt: str = "",
|
||||||
|
input_hint: str = "auto",
|
||||||
|
target: str = "auto",
|
||||||
|
style_preset: str = "flat_vector_pony",
|
||||||
|
quality_preset: str = "pony_high",
|
||||||
|
trigger: str = "mythp0rt",
|
||||||
|
prepend_trigger: bool = True,
|
||||||
|
preserve_trigger: bool = False,
|
||||||
|
nude_weight: float = 1.29,
|
||||||
|
custom_style: str = "",
|
||||||
|
custom_quality: str = "",
|
||||||
|
extra_positive: str = "",
|
||||||
|
extra_negative: str = "",
|
||||||
|
) -> dict[str, str]:
|
||||||
|
style_preset = style_preset if style_preset in SDXL_STYLE_PRESETS else "flat_vector_pony"
|
||||||
|
quality_preset = quality_preset if quality_preset in SDXL_QUALITY_PRESETS else "pony_high"
|
||||||
|
target = target if target in ("auto", "single", "softcore", "hardcore") else "auto"
|
||||||
|
nude_weight = max(0.1, min(3.0, float(nude_weight)))
|
||||||
|
row, method = _row_from_inputs(source_text, metadata_json, input_hint)
|
||||||
|
|
||||||
|
if row and row.get("mode") == "Insta/OF":
|
||||||
|
soft_row = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
|
||||||
|
hard_row = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
|
||||||
|
soft_body = _soft_tags(soft_row, row, nude_weight)
|
||||||
|
hard_body = _hard_tags(hard_row, row, nude_weight)
|
||||||
|
soft_prompt = _assemble_prompt(
|
||||||
|
soft_body,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
trigger,
|
||||||
|
prepend_trigger,
|
||||||
|
custom_style,
|
||||||
|
custom_quality,
|
||||||
|
extra_positive,
|
||||||
|
)
|
||||||
|
hard_prompt = _assemble_prompt(
|
||||||
|
hard_body,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
trigger,
|
||||||
|
prepend_trigger,
|
||||||
|
custom_style,
|
||||||
|
custom_quality,
|
||||||
|
extra_positive,
|
||||||
|
)
|
||||||
|
selected = hard_prompt if target == "hardcore" else soft_prompt
|
||||||
|
selected_negative = row.get("hardcore_negative_prompt") if target == "hardcore" else row.get("softcore_negative_prompt")
|
||||||
|
return {
|
||||||
|
"sdxl_prompt": selected,
|
||||||
|
"negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, selected_negative, negative_prompt, extra_negative),
|
||||||
|
"sdxl_softcore_prompt": soft_prompt,
|
||||||
|
"sdxl_hardcore_prompt": hard_prompt,
|
||||||
|
"softcore_negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("softcore_negative_prompt"), extra_negative),
|
||||||
|
"hardcore_negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("hardcore_negative_prompt"), extra_negative),
|
||||||
|
"method": f"{method}:sdxl(insta_of_pair)",
|
||||||
|
}
|
||||||
|
|
||||||
|
if row:
|
||||||
|
body = ", ".join(_row_core_tags(row, nude_weight))
|
||||||
|
extracted_negative = _clean(row.get("negative_prompt"))
|
||||||
|
method = f"{method}:sdxl(metadata)"
|
||||||
|
else:
|
||||||
|
body, extracted_negative, method = _fallback_text_to_sdxl(source_text, preserve_trigger, nude_weight)
|
||||||
|
|
||||||
|
prompt = _assemble_prompt(
|
||||||
|
body,
|
||||||
|
style_preset,
|
||||||
|
quality_preset,
|
||||||
|
trigger,
|
||||||
|
prepend_trigger,
|
||||||
|
custom_style,
|
||||||
|
custom_quality,
|
||||||
|
extra_positive,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"sdxl_prompt": prompt,
|
||||||
|
"negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, extracted_negative, negative_prompt, extra_negative),
|
||||||
|
"sdxl_softcore_prompt": "",
|
||||||
|
"sdxl_hardcore_prompt": "",
|
||||||
|
"softcore_negative_prompt": "",
|
||||||
|
"hardcore_negative_prompt": "",
|
||||||
|
"method": method,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user