feat: read and forget learned camera focus / view memory in Settings
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
- Export settings no longer prints a loader input the node no longer has (a stale `sla_enabled: on`) or SLA's settings while `sla` is off.
|
||||
|
||||
### Added
|
||||
- Settings ▸ Refinement & Taste lists what camera focus and views have learned, with Forget per word / view / everything, and a button to forget this project's saved shot choices.
|
||||
- Reactive focus shows each shot as typed (shortcuts unexpanded) and as written, lets you pick several views ("fine from any of these") and several targets in order (the camera travels from the first to the last).
|
||||
- Shot views now learn from ratings (liked = good, disliked = bad; picture-only dislikes teach nothing) and only offer views that can show the shot: a face shot never gets the view from behind, a back or someone leaving never a front/POV view. Reactive focus gains a second step to pick each shot's view.
|
||||
- Reactive focus (Engine Settings, needs camera moves): before Generate, pick what the camera aims at in each `[Shot N]`, hold or move, or no move. Asked once per shot; picks are remembered and steer later suggestions.
|
||||
|
||||
@@ -135,6 +135,44 @@ def learn_views(decisions):
|
||||
return n
|
||||
|
||||
|
||||
def summary(limit=40):
|
||||
"""What is remembered, for the person to read and prune: words (picked / replaced / merely
|
||||
recurring) and views (good / bad, with the per-trait rows folded under their view)."""
|
||||
data = _read()
|
||||
picks, rejects, seen = (data.get("picks") or {}), (data.get("rejects") or {}), (data.get("seen") or {})
|
||||
words = [{"word": w, "picks": int(picks.get(w, 0)), "rejects": int(rejects.get(w, 0)), "seen": int(seen.get(w, 0))}
|
||||
for w in {*picks, *rejects, *seen}]
|
||||
words.sort(key=lambda r: -(r["picks"] * 3 + r["rejects"] * 3 + r["seen"]))
|
||||
views = [{"view": k, "good": g, "bad": b} for k, (g, b) in view_stats().items() if "@" not in k]
|
||||
views.sort(key=lambda r: -(r["good"] + r["bad"]))
|
||||
return {"words": words[:limit], "more": max(0, len(words) - limit), "views": views,
|
||||
"prompts": int(data.get("prompts", 0)), "shots": int(data.get("shots", 0)),
|
||||
"kept": int(data.get("kept", 0))}
|
||||
|
||||
|
||||
def forget(kind, name=None):
|
||||
"""Drop one remembered word (`kind="word"`), one view and its per-trait rows (`"view"`), or
|
||||
everything (`"all"`). -> True if anything was removed."""
|
||||
data = _read()
|
||||
if kind == "all":
|
||||
existed = bool(data)
|
||||
data = {}
|
||||
elif kind == "word":
|
||||
existed = any(name in (data.get(k) or {}) for k in ("picks", "rejects", "seen"))
|
||||
for k in ("picks", "rejects", "seen"):
|
||||
(data.get(k) or {}).pop(name, None)
|
||||
elif kind == "view":
|
||||
views = data.get("views") or {}
|
||||
gone = [k for k in views if k == name or k.startswith(f"{name}@")]
|
||||
existed = bool(gone)
|
||||
for k in gone:
|
||||
views.pop(k)
|
||||
else:
|
||||
raise ValueError(f"unknown kind {kind!r}")
|
||||
_save(data)
|
||||
return existed
|
||||
|
||||
|
||||
def effective_chance(chance):
|
||||
"""The configured chance of a move, pulled toward how often the user actually keeps one
|
||||
(of the shots they reviewed) once enough were reviewed."""
|
||||
|
||||
@@ -173,6 +173,8 @@
|
||||
systemInfo: () => j("GET", API("/system/info")),
|
||||
focusOptions: (pid, sceneIds) => j("POST", API(`/projects/${pid}/focus_options`), { scene_ids: sceneIds }),
|
||||
focusLearn: (decisions, views) => j("POST", API("/focus/learn"), { decisions: decisions || [], views: views || [] }),
|
||||
focusMemory: () => j("GET", API("/focus/memory")),
|
||||
focusForget: (kind, name) => j("POST", API("/focus/memory/forget"), { kind, name }),
|
||||
gitStatus: () => j("GET", API("/git/status")),
|
||||
gitUpdate: (branch) => j("POST", API("/git/update"), branch ? { branch } : {}),
|
||||
gitCheckout: (branch) => j("POST", API("/git/checkout"), { branch }),
|
||||
|
||||
@@ -216,6 +216,46 @@
|
||||
return row;
|
||||
}
|
||||
|
||||
// ── Camera focus & views memory (shot_camera / focus_memory) ─────────────
|
||||
// What Reactive focus and the ratings taught, with a way to take a wrong lesson back, plus the
|
||||
// choices this project already saved (saved shots are not asked about again).
|
||||
function focusMemoryRows() {
|
||||
const box = el("div", "sw-rows");
|
||||
const API = window.MovieEditorAPI;
|
||||
const reload = () => fill().catch((e) => { clear(box); box.append(el("div", "sw-hint", `Could not read the memory: ${e.message}`)); });
|
||||
const forget = (kind, name) => API.focusForget(kind, name).then(reload);
|
||||
async function fill() {
|
||||
const m = await API.focusMemory();
|
||||
clear(box);
|
||||
const proj = window.Store?.get().project;
|
||||
const rf = proj && window.StudioSettings ? window.StudioSettings.read(proj).rf : {};
|
||||
const saved = Object.keys(rf.focus_choices || {}).length + Object.keys(rf.view_choices || {}).length;
|
||||
box.append(actionRow("This project's saved choices",
|
||||
saved ? `${saved} shot choice(s) saved; forgetting them makes Reactive focus ask again.`
|
||||
: "Nothing saved: Reactive focus asks about every shot.",
|
||||
"Forget…", () => {
|
||||
if (!confirm("Forget this project's saved focus and view choices? Reactive focus will ask again.")) return;
|
||||
window.StudioSettings.patchRefiner({ focus_choices: {}, view_choices: {} }, true);
|
||||
reload();
|
||||
}, { disabled: !saved }));
|
||||
box.append(el("div", "sw-hint",
|
||||
`Remembered across all projects: ${m.prompts} prompt(s), ${m.shots} reviewed shot(s), ${m.kept} kept a move.`));
|
||||
m.words.forEach((w) => box.append(actionRow(w.word,
|
||||
`picked ${w.picks}× · replaced ${w.rejects}× · recurring in ${w.seen} prompt(s)`, "Forget",
|
||||
() => forget("word", w.word))));
|
||||
if (m.more) box.append(el("div", "sw-hint", `…and ${m.more} more words.`));
|
||||
m.views.forEach((v) => box.append(actionRow(v.view,
|
||||
`good ${v.good.toFixed(1)} · bad ${v.bad.toFixed(1)} (and per shot type)`, "Forget",
|
||||
() => forget("view", v.view))));
|
||||
box.append(actionRow("Forget everything",
|
||||
"Wipes the recurring-word memory, picks, replaced words and view ratings.", "Clear…",
|
||||
() => { if (confirm("Forget all remembered camera focus and view lessons?")) forget("all"); },
|
||||
{ danger: true }));
|
||||
}
|
||||
reload();
|
||||
return box;
|
||||
}
|
||||
|
||||
// ── Trajectory probe ───────────────────────────────────────────────────
|
||||
// Every rating-driven mechanism only acts over the last half of a generation, so a
|
||||
// rating about MOTION arrives after motion was decided. The probe records what the model
|
||||
@@ -1044,6 +1084,9 @@
|
||||
rows.append(actionRow("Import refinement key", "Load a previously exported <key>.json onto this instance.", "Import…", () => M.importRefinementKeyFile?.()));
|
||||
wrap.append(rows);
|
||||
|
||||
wrap.append(el("div", "sw-rows-label", "Camera focus & views memory"));
|
||||
wrap.append(focusMemoryRows());
|
||||
|
||||
wrap.append(el("div", "sw-rows-label", "Trajectory probe"));
|
||||
wrap.append(probeRows());
|
||||
|
||||
|
||||
@@ -3158,6 +3158,21 @@ if web is not None and PromptServer is not None:
|
||||
return web.json_response({"detail": f"Could not remember the choices: {e}"}, status=500)
|
||||
return web.json_response({"remembered": n})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/focus/memory")
|
||||
async def _focus_memory(_req):
|
||||
import asyncio
|
||||
return web.json_response(await asyncio.to_thread(bridge._funpack_attr("focus_memory", "summary")))
|
||||
|
||||
@routes.post(UI_PREFIX + "/api/focus/memory/forget")
|
||||
async def _focus_forget(req):
|
||||
import asyncio
|
||||
body = await req.json() if req.can_read_body else {}
|
||||
kind, name = body.get("kind"), body.get("name")
|
||||
if kind not in ("all", "word", "view") or (kind != "all" and not name):
|
||||
return web.json_response({"detail": "Say what to forget: kind all, or word/view with a name."}, status=400)
|
||||
done = await asyncio.to_thread(bridge._funpack_attr("focus_memory", "forget"), kind, name)
|
||||
return web.json_response({"forgot": bool(done)})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/git/status")
|
||||
async def _git_status(_req):
|
||||
return web.json_response(git_update.status())
|
||||
|
||||
@@ -291,3 +291,17 @@ def test_raw_previews_keep_the_typed_shortcuts(monkeypatch):
|
||||
{"anchor": "", "scenes": ["$style [Shot 2] MISS CIPI"], "postfix": ""},
|
||||
[{"name": "style", "value": "intro [Shot 1] HH FF"}])
|
||||
assert raw == [{1: "HH FF", 2: "MISS CIPI"}]
|
||||
|
||||
|
||||
def test_what_is_remembered_can_be_read_and_taken_back(tmp_path, monkeypatch):
|
||||
import focus_memory as fm
|
||||
monkeypatch.setenv("SHOT_CAMERA_MEMORY", str(tmp_path / "m.json"))
|
||||
fm.learn([{"auto": "coat", "picked": ["cup"], "mode": "move"}])
|
||||
fm.rate_views([{"view": "Side view", "traits": ["face"]}], -1)
|
||||
s = fm.summary()
|
||||
assert {w["word"] for w in s["words"]} >= {"cup", "coat"} and s["views"][0]["view"] == "Side view"
|
||||
assert fm.forget("word", "cup") and "cup" not in {w["word"] for w in fm.summary()["words"]}
|
||||
assert fm.forget("view", "Side view") and fm.view_stats() == {} # per-trait rows go with it
|
||||
assert not fm.forget("word", "never-heard-of-it")
|
||||
fm.forget("all")
|
||||
assert fm.summary()["words"] == [] and fm.summary()["shots"] == 0
|
||||
|
||||
Reference in New Issue
Block a user