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": ("content",),
    "location": ("scene",),
    "set_dressing": ("scene",),
    "blocking": ("pose",),
    "action": ("pose", "role"),
    "performance": ("expression",),
    "camera": ("composition",),
    "composition": ("composition",),
    "lighting": ("composition",),
    "softcore_branch": ("content", "pose", "role"),
    "hardcore_branch": ("pose", "role"),
}
SCENE_REROLL_GROUPS = {
    "none": (),
    "category": ("category",),
    "subcategory": ("subcategory",),
    "content": ("content",),
    "person": ("person",),
    "scene": ("scene",),
    "pose": ("pose", "role"),
    "role": ("role",),
    "expression": ("expression",),
    "composition": ("composition",),
    "content_pose": ("content", "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",
}
