from __future__ import annotations import json from typing import Any STYLE_CONFIG_SCHEMA = "sxcp_style_config_v1" STYLE_COMBINE_MODES = ("replace", "add", "disabled") STYLE_PRESETS = { "category_default": { "style": "", "positive_suffix": "", "summary": "category default style", }, "realistic_photo": { "style": "realistic adult photographic scene, natural camera capture", "positive_suffix": "Use realistic skin texture, natural light response, coherent anatomy, readable contact points, and believable spatial depth.", "summary": "realistic photographic style", }, "creator_phone_photo": { "style": "realistic creator-shot phone photo, natural adult social-media image", "positive_suffix": "Use handheld camera realism, natural skin texture, readable body positioning, and believable room depth.", "summary": "creator phone photo style", }, "documentary_flash": { "style": "realistic direct-flash documentary photo, raw adult snapshot", "positive_suffix": "Use direct flash, natural skin texture, sharp foreground detail, visible contact points, and unpolished camera realism.", "summary": "direct flash documentary style", }, "cinematic_realism": { "style": "cinematic realistic adult scene, natural lens perspective", "positive_suffix": "Use realistic anatomy, readable blocking, natural depth, motivated lighting, and coherent camera perspective.", "summary": "cinematic realism style", }, "comic_pinup_colored_pencil": { "style": "adult erotic coloured-pencil comic pin-up style", "positive_suffix": "Use crisp comic linework, detailed hatching, warm erotic lighting, soft skin shading, and tactile textured paper.", "summary": "colored-pencil comic pin-up style", }, "flat_vector_comic": { "style": "flat vector adult comic illustration", "positive_suffix": "Use flat color, clean graphic shapes, crisp outlines, simplified shadows, and readable adult body positioning.", "summary": "flat vector comic style", }, } def style_pool_preset_choices() -> list[str]: return list(STYLE_PRESETS) def style_combine_mode_choices() -> list[str]: return list(STYLE_COMBINE_MODES) def _clean_text(value: Any) -> str: return str(value or "").strip() def _join_text(*values: Any) -> str: parts: list[str] = [] for value in values: text = _clean_text(value) if text and text not in parts: parts.append(text.rstrip(".")) return ". ".join(parts) def parse_style_config(style_config: str | dict[str, Any] | None) -> dict[str, Any]: if not style_config: return {"enabled": False, "combine_mode": "disabled", "style": "", "positive_suffix": "", "negative_prompt": ""} if isinstance(style_config, dict): raw = dict(style_config) else: try: raw = json.loads(str(style_config)) except json.JSONDecodeError: return {"enabled": False, "combine_mode": "disabled", "style": "", "positive_suffix": "", "negative_prompt": ""} if raw.get("schema") != STYLE_CONFIG_SCHEMA: return {"enabled": False, "combine_mode": "disabled", "style": "", "positive_suffix": "", "negative_prompt": ""} combine_mode = _clean_text(raw.get("combine_mode")) or "replace" if combine_mode not in STYLE_COMBINE_MODES: combine_mode = "replace" return { "schema": STYLE_CONFIG_SCHEMA, "version": 1, "enabled": bool(raw.get("enabled", True)) and combine_mode != "disabled", "combine_mode": combine_mode, "preset": _clean_text(raw.get("preset")) or "category_default", "style": _clean_text(raw.get("style")), "positive_suffix": _clean_text(raw.get("positive_suffix")), "negative_prompt": _clean_text(raw.get("negative_prompt")), "summary": _clean_text(raw.get("summary")) or "style config", } def build_style_config_json( *, enabled: bool = True, combine_mode: str = "replace", preset: str = "category_default", custom_style: str = "", custom_positive_suffix: str = "", custom_negative: str = "", style_config: str | dict[str, Any] | None = "", ) -> str: if combine_mode not in STYLE_COMBINE_MODES: combine_mode = "replace" base = parse_style_config(style_config) preset_entry = STYLE_PRESETS.get(preset, STYLE_PRESETS["category_default"]) style = _clean_text(custom_style) or preset_entry["style"] positive_suffix = _clean_text(custom_positive_suffix) or preset_entry["positive_suffix"] negative_prompt = _clean_text(custom_negative) if combine_mode == "add" and base.get("enabled"): style = _join_text(base.get("style"), style) positive_suffix = _join_text(base.get("positive_suffix"), positive_suffix) negative_prompt = _join_text(base.get("negative_prompt"), negative_prompt) payload = { "schema": STYLE_CONFIG_SCHEMA, "version": 1, "enabled": bool(enabled) and combine_mode != "disabled", "combine_mode": combine_mode, "preset": preset, "style": style, "positive_suffix": positive_suffix, "negative_prompt": negative_prompt, "summary": "style disabled" if not enabled or combine_mode == "disabled" else preset_entry["summary"], } return json.dumps(payload, ensure_ascii=True, sort_keys=True) def resolve_style_fields(base_style: str, base_positive_suffix: str, style_config: str | dict[str, Any] | None) -> tuple[str, str]: config = parse_style_config(style_config) if not config.get("enabled"): return base_style, base_positive_suffix if config["combine_mode"] == "add": return ( _join_text(base_style, config.get("style")), _join_text(base_positive_suffix, config.get("positive_suffix")), ) return config.get("style", "") or base_style, config.get("positive_suffix", "") or base_positive_suffix def merge_negative_prompt(base_negative: str, style_config: str | dict[str, Any] | None) -> str: config = parse_style_config(style_config) if not config.get("enabled"): return base_negative return _join_text(base_negative, config.get("negative_prompt"))