diff --git a/__init__.py b/__init__.py index 3cdfea7..8a8fa35 100644 --- a/__init__.py +++ b/__init__.py @@ -404,6 +404,10 @@ try: NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS, ) + from .node_builder import ( + NODE_CLASS_MAPPINGS as BUILDER_NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as BUILDER_NODE_DISPLAY_NAME_MAPPINGS, + ) from .node_character import ( NODE_CLASS_MAPPINGS as CHARACTER_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CHARACTER_NODE_DISPLAY_NAME_MAPPINGS, @@ -433,12 +437,7 @@ try: NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS, ) from .prompt_builder import ( - build_prompt, - build_prompt_from_configs, - category_choices, - ethnicity_choices, save_character_profile_payload, - subcategory_choices, ) except ImportError: from loop_nodes import ( @@ -453,6 +452,10 @@ except ImportError: NODE_CLASS_MAPPINGS as CAMERA_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CAMERA_NODE_DISPLAY_NAME_MAPPINGS, ) + from node_builder import ( + NODE_CLASS_MAPPINGS as BUILDER_NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as BUILDER_NODE_DISPLAY_NAME_MAPPINGS, + ) from node_character import ( NODE_CLASS_MAPPINGS as CHARACTER_NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CHARACTER_NODE_DISPLAY_NAME_MAPPINGS, @@ -482,12 +485,7 @@ except ImportError: NODE_DISPLAY_NAME_MAPPINGS as SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS, ) from prompt_builder import ( - build_prompt, - build_prompt_from_configs, - category_choices, - ethnicity_choices, save_character_profile_payload, - subcategory_choices, ) @@ -565,206 +563,8 @@ if PromptServer is not None and web is not None: 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 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", ""), - ) - - -NODE_CLASS_MAPPINGS = { - "SxCPPromptBuilder": SxCPPromptBuilder, -} +NODE_CLASS_MAPPINGS = {} +NODE_CLASS_MAPPINGS.update(BUILDER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(SEED_RESOLUTION_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(CAMERA_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(CHARACTER_NODE_CLASS_MAPPINGS) @@ -773,15 +573,11 @@ NODE_CLASS_MAPPINGS.update(FORMATTER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(INSTA_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(ROUTE_CONFIG_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(PROFILE_FILTER_NODE_CLASS_MAPPINGS) -NODE_CLASS_MAPPINGS.update({ - "SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs, -}) NODE_CLASS_MAPPINGS.update(LOOP_NODE_CLASS_MAPPINGS) _install_input_tooltips(NODE_CLASS_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS = { - "SxCPPromptBuilder": "SxCP Prompt Builder", -} +NODE_DISPLAY_NAME_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS.update(BUILDER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(SEED_RESOLUTION_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(CAMERA_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(CHARACTER_NODE_DISPLAY_NAME_MAPPINGS) @@ -790,9 +586,6 @@ NODE_DISPLAY_NAME_MAPPINGS.update(FORMATTER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(INSTA_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(ROUTE_CONFIG_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(PROFILE_FILTER_NODE_DISPLAY_NAME_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update({ - "SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs", -}) NODE_DISPLAY_NAME_MAPPINGS.update(LOOP_NODE_DISPLAY_NAME_MAPPINGS) WEB_DIRECTORY = "./web" diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index cf39039..481d8ef 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -272,10 +272,10 @@ Improve later: ### Node / UI Path -Owner: `__init__.py`, `node_seed_resolution.py`, `node_camera.py`, -`node_character.py`, `node_hardcore_position.py`, `node_formatter.py`, -`node_insta.py`, `node_route_config.py`, `node_profile_filter.py`, -`loop_nodes.py`, `web/*.js`. +Owner: `__init__.py`, `node_builder.py`, `node_seed_resolution.py`, +`node_camera.py`, `node_character.py`, `node_hardcore_position.py`, +`node_formatter.py`, `node_insta.py`, `node_route_config.py`, +`node_profile_filter.py`, `loop_nodes.py`, `web/*.js`. Keep here: @@ -283,6 +283,7 @@ Keep here: - widget behavior; - button actions; - dynamic input slots. +- direct and config-driven builder node declarations in `node_builder.py`. - seed and resolution utility node declarations in `node_seed_resolution.py`. - camera utility node declarations in `node_camera.py`. - character pool, slot, and profile node declarations in `node_character.py`. @@ -296,6 +297,8 @@ Keep here: Already isolated: +- direct and config-driven prompt builder nodes live in `node_builder.py`, with + registration maps imported by `__init__.py`. - seed/global-seed/seed-locker and SDXL/Krea2 resolution utility nodes live in `node_seed_resolution.py`, with registration maps imported by `__init__.py`. - camera/orbit/Qwen translator utility nodes live in `node_camera.py`, with diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 4ba9b6e..3f93cf7 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -25,10 +25,10 @@ When a result is wrong, first identify which layer owns the bad text: - Raw builder prompt acceptable, SDXL tags wrong: edit `sdxl_formatter.py`. - Natural caption/training caption wrong: edit `caption_naturalizer.py`. - UI/preview/loop behavior wrong: edit `__init__.py`, node family modules such - as `node_seed_resolution.py`, `node_camera.py`, `node_character.py`, - `node_hardcore_position.py`, `node_formatter.py`, `node_insta.py`, - `node_route_config.py`, or `node_profile_filter.py`, `loop_nodes.py`, or - `web/*.js`. + as `node_builder.py`, `node_seed_resolution.py`, `node_camera.py`, + `node_character.py`, `node_hardcore_position.py`, `node_formatter.py`, + `node_insta.py`, `node_route_config.py`, or `node_profile_filter.py`, + `loop_nodes.py`, or `web/*.js`. ## High-Level Routes @@ -694,6 +694,7 @@ These do not own prompt pool wording, but they affect execution and review: | Index switch | `loop_nodes.py`, `web/index_switch_slots.js` | Multi-input to selected output, and selected input to multi-output routing. | | Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. | | Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. | +| Builder node wrappers | `node_builder.py`, imported by `__init__.py` | Direct prompt builder and config-driven prompt builder ComfyUI declarations. | | Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | Global/per-axis seed configs plus SDXL/Krea width/height helpers. | | Camera utility nodes | `node_camera.py`, imported by `__init__.py` | Direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation. | | Character utility nodes | `node_character.py`, imported by `__init__.py` | Hair, age/body/eyes/clothing pools, manual details, character slots, and profile save/load nodes. | diff --git a/node_builder.py b/node_builder.py new file mode 100644 index 0000000..ee3f20a --- /dev/null +++ b/node_builder.py @@ -0,0 +1,242 @@ +from __future__ import annotations + +import json + +try: + from .prompt_builder import ( + build_prompt, + build_prompt_from_configs, + category_choices, + ethnicity_choices, + subcategory_choices, + ) +except ImportError: # Allows local smoke tests from the repository root. + from prompt_builder import ( + build_prompt, + build_prompt_from_configs, + category_choices, + ethnicity_choices, + subcategory_choices, + ) + + +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_HARDCORE_POSITION_CONFIG = "SXCP_HARDCORE_POSITION_CONFIG" +SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" +SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE" + + +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 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", ""), + ) + + +NODE_CLASS_MAPPINGS = { + "SxCPPromptBuilder": SxCPPromptBuilder, + "SxCPPromptBuilderFromConfigs": SxCPPromptBuilderFromConfigs, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SxCPPromptBuilder": "SxCP Prompt Builder", + "SxCPPromptBuilderFromConfigs": "SxCP Prompt Builder From Configs", +} diff --git a/tools/prompt_map_audit.py b/tools/prompt_map_audit.py index 74ba989..ea00b8d 100644 --- a/tools/prompt_map_audit.py +++ b/tools/prompt_map_audit.py @@ -48,10 +48,12 @@ def _assignment_dict(path: Path, name: str) -> dict[str, Any]: def _class_return_names(path: Path) -> dict[str, tuple[str, ...]]: tree = ast.parse(path.read_text(encoding="utf-8")) - result: dict[str, tuple[str, ...]] = {} + classes: dict[str, tuple[list[str], tuple[str, ...]]] = {} for node in tree.body: - if not isinstance(node, ast.ClassDef) or not node.name.startswith("SxCP"): + if not isinstance(node, ast.ClassDef): continue + bases = [base.id for base in node.bases if isinstance(base, ast.Name)] + return_names: tuple[str, ...] = () for item in node.body: if not isinstance(item, ast.Assign): continue @@ -59,7 +61,29 @@ def _class_return_names(path: Path) -> dict[str, tuple[str, ...]]: continue value = _literal_or_none(item.value) if isinstance(value, tuple) and all(isinstance(part, str) for part in value): - result[node.name] = value + return_names = value + classes[node.name] = (bases, return_names) + + def resolve(class_name: str, seen: set[str] | None = None) -> tuple[str, ...]: + seen = seen or set() + if class_name in seen: + return () + seen.add(class_name) + bases, return_names = classes.get(class_name, ([], ())) + if return_names: + return return_names + for base_name in bases: + inherited = resolve(base_name, seen) + if inherited: + return inherited + return () + + result: dict[str, tuple[str, ...]] = {} + for class_name in classes: + if class_name.startswith("SxCP"): + return_names = resolve(class_name) + if return_names: + result[class_name] = return_names return result @@ -97,6 +121,12 @@ def _category_json_paths() -> list[Path]: return sorted((ROOT / "categories").glob("*.json")) +def _node_python_paths() -> list[Path]: + paths = [ROOT / "__init__.py", ROOT / "loop_nodes.py"] + paths.extend(sorted(ROOT.glob("node_*.py"))) + return [path for path in paths if path.exists()] + + def _load_category_json(path: Path) -> dict[str, Any]: data = json.loads(path.read_text(encoding="utf-8")) return data if isinstance(data, dict) else {} @@ -246,14 +276,13 @@ def print_table(headers: tuple[str, ...], rows: list[tuple[Any, ...]]) -> None: def main() -> int: - init_path = ROOT / "__init__.py" - loop_path = ROOT / "loop_nodes.py" category_paths = _category_json_paths() - display = _assignment_dict(init_path, "NODE_DISPLAY_NAME_MAPPINGS") - loop_display = _assignment_dict(loop_path, "LOOP_NODE_DISPLAY_NAME_MAPPINGS") - display.update(loop_display) - returns = _class_return_names(init_path) - returns.update(_class_return_names(loop_path)) + display: dict[str, Any] = {} + returns: dict[str, tuple[str, ...]] = {} + for path in _node_python_paths(): + display.update(_assignment_dict(path, "NODE_DISPLAY_NAME_MAPPINGS")) + display.update(_assignment_dict(path, "LOOP_NODE_DISPLAY_NAME_MAPPINGS")) + returns.update(_class_return_names(path)) print("# Node Display Map") node_rows = [] diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 0a3b81b..1210252 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -2169,6 +2169,63 @@ def smoke_node_insta_registration() -> None: _expect(pair.get("options", {}).get("hardcore_cast") == "couple", "Insta/OF Prompt Pair lost options metadata") +def smoke_node_builder_registration() -> None: + required_nodes = [ + "SxCPPromptBuilder", + "SxCPPromptBuilderFromConfigs", + ] + for node_name in required_nodes: + _expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry") + _expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry") + + builder_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPPromptBuilder"] + builder_inputs = builder_node.INPUT_TYPES().get("required") or {} + _expect("category" in builder_inputs, "Prompt Builder lost category input") + _expect("tooltip" in builder_inputs["category"][1], "Prompt Builder tooltip injection missing") + direct_output = builder_node().build( + "woman", + "random", + 1, + 41, + 123, + "full", + "any", + "standard", + True, + 0.5, + 0.0, + "random", + 1, + 0, + -1, + -1, + Trigger, + True, + ) + direct_row = json.loads(direct_output[3]) + _expect_row_base(direct_row, "node_builder.direct_row") + _expect(direct_output[0] == direct_row.get("prompt"), "Prompt Builder prompt output drifted from metadata") + _expect(direct_output[4] == direct_row.get("main_category"), "Prompt Builder category output drifted from metadata") + _expect_trigger_once("node_builder.direct_prompt", direct_output[0], Trigger) + + config_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPPromptBuilderFromConfigs"] + config_inputs = config_node.INPUT_TYPES() + _expect("category_config" in (config_inputs.get("optional") or {}), "Prompt Builder From Configs lost category_config input") + config_output = config_node().build( + 1, + 41, + 123, + category_config=pb.build_category_config_json("woman", "random"), + cast_config=pb.build_cast_config_json("solo_woman", 1, 0), + generation_profile=pb.build_generation_profile_json(profile="balanced"), + ) + config_row = json.loads(config_output[3]) + _expect_row_base(config_row, "node_builder.config_row") + _expect(config_output[0] == config_row.get("prompt"), "Prompt Builder From Configs prompt output drifted from metadata") + _expect(config_output[4] == config_row.get("main_category"), "Prompt Builder From Configs category output drifted from metadata") + _expect_text("node_builder.config_caption", config_output[2], 20) + + def smoke_node_profile_filter_registration() -> None: required_nodes = [ "SxCPGenerationProfile", @@ -2280,6 +2337,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("node_hardcore_position_registration", smoke_node_hardcore_position_registration), ("node_formatter_registration", smoke_node_formatter_registration), ("node_insta_registration", smoke_node_insta_registration), + ("node_builder_registration", smoke_node_builder_registration), ("node_profile_filter_registration", smoke_node_profile_filter_registration), ]