feat: shot camera memory can be read and pruned from Settings (routes + Learning section)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
079a5104c7
commit
3e335a4a77
@@ -9,6 +9,7 @@ import { system } from "./system.js";
|
||||
import { packs } from "./packs.js";
|
||||
import { taste } from "./taste.js";
|
||||
import { editor } from "./editor.js";
|
||||
import { shotMemory } from "./shotmemory.js";
|
||||
|
||||
export default {
|
||||
id: "settings-sections",
|
||||
@@ -29,6 +30,10 @@ export default {
|
||||
modules(app), "Generation", "modules enable disable quarantine");
|
||||
add("refinement", "Refinement & Taste", "Learned-taste state: refinement keys and the Absolute global-taste store.", "✦", "danger",
|
||||
taste(app), "Learning", "refinement taste keys rating");
|
||||
// only when that module is installed: a section for something absent would be a dead end
|
||||
app.pipeline.ensureLoaded().then(() => {
|
||||
if (app.pipeline.modulesById()["conditioning_shot_camera"]) add("shotmemory", "Shot camera memory", "What the camera has learned from your prompts and ratings.", "◎", "neutral", shotMemory(app), "Learning", "shot camera views moves words forget");
|
||||
});
|
||||
add("system", "Updates & ComfyUI", "Server connection, FunPack code updates, pipeline health.", "⟳", "good",
|
||||
system(app), "System", "update git branch restart rollback");
|
||||
add("customnodes", "Custom Nodes", "Install, update and remove ComfyUI node packs.", "⧉", "neutral",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Shot camera memory: the words and views the camera has learned from your prompts and ratings, to read and forget.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
export const shotMemory = (app) => function mount() {
|
||||
const page = c.region.stack({ gap: "md" });
|
||||
let data = null, error = "";
|
||||
|
||||
async function refresh() {
|
||||
try { data = await app.api.shotMemory(); error = ""; } catch (err) { data = null; error = err.message; }
|
||||
draw();
|
||||
}
|
||||
async function forget(kind, name) {
|
||||
const what = kind === "all" ? "everything the camera has learned" : `“${name}”`;
|
||||
if (!(await c.modal.dialogue({ title: "Forget", message: `Forget ${what}? This cannot be undone.`, tone: "danger", confirmLabel: "Forget" }).result)) return;
|
||||
try { await app.api.forgetShotMemory(kind, name); } catch (err) { c.toast.warn({ text: err.message }); }
|
||||
refresh();
|
||||
}
|
||||
const row = (label, hint, kind, name) => c.settingsRow.default({ label, hint, control: c.button.sm({ label: "Forget", tone: "danger", onClick: () => forget(kind, name) }) });
|
||||
function draw() {
|
||||
if (error) return page.set([c.banner.warn({ text: `Could not read the camera's memory: ${error}` })]);
|
||||
if (!data) return page.set([c.hint.default({ text: "Loading…" })]);
|
||||
const words = data.words || [], views = data.views || [];
|
||||
if (!words.length && !views.length) return page.set([c.emptyState.default({ icon: "◎", title: "Nothing learned yet", hint: "Turn on camera moves or shot views, then rate renders: the camera remembers which words and views you liked." })]);
|
||||
page.set([c.hint.default({ text: `${data.prompts} prompts seen · ${data.shots} shots reviewed. Forget anything that taught it the wrong thing.` }),
|
||||
...(views.length ? [c.label.section({ text: "Views" }), ...views.map((v) => row(v.view, `liked ${v.good.toFixed(1)} · disliked ${v.bad.toFixed(1)}`, "view", v.view))] : []),
|
||||
...(words.length ? [c.label.section({ text: "Words the camera aims at" }), ...words.map((w) => row(w.word, `picked ${w.picks} · replaced ${w.rejects} · seen ${w.seen}`, "word", w.word)),
|
||||
data.more ? c.hint.default({ text: `and ${data.more} more` }) : null] : []),
|
||||
c.button.sm({ label: "Forget everything", tone: "danger", onClick: () => forget("all") })].filter(Boolean));
|
||||
}
|
||||
draw(); refresh();
|
||||
return { node: page.node, destroy: () => page.node.remove() };
|
||||
};
|
||||
@@ -51,6 +51,8 @@ export const api = {
|
||||
enhancerRuns: () => call("GET", "/api/m/conditioning_prompt_enhancer/runs"),
|
||||
rateTaste: (promptId, rating, axis) => call("POST", "/api/m/taste/rate", { prompt_id: promptId, rating, axis: axis || null }),
|
||||
newTasteGeneration: () => call("POST", "/api/m/taste/generation", {}),
|
||||
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"),
|
||||
deleteTasteKey: (name) => call("DELETE", `/api/m/taste/keys/${encodeURIComponent(name)}`),
|
||||
tasteKeyUrl: (name) => `${BASE}/api/m/taste/keys/${encodeURIComponent(name)}/export`,
|
||||
|
||||
@@ -49,4 +49,20 @@ def on_rating(prompt_id, rating, axis=None):
|
||||
return memory.on_rating(prompt_id, rating, axis)
|
||||
|
||||
|
||||
PROVIDES = {"on_rating": on_rating}
|
||||
def routes(table, base, web):
|
||||
"""What the camera has learned, to read and prune from Settings."""
|
||||
@table.get(base + "/memory")
|
||||
async def _memory(_req):
|
||||
return web.json_response(memory.summary())
|
||||
|
||||
@table.post(base + "/forget")
|
||||
async def _forget(req):
|
||||
try:
|
||||
body = await req.json()
|
||||
gone = memory.forget(body.get("kind"), body.get("name"))
|
||||
except (ValueError, AttributeError, TypeError) as exc:
|
||||
return web.json_response({"why": str(exc)}, status=400)
|
||||
return web.json_response({"forgotten": gone})
|
||||
|
||||
|
||||
PROVIDES = {"on_rating": on_rating, "routes": routes}
|
||||
|
||||
@@ -72,3 +72,31 @@ def test_more_shots_than_seconds_refuses_instead_of_writing_bad_times(tmp_path,
|
||||
text = "\n".join(f"[Shot {i}] A woman walks along street number {i}." for i in range(1, 8))
|
||||
out, said, _ = nodes.rewrite(text, {"shot_cuts": True}, seconds=5.17, pieces=[])
|
||||
assert out == text and "do not fit" in said
|
||||
|
||||
|
||||
def test_memory_routes_read_and_forget(tmp_path, monkeypatch):
|
||||
import asyncio, json
|
||||
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
|
||||
memory.record_run("a", {"views": [{"view": "side view", "traits": ["none"]}], "arms": []})
|
||||
memory.on_rating("a", "liked")
|
||||
handlers = {}
|
||||
|
||||
class Table:
|
||||
def get(self, path): return lambda fn: handlers.setdefault(("GET", path), fn)
|
||||
def post(self, path): return lambda fn: handlers.setdefault(("POST", path), fn)
|
||||
|
||||
class web:
|
||||
@staticmethod
|
||||
def json_response(data, status=200):
|
||||
return type("R", (), {"status": status, "data": data})()
|
||||
|
||||
class Req:
|
||||
def __init__(self, body=None): self._b = body
|
||||
async def json(self): return self._b
|
||||
|
||||
_mod().routes(Table(), "/b", web)
|
||||
got = asyncio.run(handlers[("GET", "/b/memory")](Req()))
|
||||
assert [v["view"] for v in got.data["views"]] == ["side view"]
|
||||
assert asyncio.run(handlers[("POST", "/b/forget")](Req({"kind": "view", "name": "side view"}))).data == {"forgotten": True}
|
||||
assert asyncio.run(handlers[("GET", "/b/memory")](Req())).data["views"] == []
|
||||
assert asyncio.run(handlers[("POST", "/b/forget")](Req({"kind": "nonsense"}))).status == 400
|
||||
|
||||
Reference in New Issue
Block a user