feat: dislikes name what went wrong (bad image / bad composition); composition learners and DynaShift read the other axis as neutral; picker labels reach the taste store

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-02 15:23:24 +03:00
co-authored by Claude Sonnet 5.5
parent a9ef7d5c44
commit 28339e9fec
9 changed files with 43 additions and 10 deletions
+1 -1
View File
@@ -315,7 +315,7 @@
previewSegmentUrl: unsupportedUrl,
// --- taste keys: modules/system/taste --------------------------------------
rateTaste: (prompt_id, rating) => j("POST", API("/api/m/taste/rate"), { prompt_id, rating }),
rateTaste: (prompt_id, rating, axis) => j("POST", API("/api/m/taste/rate"), { prompt_id, rating, axis: axis || null }),
tasteKeys: () => j("GET", API("/api/m/taste/keys")),
// --- refinement keys / absolute taste store (c) -- not built in v5 -------
+21 -2
View File
@@ -57,6 +57,8 @@
const H3_SCALE = [
{ label: "10", display: "Liked", reward: "+", hint: "This generation was good." },
{ label: "1", display: "Disliked", reward: "−", hint: "This generation was not." },
{ label: "Disliked: bad image", display: "Disliked — bad image", reward: "−", hint: "Composition was fine; the picture itself was ruined." },
{ label: "Disliked: bad composition", display: "Disliked — bad composition", reward: "−", hint: "Well drawn, but the shots/layout/movement were wrong." },
];
// canonical label -> display name, for rendering a stored value (scene button, chips).
@@ -66,7 +68,7 @@
const displayName = (label) => DISPLAY_NAMES[label] || label;
const NO_LOVED = new Set([
"Perfect", "Nailed it", "Awful", FORGET_LABEL,
"Perfect", "Nailed it", "Awful", FORGET_LABEL, "Disliked: bad image", "Disliked: bad composition",
"Missing quality", "Missing details + quality", "Missing action + quality", "Wrong action + quality",
// H3 scale (1-10): a bare number never reaches the label branch that applies the loved
// boost (normalize_refiner_v2_rating), so the heart would silently do nothing. 10 already
@@ -251,5 +253,22 @@
setTimeout(() => document.addEventListener("mousedown", onOutside, true), 0);
}
window.MovieRatingPicker = { open, close: closePicker, formatLabel, buttonLabel, FORGET_LABEL };
// What a picked label teaches the taste key. v5's ratings are liked / disliked (a dislike may
// name the axis); the sign of a category's reward decides, as v4's learners only ever read it.
// null = teaches nothing (a neutral label); clear = forget what this clip taught.
const ALL = [...CATEGORIES.flatMap((c) => c.ratings), ...NUCLEAR, ...H3_SCALE];
function tasteOf(value) {
const v = String(value || "").replace(/\|loved$/, "").trim();
if (!v) return "clear";
if (v === FORGET_LABEL) return "clear";
if (v === "Disliked: bad image") return { rating: "disliked", axis: "image" };
if (v === "Disliked: bad composition") return { rating: "disliked", axis: "composition" };
if (/^\d+$/.test(v)) return { rating: Number(v) >= 6 ? "liked" : "disliked", axis: null };
const hit = ALL.find((r) => r.label === v);
const n = hit && parseFloat(String(hit.reward).replace("−", "-"));
if (!n || Number.isNaN(n)) return null;
return { rating: n > 0 ? "liked" : "disliked", axis: null };
}
window.MovieRatingPicker = { open, close: closePicker, formatLabel, buttonLabel, tasteOf, FORGET_LABEL };
})();
+12
View File
@@ -31,3 +31,15 @@ test("forget and empty still render as nothing / the prompt", () => {
assert.strictEqual(Picker.formatLabel(Picker.FORGET_LABEL), "");
assert.strictEqual(Picker.buttonLabel(""), "Rate scene…");
});
test("what a label teaches the taste key: sign decides, a dislike may name its axis", () => {
assert.deepStrictEqual(Picker.tasteOf("10"), { rating: "liked", axis: null });
assert.deepStrictEqual(Picker.tasteOf("1"), { rating: "disliked", axis: null });
assert.deepStrictEqual(Picker.tasteOf("Disliked: bad image"), { rating: "disliked", axis: "image" });
assert.deepStrictEqual(Picker.tasteOf("Disliked: bad composition"), { rating: "disliked", axis: "composition" });
assert.deepStrictEqual(Picker.tasteOf("Perfect|loved"), { rating: "liked", axis: null });
assert.deepStrictEqual(Picker.tasteOf("Awful"), { rating: "disliked", axis: null });
assert.strictEqual(Picker.tasteOf("Wrong appearance"), null);
assert.strictEqual(Picker.tasteOf(""), "clear");
assert.strictEqual(Picker.tasteOf(Picker.FORGET_LABEL), "clear");
});
+4 -2
View File
@@ -4968,8 +4968,10 @@
patchScene(id, { rating: v || "" });
if (window.Timeline?.syncClipRatings) window.Timeline.syncClipRatings(get());
const promptId = state.sceneRenders[id]?.promptId;
if (promptId) {
API.rateTaste(promptId, v || null).then((r) => {
const taste = window.MovieRatingPicker?.tasteOf(v); // null: a label that teaches nothing
if (promptId && taste !== null) {
const t = taste === "clear" || taste === undefined ? { rating: null, axis: null } : taste;
API.rateTaste(promptId, t.rating, t.axis).then((r) => {
if (r?.why) set({ notice: `Taste key not taught: ${r.why}.` });
}).catch(() => {}); // no taste module: rating stays a label
}
+1 -1
View File
@@ -126,7 +126,7 @@ def install(patcher, values, key):
steer.attach(patcher, key)
def fresh():
live.update(checked=False, acts=False, steps=0, rows=None if manual else taste.rows(KIND))
live.update(checked=False, acts=False, steps=0, rows=None if manual else taste.rows(KIND, blind_to="image"))
live["k"] = float(values.get("k", 1.0)) if manual else 1.0
if manual:
log.once(f"{ID}:state", log.INFO, "FunPack Decisiveness", f"manual k {live['k']:.3f}")
+1 -1
View File
@@ -172,7 +172,7 @@ def install(patcher, values, key):
live = {"bank": Bank([])}
def fresh():
bank = live["bank"] = Bank(taste.rows(KIND))
bank = live["bank"] = Bank(taste.rows(KIND, blind_to="composition"))
if not bank.negatives:
summary = "learning: nothing disliked banked yet"
else:
+1 -1
View File
@@ -102,7 +102,7 @@ def install(patcher, values, key):
if manual:
return log.once(f"{ID}:state", log.INFO, "FunPack Late-branch guidance",
f"manual strength {live['w']:.2f}, branch at block {branch}")
live["w"], centre, held = DIAL.pick(rated_dial.history(taste.rows(KIND), {"b": branch}))
live["w"], centre, held = DIAL.pick(rated_dial.history(taste.rows(KIND, blind_to="image"), {"b": branch}))
log.once(f"{ID}:state", log.INFO, "FunPack Late-branch guidance",
f"key {taste.key!r}: learned {centre:.2f} from {held} rating(s), trying "
f"{live['w']:.2f}, branch at block {branch}")
+1 -1
View File
@@ -210,7 +210,7 @@ def install(patcher, values, key):
live = {"rows": [], "plan": None}
def fresh():
live["rows"], live["plan"] = shots(taste.rows(KIND)), None
live["rows"], live["plan"] = shots(taste.rows(KIND, blind_to="image")), None
liked = sum(1 for r in live["rows"] if r["reward"] > 0)
log.once(f"{ID}:state", log.INFO, "FunPack Shot memory",
f"key {taste.key!r}: {liked} liked shot(s) of {len(live['rows'])} rated")
+1 -1
View File
@@ -137,7 +137,7 @@ def install(patcher, values, key):
if manual:
return log.once(f"{ID}:state", log.INFO, "FunPack STAS",
f"manual alpha {live['alpha']:.2f} at block {block}")
live["alpha"], centre, held = DIAL.pick(rated_dial.history(taste.rows(KIND), {"b": block}))
live["alpha"], centre, held = DIAL.pick(rated_dial.history(taste.rows(KIND, blind_to="image"), {"b": block}))
log.once(f"{ID}:state", log.INFO, "FunPack STAS",
f"key {taste.key!r}: learned alpha {centre:.2f} from {held} rating(s), trying "
f"{live['alpha']:.2f} at block {block}")