diff --git a/README.md b/README.md index 6090256..2d4033a 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,16 @@ blocking, action, performance, camera, composition, and lighting layers. Use `SxCP Hardcore Branch Options`, then render both sides through `SxCP Scene Pair Output`. +Each layer can stay light on the main chain and take optional side-node inputs: +`SxCP Scene Layer Seed Options`, `SxCP Scene Cast Options`, +`SxCP Scene Character Options`, `SxCP Scene Wardrobe Options`, +`SxCP Scene Location Layout Options`, `SxCP Scene Set Dressing Options`, +`SxCP Scene Blocking Options`, `SxCP Scene Action Options`, +`SxCP Scene Performance Options`, `SxCP Scene Camera Options`, +`SxCP Scene Composition Options`, `SxCP Scene Lighting Options`, and +`SxCP Scene Branch Options`. These side nodes are chainable and only override +the layer they are connected to. + The current v2 output nodes intentionally reuse the existing builder, Insta/OF pair, and formatter metadata routes. This keeps old workflows working while giving new workflows a cleaner movie-scene structure. diff --git a/__init__.py b/__init__.py index 5d6678f..a1d45a2 100644 --- a/__init__.py +++ b/__init__.py @@ -25,6 +25,19 @@ SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT" SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE" SXCP_SCENE = "SXCP_SCENE" +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" try: from .node_tooltips import install_input_tooltips as _install_input_tooltips diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index a76cfc3..94aa31e 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -80,6 +80,16 @@ V2 scene-chain display nodes: `SxCP Scene Cast`, `SxCP Scene Character`, `SxCP Scene Branch Pair`, `SxCP Softcore Branch Options`, and `SxCP Hardcore Branch Options`. +V2 scene-chain side option nodes: `SxCP Scene Layer Seed Options`, +`SxCP Scene Cast Options`, `SxCP Scene Character Options`, +`SxCP Scene Wardrobe Options`, `SxCP Scene Location Layout Options`, +`SxCP Scene Set Dressing Options`, `SxCP Scene Blocking Options`, +`SxCP Scene Action Options`, `SxCP Scene Performance Options`, +`SxCP Scene Camera Options`, `SxCP Scene Composition Options`, +`SxCP Scene Lighting Options`, and `SxCP Scene Branch Options`. These nodes +feed typed option sockets into the main scene chain so the visible chain can +stay organized by movie-scene layer while deeper knobs remain adjacent. + Core helper ownership: | Python module | What it owns | diff --git a/node_scene.py b/node_scene.py index 88110da..8cb70de 100644 --- a/node_scene.py +++ b/node_scene.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy import json +import random from typing import Any try: @@ -42,6 +43,7 @@ try: 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. @@ -82,6 +84,7 @@ except ImportError: # Allows local smoke tests from the repository root. generation_profile_choices, hardcore_detail_density_choices, location_pool_preset_choices, + seed_reroll_axis_choices, subcategory_choices, ) @@ -102,8 +105,23 @@ 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"] @@ -113,6 +131,144 @@ 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]: @@ -204,6 +360,173 @@ 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))) + if seed_mode == "random": + resolved_seed = random.SystemRandom().randint(0, 0xFFFFFFFF) + 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 _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") @@ -358,6 +681,515 @@ def _pair_options(soft_scene: dict[str, Any], hard_scene: dict[str, Any]) -> str ) +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): @@ -444,6 +1276,8 @@ class SxCPSceneCast: }, "optional": { "cast_config": (SXCP_CAST_CONFIG,), + "cast_options": (SXCP_SCENE_CAST_OPTIONS,), + "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } @@ -452,8 +1286,14 @@ class SxCPSceneCast: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, cast_mode, women_count, men_count, central_subject, pov_participant, cast_config=""): + 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 = { @@ -465,6 +1305,7 @@ class SxCPSceneCast: } 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) @@ -494,6 +1335,8 @@ class SxCPSceneCharacter: "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,), }, } @@ -523,8 +1366,21 @@ class SxCPSceneCharacter: 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, @@ -548,6 +1404,21 @@ class SxCPSceneCharacter: _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) @@ -564,7 +1435,11 @@ class SxCPSceneWardrobe: "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") @@ -582,8 +1457,25 @@ class SxCPSceneWardrobe: 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( @@ -602,12 +1494,15 @@ class SxCPSceneWardrobe: "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) @@ -625,6 +1520,8 @@ class SxCPSceneLocation: }, "optional": { "location_config": (SXCP_LOCATION_CONFIG,), + "location_options": (SXCP_SCENE_LOCATION_OPTIONS,), + "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } @@ -633,8 +1530,16 @@ class SxCPSceneLocation: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, enabled, combine_mode, preset, custom_location, location_note, location_config=""): + 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, @@ -645,7 +1550,22 @@ class SxCPSceneLocation: ) _set_config(parsed, "location_config", config) config_summary = _json_dict(config).get("summary", "") - _set_layer(parsed, "location", {"preset": preset, "custom_location": custom, "summary": config_summary}, config_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) @@ -660,7 +1580,11 @@ class SxCPSceneSetDressing: "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") @@ -668,9 +1592,15 @@ class SxCPSceneSetDressing: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, enabled, foreground_anchors, repeated_background, props, set_prompt): + def build(self, scene, enabled, foreground_anchors, repeated_background, props, set_prompt, set_options="", seed_options=""): parsed = _parse_scene(scene) - prompt = _joined_text(foreground_anchors, repeated_background, props, set_prompt) if enabled else "" + 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, @@ -680,10 +1610,12 @@ class SxCPSceneSetDressing: "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) @@ -698,7 +1630,11 @@ class SxCPSceneBlocking: "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") @@ -706,9 +1642,22 @@ class SxCPSceneBlocking: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, enabled, blocking_mode, subject_placement, body_relation, custom_blocking): + def build(self, scene, enabled, blocking_mode, subject_placement, body_relation, custom_blocking, blocking_options="", seed_options=""): parsed = _parse_scene(scene) - prompt = _joined_text(subject_placement, body_relation, custom_blocking) if enabled else "" + 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, @@ -718,10 +1667,14 @@ class SxCPSceneBlocking: "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) @@ -738,6 +1691,8 @@ class SxCPSceneAction: }, "optional": { "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), + "action_options": (SXCP_SCENE_ACTION_OPTIONS,), + "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } @@ -746,8 +1701,13 @@ class SxCPSceneAction: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, enabled, scene_kind, category_preset, action_prompt, hardcore_position_config=""): + 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"): @@ -757,11 +1717,13 @@ class SxCPSceneAction: 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) @@ -775,7 +1737,11 @@ class SxCPScenePerformance: "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") @@ -783,16 +1749,35 @@ class SxCPScenePerformance: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, expression_enabled, expression_intensity_mode, expression_intensity, performance_prompt): + 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) @@ -816,6 +1801,8 @@ class SxCPSceneCamera: }, "optional": { "camera_config": (SXCP_CAMERA_CONFIG,), + "camera_options": (SXCP_SCENE_CAMERA_OPTIONS,), + "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } @@ -839,8 +1826,14 @@ class SxCPSceneCamera: 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( @@ -866,10 +1859,13 @@ class SxCPSceneCamera: "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) @@ -887,6 +1883,8 @@ class SxCPSceneComposition: }, "optional": { "composition_config": (SXCP_COMPOSITION_CONFIG,), + "composition_options": (SXCP_SCENE_COMPOSITION_OPTIONS,), + "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } @@ -895,8 +1893,15 @@ class SxCPSceneComposition: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, enabled, combine_mode, preset, custom_composition, composition_prompt, composition_config=""): + 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, @@ -907,7 +1912,20 @@ class SxCPSceneComposition: ) _set_config(parsed, "composition_config", config) config_summary = _json_dict(config).get("summary", "") - _set_layer(parsed, "composition", {"preset": preset, "custom_composition": custom, "summary": config_summary}, config_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) @@ -923,7 +1941,11 @@ class SxCPSceneLighting: "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") @@ -940,11 +1962,20 @@ class SxCPSceneLighting: 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 (lighting_softness, lighting_contrast, color_temperature, lighting_source) + 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 "" @@ -958,10 +1989,12 @@ class SxCPSceneLighting: "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) @@ -973,6 +2006,10 @@ class SxCPSceneBranchPair: "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,), } } @@ -981,13 +2018,20 @@ class SxCPSceneBranchPair: FUNCTION = "build" CATEGORY = "prompt_builder/v2_scene" - def build(self, scene, continuity, platform_style): + 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} @@ -1010,6 +2054,8 @@ class SxCPSoftcoreBranchOptions: }, "optional": { "softcore_camera_config": (SXCP_CAMERA_CONFIG,), + "branch_options": (SXCP_SCENE_BRANCH_OPTIONS,), + "seed_options": (SXCP_SCENE_LAYER_SEED,), }, } @@ -1029,8 +2075,16 @@ class SxCPSoftcoreBranchOptions: 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( { @@ -1047,6 +2101,7 @@ class SxCPSoftcoreBranchOptions: 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) @@ -1071,6 +2126,8 @@ class SxCPHardcoreBranchOptions: "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,), }, } @@ -1095,8 +2152,16 @@ class SxCPHardcoreBranchOptions: 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( { @@ -1119,6 +2184,7 @@ class SxCPHardcoreBranchOptions: 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) @@ -1243,6 +2309,19 @@ class SxCPScenePairOutput: 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, @@ -1263,6 +2342,19 @@ NODE_CLASS_MAPPINGS = { } 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", diff --git a/node_tooltips.py b/node_tooltips.py index 8dafbac..a67a9d6 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -17,6 +17,10 @@ COMMON_INPUT_TOOLTIPS = { "filter_config": "Ethnicity/body filter config. Ethnicity List can feed this too.", "ethnicity_list": "Optional ethnicity pool. When connected, it overrides the slot or generator ethnicity picker.", "seed_config": "Per-axis seed config. Use Global Seed for full reproducibility, Seed Locker to reroll one axis, or Seed Control for manual axis seeds.", + "layer": "Scene layer affected by this side node. all applies to every compatible scene node that receives the options.", + "seed_mode": "follow_global uses the scene seed, fixed uses the seed field, random resolves a fresh seed at queue time, disabled does nothing.", + "row_behavior": "same_for_all_rows keeps the option seed as-is; vary_by_row offsets it by row number before writing axis seeds.", + "reroll_axis": "Specific generator axis group to reroll. none uses the default axes for the selected scene layer.", "camera_config": "Camera config consumed only by nodes/options set to from_camera_config.", "location_config": "Location config from SxCP Location Pool. It can replace or add to the category scene pool.", "composition_config": "Composition config from SxCP Composition Pool or Location Theme. It can replace or add framing options.", @@ -33,6 +37,20 @@ COMMON_INPUT_TOOLTIPS = { "scene": "Structured v2 scene context. Chain Scene nodes in order, then connect to Scene Output or Scene Pair Output.", "softcore_scene": "Softcore branch scene from Scene Branch Pair, optionally refined by Softcore Branch Options.", "hardcore_scene": "Hardcore branch scene from Scene Branch Pair, optionally refined by Hardcore Branch Options.", + "options": "Incoming options of the same type. Chain option nodes with combine_mode=add when multiple side knobs should contribute.", + "seed_options": "Scene layer seed options. Connect Scene Layer Seed Options to reroll one layer without changing the whole scene.", + "cast_options": "Optional cast side-node settings that override the Cast node widgets only when connected.", + "character_options": "Optional character side-node settings that override descriptor, presence, and expression controls.", + "wardrobe_options": "Optional wardrobe side-node settings for subject-specific clothing, nudity state, and wardrobe prompt text.", + "location_options": "Optional location layout settings such as foreground anchors, midground, repetition, and public/private context.", + "set_options": "Optional set-dressing settings for props, repeated background, foreground anchors, and sensory details.", + "blocking_options": "Optional blocking settings for subject placement, orientation, depth plane, and exact body geography.", + "action_options": "Optional action settings for scene kind, action family, category preset, and manual action text.", + "performance_options": "Optional performance settings for expression, gaze, hands, body tension, and actor notes.", + "camera_options": "Optional camera side-node settings that describe camera source and freeform camera text.", + "composition_options": "Optional composition side-node settings for readability target, crop, occlusion, and framing text.", + "lighting_options": "Optional lighting side-node settings for source, softness, contrast, color, and time of day.", + "branch_options": "Optional branch settings that apply to softcore, hardcore, or both Insta/OF branches.", "target_formatter": "Intended downstream formatter target. The scene stores this as metadata; use formatter nodes for final rewriting.", "category_preset": "Category preset this scene should render through when no explicit category config overrides it.", "central_subject": "Who should be visually central in this scene metadata.", @@ -42,24 +60,43 @@ COMMON_INPUT_TOOLTIPS = { "custom_location": "Exact location text for this scene. One line or JSON entry is enough.", "location_note": "Additional location wording merged into the location pool entry.", "foreground_anchors": "Objects or surfaces that should stay near the camera or lower frame.", + "midground_layer": "Readable middle-distance scene elements between the subject and background.", + "background_repetition": "Repeated environmental structure that helps the model keep a location coherent across rerolls.", + "visibility_level": "How visible or hidden the scene should feel inside the location.", + "public_level": "Private, semi-public, or public context for the location layer.", "repeated_background": "Repeating background structure such as desks, doors, shelves, pillars, or windows.", "props": "Scene props or set dressing objects that make the location readable.", + "sensory_details": "Small material/light/surface details that make the set dressing feel specific.", "set_prompt": "Freeform set-dressing sentence appended to the scene layer.", "blocking_mode": "Broad body-placement mode. custom lets custom_blocking carry the exact placement.", "subject_placement": "Where the subject or cast sits in the space: foreground, near desk edge, on bed, in aisle, etc.", "body_relation": "Spatial relationship between participants, separate from the action itself.", + "body_orientation": "Front, side, back, three-quarter, or POV-facing body orientation.", + "depth_plane": "Whether the subjects sit in foreground, midground, background, or a layered composition.", + "distance_note": "Extra spatial distance wording, such as close together, across the table, or partly hidden behind a shelf.", "custom_blocking": "Exact blocking/positioning sentence for the scene layer.", "scene_kind": "Regular, softcore, or hardcore intent for this action layer.", + "action_family": "Broad action family such as softcore tease, oral, penetration, climax, group, or custom.", "action_prompt": "Action text stored separately from blocking and camera. Use position pools for hardcore randomization when possible.", + "gaze": "Where the character looks: camera, partner, down, away, over shoulder, or eyes closed.", + "hand_placement": "What hands are doing: relaxed, on body, on partner, holding camera, pulling clothing, or braced.", + "body_tension": "Body performance cue: relaxed, posed, arched, braced, or active motion.", "performance_prompt": "Expression, gaze, hand, and body-performance note stored separately from the action.", + "camera_source": "Where camera text comes from conceptually: config, qwen orbit, POV, phone, external, or manual.", + "preserve_location_layout": "Keep location layout wording compatible with the camera instead of letting camera text replace the space.", "camera_prompt": "Optional freeform camera note kept as scene metadata. Camera config still controls existing formatter behavior.", "custom_composition": "Exact composition/framing entry to add to the composition pool.", + "readability_target": "What the composition should keep most readable: face, body, action, room, anchor objects, or contact points.", + "crop": "Composition crop intent such as full body, three-quarter, close-up, or extreme close-up.", + "occlusion": "How much foreground or hidden-sightline occlusion the composition should allow.", "composition_prompt": "Additional composition wording merged into the composition layer.", "lighting_source": "Main light source family for the scene.", "lighting_softness": "Softness of the light: soft, balanced, or hard.", "lighting_contrast": "Overall contrast level for the lighting layer.", "color_temperature": "Warm, neutral, cool, or mixed color temperature.", + "time_of_day": "Optional time-of-day lighting context.", "custom_lighting": "Exact lighting sentence for the scene layer.", + "branch_target": "Whether branch options affect both Insta/OF branches, softcore only, or hardcore only.", "continuity": "How branch outputs share cast/location setup between softcore and hardcore scenes.", "platform_style": "Instagram/OnlyFans styling bias for Scene Pair Output.", "softcore_cast": "Whether the softcore branch uses a solo creator or the same cast as the hardcore branch.", @@ -126,6 +163,9 @@ COMMON_INPUT_TOOLTIPS = { "presence_mode": "Controls whether the character is visible or acts as the male POV participant.", "softcore_outfit": "Manual softcore outfit text for this character. Prefer Character Clothing for reusable outfit pools.", "hardcore_clothing": "Manual hardcore exposure text for this character. Use explicit nude states when you do not want clothing words repeated.", + "wardrobe_state": "High-level clothing/body-exposure state. explicit_nude avoids conflicting outfit text in hardcore prompts.", + "accessories": "Accessories that can remain visible without forcing full outfit wording.", + "avoid_clothing_when_nude": "When nude states are selected, avoid reintroducing clothing words that make the image model dress the subject.", "custom_softcore_outfits": "One custom softcore outfit per line. Used when softcore_source is custom.", "custom_hardcore_clothing": "One custom hardcore clothing/body exposure state per line.", "condition": "Loop condition. When false, the loop stops and passes current values through.", diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 0c6dbb8..504fe66 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -8792,6 +8792,19 @@ def smoke_node_insta_registration() -> None: def smoke_node_scene_chain_registration() -> None: required_nodes = [ + "SxCPSceneLayerSeedOptions", + "SxCPSceneCastOptions", + "SxCPSceneCharacterOptions", + "SxCPSceneWardrobeOptions", + "SxCPSceneLocationLayoutOptions", + "SxCPSceneSetDressingOptions", + "SxCPSceneBlockingOptions", + "SxCPSceneActionOptions", + "SxCPScenePerformanceOptions", + "SxCPSceneCameraOptions", + "SxCPSceneCompositionOptions", + "SxCPSceneLightingOptions", + "SxCPSceneBranchOptions", "SxCPSceneStart", "SxCPSceneCast", "SxCPSceneCharacter", @@ -8830,6 +8843,7 @@ def smoke_node_scene_chain_registration() -> None: parsed_scene = json.loads(scene) _expect(parsed_scene.get("schema") == "sxcp_scene_v2", "Scene Start did not emit v2 schema") + cast_options = nodes["SxCPSceneCastOptions"]().build("replace", "mixed_couple", 1, 1, "woman_a", "none")[0] scene, _cast_config, _cast_summary, _cast_metadata = nodes["SxCPSceneCast"]().build( scene, "mixed_couple", @@ -8837,7 +8851,18 @@ def smoke_node_scene_chain_registration() -> None: 1, "woman_a", "none", + cast_options=cast_options, ) + character_options = nodes["SxCPSceneCharacterOptions"]().build( + "replace", + "medium", + "visible", + "enabled", + 0.5, + -1, + -1, + "controlled slot performance note", + )[0] scene, character_cast, _slot, _summary, _metadata = nodes["SxCPSceneCharacter"]().build( scene, True, @@ -8854,6 +8879,7 @@ def smoke_node_scene_chain_registration() -> None: "visible", -1, -1, + character_options=character_options, ) scene, character_cast, _slot, _summary, _metadata = nodes["SxCPSceneCharacter"]().build( scene, @@ -8872,6 +8898,19 @@ def smoke_node_scene_chain_registration() -> None: -1, -1, ) + wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "content", "same_for_all_rows", "replace_layer")[0] + wardrobe_options = nodes["SxCPSceneWardrobeOptions"]().build( + "replace", + "woman", + "A", + "full", + "explicit_nude", + True, + "simple black dress", + "", + "thin necklace", + "", + )[0] scene, character_cast, _wardrobe_summary, _wardrobe_metadata = nodes["SxCPSceneWardrobe"]().build( scene, True, @@ -8881,12 +8920,24 @@ def smoke_node_scene_chain_registration() -> None: "simple black dress", "fully nude", "", + wardrobe_options=wardrobe_options, + seed_options=wardrobe_seed_options, ) slots = json.loads(character_cast).get("slots") or [] woman_slot = next(slot for slot in slots if slot.get("subject_type") == "woman") _expect(woman_slot.get("softcore_outfit") == "simple black dress", "Scene Wardrobe did not update softcore outfit") _expect(woman_slot.get("hardcore_clothing") == "fully nude", "Scene Wardrobe did not update hardcore clothing") + _expect(json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("seed") == 9981, "Scene Wardrobe seed options did not write seed trace") + location_options = nodes["SxCPSceneLocationLayoutOptions"]().build( + "replace", + "mirror edge", + "soft curtain layer", + "repeating lamp reflections", + "private", + "private", + "warm mirror-room geometry", + )[0] scene = nodes["SxCPSceneLocation"]().build( scene, True, @@ -8894,10 +8945,26 @@ def smoke_node_scene_chain_registration() -> None: "custom_only", "quiet studio room with a large mirror", "", + location_options=location_options, )[0] - scene = nodes["SxCPSceneSetDressing"]().build(scene, True, "mirror edge", "soft curtains", "small lamp", "")[0] - scene = nodes["SxCPSceneBlocking"]().build(scene, True, "standing", "woman near mirror", "man behind her", "")[0] - scene = nodes["SxCPScenePerformance"]().build(scene, True, "fixed", 0.4, "controlled eye contact")[0] + set_options = nodes["SxCPSceneSetDressingOptions"]().build("replace", "mirror edge", "soft curtains", "small lamp", "warm fabric texture", "")[0] + scene = nodes["SxCPSceneSetDressing"]().build(scene, True, "mirror edge", "soft curtains", "small lamp", "", set_options=set_options)[0] + blocking_options = nodes["SxCPSceneBlockingOptions"]().build( + "replace", + "standing", + "woman near mirror", + "man behind her", + "three_quarter", + "foreground", + "standing close", + "", + )[0] + scene = nodes["SxCPSceneBlocking"]().build(scene, True, "standing", "woman near mirror", "man behind her", "", blocking_options=blocking_options)[0] + action_options = nodes["SxCPSceneActionOptions"]().build("replace", "softcore", "softcore_tease", "no_change", "quiet pose transition")[0] + scene = nodes["SxCPSceneAction"]().build(scene, True, "regular", "no_change", "", action_options=action_options)[0] + performance_options = nodes["SxCPScenePerformanceOptions"]().build("replace", "enabled", "fixed", 0.4, "camera", "on_body", "posed", "controlled eye contact")[0] + scene = nodes["SxCPScenePerformance"]().build(scene, True, "fixed", 0.4, "controlled eye contact", performance_options=performance_options)[0] + camera_options = nodes["SxCPSceneCameraOptions"]().build("replace", "from_camera_config", True, "mirror-aware camera note")[0] scene = nodes["SxCPSceneCamera"]().build( scene, True, @@ -8911,9 +8978,12 @@ def smoke_node_scene_chain_registration() -> None: "strong", "compact", "", + camera_options=camera_options, )[0] - scene = nodes["SxCPSceneComposition"]().build(scene, True, "replace", "no_outfit_check", "mirror-aware three-quarter frame", "")[0] - scene = nodes["SxCPSceneLighting"]().build(scene, True, "practical_lamps", "soft", "medium", "warm", "")[0] + composition_options = nodes["SxCPSceneCompositionOptions"]().build("replace", "body", "three_quarter", "clear", "mirror-aware three-quarter frame")[0] + scene = nodes["SxCPSceneComposition"]().build(scene, True, "replace", "no_outfit_check", "", "", composition_options=composition_options)[0] + lighting_options = nodes["SxCPSceneLightingOptions"]().build("replace", "practical_lamps", "soft", "medium", "warm", "evening", "")[0] + scene = nodes["SxCPSceneLighting"]().build(scene, True, "practical_lamps", "soft", "medium", "warm", "", lighting_options=lighting_options)[0] output = nodes["SxCPSceneOutput"]().build(scene) _expect_text("node_scene_chain.prompt", output[0], 40) @@ -8921,10 +8991,14 @@ def smoke_node_scene_chain_registration() -> None: row = json.loads(output[3]) _expect(row.get("scene_chain", {}).get("schema") == "sxcp_scene_v2", "Scene Output lost scene_chain metadata") + branch_options = nodes["SxCPSceneBranchOptions"]().build("replace", "both", "same_creator_same_room", "hybrid", "shared branch note")[0] + branch_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("hardcore_branch", "fixed", 7799, "pose", "same_for_all_rows", "replace_layer")[0] soft_scene, hard_scene, _branch_summary, _branch_metadata = nodes["SxCPSceneBranchPair"]().build( scene, "same_creator_same_room", "hybrid", + branch_options=branch_options, + seed_options=branch_seed_options, ) soft_scene = nodes["SxCPSoftcoreBranchOptions"]().build( soft_scene, @@ -8935,6 +9009,7 @@ def smoke_node_scene_chain_registration() -> None: "from_camera_config", "compact", "", + branch_options=branch_options, )[0] hard_scene = nodes["SxCPHardcoreBranchOptions"]().build( hard_scene, @@ -8949,6 +9024,8 @@ def smoke_node_scene_chain_registration() -> None: "compact", "balanced", "", + branch_options=branch_options, + seed_options=branch_seed_options, )[0] pair_output = nodes["SxCPScenePairOutput"]().build(soft_scene, hard_scene) _expect_text("node_scene_chain.softcore_prompt", pair_output[0], 40) @@ -8957,6 +9034,10 @@ def smoke_node_scene_chain_registration() -> None: _expect_pair(pair, "node_scene_chain_pair") _expect(pair.get("options", {}).get("hardcore_cast") == "couple", "Scene Pair Output lost hardcore branch options") _expect("scene_chain" in pair, "Scene Pair Output lost scene_chain metadata") + _expect( + pair.get("scene_chain", {}).get("hardcore", {}).get("seed_trace", {}).get("hardcore.hardcore_branch", {}).get("seed") == 7799, + "Scene branch seed options did not write hardcore branch seed trace", + ) def smoke_node_builder_registration() -> None: