diff --git a/README.md b/README.md index aef3d6a..bc3c02c 100644 --- a/README.md +++ b/README.md @@ -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", diff --git a/__init__.py b/__init__.py index a1d45a2..86513ee 100644 --- a/__init__.py +++ b/__init__.py @@ -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" diff --git a/builder_config_route.py b/builder_config_route.py index c1e9de4..ae6a426 100644 --- a/builder_config_route.py +++ b/builder_config_route.py @@ -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), diff --git a/builder_prompt_route.py b/builder_prompt_route.py index 63e27bf..58ca6bc 100644 --- a/builder_prompt_route.py +++ b/builder_prompt_route.py @@ -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": diff --git a/categories/default_categories.json b/categories/default_categories.json index 6ab6770..5b9fa8e 100644 --- a/categories/default_categories.json +++ b/categories/default_categories.json @@ -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": [ diff --git a/categories/erotic_clothes.json b/categories/erotic_clothes.json index 44bba9a..1b97f11 100644 --- a/categories/erotic_clothes.json +++ b/categories/erotic_clothes.json @@ -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"], diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 94aa31e..9b2790f 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -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` | diff --git a/node_builder.py b/node_builder.py index ee3f20a..60ec78f 100644 --- a/node_builder.py +++ b/node_builder.py @@ -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 "", diff --git a/node_insta.py b/node_insta.py index 6006661..50c9d50 100644 --- a/node_insta.py +++ b/node_insta.py @@ -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 "", diff --git a/node_route_config.py b/node_route_config.py index 639967d..64d6e59 100644 --- a/node_route_config.py +++ b/node_route_config.py @@ -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", } diff --git a/node_scene.py b/node_scene.py index 8cb70de..a9451a2 100644 --- a/node_scene.py +++ b/node_scene.py @@ -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"], ) diff --git a/node_tooltips.py b/node_tooltips.py index a67a9d6..3997490 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -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.", diff --git a/pair_builder.py b/pair_builder.py index 4743148..0cb4f7a 100644 --- a/pair_builder.py +++ b/pair_builder.py @@ -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, diff --git a/pair_rows.py b/pair_rows.py index 95e88e3..a8b94ac 100644 --- a/pair_rows.py +++ b/pair_rows.py @@ -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, diff --git a/prompt_builder.py b/prompt_builder.py index 2f12a09..389a83b 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -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, ) diff --git a/row_rendering.py b/row_rendering.py index a48b586..f96eaaa 100644 --- a/row_rendering.py +++ b/row_rendering.py @@ -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"])), ) diff --git a/style_config.py b/style_config.py new file mode 100644 index 0000000..28e1f2d --- /dev/null +++ b/style_config.py @@ -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")) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 504fe66..612e859 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -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,