Add v2 scene chain nodes
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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.",
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user