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:
co-authored by
Claude Haiku 5.5
parent
49db27aec8
commit
cdadf0d4ba
@@ -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() };
|
||||
};
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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"]
|
||||
|
||||
@@ -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])
|
||||
Reference in New Issue
Block a user