From 28339e9fec7888cecec906e279348d4b50421e33 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:23:24 +0300 Subject: [PATCH] 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 --- app/legacy/api.js | 2 +- app/legacy/rating_picker.js | 23 +++++++++++++++++++++-- app/legacy/rating_picker.test.js | 12 ++++++++++++ app/legacy/store.js | 6 ++++-- modules/sampling/decisiveness/__init__.py | 2 +- modules/sampling/dynashift/__init__.py | 2 +- modules/sampling/late_branch/__init__.py | 2 +- modules/sampling/shot_memory/__init__.py | 2 +- modules/sampling/stas/__init__.py | 2 +- 9 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/legacy/api.js b/app/legacy/api.js index 635036a9..115d9ea7 100644 --- a/app/legacy/api.js +++ b/app/legacy/api.js @@ -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 ------- diff --git a/app/legacy/rating_picker.js b/app/legacy/rating_picker.js index 679a6a99..f3e989bf 100644 --- a/app/legacy/rating_picker.js +++ b/app/legacy/rating_picker.js @@ -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 }; })(); diff --git a/app/legacy/rating_picker.test.js b/app/legacy/rating_picker.test.js index 8e6c6f6e..88fb3070 100644 --- a/app/legacy/rating_picker.test.js +++ b/app/legacy/rating_picker.test.js @@ -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"); +}); diff --git a/app/legacy/store.js b/app/legacy/store.js index 42acfb45..9793ce7c 100644 --- a/app/legacy/store.js +++ b/app/legacy/store.js @@ -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 } diff --git a/modules/sampling/decisiveness/__init__.py b/modules/sampling/decisiveness/__init__.py index 356e3ca4..1a6b7b6c 100644 --- a/modules/sampling/decisiveness/__init__.py +++ b/modules/sampling/decisiveness/__init__.py @@ -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}") diff --git a/modules/sampling/dynashift/__init__.py b/modules/sampling/dynashift/__init__.py index 0fcc9356..fef7daf3 100644 --- a/modules/sampling/dynashift/__init__.py +++ b/modules/sampling/dynashift/__init__.py @@ -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: diff --git a/modules/sampling/late_branch/__init__.py b/modules/sampling/late_branch/__init__.py index 19f45d12..b3195c10 100644 --- a/modules/sampling/late_branch/__init__.py +++ b/modules/sampling/late_branch/__init__.py @@ -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}") diff --git a/modules/sampling/shot_memory/__init__.py b/modules/sampling/shot_memory/__init__.py index e158e19d..8f9b0e55 100644 --- a/modules/sampling/shot_memory/__init__.py +++ b/modules/sampling/shot_memory/__init__.py @@ -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") diff --git a/modules/sampling/stas/__init__.py b/modules/sampling/stas/__init__.py index b2c919cf..a2454f6e 100644 --- a/modules/sampling/stas/__init__.py +++ b/modules/sampling/stas/__init__.py @@ -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}")