Add v2 scene chain nodes

This commit is contained in:
2026-06-27 22:59:57 +02:00
parent 718da9a68d
commit 187940b45f
6 changed files with 1521 additions and 0 deletions
+31
View File
@@ -38,6 +38,23 @@ The node is registered as:
- `prompt_builder / SxCP Krea2 Formatter`
- `prompt_builder / SxCP Insta/OF Options`
- `prompt_builder / SxCP Insta/OF Prompt Pair`
- `prompt_builder / v2_scene / SxCP Scene Start`
- `prompt_builder / v2_scene / SxCP Scene Cast`
- `prompt_builder / v2_scene / SxCP Scene Character`
- `prompt_builder / v2_scene / SxCP Scene Wardrobe`
- `prompt_builder / v2_scene / SxCP Scene Location`
- `prompt_builder / v2_scene / SxCP Scene Set Dressing`
- `prompt_builder / v2_scene / SxCP Scene Blocking`
- `prompt_builder / v2_scene / SxCP Scene Action`
- `prompt_builder / v2_scene / SxCP Scene Performance`
- `prompt_builder / v2_scene / SxCP Scene Camera`
- `prompt_builder / v2_scene / SxCP Scene Composition`
- `prompt_builder / v2_scene / SxCP Scene Lighting`
- `prompt_builder / v2_scene / SxCP Scene Branch Pair`
- `prompt_builder / v2_scene / SxCP Softcore Branch Options`
- `prompt_builder / v2_scene / SxCP Hardcore Branch Options`
- `prompt_builder / v2_scene / SxCP Scene Output`
- `prompt_builder / v2_scene / SxCP Scene Pair Output`
It outputs:
@@ -101,6 +118,20 @@ The practical compact workflow is:
`Woman Slot` / `Man Slot`, and `Character Profile`
into `Prompt Builder From Configs`.
## Scene-Chain v2 Nodes
The v2 scene nodes are an additive workflow surface. They pass one structured
`SXCP_SCENE` object through cast, character, wardrobe, location, set dressing,
blocking, action, performance, camera, composition, and lighting layers. Use
`SxCP Scene Output` for a single prompt, or split a shared scene with
`SxCP Scene Branch Pair`, refine it with `SxCP Softcore Branch Options` and
`SxCP Hardcore Branch Options`, then render both sides through
`SxCP Scene Pair Output`.
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.
An importable default workflow is included at
`examples/default_task_lanes_workflow.json`. It is laid out by task instead of
as one long chain:
+11
View File
@@ -24,6 +24,7 @@ SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
SXCP_SCENE = "SXCP_SCENE"
try:
from .node_tooltips import install_input_tooltips as _install_input_tooltips
@@ -72,6 +73,10 @@ try:
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
)
from .node_scene import (
NODE_CLASS_MAPPINGS as SCENE_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as SCENE_NODE_DISPLAY_NAME_MAPPINGS,
)
from .server_routes import (
accumulator_delete_payload,
accumulator_list_payload,
@@ -120,6 +125,10 @@ except ImportError:
NODE_CLASS_MAPPINGS as SEED_RESOLUTION_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS,
)
from node_scene import (
NODE_CLASS_MAPPINGS as SCENE_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as SCENE_NODE_DISPLAY_NAME_MAPPINGS,
)
from server_routes import (
accumulator_delete_payload,
accumulator_list_payload,
@@ -186,6 +195,7 @@ NODE_CLASS_MAPPINGS.update(FORMATTER_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(INSTA_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(ROUTE_CONFIG_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(PROFILE_FILTER_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(SCENE_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS)
_install_input_tooltips(NODE_CLASS_MAPPINGS)
@@ -199,6 +209,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(FORMATTER_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(INSTA_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(SCENE_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS)
WEB_DIRECTORY = "./web"
+8
View File
@@ -68,10 +68,18 @@ cleanup such as clothing/body-access scene sanitization.
| `SxCP Prompt Builder` | `build_prompt` -> `builder_prompt_route.py` | Direct single prompt generation. Can use built-in categories or JSON categories. |
| `SxCP Prompt Builder From Configs` | `build_prompt_from_configs` -> `builder_config_route.py` -> `build_prompt` -> `builder_prompt_route.py` | Same generator, but inputs come from category/cast/profile/filter helper nodes. |
| `SxCP Insta/OF Prompt Pair` | `build_insta_of_pair` | Builds a softcore row and hardcore row with shared cast/continuity options. |
| `SxCP Scene Start` / `SxCP Scene Output` / `SxCP Scene Pair Output` | `node_scene.py` -> existing builder and pair routes | v2 structured `SXCP_SCENE` chain. Layers are split into cast, character, wardrobe, location, set dressing, blocking, action, performance, camera, composition, and lighting before compatibility rendering. |
| `SxCP Krea2 Formatter` | `format_krea2_prompt` -> `krea_format_route.py` | Converts metadata rows or pair metadata into Krea2-friendly prose. |
| `SxCP SDXL Formatter` | `format_sdxl_prompt` -> `sdxl_format_route.py` | Converts metadata rows or pair metadata into SDXL/tag style prompts. |
| `SxCP Caption Naturalizer` | `naturalize_caption` -> `caption_format_route.py` | Converts rows into more natural sentence captions. |
V2 scene-chain display nodes: `SxCP Scene Cast`, `SxCP Scene Character`,
`SxCP Scene Wardrobe`, `SxCP Scene Location`, `SxCP Scene Set Dressing`,
`SxCP Scene Blocking`, `SxCP Scene Action`, `SxCP Scene Performance`,
`SxCP Scene Camera`, `SxCP Scene Composition`, `SxCP Scene Lighting`,
`SxCP Scene Branch Pair`, `SxCP Softcore Branch Options`, and
`SxCP Hardcore Branch Options`.
Core helper ownership:
| Python module | What it owns |
+1262
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -30,6 +30,46 @@ COMMON_INPUT_TOOLTIPS = {
"custom_compositions": "One custom composition/framing phrase per line.",
"theme": "Matched location and composition theme, useful when the place needs compatible framing.",
"metadata_json": "Structured metadata from an SxCP generator. Prefer this over raw prompt text for formatters and profile save.",
"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.",
"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.",
"pov_participant": "Optional participant treated as the first-person viewer in later character/camera logic.",
"subject_label": "Character label affected by this layer. all applies the layer to every matching character slot.",
"wardrobe_prompt": "Optional wardrobe/set note carried as scene metadata and compatibility extra prompt text.",
"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.",
"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.",
"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.",
"custom_blocking": "Exact blocking/positioning sentence for the scene layer.",
"scene_kind": "Regular, softcore, or hardcore intent for this action layer.",
"action_prompt": "Action text stored separately from blocking and camera. Use position pools for hardcore randomization when possible.",
"performance_prompt": "Expression, gaze, hand, and body-performance note stored separately from the action.",
"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.",
"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.",
"custom_lighting": "Exact lighting sentence for the scene layer.",
"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.",
"hardcore_cast": "Hardcore branch cast preset or explicit count mode.",
"softcore_level": "Softcore exposure/style level for Scene Pair Output.",
"hardcore_level": "Hardcore intensity level for Scene Pair Output.",
"softcore_camera_mode": "Softcore branch camera mode, or from_camera_config to use the connected scene camera.",
"hardcore_camera_mode": "Hardcore branch camera mode, or from_camera_config to use the connected scene camera.",
"hardcore_clothing_continuity": "How wardrobe is rendered in the hardcore branch. explicit_nude avoids clothing-token conflicts.",
"hardcore_detail_density": "How much explicit action detail the current formatter route keeps for the hardcore branch.",
"source_text": "Raw prompt, caption, or metadata JSON depending on input_hint.",
"source_text_input": "Optional linked raw prompt/caption input. When connected, it overrides the source_text widget.",
"input_hint": "Tells the node how to interpret source_text. auto tries metadata first.",
+169
View File
@@ -8790,6 +8790,174 @@ def smoke_node_insta_registration() -> None:
_expect(pair.get("options", {}).get("hardcore_cast") == "couple", "Insta/OF Prompt Pair lost options metadata")
def smoke_node_scene_chain_registration() -> None:
required_nodes = [
"SxCPSceneStart",
"SxCPSceneCast",
"SxCPSceneCharacter",
"SxCPSceneWardrobe",
"SxCPSceneLocation",
"SxCPSceneSetDressing",
"SxCPSceneBlocking",
"SxCPSceneAction",
"SxCPScenePerformance",
"SxCPSceneCamera",
"SxCPSceneComposition",
"SxCPSceneLighting",
"SxCPSceneBranchPair",
"SxCPSoftcoreBranchOptions",
"SxCPHardcoreBranchOptions",
"SxCPSceneOutput",
"SxCPScenePairOutput",
]
for node_name in required_nodes:
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
nodes = sxcp_nodes.NODE_CLASS_MAPPINGS
scene, start_summary, _start_metadata = nodes["SxCPSceneStart"]().build(
1,
41,
777,
"raw",
"woman",
"random",
"balanced",
Trigger,
True,
)
_expect("scene v" in start_summary, "Scene Start summary changed unexpectedly")
parsed_scene = json.loads(scene)
_expect(parsed_scene.get("schema") == "sxcp_scene_v2", "Scene Start did not emit v2 schema")
scene, _cast_config, _cast_summary, _cast_metadata = nodes["SxCPSceneCast"]().build(
scene,
"mixed_couple",
1,
1,
"woman_a",
"none",
)
scene, character_cast, _slot, _summary, _metadata = nodes["SxCPSceneCharacter"]().build(
scene,
True,
"woman",
"A",
-1,
"25-year-old adult",
"random",
"random",
"random",
"medium",
True,
0.5,
"visible",
-1,
-1,
)
scene, character_cast, _slot, _summary, _metadata = nodes["SxCPSceneCharacter"]().build(
scene,
True,
"man",
"A",
-1,
"40-year-old adult",
"random",
"random",
"average",
"compact",
True,
0.5,
"visible",
-1,
-1,
)
scene, character_cast, _wardrobe_summary, _wardrobe_metadata = nodes["SxCPSceneWardrobe"]().build(
scene,
True,
"A",
"full",
"simple black dress",
"fully nude",
"",
)
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")
scene = nodes["SxCPSceneLocation"]().build(
scene,
True,
"replace",
"custom_only",
"quiet studio room with a large mirror",
"",
)[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]
scene = nodes["SxCPSceneCamera"]().build(
scene,
True,
"standard",
"three_quarter",
"eye_level",
"auto",
"auto",
"auto",
"auto",
"strong",
"compact",
"",
)[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]
output = nodes["SxCPSceneOutput"]().build(scene)
_expect_text("node_scene_chain.prompt", output[0], 40)
_expect_trigger_once("node_scene_chain.prompt", output[0], Trigger)
row = json.loads(output[3])
_expect(row.get("scene_chain", {}).get("schema") == "sxcp_scene_v2", "Scene Output lost scene_chain metadata")
soft_scene, hard_scene, _branch_summary, _branch_metadata = nodes["SxCPSceneBranchPair"]().build(
scene,
"same_creator_same_room",
"hybrid",
)
soft_scene = nodes["SxCPSoftcoreBranchOptions"]().build(
soft_scene,
"same_as_hardcore",
"lingerie_tease",
True,
0.45,
"from_camera_config",
"compact",
"",
)[0]
hard_scene = nodes["SxCPHardcoreBranchOptions"]().build(
hard_scene,
"couple",
1,
1,
"hardcore",
True,
0.85,
"explicit_nude",
"from_camera_config",
"compact",
"balanced",
"",
)[0]
pair_output = nodes["SxCPScenePairOutput"]().build(soft_scene, hard_scene)
_expect_text("node_scene_chain.softcore_prompt", pair_output[0], 40)
_expect_text("node_scene_chain.hardcore_prompt", pair_output[1], 40)
pair = json.loads(pair_output[7])
_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")
def smoke_node_builder_registration() -> None:
required_nodes = [
"SxCPPromptBuilder",
@@ -9018,6 +9186,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("node_hardcore_position_registration", smoke_node_hardcore_position_registration),
("node_formatter_registration", smoke_node_formatter_registration),
("node_insta_registration", smoke_node_insta_registration),
("node_scene_chain_registration", smoke_node_scene_chain_registration),
("node_builder_registration", smoke_node_builder_registration),
("node_profile_filter_registration", smoke_node_profile_filter_registration),
]