Guide blur: own strength + sigma-threshold controls, independent of ALG's
alg_blur_guides no longer borrows the sampler's anchor knobs: the Scene Chain Sampler gains alg_guide_blur_strength (default 2.0) and alg_guide_blur_sigma_threshold (default 0.975), threaded per-chunk via extra_options like the tail count. The sampler now precomputes one latent per (anchor blurred?, tail blurred?) combination — the frame sets are disjoint, so the both-blurred variant composes the two passes — and each region follows its own sigma window per step. Anchor ALG (alg_enabled/alg_strength/ alg_sigma_threshold) is untouched and back to anchor-only semantics. Editor: 'Guide blur strength' and 'Guide blur sigma threshold' rows appear under Engine ▸ Experimental when the blur toggle is on (sampler_inputs bridge, names match the new node widgets; inputs appended at the block end to keep the builder's positional widget mapping aligned). Verified live: object_info shows both widgets appended last; Editor rows appear on toggle and persist to project.sampler_inputs. 11 ALG unit tests pass (new: composed independent-strength blur); same 20 pre-existing suite failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6528acf504
commit
b8422d647b
@@ -173,6 +173,8 @@
|
||||
{ name: "joyai_audio_memory", label: "Paired audio memory", kind: "bool", default: false, dependsOn: "joyai_memory" },
|
||||
{ name: "v2a_grad_scale", label: "Video→audio coupling", kind: "float", default: 1.0, min: 0.0, max: 4.0, step: 0.25, dependsOn: "joyai_audio_memory" },
|
||||
{ name: "alg_blur_guides", label: "Blur i2v guides and JoyAI memory", kind: "bool", default: false },
|
||||
{ name: "alg_guide_blur_strength", label: "Guide blur strength", kind: "float", default: 2.0, min: 1.0, max: 4.0, step: 0.1, dependsOn: "alg_blur_guides" },
|
||||
{ name: "alg_guide_blur_sigma_threshold", label: "Guide blur sigma threshold", kind: "float", default: 0.975, min: 0.5, max: 0.999, step: 0.005, dependsOn: "alg_blur_guides" },
|
||||
{ name: "bounded_attention_enabled", label: "Bounded attention (multi-subject)", kind: "bool", default: false },
|
||||
{ name: "dynashift", label: "DynaShift (steer off bad gens)", kind: "bool", default: false },
|
||||
{ name: "dynashift_strength", label: "DynaShift strength", kind: "float", default: 0.3, min: 0.05, max: 1.0, step: 0.05, dependsOn: "dynashift" },
|
||||
@@ -281,7 +283,7 @@
|
||||
chain_timing: ["frame_overlap", "transition_duration", "use_same_seed"],
|
||||
chain_guidance: ["cfg", "embed_guidance", "embed_guidance_source", "embed_guidance_strength", "score_slider", "score_slider_strength", "output_guidance", "output_guidance_strength", "dynashift", "dynashift_strength", "dynashift_threshold"],
|
||||
chain_decode: ["decode_noise_scale", "decode_timestep", "decode_tile_size"],
|
||||
chain_experimental: ["mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "bounded_attention_enabled"],
|
||||
chain_experimental: ["mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "alg_guide_blur_strength", "alg_guide_blur_sigma_threshold", "bounded_attention_enabled"],
|
||||
};
|
||||
|
||||
function countChainView(p, id) {
|
||||
|
||||
+67
-19
@@ -1755,6 +1755,7 @@ def sample_funpack_distilled_flow(model, x, sigmas, extra_args=None, callback=No
|
||||
rescue_prompt_sig=None,
|
||||
alg_enabled=False, alg_strength=2.0, alg_sigma_threshold=0.975,
|
||||
alg_guide_tail_frames=0,
|
||||
alg_guide_blur_strength=2.0, alg_guide_blur_sigma_threshold=0.975,
|
||||
mg_enabled=False, mg_strength=0.5, mg_decay=0.5, mg_sigma_threshold=0.975,
|
||||
quality_sharpness=0.0):
|
||||
"""
|
||||
@@ -1784,8 +1785,10 @@ def sample_funpack_distilled_flow(model, x, sigmas, extra_args=None, callback=No
|
||||
attention frames this scene (mid_scene_guide / carry_i2v_guides-as-guide /
|
||||
configured guides / JoyAI memory), set by the Scene Chain Sampler's alg_blur_guides
|
||||
toggle right before this chunk's sample call. 0 = untouched (default). Standalone:
|
||||
works with alg_enabled off (anchor stays sharp, only the guide tail is blurred),
|
||||
reusing the same alg_strength / alg_sigma_threshold parameters.
|
||||
works with alg_enabled off (anchor stays sharp, only the guide tail is blurred), with
|
||||
its own independent controls — alg_guide_blur_strength / alg_guide_blur_sigma_threshold
|
||||
(also set per-chunk by the Scene Chain Sampler), separate from the anchor's
|
||||
alg_strength / alg_sigma_threshold.
|
||||
- EXPERIMENTAL mg_enabled: Momentum Guidance (arXiv:2602.20360). Keeps a running EMA
|
||||
(decay=mg_decay) of the per-step ODE direction and blends the current step's
|
||||
direction toward it (weight=mg_strength) while sigma is BELOW mg_sigma_threshold —
|
||||
@@ -1837,20 +1840,43 @@ def sample_funpack_distilled_flow(model, x, sigmas, extra_args=None, callback=No
|
||||
# denoise_mask) or if the packed-latent layout can't be read (helper returns None).
|
||||
# alg_guide_tail_frames (>0) extends the same idea, beyond the paper, to the trailing
|
||||
# guide-attention frames this scene appended (alg_blur_guides on the Scene Chain Sampler).
|
||||
# The two are independent: guide-tail blur runs standalone with alg_enabled off (anchor
|
||||
# stays sharp), sharing the same alg_strength / alg_sigma_threshold blur parameters.
|
||||
# Anchor and guide-tail blur are fully independent: each has its own strength and sigma
|
||||
# window (alg_strength/alg_sigma_threshold vs alg_guide_blur_*), so one precomputed
|
||||
# latent per (anchor blurred?, tail blurred?) combination that can occur — the frame
|
||||
# sets are disjoint, so the both-blurred variant just composes the two.
|
||||
alg_guide_tail_frames = max(0, int(alg_guide_tail_frames))
|
||||
alg_wanted = bool(alg_enabled) or alg_guide_tail_frames > 0
|
||||
alg_sharp_latent_image = getattr(model, "latent_image", None) if alg_wanted else None
|
||||
alg_active = alg_wanted and extra_args.get("denoise_mask") is not None and alg_sharp_latent_image is not None
|
||||
alg_blurred_latent_image = _alg_blur_frames(
|
||||
model, alg_sharp_latent_image, max(1.0, float(alg_strength)),
|
||||
frame_indices=(0,) if alg_enabled else (), tail_count=alg_guide_tail_frames,
|
||||
) if alg_active else None
|
||||
alg_active = alg_active and alg_blurred_latent_image is not None
|
||||
alg_anchor_on = bool(alg_enabled)
|
||||
alg_tail_on = alg_guide_tail_frames > 0
|
||||
alg_sharp_latent_image = getattr(model, "latent_image", None) if (alg_anchor_on or alg_tail_on) else None
|
||||
alg_active = ((alg_anchor_on or alg_tail_on) and extra_args.get("denoise_mask") is not None
|
||||
and alg_sharp_latent_image is not None)
|
||||
alg_latents = {}
|
||||
if alg_active:
|
||||
print(f"[FunPack AV] ALG on (anchor={'on' if alg_enabled else 'off'}, strength={alg_strength}, "
|
||||
f"sigma_threshold={alg_sigma_threshold}, guide_tail_frames={alg_guide_tail_frames}) "
|
||||
anchor_blurred = _alg_blur_frames(
|
||||
model, alg_sharp_latent_image, max(1.0, float(alg_strength)), frame_indices=(0,),
|
||||
) if alg_anchor_on else None
|
||||
tail_kappa = max(1.0, float(alg_guide_blur_strength))
|
||||
tail_blurred = _alg_blur_frames(
|
||||
model, alg_sharp_latent_image, tail_kappa, tail_count=alg_guide_tail_frames,
|
||||
) if alg_tail_on else None
|
||||
both_blurred = _alg_blur_frames(
|
||||
model, anchor_blurred, tail_kappa, tail_count=alg_guide_tail_frames,
|
||||
) if (anchor_blurred is not None and tail_blurred is not None) else None
|
||||
alg_anchor_on = alg_anchor_on and anchor_blurred is not None
|
||||
alg_tail_on = alg_tail_on and tail_blurred is not None
|
||||
alg_latents = {
|
||||
(False, False): alg_sharp_latent_image,
|
||||
(True, False): anchor_blurred,
|
||||
(False, True): tail_blurred,
|
||||
(True, True): both_blurred if both_blurred is not None else (anchor_blurred or tail_blurred),
|
||||
}
|
||||
alg_active = alg_anchor_on or alg_tail_on
|
||||
if alg_active:
|
||||
anchor_desc = (f"strength={alg_strength}, sigma_threshold={alg_sigma_threshold}"
|
||||
if alg_anchor_on else "off")
|
||||
tail_desc = (f"{alg_guide_tail_frames} frame(s), strength={alg_guide_blur_strength}, "
|
||||
f"sigma_threshold={alg_guide_blur_sigma_threshold}" if alg_tail_on else "off")
|
||||
print(f"[FunPack AV] ALG on (anchor: {anchor_desc}; guide tail: {tail_desc}) "
|
||||
f"— blurred while sigma > threshold")
|
||||
|
||||
# EXPERIMENTAL Momentum Guidance (arXiv:2602.20360): EMA of the per-step ODE direction,
|
||||
@@ -1886,7 +1912,10 @@ def sample_funpack_distilled_flow(model, x, sigmas, extra_args=None, callback=No
|
||||
velocity_target = _velocity_bias_target(sigmas, sigma)
|
||||
|
||||
if alg_active:
|
||||
model.latent_image = alg_blurred_latent_image if float(sigma) > float(alg_sigma_threshold) else alg_sharp_latent_image
|
||||
model.latent_image = alg_latents[(
|
||||
alg_anchor_on and float(sigma) > float(alg_sigma_threshold),
|
||||
alg_tail_on and float(sigma) > float(alg_guide_blur_sigma_threshold),
|
||||
)]
|
||||
|
||||
if _velocity_bias_enabled(velocity_bias_mode, "apply"):
|
||||
x_pre = x
|
||||
@@ -2066,11 +2095,11 @@ class FunPackDistilledFlowSampler:
|
||||
}),
|
||||
"alg_strength": ("FLOAT", {
|
||||
"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.1,
|
||||
"tooltip": "Downsample factor for the anchor blur (paper default 2.5, but 2.0 held character/i2v consistency noticeably better in testing here). Higher = blurrier anchor during the affected steps. Used when alg_enabled, and also by the Scene Chain Sampler's alg_blur_guides (which works without alg_enabled).",
|
||||
"tooltip": "Downsample factor for the anchor blur (paper default 2.5, but 2.0 held character/i2v consistency noticeably better in testing here). Higher = blurrier anchor during the affected steps. Only used when alg_enabled; guide-frame blur has its own controls on the Scene Chain Sampler (alg_blur_guides + alg_guide_blur_*).",
|
||||
}),
|
||||
"alg_sigma_threshold": ("FLOAT", {
|
||||
"default": 0.975, "min": 0.5, "max": 0.999, "step": 0.005,
|
||||
"tooltip": "Anchor stays blurred while sigma is above this value (the near-pure-noise steps), then swaps to sharp. Higher = narrower blurred window. Used when alg_enabled, and also by the Scene Chain Sampler's alg_blur_guides (which works without alg_enabled).",
|
||||
"tooltip": "Anchor stays blurred while sigma is above this value (the near-pure-noise steps), then swaps to sharp. Higher = narrower blurred window. Only used when alg_enabled; guide-frame blur has its own controls on the Scene Chain Sampler (alg_blur_guides + alg_guide_blur_*).",
|
||||
}),
|
||||
"mg_enabled": ("BOOLEAN", {
|
||||
"default": False,
|
||||
@@ -2330,7 +2359,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
}),
|
||||
"alg_blur_guides": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "EXPERIMENTAL: extends ALG (see the sampler's alg_enabled) from just the i2v anchor to also blur newly-appended guide-attention frames this scene (mid_scene_guide / carry_i2v_guides-as-guide / configured per-scene guides / JoyAI memory), for the same early steps. Standalone: works even with the sampler's alg_enabled off (anchor stays sharp), reusing its alg_strength / alg_sigma_threshold. Requires the FunPack Distilled Flow sampler; no effect if no guide frames were appended this scene.",
|
||||
"tooltip": "EXPERIMENTAL: extends ALG (see the sampler's alg_enabled) from just the i2v anchor to also blur newly-appended guide-attention frames this scene (mid_scene_guide / carry_i2v_guides-as-guide / configured per-scene guides / JoyAI memory), for the same early steps. Standalone: works even with the sampler's alg_enabled off (anchor stays sharp), with its own alg_guide_blur_strength / alg_guide_blur_sigma_threshold controls below. Requires the FunPack Distilled Flow sampler; no effect if no guide frames were appended this scene.",
|
||||
}),
|
||||
"bounded_attention_enabled": ("BOOLEAN", {
|
||||
"default": False,
|
||||
@@ -2358,6 +2387,14 @@ class FunPackLTXAVSceneChainSampler:
|
||||
"default": 0.6, "min": 0.3, "max": 0.95, "step": 0.05,
|
||||
"tooltip": "Frame-similarity gate: a current frame must match a banked negative frame above this cosine similarity before any steering applies. Steering strength ramps from 0 at the threshold to full at similarity 1.0, so it self-releases once the unwanted feature is gone. Lower = more aggressive (risks steering away from legitimately similar content).",
|
||||
}),
|
||||
"alg_guide_blur_strength": ("FLOAT", {
|
||||
"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.1,
|
||||
"tooltip": "Downsample factor for the guide-frame blur (alg_blur_guides). Higher = blurrier guide/JoyAI frames during the affected steps. Independent of the sampler's anchor alg_strength.",
|
||||
}),
|
||||
"alg_guide_blur_sigma_threshold": ("FLOAT", {
|
||||
"default": 0.975, "min": 0.5, "max": 0.999, "step": 0.005,
|
||||
"tooltip": "Guide frames stay blurred while sigma is above this value (the near-pure-noise steps), then swap to sharp. Higher = narrower blurred window. Independent of the sampler's anchor alg_sigma_threshold.",
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
@@ -2743,6 +2780,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
|
||||
def _sample_chunk(self, model, sampler, sigmas, seed, cfg, positive, negative, latent,
|
||||
pbar=None, step_offset=0, alg_guide_tail_frames=0,
|
||||
alg_guide_blur_strength=2.0, alg_guide_blur_sigma_threshold=0.975,
|
||||
bounded_attention_enabled=False):
|
||||
if sampler is None:
|
||||
raise ValueError("sampler input is required.")
|
||||
@@ -2765,6 +2803,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
extra_options = getattr(sampler, "extra_options", None)
|
||||
if isinstance(extra_options, dict) and sampler.sampler_function is sample_funpack_distilled_flow:
|
||||
extra_options["alg_guide_tail_frames"] = int(alg_guide_tail_frames)
|
||||
extra_options["alg_guide_blur_strength"] = float(alg_guide_blur_strength)
|
||||
extra_options["alg_guide_blur_sigma_threshold"] = float(alg_guide_blur_sigma_threshold)
|
||||
|
||||
# EXPERIMENTAL Bounded Attention: model-level attention hooks (sampler-agnostic, unlike
|
||||
# the toggles above which only work on Distilled Flow), so install/remove here rather
|
||||
@@ -4149,6 +4189,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
bounded_attention_enabled=False,
|
||||
output_guidance=False, output_guidance_strength=0.02,
|
||||
dynashift=False, dynashift_strength=0.3, dynashift_threshold=0.6,
|
||||
alg_guide_blur_strength=2.0, alg_guide_blur_sigma_threshold=0.975,
|
||||
unique_id=None, prompt=None):
|
||||
if not isinstance(positive, list) or not positive:
|
||||
raise ValueError("positive conditioning must contain at least one scene entry.")
|
||||
@@ -4200,6 +4241,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
joyai_memory_strength=joyai_memory_strength,
|
||||
joyai_audio_memory=joyai_audio_memory, v2a_grad_scale=v2a_grad_scale,
|
||||
alg_blur_guides=alg_blur_guides,
|
||||
alg_guide_blur_strength=alg_guide_blur_strength,
|
||||
alg_guide_blur_sigma_threshold=alg_guide_blur_sigma_threshold,
|
||||
bounded_attention_enabled=bounded_attention_enabled,
|
||||
output_guidance=output_guidance, output_guidance_strength=output_guidance_strength,
|
||||
dynashift=dynashift, dynashift_strength=dynashift_strength,
|
||||
@@ -4482,6 +4525,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
model, sampler, sigmas, scene_seed, cfg, scene_positive, scene_negative, chunk,
|
||||
pbar=pbar, step_offset=scene_index * steps_per_scene,
|
||||
alg_guide_tail_frames=(guide_tail if (alg_blur_guides and guide_tail > 0) else 0),
|
||||
alg_guide_blur_strength=alg_guide_blur_strength,
|
||||
alg_guide_blur_sigma_threshold=alg_guide_blur_sigma_threshold,
|
||||
bounded_attention_enabled=bounded_attention_enabled,
|
||||
)
|
||||
_scene_sample_s = _time.perf_counter() - _t_sample0
|
||||
@@ -4761,7 +4806,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
joyai_audio_memory=False, v2a_grad_scale=1.0,
|
||||
alg_blur_guides=False, bounded_attention_enabled=False,
|
||||
output_guidance=False, output_guidance_strength=0.02,
|
||||
dynashift=False, dynashift_strength=0.3, dynashift_threshold=0.6):
|
||||
dynashift=False, dynashift_strength=0.3, dynashift_threshold=0.6,
|
||||
alg_guide_blur_strength=2.0, alg_guide_blur_sigma_threshold=0.975):
|
||||
"""Sample one chain per Studio-packed variant entry (seed + index), persisting each result
|
||||
(latent + preview + per-entry cond + manifest) under ComfyUI temp for rating in Studio.
|
||||
Reuses sample() per entry with only the seed changed, so each entry is a clean generation."""
|
||||
@@ -4808,6 +4854,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
transition_duration=transition_duration,
|
||||
decode_tile_size=decode_tile_size, refinement_key_input=key,
|
||||
alg_blur_guides=alg_blur_guides,
|
||||
alg_guide_blur_strength=alg_guide_blur_strength,
|
||||
alg_guide_blur_sigma_threshold=alg_guide_blur_sigma_threshold,
|
||||
bounded_attention_enabled=bounded_attention_enabled,
|
||||
output_guidance=output_guidance, output_guidance_strength=output_guidance_strength,
|
||||
dynashift=dynashift, dynashift_strength=dynashift_strength,
|
||||
|
||||
@@ -137,6 +137,39 @@ def test_alg_blur_reduces_high_frequency_variance():
|
||||
assert float(frame0_out.var()) < float(frame0_in.var())
|
||||
|
||||
|
||||
def test_alg_blur_composes_with_independent_strengths():
|
||||
"""Anchor and guide-tail blur are separate passes with their own kappa: composing the
|
||||
tail pass onto the anchor-blurred latent must leave the anchor frame untouched, and a
|
||||
stronger tail kappa must blur the tail harder than the anchor kappa would."""
|
||||
model = _fake_model()
|
||||
latent_image = _packed_latent_image()
|
||||
anchor_only = samplers._alg_blur_frames(model, latent_image, 2.0, frame_indices=(0,))
|
||||
both = samplers._alg_blur_frames(model, anchor_only, 4.0, tail_count=1)
|
||||
v_anchor_only = _video_frames(anchor_only)
|
||||
v_both = _video_frames(both)
|
||||
|
||||
assert torch.equal(v_both[:, :, 0], v_anchor_only[:, :, 0]) # anchor pass preserved
|
||||
assert torch.equal(v_both[:, :, 1:3], v_anchor_only[:, :, 1:3]) # middle untouched
|
||||
|
||||
# Strength independence needs content that survives downsampling distinctly (the 4x4
|
||||
# checkerboard averages to uniform 0.5 at ANY factor) — use a seeded random 8x8 frame.
|
||||
# Different kappa must yield a different blurred tail: proof the per-region strength
|
||||
# actually reaches the filter (variance ordering is NOT guaranteed without antialias).
|
||||
h8 = 8
|
||||
model8 = _fake_model(shapes=((B, C, T, h8, h8), AUDIO_SHAPE))
|
||||
video8 = torch.rand(B, C, T, h8, h8, generator=torch.Generator().manual_seed(7))
|
||||
packed8 = torch.cat([video8.reshape(B, 1, C * T * h8 * h8),
|
||||
torch.full((B, 1, AUDIO_SIZE), 5.0)], dim=-1)
|
||||
weak = samplers._alg_blur_frames(model8, packed8, 2.0, tail_count=1)
|
||||
strong = samplers._alg_blur_frames(model8, packed8, 4.0, tail_count=1)
|
||||
v_sz = C * T * h8 * h8
|
||||
tail_weak = weak[..., :v_sz].reshape(B, C, T, h8, h8)[:, :, 3]
|
||||
tail_strong = strong[..., :v_sz].reshape(B, C, T, h8, h8)[:, :, 3]
|
||||
tail_orig = video8[:, :, 3]
|
||||
assert not torch.allclose(tail_weak, tail_strong) # kappa reaches the filter
|
||||
assert not torch.allclose(tail_weak, tail_orig) and not torch.allclose(tail_strong, tail_orig)
|
||||
|
||||
|
||||
def test_alg_blur_is_deterministic():
|
||||
model = _fake_model()
|
||||
latent_image = _packed_latent_image()
|
||||
|
||||
Reference in New Issue
Block a user