From 5f4dd7d77f0b9f3b01ea22437913f44e53cf5d58 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Tue, 30 Jun 2026 20:12:09 +0200 Subject: [PATCH] Tighten Krea2 POV selector formatting --- krea_configured_cast_formatter.py | 49 +++++++++++++++++++++- krea_pair_formatter.py | 54 +++++++++++++++++++++++- node_hardcore_position.py | 24 +++++------ prompt_builder.py | 18 +++++++- tools/prompt_smoke.py | 70 ++++++++++++++++++++++++++++++- 5 files changed, 194 insertions(+), 21 deletions(-) diff --git a/krea_configured_cast_formatter.py b/krea_configured_cast_formatter.py index f2d01ea..17d8694 100644 --- a/krea_configured_cast_formatter.py +++ b/krea_configured_cast_formatter.py @@ -4,6 +4,10 @@ from dataclasses import dataclass from typing import Any, Callable +MOUTH_EXPRESSION_TERMS = ("mouth", "oral", "tongue", "lips", "gagging", "saliva", "drool") +TOP_VIEW_ORAL_VARIANT = "pov_blowjob_top_down_vertical_shaft" + + @dataclass(frozen=True) class KreaConfiguredCastRequest: row: dict[str, Any] @@ -76,6 +80,45 @@ def _coworking_action_anchor(action_family: str, scene_text: str, action: str) - ) +def _list_values(value: Any) -> list[str]: + if isinstance(value, list): + return [str(item) for item in value if str(item).strip()] + if isinstance(value, str) and value.strip(): + return [part.strip() for part in value.split(",") if part.strip()] + return [] + + +def _krea2_variant_keys(row: dict[str, Any]) -> list[str]: + config = row.get("hardcore_position_config") if isinstance(row.get("hardcore_position_config"), dict) else {} + axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {} + return list(dict.fromkeys([*_list_values(config.get("krea2_variant_keys")), *_list_values(axis_values.get("krea2_variant_keys"))])) + + +def _has_krea2_variant(row: dict[str, Any], key: str) -> bool: + return key in _krea2_variant_keys(row) + + +def _filter_expression_for_krea2_variant(row: dict[str, Any], expression: Any) -> Any: + if not _has_krea2_variant(row, TOP_VIEW_ORAL_VARIANT): + return expression + clauses = [clause.strip() for clause in str(expression or "").split(";") if clause.strip()] + if not clauses: + return expression + kept = [ + clause + for clause in clauses + if not any(term in clause.lower() for term in MOUTH_EXPRESSION_TERMS) + ] + return "; ".join(kept) + + +def _filter_camera_scene_for_krea2_variant(row: dict[str, Any], camera_scene: Any) -> str: + text = str(camera_scene or "") + if _has_krea2_variant(row, TOP_VIEW_ORAL_VARIANT) and "eye-level" in text.lower(): + return "" + return text + + def format_configured_cast_result( request: KreaConfiguredCastRequest, deps: KreaConfiguredCastDependencies, @@ -97,7 +140,8 @@ def format_configured_cast_result( if not cast_labels and women_count == 1 and men_count == 1: cast_labels = ["Woman A", "Man A"] cast_labels = deps.merge_labels(cast_labels, pov_labels) - expression = deps.filter_pov_labeled_clauses(request.expression, pov_labels) + expression = _filter_expression_for_krea2_variant(row, request.expression) + expression = deps.filter_pov_labeled_clauses(expression, pov_labels) expression = deps.natural_label_text(expression, cast_labels) composition = deps.sanitize_hardcore_environment_anchors(request.composition) source_composition = deps.sanitize_hardcore_environment_anchors(request.source_composition) @@ -136,6 +180,7 @@ def format_configured_cast_result( " ".join(part for part in (request.scene, request.camera_scene, composition, source_composition) if part), action, ) + camera_scene = _filter_camera_scene_for_krea2_variant(row, request.camera_scene) output_composition = deps.pov_composition_text(composition, pov_labels) parts = [ action, @@ -145,7 +190,7 @@ def format_configured_cast_result( f"A consensual explicit adult scene with {subject}" if not action else "", f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", f"The setting is {request.scene}" if request.scene else "", - request.camera_scene, + camera_scene, deps.expression_phrase(expression), deps.composition_phrase(output_composition, action, "The image is framed as", detail_density), camera, diff --git a/krea_pair_formatter.py b/krea_pair_formatter.py index 46f8835..0ec254b 100644 --- a/krea_pair_formatter.py +++ b/krea_pair_formatter.py @@ -4,6 +4,10 @@ from dataclasses import dataclass from typing import Any, Callable +MOUTH_EXPRESSION_TERMS = ("mouth", "oral", "tongue", "lips", "gagging", "saliva", "drool") +TOP_VIEW_ORAL_VARIANT = "pov_blowjob_top_down_vertical_shaft" + + @dataclass(frozen=True) class KreaPairFormatRequest: row: dict[str, Any] @@ -54,6 +58,45 @@ class KreaPairFormatDependencies: combine_negative: Callable[..., str] +def _list_values(value: Any) -> list[str]: + if isinstance(value, list): + return [str(item) for item in value if str(item).strip()] + if isinstance(value, str) and value.strip(): + return [part.strip() for part in value.split(",") if part.strip()] + return [] + + +def _krea2_variant_keys(row: dict[str, Any]) -> list[str]: + config = row.get("hardcore_position_config") if isinstance(row.get("hardcore_position_config"), dict) else {} + axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {} + return list(dict.fromkeys([*_list_values(config.get("krea2_variant_keys")), *_list_values(axis_values.get("krea2_variant_keys"))])) + + +def _has_krea2_variant(row: dict[str, Any], key: str) -> bool: + return key in _krea2_variant_keys(row) + + +def _filter_expression_for_krea2_variant(row: dict[str, Any], expression: Any) -> Any: + if not _has_krea2_variant(row, TOP_VIEW_ORAL_VARIANT): + return expression + clauses = [clause.strip() for clause in str(expression or "").split(";") if clause.strip()] + if not clauses: + return expression + kept = [ + clause + for clause in clauses + if not any(term in clause.lower() for term in MOUTH_EXPRESSION_TERMS) + ] + return "; ".join(kept) + + +def _filter_camera_scene_for_krea2_variant(row: dict[str, Any], camera_scene: Any) -> str: + text = str(camera_scene or "") + if _has_krea2_variant(row, TOP_VIEW_ORAL_VARIANT) and "eye-level" in text.lower(): + return "" + return text + + def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairFormatDependencies) -> KreaPairPrompts: row = request.row detail_level = request.detail_level @@ -70,7 +113,10 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma soft_camera = deps.pair_camera_phrase(row.get("softcore_camera_directive"), row.get("softcore_camera_config"), soft) hard_camera = deps.pair_camera_phrase(row.get("hardcore_camera_directive"), row.get("hardcore_camera_config"), hard) soft_camera_scene = deps.camera_scene_phrase(soft) or deps.clean(row.get("softcore_camera_scene_directive")) - hard_camera_scene = deps.camera_scene_phrase(hard) or deps.clean(row.get("hardcore_camera_scene_directive")) + hard_camera_scene = _filter_camera_scene_for_krea2_variant( + hard, + deps.camera_scene_phrase(hard) or deps.clean(row.get("hardcore_camera_scene_directive")), + ) soft_style = deps.style_phrase(soft, style_mode) hard_style = deps.style_phrase(hard, style_mode) options = row.get("options") if isinstance(row.get("options"), dict) else {} @@ -166,8 +212,12 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma ) hard_expression = "" if not deps.expression_disabled(hard): - hard_expression_source = deps.filter_pov_labeled_clauses( + hard_expression_source = _filter_expression_for_krea2_variant( + hard, deps.clean(hard.get("character_expression_text")) or deps.clean(hard.get("expression")), + ) + hard_expression_source = deps.filter_pov_labeled_clauses( + hard_expression_source, pov_labels, ) hard_expression = deps.natural_label_text( diff --git a/node_hardcore_position.py b/node_hardcore_position.py index cc63bf5..15fcb28 100644 --- a/node_hardcore_position.py +++ b/node_hardcore_position.py @@ -131,17 +131,6 @@ def _merge_variant_metadata(config_json, variants): existing_statuses = config.get("krea2_variant_statuses") if isinstance(config.get("krea2_variant_statuses"), dict) else {} config["krea2_variant_statuses"] = {**existing_statuses, **selected_statuses} - prompt_cues = _unique_extend( - [*(config.get("krea2_prompt_cues") or []), *(_join_variant_cues(variants, "prompt_cues").split("; ") if variants else [])] - ) - avoid_cues = _unique_extend( - [*(config.get("krea2_avoid_cues") or []), *(_join_variant_cues(variants, "avoid_cues").split("; ") if variants else [])] - ) - if prompt_cues: - config["krea2_prompt_cues"] = prompt_cues - if avoid_cues: - config["krea2_avoid_cues"] = avoid_cues - base_summary = str(config.get("summary") or hardcore_position_summary(config)) if variant_keys and "variants=" not in base_summary: base_summary = f"{base_summary}; variants={','.join(variant_keys)}" @@ -149,6 +138,14 @@ def _merge_variant_metadata(config_json, variants): return json.dumps(config, ensure_ascii=True, sort_keys=True) +def _variant_notes(variants): + return "; ".join( + f"{variant.get('key')} ({variant.get('status') or 'unknown'})" + for variant in variants + if variant.get("key") + ) + + class SxCPHardcorePositionPool: @classmethod def INPUT_TYPES(cls): @@ -268,7 +265,7 @@ class _SxCPKrea2POVVariantFilter: "hardcore_position_config", "selected_variant_keys", "selected_positions", - "prompt_cues", + "selected_variant_notes", "summary", "variants_json", ) @@ -297,12 +294,11 @@ class _SxCPKrea2POVVariantFilter: parsed = json.loads(config) selected_keys = parsed.get("krea2_variant_keys") or [] selected_positions = parsed.get("positions") or [] - prompt_cues = _join_variant_cues(variants, "prompt_cues") return ( config, ",".join(str(key) for key in selected_keys), ",".join(str(position) for position in selected_positions), - prompt_cues, + _variant_notes(variants), str(parsed.get("summary") or ""), json.dumps(variants, ensure_ascii=True, sort_keys=True), ) diff --git a/prompt_builder.py b/prompt_builder.py index 04bb3a2..b0c4dc7 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -705,6 +705,18 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str: return hardcore_position_policy.hardcore_position_summary(config) +def _axis_values_with_krea2_variant_keys( + axis_values: dict[str, Any], + hardcore_position_config: dict[str, Any], +) -> dict[str, Any]: + variant_keys = hardcore_position_config.get("krea2_variant_keys") if isinstance(hardcore_position_config, dict) else [] + if not isinstance(variant_keys, list) or not variant_keys: + return axis_values + merged = dict(axis_values) + merged["krea2_variant_keys"] = [str(key) for key in variant_keys if str(key).strip()] + return merged + + def build_hardcore_position_pool_json( hardcore_position_config: str | dict[str, Any] | None = "", combine_mode: str = "replace", @@ -2459,6 +2471,10 @@ def _build_custom_row( action_family = action_route.action_family text_fields = _row_text_fields(category, subcategory, item, style_config) + formatter_axis_values = _axis_values_with_krea2_variant_keys( + item_axis_values, + parsed_hardcore_position_config, + ) assembly_request = row_assembly_policy.CustomRowAssemblyRequest( row_number=row_number, @@ -2470,7 +2486,7 @@ def _build_custom_row( subject_type=subject_type, item_text=item_text, item_name=item_name, - item_axis_values=item_axis_values, + item_axis_values=formatter_axis_values, item_template_metadata=item_template_metadata, formatter_hints=item_formatter_hints, item_label=text_fields.item_label, diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 4be872f..a19ad94 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -8288,6 +8288,54 @@ def smoke_pov_oral_position_routes() -> None: for term in krea_terms: _expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}") + top_variant_config = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVOralFilter"]().build( + "replace", + "", + include_blowjob_top_down_vertical_shaft=True, + )[0] + top_variant_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=top_variant_config, + location_config=_coworking_location_config(), + hardcore_camera_config=_orbit_camera( + horizontal_angle=45, + vertical_angle=0, + zoom=7.5, + subject_focus="action", + ), + ) + _expect_pair(top_variant_pair, "pov_oral_top_view_variant_filter") + top_variant_row = top_variant_pair.get("hardcore_row") or {} + top_variant_config_row = top_variant_row.get("hardcore_position_config") or {} + _expect( + top_variant_config_row.get("krea2_variant_keys") == ["pov_blowjob_top_down_vertical_shaft"], + "Top-view POV filter lost exact variant key in row metadata", + ) + top_axis_values = top_variant_row.get("item_axis_values") or {} + _expect( + top_axis_values.get("krea2_variant_keys") == ["pov_blowjob_top_down_vertical_shaft"], + "Top-view POV filter exact variant key did not reach formatter axis metadata", + ) + top_krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(top_variant_pair), target="hardcore") + top_prompt = _expect_text("pov_oral_top_view_variant_filter.krea_prompt", top_krea.get("krea_prompt"), 60).lower() + _expect("nadir-angle standing male pov top-view oral position" in top_prompt, "Top-view variant prompt lost exact top-view route") + _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}") + side_body_item = "side-lying oral position while blowjob with lips wrapped around the viewer's penis" side_body_axis = {"position": "side-lying oral position"} side_body_role_graph = hardcore_role_oral.build_oral_role_graph( @@ -10943,7 +10991,25 @@ def smoke_node_hardcore_position_registration() -> None: "include_doggy_top_down_rear_entry" not in oral_inputs, "POV Oral Filter should not expose penetration atlas checkboxes", ) - doggy_config, doggy_keys, doggy_positions, doggy_cues, doggy_summary, doggy_variants_json = penetration_filter().build( + top_config, top_keys, top_positions, top_notes, top_summary, _top_variants_json = oral_filter().build( + "replace", + "", + include_blowjob_top_down_vertical_shaft=True, + ) + parsed_top_config = json.loads(top_config) + _expect(top_keys == "pov_blowjob_top_down_vertical_shaft", "POV Oral Filter returned wrong exact top-view variant key") + _expect( + parsed_top_config.get("krea2_variant_keys") == ["pov_blowjob_top_down_vertical_shaft"], + "POV Oral Filter lost exact top-view variant metadata", + ) + _expect("kneeling" in top_positions, "POV Oral Filter lost top-view route position") + _expect( + "nadir-angle" not in top_notes and "viewer looks almost straight down" not in top_notes, + "POV Oral Filter should not emit raw atlas prompt prose as a connectable text output", + ) + _expect("variants=pov_blowjob_top_down_vertical_shaft" in top_summary, "POV Oral Filter summary lost top-view variant key") + + doggy_config, doggy_keys, doggy_positions, doggy_notes, doggy_summary, doggy_variants_json = penetration_filter().build( "replace", "", include_doggy_top_down_rear_entry=True, @@ -10957,7 +11023,7 @@ def smoke_node_hardcore_position_registration() -> None: ) _expect(doggy_keys == "pov_doggy_top_down_rear_entry", "POV Penetration Filter returned wrong selected variant keys") _expect(doggy_positions == "doggy", "POV Penetration Filter returned wrong selected positions") - _expect("top-down" in doggy_cues, "POV Penetration Filter lost prompt cues output") + _expect("pov_doggy_top_down_rear_entry" in doggy_notes and "top-down POV" not in doggy_notes, "POV Penetration Filter should emit compact variant notes, not raw prompt cues") _expect("variants=pov_doggy_top_down_rear_entry" in doggy_summary, "POV Penetration Filter summary lost selected variant") _expect(json.loads(doggy_variants_json)[0].get("key") == "pov_doggy_top_down_rear_entry", "POV Penetration Filter returned wrong variant JSON")