from __future__ import annotations import copy import json import random from typing import Any try: from .prompt_builder import ( INSTA_OF_HARDCORE_CLOTHING_CONTINUITY, INSTA_OF_HARDCORE_LEVELS, INSTA_OF_PLATFORM_STYLES, INSTA_OF_SOFT_LEVELS, build_camera_config_json, build_cast_config_json, build_category_config_json, build_character_slot_json, build_composition_pool_json, build_generation_profile_json, build_insta_of_options_json, build_insta_of_pair, build_location_pool_json, build_prompt_from_configs, camera_angle_choices, camera_detail_choices, camera_distance_choices, camera_lens_choices, camera_mode_choices, camera_orientation_choices, camera_phone_choices, camera_priority_choices, camera_shot_choices, cast_preset_choices, category_preset_choices, character_age_choices, character_body_choices, character_descriptor_detail_choices, character_ethnicity_choices, character_figure_choices, character_label_choices, character_presence_choices, composition_pool_preset_choices, generation_profile_choices, hardcore_detail_density_choices, location_pool_preset_choices, seed_reroll_axis_choices, subcategory_choices, ) except ImportError: # Allows local smoke tests from the repository root. from prompt_builder import ( INSTA_OF_HARDCORE_CLOTHING_CONTINUITY, INSTA_OF_HARDCORE_LEVELS, INSTA_OF_PLATFORM_STYLES, INSTA_OF_SOFT_LEVELS, build_camera_config_json, build_cast_config_json, build_category_config_json, build_character_slot_json, build_composition_pool_json, build_generation_profile_json, build_insta_of_options_json, build_insta_of_pair, build_location_pool_json, build_prompt_from_configs, camera_angle_choices, camera_detail_choices, camera_distance_choices, camera_lens_choices, camera_mode_choices, camera_orientation_choices, camera_phone_choices, camera_priority_choices, camera_shot_choices, cast_preset_choices, category_preset_choices, character_age_choices, character_body_choices, character_descriptor_detail_choices, character_ethnicity_choices, character_figure_choices, character_label_choices, character_presence_choices, composition_pool_preset_choices, generation_profile_choices, hardcore_detail_density_choices, location_pool_preset_choices, seed_reroll_axis_choices, subcategory_choices, ) SXCP_SCENE = "SXCP_SCENE" SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG" SXCP_CAMERA_CONFIG = "SXCP_CAMERA_CONFIG" SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG" SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG" SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG" SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG" SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE" SXCP_FILTER_CONFIG = "SXCP_FILTER_CONFIG" SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG" SXCP_ETHNICITY_LIST = "SXCP_ETHNICITY_LIST" SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT" SXCP_CHARACTER_MANUAL = "SXCP_CHARACTER_MANUAL" SXCP_CHARACTERISTICS = "SXCP_CHARACTERISTICS" SXCP_HAIR_CONFIG = "SXCP_HAIR_CONFIG" SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG" SXCP_SCENE_LAYER_SEED = "SXCP_SCENE_LAYER_SEED" SXCP_SCENE_CAST_OPTIONS = "SXCP_SCENE_CAST_OPTIONS" SXCP_SCENE_CHARACTER_OPTIONS = "SXCP_SCENE_CHARACTER_OPTIONS" SXCP_SCENE_WARDROBE_OPTIONS = "SXCP_SCENE_WARDROBE_OPTIONS" SXCP_SCENE_LOCATION_OPTIONS = "SXCP_SCENE_LOCATION_OPTIONS" SXCP_SCENE_SET_OPTIONS = "SXCP_SCENE_SET_OPTIONS" SXCP_SCENE_BLOCKING_OPTIONS = "SXCP_SCENE_BLOCKING_OPTIONS" SXCP_SCENE_ACTION_OPTIONS = "SXCP_SCENE_ACTION_OPTIONS" SXCP_SCENE_PERFORMANCE_OPTIONS = "SXCP_SCENE_PERFORMANCE_OPTIONS" SXCP_SCENE_CAMERA_OPTIONS = "SXCP_SCENE_CAMERA_OPTIONS" SXCP_SCENE_COMPOSITION_OPTIONS = "SXCP_SCENE_COMPOSITION_OPTIONS" SXCP_SCENE_LIGHTING_OPTIONS = "SXCP_SCENE_LIGHTING_OPTIONS" SXCP_SCENE_BRANCH_OPTIONS = "SXCP_SCENE_BRANCH_OPTIONS" SCENE_SCHEMA = "sxcp_scene_v2" SCENE_OPTIONS_SCHEMA = "sxcp_scene_options_v1" SCENE_LAYER_SEED_SCHEMA = "sxcp_scene_layer_seed_v1" TARGET_FORMATTERS = ["raw", "krea2", "sdxl", "caption"] SCENE_KINDS = ["regular", "softcore", "hardcore"] CENTRAL_SUBJECT_CHOICES = ["auto", "woman_a", "man_a", "none"] POV_PARTICIPANT_CHOICES = ["none", "man_a"] SUBJECT_LABEL_CHOICES = ["all"] + [choice for choice in character_label_choices() if choice != "auto_chain"] BRANCH_NAMES = ("softcore", "hardcore") SOFTCORE_CAMERA_CHOICES = ["from_camera_config"] + camera_mode_choices() HARDCORE_CAMERA_CHOICES = ["from_camera_config", "same_as_softcore"] + camera_mode_choices() HARDCORE_CLOTHING_CONTINUITY_CHOICES = list(INSTA_OF_HARDCORE_CLOTHING_CONTINUITY) SCENE_LAYER_CHOICES = [ "all", "cast", "character", "wardrobe", "location", "set_dressing", "blocking", "action", "performance", "camera", "composition", "lighting", "softcore_branch", "hardcore_branch", ] SCENE_LAYER_SEED_MODES = ["follow_global", "fixed", "random", "disabled"] SCENE_LAYER_SEED_ROW_BEHAVIORS = ["same_for_all_rows", "vary_by_row"] SCENE_OPTION_COMBINE_MODES = ["replace", "add"] SCENE_LAYER_SEED_COMBINE_MODES = ["replace_layer", "add"] WARDROBE_STATE_CHOICES = [ "no_change", "dressed", "tease", "implied_nude", "explicit_nude", "removed_nearby", "pulled_aside", "retained_top", "retained_stockings_accessories", "custom", ] LOCATION_VISIBILITY_CHOICES = ["auto", "open", "partly_occluded", "hidden", "semi_public"] LOCATION_PUBLIC_LEVEL_CHOICES = ["auto", "private", "semi_public", "public_afterhours", "public"] BLOCKING_MODE_CHOICES = ["auto", "standing", "sitting", "kneeling", "lying", "bent_over", "custom"] BODY_ORIENTATION_CHOICES = ["auto", "front", "three_quarter", "side", "back", "pov_facing_viewer", "pov_facing_away"] DEPTH_PLANE_CHOICES = ["auto", "foreground", "midground", "background", "layered"] ACTION_FAMILY_CHOICES = [ "no_change", "softcore_tease", "foreplay", "manual", "oral", "outercourse", "penetration", "anal", "climax", "group", "custom", ] GAZE_CHOICES = ["auto", "camera", "partner", "down", "away", "over_shoulder", "eyes_closed"] HAND_PLACEMENT_CHOICES = [ "auto", "relaxed", "on_body", "on_partner", "holding_camera", "pulling_clothing", "braced", "custom", ] BODY_TENSION_CHOICES = ["auto", "relaxed", "posed", "arched", "braced", "active_motion"] CAMERA_SOURCE_CHOICES = ["manual", "from_camera_config", "qwen_orbit", "pov", "phone", "external"] COMPOSITION_READABILITY_CHOICES = [ "auto", "face", "body", "action", "room", "foreground_anchor", "contact_points", ] COMPOSITION_CROP_CHOICES = ["auto", "full_body", "three_quarter", "waist_up", "close_up", "extreme_close_up"] COMPOSITION_OCCLUSION_CHOICES = ["auto", "clear", "partial", "foreground_framed", "hidden_sightline"] LIGHTING_SOURCE_CHOICES = ["auto", "daylight", "window_light", "practical_lamps", "neon", "studio", "phone_flash", "custom"] LIGHTING_SOFTNESS_CHOICES = ["auto", "soft", "balanced", "hard"] LIGHTING_CONTRAST_CHOICES = ["auto", "low", "medium", "high"] COLOR_TEMPERATURE_CHOICES = ["auto", "warm", "neutral", "cool", "mixed"] TIME_OF_DAY_CHOICES = ["auto", "morning", "day", "evening", "night", "late_night"] BRANCH_TARGET_CHOICES = ["both", "softcore", "hardcore"] SCENE_LAYER_SEED_AXES = { "cast": ("category",), "character": ("person",), "wardrobe": ("clothing",), "location": ("scene",), "set_dressing": ("scene",), "blocking": ("pose",), "action": ("pose", "role"), "performance": ("expression",), "camera": ("composition",), "composition": ("composition",), "lighting": ("composition",), "softcore_branch": ("clothing", "pose", "role"), "hardcore_branch": ("pose", "role"), } SCENE_REROLL_GROUPS = { "none": (), "category": ("category",), "subcategory": ("subcategory",), "content": ("content",), "clothing": ("clothing",), "person": ("person",), "scene": ("scene",), "pose": ("pose", "role"), "role": ("role",), "expression": ("expression",), "composition": ("composition",), "content_pose": ("content", "pose", "role"), "content_clothing": ("content", "clothing"), "clothing_pose": ("clothing", "pose", "role"), "scene_pose": ("scene", "pose", "role"), } SCENE_OPTION_TEXT_KEYS = { "accessories", "wardrobe_prompt", "location_note", "foreground_anchors", "midground_layer", "background_repetition", "props", "sensory_details", "set_prompt", "subject_placement", "body_relation", "distance_note", "custom_blocking", "action_prompt", "performance_prompt", "camera_prompt", "composition_prompt", "custom_lighting", "extra_positive", } WARDROBE_STATE_TO_CLOTHING = { "explicit_nude": "fully nude", "removed_nearby": "fully nude, removed outfit visible nearby", "pulled_aside": "clothing pulled aside where needed, body contact unobstructed", "retained_top": "top clothing retained while lower body is exposed", "retained_stockings_accessories": "stockings and accessories retained while body is exposed", "implied_nude": "partly covered implied nude body", } def _json_dict(value: str | dict[str, Any] | None) -> dict[str, Any]: if not value: return {} if isinstance(value, dict): return copy.deepcopy(value) try: parsed = json.loads(str(value)) except json.JSONDecodeError: return {} return copy.deepcopy(parsed) if isinstance(parsed, dict) else {} def _dump(value: dict[str, Any]) -> str: return json.dumps(value, ensure_ascii=True, sort_keys=True) def _parse_scene(scene: str | dict[str, Any] | None) -> dict[str, Any]: parsed = _json_dict(scene) if parsed.get("schema") != SCENE_SCHEMA: parsed = {} parsed.setdefault("schema", SCENE_SCHEMA) parsed.setdefault("version", 1) parsed.setdefault("row_number", 1) parsed.setdefault("start_index", 41) parsed.setdefault("seed", 20260614) parsed.setdefault("target_formatter", "raw") parsed.setdefault("trigger", "sxcpinup_coloredpencil") parsed.setdefault("prepend_trigger_to_prompt", True) parsed.setdefault("extra_positive", "") parsed.setdefault("extra_negative", "") parsed.setdefault("configs", {}) parsed.setdefault("layers", {}) parsed.setdefault("branches", {}) parsed.setdefault("history", []) return parsed def _scene_out(scene: dict[str, Any]) -> tuple[str, str, str]: summary = _scene_summary(scene) return _dump(scene), summary, _dump(scene) def _scene_summary(scene: dict[str, Any]) -> str: layers = ",".join(sorted(scene.get("layers", {}))) or "empty" branches = ",".join(sorted(scene.get("branches", {}))) or "none" return ( f"scene v{scene.get('version', 1)}; row={scene.get('row_number')}; " f"seed={scene.get('seed')}; layers={layers}; branches={branches}" ) def _add_history(scene: dict[str, Any], node: str, summary: str) -> None: history = scene.setdefault("history", []) if isinstance(history, list): history.append({"node": node, "summary": summary}) def _set_config(scene: dict[str, Any], key: str, value: Any) -> None: if value not in (None, ""): scene.setdefault("configs", {})[key] = value def _set_layer(scene: dict[str, Any], layer: str, values: dict[str, Any], summary: str) -> None: scene.setdefault("layers", {})[layer] = values _add_history(scene, layer, summary) def _branch(scene: dict[str, Any], name: str) -> dict[str, Any]: branches = scene.setdefault("branches", {}) branch = branches.setdefault(name, {}) branch.setdefault("configs", {}) branch.setdefault("options", {}) branch.setdefault("extra_positive", "") return branch def _text_parts(*values: Any) -> list[str]: parts: list[str] = [] for value in values: text = str(value or "").strip() if text and text not in parts: parts.append(text) return parts def _joined_text(*values: Any) -> str: return ". ".join(part.rstrip(".") for part in _text_parts(*values)) def _truthy(value: Any) -> bool: if isinstance(value, bool): return value if isinstance(value, (int, float)): return bool(value) return str(value or "").strip().lower() in {"1", "true", "yes", "on"} def _merge_option_values(base: dict[str, Any], values: dict[str, Any], combine_mode: str) -> dict[str, Any]: merged = {} if combine_mode == "replace" else copy.deepcopy(base) for key, value in values.items(): if value is None: continue if isinstance(value, str): text = value.strip() if not text: continue if combine_mode == "add" and key in SCENE_OPTION_TEXT_KEYS and merged.get(key): merged[key] = _joined_text(merged.get(key), text) else: merged[key] = text continue merged[key] = value return merged def _scene_option_values(options: Any, domain: str) -> dict[str, Any]: parsed = _json_dict(options) if parsed.get("schema") != SCENE_OPTIONS_SCHEMA: return {} if str(parsed.get("domain") or "") != domain: return {} values = parsed.get("values") return copy.deepcopy(values) if isinstance(values, dict) else {} def _scene_option_json( domain: str, combine_mode: str, incoming_options: Any, values: dict[str, Any], summary: str, ) -> str: incoming = _scene_option_values(incoming_options, domain) merged = _merge_option_values(incoming, values, combine_mode) return _dump( { "schema": SCENE_OPTIONS_SCHEMA, "version": 1, "domain": domain, "values": merged, "summary": summary, } ) def _scene_options_out(domain: str, options_json: str) -> tuple[str, str, str]: parsed = _json_dict(options_json) summary = str(parsed.get("summary") or f"{domain} options") return options_json, summary, options_json def _seed_option_items(seed_options: Any) -> list[dict[str, Any]]: parsed = _json_dict(seed_options) if parsed.get("schema") != SCENE_LAYER_SEED_SCHEMA: return [] items = parsed.get("items") if isinstance(items, list): return [copy.deepcopy(item) for item in items if isinstance(item, dict)] return [] def _layer_seed_options_json( seed_options: Any, layer: str, seed_mode: str, seed: int, reroll_axis: str, row_behavior: str, combine_mode: str, ) -> str: items = _seed_option_items(seed_options) if combine_mode == "add" else [] if combine_mode == "replace_layer": items = [item for item in _seed_option_items(seed_options) if item.get("layer") != layer] resolved_seed = max(0, min(0xFFFFFFFF, int(seed))) items.append( { "layer": layer, "seed_mode": seed_mode, "seed": resolved_seed, "reroll_axis": reroll_axis, "row_behavior": row_behavior, } ) summary = f"{layer}: {seed_mode}; axis={reroll_axis}; seed={resolved_seed}" return _dump( { "schema": SCENE_LAYER_SEED_SCHEMA, "version": 1, "items": items, "summary": summary, } ) def _merge_seed_config(seed_config: Any, axes: tuple[str, ...], seed: int) -> str: config = _json_dict(seed_config) for axis in axes: config[f"{axis}_seed"] = max(0, min(0xFFFFFFFF, int(seed))) return _dump(config) def _combined_seed_config(*seed_configs: Any) -> str: combined: dict[str, Any] = {} for seed_config in seed_configs: combined.update(_json_dict(seed_config)) return _dump(combined) if combined else "" def _apply_layer_seed(scene: dict[str, Any], layer_name: str, seed_options: Any, branch_name: str = "") -> None: items = _seed_option_items(seed_options) if not items: return for item in items: target_layer = str(item.get("layer") or "") if target_layer not in {"all", layer_name}: continue seed_mode = str(item.get("seed_mode") or "follow_global") if seed_mode == "disabled": continue if seed_mode == "follow_global": seed_value = int(scene.get("seed", 0)) else: seed_value = max(0, min(0xFFFFFFFF, int(item.get("seed") or 0))) if item.get("row_behavior") == "vary_by_row": seed_value = (seed_value + int(scene.get("row_number", 1)) * 1009) & 0xFFFFFFFF reroll_axis = str(item.get("reroll_axis") or "none") axes = SCENE_REROLL_GROUPS.get(reroll_axis) if reroll_axis != "none" else None axes = tuple(axes or SCENE_LAYER_SEED_AXES.get(layer_name, ())) if not axes: continue if branch_name: branch = _branch(scene, branch_name) current = branch["configs"].get("seed_config") or _base_config(scene, "seed_config") branch["configs"]["seed_config"] = _merge_seed_config(current, axes, seed_value) trace_key = f"{branch_name}.{layer_name}" else: current = _base_config(scene, "seed_config") _set_config(scene, "seed_config", _merge_seed_config(current, axes, seed_value)) trace_key = layer_name scene.setdefault("seed_trace", {})[trace_key] = { "seed": seed_value, "axes": list(axes), "mode": seed_mode, "reroll_axis": reroll_axis, } def _wardrobe_state_clothing(state: str, fallback: str) -> str: if state in {"no_change", "dressed", "tease", "custom"}: return fallback or "" return WARDROBE_STATE_TO_CLOTHING.get(state, fallback or "") def _branch_option_values(branch_options: Any, target: str) -> dict[str, Any]: values = _scene_option_values(branch_options, "branch") branch_target = str(values.get("branch_target") or "both") if branch_target not in {"both", target}: return {} return values def _cast_slots(character_cast: Any) -> list[dict[str, Any]]: cast = _json_dict(character_cast) slots = cast.get("slots") if isinstance(slots, list): return [dict(slot) for slot in slots if isinstance(slot, dict)] if isinstance(character_cast, list): return [dict(slot) for slot in character_cast if isinstance(slot, dict)] return [] def _slot_matches(slot: dict[str, Any], subject_type: str, subject_label: str) -> bool: if subject_type != "all" and str(slot.get("subject_type") or "").strip().lower() != subject_type: return False if subject_label == "all": return True return str(slot.get("label") or "").strip().upper() == subject_label.upper() def _update_character_cast_wardrobe( character_cast: str | dict[str, Any] | list[Any] | None, subject_type: str, subject_label: str, softcore_outfit: str, hardcore_clothing: str, ) -> str: slots = _cast_slots(character_cast) if not slots: return character_cast if isinstance(character_cast, str) else "" changed = False for slot in slots: if not _slot_matches(slot, subject_type, subject_label): continue if softcore_outfit: slot["softcore_outfit"] = softcore_outfit changed = True if hardcore_clothing: slot["hardcore_clothing"] = hardcore_clothing changed = True if not changed: return character_cast if isinstance(character_cast, str) else _dump({"profile_type": "character_cast", "version": 1, "slots": slots}) return _dump({"profile_type": "character_cast", "version": 1, "slots": slots}) def _base_config(scene: dict[str, Any], key: str, default: str = "") -> str: return str((scene.get("configs") or {}).get(key) or default or "") def _build_generation_profile_from_scene(scene: dict[str, Any]) -> str: existing = _base_config(scene, "generation_profile") if existing: profile_config = _json_dict(existing) if profile_config: profile_config["trigger"] = str(scene.get("trigger") or profile_config.get("trigger") or "sxcpinup_coloredpencil") profile_config["prepend_trigger_to_prompt"] = bool(scene.get("prepend_trigger_to_prompt", True)) return _dump(profile_config) return existing start_profile = str(scene.get("profile") or "balanced") wardrobe = scene.get("layers", {}).get("wardrobe", {}) performance = scene.get("layers", {}).get("performance", {}) clothing_override = str(wardrobe.get("clothing_override") or "profile_default") expression_enabled = bool(performance.get("expression_enabled", True)) expression_mode = str(performance.get("expression_intensity_mode") or "profile_default") expression_intensity = float(performance.get("expression_intensity", -1.0)) trigger_policy = "prepend_trigger" if scene.get("prepend_trigger_to_prompt", True) else "do_not_prepend" profile_json = build_generation_profile_json( profile=start_profile, clothing_override=clothing_override, expression_enabled=expression_enabled, expression_intensity_mode=expression_mode, expression_intensity=expression_intensity, trigger_policy=trigger_policy, ) profile_config = _json_dict(profile_json) profile_config["trigger"] = str(scene.get("trigger") or profile_config.get("trigger") or "sxcpinup_coloredpencil") profile_config["prepend_trigger_to_prompt"] = bool(scene.get("prepend_trigger_to_prompt", True)) return _dump(profile_config) def _scene_extra_positive(scene: dict[str, Any], branch_name: str = "") -> str: layers = scene.get("layers", {}) branch = scene.get("branches", {}).get(branch_name, {}) if branch_name else {} parts = _text_parts( scene.get("extra_positive"), layers.get("set_dressing", {}).get("prompt"), layers.get("blocking", {}).get("prompt"), layers.get("action", {}).get("prompt"), layers.get("performance", {}).get("prompt"), layers.get("lighting", {}).get("prompt"), branch.get("extra_positive"), ) return ". ".join(part.rstrip(".") for part in parts) def _compat_configs(scene: dict[str, Any], branch_name: str = "") -> dict[str, Any]: configs = scene.get("configs") or {} branch = scene.get("branches", {}).get(branch_name, {}) if branch_name else {} branch_configs = branch.get("configs") or {} category_config = branch_configs.get("category_config") or configs.get("category_config") if not category_config: category_config = build_category_config_json( preset=str(scene.get("category_preset") or "auto_weighted"), subcategory=str(scene.get("subcategory") or "random"), ) cast_config = branch_configs.get("cast_config") or configs.get("cast_config") if not cast_config: cast = scene.get("layers", {}).get("cast", {}) cast_config = build_cast_config_json( cast_mode=str(cast.get("cast_mode") or "mixed_couple"), women_count=int(cast.get("women_count", 1)), men_count=int(cast.get("men_count", 1)), ) return { "category_config": category_config, "cast_config": cast_config, "generation_profile": branch_configs.get("generation_profile") or _build_generation_profile_from_scene(scene), "filter_config": branch_configs.get("filter_config") or configs.get("filter_config") or "", "seed_config": branch_configs.get("seed_config") or configs.get("seed_config") or scene.get("seed_config") or "", "camera_config": branch_configs.get("camera_config") or configs.get("camera_config") or "", "location_config": branch_configs.get("location_config") or configs.get("location_config") or "", "composition_config": branch_configs.get("composition_config") or configs.get("composition_config") or "", "style_config": branch_configs.get("style_config") or configs.get("style_config") or "", "character_profile": branch_configs.get("character_profile") or configs.get("character_profile") or "", "character_cast": branch_configs.get("character_cast") or configs.get("character_cast") or "", "hardcore_position_config": branch_configs.get("hardcore_position_config") or configs.get("hardcore_position_config") or "", "extra_positive": _scene_extra_positive(scene, branch_name), "extra_negative": str(scene.get("extra_negative") or ""), } def _pair_options(soft_scene: dict[str, Any], hard_scene: dict[str, Any]) -> str: soft_pair = soft_scene.get("pair", {}) hard_pair = hard_scene.get("pair", {}) soft_options = soft_scene.get("branches", {}).get("softcore", {}).get("options", {}) hard_options = hard_scene.get("branches", {}).get("hardcore", {}).get("options", {}) return build_insta_of_options_json( softcore_cast=str(soft_options.get("softcore_cast") or "solo"), hardcore_cast=str(hard_options.get("hardcore_cast") or "use_counts"), hardcore_women_count=int(hard_options.get("hardcore_women_count", 1)), hardcore_men_count=int(hard_options.get("hardcore_men_count", 1)), softcore_level=str(soft_options.get("softcore_level") or "lingerie_tease"), hardcore_level=str(hard_options.get("hardcore_level") or "hardcore"), platform_style=str(hard_pair.get("platform_style") or soft_pair.get("platform_style") or "hybrid"), continuity=str(hard_pair.get("continuity") or soft_pair.get("continuity") or "same_creator_same_room"), hardcore_clothing_continuity=str(hard_options.get("hardcore_clothing_continuity") or "partially_removed"), softcore_camera_mode=str(soft_options.get("softcore_camera_mode") or "from_camera_config"), hardcore_camera_mode=str(hard_options.get("hardcore_camera_mode") or "from_camera_config"), camera_detail=str(hard_options.get("camera_detail") or soft_options.get("camera_detail") or "from_camera_config"), softcore_expression_enabled=bool(soft_options.get("softcore_expression_enabled", True)), hardcore_expression_enabled=bool(hard_options.get("hardcore_expression_enabled", True)), softcore_expression_intensity=float(soft_options.get("softcore_expression_intensity", 0.45)), hardcore_expression_intensity=float(hard_options.get("hardcore_expression_intensity", 0.85)), hardcore_detail_density=str(hard_options.get("hardcore_detail_density") or "balanced"), ) class SxCPSceneLayerSeedOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "layer": (SCENE_LAYER_CHOICES, {"default": "all"}), "seed_mode": (SCENE_LAYER_SEED_MODES, {"default": "follow_global"}), "seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}), "reroll_axis": (seed_reroll_axis_choices(), {"default": "none"}), "row_behavior": (SCENE_LAYER_SEED_ROW_BEHAVIORS, {"default": "same_for_all_rows"}), "combine_mode": (SCENE_LAYER_SEED_COMBINE_MODES, {"default": "replace_layer"}), }, "optional": { "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE_LAYER_SEED, "STRING", "STRING") RETURN_NAMES = ("seed_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" @classmethod def IS_CHANGED(cls, *args, **kwargs): values = list(args) + list(kwargs.values()) if "random" in values: return random.random() return tuple(args), tuple(sorted(kwargs.items())) def build(self, layer, seed_mode, seed, reroll_axis, row_behavior, combine_mode, seed_options=""): options = _layer_seed_options_json(seed_options, layer, seed_mode, seed, reroll_axis, row_behavior, combine_mode) summary = str(_json_dict(options).get("summary") or "layer seed options") return options, summary, options class SxCPSceneCastOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "cast_mode": (["no_change"] + cast_preset_choices(), {"default": "no_change"}), "women_count": ("INT", {"default": -1, "min": -1, "max": 12, "step": 1}), "men_count": ("INT", {"default": -1, "min": -1, "max": 12, "step": 1}), "central_subject": (CENTRAL_SUBJECT_CHOICES, {"default": "auto"}), "pov_participant": (POV_PARTICIPANT_CHOICES, {"default": "none"}), }, "optional": { "options": (SXCP_SCENE_CAST_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_CAST_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("cast_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, cast_mode, women_count, men_count, central_subject, pov_participant, options=""): values = { "central_subject": central_subject, "pov_participant": pov_participant, } if cast_mode != "no_change": values["cast_mode"] = cast_mode if int(women_count) >= 0: values["women_count"] = int(women_count) if int(men_count) >= 0: values["men_count"] = int(men_count) output = _scene_option_json("cast", combine_mode, options, values, f"cast options; central={central_subject}; pov={pov_participant}") return _scene_options_out("cast", output) class SxCPSceneCharacterOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "descriptor_detail": (["no_change"] + character_descriptor_detail_choices(), {"default": "no_change"}), "presence_mode": (["no_change"] + character_presence_choices(), {"default": "no_change"}), "expression_enabled": (["inherit", "enabled", "disabled"], {"default": "inherit"}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "performance_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_CHARACTER_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_CHARACTER_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("character_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build( self, combine_mode, descriptor_detail, presence_mode, expression_enabled, expression_intensity, softcore_expression_intensity, hardcore_expression_intensity, performance_prompt, options="", ): values: dict[str, Any] = {"performance_prompt": performance_prompt} if descriptor_detail != "no_change": values["descriptor_detail"] = descriptor_detail if presence_mode != "no_change": values["presence_mode"] = presence_mode if expression_enabled != "inherit": values["expression_enabled"] = expression_enabled == "enabled" if float(expression_intensity) >= 0: values["expression_intensity"] = float(expression_intensity) if float(softcore_expression_intensity) >= 0: values["softcore_expression_intensity"] = float(softcore_expression_intensity) if float(hardcore_expression_intensity) >= 0: values["hardcore_expression_intensity"] = float(hardcore_expression_intensity) output = _scene_option_json("character", combine_mode, options, values, "character options") return _scene_options_out("character", output) class SxCPSceneWardrobeOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "subject_type": (["all", "woman", "man"], {"default": "all"}), "subject_label": (SUBJECT_LABEL_CHOICES, {"default": "all"}), "clothing_override": (["no_change", "profile_default", "random", "full", "minimal"], {"default": "no_change"}), "wardrobe_state": (WARDROBE_STATE_CHOICES, {"default": "no_change"}), "avoid_clothing_when_nude": ("BOOLEAN", {"default": True}), "softcore_outfit": ("STRING", {"default": "", "multiline": True}), "hardcore_clothing": ("STRING", {"default": "", "multiline": True}), "accessories": ("STRING", {"default": "", "multiline": True}), "wardrobe_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_WARDROBE_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_WARDROBE_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("wardrobe_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build( self, combine_mode, subject_type, subject_label, clothing_override, wardrobe_state, avoid_clothing_when_nude, softcore_outfit, hardcore_clothing, accessories, wardrobe_prompt, options="", ): values = { "subject_type": subject_type, "subject_label": subject_label, "wardrobe_state": wardrobe_state, "avoid_clothing_when_nude": bool(avoid_clothing_when_nude), "softcore_outfit": softcore_outfit, "hardcore_clothing": hardcore_clothing, "accessories": accessories, "wardrobe_prompt": wardrobe_prompt, } if clothing_override != "no_change": values["clothing_override"] = clothing_override output = _scene_option_json("wardrobe", combine_mode, options, values, f"wardrobe options; {subject_type} {subject_label}; {wardrobe_state}") return _scene_options_out("wardrobe", output) class SxCPSceneLocationLayoutOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "foreground_anchors": ("STRING", {"default": "", "multiline": True}), "midground_layer": ("STRING", {"default": "", "multiline": True}), "background_repetition": ("STRING", {"default": "", "multiline": True}), "visibility_level": (LOCATION_VISIBILITY_CHOICES, {"default": "auto"}), "public_level": (LOCATION_PUBLIC_LEVEL_CHOICES, {"default": "auto"}), "location_note": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_LOCATION_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_LOCATION_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("location_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, foreground_anchors, midground_layer, background_repetition, visibility_level, public_level, location_note, options=""): values = { "foreground_anchors": foreground_anchors, "midground_layer": midground_layer, "background_repetition": background_repetition, "visibility_level": visibility_level, "public_level": public_level, "location_note": location_note, } output = _scene_option_json("location", combine_mode, options, values, "location layout options") return _scene_options_out("location", output) class SxCPSceneSetDressingOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "foreground_anchors": ("STRING", {"default": "", "multiline": True}), "repeated_background": ("STRING", {"default": "", "multiline": True}), "props": ("STRING", {"default": "", "multiline": True}), "sensory_details": ("STRING", {"default": "", "multiline": True}), "set_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_SET_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_SET_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("set_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, foreground_anchors, repeated_background, props, sensory_details, set_prompt, options=""): output = _scene_option_json( "set_dressing", combine_mode, options, { "foreground_anchors": foreground_anchors, "repeated_background": repeated_background, "props": props, "sensory_details": sensory_details, "set_prompt": set_prompt, }, "set dressing options", ) return _scene_options_out("set_dressing", output) class SxCPSceneBlockingOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "blocking_mode": (["no_change"] + BLOCKING_MODE_CHOICES, {"default": "no_change"}), "subject_placement": ("STRING", {"default": "", "multiline": True}), "body_relation": ("STRING", {"default": "", "multiline": True}), "body_orientation": (BODY_ORIENTATION_CHOICES, {"default": "auto"}), "depth_plane": (DEPTH_PLANE_CHOICES, {"default": "auto"}), "distance_note": ("STRING", {"default": "", "multiline": True}), "custom_blocking": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_BLOCKING_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_BLOCKING_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("blocking_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, blocking_mode, subject_placement, body_relation, body_orientation, depth_plane, distance_note, custom_blocking, options=""): values = { "subject_placement": subject_placement, "body_relation": body_relation, "body_orientation": body_orientation, "depth_plane": depth_plane, "distance_note": distance_note, "custom_blocking": custom_blocking, } if blocking_mode != "no_change": values["blocking_mode"] = blocking_mode output = _scene_option_json("blocking", combine_mode, options, values, "blocking options") return _scene_options_out("blocking", output) class SxCPSceneActionOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "scene_kind": (["no_change"] + SCENE_KINDS, {"default": "no_change"}), "action_family": (ACTION_FAMILY_CHOICES, {"default": "no_change"}), "category_preset": (["no_change"] + category_preset_choices(), {"default": "no_change"}), "action_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_ACTION_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_ACTION_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("action_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, scene_kind, action_family, category_preset, action_prompt, options=""): values = {"action_family": action_family, "category_preset": category_preset, "action_prompt": action_prompt} if scene_kind != "no_change": values["scene_kind"] = scene_kind output = _scene_option_json("action", combine_mode, options, values, f"action options; {action_family}") return _scene_options_out("action", output) class SxCPScenePerformanceOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "expression_enabled": (["inherit", "enabled", "disabled"], {"default": "inherit"}), "expression_intensity_mode": (["no_change", "profile_default", "random", "fixed"], {"default": "no_change"}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "gaze": (GAZE_CHOICES, {"default": "auto"}), "hand_placement": (HAND_PLACEMENT_CHOICES, {"default": "auto"}), "body_tension": (BODY_TENSION_CHOICES, {"default": "auto"}), "performance_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_PERFORMANCE_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_PERFORMANCE_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("performance_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, expression_enabled, expression_intensity_mode, expression_intensity, gaze, hand_placement, body_tension, performance_prompt, options=""): values: dict[str, Any] = { "gaze": gaze, "hand_placement": hand_placement, "body_tension": body_tension, "performance_prompt": performance_prompt, } if expression_enabled != "inherit": values["expression_enabled"] = expression_enabled == "enabled" if expression_intensity_mode != "no_change": values["expression_intensity_mode"] = expression_intensity_mode if float(expression_intensity) >= 0: values["expression_intensity"] = float(expression_intensity) output = _scene_option_json("performance", combine_mode, options, values, "performance options") return _scene_options_out("performance", output) class SxCPSceneCameraOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "camera_source": (CAMERA_SOURCE_CHOICES, {"default": "from_camera_config"}), "preserve_location_layout": ("BOOLEAN", {"default": True}), "camera_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_CAMERA_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_CAMERA_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("camera_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, camera_source, preserve_location_layout, camera_prompt, options=""): output = _scene_option_json( "camera", combine_mode, options, { "camera_source": camera_source, "preserve_location_layout": bool(preserve_location_layout), "camera_prompt": camera_prompt, }, f"camera options; {camera_source}", ) return _scene_options_out("camera", output) class SxCPSceneCompositionOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "readability_target": (COMPOSITION_READABILITY_CHOICES, {"default": "auto"}), "crop": (COMPOSITION_CROP_CHOICES, {"default": "auto"}), "occlusion": (COMPOSITION_OCCLUSION_CHOICES, {"default": "auto"}), "composition_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_COMPOSITION_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_COMPOSITION_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("composition_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, readability_target, crop, occlusion, composition_prompt, options=""): output = _scene_option_json( "composition", combine_mode, options, { "readability_target": readability_target, "crop": crop, "occlusion": occlusion, "composition_prompt": composition_prompt, }, "composition options", ) return _scene_options_out("composition", output) class SxCPSceneLightingOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "lighting_source": (LIGHTING_SOURCE_CHOICES, {"default": "auto"}), "lighting_softness": (LIGHTING_SOFTNESS_CHOICES, {"default": "auto"}), "lighting_contrast": (LIGHTING_CONTRAST_CHOICES, {"default": "auto"}), "color_temperature": (COLOR_TEMPERATURE_CHOICES, {"default": "auto"}), "time_of_day": (TIME_OF_DAY_CHOICES, {"default": "auto"}), "custom_lighting": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_LIGHTING_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_LIGHTING_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("lighting_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, lighting_source, lighting_softness, lighting_contrast, color_temperature, time_of_day, custom_lighting, options=""): output = _scene_option_json( "lighting", combine_mode, options, { "lighting_source": lighting_source, "lighting_softness": lighting_softness, "lighting_contrast": lighting_contrast, "color_temperature": color_temperature, "time_of_day": time_of_day, "custom_lighting": custom_lighting, }, "lighting options", ) return _scene_options_out("lighting", output) class SxCPSceneBranchOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (SCENE_OPTION_COMBINE_MODES, {"default": "replace"}), "branch_target": (BRANCH_TARGET_CHOICES, {"default": "both"}), "continuity": (["no_change", "same_creator_same_room", "same_creator_new_scene"], {"default": "no_change"}), "platform_style": (["no_change"] + list(INSTA_OF_PLATFORM_STYLES), {"default": "no_change"}), "extra_positive": ("STRING", {"default": "", "multiline": True}), }, "optional": { "options": (SXCP_SCENE_BRANCH_OPTIONS,), }, } RETURN_TYPES = (SXCP_SCENE_BRANCH_OPTIONS, "STRING", "STRING") RETURN_NAMES = ("branch_options", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene/options" def build(self, combine_mode, branch_target, continuity, platform_style, extra_positive, options=""): values = {"branch_target": branch_target, "extra_positive": extra_positive} if continuity != "no_change": values["continuity"] = continuity if platform_style != "no_change": values["platform_style"] = platform_style output = _scene_option_json("branch", combine_mode, options, values, f"branch options; target={branch_target}") return _scene_options_out("branch", output) class SxCPSceneStart: @classmethod def INPUT_TYPES(cls): return { "required": { "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}), "seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}), "target_formatter": (TARGET_FORMATTERS, {"default": "raw"}), "category_preset": (category_preset_choices(), {"default": "auto_weighted"}), "subcategory": (subcategory_choices(), {"default": "random"}), "profile": (generation_profile_choices(), {"default": "balanced"}), "trigger": ("STRING", {"default": "sxcpinup_coloredpencil"}), "prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}), }, "optional": { "seed_config": (SXCP_SEED_CONFIG,), "category_config": (SXCP_CATEGORY_CONFIG,), "generation_profile": (SXCP_GENERATION_PROFILE,), "filter_config": (SXCP_FILTER_CONFIG,), "style_config": (SXCP_STYLE_CONFIG,), "extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}), }, } RETURN_TYPES = (SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, row_number, start_index, seed, target_formatter, category_preset, subcategory, profile, trigger, prepend_trigger_to_prompt, seed_config="", category_config="", generation_profile="", filter_config="", style_config="", extra_positive="", extra_negative="", ): scene = _parse_scene("") scene.update( { "row_number": int(row_number), "start_index": int(start_index), "seed": int(seed), "target_formatter": target_formatter if target_formatter in TARGET_FORMATTERS else "raw", "category_preset": category_preset, "subcategory": subcategory, "profile": profile, "trigger": str(trigger or ""), "prepend_trigger_to_prompt": bool(prepend_trigger_to_prompt), "extra_positive": extra_positive or "", "extra_negative": extra_negative or "", } ) _set_config(scene, "seed_config", seed_config) _set_config(scene, "category_config", category_config) _set_config(scene, "generation_profile", generation_profile) _set_config(scene, "filter_config", filter_config) _set_config(scene, "style_config", style_config) _add_history(scene, "scene_start", f"{category_preset}/{subcategory}; {profile}") return _scene_out(scene) class SxCPSceneCast: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "cast_mode": (cast_preset_choices(), {"default": "mixed_couple"}), "women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "central_subject": (CENTRAL_SUBJECT_CHOICES, {"default": "auto"}), "pov_participant": (POV_PARTICIPANT_CHOICES, {"default": "none"}), }, "optional": { "cast_config": (SXCP_CAST_CONFIG,), "cast_options": (SXCP_SCENE_CAST_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_CAST_CONFIG, "STRING", "STRING") RETURN_NAMES = ("scene", "cast_config", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, cast_mode, women_count, men_count, central_subject, pov_participant, cast_config="", cast_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(cast_options, "cast") cast_mode = str(options.get("cast_mode") or cast_mode) women_count = int(options.get("women_count", women_count)) men_count = int(options.get("men_count", men_count)) central_subject = str(options.get("central_subject") or central_subject) pov_participant = str(options.get("pov_participant") or pov_participant) config = cast_config or build_cast_config_json(cast_mode, women_count, men_count) _set_config(parsed, "cast_config", config) layer = { "cast_mode": cast_mode, "women_count": int(women_count), "men_count": int(men_count), "central_subject": central_subject, "pov_participant": pov_participant, } summary = f"{women_count} women, {men_count} men; central={central_subject}; pov={pov_participant}" _set_layer(parsed, "cast", layer, summary) _apply_layer_seed(parsed, "cast", seed_options) return _dump(parsed), config, summary, _dump(parsed) class SxCPSceneCharacter: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "subject_type": (["woman", "man"], {"default": "woman"}), "label": (character_label_choices(), {"default": "auto_chain"}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}), "figure": (character_figure_choices(), {"default": "random"}), "body": ([choice for choice in character_body_choices() if choice != "manual"], {"default": "random"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "presence_mode": (character_presence_choices(), {"default": "visible"}), "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), }, "optional": { "manual": (SXCP_CHARACTER_MANUAL,), "ethnicity_list": (SXCP_ETHNICITY_LIST,), "characteristics": (SXCP_CHARACTERISTICS,), "hair_config": (SXCP_HAIR_CONFIG,), "character_options": (SXCP_SCENE_CHARACTER_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") RETURN_NAMES = ("scene", "character_cast", "character_slot", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, scene, enabled, subject_type, label, slot_seed, age, ethnicity, figure, body, descriptor_detail, expression_enabled, expression_intensity, presence_mode, softcore_expression_intensity, hardcore_expression_intensity, manual="", ethnicity_list="", characteristics="", hair_config="", character_options="", seed_options="", ): parsed = _parse_scene(scene) options = _scene_option_values(character_options, "character") descriptor_detail = str(options.get("descriptor_detail") or descriptor_detail) presence_mode = str(options.get("presence_mode") or presence_mode) if "expression_enabled" in options: expression_enabled = bool(options.get("expression_enabled")) if "expression_intensity" in options: expression_intensity = float(options.get("expression_intensity")) if "softcore_expression_intensity" in options: softcore_expression_intensity = float(options.get("softcore_expression_intensity")) if "hardcore_expression_intensity" in options: hardcore_expression_intensity = float(options.get("hardcore_expression_intensity")) result = build_character_slot_json( subject_type=subject_type, label=label, slot_seed=slot_seed, age=age, manual=manual, ethnicity=ethnicity_list or ethnicity, figure=figure, body=body, characteristics=characteristics, hair_config=hair_config, descriptor_detail=descriptor_detail, expression_enabled=expression_enabled, expression_intensity=expression_intensity, presence_mode=presence_mode, softcore_expression_intensity=softcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity, enabled=enabled, character_cast=_base_config(parsed, "character_cast"), ) _set_config(parsed, "character_cast", result["character_cast"]) summary = result["summary"] if enabled else "character disabled" _add_history(parsed, "character", summary) layer = parsed.setdefault("layers", {}).setdefault("character", {"slots": []}) slots = layer.setdefault("slots", []) if isinstance(slots, list): slots.append( { "enabled": bool(enabled), "subject_type": subject_type, "label": label, "descriptor_detail": descriptor_detail, "presence_mode": presence_mode, "expression_enabled": bool(expression_enabled), "performance_prompt": str(options.get("performance_prompt") or ""), } ) _apply_layer_seed(parsed, "character", seed_options) return _dump(parsed), result["character_cast"], result["character_slot"], summary, _dump(parsed) class SxCPSceneWardrobe: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "subject_type": (["all", "woman", "man"], {"default": "woman"}), "subject_label": (SUBJECT_LABEL_CHOICES, {"default": "all"}), "clothing_override": (["profile_default", "random", "full", "minimal"], {"default": "profile_default"}), "softcore_outfit": ("STRING", {"default": "", "multiline": True}), "hardcore_clothing": ("STRING", {"default": "", "multiline": True}), "wardrobe_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "wardrobe_options": (SXCP_SCENE_WARDROBE_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_CHARACTER_CAST, "STRING", "STRING") RETURN_NAMES = ("scene", "character_cast", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, scene, enabled, subject_type, subject_label, clothing_override, softcore_outfit, hardcore_clothing, wardrobe_prompt, wardrobe_options="", seed_options="", ): parsed = _parse_scene(scene) options = _scene_option_values(wardrobe_options, "wardrobe") subject_type = str(options.get("subject_type") or subject_type) subject_label = str(options.get("subject_label") or subject_label) clothing_override = str(options.get("clothing_override") or clothing_override) wardrobe_state = str(options.get("wardrobe_state") or "no_change") avoid_clothing_when_nude = _truthy(options.get("avoid_clothing_when_nude", True)) softcore_outfit = str(options.get("softcore_outfit") or softcore_outfit or "") hardcore_clothing = str(options.get("hardcore_clothing") or hardcore_clothing or "") hardcore_clothing = _wardrobe_state_clothing(wardrobe_state, hardcore_clothing) accessories = str(options.get("accessories") or "") option_prompt = str(options.get("wardrobe_prompt") or "") if wardrobe_state in {"explicit_nude", "removed_nearby"} and avoid_clothing_when_nude: wardrobe_prompt = _joined_text(accessories, option_prompt) else: wardrobe_prompt = _joined_text(accessories, option_prompt, wardrobe_prompt) current_cast = _base_config(parsed, "character_cast") if enabled: updated_cast = _update_character_cast_wardrobe( current_cast, subject_type, subject_label, softcore_outfit, hardcore_clothing, ) if updated_cast: _set_config(parsed, "character_cast", updated_cast) else: updated_cast = current_cast layer = { "enabled": bool(enabled), "subject_type": subject_type, "subject_label": subject_label, "clothing_override": clothing_override, "wardrobe_state": wardrobe_state, "avoid_clothing_when_nude": bool(avoid_clothing_when_nude), "softcore_outfit": softcore_outfit or "", "hardcore_clothing": hardcore_clothing or "", "prompt": wardrobe_prompt or "", } summary = "disabled" if not enabled else f"{subject_type} {subject_label}; {clothing_override}" _set_layer(parsed, "wardrobe", layer, summary) _apply_layer_seed(parsed, "wardrobe", seed_options) return _dump(parsed), updated_cast or "", summary, _dump(parsed) class SxCPSceneLocation: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "preset": (location_pool_preset_choices(), {"default": "custom_only"}), "custom_location": ("STRING", {"default": "", "multiline": True}), "location_note": ("STRING", {"default": "", "multiline": True}), }, "optional": { "location_config": (SXCP_LOCATION_CONFIG,), "location_options": (SXCP_SCENE_LOCATION_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_LOCATION_CONFIG, "STRING", "STRING") RETURN_NAMES = ("scene", "location_config", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, enabled, combine_mode, preset, custom_location, location_note, location_config="", location_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(location_options, "location") location_note = _joined_text( location_note, options.get("location_note"), options.get("foreground_anchors"), options.get("midground_layer"), options.get("background_repetition"), ) custom = "\n".join(_text_parts(custom_location, location_note)) config = build_location_pool_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_locations=custom, location_config=location_config or _base_config(parsed, "location_config"), ) _set_config(parsed, "location_config", config) config_summary = _json_dict(config).get("summary", "") _set_layer( parsed, "location", { "preset": preset, "custom_location": custom, "foreground_anchors": options.get("foreground_anchors", ""), "midground_layer": options.get("midground_layer", ""), "background_repetition": options.get("background_repetition", ""), "visibility_level": options.get("visibility_level", "auto"), "public_level": options.get("public_level", "auto"), "summary": config_summary, }, config_summary, ) _apply_layer_seed(parsed, "location", seed_options) return _dump(parsed), config, config_summary, _dump(parsed) class SxCPSceneSetDressing: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "foreground_anchors": ("STRING", {"default": "", "multiline": True}), "repeated_background": ("STRING", {"default": "", "multiline": True}), "props": ("STRING", {"default": "", "multiline": True}), "set_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "set_options": (SXCP_SCENE_SET_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, enabled, foreground_anchors, repeated_background, props, set_prompt, set_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(set_options, "set_dressing") foreground_anchors = _joined_text(foreground_anchors, options.get("foreground_anchors")) repeated_background = _joined_text(repeated_background, options.get("repeated_background")) props = _joined_text(props, options.get("props")) sensory_details = str(options.get("sensory_details") or "") set_prompt = _joined_text(set_prompt, options.get("set_prompt")) prompt = _joined_text(foreground_anchors, repeated_background, props, sensory_details, set_prompt) if enabled else "" summary = "set dressing disabled" if not enabled else (prompt[:120] or "set dressing empty") _set_layer( parsed, "set_dressing", { "enabled": bool(enabled), "foreground_anchors": foreground_anchors or "", "repeated_background": repeated_background or "", "props": props or "", "sensory_details": sensory_details, "prompt": prompt, }, summary, ) _apply_layer_seed(parsed, "set_dressing", seed_options) return _scene_out(parsed) class SxCPSceneBlocking: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "blocking_mode": (["auto", "standing", "sitting", "kneeling", "lying", "bent_over", "custom"], {"default": "auto"}), "subject_placement": ("STRING", {"default": "", "multiline": True}), "body_relation": ("STRING", {"default": "", "multiline": True}), "custom_blocking": ("STRING", {"default": "", "multiline": True}), }, "optional": { "blocking_options": (SXCP_SCENE_BLOCKING_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, enabled, blocking_mode, subject_placement, body_relation, custom_blocking, blocking_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(blocking_options, "blocking") blocking_mode = str(options.get("blocking_mode") or blocking_mode) subject_placement = _joined_text(subject_placement, options.get("subject_placement")) body_relation = _joined_text(body_relation, options.get("body_relation")) body_orientation = str(options.get("body_orientation") or "auto") depth_plane = str(options.get("depth_plane") or "auto") distance_note = str(options.get("distance_note") or "") custom_blocking = _joined_text(custom_blocking, options.get("custom_blocking")) generated = " ".join( value.replace("_", " ") for value in (body_orientation, depth_plane) if value and value != "auto" ).strip() prompt = _joined_text(subject_placement, body_relation, generated, distance_note, custom_blocking) if enabled else "" summary = "blocking disabled" if not enabled else f"{blocking_mode}: {prompt[:100] or 'auto'}" _set_layer( parsed, "blocking", { "enabled": bool(enabled), "blocking_mode": blocking_mode, "subject_placement": subject_placement or "", "body_relation": body_relation or "", "body_orientation": body_orientation, "depth_plane": depth_plane, "distance_note": distance_note, "prompt": prompt, }, summary, ) _apply_layer_seed(parsed, "blocking", seed_options) return _scene_out(parsed) class SxCPSceneAction: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "scene_kind": (SCENE_KINDS, {"default": "regular"}), "category_preset": (["no_change"] + category_preset_choices(), {"default": "no_change"}), "action_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), "action_options": (SXCP_SCENE_ACTION_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING") RETURN_NAMES = ("scene", "hardcore_position_config", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, enabled, scene_kind, category_preset, action_prompt, hardcore_position_config="", action_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(action_options, "action") scene_kind = str(options.get("scene_kind") or scene_kind) category_preset = str(options.get("category_preset") or category_preset) action_family = str(options.get("action_family") or "no_change") action_prompt = _joined_text(action_prompt, options.get("action_prompt")) if enabled and category_preset != "no_change": _set_config(parsed, "category_config", build_category_config_json(category_preset, "random")) elif enabled and scene_kind == "hardcore" and not _base_config(parsed, "category_config"): _set_config(parsed, "category_config", build_category_config_json("hardcore_pose", "random")) if hardcore_position_config: _set_config(parsed, "hardcore_position_config", hardcore_position_config) layer = { "enabled": bool(enabled), "scene_kind": scene_kind, "action_family": action_family, "category_preset": category_preset, "prompt": action_prompt or "", } summary = "action disabled" if not enabled else f"{scene_kind}; category={category_preset}" _set_layer(parsed, "action", layer, summary) _apply_layer_seed(parsed, "action", seed_options) return _dump(parsed), hardcore_position_config or _base_config(parsed, "hardcore_position_config"), summary, _dump(parsed) class SxCPScenePerformance: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity_mode": (["profile_default", "random", "fixed"], {"default": "profile_default"}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "performance_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "performance_options": (SXCP_SCENE_PERFORMANCE_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, expression_enabled, expression_intensity_mode, expression_intensity, performance_prompt, performance_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(performance_options, "performance") if "expression_enabled" in options: expression_enabled = bool(options.get("expression_enabled")) expression_intensity_mode = str(options.get("expression_intensity_mode") or expression_intensity_mode) if "expression_intensity" in options: expression_intensity = float(options.get("expression_intensity")) gaze = str(options.get("gaze") or "auto") hand_placement = str(options.get("hand_placement") or "auto") body_tension = str(options.get("body_tension") or "auto") generated = " ".join( value.replace("_", " ") for value in (gaze, hand_placement, body_tension) if value and value != "auto" ).strip() performance_prompt = _joined_text(generated, performance_prompt, options.get("performance_prompt")) layer = { "expression_enabled": bool(expression_enabled), "expression_intensity_mode": expression_intensity_mode, "expression_intensity": float(expression_intensity), "gaze": gaze, "hand_placement": hand_placement, "body_tension": body_tension, "prompt": performance_prompt or "", } summary = "expression disabled" if not expression_enabled else f"expression {expression_intensity_mode}" _set_layer(parsed, "performance", layer, summary) _apply_layer_seed(parsed, "performance", seed_options) return _scene_out(parsed) class SxCPSceneCamera: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "camera_mode": (camera_mode_choices(), {"default": "standard"}), "shot_size": (camera_shot_choices(), {"default": "auto"}), "angle": (camera_angle_choices(), {"default": "auto"}), "lens": (camera_lens_choices(), {"default": "auto"}), "distance": (camera_distance_choices(), {"default": "auto"}), "orientation": (camera_orientation_choices(), {"default": "auto"}), "phone_visibility": (camera_phone_choices(), {"default": "auto"}), "priority": (camera_priority_choices(), {"default": "strong"}), "camera_detail": (camera_detail_choices(), {"default": "compact"}), "camera_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "camera_config": (SXCP_CAMERA_CONFIG,), "camera_options": (SXCP_SCENE_CAMERA_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_CAMERA_CONFIG, "STRING", "STRING") RETURN_NAMES = ("scene", "camera_config", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, scene, enabled, camera_mode, shot_size, angle, lens, distance, orientation, phone_visibility, priority, camera_detail, camera_prompt, camera_config="", camera_options="", seed_options="", ): parsed = _parse_scene(scene) options = _scene_option_values(camera_options, "camera") camera_prompt = _joined_text(camera_prompt, options.get("camera_prompt")) camera_source = str(options.get("camera_source") or "manual") preserve_location_layout = bool(options.get("preserve_location_layout", True)) config = "" if enabled: config = camera_config or build_camera_config_json( camera_mode=camera_mode, shot_size=shot_size, angle=angle, lens=lens, distance=distance, orientation=orientation, phone_visibility=phone_visibility, priority=priority, camera_detail=camera_detail, ) _set_config(parsed, "camera_config", config) layer = { "enabled": bool(enabled), "camera_mode": camera_mode, "shot_size": shot_size, "angle": angle, "lens": lens, "distance": distance, "orientation": orientation, "phone_visibility": phone_visibility, "priority": priority, "camera_detail": camera_detail, "camera_source": camera_source, "preserve_location_layout": preserve_location_layout, "prompt": camera_prompt or "", } summary = "camera disabled" if not enabled else f"{camera_mode}; {shot_size}; {angle}" _set_layer(parsed, "camera", layer, summary) _apply_layer_seed(parsed, "camera", seed_options) return _dump(parsed), config, summary, _dump(parsed) class SxCPSceneComposition: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "preset": (composition_pool_preset_choices(), {"default": "no_outfit_check"}), "custom_composition": ("STRING", {"default": "", "multiline": True}), "composition_prompt": ("STRING", {"default": "", "multiline": True}), }, "optional": { "composition_config": (SXCP_COMPOSITION_CONFIG,), "composition_options": (SXCP_SCENE_COMPOSITION_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_COMPOSITION_CONFIG, "STRING", "STRING") RETURN_NAMES = ("scene", "composition_config", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, enabled, combine_mode, preset, custom_composition, composition_prompt, composition_config="", composition_options="", seed_options=""): parsed = _parse_scene(scene) options = _scene_option_values(composition_options, "composition") generated = " ".join( value.replace("_", " ") for value in (options.get("readability_target"), options.get("crop"), options.get("occlusion")) if value and value != "auto" ).strip() composition_prompt = _joined_text(generated, composition_prompt, options.get("composition_prompt")) custom = "\n".join(_text_parts(custom_composition, composition_prompt)) config = build_composition_pool_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_compositions=custom, composition_config=composition_config or _base_config(parsed, "composition_config"), ) _set_config(parsed, "composition_config", config) config_summary = _json_dict(config).get("summary", "") _set_layer( parsed, "composition", { "preset": preset, "custom_composition": custom, "readability_target": options.get("readability_target", "auto"), "crop": options.get("crop", "auto"), "occlusion": options.get("occlusion", "auto"), "summary": config_summary, }, config_summary, ) _apply_layer_seed(parsed, "composition", seed_options) return _dump(parsed), config, config_summary, _dump(parsed) class SxCPSceneLighting: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "enabled": ("BOOLEAN", {"default": True}), "lighting_source": (["auto", "daylight", "window_light", "practical_lamps", "neon", "studio", "phone_flash", "custom"], {"default": "auto"}), "lighting_softness": (["auto", "soft", "balanced", "hard"], {"default": "auto"}), "lighting_contrast": (["auto", "low", "medium", "high"], {"default": "auto"}), "color_temperature": (["auto", "warm", "neutral", "cool", "mixed"], {"default": "auto"}), "custom_lighting": ("STRING", {"default": "", "multiline": True}), }, "optional": { "lighting_options": (SXCP_SCENE_LIGHTING_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, scene, enabled, lighting_source, lighting_softness, lighting_contrast, color_temperature, custom_lighting, lighting_options="", seed_options="", ): parsed = _parse_scene(scene) options = _scene_option_values(lighting_options, "lighting") lighting_source = str(options.get("lighting_source") or lighting_source) lighting_softness = str(options.get("lighting_softness") or lighting_softness) lighting_contrast = str(options.get("lighting_contrast") or lighting_contrast) color_temperature = str(options.get("color_temperature") or color_temperature) time_of_day = str(options.get("time_of_day") or "auto") custom_lighting = _joined_text(custom_lighting, options.get("custom_lighting")) generated = " ".join( value.replace("_", " ") for value in (time_of_day, lighting_softness, lighting_contrast, color_temperature, lighting_source) if value and value != "auto" ).strip() prompt = _joined_text(generated, custom_lighting) if enabled else "" summary = "lighting disabled" if not enabled else (prompt or "lighting auto") _set_layer( parsed, "lighting", { "enabled": bool(enabled), "lighting_source": lighting_source, "lighting_softness": lighting_softness, "lighting_contrast": lighting_contrast, "color_temperature": color_temperature, "time_of_day": time_of_day, "prompt": prompt, }, summary, ) _apply_layer_seed(parsed, "lighting", seed_options) return _scene_out(parsed) class SxCPSceneBranchPair: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "continuity": (["same_creator_same_room", "same_creator_new_scene"], {"default": "same_creator_same_room"}), "platform_style": (list(INSTA_OF_PLATFORM_STYLES), {"default": "hybrid"}), }, "optional": { "branch_options": (SXCP_SCENE_BRANCH_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), } } RETURN_TYPES = (SXCP_SCENE, SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("softcore_scene", "hardcore_scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene, continuity, platform_style, branch_options="", seed_options=""): soft_scene = _parse_scene(scene) hard_scene = _parse_scene(scene) shared_options = _branch_option_values(branch_options, "softcore") or _branch_option_values(branch_options, "hardcore") continuity = str(shared_options.get("continuity") or continuity) platform_style = str(shared_options.get("platform_style") or platform_style) for branch_name, branch_scene in (("softcore", soft_scene), ("hardcore", hard_scene)): branch_scene["active_branch"] = branch_name branch_scene["pair"] = {"continuity": continuity, "platform_style": platform_style} _branch(branch_scene, branch_name) target_options = _branch_option_values(branch_options, branch_name) if target_options.get("extra_positive"): _branch(branch_scene, branch_name)["extra_positive"] = str(target_options.get("extra_positive") or "") _apply_layer_seed(branch_scene, f"{branch_name}_branch", seed_options, branch_name) _add_history(branch_scene, "branch_pair", f"{branch_name}; {continuity}; {platform_style}") summary = f"pair branch; {continuity}; {platform_style}" metadata = {"softcore_scene": soft_scene, "hardcore_scene": hard_scene, "summary": summary} return _dump(soft_scene), _dump(hard_scene), summary, _dump(metadata) class SxCPSoftcoreBranchOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "softcore_cast": (["solo", "same_as_hardcore"], {"default": "solo"}), "softcore_level": (list(INSTA_OF_SOFT_LEVELS), {"default": "lingerie_tease"}), "softcore_expression_enabled": ("BOOLEAN", {"default": True}), "softcore_expression_intensity": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01}), "softcore_camera_mode": (SOFTCORE_CAMERA_CHOICES, {"default": "from_camera_config"}), "camera_detail": (["from_camera_config"] + camera_detail_choices(), {"default": "from_camera_config"}), "extra_positive": ("STRING", {"default": "", "multiline": True}), }, "optional": { "softcore_camera_config": (SXCP_CAMERA_CONFIG,), "branch_options": (SXCP_SCENE_BRANCH_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("scene", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, scene, softcore_cast, softcore_level, softcore_expression_enabled, softcore_expression_intensity, softcore_camera_mode, camera_detail, extra_positive, softcore_camera_config="", branch_options="", seed_options="", ): parsed = _parse_scene(scene) options = _branch_option_values(branch_options, "softcore") if options.get("platform_style"): parsed.setdefault("pair", {})["platform_style"] = str(options.get("platform_style")) if options.get("continuity"): parsed.setdefault("pair", {})["continuity"] = str(options.get("continuity")) extra_positive = _joined_text(extra_positive, options.get("extra_positive")) branch = _branch(parsed, "softcore") branch["options"].update( { "softcore_cast": softcore_cast, "softcore_level": softcore_level, "softcore_expression_enabled": bool(softcore_expression_enabled), "softcore_expression_intensity": float(softcore_expression_intensity), "softcore_camera_mode": softcore_camera_mode, "camera_detail": camera_detail, } ) branch["extra_positive"] = extra_positive or "" if softcore_camera_config: branch["configs"]["camera_config"] = softcore_camera_config summary = f"softcore {softcore_level}; cast={softcore_cast}; camera={softcore_camera_mode}" _add_history(parsed, "softcore_branch_options", summary) _apply_layer_seed(parsed, "softcore_branch", seed_options, "softcore") return _dump(parsed), summary, _dump(parsed) class SxCPHardcoreBranchOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), "hardcore_cast": (["use_counts", "couple", "threesome", "group"], {"default": "use_counts"}), "hardcore_women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "hardcore_men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "hardcore_level": (list(INSTA_OF_HARDCORE_LEVELS), {"default": "hardcore"}), "hardcore_expression_enabled": ("BOOLEAN", {"default": True}), "hardcore_expression_intensity": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}), "hardcore_clothing_continuity": (HARDCORE_CLOTHING_CONTINUITY_CHOICES, {"default": "partially_removed"}), "hardcore_camera_mode": (HARDCORE_CAMERA_CHOICES, {"default": "from_camera_config"}), "camera_detail": (["from_camera_config"] + camera_detail_choices(), {"default": "from_camera_config"}), "hardcore_detail_density": (hardcore_detail_density_choices(), {"default": "balanced"}), "extra_positive": ("STRING", {"default": "", "multiline": True}), }, "optional": { "hardcore_camera_config": (SXCP_CAMERA_CONFIG,), "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), "branch_options": (SXCP_SCENE_BRANCH_OPTIONS,), "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } RETURN_TYPES = (SXCP_SCENE, SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING") RETURN_NAMES = ("scene", "hardcore_position_config", "summary", "metadata_json") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build( self, scene, hardcore_cast, hardcore_women_count, hardcore_men_count, hardcore_level, hardcore_expression_enabled, hardcore_expression_intensity, hardcore_clothing_continuity, hardcore_camera_mode, camera_detail, hardcore_detail_density, extra_positive, hardcore_camera_config="", hardcore_position_config="", branch_options="", seed_options="", ): parsed = _parse_scene(scene) options = _branch_option_values(branch_options, "hardcore") if options.get("platform_style"): parsed.setdefault("pair", {})["platform_style"] = str(options.get("platform_style")) if options.get("continuity"): parsed.setdefault("pair", {})["continuity"] = str(options.get("continuity")) extra_positive = _joined_text(extra_positive, options.get("extra_positive")) branch = _branch(parsed, "hardcore") branch["options"].update( { "hardcore_cast": hardcore_cast, "hardcore_women_count": int(hardcore_women_count), "hardcore_men_count": int(hardcore_men_count), "hardcore_level": hardcore_level, "hardcore_expression_enabled": bool(hardcore_expression_enabled), "hardcore_expression_intensity": float(hardcore_expression_intensity), "hardcore_clothing_continuity": hardcore_clothing_continuity, "hardcore_camera_mode": hardcore_camera_mode, "camera_detail": camera_detail, "hardcore_detail_density": hardcore_detail_density, } ) branch["extra_positive"] = extra_positive or "" if hardcore_camera_config: branch["configs"]["camera_config"] = hardcore_camera_config if hardcore_position_config: branch["configs"]["hardcore_position_config"] = hardcore_position_config summary = f"hardcore {hardcore_level}; cast={hardcore_cast}; camera={hardcore_camera_mode}" _add_history(parsed, "hardcore_branch_options", summary) _apply_layer_seed(parsed, "hardcore_branch", seed_options, "hardcore") return _dump(parsed), hardcore_position_config or _base_config(parsed, "hardcore_position_config"), summary, _dump(parsed) class SxCPSceneOutput: @classmethod def INPUT_TYPES(cls): return { "required": { "scene": (SXCP_SCENE,), } } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", SXCP_SCENE, "STRING", "STRING") RETURN_NAMES = ("prompt", "negative_prompt", "caption", "metadata_json", "scene", "category", "subcategory") FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, scene): parsed = _parse_scene(scene) configs = _compat_configs(parsed, str(parsed.get("active_branch") or "")) row = build_prompt_from_configs( row_number=int(parsed.get("row_number", 1)), start_index=int(parsed.get("start_index", 41)), seed=int(parsed.get("seed", 20260614)), category_config=configs["category_config"], cast_config=configs["cast_config"], generation_profile=configs["generation_profile"], filter_config=configs["filter_config"], seed_config=configs["seed_config"], camera_config=configs["camera_config"], character_profile=configs["character_profile"], character_cast=configs["character_cast"], hardcore_position_config=configs["hardcore_position_config"], location_config=configs["location_config"], composition_config=configs["composition_config"], style_config=configs["style_config"], extra_positive=configs["extra_positive"], extra_negative=configs["extra_negative"], ) row = dict(row) row["scene_chain"] = parsed metadata = _dump(row) return ( row["prompt"], row["negative_prompt"], row["caption"], metadata, _dump(parsed), row.get("main_category", ""), row.get("subcategory", ""), ) class SxCPScenePairOutput: @classmethod def INPUT_TYPES(cls): return { "required": { "softcore_scene": (SXCP_SCENE,), "hardcore_scene": (SXCP_SCENE,), } } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", SXCP_SCENE) RETURN_NAMES = ( "softcore_prompt", "hardcore_prompt", "softcore_negative_prompt", "hardcore_negative_prompt", "softcore_caption", "hardcore_caption", "shared_descriptor", "metadata_json", "scene_metadata_json", ) FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" def build(self, softcore_scene, hardcore_scene): soft_scene = _parse_scene(softcore_scene) hard_scene = _parse_scene(hardcore_scene) base_configs = _compat_configs(soft_scene, "softcore") hard_configs = _compat_configs(hard_scene, "hardcore") shared_seed_config = _base_config(soft_scene, "seed_config") options_json = _pair_options(soft_scene, hard_scene) row = build_insta_of_pair( row_number=int(soft_scene.get("row_number", 1)), start_index=int(soft_scene.get("start_index", 41)), seed=int(soft_scene.get("seed", 20260614)), ethnicity="any", figure="random", no_plus_women=False, no_black=False, trigger=str(soft_scene.get("trigger") or "sxcpinup_coloredpencil"), prepend_trigger_to_prompt=bool(soft_scene.get("prepend_trigger_to_prompt", True)), seed_config=shared_seed_config, softcore_seed_config=base_configs["seed_config"], hardcore_seed_config=hard_configs["seed_config"], options_json=options_json, filter_config=base_configs["filter_config"] or hard_configs["filter_config"], camera_config=base_configs["camera_config"], softcore_camera_config=base_configs["camera_config"], hardcore_camera_config=hard_configs["camera_config"], character_profile=base_configs["character_profile"] or hard_configs["character_profile"], character_cast=base_configs["character_cast"] or hard_configs["character_cast"], hardcore_position_config=hard_configs["hardcore_position_config"], location_config=base_configs["location_config"] or hard_configs["location_config"], composition_config=base_configs["composition_config"] or hard_configs["composition_config"], style_config=base_configs["style_config"] or hard_configs["style_config"], extra_positive=_joined_text(base_configs["extra_positive"], hard_configs["extra_positive"]), extra_negative=base_configs["extra_negative"] or hard_configs["extra_negative"], ) row = dict(row) row["scene_chain"] = {"softcore": soft_scene, "hardcore": hard_scene} metadata = _dump(row) return ( row["softcore_prompt"], row["hardcore_prompt"], row["softcore_negative_prompt"], row["hardcore_negative_prompt"], row["softcore_caption"], row["hardcore_caption"], row["shared_descriptor"], metadata, _dump({"softcore": soft_scene, "hardcore": hard_scene}), ) NODE_CLASS_MAPPINGS = { "SxCPSceneLayerSeedOptions": SxCPSceneLayerSeedOptions, "SxCPSceneCastOptions": SxCPSceneCastOptions, "SxCPSceneCharacterOptions": SxCPSceneCharacterOptions, "SxCPSceneWardrobeOptions": SxCPSceneWardrobeOptions, "SxCPSceneLocationLayoutOptions": SxCPSceneLocationLayoutOptions, "SxCPSceneSetDressingOptions": SxCPSceneSetDressingOptions, "SxCPSceneBlockingOptions": SxCPSceneBlockingOptions, "SxCPSceneActionOptions": SxCPSceneActionOptions, "SxCPScenePerformanceOptions": SxCPScenePerformanceOptions, "SxCPSceneCameraOptions": SxCPSceneCameraOptions, "SxCPSceneCompositionOptions": SxCPSceneCompositionOptions, "SxCPSceneLightingOptions": SxCPSceneLightingOptions, "SxCPSceneBranchOptions": SxCPSceneBranchOptions, "SxCPSceneStart": SxCPSceneStart, "SxCPSceneCast": SxCPSceneCast, "SxCPSceneCharacter": SxCPSceneCharacter, "SxCPSceneWardrobe": SxCPSceneWardrobe, "SxCPSceneLocation": SxCPSceneLocation, "SxCPSceneSetDressing": SxCPSceneSetDressing, "SxCPSceneBlocking": SxCPSceneBlocking, "SxCPSceneAction": SxCPSceneAction, "SxCPScenePerformance": SxCPScenePerformance, "SxCPSceneCamera": SxCPSceneCamera, "SxCPSceneComposition": SxCPSceneComposition, "SxCPSceneLighting": SxCPSceneLighting, "SxCPSceneBranchPair": SxCPSceneBranchPair, "SxCPSoftcoreBranchOptions": SxCPSoftcoreBranchOptions, "SxCPHardcoreBranchOptions": SxCPHardcoreBranchOptions, "SxCPSceneOutput": SxCPSceneOutput, "SxCPScenePairOutput": SxCPScenePairOutput, } NODE_DISPLAY_NAME_MAPPINGS = { "SxCPSceneLayerSeedOptions": "SxCP Scene Layer Seed Options", "SxCPSceneCastOptions": "SxCP Scene Cast Options", "SxCPSceneCharacterOptions": "SxCP Scene Character Options", "SxCPSceneWardrobeOptions": "SxCP Scene Wardrobe Options", "SxCPSceneLocationLayoutOptions": "SxCP Scene Location Layout Options", "SxCPSceneSetDressingOptions": "SxCP Scene Set Dressing Options", "SxCPSceneBlockingOptions": "SxCP Scene Blocking Options", "SxCPSceneActionOptions": "SxCP Scene Action Options", "SxCPScenePerformanceOptions": "SxCP Scene Performance Options", "SxCPSceneCameraOptions": "SxCP Scene Camera Options", "SxCPSceneCompositionOptions": "SxCP Scene Composition Options", "SxCPSceneLightingOptions": "SxCP Scene Lighting Options", "SxCPSceneBranchOptions": "SxCP Scene Branch Options", "SxCPSceneStart": "SxCP Scene Start", "SxCPSceneCast": "SxCP Scene Cast", "SxCPSceneCharacter": "SxCP Scene Character", "SxCPSceneWardrobe": "SxCP Scene Wardrobe", "SxCPSceneLocation": "SxCP Scene Location", "SxCPSceneSetDressing": "SxCP Scene Set Dressing", "SxCPSceneBlocking": "SxCP Scene Blocking", "SxCPSceneAction": "SxCP Scene Action", "SxCPScenePerformance": "SxCP Scene Performance", "SxCPSceneCamera": "SxCP Scene Camera", "SxCPSceneComposition": "SxCP Scene Composition", "SxCPSceneLighting": "SxCP Scene Lighting", "SxCPSceneBranchPair": "SxCP Scene Branch Pair", "SxCPSoftcoreBranchOptions": "SxCP Softcore Branch Options", "SxCPHardcoreBranchOptions": "SxCP Hardcore Branch Options", "SxCPSceneOutput": "SxCP Scene Output", "SxCPScenePairOutput": "SxCP Scene Pair Output", }