from __future__ import annotations import re from typing import Any try: from . import formatter_input as input_policy from . import route_metadata as route_metadata_policy from . import sdxl_presets as sdxl_policy from .prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt except ImportError: # Allows local smoke tests with `python -c`. import formatter_input as input_policy import route_metadata as route_metadata_policy import sdxl_presets as sdxl_policy from prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt TRIGGER_CANDIDATES = ( "sxcpinup_coloredpencil", "sxcppnl7", "mythp0rt", ) SDXL_STYLE_PRESETS = sdxl_policy.SDXL_STYLE_PRESETS SDXL_QUALITY_PRESETS = sdxl_policy.SDXL_QUALITY_PRESETS SDXL_FORMATTER_PROFILES = sdxl_policy.SDXL_FORMATTER_PROFILES SDXL_DEFAULT_NEGATIVE = sdxl_policy.SDXL_DEFAULT_NEGATIVE SDXL_ACTION_FAMILY_TAGS = sdxl_policy.SDXL_ACTION_FAMILY_TAGS SDXL_POSITION_FAMILY_TAGS = sdxl_policy.SDXL_POSITION_FAMILY_TAGS PROMPT_FIELD_LABELS = input_policy.prompt_field_labels() def sdxl_style_preset_choices() -> list[str]: return sdxl_policy.sdxl_style_preset_choices() def sdxl_quality_preset_choices() -> list[str]: return sdxl_policy.sdxl_quality_preset_choices() def sdxl_formatter_profile_choices() -> list[str]: return sdxl_policy.sdxl_formatter_profile_choices() def _clean(value: Any) -> str: return input_policy.clean_text(value) def _maybe_json(text: str) -> dict[str, Any] | None: return input_policy.maybe_json(text) def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]: return input_policy.row_from_inputs(source_text, metadata_json, input_hint) def _strip_trigger(text: str, preserve_trigger: bool) -> str: return input_policy.strip_trigger_prefix(text, TRIGGER_CANDIDATES, preserve_trigger=preserve_trigger) def _split_avoid(text: str) -> tuple[str, str]: return input_policy.split_avoid(text) def _strip_prompt_field_labels(text: str) -> str: return input_policy.strip_prompt_field_labels(text, field_labels=PROMPT_FIELD_LABELS) def _prompt_field(text: str, label: str) -> str: return input_policy.prompt_field(text, label, field_labels=PROMPT_FIELD_LABELS) def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str: return input_policy.row_value(row, key, labels, field_labels=PROMPT_FIELD_LABELS) 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|visible remaining styling|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 _metadata_family_tags(row: dict[str, Any]) -> list[str]: tags: list[str] = [] action_family = route_metadata_policy.row_action_family(row) tags.extend(SDXL_ACTION_FAMILY_TAGS.get(action_family, ())) position_family = route_metadata_policy.row_position_family(row) tags.extend(SDXL_POSITION_FAMILY_TAGS.get(position_family, ())) for key in route_metadata_policy.row_position_keys(row, include_unknown=True): key_text = _clean(key) if key_text: tags.append(key_text.replace("_", " ")) return tags def _formatter_hint_tags(*rows: dict[str, Any]) -> list[str]: tags: list[str] = [] for row in rows: if not isinstance(row, dict): continue for hint in route_metadata_policy.row_formatter_hints(row, "sdxl"): hint = _clean(hint).strip(" ,.") if hint and hint not in tags: tags.append(hint) return tags 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) for tag in _metadata_family_tags(row): _add_one(tags, seen, tag) for tag in _formatter_hint_tags(row): _add(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[sdxl_policy.DEFAULT_STYLE_PRESET], ) 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[sdxl_policy.DEFAULT_QUALITY_PRESET], ) 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} for tag in _formatter_hint_tags(root): _add(tags, seen, tag) 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) for tag in _metadata_family_tags(row): _add_one(tags, seen, tag) for tag in _formatter_hint_tags(row, root): _add(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 sanitize_tag_prompt( _combine_tags( _style_prefix(style_preset, trigger, prepend_trigger, custom_style), body_tags, _quality_tail(quality_preset, custom_quality), extra_positive, ), triggers=(trigger,), ) 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 = _strip_prompt_field_labels(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 = "", formatter_profile: str = "manual_controls", ) -> dict[str, str]: style_preset, quality_preset = sdxl_policy.apply_formatter_profile( formatter_profile, style_preset=style_preset, quality_preset=quality_preset, ) 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": sanitize_negative_text( _combine_negative(SDXL_DEFAULT_NEGATIVE, selected_negative, negative_prompt, extra_negative) ), "sdxl_softcore_prompt": soft_prompt, "sdxl_hardcore_prompt": hard_prompt, "softcore_negative_prompt": sanitize_negative_text( _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("softcore_negative_prompt"), extra_negative) ), "hardcore_negative_prompt": sanitize_negative_text( _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": sanitize_negative_text( _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, }