From b4abd24b35407578b5c2763ba50b66eae0b08cfe Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 25 Jun 2026 11:23:36 +0200 Subject: [PATCH] Add SDXL prompt formatter --- __init__.py | 96 ++++++++ sdxl_formatter.py | 543 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 639 insertions(+) create mode 100644 sdxl_formatter.py diff --git a/__init__.py b/__init__.py index 242ff0b..9f65bd1 100644 --- a/__init__.py +++ b/__init__.py @@ -254,6 +254,14 @@ NODE_INPUT_TOOLTIPS = { "SxCPKrea2Formatter": { "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": { "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.", @@ -413,6 +421,7 @@ try: ) from .caption_naturalizer import naturalize_caption from .krea_formatter import format_krea2_prompt + from .sdxl_formatter import format_sdxl_prompt, sdxl_quality_preset_choices, sdxl_style_preset_choices except ImportError: from loop_nodes import ( ANY_TYPE, @@ -489,6 +498,7 @@ except ImportError: ) from caption_naturalizer import naturalize_caption 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: @@ -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: @classmethod def INPUT_TYPES(cls): @@ -2399,6 +2493,7 @@ NODE_CLASS_MAPPINGS = { "SxCPCharacterProfileLoad": SxCPCharacterProfileLoad, "SxCPCaptionNaturalizer": SxCPCaptionNaturalizer, "SxCPKrea2Formatter": SxCPKrea2Formatter, + "SxCPSDXLFormatter": SxCPSDXLFormatter, "SxCPInstaOFOptions": SxCPInstaOFOptions, "SxCPInstaOFPromptPair": SxCPInstaOFPromptPair, } @@ -2438,6 +2533,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "SxCPCharacterProfileLoad": "SxCP Character Profile Load", "SxCPCaptionNaturalizer": "SxCP Caption Naturalizer", "SxCPKrea2Formatter": "SxCP Krea2 Formatter", + "SxCPSDXLFormatter": "SxCP SDXL Formatter", "SxCPInstaOFOptions": "SxCP Insta/OF Options", "SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair", } diff --git a/sdxl_formatter.py b/sdxl_formatter.py new file mode 100644 index 0000000..100fb7d --- /dev/null +++ b/sdxl_formatter.py @@ -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, + }