feat: block influence's four-group report lives in Settings > Refinement & Taste, with its recording switch; the script goes

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-10 15:19:03 +03:00
co-authored by Claude Haiku 5.5
parent 49db27aec8
commit cdadf0d4ba
7 changed files with 110 additions and 35 deletions
+34 -2
View File
@@ -45,6 +45,38 @@ export const taste = (app) => function mount() {
c.dropzone.default({ label: "Drop or choose an exported key (.zip)", hint: "from another machine", accept: ".zip,application/zip", multiple: false, onFiles: bring }),
].filter(Boolean));
}
draw(); refresh();
return { node: page.node, destroy: () => page.node.remove() };
// Block influence: research recording, read here. Its numbers say whether the four groups point apart; no clip changes.
const research = c.region.stack({ gap: "sm" });
let influence = null;
const pct = (x) => (x >= 0 ? "+" : "") + x.toFixed(2);
async function refreshResearch() {
try {
const [state, four] = await Promise.all([app.api.blockInfluence("default"), app.api.blockInfluenceGroups("default")]);
influence = { state, four, error: "" };
} catch (err) { influence = { state: null, four: null, error: err.message }; }
drawResearch();
}
function drawResearch() {
const rows = [c.label.section({ text: "Block influence (research)" })];
if (influence && influence.error) rows.push(c.banner.warn({ text: `Could not read the recording: ${influence.error}` }));
if (influence && influence.state) {
const { state, four } = influence;
rows.push(c.toggle.default({ label: "Record block influence", hint: "On: every rated run on a Taste key is measured. Off: nothing is recorded.",
checked: !!state.enabled, onChange: async (on) => { try { await app.api.setBlockInfluence(on); } catch (err) { tell(err.message); } refreshResearch(); } }));
if (state.problem) rows.push(c.banner.warn({ text: state.problem }));
rows.push(c.settingsRow.default({ label: "Clips rated", hint: `Key: ${four.key || state.key || "none yet"}`,
control: c.hint.default({ text: four.used ? Object.entries(four.counts || {}).map(([g, n]) => `${g} ${n}`).join(", ") : "none yet" }) }));
if (four.note) rows.push(c.hint.default({ text: four.note }));
for (const [pair, cos] of Object.entries(four.cos || {})) {
const c0 = four.chance[pair];
rows.push(c.settingsRow.default({ label: pair, hint: `Chance from shuffled labels: mean ${pct(c0.mean)}, 95th ${pct(c0.p95)}`,
control: c.hint.default({ text: pct(cos) }) }));
}
}
research.set(rows);
}
draw(); refresh(); refreshResearch();
const node = document.createElement("div");
node.append(page.node, research.node);
return { node, destroy: () => node.remove() };
};
+44
View File
@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
import { setupDom, teardownDom } from "../../composer/tests/_dom.js";
let taste;
test.before(async () => { setupDom(); ({ taste } = await import("./taste.js")); });
test.after(() => teardownDom());
const tick = () => new Promise((r) => setTimeout(r, 0));
test("the panel shows block influence: the switch, the clip counts, and each pair against chance", async () => {
const sets = [];
const app = { api: {
tasteKeys: async () => ({ keys: [] }),
blockInfluence: async () => ({ key: "fox", enabled: true, problem: null }),
blockInfluenceGroups: async () => ({ key: "fox", used: 8, counts: { liked: 2, image: 2, composition: 2, both: 2 },
cos: { "image vs composition": 0.5 }, chance: { "image vs composition": { mean: 0.02, p95: 0.4 } } }),
setBlockInfluence: async (on) => { sets.push(on); return { enabled: on }; },
} };
const page = taste(app)();
document.body.append(page.node);
await tick(); await tick();
const text = page.node.textContent;
assert.match(text, /Record block influence/);
assert.match(text, /liked 2, image 2, composition 2, both 2/);
assert.match(text, /image vs composition/);
assert.match(text, /\+0\.50/);
assert.match(text, /95th \+0\.40/);
const toggle = page.node.querySelector('input[type="checkbox"]');
assert.equal(toggle.checked, true);
toggle.click(); await tick(); await tick();
assert.deepEqual(sets, [false]);
});
test("nothing recorded yet is said, not shown as a comparison", async () => {
const app = { api: {
tasteKeys: async () => ({ keys: [] }),
blockInfluence: async () => ({ key: "none", enabled: false, problem: null }),
blockInfluenceGroups: async () => ({ key: "none", used: 0, counts: {}, cos: {}, chance: {}, note: "nothing recorded yet" }),
} };
const page = taste(app)();
document.body.append(page.node);
await tick(); await tick();
assert.match(page.node.textContent, /nothing recorded yet/);
});
+3
View File
@@ -54,6 +54,9 @@ export const api = {
shotMemory: () => call("GET", "/api/m/conditioning_shot_camera/memory"),
forgetShotMemory: (kind, name) => call("POST", "/api/m/conditioning_shot_camera/forget", { kind, name }),
tasteKeys: () => call("GET", "/api/m/taste/keys"),
blockInfluence: (key) => call("GET", `/api/m/block_influence/status?key=${encodeURIComponent(key || "default")}`),
blockInfluenceGroups: (key) => call("GET", `/api/m/block_influence/groups?key=${encodeURIComponent(key || "default")}`),
setBlockInfluence: (on) => call("POST", `/api/m/block_influence/enabled?key=default`, { enabled: !!on }),
libraryFiles: () => call("GET", "/api/files"),
deleteLibraryFile: (name) => call("DELETE", `/api/files/${encodeURIComponent(name)}`),
deleteTasteKey: (name) => call("DELETE", `/api/m/taste/keys/${encodeURIComponent(name)}`),
@@ -114,6 +114,13 @@ def routes(table, base, web):
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
@table.get(base + "/groups")
async def _groups(req):
try:
return web.json_response(measure.four_groups(key_of(req)))
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
@table.post(base + "/enabled")
async def _enabled(req):
body = await req.json()
@@ -234,6 +234,12 @@ def state(key, fallback=True) -> dict:
**prof}
def four_groups(key, fallback=True) -> dict:
"""The four-group report for one key, for the panel (same key rules as state())."""
key = resolve(key) if fallback else key
return {"key": key, "enabled": enabled(), **groups(_kind(key).rows())}
def clear(key):
_kind(key).clear()
@@ -44,3 +44,19 @@ def test_too_few_clips_says_so_instead_of_comparing():
def test_nothing_recorded_is_said():
assert measure.groups([])["note"] == "nothing recorded yet"
def test_the_panel_report_reads_the_key_and_says_its_counts(monkeypatch):
rows = [_row(1, None, [1, 0]), _row(1, None, [1, 0]), _row(-1, "image", [0, 1]), _row(-1, "image", [0, 1]),
_row(-1, "composition", [0, -1]), _row(-1, "composition", [0, -1])]
class Kind:
def rows(self):
return rows
monkeypatch.setattr(measure, "_kind", lambda key: Kind())
monkeypatch.setattr(measure, "resolve", lambda key: key)
out = measure.four_groups("fox", fallback=False)
assert out["key"] == "fox" and out["used"] == 6
assert out["counts"]["image"] == 2 and out["counts"]["composition"] == 2
assert "composition vs image" in out["cos"]
-33
View File
@@ -1,33 +0,0 @@
"""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])