Multi-key: stop training every refinement key on every clip

Per-scene key attribution diverged from the generation split whenever the
advisor rewrote prompt_to_encode away from the raw prompt, so
resolve_scene_refinement_keys hit its all-keys UNION fallback and assigned
every key to every scene. That union got persisted into the run and read
back at rating time, training every key on every clip (value_fn bloat).

- prompt_scene_shortcut_keys now scans both expanded and original text
  (mirrors split_timeline_verbatim), so identical structure -> matching
  scene count -> precise attribution far more often.
- Divergence fallback is now default-key only (empty sets), not the
  all-keys union; a diverged run under-trains instead of cross-training.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-14 23:22:12 +03:00
co-authored by Claude Opus 4.8
parent 5ed5fcd59a
commit ab4f78695d
3 changed files with 33 additions and 7 deletions
+11
View File
@@ -2,6 +2,17 @@
## [Unreleased]
### Fixed
- Multi-key refinement: stop training every key on every clip. Per-scene key attribution
(`prompt_scene_shortcut_keys`) now scans both the expanded and original prompt — matching the
generation splitter `split_timeline_verbatim` — so structurally-identical prompts land on the
same scene count and attribution stays precise. When the count still diverges (advisor/repair
rewrote `prompt_to_encode` away from the raw prompt), `resolve_scene_refinement_keys` now falls
back to the project **default key only** (empty sets) instead of the all-keys **union**. The
union assigned every key to every scene, which both steered and — once persisted into the run
and read back at rating time — trained every key on every clip (cross-contamination + value
function bloat, e.g. an ~80 MB jump on a single 3-clip rating pass).
## [3.0.1] - 2026-06-14
### Added
+19 -5
View File
@@ -857,6 +857,7 @@ def prompt_scene_shortcut_keys(prompt):
split_pattern = re.compile(_GENERIC_SCENE_LABEL_PATTERN, re.IGNORECASE)
cuts = []
# (1) Scan the EXPANDED text: catches a shortcut whose expansion resolves to a transition.
for m in split_pattern.finditer(expanded):
info = custom_map.get(re.sub(r"\s+", " ", m.group(0).strip().lower())) or {}
if info.get("placement") == "start":
@@ -864,6 +865,14 @@ def prompt_scene_shortcut_keys(prompt):
else:
orig = _project_offset(pieces, m.end(), "after")
cuts.append(orig)
# (2) Scan the ORIGINAL text too: a transition trigger that is ALSO a shortcut gets expanded
# away in (1) before detection, so its split would be lost. Matching the verbatim text
# catches it at its true offset. This mirrors split_timeline_verbatim so this attribution
# split lands on the same scene count as the generation split (fewer divergence fallbacks).
for m in split_pattern.finditer(text):
info = custom_map.get(re.sub(r"\s+", " ", m.group(0).strip().lower())) or {}
orig = m.start() if info.get("placement") == "start" else m.end()
cuts.append(orig)
seen, uniq = set(), []
for off in sorted(cuts):
if off <= 0 or off in seen:
@@ -891,10 +900,12 @@ def resolve_scene_refinement_keys(raw_positive, scene_count):
"""Per-scene non-default refinement keys, aligned to split_scene_texts.
A key participates in a scene when one of its bound shortcuts fired in that scene's
text (anchor keys count for every scene). Falls back to the union across all scenes
when attribution can't be trusted — the raw-prompt scene count diverges from the
actual split (advisor/repair rewrote the prompt, unusual transition stacking, etc.).
Returns a list of sets aligned to scenes; empty sets => default key (today's path).
text (anchor keys count for every scene). Falls back to the project DEFAULT key only
(empty sets) when attribution can't be trusted — the raw-prompt scene count diverges
from the actual split (advisor/repair rewrote the prompt, unusual transition stacking,
etc.). The old behaviour here was the all-keys UNION, which trained EVERY key on EVERY
scene (cross-contamination + value-function bloat); under-training one diverged run is
far safer. Returns a list of sets aligned to scenes; empty sets => default key.
This is the single source of truth shared by generation (FunPackStudio._v2_scene_refinement_keys)
and the Movie Editor preview, so what the editor shows is exactly what will run."""
@@ -908,7 +919,10 @@ def resolve_scene_refinement_keys(raw_positive, scene_count):
return [set() for _ in range(scene_count)]
if n == scene_count:
return scene_sets
return [set(all_keys) for _ in range(scene_count)]
# Divergence: attribution cannot be trusted. Steer/train with the project default key only
# (empty sets) instead of the all-keys union, so a diverged run never trains every key on
# every scene. all_keys stays available to callers that need the full set (e.g. session reset).
return [set() for _ in range(scene_count)]
def parse_timeline_segments(prompt):
+3 -2
View File
@@ -176,9 +176,10 @@ def test_studio_scene_refinement_keys_fallback(monkeypatch):
raw = "wide shot scene 1 alpha here scene 2 beta here" # attribution finds 2 scenes
# scene_count matches -> precise per-scene
assert studio._v2_scene_refinement_keys(raw, 2) == [{"keyA"}, {"keyB"}]
# scene_count diverges (advisor rewrote / stacking) -> safe union for every scene
# scene_count diverges (advisor rewrote / stacking) -> default key only (empty sets), NOT the
# all-keys union; a union here would train every key on every scene (cross-contamination).
out = studio._v2_scene_refinement_keys(raw, 3)
assert out == [{"keyA", "keyB"}, {"keyA", "keyB"}, {"keyA", "keyB"}]
assert out == [set(), set(), set()]
def test_studio_scene_refinement_keys_no_bindings(monkeypatch):