feat(h3): reference weight applies only after the shot has been composed
Measured: 0.4 loose and inventive, 0.6-0.75 several versions of the shot attempted at once with the strongest winning, 0.75 and up simply the reference. No usable band anywhere in the range. The timestep is how the model knows what stage everything is at, so a prompt that disagrees with the picture about the stage costs it the ability to settle on one reading — which is what "multiple variants in one video" is. Structure is chosen early, so the edit now waits until it has been: ramped in through _make_steer_ramp, the same late-step gate the rating-driven wrappers share, interpolating from the prompt's own row so a closed gate is byte-identical to not being installed. Third hypothesis for this dial. If it still just reproduces the reference, the feature does not earn its place and comes out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
217882e695
commit
cb5910568e
+41
-6
@@ -1516,16 +1516,37 @@ def last_prompt_segment(mod_segments):
|
||||
return found
|
||||
|
||||
|
||||
def current_sigma(transformer_options):
|
||||
"""The step's sigma, or None when the sampler did not record one."""
|
||||
try:
|
||||
sigmas = (transformer_options or {}).get("sigmas")
|
||||
return float(sigmas.flatten()[0]) if sigmas is not None else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
class PromptTimestepBlock:
|
||||
"""Per-block replace patch: run the block with the prompt on its own timestep.
|
||||
|
||||
Appends one row to ``t_emb`` and points the prompt's segment at it. Every index the
|
||||
model computed for its own rows still means what it meant — the new row is only ever
|
||||
addressed by the segment rewritten here.
|
||||
|
||||
RAMPED, not constant. The timestep is how the model knows what stage everything is at,
|
||||
and a prompt that disagrees with the picture about the stage costs it the ability to
|
||||
settle on one reading: reported from a rental at 0.6-0.75 as several versions of the
|
||||
shot being attempted at once in a single video, with the strongest signal winning.
|
||||
Structure is chosen early, so the edit is held off until it is chosen — `ramp` is the
|
||||
same late-step gate the rating-driven wrappers share, zero over the first half of the
|
||||
schedule and rising to full at the end.
|
||||
|
||||
The ramp interpolates from the prompt's OWN current row, which the segment names, so a
|
||||
gate of 0 is byte-identical to not being installed at all.
|
||||
"""
|
||||
|
||||
def __init__(self, row):
|
||||
def __init__(self, row, ramp=None):
|
||||
self.row = row
|
||||
self.ramp = ramp
|
||||
|
||||
def __call__(self, args, extra):
|
||||
run = extra["original_block"]
|
||||
@@ -1534,19 +1555,32 @@ class PromptTimestepBlock:
|
||||
target = last_prompt_segment(segments)
|
||||
if target is None or t_emb is None:
|
||||
return run(args)
|
||||
gate = 1.0
|
||||
if self.ramp is not None:
|
||||
sigma = current_sigma(args.get("transformer_options"))
|
||||
if sigma is not None:
|
||||
try:
|
||||
gate = max(0.0, min(1.0, float(self.ramp(sigma))))
|
||||
except Exception:
|
||||
gate = 1.0
|
||||
if gate <= 0.0:
|
||||
return run(args) # too early to disagree about the stage
|
||||
try:
|
||||
extra_row = self.row(t_emb)
|
||||
except Exception: # no time basis to read — run the prompt as trained
|
||||
return run(args)
|
||||
rows = int(t_emb.shape[0])
|
||||
start, stop, _ = segments[target][:3]
|
||||
start, stop, row_index = segments[target][:3]
|
||||
if gate < 1.0:
|
||||
own = t_emb[row_index // _ADALN_MODALITIES].unsqueeze(0)
|
||||
extra_row = torch.lerp(own, extra_row.to(own.dtype), gate)
|
||||
rewritten = list(segments)
|
||||
rewritten[target] = (start, stop, rows * _ADALN_MODALITIES + MODALITY_TAGS["text"])
|
||||
return run({**args, "t_emb": torch.cat([t_emb, extra_row], dim=0),
|
||||
"mod_segments": rewritten})
|
||||
|
||||
|
||||
def apply_prompt_timestep(model, timestep):
|
||||
def apply_prompt_timestep(model, timestep, ramp=None):
|
||||
"""Give the prompt rows their own timestep on every block. Returns (model, note)."""
|
||||
if float(timestep or 0.0) <= 0.0:
|
||||
return model, None
|
||||
@@ -1561,11 +1595,12 @@ def apply_prompt_timestep(model, timestep):
|
||||
if row is None:
|
||||
return model, f"Prompt timestep skipped — {why}."
|
||||
patched = model.clone()
|
||||
patch = PromptTimestepBlock(row)
|
||||
patch = PromptTimestepBlock(row, ramp=ramp)
|
||||
for index in range(len(blocks)):
|
||||
patched.set_model_patch_replace(patch, "dit", "double_block", index)
|
||||
return patched, (f"Prompt read at timestep {float(timestep):g} on {len(blocks)} blocks "
|
||||
f"— the prompt rows only, never the reference label ahead of them.")
|
||||
where = "ramped in over the second half of the schedule" if ramp is not None \
|
||||
else "on every step"
|
||||
return patched, (f"Reference weight {float(timestep):g} on {len(blocks)} blocks, {where}.")
|
||||
|
||||
|
||||
# ── final layer: the one video-only edit ─────────────────────────────────────
|
||||
|
||||
@@ -358,7 +358,7 @@
|
||||
hint: "Makes the picture crisper or softer without changing the sound at all. Above 1.0 = more detail and contrast, below is softer. The step from 1.0 is small — reach for 1.4-1.8 before deciding it does nothing. Free.",
|
||||
detail: "Every block of the model shares one attention pass, so anything that moves the picture also reaches the soundtrack. This runs after the last one, where there is no path left for it to travel — it is the audio-safe twin of Write gain \u00b7 picture." },
|
||||
{ name: "h3_prompt_time", label: "Reference weight", kind: "float", default: 0.0, min: 0.0, max: 1.0, step: 0.01,
|
||||
hint: "How literally the model takes the reference you gave it. 1.0 reproduces it almost exactly. Around 0.4 it follows the prompt but invents things that were in neither. Below 0.3 the picture starts breaking up. 0.0 = off. Measured on a rental, not predicted \u2014 start at 0.7. Free.",
|
||||
hint: "How much the reference you gave it drives the final video. It now only applies over the second half of the schedule, so the shot is composed first and pulled toward the reference after. 0.0 = off. Try 1.0 \u2014 earlier builds applied it throughout and anything above about 0.5 simply reproduced the reference. Free.",
|
||||
detail: "The model marks each part of its sequence as finished or still forming, and \u0027finished\u0027 is how it recognises reference material \u2014 something given, to be reproduced. This dial sets that mark on your prompt. Turned up, the prompt joins the given material and the reference image takes over the frame; turned down, the prompt reads as a rough draft and the model fills in around it. It was built expecting to improve prompt adherence and does not: what it actually moves was found by testing. Not learned from ratings, because its off value sits at the end of its range rather than the middle." },
|
||||
{ name: "segmented_detailing", label: "Segmented detailing (region refine)", kind: "bool", default: false,
|
||||
hint: "Re-renders small regions like hands at higher detail after each scene. Costs about 15%.",
|
||||
|
||||
+5
-1
@@ -6957,7 +6957,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
"not a MiniMax H3 model. Per-row timesteps are an H3-only lane.")
|
||||
return model
|
||||
try:
|
||||
patched, note = h3mod.apply_prompt_timestep(model, timestep)
|
||||
patched, note = h3mod.apply_prompt_timestep(
|
||||
model, timestep, ramp=getattr(self, "_steer_ramp", None))
|
||||
except Exception as error: # noqa: BLE001
|
||||
_log.failed("FunPackSceneChain", "Prompt timestep", error,
|
||||
"the prompt is read at the picture's own noise level")
|
||||
@@ -7352,6 +7353,9 @@ class FunPackLTXAVSceneChainSampler:
|
||||
# The gate every rating-driven wrapper shares. On H3 it is read off the schedule's
|
||||
# own base grid; on LTX it stays the absolute-sigma gate it was validated with.
|
||||
_steer_ramp = _make_steer_ramp(sigmas, self._is_h3)
|
||||
# Also read by the reference-weight patch, which is installed per scene rather than
|
||||
# threaded through the sample call like the wrappers below.
|
||||
self._steer_ramp = _steer_ramp
|
||||
if self._is_h3:
|
||||
# H3 only generates on a 17k+5 pixel-frame grid. Empty MiniMax H3 AV Latent snaps
|
||||
# its own `length` up silently, so an off-grid scene length produces a template
|
||||
|
||||
@@ -256,10 +256,9 @@ def test_a_core_without_the_block_hook_declines_rather_than_half_working():
|
||||
assert "per-block replace hook" in note
|
||||
|
||||
|
||||
def test_the_note_says_the_label_is_left_alone():
|
||||
"""The one thing a reader needs to know, because the first version did the opposite."""
|
||||
def test_the_note_names_the_value_and_when_it_applies():
|
||||
_out, note = h3.apply_prompt_timestep(FakePatcher(), 0.9)
|
||||
assert "never the reference label" in note
|
||||
assert "Reference weight 0.9" in note and "every step" in note
|
||||
|
||||
|
||||
# ── what it is called, and why ──────────────────────────────────────────────
|
||||
@@ -291,3 +290,91 @@ def test_the_editor_label_matches():
|
||||
/ "engine_settings.js").read_text(encoding="utf-8")
|
||||
row = next(ln for ln in js.splitlines() if '{ name: "h3_prompt_time"' in ln)
|
||||
assert 'label: "Reference weight"' in row
|
||||
|
||||
|
||||
# ── ramped, so the model can commit first ───────────────────────────────────
|
||||
#
|
||||
# The timestep is how the model knows what stage everything is at. A prompt that disagrees
|
||||
# with the picture about the stage costs it the ability to settle on ONE reading — reported
|
||||
# from a rental at 0.6-0.75 as several versions of the shot attempted at once in a single
|
||||
# video, strongest signal winning. Structure is chosen early, so the edit is held off until
|
||||
# it has been chosen.
|
||||
|
||||
def ramped(segments, t_emb, sigma, ramp, row=None):
|
||||
seen = {}
|
||||
|
||||
def original(args):
|
||||
seen.update(args)
|
||||
return args["img"]
|
||||
patch = h3.PromptTimestepBlock(row or constant_row(0.95), ramp=ramp)
|
||||
patch({"img": "IMG", "t_emb": t_emb, "mod_segments": segments,
|
||||
"transformer_options": {"sigmas": torch.tensor([sigma])}},
|
||||
{"original_block": original})
|
||||
return seen
|
||||
|
||||
|
||||
def half_way(sigma):
|
||||
"""Stand-in for _make_steer_ramp: nothing early, full late."""
|
||||
return 0.0 if sigma > 0.5 else 1.0
|
||||
|
||||
|
||||
def test_early_steps_run_exactly_as_if_it_were_not_installed():
|
||||
"""Not 'weaker' — identical. The disagreement is the cost, so there must be none."""
|
||||
got = ramped(T2V, torch.tensor([[0.2]]), sigma=0.9, ramp=half_way)
|
||||
assert got["t_emb"].shape[0] == 1
|
||||
assert got["mod_segments"] == T2V
|
||||
|
||||
|
||||
def test_late_steps_get_the_full_value():
|
||||
got = ramped(T2V, torch.tensor([[0.2]]), sigma=0.1, ramp=half_way,
|
||||
row=constant_row(0.93))
|
||||
assert float(got["t_emb"][1][0]) == pytest.approx(0.93)
|
||||
|
||||
|
||||
def test_a_partial_gate_interpolates_from_the_prompts_own_row():
|
||||
"""From its OWN row, which the segment names — so a gate of 0 is the untouched model
|
||||
rather than 'whatever row 0 happens to be'."""
|
||||
segments = [seg(0, 10, 1, TEXT)] # the prompt sits at t_row 1
|
||||
t_emb = torch.tensor([[0.2], [0.6]])
|
||||
got = ramped(segments, t_emb, sigma=0.0, ramp=lambda _s: 0.5, row=constant_row(1.0))
|
||||
assert float(got["t_emb"][2][0]) == pytest.approx(0.8) # halfway 0.6 -> 1.0
|
||||
|
||||
|
||||
def test_no_ramp_means_every_step( ):
|
||||
"""Without a ramp the patch is unconditional, which is what the tests above assume."""
|
||||
got = run_block(T2V, torch.tensor([[0.2]]))
|
||||
assert got["t_emb"].shape[0] == 2
|
||||
|
||||
|
||||
def test_a_sampler_that_records_no_sigma_still_applies_it():
|
||||
"""Better fully on than silently off: an inert feature that reports itself as active is
|
||||
the failure mode funpack_log exists to stop."""
|
||||
seen = {}
|
||||
|
||||
def original(args):
|
||||
seen.update(args)
|
||||
return args["img"]
|
||||
h3.PromptTimestepBlock(constant_row(), ramp=half_way)(
|
||||
{"img": "IMG", "t_emb": torch.tensor([[0.2]]), "mod_segments": T2V,
|
||||
"transformer_options": {}}, {"original_block": original})
|
||||
assert seen["t_emb"].shape[0] == 2
|
||||
|
||||
|
||||
def test_a_ramp_that_raises_does_not_take_the_render_with_it():
|
||||
def boom(_sigma):
|
||||
raise RuntimeError("no")
|
||||
got = ramped(T2V, torch.tensor([[0.2]]), sigma=0.1, ramp=boom)
|
||||
assert got["t_emb"].shape[0] == 2
|
||||
|
||||
|
||||
def test_the_note_says_it_is_ramped():
|
||||
_out, note = h3.apply_prompt_timestep(FakePatcher(), 0.9, ramp=half_way)
|
||||
assert "second half of the schedule" in note
|
||||
|
||||
|
||||
def test_the_sampler_hands_it_the_ramp_the_other_wrappers_use():
|
||||
import inspect
|
||||
import samplers
|
||||
src = inspect.getsource(
|
||||
samplers.FunPackLTXAVSceneChainSampler._install_h3_prompt_timestep)
|
||||
assert 'ramp=getattr(self, "_steer_ramp", None)' in src
|
||||
|
||||
Reference in New Issue
Block a user