Add separate style pool config

This commit is contained in:
2026-06-28 00:24:40 +02:00
parent 4c8edc0d3e
commit 78e39734b5
18 changed files with 378 additions and 27 deletions
+8 -2
View File
@@ -22,6 +22,7 @@ The node is registered as:
- `prompt_builder / SxCP Cast Control`
- `prompt_builder / SxCP Cast Bias`
- `prompt_builder / SxCP Generation Profile`
- `prompt_builder / SxCP Style Pool`
- `prompt_builder / SxCP Ethnicity List`
- `prompt_builder / SxCP Hair Length`
- `prompt_builder / SxCP Hair Color`
@@ -93,6 +94,11 @@ node. For cleaner workflows, use the split nodes:
`composition_config`. Themes such as `classical_library`,
`semi_public_affair`, `hotel_corridor`, `parking_garage`, and
`theater_backstage` keep scene and framing compatible.
- `SxCP Style Pool` outputs `style_config` for visual rendering style only.
It can force realistic/photo/cinematic/comic output independently from
category, action, pose, location, and camera. The previous colored-pencil
comic wording is available as the `comic_pinup_colored_pencil` preset instead
of being baked into hardcore pose prompts.
- `SxCP Generation Profile` outputs `generation_profile` for common behavior
presets such as casual-clean, evocative-softcore, hardcore-intense,
Krea2-friendly, or Flux-original. Its clothing and pose overrides can be
@@ -115,7 +121,7 @@ The practical compact workflow is:
`Category Preset` + `Cast Control` + `Generation Profile` + optional
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control` or
`Camera Orbit Control`, `Location Theme` or `Location Pool` + `Composition Pool`,
`Woman Slot` / `Man Slot`, and `Character Profile`
`Style Pool`, `Woman Slot` / `Man Slot`, and `Character Profile`
into `Prompt Builder From Configs`.
## Scene-Chain v2 Nodes
@@ -734,7 +740,7 @@ Example:
"slug": "casual_clothes",
"subject_type": "woman",
"item_label": "Clothing",
"style": "tasteful adult fashion-editorial coloured-pencil comic illustration",
"style": "tasteful adult fashion-editorial scene",
"subcategories": [
{
"name": "Streetwear",
+1
View File
@@ -20,6 +20,7 @@ SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE"
SXCP_INSTA_OF_OPTIONS = "SXCP_INSTA_OF_OPTIONS"
SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG"
SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
+2
View File
@@ -20,6 +20,7 @@ class PromptFromConfigsRequest:
hardcore_position_config: str | dict[str, Any] | None = ""
location_config: str | dict[str, Any] | None = ""
composition_config: str | dict[str, Any] | None = ""
style_config: str | dict[str, Any] | None = ""
extra_positive: str = ""
extra_negative: str = ""
@@ -82,6 +83,7 @@ def build_prompt_from_configs_result(
"hardcore_position_config": request.hardcore_position_config or "",
"location_config": request.location_config or "",
"composition_config": request.composition_config or "",
"style_config": request.style_config or "",
}
return PromptFromConfigsRoute(
row=deps.build_prompt(**build_kwargs),
+2
View File
@@ -41,6 +41,7 @@ class PromptBuildRequest:
hardcore_position_config: str | dict[str, Any] | None = None
location_config: str | dict[str, Any] | None = None
composition_config: str | dict[str, Any] | None = None
style_config: str | dict[str, Any] | None = None
@dataclass(frozen=True)
@@ -226,6 +227,7 @@ def build_prompt_result(request: PromptBuildRequest, deps: PromptBuildDependenci
request.hardcore_position_config,
parsed_location_config,
parsed_composition_config,
request.style_config,
)
if row.get("source") == "built_in_generator":
+6 -6
View File
@@ -7,8 +7,8 @@
"weight": 1.0,
"subject_type": "woman",
"item_label": "Clothing",
"style": "tasteful adult fashion-editorial coloured-pencil comic illustration with casual everyday styling",
"positive_suffix": "Use crisp clean comic linework, soft fabric texture, detailed hatching, warm natural light, and tactile textured paper.",
"style": "tasteful adult fashion-editorial scene with casual everyday styling",
"positive_suffix": "Use readable full outfits, clear fabric texture, natural light, coherent anatomy, and polished styling detail.",
"expression_pools": ["casual_observational_expressions"],
"composition_pools": ["casual_fashion_compositions"],
"subcategories": [
@@ -833,8 +833,8 @@
"weight": 1.0,
"subject_type": "man",
"item_label": "Clothing",
"style": "tasteful adult menswear fashion-editorial coloured-pencil comic illustration with casual everyday styling",
"positive_suffix": "Use crisp clean comic linework, structured fabric texture, detailed hatching, natural light, and tactile textured paper.",
"style": "tasteful adult menswear fashion-editorial scene with casual everyday styling",
"positive_suffix": "Use readable full outfits, structured fabric texture, natural light, coherent anatomy, and polished styling detail.",
"expression_pools": ["men_casual_expressions"],
"composition_pools": ["men_casual_compositions"],
"subcategories": [
@@ -1285,8 +1285,8 @@
"weight": 1.0,
"subject_type": "couple",
"item_label": "Clothing",
"style": "tasteful adult couple fashion-editorial coloured-pencil comic illustration with coordinated casual styling",
"positive_suffix": "Use crisp clean comic linework, readable full outfits, detailed hatching, warm natural light, and tactile textured paper.",
"style": "tasteful adult couple fashion-editorial scene with coordinated casual styling",
"positive_suffix": "Use readable coordinated outfits, clear fabric texture, warm natural light, coherent body placement, and polished styling detail.",
"expression_pools": ["couple_casual_expressions"],
"composition_pools": ["couple_casual_compositions"],
"subcategories": [
+2 -2
View File
@@ -7,8 +7,8 @@
"weight": 1.0,
"subject_type": "woman",
"item_label": "Erotic outfit",
"style": "explicit adult erotic fashion illustration, sensual pin-up coloured-pencil comic style, adults only",
"positive_suffix": "Use crisp clean comic linework, detailed hatching, soft skin shading, tactile fabric texture, warm intimate lighting, and textured paper.",
"style": "explicit adult erotic fashion scene with sensual pin-up styling, adults only",
"positive_suffix": "Use clear adult anatomy, readable erotic outfit construction, tactile fabric texture, warm intimate lighting, coherent body placement, and polished detail.",
"negative_prompt": "minors, childlike appearance, schoolgirl, childlike costume, non-consensual, coercion, violence, injury, watermark",
"scene_pools": ["softcore_creator_scenes", "mirror_scenes"],
"expression_pools": ["softcore_creator_expressions", "erotic_inviting_expressions"],
+5 -3
View File
@@ -120,6 +120,7 @@ Core helper ownership:
| `subject_context.py` | Row subject-context routing for single, couple, configured-cast, group, and layout subjects, combining appearance policy, cast metadata, and generator subject pools. |
| `row_subject_route.py` | Row subject route orchestration, character slot/profile precedence, configured-cast POV labels, visible cast descriptor collection, and descriptor prompt cleanup. |
| `location_config.py` | Location/composition preset schemas, themed location packs, custom location/composition parsing, pool merge behavior, and location/composition config parsing. |
| `style_config.py` | Visual style preset schemas, style-pool merge behavior, positive style suffixes, and style negative-prompt merging. |
| `row_location.py` | Built-in row location/composition config application, deterministic scene/composition choice, source metadata, and legacy prompt/caption rewrites. |
| `row_expression.py` | Row expression cleanup, expression route resolution, expression intensity weighting, character-slot/cast expression override resolution, per-character expression selection, and action-aware character-expression sanitizing. |
| `row_pools.py` | Row scene/expression/pose/composition pool routing, category inheritance handling, runtime location/composition pool overrides, and generator fallback pools. |
@@ -173,9 +174,9 @@ recoverable.
| Node | Important inputs | Important outputs |
| --- | --- | --- |
| `SxCP Prompt Builder` | category, subcategory, seed, optional config nodes | `prompt`, `negative_prompt`, `caption`, `metadata_json`, `category`, `subcategory` |
| `SxCP Prompt Builder From Configs` | category/cast/profile/filter/config node outputs | Same as `SxCP Prompt Builder` |
| `SxCP Insta/OF Prompt Pair` | options, seed_config, character_cast, location/composition/camera, hardcore_position_config | `softcore_prompt`, `hardcore_prompt`, both negatives, both captions, `shared_descriptor`, `metadata_json` |
| `SxCP Prompt Builder` | category, subcategory, seed, optional config nodes including location/composition/style | `prompt`, `negative_prompt`, `caption`, `metadata_json`, `category`, `subcategory` |
| `SxCP Prompt Builder From Configs` | category/cast/profile/filter/config node outputs including style_config | Same as `SxCP Prompt Builder` |
| `SxCP Insta/OF Prompt Pair` | options, seed_config, character_cast, location/composition/style/camera, hardcore_position_config | `softcore_prompt`, `hardcore_prompt`, both negatives, both captions, `shared_descriptor`, `metadata_json` |
| `SxCP Krea2 Formatter` | `source_text`, connectable `metadata_json`, target | `krea_prompt`, both pair prompts if pair metadata exists, negative outputs, method, `route_trace_json` |
| `SxCP SDXL Formatter` | `source_text`, connectable `metadata_json`, target, style/quality preset | `sdxl_prompt`, both pair prompts if pair metadata exists, negative outputs, method, `route_trace_json` |
| `SxCP Caption Naturalizer` | `source_text`, connectable `metadata_json`, target | `natural_caption`, method, `route_trace_json` |
@@ -195,6 +196,7 @@ These recipes identify the intended road before editing prompt text.
| Change only outfit/clothing | Character clothing or category content route | Keep `person_seed`, `scene_seed`, `pose_seed`; change `content_seed`; slot `softcore_outfit` overrides Insta/OF outfit | `SxCP Character Clothing`, `pair_options.py`, category item templates |
| Force a custom location | `SxCP Location Pool` or `SxCP Location Theme` -> builder/pair | `combine_mode=replace` to force; `add` to mix with category scenes | `_scene_pool`, `row_location.apply_location_config_to_legacy_row`, camera scene adapter |
| Force a custom frame/composition | `SxCP Composition Pool` or `SxCP Location Theme` -> builder/pair | `combine_mode=replace` to force; `add` to mix | `_composition_pool`, `row_location.apply_composition_config_to_legacy_row`, Krea composition phrase |
| Force realistic/photo/comic style | `SxCP Style Pool` -> builder/pair/Scene Start | `combine_mode=replace` to override category style; `add` to append; choose realistic/photo/comic preset | `style_config.py`, `row_rendering.resolve_row_text_fields`, row `style` / `positive_suffix` metadata |
| Use Qwen/orbit camera geometry | Qwen/orbit node -> camera_config -> builder/pair | For pair, use `softcore_camera_config` and/or `hardcore_camera_config`; set mode from config in options | `_camera_config_with_mode`, `_camera_directive`, `_camera_scene_directive_for_context` |
| Use Krea2 for only hard prompt from a pair | Pair `metadata_json` -> Krea2 Formatter | `target=hardcore`, `input_hint=metadata_json` or auto with metadata connected | `_insta_pair_to_krea`, hard row fields |
| Convert builder output to SDXL tags | Builder/pair metadata -> SDXL Formatter | Use metadata input; set `target`; select style and quality preset | `sdxl_tag_routes.py`, `sdxl_tag_policy.py`, compatibility wrappers `_row_core_tags` / `_soft_tags` / `_hard_tags` |
+7
View File
@@ -29,6 +29,7 @@ SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG"
SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE"
SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG"
SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG"
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
@@ -64,6 +65,7 @@ class SxCPPromptBuilder:
"camera_config": (SXCP_CAMERA_CONFIG,),
"location_config": (SXCP_LOCATION_CONFIG,),
"composition_config": (SXCP_COMPOSITION_CONFIG,),
"style_config": (SXCP_STYLE_CONFIG,),
"character_profile": (SXCP_CHARACTER_PROFILE,),
"character_cast": (SXCP_CHARACTER_CAST,),
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
@@ -101,6 +103,7 @@ class SxCPPromptBuilder:
camera_config="",
location_config="",
composition_config="",
style_config="",
character_profile="",
character_cast="",
hardcore_position_config="",
@@ -137,6 +140,7 @@ class SxCPPromptBuilder:
camera_config=camera_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
style_config=style_config or "",
character_profile=character_profile or "",
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
@@ -170,6 +174,7 @@ class SxCPPromptBuilderFromConfigs:
"camera_config": (SXCP_CAMERA_CONFIG,),
"location_config": (SXCP_LOCATION_CONFIG,),
"composition_config": (SXCP_COMPOSITION_CONFIG,),
"style_config": (SXCP_STYLE_CONFIG,),
"character_profile": (SXCP_CHARACTER_PROFILE,),
"character_cast": (SXCP_CHARACTER_CAST,),
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
@@ -197,6 +202,7 @@ class SxCPPromptBuilderFromConfigs:
camera_config="",
location_config="",
composition_config="",
style_config="",
character_profile="",
character_cast="",
hardcore_position_config="",
@@ -215,6 +221,7 @@ class SxCPPromptBuilderFromConfigs:
camera_config=camera_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
style_config=style_config or "",
character_profile=character_profile or "",
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
+4
View File
@@ -32,6 +32,7 @@ SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE"
SXCP_ETHNICITY_LIST = "SXCP_ETHNICITY_LIST"
SXCP_FILTER_CONFIG = "SXCP_FILTER_CONFIG"
SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG"
class SxCPInstaOFOptions:
@@ -130,6 +131,7 @@ class SxCPInstaOFPromptPair:
"hardcore_camera_config": (SXCP_CAMERA_CONFIG,),
"location_config": (SXCP_LOCATION_CONFIG,),
"composition_config": (SXCP_COMPOSITION_CONFIG,),
"style_config": (SXCP_STYLE_CONFIG,),
"character_profile": (SXCP_CHARACTER_PROFILE,),
"character_cast": (SXCP_CHARACTER_CAST,),
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
@@ -170,6 +172,7 @@ class SxCPInstaOFPromptPair:
hardcore_camera_config="",
location_config="",
composition_config="",
style_config="",
character_profile="",
character_cast="",
hardcore_position_config="",
@@ -196,6 +199,7 @@ class SxCPInstaOFPromptPair:
hardcore_camera_config=hardcore_camera_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
style_config=style_config or "",
character_profile=character_profile or "",
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
+58
View File
@@ -22,6 +22,11 @@ try:
location_pool_preset_choices,
location_theme_choices,
)
from .style_config import (
build_style_config_json,
style_combine_mode_choices,
style_pool_preset_choices,
)
except ImportError: # Allows local smoke tests from the repository root.
from category_cast_config import (
build_cast_config_json,
@@ -41,6 +46,11 @@ except ImportError: # Allows local smoke tests from the repository root.
location_pool_preset_choices,
location_theme_choices,
)
from style_config import (
build_style_config_json,
style_combine_mode_choices,
style_pool_preset_choices,
)
SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
@@ -48,6 +58,7 @@ SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG"
SXCP_COMPOSITION_CONFIG = "SXCP_COMPOSITION_CONFIG"
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG"
SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG"
class SxCPCategoryPreset:
@@ -178,6 +189,51 @@ class SxCPLocationTheme:
)
class SxCPStylePool:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"enabled": ("BOOLEAN", {"default": True}),
"combine_mode": (style_combine_mode_choices(), {"default": "replace"}),
"preset": (style_pool_preset_choices(), {"default": "realistic_photo"}),
"custom_style": ("STRING", {"default": "", "multiline": True}),
"custom_positive_suffix": ("STRING", {"default": "", "multiline": True}),
"custom_negative": ("STRING", {"default": "", "multiline": True}),
},
"optional": {
"style_config": (SXCP_STYLE_CONFIG,),
},
}
RETURN_TYPES = (SXCP_STYLE_CONFIG, "STRING")
RETURN_NAMES = ("style_config", "summary")
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(
self,
enabled,
combine_mode,
preset,
custom_style,
custom_positive_suffix,
custom_negative,
style_config="",
):
config = build_style_config_json(
enabled=enabled,
combine_mode=combine_mode,
preset=preset,
custom_style=custom_style or "",
custom_positive_suffix=custom_positive_suffix or "",
custom_negative=custom_negative or "",
style_config=style_config or "",
)
parsed = json.loads(config)
return config, parsed.get("summary", "")
class SxCPCastControl:
@classmethod
def INPUT_TYPES(cls):
@@ -313,6 +369,7 @@ NODE_CLASS_MAPPINGS = {
"SxCPLocationPool": SxCPLocationPool,
"SxCPCompositionPool": SxCPCompositionPool,
"SxCPLocationTheme": SxCPLocationTheme,
"SxCPStylePool": SxCPStylePool,
"SxCPCastControl": SxCPCastControl,
"SxCPCastBias": SxCPCastBias,
}
@@ -322,6 +379,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SxCPLocationPool": "SxCP Location Pool",
"SxCPCompositionPool": "SxCP Composition Pool",
"SxCPLocationTheme": "SxCP Location Theme",
"SxCPStylePool": "SxCP Style Pool",
"SxCPCastControl": "SxCP Cast Control",
"SxCPCastBias": "SxCP Cast Bias",
}
+7
View File
@@ -98,6 +98,7 @@ SXCP_CATEGORY_CONFIG = "SXCP_CATEGORY_CONFIG"
SXCP_CAST_CONFIG = "SXCP_CAST_CONFIG"
SXCP_GENERATION_PROFILE = "SXCP_GENERATION_PROFILE"
SXCP_FILTER_CONFIG = "SXCP_FILTER_CONFIG"
SXCP_STYLE_CONFIG = "SXCP_STYLE_CONFIG"
SXCP_ETHNICITY_LIST = "SXCP_ETHNICITY_LIST"
SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST"
SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT"
@@ -647,6 +648,7 @@ def _compat_configs(scene: dict[str, Any], branch_name: str = "") -> dict[str, A
"camera_config": branch_configs.get("camera_config") or configs.get("camera_config") or "",
"location_config": branch_configs.get("location_config") or configs.get("location_config") or "",
"composition_config": branch_configs.get("composition_config") or configs.get("composition_config") or "",
"style_config": branch_configs.get("style_config") or configs.get("style_config") or "",
"character_profile": branch_configs.get("character_profile") or configs.get("character_profile") or "",
"character_cast": branch_configs.get("character_cast") or configs.get("character_cast") or "",
"hardcore_position_config": branch_configs.get("hardcore_position_config") or configs.get("hardcore_position_config") or "",
@@ -1210,6 +1212,7 @@ class SxCPSceneStart:
"category_config": (SXCP_CATEGORY_CONFIG,),
"generation_profile": (SXCP_GENERATION_PROFILE,),
"filter_config": (SXCP_FILTER_CONFIG,),
"style_config": (SXCP_STYLE_CONFIG,),
"extra_positive": ("STRING", {"default": "", "multiline": True}),
"extra_negative": ("STRING", {"default": "", "multiline": True}),
},
@@ -1235,6 +1238,7 @@ class SxCPSceneStart:
category_config="",
generation_profile="",
filter_config="",
style_config="",
extra_positive="",
extra_negative="",
):
@@ -1258,6 +1262,7 @@ class SxCPSceneStart:
_set_config(scene, "category_config", category_config)
_set_config(scene, "generation_profile", generation_profile)
_set_config(scene, "filter_config", filter_config)
_set_config(scene, "style_config", style_config)
_add_history(scene, "scene_start", f"{category_preset}/{subcategory}; {profile}")
return _scene_out(scene)
@@ -2220,6 +2225,7 @@ class SxCPSceneOutput:
hardcore_position_config=configs["hardcore_position_config"],
location_config=configs["location_config"],
composition_config=configs["composition_config"],
style_config=configs["style_config"],
extra_positive=configs["extra_positive"],
extra_negative=configs["extra_negative"],
)
@@ -2289,6 +2295,7 @@ class SxCPScenePairOutput:
hardcore_position_config=hard_configs["hardcore_position_config"],
location_config=base_configs["location_config"] or hard_configs["location_config"],
composition_config=base_configs["composition_config"] or hard_configs["composition_config"],
style_config=base_configs["style_config"] or hard_configs["style_config"],
extra_positive=_joined_text(base_configs["extra_positive"], hard_configs["extra_positive"]),
extra_negative=base_configs["extra_negative"] or hard_configs["extra_negative"],
)
+13
View File
@@ -24,6 +24,7 @@ COMMON_INPUT_TOOLTIPS = {
"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.",
"style_config": "Visual style config from SxCP Style Pool. It controls realistic/photo/comic rendering separately from category, action, and pose logic.",
"softcore_camera_config": "Camera config used only for the softcore Insta/OF prompt. Falls back to camera_config if empty.",
"hardcore_camera_config": "Camera config used only for the hardcore Insta/OF prompt. Falls back to camera_config if empty.",
"character_profile": "Saved or loaded single-character profile. Character slots override this for configured casts.",
@@ -32,6 +33,9 @@ COMMON_INPUT_TOOLTIPS = {
"hardcore_position_config": "Hardcore action/position config. Chain Position Pool into Action Filter, then into the generator.",
"custom_locations": "One custom location per line. Use plain text, or slug: location text.",
"custom_compositions": "One custom composition/framing phrase per line.",
"custom_style": "Manual visual style phrase. Use this when the preset list is not specific enough.",
"custom_positive_suffix": "Manual style/quality suffix merged with the selected style preset.",
"custom_negative": "Negative style terms added by the style pool.",
"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.",
@@ -304,6 +308,15 @@ NODE_INPUT_TOOLTIPS = {
"phone_visibility": "Leave auto when using Qwen/Orbit camera prompts unless you explicitly want phone visibility text.",
"suppress_phone_visibility": "Avoid adding phone visibility text unless you explicitly set a phone option.",
},
"SxCPStylePool": {
"enabled": "Disable to keep the node wired while preserving category/default style behavior.",
"combine_mode": "replace overrides category style; add appends this visual style to incoming/category style; disabled emits no style override.",
"preset": "Visual rendering preset only. It does not select content, pose, exposure, or camera.",
"style_config": "Optional incoming style config. Use combine_mode=add to chain multiple style nodes.",
"custom_style": "Manual visual style phrase, for example realistic phone photo or colored-pencil pin-up.",
"custom_positive_suffix": "Extra rendering/detail sentence added to the prompt when the style is active.",
"custom_negative": "Negative style terms merged into the generated negative prompt.",
},
"SxCPHardcorePositionPool": {
"family": "Restrict the broad hardcore family. Use any when you want oral and penetration to both be possible.",
"combine_mode": "replace discards incoming position choices; add merges these choices with the incoming config.",
+2
View File
@@ -44,6 +44,7 @@ class InstaPairBuildRequest:
hardcore_position_config: str | dict[str, Any] | None = ""
location_config: str | dict[str, Any] | None = ""
composition_config: str | dict[str, Any] | None = ""
style_config: str | dict[str, Any] | None = ""
extra_positive: str = ""
extra_negative: str = ""
@@ -148,6 +149,7 @@ def build_insta_of_pair(request: InstaPairBuildRequest, deps: InstaPairBuildDepe
hardcore_position_config=request.hardcore_position_config,
location_config=request.location_config or "",
composition_config=request.composition_config or "",
style_config=request.style_config or "",
build_prompt=deps.build_prompt,
axis_rng=deps.axis_rng,
cast_expression_intensity_override=deps.cast_expression_intensity_override,
+5
View File
@@ -67,6 +67,7 @@ def build_insta_pair_rows_result(
softcore_item_prompt_label: Callable[[str], str],
pov_prompt_directive: Callable[[list[str]], str],
pov_composition_prompt: Callable[[Any, list[str]], str],
style_config: str | dict[str, Any] | None = "",
) -> InstaPairRowsRoute:
soft_content_rng = axis_rng(parsed_seed_config, "content", seed, row_number + 311)
hard_content_rng = axis_rng(parsed_seed_config, "content", seed, row_number + 317)
@@ -131,6 +132,7 @@ def build_insta_pair_rows_result(
character_cast="",
location_config=location_config or "",
composition_config=composition_config or "",
style_config=style_config or "",
)
soft_row["expression_intensity_source"] = soft_expression_intensity_source
if primary_slot_context:
@@ -196,6 +198,7 @@ def build_insta_pair_rows_result(
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
style_config=style_config or "",
)
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
hard_row["pov_character_labels"] = pov_character_labels
@@ -248,6 +251,7 @@ def build_insta_pair_rows(
softcore_item_prompt_label: Callable[[str], str],
pov_prompt_directive: Callable[[list[str]], str],
pov_composition_prompt: Callable[[Any, list[str]], str],
style_config: str | dict[str, Any] | None = "",
) -> dict[str, Any]:
return build_insta_pair_rows_result(
row_number=row_number,
@@ -273,6 +277,7 @@ def build_insta_pair_rows(
hardcore_position_config=hardcore_position_config,
location_config=location_config,
composition_config=composition_config,
style_config=style_config,
build_prompt=build_prompt,
axis_rng=axis_rng,
cast_expression_intensity_override=cast_expression_intensity_override,
+45 -2
View File
@@ -47,6 +47,7 @@ try:
from . import row_route_metadata as row_route_policy
from . import row_subject_route as row_subject_route_policy
from . import seed_config as seed_policy
from . import style_config as style_policy
from . import subject_context as subject_context_policy
from .hardcore_text_cleanup import (
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
@@ -95,6 +96,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
import row_route_metadata as row_route_policy
import row_subject_route as row_subject_route_policy
import seed_config as seed_policy
import style_config as style_policy
import subject_context as subject_context_policy
from hardcore_text_cleanup import (
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
@@ -376,6 +378,7 @@ CATEGORY_PRESETS = category_cast_policy.CATEGORY_PRESETS
CAST_PRESETS = category_cast_policy.CAST_PRESETS
GENERATION_PROFILE_PRESETS = generation_profile_policy.GENERATION_PROFILE_PRESETS
STYLE_PRESETS = style_policy.STYLE_PRESETS
def category_preset_choices() -> list[str]:
@@ -390,6 +393,14 @@ def generation_profile_choices() -> list[str]:
return generation_profile_policy.generation_profile_choices()
def style_pool_preset_choices() -> list[str]:
return style_policy.style_pool_preset_choices()
def style_combine_mode_choices() -> list[str]:
return style_policy.style_combine_mode_choices()
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
return category_cast_policy.build_category_config_json(preset=preset, subcategory=subcategory)
@@ -436,6 +447,30 @@ def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> di
return generation_profile_policy.parse_generation_profile(profile_config)
def build_style_config_json(
enabled: bool = True,
combine_mode: str = "replace",
preset: str = "category_default",
custom_style: str = "",
custom_positive_suffix: str = "",
custom_negative: str = "",
style_config: str | dict[str, Any] | None = "",
) -> str:
return style_policy.build_style_config_json(
enabled=enabled,
combine_mode=combine_mode,
preset=preset,
custom_style=custom_style,
custom_positive_suffix=custom_positive_suffix,
custom_negative=custom_negative,
style_config=style_config,
)
def _parse_style_config(style_config: str | dict[str, Any] | None) -> dict[str, Any]:
return style_policy.parse_style_config(style_config)
def build_filter_config_json(
ethnicity: str = "any",
figure: str = "curvy",
@@ -880,8 +915,9 @@ def _row_text_fields(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
style_config: str | dict[str, Any] | None = None,
) -> row_rendering_policy.RowTextFields:
return row_rendering_policy.resolve_row_text_fields(category, subcategory, item)
return row_rendering_policy.resolve_row_text_fields(category, subcategory, item, style_config)
def _clean_prompt_punctuation(text: str) -> str:
@@ -2284,6 +2320,7 @@ def _build_custom_row(
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
style_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
scene_rng = _axis_rng(seed_config, "scene", seed, row_number)
pose_rng = _axis_rng(seed_config, "pose", seed, row_number)
@@ -2421,7 +2458,7 @@ def _build_custom_row(
position_key = action_route.position_key
action_family = action_route.action_family
text_fields = _row_text_fields(category, subcategory, item)
text_fields = _row_text_fields(category, subcategory, item, style_config)
assembly_request = row_assembly_policy.CustomRowAssemblyRequest(
row_number=row_number,
@@ -2542,6 +2579,7 @@ def build_prompt(
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
style_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
return builder_prompt_route_policy.build_prompt(
builder_prompt_route_policy.PromptBuildRequest(
@@ -2575,6 +2613,7 @@ def build_prompt(
hardcore_position_config=hardcore_position_config,
location_config=location_config,
composition_config=composition_config,
style_config=style_config,
),
_prompt_build_dependencies(),
)
@@ -2605,6 +2644,7 @@ def build_prompt_from_configs(
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
style_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
@@ -2624,6 +2664,7 @@ def build_prompt_from_configs(
hardcore_position_config=hardcore_position_config,
location_config=location_config,
composition_config=composition_config,
style_config=style_config,
extra_positive=extra_positive,
extra_negative=extra_negative,
),
@@ -2801,6 +2842,7 @@ def build_insta_of_pair(
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
style_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
@@ -2825,6 +2867,7 @@ def build_insta_of_pair(
hardcore_position_config=hardcore_position_config,
location_config=location_config,
composition_config=composition_config,
style_config=style_config,
extra_positive=extra_positive,
extra_negative=extra_negative,
)
+19 -12
View File
@@ -8,18 +8,20 @@ try:
from . import category_library as category_policy
from . import generate_prompt_batches as g
from . import row_camera as row_camera_policy
from . import style_config as style_config_policy
except ImportError: # Allows local smoke tests from the repository root.
import category_library as category_policy
import generate_prompt_batches as g
import row_camera as row_camera_policy
import style_config as style_config_policy
GENERIC_POSITIVE_SUFFIX = (
"Use crisp clean comic linework, detailed hatching, soft blended shading, "
"pastel skin tones, muted blues and pinks, warm sensual lighting, and tactile textured paper."
"Use coherent anatomy, readable body placement, natural light response, "
"clear material texture, stable spatial depth, and polished visual detail."
)
DEFAULT_STYLE = "sexy but tasteful adult pin-up coloured-pencil comic illustration"
DEFAULT_STYLE = "realistic adult scene with natural camera realism"
@dataclass(frozen=True)
@@ -55,7 +57,7 @@ LAYOUT_TEMPLATE = (
)
DEFAULT_CAPTION_TEMPLATE = (
"{trigger}, {subject_phrase}, {age}, {item}, {scene}, {composition}, coloured pencil comic illustration"
"{trigger}, {subject_phrase}, {age}, {item}, {scene}, {composition}"
)
@@ -72,15 +74,20 @@ def format_template(template: str, context: dict[str, Any]) -> str:
return template.format_map(safe_context)
def resolve_row_text_fields(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> RowTextFields:
def resolve_row_text_fields(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
style_config: str | dict[str, Any] | None = None,
) -> RowTextFields:
base_negative = str(category_policy.merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
base_suffix = str(category_policy.merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
base_style = str(category_policy.merged_field(category, subcategory, item, "style", DEFAULT_STYLE))
style, positive_suffix = style_config_policy.resolve_style_fields(base_style, base_suffix, style_config)
return RowTextFields(
negative_prompt=str(
category_policy.merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT)
),
positive_suffix=str(
category_policy.merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX)
),
style=str(category_policy.merged_field(category, subcategory, item, "style", DEFAULT_STYLE)),
negative_prompt=style_config_policy.merge_negative_prompt(base_negative, style_config),
positive_suffix=positive_suffix,
style=style,
item_label=str(category_policy.merged_field(category, subcategory, item, "item_label", category["name"])),
)
+148
View File
@@ -0,0 +1,148 @@
from __future__ import annotations
import json
from typing import Any
STYLE_CONFIG_SCHEMA = "sxcp_style_config_v1"
STYLE_COMBINE_MODES = ("replace", "add", "disabled")
STYLE_PRESETS = {
"category_default": {
"style": "",
"positive_suffix": "",
"summary": "category default style",
},
"realistic_photo": {
"style": "realistic adult photographic scene, natural camera capture",
"positive_suffix": "Use realistic skin texture, natural light response, coherent anatomy, readable contact points, and believable spatial depth.",
"summary": "realistic photographic style",
},
"creator_phone_photo": {
"style": "realistic creator-shot phone photo, natural adult social-media image",
"positive_suffix": "Use handheld camera realism, natural skin texture, readable body positioning, and believable room depth.",
"summary": "creator phone photo style",
},
"documentary_flash": {
"style": "realistic direct-flash documentary photo, raw adult snapshot",
"positive_suffix": "Use direct flash, natural skin texture, sharp foreground detail, visible contact points, and unpolished camera realism.",
"summary": "direct flash documentary style",
},
"cinematic_realism": {
"style": "cinematic realistic adult scene, natural lens perspective",
"positive_suffix": "Use realistic anatomy, readable blocking, natural depth, motivated lighting, and coherent camera perspective.",
"summary": "cinematic realism style",
},
"comic_pinup_colored_pencil": {
"style": "adult erotic coloured-pencil comic pin-up style",
"positive_suffix": "Use crisp comic linework, detailed hatching, warm erotic lighting, soft skin shading, and tactile textured paper.",
"summary": "colored-pencil comic pin-up style",
},
"flat_vector_comic": {
"style": "flat vector adult comic illustration",
"positive_suffix": "Use flat color, clean graphic shapes, crisp outlines, simplified shadows, and readable adult body positioning.",
"summary": "flat vector comic style",
},
}
def style_pool_preset_choices() -> list[str]:
return list(STYLE_PRESETS)
def style_combine_mode_choices() -> list[str]:
return list(STYLE_COMBINE_MODES)
def _clean_text(value: Any) -> str:
return str(value or "").strip()
def _join_text(*values: Any) -> str:
parts: list[str] = []
for value in values:
text = _clean_text(value)
if text and text not in parts:
parts.append(text.rstrip("."))
return ". ".join(parts)
def parse_style_config(style_config: str | dict[str, Any] | None) -> dict[str, Any]:
if not style_config:
return {"enabled": False, "combine_mode": "disabled", "style": "", "positive_suffix": "", "negative_prompt": ""}
if isinstance(style_config, dict):
raw = dict(style_config)
else:
try:
raw = json.loads(str(style_config))
except json.JSONDecodeError:
return {"enabled": False, "combine_mode": "disabled", "style": "", "positive_suffix": "", "negative_prompt": ""}
if raw.get("schema") != STYLE_CONFIG_SCHEMA:
return {"enabled": False, "combine_mode": "disabled", "style": "", "positive_suffix": "", "negative_prompt": ""}
combine_mode = _clean_text(raw.get("combine_mode")) or "replace"
if combine_mode not in STYLE_COMBINE_MODES:
combine_mode = "replace"
return {
"schema": STYLE_CONFIG_SCHEMA,
"version": 1,
"enabled": bool(raw.get("enabled", True)) and combine_mode != "disabled",
"combine_mode": combine_mode,
"preset": _clean_text(raw.get("preset")) or "category_default",
"style": _clean_text(raw.get("style")),
"positive_suffix": _clean_text(raw.get("positive_suffix")),
"negative_prompt": _clean_text(raw.get("negative_prompt")),
"summary": _clean_text(raw.get("summary")) or "style config",
}
def build_style_config_json(
*,
enabled: bool = True,
combine_mode: str = "replace",
preset: str = "category_default",
custom_style: str = "",
custom_positive_suffix: str = "",
custom_negative: str = "",
style_config: str | dict[str, Any] | None = "",
) -> str:
if combine_mode not in STYLE_COMBINE_MODES:
combine_mode = "replace"
base = parse_style_config(style_config)
preset_entry = STYLE_PRESETS.get(preset, STYLE_PRESETS["category_default"])
style = _clean_text(custom_style) or preset_entry["style"]
positive_suffix = _clean_text(custom_positive_suffix) or preset_entry["positive_suffix"]
negative_prompt = _clean_text(custom_negative)
if combine_mode == "add" and base.get("enabled"):
style = _join_text(base.get("style"), style)
positive_suffix = _join_text(base.get("positive_suffix"), positive_suffix)
negative_prompt = _join_text(base.get("negative_prompt"), negative_prompt)
payload = {
"schema": STYLE_CONFIG_SCHEMA,
"version": 1,
"enabled": bool(enabled) and combine_mode != "disabled",
"combine_mode": combine_mode,
"preset": preset,
"style": style,
"positive_suffix": positive_suffix,
"negative_prompt": negative_prompt,
"summary": "style disabled" if not enabled or combine_mode == "disabled" else preset_entry["summary"],
}
return json.dumps(payload, ensure_ascii=True, sort_keys=True)
def resolve_style_fields(base_style: str, base_positive_suffix: str, style_config: str | dict[str, Any] | None) -> tuple[str, str]:
config = parse_style_config(style_config)
if not config.get("enabled"):
return base_style, base_positive_suffix
if config["combine_mode"] == "add":
return (
_join_text(base_style, config.get("style")),
_join_text(base_positive_suffix, config.get("positive_suffix")),
)
return config.get("style", "") or base_style, config.get("positive_suffix", "") or base_positive_suffix
def merge_negative_prompt(base_negative: str, style_config: str | dict[str, Any] | None) -> str:
config = parse_style_config(style_config)
if not config.get("enabled"):
return base_negative
return _join_text(base_negative, config.get("negative_prompt"))
+44
View File
@@ -477,6 +477,7 @@ def _prompt_row(
camera_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
style_config: str | dict[str, Any] | None = "",
) -> dict[str, Any]:
row = pb.build_prompt(
category=category,
@@ -507,6 +508,7 @@ def _prompt_row(
camera_config=camera_config,
location_config=location_config,
composition_config=composition_config,
style_config=style_config,
)
_expect_row_base(row, name)
return row
@@ -3385,6 +3387,16 @@ def smoke_row_rendering_policy() -> None:
_expect(default_text_fields.positive_suffix == row_rendering.GENERIC_POSITIVE_SUFFIX, "Row text fields lost suffix default")
_expect(default_text_fields.style == row_rendering.DEFAULT_STYLE, "Row text fields lost style default")
_expect(default_text_fields.item_label == "Default Category", "Row text fields lost category-name label default")
style_config = pb.build_style_config_json(preset="comic_pinup_colored_pencil")
styled_fields = row_rendering.resolve_row_text_fields(category_text, subcategory_text, item_text, style_config)
_expect("comic pin-up" in styled_fields.style, "Style Pool did not override row style")
_expect("comic linework" in styled_fields.positive_suffix, "Style Pool did not override row style suffix")
negative_style_config = pb.build_style_config_json(
preset="realistic_photo",
custom_negative="flat vector, comic paper texture",
)
negative_fields = row_rendering.resolve_row_text_fields(category_text, subcategory_text, item_text, negative_style_config)
_expect("comic paper texture" in negative_fields.negative_prompt, "Style Pool did not merge style negatives")
context = {
"trigger": Trigger,
@@ -5713,6 +5725,20 @@ def smoke_hardcore_category_routes() -> None:
_expect(sdxl_tag in (sdxl.get("sdxl_prompt") or "").lower(), f"{name} SDXL prompt did not include family tag {sdxl_tag!r}")
caption, _method = caption_naturalizer.naturalize_caption("", metadata_json=_json(row), trigger=Trigger, include_trigger=True)
_expect(caption_label in caption.lower(), f"{name} caption did not include family label {caption_label!r}")
styled_row = _prompt_row(
name="hardcore_style_pool_override",
category="Hardcore sexual poses",
subcategory="Penetrative sex",
seed=1181,
character_cast=cast,
women_count=1,
men_count=1,
hardcore_position_config=_action_filter("penetration_only"),
style_config=pb.build_style_config_json(preset="comic_pinup_colored_pencil"),
)
_expect("comic pin-up" in styled_row.get("style", ""), "Style Pool did not reach generated hardcore row style")
_expect("comic linework" in styled_row.get("positive_suffix", ""), "Style Pool did not reach generated hardcore row suffix")
_expect("comic pin-up" in styled_row.get("prompt", ""), "Style Pool style was not rendered into hardcore prompt")
multi_cases = [
("hardcore_threesome", "Threesomes", "threesome_only", "threesome", {"threesome", "toy_double"}, "threesome", "three-person action", 1, 2),
("hardcore_group", "Group sex and orgy", "group_only", "group", {"group", "toy_double"}, "group sex", "group action", 2, 2),
@@ -8239,6 +8265,7 @@ def smoke_node_route_config_registration() -> None:
"SxCPLocationPool",
"SxCPCompositionPool",
"SxCPLocationTheme",
"SxCPStylePool",
"SxCPCastControl",
"SxCPCastBias",
]
@@ -8286,6 +8313,23 @@ def smoke_node_route_config_registration() -> None:
_expect(json.loads(theme_composition).get("composition_entries"), "Location Theme did not output compositions")
_expect("semi_public_affair" in theme_summary, "Location Theme summary lost theme name")
style_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPStylePool"]
style_inputs = style_node.INPUT_TYPES().get("required") or {}
_expect("preset" in style_inputs, "Style Pool lost preset input")
_expect("tooltip" in style_inputs["preset"][1], "Style Pool tooltip injection missing")
style_config, style_summary = style_node().build(
True,
"replace",
"comic_pinup_colored_pencil",
"",
"",
"",
)
parsed_style = json.loads(style_config)
_expect(parsed_style.get("schema") == "sxcp_style_config_v1", "Style Pool emitted wrong schema")
_expect("comic pin-up" in parsed_style.get("style", ""), "Style Pool lost comic preset style")
_expect("comic pin-up" in style_summary, "Style Pool summary lost preset label")
cast_config, women_count, men_count, cast_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCastControl"]().build(
"mixed_couple",
1,