From caeafa0714221ed4cf797d18b859cb7e0d338416 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Tue, 30 Jun 2026 23:25:28 +0200 Subject: [PATCH] Make POV prompt restore affect Krea output --- hardcore_text_cleanup.py | 12 +++- item_axis_policy.py | 10 +++- krea_pov_actions.py | 7 ++- node_hardcore_position.py | 2 +- node_tooltips.py | 8 +++ row_category_route.py | 112 ++++++++++++++++++++++++++++++++++++++ tools/prompt_smoke.py | 57 +++++++++++++++++++ 7 files changed, 203 insertions(+), 5 deletions(-) diff --git a/hardcore_text_cleanup.py b/hardcore_text_cleanup.py index 87e7789..bc70992 100644 --- a/hardcore_text_cleanup.py +++ b/hardcore_text_cleanup.py @@ -72,10 +72,18 @@ def sanitize_hardcore_environment_anchors(value: Any) -> str: return text.strip() -def sanitize_hardcore_axis_values(values: Any) -> dict[str, str]: +def sanitize_hardcore_axis_value(value: Any) -> Any: + if isinstance(value, list): + return [sanitize_hardcore_axis_value(item) for item in value] + if isinstance(value, dict): + return {str(key): sanitize_hardcore_axis_value(item) for key, item in value.items()} + return sanitize_hardcore_environment_anchors(value) + + +def sanitize_hardcore_axis_values(values: Any) -> dict[str, Any]: if not isinstance(values, dict): return {} return { - str(key): sanitize_hardcore_environment_anchors(value) + str(key): sanitize_hardcore_axis_value(value) for key, value in values.items() } diff --git a/item_axis_policy.py b/item_axis_policy.py index 0652b1e..503736e 100644 --- a/item_axis_policy.py +++ b/item_axis_policy.py @@ -6,7 +6,14 @@ from typing import Any PLACEHOLDER_VALUES = {"", "any", "auto", "random", "none", "null"} PREFERRED_VALUE_KEYS = ("text", "prompt", "template", "value", "name") -METADATA_AXIS_KEYS = {"action_family", "position_family", "position_key", "position_keys", "krea2_variant_keys"} +METADATA_AXIS_KEYS = { + "action_family", + "position_family", + "position_key", + "position_keys", + "krea2_variant_keys", + "restored_prompt_axes", +} ACTION_CONTEXT_PRIORITY = ( "position", "body_position", @@ -14,6 +21,7 @@ ACTION_CONTEXT_PRIORITY = ( "arrangement", "angle", "surface", + "restored_prompt_details", "body_contact", "leg_detail", "outer_act", diff --git a/krea_pov_actions.py b/krea_pov_actions.py index d793f41..65f12b6 100644 --- a/krea_pov_actions.py +++ b/krea_pov_actions.py @@ -110,7 +110,12 @@ def _krea2_atlas_variant_sentence(axis_values: Any) -> str: if not variant: return "" cues = _unique_texts(list(variant.get("prompt_cues") or []) or [variant.get("canonical_geometry")]) - return _clean(". ".join(cues)).rstrip(".") + sentence = _clean(". ".join(cues)).rstrip(".") + if isinstance(axis_values, dict): + restored_details = _unique_texts(_list_values(axis_values.get("restored_prompt_details"))) + if restored_details: + sentence = f"{sentence}. Additional visible detail: {'; '.join(restored_details)}" + return sentence def pov_ejaculation_target(context: str) -> str: diff --git a/node_hardcore_position.py b/node_hardcore_position.py index b4d2a44..e627210 100644 --- a/node_hardcore_position.py +++ b/node_hardcore_position.py @@ -345,7 +345,7 @@ 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"] + CAMERA_PRESENTATION_AXES = ["performance_act", "visibility"] @classmethod def INPUT_TYPES(cls): diff --git a/node_tooltips.py b/node_tooltips.py index 9670dc4..ec2a4ed 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -327,6 +327,14 @@ NODE_INPUT_TOOLTIPS = { "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.", }, + "SxCPKrea2POVPromptRestore": { + "restore_clothing_detail": "Let compatible clothing/body-exposure detail survive a strict Krea2 POV atlas pose lock when the source category has that axis.", + "restore_face_expression_detail": "Restore compatible face, expression, mouth, and reaction detail as visible prompt detail without changing the atlas pose.", + "restore_body_touch_detail": "Restore compatible body-contact, hand, touch, and foreplay detail as visible prompt detail while keeping the pose locked.", + "restore_camera_presentation_detail": "Restore compatible presentation and visibility wording. Camera angle axes stay locked to the atlas pose.", + "relax_non_pose_axis_conflicts": "Allow restored non-pose axes to pass position-conflict pruning. Position axes remain locked to the selected atlas pose.", + "hardcore_position_config": "Optional incoming Krea2 POV position config. Use before or after a POV filter to add restored prompt-detail axes.", + }, "SxCPKrea2VariantEvidence": { "variant_key": "Catalog variant whose fixed-seed eval evidence should be shown.", "result": "Filter eval entries by result. accepted is the evidence used for proven variants.", diff --git a/row_category_route.py b/row_category_route.py index 5016ad8..4bdb6a9 100644 --- a/row_category_route.py +++ b/row_category_route.py @@ -100,6 +100,101 @@ class CategoryItemRoute: } +def _unique_texts(values: list[Any]) -> list[str]: + selected: list[str] = [] + seen: set[str] = set() + for value in values: + text = row_item_policy.entry_text(value).strip(" .;") + lower = text.lower() + if not text or lower in seen: + continue + selected.append(text) + seen.add(lower) + return selected + + +def _restore_axis_values_for_context( + values: list[Any], + *, + subcategory_slug: str, + axis_name: str, + item_axis_values: dict[str, Any], + women_count: int, + men_count: int, +) -> list[Any]: + values = category_policy.compatible_entries(values, women_count, men_count) + if subcategory_slug == "oral_sex": + return row_item_policy.oral_axis_values_for_context( + values, + str(item_axis_values.get("position") or ""), + str(item_axis_values.get("oral_act") or ""), + axis_name, + ) + if subcategory_slug == "outercourse_sex": + return row_item_policy.outercourse_axis_values_for_position( + values, + str(item_axis_values.get("position") or ""), + axis_name, + ) + if subcategory_slug == "anal_double_penetration": + return row_item_policy.anal_axis_values_for_position( + values, + str(item_axis_values.get("position") or ""), + axis_name, + ) + return values + + +def _restored_prompt_axis_values( + rng: Any, + subcategory: dict[str, Any], + item_axis_values: dict[str, Any], + hardcore_position_config: dict[str, Any], + women_count: int, + men_count: int, +) -> dict[str, str]: + restore_axes = hardcore_position_policy.normalize_restore_prompt_axes( + hardcore_position_config.get("restore_prompt_axes") if isinstance(hardcore_position_config, dict) else [] + ) + raw_axes = subcategory.get("item_axes") + if not restore_axes or not isinstance(raw_axes, dict): + return {} + + restored: dict[str, str] = {} + subcategory_slug = str(subcategory.get("slug") or "").lower() + for axis_name in restore_axes: + existing = "" + if axis_name in item_axis_values and item_axis_values.get(axis_name) is not None: + existing = row_item_policy.entry_text(item_axis_values.get(axis_name)).strip(" .;") + if existing: + restored[axis_name] = existing + continue + values = _list_from(raw_axes.get(axis_name)) + if not values: + continue + values = _restore_axis_values_for_context( + values, + subcategory_slug=subcategory_slug, + axis_name=axis_name, + item_axis_values=item_axis_values, + women_count=women_count, + men_count=men_count, + ) + if not values: + continue + restored[axis_name] = row_item_policy.entry_text(row_item_policy.weighted_choice(rng, values)).strip(" .;") + return {axis: value for axis, value in restored.items() if value} + + +def _append_restored_prompt_details(item_text: str, details: list[str]) -> str: + details = [detail for detail in _unique_texts(details) if detail.lower() not in str(item_text or "").lower()] + if not details: + return item_text + if not item_text: + return "; ".join(details) + return f"{str(item_text).rstrip(' .')}, with {'; '.join(details)}" + + def select_category_item_route_result( *, category_choice: str, @@ -159,6 +254,23 @@ def select_category_item_route_result( women_count, men_count, ) + restored_axis_values = _restored_prompt_axis_values( + content_rng, + subcategory, + item_axis_values, + parsed_hardcore_position_config, + women_count, + men_count, + ) + if restored_axis_values: + restored_details = _unique_texts(list(restored_axis_values.values())) + item_axis_values = { + **item_axis_values, + **restored_axis_values, + "restored_prompt_axes": list(restored_axis_values.keys()), + "restored_prompt_details": restored_details, + } + item_text = _append_restored_prompt_details(item_text, restored_details) if is_pose_category: item_text = sanitize_hardcore_environment_anchors(item_text) item_axis_values = sanitize_hardcore_axis_values(item_axis_values) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index fd801f8..77b33cd 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -8442,6 +8442,63 @@ def smoke_pov_oral_position_routes() -> None: _expect("eye-level shot" not in top_prompt, f"Top-view variant prompt kept contradictory eye-level camera text: {top_prompt}") _expect("tongue extended toward genitals" not in top_prompt, f"Top-view variant prompt kept contradictory tongue-extension expression: {top_prompt}") + restored_top_config = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVPromptRestore"]().build( + True, + True, + True, + True, + True, + top_variant_config, + )[0] + restored_top_pair = pb.build_insta_of_pair( + row_number=1, + start_index=1, + seed=3828, + ethnicity="any", + figure="random", + no_plus_women=False, + no_black=False, + trigger=Trigger, + prepend_trigger_to_prompt=True, + options_json=_insta_options( + softcore_camera_mode="from_camera_config", + hardcore_camera_mode="from_camera_config", + camera_detail="compact", + ), + character_cast=_character_cast(pov_man=True), + hardcore_position_config=restored_top_config, + location_config=_coworking_location_config(), + hardcore_camera_config=_orbit_camera( + horizontal_angle=45, + vertical_angle=0, + zoom=7.5, + subject_focus="action", + ), + ) + restored_top_row = restored_top_pair.get("hardcore_row") or {} + restored_top_axis = restored_top_row.get("item_axis_values") or {} + restored_details = restored_top_axis.get("restored_prompt_details") or [] + _expect(isinstance(restored_details, list), "Krea2 POV Prompt Restore should keep restored prompt details as a list") + _expect(restored_details, "Krea2 POV Prompt Restore should add sampled restored axis details to row metadata") + restored_top_krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(restored_top_pair), target="hardcore") + restored_top_prompt = _expect_text("pov_oral_top_view_restore.krea_prompt", restored_top_krea.get("krea_prompt"), 60).lower() + _expect( + restored_top_prompt != top_prompt, + "Krea2 POV Prompt Restore should visibly change the final Krea prompt, not only metadata", + ) + _expect( + "nadir-angle standing male pov top-view oral position" in restored_top_prompt, + "Krea2 POV Prompt Restore should preserve the exact atlas pose while adding detail", + ) + _expect( + "eye-level" not in restored_top_prompt, + f"Krea2 POV Prompt Restore should not reintroduce contradictory eye-level camera wording: {restored_top_prompt}", + ) + _expect( + any(str(detail).lower() in restored_top_prompt for detail in restored_details), + f"Krea2 POV Prompt Restore final prompt did not include sampled restored details: {restored_top_prompt}", + ) + sitting_variant_config = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVOralFilter"]().build( "replace", "",