fix: block influence flatness reads raw push; the switch stays visible and works when the status fails or a file is damaged; a block some clip never measured is left out of the groups; the rated-runs label says what it counts

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-10 15:35:32 +03:00
co-authored by Claude Haiku 5.5
parent 7a809ae7d2
commit 7bf9b51007
6 changed files with 51 additions and 6 deletions
+5 -3
View File
@@ -74,12 +74,14 @@ export const taste = (app) => function mount() {
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}` }));
// The switch is always here, even when the report could not be read: a failed read must not take away the way to turn recording off.
rows.push(c.toggle.default({ label: "Record block influence", hint: "On: each run on a Taste key is measured, and kept once you rate it. Off: nothing is recorded.",
checked: influence && influence.state ? !!influence.state.enabled : true,
onChange: async (on) => { try { await app.api.setBlockInfluence(on); } catch (err) { tell(err.message); } refreshResearch(); } }));
if (influence && influence.state) {
const { state, four } = influence;
rows.push(c.toggle.default({ label: "Record block influence", hint: "On: each run on a Taste key is measured, and kept once you rate it. 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: "Runs measured", hint: `Runs on this key that have a rating: ${state.runs}, of which ${state.skipped} had no usable picture rows`,
rows.push(c.settingsRow.default({ label: "Rated runs kept", hint: `Runs on this key that have a rating: ${state.runs}, of which ${state.skipped} had no usable picture rows`,
control: c.hint.default({ text: `${state.used} kept` }) }));
if (state.used) rows.push(c.settingsRow.default({ label: "Flatness", hint: "How evenly the blocks move the picture. Near 0 means every block moves it equally, so there is nothing to aim at.",
control: c.hint.default({ text: state.flatness == null ? "n/a" : state.flatness.toFixed(3) }) }));
+17
View File
@@ -78,3 +78,20 @@ test("the block influence readout shows flatness, and Clear asks before it clear
assert.match(page.node.textContent, /Download/);
assert.match(page.node.textContent, /Clear/);
});
test("a failed status request still shows the switch, so recording can be turned off", async () => {
const sets = [];
const app = { api: {
tasteKeys: async () => ({ keys: [] }),
blockInfluence: async () => { throw new Error("HTTP 500"); },
blockInfluenceGroups: async () => ({ key: "fox", used: 0, counts: {}, cos: {}, chance: {}, note: "nothing recorded yet" }),
setBlockInfluence: async (on) => { sets.push(on); return { enabled: on }; },
} };
const page = taste(app)();
document.body.append(page.node);
await tick(); await tick();
const toggle = page.node.querySelector('input[type="checkbox"]');
assert.ok(toggle, "the switch is still there");
toggle.click(); await tick(); await tick();
assert.deepEqual(sets, [false], "the unknown state shows on, so one click turns it off");
});
@@ -129,6 +129,8 @@ def routes(table, base, web):
state = measure.state(key_of(req, body)) # refuse BEFORE flipping the switch
except ValueError as exc:
return web.json_response({"why": str(exc)}, status=400)
except Exception: # a damaged file must not keep the switch from going off
state = {}
measure.set_enabled(body.get("enabled") is True)
return web.json_response({**state, "enabled": measure.enabled()})
+9 -3
View File
@@ -106,7 +106,7 @@ def profile(rows) -> dict:
`overall` is where the model does its work regardless of ratings; `share` is
each block's RAW push as a fraction of all blocks' (rank by this: the ratio
shrinks with depth by construction, since the stream grows); `difference` is
liked minus disliked; `flatness` is the spread of `overall` as a fraction of
liked minus disliked; `flatness` is the spread of RAW push as a fraction of
its mean -- near 0 means every block moves the stream equally and there is
nothing to aim at. `novelty` ~1: a block amplifies its predecessor, ~0 it
adds something new, negative it partly undoes it.
@@ -135,7 +135,9 @@ def profile(rows) -> dict:
difference = None
if int(liked.sum()) >= MIN_PER_GROUP and int(disliked.sum()) >= MIN_PER_GROUP:
difference = table(mean("ratio", liked) - mean("ratio", disliked))
vals = overall[torch.isfinite(overall)]
# Flatness is read on RAW push: the ratio falls with depth by construction (the stream grows), so
# a flat model would read as uneven on it.
vals = raw[torch.isfinite(raw)]
flat = float(vals.std(unbiased=False) / vals.mean()) if len(vals) and abs(float(vals.mean())) > 1e-12 else None
nov = novelty[torch.isfinite(novelty)]
return {"used": len(rows), "overall": table(overall), "difference": difference, "share": share,
@@ -170,7 +172,11 @@ def groups(rows, shuffles=200, seed=0) -> dict:
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))
# A block some clip never measured is left out of every group, not filled in with a guess.
stack = stack[:, torch.isfinite(stack).all(dim=0)]
if stack.shape[1] == 0:
return {"used": len(rows), "counts": {g: sum(1 for r in rows if _group(r) == g) for g in GROUPS},
"cos": {}, "note": "no block was measured on every clip"}
labels = [_group(r) for r in rows]
counts = {g: labels.count(g) for g in GROUPS}
@@ -349,3 +349,12 @@ def test_the_panel_says_a_taste_key_is_needed_even_when_toggled_on_after_install
assert "Taste key" in measure.state("default")["problem"]
_load(tiny_h3) # a key is set: the banner goes
assert measure.state("default")["problem"] is None
def test_flatness_reads_raw_push_so_a_growing_stream_with_equal_pushes_is_flat():
from modules.sampling.block_influence.measure import profile
# every block pushes 1.0 raw, but the stream grows with depth: the ratio falls, the raw push does not
ratio = [1.0 / (1.0 + i * 0.5) for i in range(10)]
rows = [{"reward": w, "rows": {"ratio": torch.tensor(ratio), "raw": torch.ones(10),
"novelty": torch.full((10,), 0.5)}} for w in (1.0, 1.0, -1.0, -1.0)]
assert profile(rows)["flatness"] == pytest.approx(0.0, abs=1e-6)
@@ -60,3 +60,12 @@ def test_the_panel_report_reads_the_key_and_says_its_counts(monkeypatch):
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"]
def test_a_block_some_clip_never_measured_is_left_out_not_guessed():
nan = float("nan")
rows = [_row(1, None, [1, 0, nan]), _row(1, None, [1, 0, nan]), _row(-1, "image", [0, 1, nan]),
_row(-1, "image", [0, 1, nan]), _row(-1, "composition", [0, -1, 0.5]), _row(-1, "composition", [0, -1, 0.5])]
out = measure.groups(rows, shuffles=10)
assert out["cos"], "the blocks everyone measured still compare"
assert out["counts"]["composition"] == 2