Files
olivv-cs-ComfyUI-FunPack/negative_memory.py
T
digital-garbageandClaude Fable 5 9195906c03 DynaShift intake: bank the quality-missing rating family, not just intrusions
'Awful' / 'Wrong appearance' banked on intrusion semantics; now any rating
whose profile reward marks the run a bad OUTCOME (<= -0.25: Missing
quality, Missing details+quality, Missing action+quality, Missing
details+action+quality) also feeds the negative bank - degradation is
visible in the latent, so steering off that attractor is meaningful.
Near-miss ratings with positive reward (Missing details +0.35, Missing
action +0.05) stay excluded: those latents are mostly-wanted content and
banking them would repel future runs from material the user asked for.

Decision moves into negative_memory.is_negative_profile (intrusion keys OR
reward threshold, robust to future profiles); promoted entries now carry
the rating key as provenance for severity-weighted steering later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:29:46 +03:00

148 lines
6.2 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" -
# these bank regardless of reward (wrong_appearance carries reward 0.0 because it is a
# repair signal, but it is THE canonical intrusion rating).
NEGATIVE_RATING_KEYS = ("awful", "wrong_appearance")
# Ratings without an intrusion label still bank when the profile reward marks the run a
# genuinely bad OUTCOME: the quality-missing family (Missing quality -0.25 through
# Missing details+action+quality -0.65) - degradation is visible in the latent, so
# steering off that attractor is meaningful. Deliberately excludes the near-miss
# ratings with positive reward (Missing details +0.35, Missing action +0.05): those
# latents are mostly-correct content, and banking them would repel future runs from
# material the user actually wants.
NEGATIVE_REWARD_THRESHOLD = -0.25
def is_negative_profile(learning_profile):
"""True when a V2 rating profile should feed the negative bank."""
if not isinstance(learning_profile, dict):
return False
if learning_profile.get("key") in NEGATIVE_RATING_KEYS:
return True
try:
return float(learning_profile.get("reward", 0.0)) <= NEGATIVE_REWARD_THRESHOLD
except (TypeError, ValueError):
return False
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, rating_key=None):
"""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. `rating_key` is
stored on the entry as provenance (which rating banked it - available for
severity-weighted steering later)."""
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):
if rating_key:
pending["rating"] = str(rating_key)
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 []