from __future__ import annotations import json import random import re try: from aiohttp import web from server import PromptServer except Exception: web = None PromptServer = None SXCP_HAIR_CONFIG = "SXCP_HAIR_CONFIG" SXCP_CHARACTERISTICS = "SXCP_CHARACTERISTICS" SXCP_CHARACTER_MANUAL = "SXCP_CHARACTER_MANUAL" SXCP_ETHNICITY_LIST = "SXCP_ETHNICITY_LIST" SXCP_FILTER_CONFIG = "SXCP_FILTER_CONFIG" SXCP_SEED_CONFIG = "SXCP_SEED_CONFIG" SXCP_CAMERA_CONFIG = "SXCP_CAMERA_CONFIG" SXCP_LOCATION_CONFIG = "SXCP_LOCATION_CONFIG" 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_INSTA_OF_OPTIONS = "SXCP_INSTA_OF_OPTIONS" SXCP_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG" SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT" SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE" SDXL_BUCKET_RESOLUTIONS = [ {"orientation": "portrait", "width": 896, "height": 1792, "aspect": 0.50, "mp": 1.61}, {"orientation": "portrait", "width": 960, "height": 1664, "aspect": 0.58, "mp": 1.60}, {"orientation": "portrait", "width": 1024, "height": 1600, "aspect": 0.64, "mp": 1.64}, {"orientation": "portrait", "width": 1088, "height": 1472, "aspect": 0.74, "mp": 1.60}, {"orientation": "portrait", "width": 1152, "height": 1408, "aspect": 0.82, "mp": 1.62}, {"orientation": "portrait", "width": 1216, "height": 1344, "aspect": 0.90, "mp": 1.63}, {"orientation": "square", "width": 1280, "height": 1280, "aspect": 1.00, "mp": 1.64}, {"orientation": "landscape", "width": 1344, "height": 1216, "aspect": 1.11, "mp": 1.63}, {"orientation": "landscape", "width": 1408, "height": 1152, "aspect": 1.22, "mp": 1.62}, {"orientation": "landscape", "width": 1472, "height": 1088, "aspect": 1.35, "mp": 1.60}, {"orientation": "landscape", "width": 1536, "height": 1024, "aspect": 1.50, "mp": 1.57}, ] COMMON_INPUT_TOOLTIPS = { "row_number": "Generation row to use. Changing it advances the deterministic selection without changing the main seed.", "start_index": "Metadata/output index offset only. It does not limit category pools or random choices.", "seed": "Main seed used when no more specific seed config overrides an axis.", "global_seed": "One seed that locks all prompt axes so the same inputs can recreate the same result.", "base_seed": "Base seed used by Seed Locker before applying a selected reroll axis.", "reroll_seed": "Seed for the selected reroll axis. Use -1 to derive it from the base seed.", "category": "Main category source. auto_weighted is legacy random; auto_full mixes legacy random with JSON categories including hardcore.", "subcategory": "Specific subcategory, or random to choose within the selected category.", "category_config": "Category/subcategory config from SxCP Category Preset.", "cast_config": "Cast size config from SxCP Cast Control.", "generation_profile": "General style/intensity profile from SxCP Generation Profile.", "filter_config": "Ethnicity/body filter config. Ethnicity List can feed this too.", "ethnicity_list": "Optional ethnicity pool. When connected, it overrides the slot or generator ethnicity picker.", "seed_config": "Per-axis seed config. Connect Global Seed, Seed Locker, or Seed Control here.", "camera_config": "Camera config used by the prompt formatter when camera mode is 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.", "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.", "character_cast": "Chain character slots here. The node closest to the final generator becomes the next auto_chain label.", "character_slot": "Single slot payload for saving/loading profiles or debugging one character.", "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.", "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.", "source_text": "Raw prompt, caption, or metadata JSON depending on input_hint.", "source_text_input": "Optional linked raw prompt/caption input. When connected, it overrides the source_text widget.", "input_hint": "Tells the node how to interpret source_text. auto tries metadata first.", "target": "For dual prompts, choose which side to output as the main Krea prompt.", "detail_level": "Controls how much detail the rewriter keeps. concise is shorter, dense keeps more clauses.", "style_mode": "How strongly the formatter rewrites visual style terms.", "preserve_trigger": "Keep the trigger token in the formatted prompt instead of stripping it.", "negative_prompt": "Negative prompt text to pass through or merge with generated negatives.", "extra_positive": "Extra positive text appended after the generated prompt.", "extra_negative": "Extra negative text appended after the generated negative prompt.", "trigger": "Training or style trigger token.", "prepend_trigger_to_prompt": "If enabled, put the trigger token at the start of generated prompts.", "bucket_index": "0 picks a random bucket. 1+ picks that position inside the selected orientation pool.", "megapixels": "Approximate megapixel count for the selected bucket.", "enabled": "Enable this node's effect while keeping it wired in the graph.", "combine_mode": "replace starts a new pool/config; add merges selected values into the incoming config.", "manual": "Manual character details config. Non-empty manual fields override generated slot details.", "characteristics": "Chainable character characteristic pool such as age/body/eyes/clothing.", "hair_config": "Chainable hair pool. Combine length, color, and style nodes before the character slot.", "summary": "Human-readable description of the config produced by this node.", "status": "Operation result or warning text.", "profile_name": "Name of the profile to save, load, rename, or delete.", "manual_profile_name": "Free-text profile name used when profile_name is set to manual.", "fallback_profile_json": "Profile JSON to use when a named profile cannot be loaded.", "rename_to": "New profile name used only when rename_now is enabled.", "save_now": "Writes the profile to disk only when enabled. Keep off while adjusting fields.", "delete_now": "Deletes the selected profile when enabled.", "rename_now": "Renames the selected profile when enabled.", "source": "Where the save node reads character data from.", "subject_type": "Character type for this slot or saved profile.", "label": "Character label. auto_chain assigns the next Woman/Man label based on incoming cast order.", "slot_seed": "Per-character seed. Use -1 to follow the generator person seed.", "age": "Age choice for this slot. Use Age Range node for a custom random age pool.", "manual_age": "Exact age phrase override, for example '32-year-old adult'.", "ethnicity": "Ethnicity choice for this slot. A connected Ethnicity List overrides this picker.", "figure": "General figure bias for generated body descriptors.", "figure_bias": "Woman-slot figure bias. Body pool can give more precise body choices.", "women_count": "Number of women in the generated cast when no Insta/OF preset overrides it.", "men_count": "Number of men in the generated cast when no Insta/OF preset overrides it.", "hardcore_women_count": "Number of women in the hardcore cast when hardcore_cast is use_counts.", "hardcore_men_count": "Number of men in the hardcore cast when hardcore_cast is use_counts.", "body": "Body choice for this slot. A Body Pool node can replace the random list.", "manual_body": "Exact body phrase override.", "body_phrase": "Full custom body wording. Use only when the body picker is not specific enough.", "skin": "Manual skin/complexion phrase.", "hair": "Manual hair phrase. Hair config nodes are better for controlled random choices.", "eyes": "Manual eye description.", "descriptor_detail": "How detailed this character's descriptor should be. Men usually work better compact.", "expression_enabled": "Master expression toggle for this generator or character.", "expression_intensity": "Expression intensity from 0 to 1. On the direct builder, -1 randomizes per row; on slots, -1 inherits the generator setting.", "expression_intensity_mode": "For Generation Profile, choose profile_default, random, or fixed value from expression_intensity.", "softcore_expression_intensity": "Optional expression intensity override for this character in softcore prompts. -1 inherits.", "hardcore_expression_intensity": "Optional expression intensity override for this character in hardcore prompts. -1 inherits.", "presence_mode": "Controls whether the character is visible, implied POV, or otherwise present.", "softcore_outfit": "Manual softcore outfit text for this character.", "hardcore_clothing": "Manual hardcore clothing/body exposure text for this character.", "custom_softcore_outfits": "One custom softcore outfit per line. Used when softcore_source is custom.", "custom_hardcore_clothing": "One custom hardcore clothing/body exposure state per line.", "condition": "Loop condition. When false, the loop stops and passes current values through.", "total": "Total number of loop iterations.", "skip": "Number of leading loop indexes to skip. skip=1 starts generation at index 2.", "collection": "Existing accumulated value or batch.", "value": "Value to append, store, or pass through.", "store_key": "Accumulator memory key. Same key shares stored entries across executions.", "store_key_input": "Connect SxCP Accumulator store_key here so preview/delete/save uses the same accumulator and graph dependency.", "action": "Accumulator operation: append, replace, clear, read, or append a variant.", "max_items": "Maximum stored entries kept in this accumulator.", "image_batch_mode": "How image entries are batched when dimensions differ.", "skip_empty": "Ignore empty inputs instead of adding blank entries.", "image": "Image to store in the accumulator.", "entry_id": "Stable ID used for replace_by_entry_id or grouping variants.", "entry_tag": "Optional suffix added to entry_id.", "preview_limit": "Maximum number of accumulator images to show in the preview panel.", "view_mode": "Accumulator Preview layout: grid shows many images, carousel shows one large image at a time.", "zoom_level": "Accumulator Preview image scale. Higher values make grid thumbnails or carousel image area larger.", "carousel_index": "1-based image position shown in carousel mode. The previous/next buttons update this value.", "delete_action": "Optional execution-time delete operation. JS buttons can delete interactively without setting this.", "delete_entry_id": "Entry id to delete when delete_action is delete_entry_id.", "delete_index": "1-based entry index to delete when delete_action is delete_index. 0 disables it.", "save_batch": "When enabled, save all current accumulator images once finished is true.", "finished": "Gate for saving. Outside a loop, leave true; inside a loop, wire a final-iteration signal.", "save_path": "Folder to save the accumulator batch. Relative paths are inside ComfyUI output; absolute paths are used directly.", "filename_prefix": "Filename prefix for saved accumulator images.", "clear_after_save": "Clear the accumulator store after a successful batch save.", "preview_text": "Serialized persistent text preview. It is updated after execution and saved with the workflow.", "preview_format": "How to convert an arbitrary input to preview text.", "max_chars": "Maximum stored preview characters. 0 disables truncation.", "mode": "Switch direction: pick_input selects one input to value, route_output sends route_value to one output.", "index": "Index used by SxCP Index Switch. For Loop Start outputs one_based indexes by default.", "index_base": "one_based means index 1 selects input_1. zero_based means index 0 selects input_1.", "missing_behavior": "What to do when the requested switch input is not connected: use fallback, output none, clamp, or wrap.", "fallback": "Optional value used by SxCP Index Switch when the requested input is missing and missing_behavior is fallback.", "route_value": "Value routed to output_N when mode is route_output.", "clothing": "Built-in clothing density for legacy direct generation. random picks full/minimal from the seeded row.", "poses": "Built-in pose pool for legacy direct generation. random picks standard/evocative from the seeded row.", "backside_bias": "Legacy bias toward rear/backside poses where that category supports it.", "minimal_clothing_ratio": "Legacy weighted ratio override. -1 keeps the category/profile default.", "standard_pose_ratio": "Legacy weighted ratio override. -1 keeps the category/profile default.", "profile": "Generation profile preset for broad style, clothing, pose, and expression defaults.", "clothing_override": "Override the profile clothing setting, or leave profile_default.", "poses_override": "Override the profile pose setting, or leave profile_default.", "trigger_policy": "Controls whether the profile prepends the trigger token.", "cast_mode": "Preset cast shape. Custom counts are used when the preset allows them.", "women_weights": "Comma-separated count weights. First value maps to women_start_count, second to +1, and so on.", "men_weights": "Comma-separated count weights. First value maps to men_start_count, second to +1, and so on.", "women_start_count": "Woman count represented by the first women_weights value.", "men_start_count": "Man count represented by the first men_weights value.", "empty_behavior": "What to do if the weighted pick selects zero women and zero men.", "preset": "Category preset for common workflow lanes.", "camera_mode": "Camera style preset.", "shot_size": "How much of the body/frame should be visible.", "angle": "Camera angle relative to the subject.", "lens": "Lens wording to include in the prompt.", "distance": "Camera distance wording.", "orientation": "Horizontal/vertical framing wording.", "phone_visibility": "Whether the prompt mentions a visible/hidden phone.", "priority": "How strictly the prompt should enforce the camera wording.", "camera_detail": "off omits camera text, compact keeps one line, full emits detailed camera wording.", "subject_focus": "Optional camera focus phrase, such as face/body/contact emphasis.", "strict_excludes": "When enabled, only selected ethnicity groups are used. When off, selections act more like soft includes.", "min_age": "Minimum adult age in this custom age pool.", "max_age": "Maximum adult age in this custom age pool.", "softcore_source": "Softcore outfit source for this character. custom reads custom_softcore_outfits.", "hardcore_state": "Hardcore clothing/body exposure state for this character.", "softcore_expression_enabled": "Enable expression text in the softcore prompt.", "hardcore_expression_enabled": "Enable expression text in the hardcore prompt.", "flow": "Loop flow-control socket. Wire from the matching loop start node.", "collection_mode": "How the loop end collects per-iteration values.", "skip_none": "Do not add empty values to the collection.", "collected": "Current accumulated value carried through the loop.", "collect_value": "Value captured from the current loop iteration.", "a": "First integer/boolean helper input.", "b": "Second integer/boolean helper input.", } NODE_INPUT_TOOLTIPS = { "SxCPSeedControl": { "category_seed_mode": "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis each queue.", "subcategory_seed_mode": "Controls which subcategory is selected. Change this to switch oral vs penetration when both are allowed.", "content_seed_mode": "Controls item/outfit content for non-pose categories.", "person_seed_mode": "Controls generated character appearance unless a slot seed overrides it.", "scene_seed_mode": "Controls location/scene selection.", "pose_seed_mode": "Controls pose/item selection for pose categories, including hardcore positions.", "role_seed_mode": "Controls role assignment and secondary action details.", "expression_seed_mode": "Controls selected expression text.", "composition_seed_mode": "Controls framing/composition text.", }, "SxCPSeedLocker": { "reroll_axis": "Choose the one axis to change while the rest stays locked. Use pose for sexual pose, scene for location, person for appearance.", }, "SxCPCastBias": { "seed": "Fixed cast-bias seed. Use -1 for a fresh cast each queue, or connect Global Seed/Seed Locker through seed_config.", "seed_config": "Optional seed config. The category seed controls weighted cast selection.", "women_weights": "Example with women_start_count=1: 0.6,0.25,0.1 means 60% one woman, 25% two women, 10% three women.", "men_weights": "Example with men_start_count=0: 0.5,0.35,0.1 means 50% no man, 35% one man, 10% two men.", "empty_behavior": "Prevents accidental empty casts when both weighted pools pick zero.", }, "SxCPSDXLBucketSize": { "orientation": "Bucket orientation filter. any uses the full table; portrait/square/landscape restrict random selection.", "seed": "Fixed bucket seed. Use -1 for a fresh random bucket each queue, or connect Global Seed for reproducible sizes.", "row_number": "Deterministic row offset for the bucket. With a fixed seed, changing this advances the bucket choice.", "bucket_index": "0=random. 1+ selects that bucket position inside the selected orientation pool and ignores seed.", "seed_config": "Optional seed config. The composition seed controls bucket choice, so Seed Locker can keep sizes fixed while rerolling pose/person.", }, "SxCPCameraControl": { "camera_mode": "Camera style preset. Use from_camera_config in Insta/OF options to consume this.", "priority": "locked makes the camera wording strict; soft_hint allows the model more freedom.", "camera_detail": "off omits camera text, compact keeps one short line, full emits detailed camera constraints.", "phone_visibility": "Use phone_hidden or suppress_phone_visibility when you do not want 'phone hidden' text in prompts.", }, "SxCPCameraOrbitControl": { "horizontal_angle": "Orbit angle in degrees. 0=front, 90=right side, 180=back, 270=left side.", "vertical_angle": "Camera elevation. Negative looks up, positive looks down.", "zoom": "Maps to distance/framing when framing is from_zoom.", "framing": "How zoom should be translated into shot size/distance wording.", "include_degrees": "Include numeric degree wording in addition to human camera direction.", }, "SxCPQwenCameraTranslator": { "qwen_prompt": "Camera prompt from Qwen MultiAngle, for example ' front-right quarter view eye-level shot medium shot'.", "camera_info": "Optional structured camera_info from Qwen MultiAngle. Used before qwen_prompt when prefer_camera_info is true.", "prefer_camera_info": "Use structured camera_info values when available instead of parsing the text prompt.", "suppress_phone_visibility": "Avoid adding phone visibility text unless you explicitly set a phone option.", }, "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.", "hardcore_position_config": "Optional incoming config. Usually connect previous Position Pool here only when chaining pools.", }, "SxCPHardcoreActionFilter": { "focus": "keep_pool preserves/broadens the incoming pool; *_only modes force one action family.", "allow_toys": "Allow toy/strap-on wording in hardcore actions.", "allow_double": "Allow double-penetration or second-contact wording.", "allow_penetration": "Allow vaginal/penetrative sex subcategories.", "allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.", "allow_oral": "Allow oral sex subcategories.", "allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.", "allow_anal": "Allow anal subcategories.", "allow_climax": "Allow cumshot/climax aftermath subcategories.", }, "SxCPInstaOFOptions": { "softcore_cast": "solo keeps softcore focused on Woman A; same_as_hardcore includes the same cast as the hardcore prompt.", "hardcore_cast": "use_counts reads hardcore_women_count/hardcore_men_count; presets set the counts automatically.", "softcore_level": "Controls the soft prompt exposure/outfit level.", "hardcore_level": "Controls how explicit the hardcore prompt style is.", "platform_style": "Instagram/OnlyFans styling bias for the dual prompt pair.", "continuity": "Whether the softcore and hardcore prompts share the room/creator setup.", "hardcore_clothing_continuity": "How clothing carries from softcore to hardcore. explicit_nude omits clothing references.", "softcore_camera_mode": "Camera mode for the softcore prompt, or from_camera_config.", "hardcore_camera_mode": "Camera mode for the hardcore prompt. same_as_softcore reuses the softcore setting.", "camera_detail": "Global camera verbosity for the pair unless a camera config overrides it.", "hardcore_detail_density": "How dense the hardcore action sentence should be in the Krea formatter.", }, "SxCPInstaOFPromptPair": { "options_json": "Options from SxCP Insta/OF Options. If empty, defaults are used.", "ethnicity": "Fallback ethnicity when no filter/ethnicity list or character slots are connected.", "figure": "Fallback figure bias when no character slot overrides it.", }, "SxCPPromptBuilderFromConfigs": { "seed": "Main seed. Connect Seed Config for per-axis control.", }, "SxCPCharacterProfileSave": { "profile_name": "Profile filename stem. Saving requires save_now=true.", "metadata_json": "Use generator metadata to save the currently generated character without regenerating it.", "character_slot": "Use this when saving a configured slot directly.", }, "SxCPCharacterProfileLoad": { "enabled": "When false, outputs an empty profile and leaves downstream generation unchanged.", "override_age": "Optional loaded-profile override. Empty keeps the profile value.", "override_body": "Optional body override. Empty keeps the profile value.", "override_descriptor_detail": "Override descriptor verbosity while keeping the rest of the loaded profile.", }, "SxCPKrea2Formatter": { "metadata_json": "Best input for Krea2 formatting because it preserves cast, camera, and hardcore action metadata.", "preserve_trigger": "Reminder: Krea2 formatting is intended to remove training/style triggers. Leave false unless you intentionally want a raw text trigger preserved.", "source_text": "Raw prompt fallback. Known trigger tokens are stripped by default for Krea2.", }, "SxCPSDXLFormatter": { "metadata_json": "Best input for SDXL tag formatting because it preserves cast, camera, outfit, and explicit action metadata.", "style_preset": "Positive style anchor preset. flat_vector_pony matches the old SDXL tag style.", "quality_preset": "Quality/score tag tail for SDXL or Pony-style checkpoints.", "custom_style": "Optional replacement for the style preset. Leave empty to use style_preset.", "custom_quality": "Optional replacement for the quality preset. Leave empty to use quality_preset.", "nude_weight": "Weight used when explicit nude/body exposure tags are inferred.", }, "SxCPCaptionNaturalizer": { "metadata_json": "Best input for training captions because it preserves structured generator details.", "style_policy": "drop_style_tail removes generation/style boilerplate; keep_style_terms preserves more of it.", "include_trigger": "Keep this true for LoRA/training captions so the trigger token is learned.", }, "SxCPForLoopStart": { "index": "Output loop index. First generated index is skip + 1.", "collected": "Current accumulated value carried through the loop.", }, "SxCPLoopAppend": { "mode": "auto_batch tries tensor/latent batching first, then falls back to a list.", }, "SxCPAccumulator": { "image_batch_mode": "same_size_only keeps incompatible sizes separate; resize_to_first forces one image batch.", }, } def _tooltip_for_input(node_name: str, input_name: str) -> str: node_tooltips = NODE_INPUT_TOOLTIPS.get(node_name, {}) if input_name in node_tooltips: return node_tooltips[input_name] if input_name in COMMON_INPUT_TOOLTIPS: return COMMON_INPUT_TOOLTIPS[input_name] if input_name.endswith("_seed_mode"): axis = input_name[: -len("_seed_mode")] return f"How the {axis} seed is resolved: follow the main seed, use the fixed field, or reroll randomly." if input_name.endswith("_seed"): axis = input_name[: -len("_seed")] return f"Fixed {axis} seed value. Used only when the matching seed mode is fixed, or as a fallback for auto modes." if input_name.startswith("include_"): value = input_name[len("include_") :].replace("_", " ") return f"Include {value} in this random pool." if input_name.startswith("initial_value"): return "Carry value passed into the loop body and returned on the matching output." if re.match(r"^input_\d+$", input_name): return "Autoscaling switch input. Connect the last visible input to reveal the next one." if re.match(r"^output_\d+$", input_name): return "Autoscaling routed output. Connect the last visible output to reveal the next one." if input_name.startswith("override_"): return "Optional loaded-profile override. Leave empty or keep_profile to preserve the profile value." return "" def _copy_input_spec_with_tooltip(input_spec, tooltip: str): if not tooltip or not isinstance(input_spec, tuple): return input_spec if len(input_spec) >= 2 and isinstance(input_spec[1], dict): options = dict(input_spec[1]) options.setdefault("tooltip", tooltip) return (input_spec[0], options, *input_spec[2:]) if len(input_spec) == 1: return (input_spec[0], {"tooltip": tooltip}) return input_spec def _inject_input_tooltips(input_types: dict, node_name: str) -> dict: patched = dict(input_types) for group_name in ("required", "optional"): group = patched.get(group_name) if not isinstance(group, dict): continue patched_group = {} for input_name, input_spec in group.items(): patched_group[input_name] = _copy_input_spec_with_tooltip( input_spec, _tooltip_for_input(node_name, input_name), ) patched[group_name] = patched_group return patched def _install_input_tooltips(node_classes: dict[str, type]) -> None: for node_name, node_class in node_classes.items(): original = getattr(node_class, "INPUT_TYPES", None) if original is None or getattr(node_class, "_sxcp_tooltips_installed", False): continue def input_types(cls, _original=original, _node_name=node_name): return _inject_input_tooltips(_original(), _node_name) node_class.INPUT_TYPES = classmethod(input_types) node_class._sxcp_tooltips_installed = True try: from .loop_nodes import ( ANY_TYPE, LOOP_NODE_CLASS_MAPPINGS, LOOP_NODE_DISPLAY_NAME_MAPPINGS, accumulator_delete_entries, accumulator_list_entries, accumulator_move_entry, accumulator_save_entries, ) from .prompt_builder import ( build_camera_config_json, build_camera_orbit_config_json, build_qwen_camera_config_json, build_cast_config_json, build_category_config_json, build_character_slot_json, build_character_manual_config_json, build_character_profile_json, build_characteristics_config_json, build_composition_pool_json, build_ethnicity_list_json, build_filter_config_json, build_generation_profile_json, build_hair_config_json, build_hardcore_action_filter_json, build_hardcore_position_pool_json, build_insta_of_options_json, build_location_pool_json, build_thematic_location_json, build_insta_of_pair, build_prompt, build_prompt_from_configs, build_seed_config_json, build_seed_lock_config_json, camera_angle_choices, camera_detail_choices, camera_distance_choices, camera_lens_choices, camera_mode_choices, camera_orbit_focus_choices, camera_orbit_framing_choices, camera_orientation_choices, camera_phone_choices, camera_priority_choices, camera_shot_choices, cast_preset_choices, category_preset_choices, category_choices, character_age_choices, character_body_choices, character_descriptor_detail_choices, character_ethnicity_choices, character_eye_color_choices, character_figure_choices, character_hair_color_choices, character_hair_length_choices, character_hair_style_choices, character_label_choices, character_man_body_choices, character_presence_choices, character_profile_choices, character_hardcore_clothing_state_choices, character_hardcore_clothing_values, character_softcore_outfit_source_choices, character_softcore_outfit_values, character_woman_body_choices, composition_pool_preset_choices, ethnicity_choices, generation_profile_choices, hardcore_position_family_choices, hardcore_position_focus_choices, hardcore_position_key_choices, hardcore_detail_density_choices, load_character_profile_json, location_theme_choices, location_pool_preset_choices, save_character_profile_payload, seed_mode_choices, subcategory_choices, ) from .caption_naturalizer import naturalize_caption from .krea_formatter import format_krea2_prompt from .sdxl_formatter import format_sdxl_prompt, sdxl_quality_preset_choices, sdxl_style_preset_choices except ImportError: from loop_nodes import ( ANY_TYPE, LOOP_NODE_CLASS_MAPPINGS, LOOP_NODE_DISPLAY_NAME_MAPPINGS, accumulator_delete_entries, accumulator_list_entries, accumulator_move_entry, accumulator_save_entries, ) from prompt_builder import ( build_camera_config_json, build_camera_orbit_config_json, build_qwen_camera_config_json, build_cast_config_json, build_category_config_json, build_character_slot_json, build_character_manual_config_json, build_character_profile_json, build_characteristics_config_json, build_composition_pool_json, build_ethnicity_list_json, build_filter_config_json, build_generation_profile_json, build_hair_config_json, build_hardcore_action_filter_json, build_hardcore_position_pool_json, build_insta_of_options_json, build_location_pool_json, build_thematic_location_json, build_insta_of_pair, build_prompt, build_prompt_from_configs, build_seed_config_json, build_seed_lock_config_json, camera_angle_choices, camera_detail_choices, camera_distance_choices, camera_lens_choices, camera_mode_choices, camera_orbit_focus_choices, camera_orbit_framing_choices, camera_orientation_choices, camera_phone_choices, camera_priority_choices, camera_shot_choices, cast_preset_choices, category_preset_choices, category_choices, character_age_choices, character_body_choices, character_descriptor_detail_choices, character_ethnicity_choices, character_eye_color_choices, character_figure_choices, character_hair_color_choices, character_hair_length_choices, character_hair_style_choices, character_label_choices, character_man_body_choices, character_presence_choices, character_profile_choices, character_hardcore_clothing_state_choices, character_hardcore_clothing_values, character_softcore_outfit_source_choices, character_softcore_outfit_values, character_woman_body_choices, composition_pool_preset_choices, ethnicity_choices, generation_profile_choices, hardcore_position_family_choices, hardcore_position_focus_choices, hardcore_position_key_choices, hardcore_detail_density_choices, load_character_profile_json, location_theme_choices, location_pool_preset_choices, save_character_profile_payload, seed_mode_choices, subcategory_choices, ) from caption_naturalizer import naturalize_caption from krea_formatter import format_krea2_prompt from sdxl_formatter import format_sdxl_prompt, sdxl_quality_preset_choices, sdxl_style_preset_choices if PromptServer is not None and web is not None: @PromptServer.instance.routes.post("/sxcp/profile/save_cached") async def sxcp_save_cached_profile(request): try: payload = await request.json() result = save_character_profile_payload( profile_name=str(payload.get("profile_name") or ""), profile_json=payload.get("profile_json") or "", ) return web.json_response(result) except Exception as exc: return web.json_response({"error": str(exc)}, status=400) @PromptServer.instance.routes.post("/sxcp/accumulator/list") async def sxcp_accumulator_list(request): try: payload = await request.json() result = accumulator_list_entries( str(payload.get("store_key") or ""), preview_limit=int(payload.get("preview_limit") or 0), ) return web.json_response(result) except Exception as exc: return web.json_response({"error": str(exc)}, status=400) @PromptServer.instance.routes.post("/sxcp/accumulator/delete") async def sxcp_accumulator_delete(request): try: payload = await request.json() result = accumulator_delete_entries( store_key=str(payload.get("store_key") or ""), preview_key=str(payload.get("preview_key") or ""), entry_id=str(payload.get("entry_id") or ""), index=int(payload.get("index") or 0), clear=bool(payload.get("clear")), preview_limit=int(payload.get("preview_limit") or 0), ) return web.json_response(result) except Exception as exc: return web.json_response({"error": str(exc)}, status=400) @PromptServer.instance.routes.post("/sxcp/accumulator/save") async def sxcp_accumulator_save(request): try: payload = await request.json() result = accumulator_save_entries( store_key=str(payload.get("store_key") or ""), save_path=str(payload.get("save_path") or "sxcp_accumulator"), filename_prefix=str(payload.get("filename_prefix") or "sxcp_accum"), clear_after_save=bool(payload.get("clear_after_save")), preview_limit=int(payload.get("preview_limit") or 0), ) return web.json_response(result) except Exception as exc: return web.json_response({"error": str(exc)}, status=400) @PromptServer.instance.routes.post("/sxcp/accumulator/move") async def sxcp_accumulator_move(request): try: payload = await request.json() result = accumulator_move_entry( store_key=str(payload.get("store_key") or ""), preview_key=str(payload.get("preview_key") or ""), entry_id=str(payload.get("entry_id") or ""), index=int(payload.get("index") or 0), direction=str(payload.get("direction") or "up"), target_index=int(payload.get("target_index") or 0), preview_limit=int(payload.get("preview_limit") or 0), ) return web.json_response(result) except Exception as exc: return web.json_response({"error": str(exc)}, status=400) class SxCPPromptBuilder: @classmethod def INPUT_TYPES(cls): return { "required": { "category": (category_choices(), {"default": "auto_weighted"}), "subcategory": (subcategory_choices(), {"default": "random"}), "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}), "seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}), "clothing": (["random", "full", "minimal"], {"default": "random"}), "ethnicity": (ethnicity_choices(), {"default": "any"}), "poses": (["random", "standard", "evocative"], {"default": "random"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "backside_bias": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), "figure": (["random", "curvy", "balanced", "bombshell"], {"default": "random"}), "women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "standard_pose_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "trigger": ("STRING", {"default": "sxcpinup_coloredpencil"}), "prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}), }, "optional": { "ethnicity_list": (SXCP_ETHNICITY_LIST,), "seed_config": (SXCP_SEED_CONFIG,), "camera_config": (SXCP_CAMERA_CONFIG,), "location_config": (SXCP_LOCATION_CONFIG,), "composition_config": (SXCP_COMPOSITION_CONFIG,), "character_profile": (SXCP_CHARACTER_PROFILE,), "character_cast": (SXCP_CHARACTER_CAST,), "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), "extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}), }, } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING") RETURN_NAMES = ("prompt", "negative_prompt", "caption", "metadata_json", "category", "subcategory") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, category, subcategory, row_number, start_index, seed, clothing, ethnicity, poses, expression_enabled, expression_intensity, backside_bias, figure, women_count, men_count, minimal_clothing_ratio, standard_pose_ratio, trigger, prepend_trigger_to_prompt, seed_config="", camera_config="", location_config="", composition_config="", character_profile="", character_cast="", hardcore_position_config="", extra_positive="", extra_negative="", no_plus_women=False, no_black=False, ethnicity_list="", ): row = build_prompt( category=category, subcategory=subcategory, row_number=row_number, start_index=start_index, seed=seed, clothing=clothing, ethnicity=ethnicity_list or ethnicity, poses=poses, expression_enabled=expression_enabled, expression_intensity=expression_intensity, backside_bias=backside_bias, figure=figure, no_plus_women=no_plus_women, no_black=no_black, women_count=women_count, men_count=men_count, minimal_clothing_ratio=minimal_clothing_ratio, standard_pose_ratio=standard_pose_ratio, trigger=trigger, prepend_trigger_to_prompt=prepend_trigger_to_prompt, extra_positive=extra_positive or "", extra_negative=extra_negative or "", seed_config=seed_config or "", camera_config=camera_config or "", location_config=location_config or "", composition_config=composition_config or "", character_profile=character_profile or "", character_cast=character_cast or "", hardcore_position_config=hardcore_position_config or "", ) return ( row["prompt"], row["negative_prompt"], row["caption"], json.dumps(row, ensure_ascii=True, sort_keys=True), row.get("main_category", category), row.get("subcategory", subcategory), ) class SxCPSeedControl: SEED_AXES = ( "category", "subcategory", "content", "person", "scene", "pose", "role", "expression", "composition", ) @classmethod def INPUT_TYPES(cls): seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1} required = {} for axis in cls.SEED_AXES: required[f"{axis}_seed_mode"] = (seed_mode_choices(), {"default": "auto"}) required[f"{axis}_seed"] = ("INT", seed_spec) return { "required": required } RETURN_TYPES = (SXCP_SEED_CONFIG,) RETURN_NAMES = ("seed_config",) FUNCTION = "build" CATEGORY = "prompt_builder" @classmethod def IS_CHANGED(cls, *args, **kwargs): values = list(args) + list(kwargs.values()) if "random" in values: return random.random() return tuple(args), tuple(sorted(kwargs.items())) def build( self, category_seed_mode, category_seed, subcategory_seed_mode, subcategory_seed, content_seed_mode, content_seed, person_seed_mode, person_seed, scene_seed_mode, scene_seed, pose_seed_mode, pose_seed, role_seed_mode, role_seed, expression_seed_mode, expression_seed, composition_seed_mode, composition_seed, ): return ( build_seed_config_json( category_seed=category_seed, subcategory_seed=subcategory_seed, content_seed=content_seed, person_seed=person_seed, scene_seed=scene_seed, pose_seed=pose_seed, role_seed=role_seed, expression_seed=expression_seed, composition_seed=composition_seed, category_seed_mode=category_seed_mode, subcategory_seed_mode=subcategory_seed_mode, content_seed_mode=content_seed_mode, person_seed_mode=person_seed_mode, scene_seed_mode=scene_seed_mode, pose_seed_mode=pose_seed_mode, role_seed_mode=role_seed_mode, expression_seed_mode=expression_seed_mode, composition_seed_mode=composition_seed_mode, ), ) class SxCPGlobalSeed: @classmethod def INPUT_TYPES(cls): seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1} return { "required": { "global_seed": ("INT", seed_spec), } } RETURN_TYPES = ("INT", SXCP_SEED_CONFIG, "STRING") RETURN_NAMES = ("seed", "seed_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, global_seed): seed = max(0, min(0xFFFFFFFF, int(global_seed))) config = build_seed_lock_config_json(base_seed=seed, reroll_axis="none", reroll_seed=-1) return seed, config, f"global seed {seed}; all axes locked" class SxCPSeedLocker: @classmethod def INPUT_TYPES(cls): seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1} reroll_seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1} return { "required": { "base_seed": ("INT", seed_spec), "reroll_axis": ( [ "none", "category", "subcategory", "content", "person", "scene", "pose", "role", "expression", "composition", "content_pose", "scene_pose", ], {"default": "none"}, ), "reroll_seed": ("INT", reroll_seed_spec), } } RETURN_TYPES = (SXCP_SEED_CONFIG, "STRING") RETURN_NAMES = ("seed_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, base_seed, reroll_axis, reroll_seed): config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed) summary = f"base {base_seed}; reroll {reroll_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}" return config, summary class SxCPSDXLBucketSize: @classmethod def INPUT_TYPES(cls): return { "required": { "orientation": (["any", "portrait", "square", "landscape"], {"default": "any"}), "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}), "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "bucket_index": ("INT", {"default": 0, "min": 0, "max": len(SDXL_BUCKET_RESOLUTIONS), "step": 1}), }, "optional": { "seed_config": (SXCP_SEED_CONFIG,), }, } RETURN_TYPES = ("INT", "INT", "STRING", "STRING", "FLOAT", "FLOAT", "INT", "STRING") RETURN_NAMES = ("width", "height", "resolution", "orientation", "aspect", "megapixels", "bucket_index", "summary") FUNCTION = "build" CATEGORY = "prompt_builder/util" @staticmethod def _configured_bucket_seed(seed_config): if not seed_config: return None if isinstance(seed_config, dict): raw = seed_config else: try: raw = json.loads(str(seed_config)) except (TypeError, ValueError, json.JSONDecodeError): return None if not isinstance(raw, dict): return None for key in ("composition_seed", "content_seed", "seed", "global_seed"): try: value = int(raw.get(key)) except (TypeError, ValueError): continue if value >= 0: return value return None @classmethod def IS_CHANGED(cls, *args, **kwargs): seed_value = kwargs.get("seed") if seed_value is None and len(args) > 1: seed_value = args[1] bucket_index = kwargs.get("bucket_index") if bucket_index is None and len(args) > 3: bucket_index = args[3] seed_config = kwargs.get("seed_config", "") if not seed_config and len(args) > 4: seed_config = args[4] try: seed = int(seed_value) except (TypeError, ValueError): seed = -1 try: index = int(bucket_index) except (TypeError, ValueError): index = 0 if index <= 0 and seed < 0 and cls._configured_bucket_seed(seed_config) is None: return random.random() return tuple(args), tuple(sorted(kwargs.items())) def build(self, orientation, seed, row_number, bucket_index, seed_config=""): orientation = str(orientation or "any").strip().lower() pool = [ (index + 1, bucket) for index, bucket in enumerate(SDXL_BUCKET_RESOLUTIONS) if orientation == "any" or bucket["orientation"] == orientation ] if not pool: pool = list(enumerate(SDXL_BUCKET_RESOLUTIONS, start=1)) if int(bucket_index) > 0: pool_position = max(1, min(len(pool), int(bucket_index))) - 1 else: configured_seed = self._configured_bucket_seed(seed_config) if configured_seed is None and int(seed) < 0: rng = random.Random(random.getrandbits(64)) else: bucket_seed = configured_seed if configured_seed is not None else int(seed) rng = random.Random(f"sdxl_bucket:{bucket_seed}:{int(row_number)}:{orientation}") pool_position = rng.randrange(len(pool)) selected_index, selected = pool[pool_position] width = int(selected["width"]) height = int(selected["height"]) selected_orientation = str(selected["orientation"]) aspect = float(selected["aspect"]) mp = float(selected["mp"]) resolution = f"{width}x{height}" summary = ( f"{selected_orientation} bucket {pool_position + 1}/{len(pool)} " f"(table {selected_index}): {resolution}, aspect {aspect:.2f}, {mp:.2f} MP" ) return width, height, resolution, selected_orientation, aspect, mp, selected_index, summary class SxCPCameraControl: @classmethod def INPUT_TYPES(cls): return { "required": { "camera_mode": (camera_mode_choices(), {"default": "handheld_selfie"}), "shot_size": (camera_shot_choices(), {"default": "auto"}), "angle": (camera_angle_choices(), {"default": "auto"}), "lens": (camera_lens_choices(), {"default": "smartphone_wide"}), "distance": (camera_distance_choices(), {"default": "arm_length"}), "orientation": (camera_orientation_choices(), {"default": "vertical_story"}), "phone_visibility": (camera_phone_choices(), {"default": "phone_visible"}), "priority": (camera_priority_choices(), {"default": "locked"}), "camera_detail": (camera_detail_choices(), {"default": "compact"}), } } RETURN_TYPES = (SXCP_CAMERA_CONFIG,) RETURN_NAMES = ("camera_config",) FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, camera_mode, shot_size, angle, lens, distance, orientation, phone_visibility, priority, camera_detail, ): return ( build_camera_config_json( camera_mode=camera_mode, shot_size=shot_size, angle=angle, lens=lens, distance=distance, orientation=orientation, phone_visibility=phone_visibility, priority=priority, camera_detail=camera_detail, ), ) class SxCPCameraOrbitControl: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "camera_mode": (camera_mode_choices(), {"default": "standard"}), "horizontal_angle": ("INT", {"default": 0, "min": 0, "max": 359, "step": 1}), "vertical_angle": ("INT", {"default": 0, "min": -90, "max": 90, "step": 1}), "zoom": ("FLOAT", {"default": 5.0, "min": 0.0, "max": 10.0, "step": 0.1}), "framing": (camera_orbit_framing_choices(), {"default": "from_zoom"}), "subject_focus": (camera_orbit_focus_choices(), {"default": "auto"}), "lens": (camera_lens_choices(), {"default": "auto"}), "orientation": (camera_orientation_choices(), {"default": "auto"}), "phone_visibility": (camera_phone_choices(), {"default": "auto"}), "priority": (camera_priority_choices(), {"default": "locked"}), "camera_detail": (camera_detail_choices(), {"default": "compact"}), "include_degrees": ("BOOLEAN", {"default": True}), } } RETURN_TYPES = (SXCP_CAMERA_CONFIG, "STRING", "STRING") RETURN_NAMES = ("camera_config", "camera_prompt", "camera_info_json") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, camera_mode, horizontal_angle, vertical_angle, zoom, framing, subject_focus, lens, orientation, phone_visibility, priority, camera_detail, include_degrees, ): config = build_camera_orbit_config_json( enabled=enabled, camera_mode=camera_mode, horizontal_angle=horizontal_angle, vertical_angle=vertical_angle, zoom=zoom, framing=framing, subject_focus=subject_focus, lens=lens, orientation=orientation, phone_visibility=phone_visibility, priority=priority, camera_detail=camera_detail, include_degrees=include_degrees, ) parsed = json.loads(config) camera_prompt = parsed.get("custom_camera_prompt", "") return config, camera_prompt, json.dumps(parsed, ensure_ascii=True, sort_keys=True) class SxCPQwenCameraTranslator: @classmethod def INPUT_TYPES(cls): return { "required": { "qwen_prompt": ("STRING", {"default": ""}), "prefer_camera_info": ("BOOLEAN", {"default": True}), "camera_mode": (camera_mode_choices(), {"default": "standard"}), "subject_focus": (camera_orbit_focus_choices(), {"default": "auto"}), "lens": (camera_lens_choices(), {"default": "auto"}), "orientation": (camera_orientation_choices(), {"default": "auto"}), "phone_visibility": (camera_phone_choices(), {"default": "auto"}), "priority": (camera_priority_choices(), {"default": "locked"}), "camera_detail": (camera_detail_choices(), {"default": "compact"}), "include_degrees": ("BOOLEAN", {"default": False}), "suppress_phone_visibility": ("BOOLEAN", {"default": True}), }, "optional": { "camera_info": (ANY_TYPE,), }, } RETURN_TYPES = (SXCP_CAMERA_CONFIG, "STRING", "STRING") RETURN_NAMES = ("camera_config", "camera_prompt", "camera_info_json") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, qwen_prompt, prefer_camera_info, camera_mode, subject_focus, lens, orientation, phone_visibility, priority, camera_detail, include_degrees, suppress_phone_visibility, camera_info=None, ): config = build_qwen_camera_config_json( qwen_prompt=qwen_prompt or "", camera_info=camera_info, prefer_camera_info=prefer_camera_info, camera_mode=camera_mode, subject_focus=subject_focus, lens=lens, orientation=orientation, phone_visibility=phone_visibility, priority=priority, camera_detail=camera_detail, include_degrees=include_degrees, suppress_phone_visibility=suppress_phone_visibility, ) parsed = json.loads(config) camera_prompt = parsed.get("custom_camera_prompt", "") return config, camera_prompt, json.dumps(parsed, ensure_ascii=True, sort_keys=True) class SxCPCategoryPreset: @classmethod def INPUT_TYPES(cls): return { "required": { "preset": (category_preset_choices(), {"default": "auto_weighted"}), "subcategory": (subcategory_choices(), {"default": "random"}), } } RETURN_TYPES = (SXCP_CATEGORY_CONFIG, "STRING", "STRING") RETURN_NAMES = ("category_config", "category", "subcategory") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, preset, subcategory): config = build_category_config_json(preset=preset, subcategory=subcategory) parsed = json.loads(config) return config, parsed["category"], parsed["subcategory"] class SxCPLocationPool: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "preset": (location_pool_preset_choices(), {"default": "custom_only"}), "custom_locations": ("STRING", {"default": "", "multiline": True}), }, "optional": { "location_config": (SXCP_LOCATION_CONFIG,), }, } RETURN_TYPES = (SXCP_LOCATION_CONFIG, "STRING") RETURN_NAMES = ("location_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, enabled, combine_mode, preset, custom_locations, location_config=""): config = build_location_pool_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_locations=custom_locations or "", location_config=location_config or "", ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPCompositionPool: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "preset": (composition_pool_preset_choices(), {"default": "no_outfit_check"}), "custom_compositions": ("STRING", {"default": "", "multiline": True}), }, "optional": { "composition_config": (SXCP_COMPOSITION_CONFIG,), }, } RETURN_TYPES = (SXCP_COMPOSITION_CONFIG, "STRING") RETURN_NAMES = ("composition_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, enabled, combine_mode, preset, custom_compositions, composition_config=""): config = build_composition_pool_json( enabled=enabled, combine_mode=combine_mode, preset=preset, custom_compositions=custom_compositions or "", composition_config=composition_config or "", ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPLocationTheme: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "combine_mode": (["replace", "add"], {"default": "replace"}), "theme": (location_theme_choices(), {"default": "semi_public_affair"}), "custom_locations": ("STRING", {"default": "", "multiline": True}), "custom_compositions": ("STRING", {"default": "", "multiline": True}), }, "optional": { "location_config": (SXCP_LOCATION_CONFIG,), "composition_config": (SXCP_COMPOSITION_CONFIG,), }, } RETURN_TYPES = (SXCP_LOCATION_CONFIG, SXCP_COMPOSITION_CONFIG, "STRING") RETURN_NAMES = ("location_config", "composition_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, combine_mode, theme, custom_locations, custom_compositions, location_config="", composition_config="", ): return build_thematic_location_json( enabled=enabled, combine_mode=combine_mode, theme=theme, custom_locations=custom_locations or "", custom_compositions=custom_compositions or "", location_config=location_config or "", composition_config=composition_config or "", ) class SxCPCastControl: @classmethod def INPUT_TYPES(cls): return { "required": { "cast_mode": (cast_preset_choices(), {"default": "mixed_couple"}), "women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), } } RETURN_TYPES = (SXCP_CAST_CONFIG, "INT", "INT", "STRING") RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, cast_mode, women_count, men_count): config = build_cast_config_json(cast_mode=cast_mode, women_count=women_count, men_count=men_count) parsed = json.loads(config) summary = f"{parsed['women_count']} women, {parsed['men_count']} men" return config, parsed["women_count"], parsed["men_count"], summary class SxCPCastBias: @classmethod def INPUT_TYPES(cls): return { "required": { "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}), "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "women_weights": ("STRING", {"default": "0.60,0.25,0.10,0.05"}), "women_start_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "men_weights": ("STRING", {"default": "0.45,0.40,0.10,0.05"}), "men_start_count": ("INT", {"default": 0, "min": 0, "max": 12, "step": 1}), "empty_behavior": (["force_one_woman", "force_one_man", "allow_empty"], {"default": "force_one_woman"}), }, "optional": { "seed_config": (SXCP_SEED_CONFIG,), }, } RETURN_TYPES = (SXCP_CAST_CONFIG, "INT", "INT", "STRING") RETURN_NAMES = ("cast_config", "women_count", "men_count", "cast_summary") FUNCTION = "build" CATEGORY = "prompt_builder" @staticmethod def _configured_cast_seed(seed_config): if not seed_config: return None if isinstance(seed_config, dict): raw = seed_config else: try: raw = json.loads(str(seed_config)) except (TypeError, ValueError, json.JSONDecodeError): return None if not isinstance(raw, dict): return None for key in ("category_seed", "content_seed", "role_seed", "seed", "global_seed"): try: value = int(raw.get(key)) except (TypeError, ValueError): continue if value >= 0: return value return None @staticmethod def _weight_pairs(weights_text, start_count): pairs = [] start = max(0, min(12, int(start_count))) parts = str(weights_text or "").replace("\n", ",").split(",") for offset, raw in enumerate(parts): count = start + offset if count > 12: break try: weight = float(raw.strip()) except (TypeError, ValueError): continue if weight > 0: pairs.append((count, weight)) return pairs or [(start, 1.0)] @staticmethod def _weighted_count(rng, pairs): total = sum(weight for _count, weight in pairs) point = rng.random() * total upto = 0.0 for count, weight in pairs: upto += weight if point <= upto: return int(count) return int(pairs[-1][0]) @classmethod def IS_CHANGED(cls, *args, **kwargs): seed_value = kwargs.get("seed") if seed_value is None and args: seed_value = args[0] seed_config = kwargs.get("seed_config", "") if not seed_config and len(args) > 7: seed_config = args[7] try: seed = int(seed_value) except (TypeError, ValueError): seed = -1 if seed < 0 and cls._configured_cast_seed(seed_config) is None: return random.random() return tuple(args), tuple(sorted(kwargs.items())) def build( self, seed, row_number, women_weights, women_start_count, men_weights, men_start_count, empty_behavior, seed_config="", ): configured_seed = self._configured_cast_seed(seed_config) if configured_seed is None and int(seed) < 0: rng = random.Random(random.getrandbits(64)) else: cast_seed = configured_seed if configured_seed is not None else int(seed) rng = random.Random(f"sxcp_cast_bias:{cast_seed}:{int(row_number)}") women_pairs = self._weight_pairs(women_weights, women_start_count) men_pairs = self._weight_pairs(men_weights, men_start_count) women_count = self._weighted_count(rng, women_pairs) men_count = self._weighted_count(rng, men_pairs) if women_count + men_count == 0: if empty_behavior == "force_one_man": men_count = 1 elif empty_behavior != "allow_empty": women_count = 1 config = build_cast_config_json(cast_mode="custom_counts", women_count=women_count, men_count=men_count) parsed = json.loads(config) summary = f"weighted cast: {parsed['women_count']} women, {parsed['men_count']} men" return config, parsed["women_count"], parsed["men_count"], summary class SxCPGenerationProfile: @classmethod def INPUT_TYPES(cls): return { "required": { "profile": (generation_profile_choices(), {"default": "balanced"}), "clothing_override": (["profile_default", "random", "full", "minimal"], {"default": "profile_default"}), "poses_override": (["profile_default", "random", "standard", "evocative"], {"default": "profile_default"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity_mode": (["profile_default", "random", "fixed"], {"default": "profile_default"}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "backside_bias": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "standard_pose_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "trigger_policy": (["profile_default", "prepend_trigger", "do_not_prepend"], {"default": "profile_default"}), } } RETURN_TYPES = (SXCP_GENERATION_PROFILE, "STRING") RETURN_NAMES = ("generation_profile", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, profile, clothing_override, poses_override, expression_enabled, expression_intensity_mode, expression_intensity, backside_bias, minimal_clothing_ratio, standard_pose_ratio, trigger_policy, ): config = build_generation_profile_json( profile=profile, clothing_override=clothing_override, poses_override=poses_override, expression_enabled=expression_enabled, expression_intensity_mode=expression_intensity_mode, expression_intensity=expression_intensity, backside_bias=backside_bias, minimal_clothing_ratio=minimal_clothing_ratio, standard_pose_ratio=standard_pose_ratio, trigger_policy=trigger_policy, ) parsed = json.loads(config) if not parsed.get("expression_enabled", True): expression_summary = "expression disabled" elif float(parsed.get("expression_intensity", 0.5)) < 0: expression_summary = "expression random" else: expression_summary = f"expression {parsed['expression_intensity']}" summary = f"{parsed['profile']}: {parsed['clothing']}, {parsed['poses']}, {expression_summary}" return config, summary class SxCPAdvancedFilters: @classmethod def INPUT_TYPES(cls): return { "required": { "include_european": ("BOOLEAN", {"default": True}), "include_mediterranean_mena": ("BOOLEAN", {"default": True}), "include_latina": ("BOOLEAN", {"default": True}), "include_east_asian": ("BOOLEAN", {"default": True}), "include_southeast_asian": ("BOOLEAN", {"default": True}), "include_south_asian": ("BOOLEAN", {"default": True}), "include_black_african": ("BOOLEAN", {"default": True}), "include_indigenous": ("BOOLEAN", {"default": True}), "include_mixed": ("BOOLEAN", {"default": True}), "include_plus_size": ("BOOLEAN", {"default": True}), "figure": (["random", "curvy", "balanced", "bombshell"], {"default": "random"}), } } RETURN_TYPES = (SXCP_FILTER_CONFIG,) RETURN_NAMES = ("filter_config",) FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, include_european, include_mediterranean_mena, include_latina, include_east_asian, include_southeast_asian, include_south_asian, include_black_african, include_indigenous, include_mixed, include_plus_size, figure, ): return ( build_filter_config_json( figure=figure, include_european=include_european, include_mediterranean_mena=include_mediterranean_mena, include_latina=include_latina, include_east_asian=include_east_asian, include_southeast_asian=include_southeast_asian, include_south_asian=include_south_asian, include_black_african=include_black_african, include_indigenous=include_indigenous, include_mixed=include_mixed, include_plus_size=include_plus_size, ), ) class SxCPEthnicityList: @classmethod def INPUT_TYPES(cls): return { "required": { "include_european": ("BOOLEAN", {"default": False}), "include_mediterranean_mena": ("BOOLEAN", {"default": False}), "include_latina": ("BOOLEAN", {"default": False}), "include_east_asian": ("BOOLEAN", {"default": False}), "include_southeast_asian": ("BOOLEAN", {"default": False}), "include_south_asian": ("BOOLEAN", {"default": False}), "include_black_african": ("BOOLEAN", {"default": False}), "include_indigenous": ("BOOLEAN", {"default": False}), "include_mixed": ("BOOLEAN", {"default": False}), "include_asian": ("BOOLEAN", {"default": False}), "include_white_asian": ("BOOLEAN", {"default": False}), "include_western_european": ("BOOLEAN", {"default": False}), "include_french_european": ("BOOLEAN", {"default": False}), "include_germanic_european": ("BOOLEAN", {"default": False}), "include_nordic_european": ("BOOLEAN", {"default": False}), "include_celtic_european": ("BOOLEAN", {"default": False}), "include_slavic_european": ("BOOLEAN", {"default": False}), "include_baltic_european": ("BOOLEAN", {"default": False}), "include_alpine_european": ("BOOLEAN", {"default": False}), "include_balkan_european": ("BOOLEAN", {"default": False}), "include_greek_mediterranean": ("BOOLEAN", {"default": False}), "include_italian_mediterranean": ("BOOLEAN", {"default": False}), "include_iberian_mediterranean": ("BOOLEAN", {"default": False}), "strict_excludes": ("BOOLEAN", {"default": True}), } } RETURN_TYPES = (SXCP_ETHNICITY_LIST, SXCP_FILTER_CONFIG, "STRING") RETURN_NAMES = ("ethnicity_list", "filter_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, include_european, include_mediterranean_mena, include_latina, include_east_asian, include_southeast_asian, include_south_asian, include_black_african, include_indigenous, include_mixed, include_asian, include_white_asian, include_western_european, include_french_european, include_germanic_european, include_nordic_european, include_celtic_european, include_slavic_european, include_baltic_european, include_alpine_european, include_balkan_european, include_greek_mediterranean, include_italian_mediterranean, include_iberian_mediterranean, strict_excludes, ): result = build_ethnicity_list_json( include_european=include_european, include_mediterranean_mena=include_mediterranean_mena, include_latina=include_latina, include_east_asian=include_east_asian, include_southeast_asian=include_southeast_asian, include_south_asian=include_south_asian, include_black_african=include_black_african, include_indigenous=include_indigenous, include_mixed=include_mixed, include_asian=include_asian, include_white_asian=include_white_asian, include_western_european=include_western_european, include_french_european=include_french_european, include_germanic_european=include_germanic_european, include_nordic_european=include_nordic_european, include_celtic_european=include_celtic_european, include_slavic_european=include_slavic_european, include_baltic_european=include_baltic_european, include_alpine_european=include_alpine_european, include_balkan_european=include_balkan_european, include_greek_mediterranean=include_greek_mediterranean, include_italian_mediterranean=include_italian_mediterranean, include_iberian_mediterranean=include_iberian_mediterranean, strict_excludes=strict_excludes, ) return result["ethnicity"], result["filter_config"], result["summary"] class _SxCPHairAxisNode: AXIS = "color" PREFIX = "include" @classmethod def _choices(cls): if cls.AXIS == "color": return [choice for choice in character_hair_color_choices() if choice != "random"] if cls.AXIS == "length": return [choice for choice in character_hair_length_choices() if choice != "random"] return [choice for choice in character_hair_style_choices() if choice != "random"] @classmethod def INPUT_TYPES(cls): required = { "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), } for choice in cls._choices(): required[f"{cls.PREFIX}_{choice}"] = ("BOOLEAN", {"default": False}) return { "required": required, "optional": { "hair_config": (SXCP_HAIR_CONFIG,), }, } RETURN_TYPES = (SXCP_HAIR_CONFIG, "STRING") RETURN_NAMES = ("hair_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, combine_mode="replace_axis", hair_config="", **kwargs): selected = [ choice for choice in self._choices() if bool(kwargs.get(f"{self.PREFIX}_{choice}", False)) ] config = build_hair_config_json( hair_config=hair_config or "", axis=self.AXIS, selected_values=selected, combine_mode=combine_mode, ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPHairColor(_SxCPHairAxisNode): AXIS = "color" class SxCPHairLength(_SxCPHairAxisNode): AXIS = "length" class SxCPHairStyle(_SxCPHairAxisNode): AXIS = "style" def _choice_input_key(prefix, choice): key = "".join(char if char.isalnum() else "_" for char in str(choice).lower()).strip("_") while "__" in key: key = key.replace("__", "_") return f"{prefix}_{key}" class SxCPCharacterAgeRange: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), "min_age": ("INT", {"default": 21, "min": 21, "max": 85, "step": 1}), "max_age": ("INT", {"default": 35, "min": 21, "max": 85, "step": 1}), }, "optional": { "characteristics": (SXCP_CHARACTERISTICS,), }, } RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") RETURN_NAMES = ("characteristics", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, combine_mode, min_age, max_age, characteristics=""): start = max(21, min(85, int(min_age))) end = max(21, min(85, int(max_age))) if end < start: start, end = end, start ages = [f"{age}-year-old adult" for age in range(start, end + 1)] config = build_characteristics_config_json( characteristics=characteristics or "", axis="ages", selected_values=ages, combine_mode=combine_mode, ) return config, json.loads(config).get("summary", "") class _SxCPBodyPoolNode: SUBJECT = "character" @classmethod def _choices(cls): if cls.SUBJECT == "woman": return [choice for choice in character_woman_body_choices() if choice not in ("random", "manual")] if cls.SUBJECT == "man": return [choice for choice in character_man_body_choices() if choice not in ("random", "manual")] return [choice for choice in character_body_choices() if choice not in ("random", "manual")] @classmethod def INPUT_TYPES(cls): required = { "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), } for choice in cls._choices(): required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) return { "required": required, "optional": { "characteristics": (SXCP_CHARACTERISTICS,), }, } RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") RETURN_NAMES = ("characteristics", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, combine_mode="replace_axis", characteristics="", **kwargs): selected = [ choice for choice in self._choices() if bool(kwargs.get(_choice_input_key("include", choice), False)) ] config = build_characteristics_config_json( characteristics=characteristics or "", axis="bodies", selected_values=selected, combine_mode=combine_mode, ) return config, json.loads(config).get("summary", "") class SxCPCharacterBodyPool(_SxCPBodyPoolNode): SUBJECT = "character" class SxCPWomanBodyPool(_SxCPBodyPoolNode): SUBJECT = "woman" class SxCPManBodyPool(_SxCPBodyPoolNode): SUBJECT = "man" class SxCPEyeColorPool: @classmethod def INPUT_TYPES(cls): required = { "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), } for choice in character_eye_color_choices(): if choice != "random": required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) return { "required": required, "optional": { "characteristics": (SXCP_CHARACTERISTICS,), }, } RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") RETURN_NAMES = ("characteristics", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, combine_mode="replace_axis", characteristics="", **kwargs): selected = [ choice for choice in character_eye_color_choices() if choice != "random" and bool(kwargs.get(_choice_input_key("include", choice), False)) ] config = build_characteristics_config_json( characteristics=characteristics or "", axis="eyes", selected_values=selected, combine_mode=combine_mode, ) return config, json.loads(config).get("summary", "") class SxCPCharacterClothing: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (["replace_axis", "add_to_axis"], {"default": "replace_axis"}), "softcore_source": (character_softcore_outfit_source_choices(), {"default": "no_change"}), "hardcore_state": (character_hardcore_clothing_state_choices(), {"default": "no_change"}), "custom_softcore_outfits": ("STRING", {"default": "", "multiline": True}), "custom_hardcore_clothing": ("STRING", {"default": "", "multiline": True}), }, "optional": { "characteristics": (SXCP_CHARACTERISTICS,), }, } RETURN_TYPES = (SXCP_CHARACTERISTICS, "STRING") RETURN_NAMES = ("characteristics", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, combine_mode, softcore_source, hardcore_state, custom_softcore_outfits, custom_hardcore_clothing, characteristics="", ): config = characteristics or "" if softcore_source != "no_change": config = build_characteristics_config_json( characteristics=config, axis="softcore_outfits", selected_values=character_softcore_outfit_values(softcore_source, custom_softcore_outfits), combine_mode=combine_mode, ) if hardcore_state != "no_change": config = build_characteristics_config_json( characteristics=config, axis="hardcore_clothing", selected_values=character_hardcore_clothing_values(hardcore_state, custom_hardcore_clothing), combine_mode=combine_mode, ) if not config: config = build_characteristics_config_json(axis="", selected_values=[]) return config, json.loads(config).get("summary", "") class SxCPHardcorePositionPool: @classmethod def INPUT_TYPES(cls): required = { "combine_mode": (["replace", "add"], {"default": "replace"}), "family": (hardcore_position_family_choices(), {"default": "any"}), } for choice in hardcore_position_key_choices(): required[_choice_input_key("include", choice)] = ("BOOLEAN", {"default": False}) return { "required": required, "optional": { "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), }, } RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING") RETURN_NAMES = ("hardcore_position_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build(self, combine_mode="replace", family="any", hardcore_position_config="", **kwargs): selected = [ choice for choice in hardcore_position_key_choices() if bool(kwargs.get(_choice_input_key("include", choice), False)) ] config = build_hardcore_position_pool_json( hardcore_position_config=hardcore_position_config or "", combine_mode=combine_mode, family=family, selected_positions=selected, ) return config, json.loads(config).get("summary", "") class SxCPHardcoreActionFilter: @classmethod def INPUT_TYPES(cls): return { "required": { "focus": (hardcore_position_focus_choices(), {"default": "keep_pool"}), "allow_toys": ("BOOLEAN", {"default": False}), "allow_double": ("BOOLEAN", {"default": False}), "allow_penetration": ("BOOLEAN", {"default": True}), "allow_foreplay": ("BOOLEAN", {"default": True}), "allow_oral": ("BOOLEAN", {"default": True}), "allow_outercourse": ("BOOLEAN", {"default": True}), "allow_anal": ("BOOLEAN", {"default": True}), "allow_climax": ("BOOLEAN", {"default": True}), }, "optional": { "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), }, } RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING") RETURN_NAMES = ("hardcore_position_config", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, focus, allow_toys, allow_double, allow_penetration, allow_foreplay, allow_oral, allow_outercourse, allow_anal, allow_climax, hardcore_position_config="", ): config = build_hardcore_action_filter_json( hardcore_position_config=hardcore_position_config or "", focus=focus, allow_toys=allow_toys, allow_double=allow_double, allow_penetration=allow_penetration, allow_foreplay=allow_foreplay, allow_oral=allow_oral, allow_outercourse=allow_outercourse, allow_anal=allow_anal, allow_climax=allow_climax, ) return config, json.loads(config).get("summary", "") class SxCPCharacterManualDetails: @classmethod def INPUT_TYPES(cls): return { "required": { "combine_mode": (["merge_nonempty", "replace_all"], {"default": "merge_nonempty"}), "manual_age": ("STRING", {"default": ""}), "manual_body": ("STRING", {"default": ""}), "body_phrase": ("STRING", {"default": ""}), "skin": ("STRING", {"default": ""}), "hair": ("STRING", {"default": ""}), "eyes": ("STRING", {"default": ""}), "softcore_outfit": ("STRING", {"default": ""}), "hardcore_clothing": ("STRING", {"default": ""}), }, "optional": { "manual": (SXCP_CHARACTER_MANUAL,), }, } RETURN_TYPES = (SXCP_CHARACTER_MANUAL, "STRING") RETURN_NAMES = ("manual", "summary") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, combine_mode, manual_age, manual_body, body_phrase, skin, hair, eyes, softcore_outfit, hardcore_clothing, manual="", ): config = build_character_manual_config_json( manual=manual or "", combine_mode=combine_mode, manual_age=manual_age, manual_body=manual_body, body_phrase=body_phrase, skin=skin, hair=hair, eyes=eyes, softcore_outfit=softcore_outfit, hardcore_clothing=hardcore_clothing, ) parsed = json.loads(config) return config, parsed.get("summary", "") class SxCPPromptBuilderFromConfigs: @classmethod def INPUT_TYPES(cls): return { "required": { "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}), "seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}), }, "optional": { "category_config": (SXCP_CATEGORY_CONFIG,), "cast_config": (SXCP_CAST_CONFIG,), "generation_profile": (SXCP_GENERATION_PROFILE,), "filter_config": (SXCP_FILTER_CONFIG,), "ethnicity_list": (SXCP_ETHNICITY_LIST,), "seed_config": (SXCP_SEED_CONFIG,), "camera_config": (SXCP_CAMERA_CONFIG,), "location_config": (SXCP_LOCATION_CONFIG,), "composition_config": (SXCP_COMPOSITION_CONFIG,), "character_profile": (SXCP_CHARACTER_PROFILE,), "character_cast": (SXCP_CHARACTER_CAST,), "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), "extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}), }, } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING") RETURN_NAMES = ("prompt", "negative_prompt", "caption", "metadata_json", "category", "subcategory") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, row_number, start_index, seed, category_config="", cast_config="", generation_profile="", filter_config="", ethnicity_list="", seed_config="", camera_config="", location_config="", composition_config="", character_profile="", character_cast="", hardcore_position_config="", extra_positive="", extra_negative="", ): row = build_prompt_from_configs( row_number=row_number, start_index=start_index, seed=seed, category_config=category_config or "", cast_config=cast_config or "", generation_profile=generation_profile or "", filter_config=ethnicity_list or filter_config or "", seed_config=seed_config or "", camera_config=camera_config or "", location_config=location_config or "", composition_config=composition_config or "", character_profile=character_profile or "", character_cast=character_cast or "", hardcore_position_config=hardcore_position_config or "", extra_positive=extra_positive or "", extra_negative=extra_negative or "", ) return ( row["prompt"], row["negative_prompt"], row["caption"], json.dumps(row, ensure_ascii=True, sort_keys=True), row.get("main_category", ""), row.get("subcategory", ""), ) class SxCPCharacterSlot: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "subject_type": (["woman", "man"], {"default": "woman"}), "label": (character_label_choices(), {"default": "auto_chain"}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}), "figure": (character_figure_choices(), {"default": "random"}), "body": ([choice for choice in character_body_choices() if choice != "manual"], {"default": "random"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "presence_mode": (character_presence_choices(), {"default": "visible"}), "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), }, "optional": { "manual": (SXCP_CHARACTER_MANUAL,), "ethnicity_list": (SXCP_ETHNICITY_LIST,), "characteristics": (SXCP_CHARACTERISTICS,), "hair_config": (SXCP_HAIR_CONFIG,), "character_cast": (SXCP_CHARACTER_CAST,), }, } RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, subject_type, label, slot_seed, age, ethnicity, figure, body, descriptor_detail="auto", expression_enabled=True, expression_intensity=-1.0, presence_mode="visible", softcore_expression_intensity=-1.0, hardcore_expression_intensity=-1.0, character_cast="", ethnicity_list="", characteristics="", hair_config="", manual="", ): result = build_character_slot_json( subject_type=subject_type, label=label, slot_seed=slot_seed, age=age, manual=manual, ethnicity=ethnicity_list or ethnicity, figure=figure, body=body, manual_body="", body_phrase="", skin="", hair="", characteristics=characteristics, hair_config=hair_config, eyes="", descriptor_detail=descriptor_detail, expression_enabled=expression_enabled, expression_intensity=expression_intensity, presence_mode=presence_mode, softcore_expression_intensity=softcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity, softcore_outfit="", hardcore_clothing="", enabled=enabled, character_cast=character_cast or "", ) return result["character_cast"], result["character_slot"], result["summary"], result["status"] class SxCPWomanSlot: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "label": (character_label_choices(), {"default": "auto_chain"}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}), "figure_bias": (character_figure_choices(), {"default": "random"}), "body": ([choice for choice in character_woman_body_choices() if choice != "manual"], {"default": "random"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "auto"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), }, "optional": { "manual": (SXCP_CHARACTER_MANUAL,), "ethnicity_list": (SXCP_ETHNICITY_LIST,), "characteristics": (SXCP_CHARACTERISTICS,), "hair_config": (SXCP_HAIR_CONFIG,), "character_cast": (SXCP_CHARACTER_CAST,), }, } RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, label, slot_seed, age, ethnicity, figure_bias, body, descriptor_detail="auto", expression_enabled=True, expression_intensity=-1.0, softcore_expression_intensity=-1.0, hardcore_expression_intensity=-1.0, character_cast="", ethnicity_list="", characteristics="", hair_config="", manual="", ): result = build_character_slot_json( subject_type="woman", label=label, slot_seed=slot_seed, age=age, manual=manual, ethnicity=ethnicity_list or ethnicity, figure=figure_bias, body=body, manual_body="", body_phrase="", skin="", hair="", characteristics=characteristics, hair_config=hair_config, eyes="", descriptor_detail=descriptor_detail, expression_enabled=expression_enabled, expression_intensity=expression_intensity, softcore_expression_intensity=softcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity, softcore_outfit="", hardcore_clothing="", enabled=enabled, character_cast=character_cast or "", ) return result["character_cast"], result["character_slot"], result["summary"], result["status"] class SxCPManSlot: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "label": (character_label_choices(), {"default": "auto_chain"}), "slot_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF}), "age": ([choice for choice in character_age_choices() if choice != "manual"], {"default": "random"}), "ethnicity": (character_ethnicity_choices(), {"default": "random"}), "body": ([choice for choice in character_man_body_choices() if choice != "manual"], {"default": "random"}), "descriptor_detail": (character_descriptor_detail_choices(), {"default": "compact"}), "expression_enabled": ("BOOLEAN", {"default": True}), "expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "presence_mode": (character_presence_choices(), {"default": "visible"}), "softcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), }, "optional": { "manual": (SXCP_CHARACTER_MANUAL,), "ethnicity_list": (SXCP_ETHNICITY_LIST,), "characteristics": (SXCP_CHARACTERISTICS,), "hair_config": (SXCP_HAIR_CONFIG,), "character_cast": (SXCP_CHARACTER_CAST,), }, } RETURN_TYPES = (SXCP_CHARACTER_CAST, SXCP_CHARACTER_SLOT, "STRING", "STRING") RETURN_NAMES = ("character_cast", "character_slot", "summary", "status") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, label, slot_seed, age, ethnicity, body, descriptor_detail="compact", expression_enabled=True, expression_intensity=-1.0, presence_mode="visible", softcore_expression_intensity=-1.0, hardcore_expression_intensity=-1.0, character_cast="", ethnicity_list="", characteristics="", hair_config="", manual="", ): result = build_character_slot_json( subject_type="man", label=label, slot_seed=slot_seed, age=age, manual=manual, ethnicity=ethnicity_list or ethnicity, figure="random", body=body, manual_body="", body_phrase="", skin="", hair="", characteristics=characteristics, hair_config=hair_config, eyes="", descriptor_detail=descriptor_detail, expression_enabled=expression_enabled, expression_intensity=expression_intensity, presence_mode=presence_mode, softcore_expression_intensity=softcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity, softcore_outfit="", hardcore_clothing="", enabled=enabled, character_cast=character_cast or "", ) return result["character_cast"], result["character_slot"], result["summary"], result["status"] class SxCPCharacterProfileSave: OUTPUT_NODE = True @classmethod def INPUT_TYPES(cls): return { "required": { "profile_name": ("STRING", {"default": "saved_character"}), "source": (["metadata_json", "character_slot", "manual"], {"default": "metadata_json"}), "subject_type": (["woman", "man"], {"default": "woman"}), "age": ("STRING", {"default": ""}), "body": ("STRING", {"default": ""}), "body_phrase": ("STRING", {"default": ""}), "skin": ("STRING", {"default": ""}), "hair": ("STRING", {"default": ""}), "eyes": ("STRING", {"default": ""}), "figure": ("STRING", {"default": ""}), "save_now": ("BOOLEAN", {"default": False}), }, "optional": { "metadata_json": ("STRING", {"default": "", "multiline": True}), "character_slot": (SXCP_CHARACTER_SLOT,), }, } RETURN_TYPES = (SXCP_CHARACTER_PROFILE, "STRING", "STRING", "STRING", "STRING", SXCP_CHARACTER_PROFILE) RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "profile_json") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, profile_name, source, subject_type, age, body, body_phrase, skin, hair, eyes, figure, save_now, metadata_json="", character_slot="", ): profile = build_character_profile_json( profile_name=profile_name, source=source, metadata_json=metadata_json or "", character_slot=character_slot or "", subject_type=subject_type, age=age, body=body, body_phrase=body_phrase, skin=skin, hair=hair, eyes=eyes, figure=figure, save_now=save_now, ) result = ( profile["profile_json"], profile["descriptor"], profile["profile_name"], profile["saved_path"], profile["status"], profile["profile_json"], ) return { "ui": { "profile_json": [profile["profile_json"]], "descriptor": [profile["descriptor"]], "profile_name": [profile["profile_name"]], "saved_path": [profile["saved_path"]], "status": [profile["status"]], }, "result": result, } class SxCPCharacterProfileLoad: @classmethod def INPUT_TYPES(cls): return { "required": { "enabled": ("BOOLEAN", {"default": True}), "profile_name": (character_profile_choices(), {"default": "manual"}), "rename_to": ("STRING", {"default": ""}), "delete_now": ("BOOLEAN", {"default": False}), "rename_now": ("BOOLEAN", {"default": False}), }, "optional": { "manual_profile_name": ("STRING", {"default": ""}), "fallback_profile_json": (SXCP_CHARACTER_PROFILE,), "override_subject_type": (["keep_profile", "woman", "man"], {"default": "keep_profile"}), "override_age": ("STRING", {"default": ""}), "override_body": ("STRING", {"default": ""}), "override_body_phrase": ("STRING", {"default": ""}), "override_skin": ("STRING", {"default": ""}), "override_hair": ("STRING", {"default": ""}), "override_eyes": ("STRING", {"default": ""}), "override_figure": ("STRING", {"default": ""}), "override_descriptor_detail": (["keep_profile"] + character_descriptor_detail_choices(), {"default": "keep_profile"}), }, } RETURN_TYPES = (SXCP_CHARACTER_PROFILE, "STRING", "STRING", "STRING", "STRING", SXCP_CHARACTER_PROFILE) RETURN_NAMES = ("character_profile", "descriptor", "profile_name", "saved_path", "status", "profile_json") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, enabled, profile_name, rename_to, delete_now, rename_now, manual_profile_name="", fallback_profile_json="", override_subject_type="keep_profile", override_age="", override_body="", override_body_phrase="", override_skin="", override_hair="", override_eyes="", override_figure="", override_descriptor_detail="keep_profile", ): chosen_name = manual_profile_name.strip() if profile_name == "manual" and manual_profile_name.strip() else profile_name profile = load_character_profile_json( profile_name=chosen_name, fallback_profile_json=fallback_profile_json or "", enabled=enabled, delete_now=delete_now, rename_now=rename_now, rename_to=rename_to, override_subject_type=override_subject_type, override_age=override_age, override_body=override_body, override_body_phrase=override_body_phrase, override_skin=override_skin, override_hair=override_hair, override_eyes=override_eyes, override_figure=override_figure, override_descriptor_detail=override_descriptor_detail, ) return ( profile["profile_json"], profile["descriptor"], profile["profile_name"], profile["saved_path"], profile["status"], profile["profile_json"], ) class SxCPCaptionNaturalizer: @classmethod def INPUT_TYPES(cls): return { "required": { "source_text": ("STRING", {"default": "", "multiline": True}), "input_hint": (["auto", "metadata_json", "caption_or_prompt"], {"default": "auto"}), "detail_level": (["balanced", "concise", "dense"], {"default": "balanced"}), "style_policy": (["drop_style_tail", "keep_style_terms"], {"default": "drop_style_tail"}), "trigger": ("STRING", {"default": "sxcppnl7"}), "include_trigger": ("BOOLEAN", {"default": True}), }, "optional": { "source_text_input": ("STRING", {"forceInput": True}), "metadata_json": ("STRING", {"forceInput": True}), }, } RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("natural_caption", "method") FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, source_text, input_hint, detail_level, style_policy, trigger, include_trigger, source_text_input="", metadata_json="", ): active_source_text = source_text_input or source_text or "" return naturalize_caption( source_text=active_source_text, metadata_json=metadata_json or "", input_hint=input_hint, trigger=trigger, include_trigger=include_trigger, detail_level=detail_level, style_policy=style_policy, ) class SxCPKrea2Formatter: @classmethod def INPUT_TYPES(cls): return { "required": { "source_text": ("STRING", {"default": "", "multiline": True}), "input_hint": (["auto", "metadata_json", "prompt"], {"default": "auto"}), "target": (["auto", "single", "softcore", "hardcore"], {"default": "auto"}), "detail_level": (["balanced", "concise", "dense"], {"default": "balanced"}), "style_mode": (["preserve", "photographic", "minimal"], {"default": "preserve"}), "preserve_trigger": ("BOOLEAN", {"default": False}), }, "optional": { "metadata_json": ("STRING", {"default": "", "multiline": True}), "negative_prompt": ("STRING", {"default": "", "multiline": True}), "extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}), }, } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") RETURN_NAMES = ( "krea_prompt", "negative_prompt", "krea_softcore_prompt", "krea_hardcore_prompt", "softcore_negative_prompt", "hardcore_negative_prompt", "method", ) FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, source_text, input_hint, target, detail_level, style_mode, preserve_trigger, metadata_json="", negative_prompt="", extra_positive="", extra_negative="", ): row = format_krea2_prompt( source_text=source_text or "", metadata_json=metadata_json or "", negative_prompt=negative_prompt or "", input_hint=input_hint, target=target, detail_level=detail_level, style_mode=style_mode, preserve_trigger=preserve_trigger, extra_positive=extra_positive or "", extra_negative=extra_negative or "", ) return ( row["krea_prompt"], row["negative_prompt"], row["krea_softcore_prompt"], row["krea_hardcore_prompt"], row["softcore_negative_prompt"], row["hardcore_negative_prompt"], row["method"], ) class SxCPSDXLFormatter: @classmethod def INPUT_TYPES(cls): return { "required": { "source_text": ("STRING", {"default": "", "multiline": True}), "input_hint": (["auto", "metadata_json", "prompt"], {"default": "auto"}), "target": (["auto", "single", "softcore", "hardcore"], {"default": "auto"}), "style_preset": (sdxl_style_preset_choices(), {"default": "flat_vector_pony"}), "quality_preset": (sdxl_quality_preset_choices(), {"default": "pony_high"}), "trigger": ("STRING", {"default": "mythp0rt", "multiline": False}), "prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}), "preserve_trigger": ("BOOLEAN", {"default": False}), "nude_weight": ("FLOAT", {"default": 1.29, "min": 0.1, "max": 3.0, "step": 0.01}), }, "optional": { "source_text_input": ("STRING", {"forceInput": True}), "metadata_json": ("STRING", {"forceInput": True}), "negative_prompt": ("STRING", {"forceInput": True}), "custom_style": ("STRING", {"default": "", "multiline": True}), "custom_quality": ("STRING", {"default": "", "multiline": True}), "extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}), }, } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") RETURN_NAMES = ( "sdxl_prompt", "negative_prompt", "sdxl_softcore_prompt", "sdxl_hardcore_prompt", "softcore_negative_prompt", "hardcore_negative_prompt", "method", ) FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, source_text, input_hint, target, style_preset, quality_preset, trigger, prepend_trigger_to_prompt, preserve_trigger, nude_weight, source_text_input="", metadata_json="", negative_prompt="", custom_style="", custom_quality="", extra_positive="", extra_negative="", ): active_source_text = source_text_input or source_text or "" row = format_sdxl_prompt( source_text=active_source_text, metadata_json=metadata_json or "", negative_prompt=negative_prompt or "", input_hint=input_hint, target=target, style_preset=style_preset, quality_preset=quality_preset, trigger=trigger, prepend_trigger=prepend_trigger_to_prompt, preserve_trigger=preserve_trigger, nude_weight=nude_weight, custom_style=custom_style or "", custom_quality=custom_quality or "", extra_positive=extra_positive or "", extra_negative=extra_negative or "", ) return ( row["sdxl_prompt"], row["negative_prompt"], row["sdxl_softcore_prompt"], row["sdxl_hardcore_prompt"], row["softcore_negative_prompt"], row["hardcore_negative_prompt"], row["method"], ) class SxCPInstaOFOptions: @classmethod def INPUT_TYPES(cls): return { "required": { "softcore_cast": (["solo", "same_as_hardcore"], {"default": "solo"}), "hardcore_cast": (["use_counts", "couple", "threesome", "group"], {"default": "use_counts"}), "hardcore_women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "hardcore_men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), "softcore_level": (["social_tease", "lingerie_tease", "implied_nude", "explicit_tease", "explicit_nude"], {"default": "lingerie_tease"}), "hardcore_level": (["explicit", "hardcore"], {"default": "hardcore"}), "softcore_expression_enabled": ("BOOLEAN", {"default": True}), "hardcore_expression_enabled": ("BOOLEAN", {"default": True}), "softcore_expression_intensity": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01}), "hardcore_expression_intensity": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}), "platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}), "continuity": (["same_creator_same_room", "same_creator_new_scene"], {"default": "same_creator_same_room"}), "hardcore_clothing_continuity": (["none", "same_outfit", "partially_removed", "implied_nude", "explicit_nude"], {"default": "partially_removed"}), "softcore_camera_mode": (["from_camera_config"] + camera_mode_choices(), {"default": "handheld_selfie"}), "hardcore_camera_mode": (["from_camera_config", "same_as_softcore"] + camera_mode_choices(), {"default": "from_camera_config"}), "camera_detail": (["from_camera_config"] + camera_detail_choices(), {"default": "from_camera_config"}), "hardcore_detail_density": (hardcore_detail_density_choices(), {"default": "balanced"}), } } RETURN_TYPES = (SXCP_INSTA_OF_OPTIONS,) RETURN_NAMES = ("options_json",) FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, softcore_cast, hardcore_cast, hardcore_women_count, hardcore_men_count, softcore_level, hardcore_level, softcore_expression_enabled, hardcore_expression_enabled, softcore_expression_intensity, hardcore_expression_intensity, platform_style, continuity, hardcore_clothing_continuity, softcore_camera_mode, hardcore_camera_mode, camera_detail, hardcore_detail_density, ): return ( build_insta_of_options_json( softcore_cast=softcore_cast, hardcore_cast=hardcore_cast, hardcore_women_count=hardcore_women_count, hardcore_men_count=hardcore_men_count, softcore_level=softcore_level, hardcore_level=hardcore_level, softcore_expression_enabled=softcore_expression_enabled, hardcore_expression_enabled=hardcore_expression_enabled, softcore_expression_intensity=softcore_expression_intensity, hardcore_expression_intensity=hardcore_expression_intensity, platform_style=platform_style, continuity=continuity, hardcore_clothing_continuity=hardcore_clothing_continuity, softcore_camera_mode=softcore_camera_mode, hardcore_camera_mode=hardcore_camera_mode, camera_detail=camera_detail, hardcore_detail_density=hardcore_detail_density, ), ) class SxCPInstaOFPromptPair: @classmethod def INPUT_TYPES(cls): return { "required": { "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), "start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}), "seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}), "ethnicity": (ethnicity_choices(), {"default": "any"}), "figure": (["random", "curvy", "balanced", "bombshell"], {"default": "random"}), "trigger": ("STRING", {"default": "sxcpinup_coloredpencil"}), "prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}), }, "optional": { "seed_config": (SXCP_SEED_CONFIG,), "options_json": (SXCP_INSTA_OF_OPTIONS,), "filter_config": (SXCP_FILTER_CONFIG,), "ethnicity_list": (SXCP_ETHNICITY_LIST,), "camera_config": (SXCP_CAMERA_CONFIG,), "softcore_camera_config": (SXCP_CAMERA_CONFIG,), "hardcore_camera_config": (SXCP_CAMERA_CONFIG,), "location_config": (SXCP_LOCATION_CONFIG,), "composition_config": (SXCP_COMPOSITION_CONFIG,), "character_profile": (SXCP_CHARACTER_PROFILE,), "character_cast": (SXCP_CHARACTER_CAST,), "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), "extra_positive": ("STRING", {"default": "", "multiline": True}), "extra_negative": ("STRING", {"default": "", "multiline": True}), }, } RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") RETURN_NAMES = ( "softcore_prompt", "hardcore_prompt", "softcore_negative_prompt", "hardcore_negative_prompt", "softcore_caption", "hardcore_caption", "shared_descriptor", "metadata_json", ) FUNCTION = "build" CATEGORY = "prompt_builder" def build( self, row_number, start_index, seed, ethnicity, figure, trigger, prepend_trigger_to_prompt, seed_config="", options_json="", filter_config="", ethnicity_list="", camera_config="", softcore_camera_config="", hardcore_camera_config="", location_config="", composition_config="", character_profile="", character_cast="", hardcore_position_config="", extra_positive="", extra_negative="", no_plus_women=False, no_black=False, ): row = build_insta_of_pair( row_number=row_number, start_index=start_index, seed=seed, ethnicity=ethnicity, figure=figure, no_plus_women=no_plus_women, no_black=no_black, trigger=trigger, prepend_trigger_to_prompt=prepend_trigger_to_prompt, seed_config=seed_config or "", options_json=options_json or "", filter_config=ethnicity_list or filter_config or "", camera_config=camera_config or "", softcore_camera_config=softcore_camera_config or "", hardcore_camera_config=hardcore_camera_config or "", location_config=location_config or "", composition_config=composition_config or "", character_profile=character_profile or "", character_cast=character_cast or "", hardcore_position_config=hardcore_position_config or "", extra_positive=extra_positive or "", extra_negative=extra_negative or "", ) return ( row["softcore_prompt"], row["hardcore_prompt"], row["softcore_negative_prompt"], row["hardcore_negative_prompt"], row["softcore_caption"], row["hardcore_caption"], row["shared_descriptor"], json.dumps(row, ensure_ascii=True, sort_keys=True), ) NODE_CLASS_MAPPINGS = { "SxCPPromptBuilder": SxCPPromptBuilder, "SxCPGlobalSeed": SxCPGlobalSeed, "SxCPSeedControl": SxCPSeedControl, "SxCPSeedLocker": SxCPSeedLocker, "SxCPSDXLBucketSize": SxCPSDXLBucketSize, "SxCPCameraControl": SxCPCameraControl, "SxCPCameraOrbitControl": SxCPCameraOrbitControl, "SxCPQwenCameraTranslator": SxCPQwenCameraTranslator, "SxCPCategoryPreset": SxCPCategoryPreset, "SxCPLocationPool": SxCPLocationPool, "SxCPCompositionPool": SxCPCompositionPool, "SxCPLocationTheme": SxCPLocationTheme, "SxCPCastControl": SxCPCastControl, "SxCPCastBias": SxCPCastBias, "SxCPGenerationProfile": SxCPGenerationProfile, "SxCPEthnicityList": SxCPEthnicityList, "SxCPHairLength": SxCPHairLength, "SxCPHairColor": SxCPHairColor, "SxCPHairStyle": SxCPHairStyle, "SxCPCharacterAgeRange": SxCPCharacterAgeRange, "SxCPCharacterBodyPool": SxCPCharacterBodyPool, "SxCPWomanBodyPool": SxCPWomanBodyPool, "SxCPManBodyPool": SxCPManBodyPool, "SxCPEyeColorPool": SxCPEyeColorPool, "SxCPCharacterClothing": SxCPCharacterClothing, "SxCPHardcorePositionPool": SxCPHardcorePositionPool, "SxCPHardcoreActionFilter": SxCPHardcoreActionFilter, "SxCPCharacterManualDetails": SxCPCharacterManualDetails, "SxCPAdvancedFilters": SxCPAdvancedFilters, "SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs, "SxCPWomanSlot": SxCPWomanSlot, "SxCPManSlot": SxCPManSlot, "SxCPCharacterSlot": SxCPCharacterSlot, "SxCPCharacterProfileSave": SxCPCharacterProfileSave, "SxCPCharacterProfileLoad": SxCPCharacterProfileLoad, "SxCPCaptionNaturalizer": SxCPCaptionNaturalizer, "SxCPKrea2Formatter": SxCPKrea2Formatter, "SxCPSDXLFormatter": SxCPSDXLFormatter, "SxCPInstaOFOptions": SxCPInstaOFOptions, "SxCPInstaOFPromptPair": SxCPInstaOFPromptPair, } NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS) _install_input_tooltips(NODE_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS = { "SxCPPromptBuilder": "SxCP Prompt Builder", "SxCPGlobalSeed": "SxCP Global Seed", "SxCPSeedControl": "SxCP Seed Control", "SxCPSeedLocker": "SxCP Seed Locker", "SxCPSDXLBucketSize": "SxCP SDXL Bucket Size", "SxCPCameraControl": "SxCP Camera Control", "SxCPCameraOrbitControl": "SxCP Camera Orbit Control", "SxCPQwenCameraTranslator": "SxCP Qwen Camera Translator", "SxCPCategoryPreset": "SxCP Category Preset", "SxCPLocationPool": "SxCP Location Pool", "SxCPCompositionPool": "SxCP Composition Pool", "SxCPLocationTheme": "SxCP Location Theme", "SxCPCastControl": "SxCP Cast Control", "SxCPCastBias": "SxCP Cast Bias", "SxCPGenerationProfile": "SxCP Generation Profile", "SxCPEthnicityList": "SxCP Ethnicity List", "SxCPHairLength": "SxCP Hair Length", "SxCPHairColor": "SxCP Hair Color", "SxCPHairStyle": "SxCP Hair Style/Cut", "SxCPCharacterAgeRange": "SxCP Character Age Range", "SxCPCharacterBodyPool": "SxCP Character Body Pool", "SxCPWomanBodyPool": "SxCP Woman Body Pool", "SxCPManBodyPool": "SxCP Man Body Pool", "SxCPEyeColorPool": "SxCP Eye Color Pool", "SxCPCharacterClothing": "SxCP Character Clothing", "SxCPHardcorePositionPool": "SxCP Hardcore Position Pool", "SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter", "SxCPCharacterManualDetails": "SxCP Character Manual Details", "SxCPAdvancedFilters": "SxCP Advanced Filters", "SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs", "SxCPWomanSlot": "SxCP Woman Slot", "SxCPManSlot": "SxCP Man Slot", "SxCPCharacterSlot": "SxCP Character Slot", "SxCPCharacterProfileSave": "SxCP Character Profile Save", "SxCPCharacterProfileLoad": "SxCP Character Profile Load", "SxCPCaptionNaturalizer": "SxCP Caption Naturalizer", "SxCPKrea2Formatter": "SxCP Krea2 Formatter", "SxCPSDXLFormatter": "SxCP SDXL Formatter", "SxCPInstaOFOptions": "SxCP Insta/OF Options", "SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair", } NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS) WEB_DIRECTORY = "./web" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]