Store what went WRONG, not just what was asked for. Ratings 'Awful' and 'Wrong appearance' promote that run's final video latent (fp16, batch squeezed) + mean-pooled conditioning into a per-key negative bank (<key>.negative_latents.pt, ring buffer of 8). The Chain Sampler writes a pending candidate at the end of every keyed run; the rating that scores that run promotes or discards it - same pairing as the x0 snapshot, and the stem glob in delete_refinement_key covers both new sidecars. Sampling (dynashift toggle + strength/threshold, off by default): per late step each current video frame is pooled and compared against every banked negative frame (alignment-free in time, so chain position / guide tails don't matter; other resolutions are skipped). Frames matching above threshold get the matched negative frame's aligned component subtracted (projection removal, coef clamped >= 0), scaled by above-threshold margin - steering self-releases once the unwanted feature is gone. Negatives are weighted by prompt similarity so bad gens from unrelated prompts steer less. No uncond pass, no extra model forward; audio byte-identical (video span slice + cat). Wrapper rides the tagged per-scene lifecycle (leak-strip covered) and threads through batch training. Editor: DynaShift knobs land in Engine Settings > Guidance same-change (sampler_inputs pass-through, no backend change), verified rendering live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
123 lines
5.0 KiB
Python
123 lines
5.0 KiB
Python
"""DynaShift negative latent memory.
|
|
|
|
Per-refinement-key store of generations the user rated as containing something
|
|
UNWANTED (an extra person, wrong appearance, ...). The Scene Chain sampler saves the
|
|
final video latent of every keyed run as a PENDING entry; the rating that scores that
|
|
run either promotes it into the bank (negative ratings) or discards it. At sampling
|
|
time the bank acts as a latent-space negative prompt at CFG=1: see
|
|
samplers._build_dynashift_wrapper for the steering math.
|
|
|
|
This is the latent-space sibling of the Refiner's conditioning-space `bad_dir`:
|
|
bad_dir repels the INPUT (what we ask for), the negative bank repels the OUTPUT
|
|
(what actually appeared). Files ride the refinement-key sidecar convention
|
|
(<key>.negative_latents.pt / <key>.pending_negative.pt), so delete_refinement_key's
|
|
stem glob removes them atomically with the key - no orphaned-sidecar drift.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
|
|
import torch
|
|
|
|
# Bank size: enough to cover the recurring failure modes of one key without turning
|
|
# the store into a museum. Oldest entries roll off; keys are disposable anyway.
|
|
MAX_NEGATIVES = 8
|
|
|
|
# V2 rating profile keys whose semantics are "something appeared that I did not want".
|
|
# Missing-* ratings are absences, not intrusions - steering away from an absence would
|
|
# push toward arbitrary content, so they never feed the bank.
|
|
NEGATIVE_RATING_KEYS = ("awful", "wrong_appearance")
|
|
|
|
|
|
def _state_path(refinement_key, mode):
|
|
try:
|
|
from .conditioning import refinement_state_path
|
|
except ImportError:
|
|
from conditioning import refinement_state_path
|
|
return refinement_state_path(refinement_key, mode, prefix="refine_v2", extension="pt")
|
|
|
|
|
|
def _atomic_save(payload, path):
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
tmp = path + ".tmp"
|
|
torch.save(payload, tmp)
|
|
os.replace(tmp, path) # a concurrent read never sees a partial write
|
|
|
|
|
|
def save_pending(refinement_key, video_latent, conditioning=None):
|
|
"""Persist this run's final video latent (+ mean-pooled conditioning) as the
|
|
pending candidate the NEXT rating will judge. fp16 CPU, batch squeezed; one
|
|
pending per key (a new run overwrites - each rating scores its own run)."""
|
|
if not refinement_key or not isinstance(video_latent, torch.Tensor):
|
|
return False
|
|
try:
|
|
lat = video_latent.detach()
|
|
if lat.dim() >= 4 and lat.shape[0] == 1:
|
|
lat = lat.squeeze(0)
|
|
cond_vec = None
|
|
if isinstance(conditioning, torch.Tensor):
|
|
c = conditioning.detach().float()
|
|
if c.dim() == 3:
|
|
c = c.squeeze(0)
|
|
if c.dim() == 2:
|
|
c = c.mean(dim=0)
|
|
cond_vec = c.to(torch.float16).cpu()
|
|
_atomic_save({
|
|
"latent": lat.to(torch.float16).cpu(),
|
|
"cond": cond_vec,
|
|
"stamp": time.strftime("%Y-%m-%d_%H-%M-%S"),
|
|
}, _state_path(refinement_key, "pending_negative"))
|
|
return True
|
|
except Exception as e:
|
|
print(f"[FunPack DynaShift] pending negative save failed: {e}")
|
|
return False
|
|
|
|
|
|
def consume_pending(refinement_key, promote):
|
|
"""Pair the pending latent with the rating that scores its run: promote it into
|
|
the negative bank (ring buffer) or discard it. Always removes the pending file so
|
|
a stale candidate can never be promoted by a later, unrelated rating. Returns the
|
|
bank size after promotion, or None when nothing was promoted."""
|
|
if not refinement_key:
|
|
return None
|
|
pending_path = _state_path(refinement_key, "pending_negative")
|
|
if not os.path.exists(pending_path):
|
|
return None
|
|
count = None
|
|
try:
|
|
if promote:
|
|
pending = torch.load(pending_path, map_location="cpu", weights_only=False)
|
|
if isinstance(pending, dict) and isinstance(pending.get("latent"), torch.Tensor):
|
|
store_path = _state_path(refinement_key, "negative_latents")
|
|
entries = load_negatives(refinement_key)
|
|
entries.append(pending)
|
|
entries = entries[-MAX_NEGATIVES:]
|
|
_atomic_save({"version": 1, "entries": entries}, store_path)
|
|
count = len(entries)
|
|
except Exception as e:
|
|
print(f"[FunPack DynaShift] negative bank update failed: {e}")
|
|
finally:
|
|
try:
|
|
os.remove(pending_path)
|
|
except OSError:
|
|
pass
|
|
return count
|
|
|
|
|
|
def load_negatives(refinement_key):
|
|
"""Bank entries ([{latent: fp16 [C,T,H,W], cond: fp16 [D]|None, stamp}], oldest
|
|
first), or [] when the key has no bank yet."""
|
|
if not refinement_key:
|
|
return []
|
|
path = _state_path(refinement_key, "negative_latents")
|
|
if not os.path.exists(path):
|
|
return []
|
|
try:
|
|
data = torch.load(path, map_location="cpu", weights_only=False)
|
|
entries = data.get("entries") if isinstance(data, dict) else None
|
|
return [e for e in (entries or [])
|
|
if isinstance(e, dict) and isinstance(e.get("latent"), torch.Tensor)]
|
|
except Exception as e:
|
|
print(f"[FunPack DynaShift] negative bank load failed: {e}")
|
|
return []
|