diff --git a/docs/krea2-eval-log.json b/docs/krea2-eval-log.json index 3dc0d69..287676c 100644 --- a/docs/krea2-eval-log.json +++ b/docs/krea2-eval-log.json @@ -1,6 +1,7 @@ { "version": 1, "purpose": "Structured fixed-seed Krea2 prompt/image evidence for SxCP atlas pose variants.", + "artifact_policy": "Image paths are external ComfyUI artifacts and may be cleaned; seed, summaries, observation, decision, and commit are the durable record.", "entries": [ { "id": "doggy-52-climax-target-structural", diff --git a/docs/krea2-pov-pose-atlas.md b/docs/krea2-pov-pose-atlas.md index 2dd174c..f12fec2 100644 --- a/docs/krea2-pov-pose-atlas.md +++ b/docs/krea2-pov-pose-atlas.md @@ -19,7 +19,9 @@ 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. +the existing Position Pool / Action Filter / Insta-OF chain. Pair it with +`SxCP Krea2 Variant Evidence` to display the fixed-seed eval entry, image paths, +and generator decision behind that variant. ## Inventory diff --git a/docs/sxcp-eval-loop.md b/docs/sxcp-eval-loop.md index cc981f0..8a69115 100644 --- a/docs/sxcp-eval-loop.md +++ b/docs/sxcp-eval-loop.md @@ -39,7 +39,9 @@ Runtime logs are written under `.sxcp_eval/` and ignored by git. Durable fixed-seed findings that justify a guide rule, generator patch, or pose variant promotion are recorded in [`krea2-eval-log.json`](krea2-eval-log.json). Use runtime logs for scratch notes; use the JSON log only for evidence that -should remain tied to a catalog variant. +should remain tied to a catalog variant. Image paths in that log point at +external ComfyUI artifacts and may be cleaned; the durable evidence is the fixed +seed, prompt summaries, observation, decision, and commit. ## Optional Command Hook diff --git a/node_hardcore_position.py b/node_hardcore_position.py index 8a539e8..a2ef6bf 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_eval_log from . import krea2_pose_variant_catalog from .hardcore_position_config import ( build_hardcore_action_filter_json, @@ -12,6 +13,7 @@ try: hardcore_position_key_choices, ) except ImportError: # Allows local smoke tests from the repository root. + import krea2_eval_log import krea2_pose_variant_catalog from hardcore_position_config import ( build_hardcore_action_filter_json, @@ -140,6 +142,59 @@ class SxCPKrea2PoseVariant: ) +class SxCPKrea2VariantEvidence: + @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"}), + "result": (["accepted", "rejected", "inconclusive", "any"], {"default": "accepted"}), + }, + "optional": { + "variant_key_in": ("STRING", {"default": ""}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "INT", "STRING") + RETURN_NAMES = ( + "summary", + "baseline_image_path", + "candidate_image_path", + "evidence_json", + "seed", + "decision", + ) + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build(self, variant_key, result="accepted", variant_key_in=""): + key = str(variant_key_in or variant_key or "").strip() + result_filter = None if result == "any" else result + entries = krea2_eval_log.entries_for_variant(key, result=result_filter) + if not entries: + empty = { + "variant_key": key, + "result": result, + "summary": "no Krea2 eval evidence found", + } + return empty["summary"], "", "", json.dumps(empty, ensure_ascii=True, sort_keys=True), -1, "" + entry = entries[0] + summary = ( + f"evidence={entry.get('id')}; variant={entry.get('variant_key')}; " + f"seed={entry.get('seed')}; result={entry.get('result')}; decision={entry.get('decision')}" + ) + seed = entry.get("seed") + return ( + summary, + str(entry.get("baseline_image") or ""), + str(entry.get("candidate_image") or ""), + json.dumps(entry, ensure_ascii=True, sort_keys=True), + int(seed) if isinstance(seed, int) else -1, + str(entry.get("decision") or ""), + ) + + class SxCPHardcoreActionFilter: @classmethod def INPUT_TYPES(cls): @@ -203,10 +258,12 @@ NODE_CLASS_MAPPINGS = { "SxCPHardcorePositionPool": SxCPHardcorePositionPool, "SxCPHardcoreActionFilter": SxCPHardcoreActionFilter, "SxCPKrea2PoseVariant": SxCPKrea2PoseVariant, + "SxCPKrea2VariantEvidence": SxCPKrea2VariantEvidence, } NODE_DISPLAY_NAME_MAPPINGS = { "SxCPHardcorePositionPool": "SxCP Hardcore Position Pool", "SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter", "SxCPKrea2PoseVariant": "SxCP Krea2 Pose Variant", + "SxCPKrea2VariantEvidence": "SxCP Krea2 Variant Evidence", } diff --git a/node_tooltips.py b/node_tooltips.py index 2fb60e0..9670dc4 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -327,6 +327,11 @@ 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.", }, + "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.", + "variant_key_in": "Optional connected variant key from SxCP Krea2 Pose Variant. When connected, it overrides the selector.", + }, "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 00205b8..964bfa8 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -6807,6 +6807,7 @@ def smoke_krea2_pose_variant_catalog_policy() -> None: def smoke_krea2_eval_log_policy() -> None: log = krea2_eval_log.load_eval_log() _expect(log.get("version") == 1, "Krea2 eval log version changed unexpectedly") + _expect("external ComfyUI artifacts" in str(log.get("artifact_policy") or ""), "Krea2 eval log should document external artifact policy") entries = krea2_eval_log.entries() _expect(entries, "Krea2 eval log has no entries") catalog_keys = set(krea2_pose_variant_catalog.variant_keys()) @@ -6833,7 +6834,7 @@ def smoke_krea2_eval_log_policy() -> None: image_path = str(entry.get(image_key) or "") if image_path: _expect(Path(image_path).is_absolute(), f"{entry_id}.{image_key} should be absolute when present") - _expect(Path(image_path).is_file(), f"{entry_id}.{image_key} is missing: {image_path}") + _expect(Path(image_path).suffix.lower() == ".png", f"{entry_id}.{image_key} should reference a PNG artifact") boobjob_entries = krea2_eval_log.entries_for_variant("pov_boobjob_upright_cleavage", result="accepted") _expect(boobjob_entries and boobjob_entries[0].get("seed") == 7302, "Boobjob accepted eval evidence changed") mutation = krea2_eval_log.entries_for_variant("pov_handjob_upright_centered")[0] @@ -8870,6 +8871,7 @@ def smoke_node_hardcore_position_registration() -> None: "SxCPHardcorePositionPool", "SxCPHardcoreActionFilter", "SxCPKrea2PoseVariant", + "SxCPKrea2VariantEvidence", ] for node_name in required_nodes: _expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry") @@ -8931,6 +8933,29 @@ def smoke_node_hardcore_position_registration() -> None: _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") + evidence_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2VariantEvidence"] + evidence_inputs = evidence_node.INPUT_TYPES().get("required") or {} + _expect("variant_key" in evidence_inputs, "Krea2 Variant Evidence lost variant selector") + _expect("tooltip" in evidence_inputs["variant_key"][1], "Krea2 Variant Evidence tooltip injection missing") + ( + evidence_summary, + baseline_image, + candidate_image, + evidence_json, + evidence_seed, + evidence_decision, + ) = evidence_node().build( + "pov_boobjob_upright_cleavage", + "accepted", + "", + ) + parsed_evidence = json.loads(evidence_json) + _expect(evidence_seed == 7302, "Krea2 Variant Evidence returned wrong fixed seed") + _expect(evidence_decision == "generator_patch", "Krea2 Variant Evidence returned wrong decision") + _expect("boobjob-7302" in evidence_summary, "Krea2 Variant Evidence summary lost entry id") + _expect(baseline_image.endswith(".png") and candidate_image.endswith(".png"), "Krea2 Variant Evidence lost image paths") + _expect(parsed_evidence.get("variant_key") == "pov_boobjob_upright_cleavage", "Krea2 Variant Evidence returned wrong JSON") + def smoke_node_formatter_registration() -> None: required_nodes = [