diff --git a/docs/krea2-pov-pose-atlas.md b/docs/krea2-pov-pose-atlas.md index ebcf2cf..2dd174c 100644 --- a/docs/krea2-pov-pose-atlas.md +++ b/docs/krea2-pov-pose-atlas.md @@ -17,6 +17,10 @@ geometry summary, cue phrases, avoid phrases, references, and a known generator hook. Code should read it through `krea2_pose_variant_catalog.py` instead of parsing the JSON directly. +In ComfyUI, use the `SxCP Krea2 Pose Variant` node when you want a workflow to +select one catalog variant and emit a compatible `hardcore_position_config` for +the existing Position Pool / Action Filter / Insta-OF chain. + ## Inventory | Family | Pose images | Control images | First sample | diff --git a/node_hardcore_position.py b/node_hardcore_position.py index 4ade145..8a539e8 100644 --- a/node_hardcore_position.py +++ b/node_hardcore_position.py @@ -3,6 +3,7 @@ from __future__ import annotations import json try: + from . import krea2_pose_variant_catalog from .hardcore_position_config import ( build_hardcore_action_filter_json, build_hardcore_position_pool_json, @@ -11,6 +12,7 @@ try: hardcore_position_key_choices, ) except ImportError: # Allows local smoke tests from the repository root. + import krea2_pose_variant_catalog from hardcore_position_config import ( build_hardcore_action_filter_json, build_hardcore_position_pool_json, @@ -30,6 +32,18 @@ def _choice_input_key(prefix, choice): return f"{prefix}_{key}" +def _variant_family(value): + family = str(value or "any") + if family == "penetration": + family = "penetrative" + return family if family in hardcore_position_family_choices() else "any" + + +def _variant_positions(variant): + valid = set(hardcore_position_key_choices()) + return [str(key) for key in variant.get("position_keys", []) if str(key) in valid] + + class SxCPHardcorePositionPool: @classmethod def INPUT_TYPES(cls): @@ -66,6 +80,66 @@ class SxCPHardcorePositionPool: return config, json.loads(config).get("summary", "") +class SxCPKrea2PoseVariant: + @classmethod + def INPUT_TYPES(cls): + keys = krea2_pose_variant_catalog.variant_keys() + return { + "required": { + "variant_key": (keys or ["missing_catalog_variant"], {"default": keys[0] if keys else "missing_catalog_variant"}), + "combine_mode": (["replace", "add"], {"default": "replace"}), + }, + "optional": { + "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), + }, + } + + RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ( + "hardcore_position_config", + "variant_key", + "prompt_cues", + "avoid_cues", + "summary", + "variant_json", + ) + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build(self, variant_key, combine_mode="replace", hardcore_position_config=""): + variant = krea2_pose_variant_catalog.get_variant(variant_key) + if not variant: + empty = { + "key": str(variant_key or ""), + "status": "missing", + "summary": "missing Krea2 pose variant", + } + return hardcore_position_config or "", str(variant_key or ""), "", "", empty["summary"], json.dumps(empty, sort_keys=True) + + positions = _variant_positions(variant) + family = _variant_family(variant.get("action_family") or variant.get("family")) + config = build_hardcore_position_pool_json( + hardcore_position_config=hardcore_position_config or "", + combine_mode=combine_mode, + family=family, + selected_positions=positions, + ) + prompt_cues = "; ".join(str(cue) for cue in variant.get("prompt_cues", []) if str(cue).strip()) + avoid_cues = "; ".join(str(cue) for cue in variant.get("avoid_cues", []) if str(cue).strip()) + summary = ( + f"variant={variant.get('key')}; status={variant.get('status')}; " + f"family={family}; positions={','.join(positions) or 'none'}" + ) + return ( + config, + str(variant.get("key") or variant_key), + prompt_cues, + avoid_cues, + summary, + json.dumps(variant, ensure_ascii=True, sort_keys=True), + ) + + class SxCPHardcoreActionFilter: @classmethod def INPUT_TYPES(cls): @@ -128,9 +202,11 @@ class SxCPHardcoreActionFilter: NODE_CLASS_MAPPINGS = { "SxCPHardcorePositionPool": SxCPHardcorePositionPool, "SxCPHardcoreActionFilter": SxCPHardcoreActionFilter, + "SxCPKrea2PoseVariant": SxCPKrea2PoseVariant, } NODE_DISPLAY_NAME_MAPPINGS = { "SxCPHardcorePositionPool": "SxCP Hardcore Position Pool", "SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter", + "SxCPKrea2PoseVariant": "SxCP Krea2 Pose Variant", } diff --git a/node_tooltips.py b/node_tooltips.py index 96f7f9b..2fb60e0 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -322,6 +322,11 @@ NODE_INPUT_TOOLTIPS = { "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.", }, + "SxCPKrea2PoseVariant": { + "variant_key": "Atlas-calibrated Krea2 POV pose variant. Proven variants have fixed-seed evidence in the eval log.", + "combine_mode": "replace discards incoming position choices; add merges this variant with the incoming position config.", + "hardcore_position_config": "Optional incoming hardcore position config. Connect this when layering a variant on an existing pool.", + }, "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.", diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 47737fd..00205b8 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -8869,6 +8869,7 @@ def smoke_node_hardcore_position_registration() -> None: required_nodes = [ "SxCPHardcorePositionPool", "SxCPHardcoreActionFilter", + "SxCPKrea2PoseVariant", ] for node_name in required_nodes: _expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry") @@ -8911,6 +8912,25 @@ def smoke_node_hardcore_position_registration() -> None: _expect(parsed_filter.get("allow_outercourse") is True, "Hardcore Action Filter should allow outercourse") _expect("blocked=" in filter_summary, "Hardcore Action Filter summary lost blocked-gate details") + variant_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2PoseVariant"] + variant_inputs = variant_node.INPUT_TYPES().get("required") or {} + _expect("variant_key" in variant_inputs, "Krea2 Pose Variant lost variant selector") + _expect("tooltip" in variant_inputs["variant_key"][1], "Krea2 Pose Variant tooltip injection missing") + _expect("pov_boobjob_upright_cleavage" in variant_inputs["variant_key"][0], "Krea2 Pose Variant lost boobjob option") + variant_config, variant_key, prompt_cues, avoid_cues, variant_summary, variant_json = variant_node().build( + "pov_boobjob_upright_cleavage", + "replace", + "", + ) + parsed_variant_config = json.loads(variant_config) + parsed_variant = json.loads(variant_json) + _expect(variant_key == "pov_boobjob_upright_cleavage", "Krea2 Pose Variant returned wrong key") + _expect(parsed_variant_config.get("positions") == ["boobjob"], "Krea2 Pose Variant did not map to boobjob position config") + _expect(parsed_variant.get("status") == "proven", "Krea2 Pose Variant lost status metadata") + _expect("pressed-together breasts" in prompt_cues, "Krea2 Pose Variant lost prompt cues output") + _expect("torso bent forward" in avoid_cues, "Krea2 Pose Variant lost avoid cues output") + _expect("variant=pov_boobjob_upright_cleavage" in variant_summary, "Krea2 Pose Variant summary lost key") + def smoke_node_formatter_registration() -> None: required_nodes = [