Files
digital-garbageandClaude Sonnet 5.5 f5050a4574 Shorter in-app hints: what a setting does, where to start, what it needs
Long hints for 33 settings are cut to one or two sentences each, and the
Taste steering strength and blocks hints now give a starting value and
recommended blocks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 13:26:17 +02:00

131 lines
5.3 KiB
Python

"""Taste steering: push chosen blocks' picture rows toward what you liked.
REINS-style ("Pulling The REINS", arXiv:2606.17257), training-free: every run
records the average picture row at every block on its last step. Once a block
has 2+ liked and 2+ disliked runs on the taste key, liked-minus-disliked is
added back into that block's picture rows, scaled by their own size.
Captured at EVERY block, steers only the named ones: changing which blocks
steer never starts the learning over. Captured before the push, so what is
learned is the model's own behaviour, not this run's strength.
Picture rows only. Sound follows through joint attention, never by being
pushed directly (the LTXAV self-consistency failure was exactly that).
"""
import torch
from ..._core import dit_hooks, log, registry
ID = "reins"
TITLE = "Taste steering (REINS)"
MOUNT = "generation.sampling"
STAGE = "guidance"
CATEGORY = "guidance"
STATUS = "experimental"
REQUIRES = ["block_rows"]
USES = ["taste_store"]
SETTINGS = {
"enabled": {
"type": "bool", "default": False,
"label": "Taste steering (REINS)",
"hint": "Adds stylistic changes according to your ratings. Needs 2+ liked and 2+ disliked runs on the Taste key.",
},
"strength": {
"type": "float", "default": 0.01, "min": 0.0, "max": 2.0, "step": 0.01,
"label": "Strength", "ui": "slider",
"hint": "0 only learns. Start with a low value around 0.02; too high breaks the picture.",
"when": {"enabled": True},
},
"blocks": {
"type": "text", "model_blocks": True, "default": "43-44",
"label": "Blocks",
"hint": "Where it steers, e.g. 43-44. Recommended: any, except 0-2 and 46-49. Every block keeps learning either way.",
"when": {"enabled": True},
},
}
KIND = "reins"
def _say(message):
log.once(f"{ID}:{message}", log.ALERT, "FunUI Taste steering (REINS)", message)
def install(patcher, values, key):
if not values.get("enabled"):
return None
taste = registry.current().ask("taste_store", patcher)
if taste is None:
_say("off: set a Taste key first -- it is what your ratings teach")
return None
n = dit_hooks.block_count(patcher)
steer, problems = dit_hooks.parse_blocks(values.get("blocks"), n)
for problem in problems:
_say(f"ignored {problem}")
strength = float(values.get("strength", 0.0))
live = {"dirs": {}}
cast = {}
def fresh():
live["dirs"], summary = taste.directions(KIND, steer, strength)
cast.clear()
log.once(f"{ID}:state", log.INFO, "FunUI Taste steering (REINS)", summary)
captured = taste.collect(patcher, key, KIND, fresh=fresh)
spans = {}
def video_span(args, out):
seq = int(out.shape[0])
if seq not in spans:
spans[seq] = dit_hooks.row_span(dit_hooks.target_rows(
args.get("mod_segments"), seq, out.device, "video"))
return spans[seq]
def make(block):
def hook(args, extra):
out = extra["original_block"](args)["img"]
if isinstance(out, (tuple, list)): # an audio+video block hands back (video, audio): the video is all picture
rows, width = out[0].reshape(-1, out[0].shape[-1]), out[0].shape[-1]
else:
span = video_span(args, out)
if span is None:
return {"img": out}
rows, width = out[span[0]:span[1]], out.shape[-1]
if dit_hooks.last_step(args.get("transformer_options")):
captured[block] = rows.mean(0, dtype=torch.float32).detach()
direction = live["dirs"].get(block)
if direction is None or strength <= 0.0:
return {"img": out}
if direction.numel() != width:
_say(f"block {block}'s learned direction is from a different model; "
"learning only there")
return {"img": out}
d = cast.get((block, rows.dtype))
if d is None:
d = cast[(block, rows.dtype)] = direction.to(device=rows.device, dtype=rows.dtype)
norm = torch.linalg.vector_norm(rows, dim=-1, dtype=torch.float32).mean()
# The Block map (when this model has a profile) nudges the strength per block; a big setting is kept, up to what the block takes.
push = dit_hooks.block_map_value(args.get("transformer_options"), "residual", block, strength, steer)
# No host sync: a bad norm makes the push zero instead of a branch.
scale = torch.where(torch.isfinite(norm) & (norm > 0), norm * push,
torch.zeros_like(norm))
rows += d * scale.to(rows.dtype)
if isinstance(out, (tuple, list)) and rows.data_ptr() != out[0].data_ptr():
out[0].copy_(rows.view_as(out[0])) # the reshape had to copy: put the push back
return {"img": out}
return hook
for b in range(n):
dit_hooks.add_block_hook(patcher, key, b, make(b))
# Changes what the hooked blocks allocate: see dit_hooks.without_compiler.
dit_hooks.without_compiler(patcher, key)
return (f"learning at every block; steers {','.join(map(str, steer))} at {strength:g} "
"once each has 2 liked + 2 disliked (read fresh every run)")
PROVIDES = {"modifier": install}