feat: block-influence recording is on by default; four-group comparison with shuffled-label control, and tools/taste_groups.py to read it

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-10 15:15:41 +03:00
co-authored by Claude Haiku 5.5
parent f313ed6ef6
commit 49db27aec8
5 changed files with 158 additions and 3 deletions
+10
View File
@@ -168,6 +168,16 @@ def _quarantine_in_tmp(tmp_path, monkeypatch):
monkeypatch.setattr(config, "QUARANTINE_FILE", tmp_path / "quarantine.json")
@pytest.fixture(autouse=True)
def _block_influence_off_in_tmp(tmp_path, monkeypatch):
"""Recording is on by default, so a test that rates a clip would also pick up its block-influence
capture. Off here; a test that wants it on turns it on itself."""
from modules.sampling.block_influence import measure
switch = tmp_path / "block_influence.enabled"
switch.write_text("0")
monkeypatch.setattr(measure, "SWITCH", switch)
class TinyLTX:
"""A REAL LTX audio+video model -- ComfyUI's own LTXAVModel, four blocks wide as a thumb.
+65 -2
View File
@@ -14,10 +14,11 @@ problem = None
def enabled() -> bool:
# On until someone turns it off: with no switch file yet (a fresh rental) recording runs.
try:
return SWITCH.read_text().strip() == "1"
return SWITCH.read_text().strip() != "0"
except OSError:
return False
return True
def set_enabled(on: bool) -> bool:
@@ -142,6 +143,68 @@ def profile(rows) -> dict:
"flatness": flat, "n_liked": int(liked.sum()), "n_disliked": int(disliked.sum())}
GROUPS = ("liked", "image", "composition", "both")
def _group(row):
"""liked; or a dislike sorted by its axis: image, composition, or both (plain dislike)."""
reward = float(row["reward"])
if reward > 0:
return "liked"
if reward == 0:
return None
return row.get("axis") if row.get("axis") in ("image", "composition") else "both"
def groups(rows, shuffles=200, seed=0) -> dict:
"""Do the four groups point in different directions over the block profile?
For each dislike group, `d = mean(group) - mean(liked)`; the answer is the cosine between
those directions. The control reshuffles the group labels over the same clips (same group
sizes) and asks how often that cosine comes out as high. A real difference should sit
above the shuffled range; inside it, the ratings are not telling the groups apart yet.
"""
rows = [r for r in rows if "ratio" in r["rows"]]
if not rows:
return {"used": 0, "counts": {}, "cos": {}, "note": "nothing recorded yet"}
n = len(rows[-1]["rows"]["ratio"])
rows = [r for r in rows if len(r["rows"]["ratio"]) == n]
stack = torch.stack([r["rows"]["ratio"].float() for r in rows])
stack = torch.where(torch.isfinite(stack), stack, torch.nanmean(stack, dim=0).nan_to_num(0.0))
labels = [_group(r) for r in rows]
counts = {g: labels.count(g) for g in GROUPS}
def directions(labs):
means = {}
for g in GROUPS:
idx = [i for i, x in enumerate(labs) if x == g]
if len(idx) >= MIN_PER_GROUP:
means[g] = stack[idx].mean(dim=0)
if "liked" not in means:
return {}
return {g: means[g] - means["liked"] for g in ("image", "composition", "both") if g in means}
def cosines(d):
pairs = {}
names = sorted(d)
for i, a in enumerate(names):
for b in names[i + 1:]:
pairs[f"{a} vs {b}"] = float(torch.nn.functional.cosine_similarity(d[a], d[b], dim=0))
return pairs
observed = cosines(directions(labels))
if not observed:
return {"used": len(rows), "counts": counts, "cos": {}, "note": "not enough clips in each group yet"}
gen = torch.Generator().manual_seed(seed)
null = {k: [] for k in observed}
for _ in range(shuffles):
perm = torch.randperm(len(labels), generator=gen).tolist()
for k, v in cosines(directions([labels[i] for i in perm])).items():
null[k].append(v)
chance = {k: {"mean": sum(v) / len(v), "p95": sorted(v)[int(0.95 * (len(v) - 1))]} for k, v in null.items() if v}
return {"used": len(rows), "counts": counts, "cos": observed, "chance": chance}
def _kind(key):
kind = registry.current().ask("taste_kind", key, KIND)
if kind is None:
@@ -26,9 +26,11 @@ def _load(tiny, key="fox"):
return FunPackLoadModifiers.execute(tiny.patcher, settings).result
def test_off_by_default_it_stores_nothing_changes_nothing_and_says_so(tiny_h3):
def test_switched_off_it_stores_nothing_changes_nothing_and_says_so(tiny_h3):
from core import log
from modules.sampling.block_influence import measure
from modules.system.taste import store
measure.set_enabled(False)
base = tiny_h3.run()
patched, _ = _load(tiny_h3)
log.new_run()
@@ -199,6 +201,7 @@ def test_a_bad_key_is_refused_before_the_switch_flips_and_a_placeholder_finds_th
async def json(self):
return self._body
measure.set_enabled(False)
out = asyncio.run(handlers[("POST", "/b/enabled")](Req({"enabled": True, "key": "no/slash"})))
assert out.status == 400 and not measure.enabled()
patched, _ = _load(tiny_h3, key="mystyle")
@@ -0,0 +1,46 @@
"""The four-group comparison and the recording default, on synthetic rows (no model needed)."""
import torch
from modules.sampling.block_influence import measure
def _row(reward, axis, profile):
return {"reward": reward, "axis": axis, "rows": {"ratio": torch.tensor(profile, dtype=torch.float32)}}
def test_recording_is_on_when_no_switch_file_exists(tmp_path, monkeypatch):
monkeypatch.setattr(measure, "SWITCH", tmp_path / "switch")
assert measure.enabled() is True
measure.set_enabled(False)
assert measure.enabled() is False
measure.set_enabled(True)
assert measure.enabled() is True
def test_groups_sort_dislikes_by_axis():
assert measure._group(_row(1.0, None, [0])) == "liked"
assert measure._group(_row(-1.0, "image", [0])) == "image"
assert measure._group(_row(-1.0, "composition", [0])) == "composition"
assert measure._group(_row(-1.0, None, [0])) == "both"
assert measure._group(_row(0.0, None, [0])) is None
def test_opposite_directions_show_as_opposed_cosines():
rows = [_row(1, None, [1, 0, 0]) for _ in range(3)] + [_row(-1, "image", [0, 1, 0]) for _ in range(3)] \
+ [_row(-1, "composition", [0, 0, 1]) for _ in range(3)] + [_row(-1, None, [0, 0, 0]) for _ in range(3)]
out = measure.groups(rows, shuffles=50)
assert out["counts"] == {"liked": 3, "image": 3, "composition": 3, "both": 3}
# Each dislike is "liked minus a different block": the two directions share the "away from liked"
# part (the -1 on block 0), so they agree half way, not fully and not at all.
assert abs(out["cos"]["composition vs image"] - 0.5) < 1e-5
assert set(out["chance"]) == set(out["cos"])
def test_too_few_clips_says_so_instead_of_comparing():
out = measure.groups([_row(1, None, [1, 0]), _row(-1, "image", [0, 1])], shuffles=5)
assert out["cos"] == {} and "not enough" in out["note"]
def test_nothing_recorded_is_said():
assert measure.groups([])["note"] == "nothing recorded yet"
+33
View File
@@ -0,0 +1,33 @@
"""Print the four-group comparison for a Taste key's block-influence recordings.
python tools/taste_groups.py <taste key>
Reads the rows the recording wrote (liked, bad image, bad composition, both bad) and prints how
the group directions line up, next to what shuffled labels give by chance. Reads only; changes nothing.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
def main(key: str) -> None:
from modules.sampling.block_influence import measure
from modules.system.taste import store
rows = store.load(key, measure.KIND)["rows"]
out = measure.groups(rows)
print(f"key {key!r}: {out['used']} clips rated")
print("counts:", out.get("counts", {}))
if out.get("note"):
print(out["note"])
for pair, cos in out.get("cos", {}).items():
c = out["chance"][pair]
print(f"{pair}: {cos:+.3f} (chance: mean {c['mean']:+.3f}, 95th {c['p95']:+.3f})")
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(__doc__)
main(sys.argv[1])