Files
Kevin Gilper 7ce90da3a9 Run Stage 1 live: mechanism confirmed, taps show no attribute specialization
Installed V10 + the probe node on the render box, generated a controlled
reference set, and ran the encode-only selectivity harness on the real Krea 2
model. First actual measurement of the 12 text-layer taps:

- Mechanism confirmed live: probe reports feature width 30720 (= 12 x 2560),
  divisible by 12, so the tap split engages on the model (no fallback). Tap
  magnitude rises with depth.
- No strong per-tap specialization: after removing the depth-magnitude
  confound, every tap carries essentially the same attribute mix (palette
  0.20 / structure 0.33 / texture 0.35 / lighting 0.12; cross-tap std-dev
  ~0.01). Taps differ by overall magnitude (depth), not by which attribute
  they encode - only a faint depth gradient in the design direction, far too
  weak to justify the tables' 5x spikes. The "tap 8 = palette" premise is not
  supported.

Caveats recorded honestly (seq-mean signature, weak palette axis from a
near-neutral base, single scene). README Stage 1 updated with the result;
raw signatures + full write-up in local_records. Also fixes the probe graph
generator to supply the V10 card's full widget set (API validation).
2026-07-03 07:24:09 -04:00

166 lines
7.9 KiB
Python

"""Build the encode-only graphs that feed the layer-selectivity probe (Stage 1).
For each controlled reference image (plus a prompt-only baseline) this emits a
ComfyUI API graph that: loads the image, runs it through a V10 guide card at
NEUTRAL settings (balanced role, strength 1.0, literal feel - so the stack's
output is the plain encode of prompt+reference, not a recipe-shaped delta),
and routes the conditioning into the KG Conditioning Probe node, which saves
the per-tap signature. No UNET, no sampler, no VAE - nothing is rendered, so
this is cheap and touches only the text encoder.
Requires on the render box: the V10 nodes (kg_krea_v10) and the probe node
(probe_node/) installed, plus the controlled reference images in the ComfyUI
input folder.
Usage:
python generate_probe_graphs.py --dry-run # build + validate, write a sample manifest
python generate_probe_graphs.py --refs refs.json --server URL # encode on the render box
"""
import argparse
import json
import time
import urllib.parse
import urllib.request
from pathlib import Path
HERE = Path(__file__).resolve().parent
OUT_DIR = HERE / "probe_out"
CLIP = "krea2\\qwen3vl_4b_fp8_scaled.safetensors"
PROMPT = "a product photograph on a plain seamless background, soft even light"
# A controlled design: vary ONE attribute at a time across levels, others held
# fixed. Replace the image paths with real controlled sets (see README 5).
SAMPLE_REFS = {
"attributes": ["palette", "structure", "texture", "lighting"],
"baseline": "baseline",
"references": {
"palette_0": {"image": "layerprobe/palette_0.png", "palette": 0, "structure": 0, "texture": 0, "lighting": 0},
"palette_1": {"image": "layerprobe/palette_1.png", "palette": 1, "structure": 0, "texture": 0, "lighting": 0},
"palette_2": {"image": "layerprobe/palette_2.png", "palette": 2, "structure": 0, "texture": 0, "lighting": 0},
"structure_1": {"image": "layerprobe/structure_1.png", "palette": 0, "structure": 1, "texture": 0, "lighting": 0},
"structure_2": {"image": "layerprobe/structure_2.png", "palette": 0, "structure": 2, "texture": 0, "lighting": 0},
"texture_1": {"image": "layerprobe/texture_1.png", "palette": 0, "structure": 0, "texture": 1, "lighting": 0},
"texture_2": {"image": "layerprobe/texture_2.png", "palette": 0, "structure": 0, "texture": 2, "lighting": 0},
"lighting_1": {"image": "layerprobe/lighting_1.png", "palette": 0, "structure": 0, "texture": 0, "lighting": 1},
"lighting_2": {"image": "layerprobe/lighting_2.png", "palette": 0, "structure": 0, "texture": 0, "lighting": 2},
},
}
STACK_NEUTRAL = {
"Written prompt strength": 1.0,
"Image slider feel": "literal slider values",
"Image detail level": "medium - balanced default (384)",
"Image framing": "keep full image shape",
"When images guide": "guide the whole image",
"Early-to-final handoff": 0.4,
"Text/logo guard prompt handling": "gentle guard - keep my prompt words",
"Balance strong cards": "off - use my values",
"Reuse image studies": "always re-study",
}
def stack_inputs(with_card):
inp = {"Krea CLIP": ["clip", 0], "Final image prompt": PROMPT}
inp.update(STACK_NEUTRAL)
if with_card:
inp["Reference 1 guide card"] = ["card", 0]
return inp
def build_graph(label, image):
"""Encode-only graph -> probe. image=None for the baseline."""
g = {
"clip": {"class_type": "CLIPLoader", "inputs": {"clip_name": CLIP, "type": "krea2", "device": "default"}},
"stack": {"class_type": "KGTextEncodeKreaImageReferencesV10", "inputs": stack_inputs(image is not None)},
"probe": {"class_type": "KGConditioningProbe", "inputs": {"conditioning": ["stack", 0], "label": label}},
}
if image is not None:
g["load"] = {"class_type": "LoadImage", "inputs": {"image": image, "upload": "image"}}
# The API format requires every required widget; balanced ignores the
# manual rows but ComfyUI still validates their presence. Neutral card:
# strength 1.0 + literal feel -> stack output is the plain encode.
g["card"] = {"class_type": "KGKrea2ImageGuideCardV10", "inputs": {
"Reference image": ["load", 0],
"How strongly this image guides": 1.0,
"Use image for": "balanced",
"Manual mode borrows": "overall image",
"Prepare image by": "use image as-is",
"Color kept": 1.0,
"Small details kept": 1.0,
"Study this image at": "use stack setting",
"Frame this reference by": "use stack setting",
"Subject copying": "recipe decides",
"Early layout guidance": 1.0,
"Final detail copying": 1.0,
"Maximum image pull": 3.0,
"Shape copied": 1.0,
"Overall style reach": 1.0,
"Guide direction": "toward this image",
"When this card guides": "recipe decides",
"Structure layers pull": 1.0,
"Finish layers pull": 1.0,
}}
return g
def post_and_wait(server, graph, timeout=180):
payload = json.dumps({"prompt": graph}).encode("utf-8")
req = urllib.request.Request(server + "/prompt", data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
pid = json.load(r)["prompt_id"]
start = time.time()
while time.time() - start < timeout:
with urllib.request.urlopen(server + "/history/" + pid, timeout=30) as r:
entry = json.load(r).get(pid)
if entry and (entry.get("outputs") or entry.get("status", {}).get("completed")):
return
if entry and entry.get("status", {}).get("status_str") == "error":
raise RuntimeError(json.dumps(entry["status"])[:800])
time.sleep(1.5)
raise TimeoutError(pid)
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--refs", help="controlled-reference manifest json (see SAMPLE_REFS)")
ap.add_argument("--server", help="ComfyUI base URL (V10 + probe node installed)")
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
OUT_DIR.mkdir(parents=True, exist_ok=True)
refs = json.loads(Path(args.refs).read_text(encoding="utf-8")) if args.refs else SAMPLE_REFS
plan = [("baseline", None)] + [(lbl, r["image"]) for lbl, r in refs["references"].items()]
graphs = {lbl: build_graph(lbl, img) for lbl, img in plan}
# structural validation
for lbl, g in graphs.items():
assert g["probe"]["inputs"]["conditioning"] == ["stack", 0]
assert g["probe"]["inputs"]["label"] == lbl
if args.dry_run or not args.server:
(OUT_DIR / "sample-refs.json").write_text(json.dumps(SAMPLE_REFS, indent=2) + "\n", encoding="utf-8")
(OUT_DIR / "probe-manifest.json").write_text(json.dumps({
"attributes": refs["attributes"], "baseline": "baseline",
"references": {k: {a: v[a] for a in refs["attributes"]} for k, v in refs["references"].items()},
}, indent=2) + "\n", encoding="utf-8")
print("dry run OK: {} encode graphs (1 baseline + {} references) validated".format(
len(graphs), len(graphs) - 1))
print("wrote sample-refs.json + probe-manifest.json to", OUT_DIR)
print("to run: install V10 + probe_node on the render box, put controlled images in")
print("ComfyUI/input/layerprobe/, then rerun with --refs <yours> --server http://HOST:8188")
return 0
server = args.server.rstrip("/")
for lbl, img in plan:
print("encoding", lbl, flush=True)
post_and_wait(server, graphs[lbl])
print("done - probe signatures saved under output/claude-generations/kg-selectivity-probe/ on the box")
print("copy them back, then: python probe_selectivity.py --probes <dir> --manifest probe_out/probe-manifest.json")
return 0
if __name__ == "__main__":
raise SystemExit(main())