Session Reset wipes all prompt keys; warn with the key list

Per-scene multi-key learning trains every refinement key whose shortcut
fired in the prompt, so a reset must clear them all — not just the
project/default key, which left stale per-key state behind.

- conditioning.py: _v2_reset_prompt_keys() — on reset_session, union the
  non-default keys from the raw positive + intent prompts and reset/save
  each (primary key excluded; main flow already handles it). Reported in
  the state status string.
- store.js: "Reset Studio session" now confirms before arming, listing
  every key it will wipe (default + non-default keys in the prompt, from
  the preview's scene_refinement_keys). Disarming does not re-prompt.
- tests: union/primary-exclusion + no-op cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-14 09:53:40 +03:00
co-authored by Claude Opus 4.8
parent b78de23ccd
commit c8ec0e3d55
4 changed files with 87 additions and 1 deletions
+2
View File
@@ -8,6 +8,8 @@
**Per-scene refinement-key preview.** The timeline preview now shows, on each scene, the refinement key(s) it will actually steer with before you generate — explicit keys (with `(avg)` when more than one), or the project default key (greyed, `(default)`) when no shortcut key fired in that scene. The preview reuses the exact generation-time resolver (`resolve_scene_refinement_keys`), so what you see matches what runs, including the safe-union fallback.
**Session Reset now wipes every key a run trains, not just the default.** Because per-scene multi-key learning trains *every* refinement key whose shortcut fired in the prompt, a Studio Session Reset now clears all of those keys too (project/default key + each non-default key activated by a shortcut), so no stale per-key state survives a reset (`FunPackVideoRefinerV2._v2_reset_prompt_keys`). The Movie Editor's "Reset Studio session" now confirms first, listing exactly which keys will be wiped: *"This action will reset Studio learning for keys: default, key1, key2… To avoid resetting a non-default key, remove the shortcut that activates it from the prompt. Proceed?"* — disarming a mis-armed reset does not re-prompt.
## [3.0.0] - 2026-06-11
### Added
+31
View File
@@ -6461,6 +6461,32 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
except (json.JSONDecodeError, OSError, ValueError):
return self._v2_empty_state(refinement_key), "reset unreadable"
def _v2_reset_prompt_keys(self, primary_key, *raw_prompts):
"""On Session Reset, also wipe every NON-default refinement key whose shortcut fired in
the prompt(s) — not just the project/default key. Any key a run can TRAIN (per-scene
multi-key learning) must also be resettable by that run, otherwise stale state for those
keys survives a reset. primary_key is excluded (the main flow already reset + will resave
it). Returns the extra keys reset, for the status string."""
keys = set()
for text in raw_prompts:
try:
_, _, all_keys = prompt_scene_shortcut_keys(text)
except Exception as error:
print(f"[FunPackVideoRefinerV2] Reset key scan failed: {error}")
all_keys = set()
keys |= all_keys
keys.discard(str(primary_key or "default"))
keys.discard("")
done = []
for key in sorted(keys):
try:
fresh_state, _ = self._v2_load_state(key, reset_session=True)
self._v2_save_state(fresh_state, key)
done.append(key)
except Exception as error:
print(f"[FunPackVideoRefinerV2] Reset of refinement key '{key}' failed: {error}")
return done
def _v2_save_state(self, data, refinement_key):
path = self._v2_state_path(refinement_key)
os.makedirs(os.path.dirname(path), exist_ok=True)
@@ -11984,6 +12010,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
# Pre-shortcut prompt, kept so Batch Training can re-resolve shortcuts with different
# seeds to build N variant conditionings (Studio owns conditioning production).
_raw_positive_prompt = str(positive_prompt or "")
_raw_intent_prompt = str(user_intent_prompt or "")
positive_prompt, shortcut_status, shortcut_applied = self._v2_apply_global_shortcuts(positive_prompt, seed=seed)
user_intent_prompt, intent_shortcut_status, intent_shortcut_applied = self._v2_apply_global_shortcuts(user_intent_prompt, seed=seed)
if intent_shortcut_applied:
@@ -12006,6 +12033,10 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
advisor_clip = advisor_clip if advisor_clip is not None else clip
prompt_only_mode = execution_mode == "Prompt only"
state, state_status = self._v2_load_state(refinement_key, reset_session=reset_session)
if reset_session:
extra_reset = self._v2_reset_prompt_keys(refinement_key, _raw_positive_prompt, _raw_intent_prompt)
if extra_reset:
state_status = f"{state_status} (also reset keys: {', '.join(extra_reset)})"
scene_db, scene_builder_status = self._v2_scene_builder_db(state)
global_state = state.setdefault("global", {})
global_state.setdefault("phrase_memory", {})
+22 -1
View File
@@ -2786,10 +2786,31 @@
);
}
// Every refinement key a reset will wipe: the project/default key plus every non-default
// key whose shortcut fires in the current prompt (those keys also get TRAINED per scene, so
// a reset must clear them too). Mirrors the backend _v2_reset_prompt_keys union.
function sessionResetKeys() {
const pv = state.preview || {};
const defKey = (state.project?.refinement_key || "default").trim() || "default";
const out = new Set([defKey]);
(pv.scene_refinement_keys || []).forEach((rk) => {
if (rk && !rk.uses_default) (rk.keys || []).forEach((k) => { if (k) out.add(k); });
});
return [...out];
}
// Toggle a pending Studio session reset — applied to the FIRST run of the next
// generation. Clicking again disarms it (in case of a mis-click).
// generation. Clicking again disarms it (in case of a mis-click). Arming asks for
// confirmation, listing every key it will wipe so non-default keys aren't lost silently.
let _resetSessionPending = false;
function resetStudioSession() {
if (!_resetSessionPending) {
const keys = sessionResetKeys();
const msg = `This action will reset Studio learning for keys: ${keys.join(", ")}.\n\n`
+ "It is applied on the next generation. To avoid resetting a non-default key, "
+ "remove the shortcut that activates it from the prompt.\n\nProceed?";
if (!confirm(msg)) return;
}
_resetSessionPending = !_resetSessionPending;
set({ resetSessionArmed: _resetSessionPending });
}
+32
View File
@@ -133,6 +133,38 @@ def test_resolver_matches_studio(monkeypatch):
assert conditioning.resolve_scene_refinement_keys(raw, n) == studio._v2_scene_refinement_keys(raw, n)
def test_reset_prompt_keys_union_excludes_primary(monkeypatch):
"""Session reset wipes the project key (caller) PLUS every non-default key in the prompt."""
db = _db({"a": (["alpha"], ["AAA"], "keyA"), "b": (["beta"], ["BBB"], "keyB"),
"c": (["gamma"], ["CCC"], "keyC")})
monkeypatch.setattr(templates, "load_shortcut_db", lambda: db)
monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {})
studio = conditioning.FunPackVideoRefinerV2()
reset_calls, saved = [], []
monkeypatch.setattr(studio, "_v2_load_state",
lambda key, reset_session=False: (reset_calls.append((key, reset_session)) or ({}, "fresh")))
monkeypatch.setattr(studio, "_v2_save_state", lambda data, key: saved.append(key))
# keyA fires in positive, keyC fires in intent; primary "default" is excluded.
done = studio._v2_reset_prompt_keys("default", "alpha shot", "gamma intent")
assert done == ["keyA", "keyC"]
assert all(rs is True for _, rs in reset_calls)
assert sorted(k for k, _ in reset_calls) == ["keyA", "keyC"]
assert sorted(saved) == ["keyA", "keyC"]
def test_reset_prompt_keys_skips_when_primary_only(monkeypatch):
db = _db({"a": (["alpha"], ["AAA"], "keyA")})
monkeypatch.setattr(templates, "load_shortcut_db", lambda: db)
monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {})
studio = conditioning.FunPackVideoRefinerV2()
monkeypatch.setattr(studio, "_v2_load_state", lambda *a, **k: ({}, "fresh"))
monkeypatch.setattr(studio, "_v2_save_state", lambda *a, **k: None)
# primary key is keyA itself -> nothing extra to reset
assert studio._v2_reset_prompt_keys("keyA", "alpha shot") == []
# no keyed shortcuts at all -> nothing extra
assert studio._v2_reset_prompt_keys("default", "plain prompt") == []
def test_bridge_scene_refinement_keys_preview(monkeypatch):
"""Preview payload: explicit keys vs project-default fallback per scene."""
from movie_editor.backend import bridge