Add Krea2 POV prompt restore node
This commit is contained in:
@@ -217,6 +217,20 @@ HARDCORE_POSITION_AXIS_KEYS = {
|
|||||||
"aftercare_act",
|
"aftercare_act",
|
||||||
"cleanup_detail",
|
"cleanup_detail",
|
||||||
}
|
}
|
||||||
|
RESTORE_PROMPT_AXIS_CHOICES = [
|
||||||
|
"clothing_detail",
|
||||||
|
"face_detail",
|
||||||
|
"expression_detail",
|
||||||
|
"mouth_detail",
|
||||||
|
"reaction_detail",
|
||||||
|
"body_contact",
|
||||||
|
"hand_detail",
|
||||||
|
"touch_detail",
|
||||||
|
"foreplay_detail",
|
||||||
|
"performance_act",
|
||||||
|
"visibility",
|
||||||
|
"angle",
|
||||||
|
]
|
||||||
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
|
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
|
||||||
"penetrative_sex": "penetrative",
|
"penetrative_sex": "penetrative",
|
||||||
"foreplay_teasing": "foreplay",
|
"foreplay_teasing": "foreplay",
|
||||||
@@ -264,6 +278,16 @@ def _list_from(value: Any) -> list[Any]:
|
|||||||
return [value]
|
return [value]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_restore_prompt_axes(values: Any) -> list[str]:
|
||||||
|
allowed = set(RESTORE_PROMPT_AXIS_CHOICES)
|
||||||
|
normalized: list[str] = []
|
||||||
|
for value in _list_from(values):
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text in allowed and text not in normalized:
|
||||||
|
normalized.append(text)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
def _entry_text(item: Any) -> str:
|
def _entry_text(item: Any) -> str:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
return str(
|
return str(
|
||||||
@@ -376,6 +400,8 @@ def empty_hardcore_position_config() -> dict[str, Any]:
|
|||||||
"allow_outercourse": True,
|
"allow_outercourse": True,
|
||||||
"allow_anal": True,
|
"allow_anal": True,
|
||||||
"allow_climax": True,
|
"allow_climax": True,
|
||||||
|
"restore_prompt_axes": [],
|
||||||
|
"relax_non_pose_axis_conflicts": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -409,6 +435,8 @@ def parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[s
|
|||||||
"allow_climax",
|
"allow_climax",
|
||||||
):
|
):
|
||||||
parsed[key] = not _is_false(parsed.get(key, True))
|
parsed[key] = not _is_false(parsed.get(key, True))
|
||||||
|
parsed["restore_prompt_axes"] = normalize_restore_prompt_axes(parsed.get("restore_prompt_axes"))
|
||||||
|
parsed["relax_non_pose_axis_conflicts"] = not _is_false(parsed.get("relax_non_pose_axis_conflicts", False))
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
@@ -439,6 +467,11 @@ def hardcore_position_summary(config: dict[str, Any]) -> str:
|
|||||||
]
|
]
|
||||||
if disabled:
|
if disabled:
|
||||||
parts.append("blocked=" + ",".join(disabled))
|
parts.append("blocked=" + ",".join(disabled))
|
||||||
|
restore_axes = normalize_restore_prompt_axes(config.get("restore_prompt_axes"))
|
||||||
|
if restore_axes:
|
||||||
|
parts.append("restore_axes=" + ",".join(restore_axes))
|
||||||
|
if restore_axes and config.get("relax_non_pose_axis_conflicts"):
|
||||||
|
parts.append("relaxed_non_pose_conflicts")
|
||||||
return "; ".join(parts)
|
return "; ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -787,6 +820,14 @@ def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> boo
|
|||||||
return bool(matched) and not bool(matched & selected)
|
return bool(matched) and not bool(matched & selected)
|
||||||
|
|
||||||
|
|
||||||
|
def restored_prompt_axis_relaxes_conflicts(axis_name: str, config: dict[str, Any]) -> bool:
|
||||||
|
if str(axis_name or "") in HARDCORE_POSITION_AXIS_KEYS:
|
||||||
|
return False
|
||||||
|
if not config.get("relax_non_pose_axis_conflicts"):
|
||||||
|
return False
|
||||||
|
return str(axis_name or "") in set(normalize_restore_prompt_axes(config.get("restore_prompt_axes")))
|
||||||
|
|
||||||
|
|
||||||
def hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
|
def hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
|
||||||
if not hardcore_position_template_required(config):
|
if not hardcore_position_template_required(config):
|
||||||
return True
|
return True
|
||||||
@@ -809,7 +850,11 @@ def filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, An
|
|||||||
value
|
value
|
||||||
for value in values
|
for value in values
|
||||||
if not hardcore_entry_blocked_by_action(value, axis_name, config)
|
if not hardcore_entry_blocked_by_action(value, axis_name, config)
|
||||||
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and hardcore_position_entry_conflicts(value, config))
|
and not (
|
||||||
|
axis_name not in HARDCORE_POSITION_AXIS_KEYS
|
||||||
|
and not restored_prompt_axis_relaxes_conflicts(axis_name, config)
|
||||||
|
and hardcore_position_entry_conflicts(value, config)
|
||||||
|
)
|
||||||
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
|
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or hardcore_position_entry_matches(value, config))
|
||||||
]
|
]
|
||||||
return filtered or values
|
return filtered or values
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ try:
|
|||||||
hardcore_position_focus_choices,
|
hardcore_position_focus_choices,
|
||||||
hardcore_position_key_choices,
|
hardcore_position_key_choices,
|
||||||
hardcore_position_summary,
|
hardcore_position_summary,
|
||||||
|
normalize_restore_prompt_axes,
|
||||||
parse_hardcore_position_config,
|
parse_hardcore_position_config,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests from the repository root.
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
@@ -26,6 +27,7 @@ except ImportError: # Allows local smoke tests from the repository root.
|
|||||||
hardcore_position_focus_choices,
|
hardcore_position_focus_choices,
|
||||||
hardcore_position_key_choices,
|
hardcore_position_key_choices,
|
||||||
hardcore_position_summary,
|
hardcore_position_summary,
|
||||||
|
normalize_restore_prompt_axes,
|
||||||
parse_hardcore_position_config,
|
parse_hardcore_position_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,6 +341,62 @@ class SxCPKrea2POVInteractionFilter(_SxCPKrea2POVVariantFilter):
|
|||||||
POSITION_FAMILY = "interaction"
|
POSITION_FAMILY = "interaction"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVPromptRestore:
|
||||||
|
CLOTHING_AXES = ["clothing_detail"]
|
||||||
|
FACE_EXPRESSION_AXES = ["face_detail", "expression_detail", "mouth_detail", "reaction_detail"]
|
||||||
|
BODY_TOUCH_AXES = ["body_contact", "hand_detail", "touch_detail", "foreplay_detail"]
|
||||||
|
CAMERA_PRESENTATION_AXES = ["performance_act", "visibility", "angle"]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
|
||||||
|
"restore_clothing_detail": ("BOOLEAN", {"default": True}),
|
||||||
|
"restore_face_expression_detail": ("BOOLEAN", {"default": True}),
|
||||||
|
"restore_body_touch_detail": ("BOOLEAN", {"default": False}),
|
||||||
|
"restore_camera_presentation_detail": ("BOOLEAN", {"default": False}),
|
||||||
|
"relax_non_pose_axis_conflicts": ("BOOLEAN", {"default": True}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING")
|
||||||
|
RETURN_NAMES = ("hardcore_position_config", "summary")
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
hardcore_position_config,
|
||||||
|
restore_clothing_detail=True,
|
||||||
|
restore_face_expression_detail=True,
|
||||||
|
restore_body_touch_detail=False,
|
||||||
|
restore_camera_presentation_detail=False,
|
||||||
|
relax_non_pose_axis_conflicts=True,
|
||||||
|
):
|
||||||
|
config = parse_hardcore_position_config(hardcore_position_config)
|
||||||
|
axes: list[str] = []
|
||||||
|
if restore_clothing_detail:
|
||||||
|
axes.extend(self.CLOTHING_AXES)
|
||||||
|
config["allow_foreplay"] = True
|
||||||
|
config["allow_interaction"] = True
|
||||||
|
if restore_face_expression_detail:
|
||||||
|
axes.extend(self.FACE_EXPRESSION_AXES)
|
||||||
|
config["allow_foreplay"] = True
|
||||||
|
config["allow_interaction"] = True
|
||||||
|
if restore_body_touch_detail:
|
||||||
|
axes.extend(self.BODY_TOUCH_AXES)
|
||||||
|
config["allow_foreplay"] = True
|
||||||
|
config["allow_interaction"] = True
|
||||||
|
if restore_camera_presentation_detail:
|
||||||
|
axes.extend(self.CAMERA_PRESENTATION_AXES)
|
||||||
|
config["allow_interaction"] = True
|
||||||
|
config["restore_prompt_axes"] = normalize_restore_prompt_axes(axes)
|
||||||
|
config["relax_non_pose_axis_conflicts"] = bool(relax_non_pose_axis_conflicts)
|
||||||
|
config["summary"] = hardcore_position_summary(config)
|
||||||
|
return json.dumps(config, ensure_ascii=True, sort_keys=True), str(config["summary"])
|
||||||
|
|
||||||
|
|
||||||
class SxCPKrea2VariantEvidence:
|
class SxCPKrea2VariantEvidence:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -462,6 +520,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SxCPKrea2POVToyFilter": SxCPKrea2POVToyFilter,
|
"SxCPKrea2POVToyFilter": SxCPKrea2POVToyFilter,
|
||||||
"SxCPKrea2POVClimaxFilter": SxCPKrea2POVClimaxFilter,
|
"SxCPKrea2POVClimaxFilter": SxCPKrea2POVClimaxFilter,
|
||||||
"SxCPKrea2POVInteractionFilter": SxCPKrea2POVInteractionFilter,
|
"SxCPKrea2POVInteractionFilter": SxCPKrea2POVInteractionFilter,
|
||||||
|
"SxCPKrea2POVPromptRestore": SxCPKrea2POVPromptRestore,
|
||||||
"SxCPKrea2VariantEvidence": SxCPKrea2VariantEvidence,
|
"SxCPKrea2VariantEvidence": SxCPKrea2VariantEvidence,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,5 +535,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SxCPKrea2POVToyFilter": "SxCP Krea2 POV Toy Filter",
|
"SxCPKrea2POVToyFilter": "SxCP Krea2 POV Toy Filter",
|
||||||
"SxCPKrea2POVClimaxFilter": "SxCP Krea2 POV Climax Filter",
|
"SxCPKrea2POVClimaxFilter": "SxCP Krea2 POV Climax Filter",
|
||||||
"SxCPKrea2POVInteractionFilter": "SxCP Krea2 POV Interaction Filter",
|
"SxCPKrea2POVInteractionFilter": "SxCP Krea2 POV Interaction Filter",
|
||||||
|
"SxCPKrea2POVPromptRestore": "SxCP Krea2 POV Prompt Restore",
|
||||||
"SxCPKrea2VariantEvidence": "SxCP Krea2 Variant Evidence",
|
"SxCPKrea2VariantEvidence": "SxCP Krea2 Variant Evidence",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11195,6 +11195,7 @@ def smoke_node_hardcore_position_registration() -> None:
|
|||||||
"SxCPKrea2POVToyFilter",
|
"SxCPKrea2POVToyFilter",
|
||||||
"SxCPKrea2POVClimaxFilter",
|
"SxCPKrea2POVClimaxFilter",
|
||||||
"SxCPKrea2POVInteractionFilter",
|
"SxCPKrea2POVInteractionFilter",
|
||||||
|
"SxCPKrea2POVPromptRestore",
|
||||||
"SxCPKrea2VariantEvidence",
|
"SxCPKrea2VariantEvidence",
|
||||||
]
|
]
|
||||||
for node_name in required_nodes:
|
for node_name in required_nodes:
|
||||||
@@ -11350,6 +11351,80 @@ def smoke_node_hardcore_position_registration() -> None:
|
|||||||
_expect("doggy" in mixed_positions and "penis_licking" in mixed_positions, "Chained POV filters returned wrong positions summary")
|
_expect("doggy" in mixed_positions and "penis_licking" in mixed_positions, "Chained POV filters returned wrong positions summary")
|
||||||
_expect("family=any" in mixed_summary, "Chained POV filters summary should show mixed family")
|
_expect("family=any" in mixed_summary, "Chained POV filters summary should show mixed family")
|
||||||
|
|
||||||
|
restore_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVPromptRestore"]
|
||||||
|
restore_inputs = restore_node.INPUT_TYPES().get("required") or {}
|
||||||
|
for key in (
|
||||||
|
"restore_clothing_detail",
|
||||||
|
"restore_face_expression_detail",
|
||||||
|
"restore_body_touch_detail",
|
||||||
|
"restore_camera_presentation_detail",
|
||||||
|
"relax_non_pose_axis_conflicts",
|
||||||
|
):
|
||||||
|
_expect(key in restore_inputs, f"Krea2 POV Prompt Restore lost input {key}")
|
||||||
|
restored_config, restored_summary = restore_node().build(
|
||||||
|
doggy_config,
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
parsed_restored_config = json.loads(restored_config)
|
||||||
|
_expect(
|
||||||
|
parsed_restored_config.get("krea2_variant_keys") == ["pov_doggy_top_down_rear_entry"],
|
||||||
|
"Krea2 POV Prompt Restore should preserve selected atlas variant metadata",
|
||||||
|
)
|
||||||
|
_expect(parsed_restored_config.get("positions") == ["doggy"], "Krea2 POV Prompt Restore should preserve selected pose lock")
|
||||||
|
_expect(parsed_restored_config.get("family") == "penetrative", "Krea2 POV Prompt Restore should preserve atlas family lock")
|
||||||
|
_expect(
|
||||||
|
parsed_restored_config.get("restore_prompt_axes") == [
|
||||||
|
"clothing_detail",
|
||||||
|
"face_detail",
|
||||||
|
"expression_detail",
|
||||||
|
"mouth_detail",
|
||||||
|
"reaction_detail",
|
||||||
|
],
|
||||||
|
"Krea2 POV Prompt Restore should record requested prompt axes",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
parsed_restored_config.get("relax_non_pose_axis_conflicts") is True,
|
||||||
|
"Krea2 POV Prompt Restore should enable non-pose conflict relaxation",
|
||||||
|
)
|
||||||
|
_expect("restore_axes=clothing_detail,face_detail" in restored_summary, "Krea2 POV Prompt Restore summary lost restored axes")
|
||||||
|
parsed_restored = hardcore_position_config.parse_hardcore_position_config(parsed_restored_config)
|
||||||
|
conflicting_clothing = {"text": "shirt pulled open while standing", "position_keys": ["standing"]}
|
||||||
|
neutral_clothing = {"text": "shirt pulled open at the hips"}
|
||||||
|
restored_clothing = hardcore_position_config.filter_hardcore_axis(
|
||||||
|
"clothing_detail",
|
||||||
|
[conflicting_clothing, neutral_clothing],
|
||||||
|
parsed_restored,
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
restored_clothing == [conflicting_clothing, neutral_clothing],
|
||||||
|
"Krea2 POV Prompt Restore should allow restored clothing detail through non-pose conflict pruning",
|
||||||
|
)
|
||||||
|
unrestored_hand = hardcore_position_config.filter_hardcore_axis(
|
||||||
|
"hand_detail",
|
||||||
|
[conflicting_clothing, neutral_clothing],
|
||||||
|
parsed_restored,
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
unrestored_hand == [neutral_clothing],
|
||||||
|
"Krea2 POV Prompt Restore should not relax unrequested non-pose axes",
|
||||||
|
)
|
||||||
|
restored_position = hardcore_position_config.filter_hardcore_axis(
|
||||||
|
"position",
|
||||||
|
[
|
||||||
|
{"text": "standing sex position", "position_keys": ["standing"]},
|
||||||
|
{"text": "doggy style position", "position_keys": ["doggy"]},
|
||||||
|
],
|
||||||
|
parsed_restored,
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
restored_position == [{"text": "doggy style position", "position_keys": ["doggy"]}],
|
||||||
|
"Krea2 POV Prompt Restore should keep position axes locked to the atlas pose",
|
||||||
|
)
|
||||||
|
|
||||||
evidence_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2VariantEvidence"]
|
evidence_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2VariantEvidence"]
|
||||||
evidence_inputs = evidence_node.INPUT_TYPES().get("required") or {}
|
evidence_inputs = evidence_node.INPUT_TYPES().get("required") or {}
|
||||||
_expect("variant_key" in evidence_inputs, "Krea2 Variant Evidence lost variant selector")
|
_expect("variant_key" in evidence_inputs, "Krea2 Variant Evidence lost variant selector")
|
||||||
|
|||||||
Reference in New Issue
Block a user