second_pass: both schedules run in FULL — remove the cut I invented
User, after a live run: "First pass runs its own FULL schedule. Then 2nd pass begins. If I want to stop 1st pass earlier - I remove sigmas from schedule... 'Continue from' is ONLY relevant in our shifting technique which is a separate entity." Correct, and the cut was mine, not theirs. I carried anchor_shift's cut/restart semantics into a feature that has no use for them. Consequences they hit on the rental: - Steps. Pass 1 was cut at the second schedule's first sigma, so a 9-step main plus a 4-step second pass ran 3+4=7, not 13. They noticed the missing steps. - Distortion. With the cut, the between-pass operation ran on a MID-TRAJECTORY latent, so "sharpen" was sharpening the noise still in it. It only makes sense on a finished clip. Now: pass 1 runs the main schedule in full, exactly as written. The operation runs on that finished clip. Pass 2 runs second_pass_sigmas in full, exactly as written, entering the way any img2img refine does — comfy's ordinary CONST scaling on a clean latent, so continue_from_state stays off. Total steps are the two schedules added up. To make pass 1 shorter, shorten the main schedule. second_pass_sigma and second_pass_restart_sigma are deleted outright: with both schedules run as written there is nothing to cut and nothing to continue from. The second schedule's first sigma is now simply the strength dial, the way it is in every img2img refine. _second_pass_plan is replaced by _second_pass_schedule, which only validates. Also reported: "I have only sharpening enabled but log tells me upsampling is enabled as well." The load line printed "[FunPackSceneChain] segmented detailing upsampler: ..." regardless of which feature pulled it in, and sharpen legitimately loads that model — so a sharpen-only run announced a feature that was off. Now "latent upsampler loaded". Kept: the malformed-schedule refusals (ascending, or not ending at 0) — both are silent in the output rather than loud at runtime, which is how the distorted run read. tests/test_second_pass.py rewritten around the corrected shape, including that a 9-step main plus a 4-step second pass is 13. Suites at baseline: 22 pre-existing failures, 467 passing. Verified live that typing a schedule sets second_pass with no cut knob leaking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1c6afe2e4d
commit
63f225e231
@@ -554,24 +554,17 @@
|
||||
else S.setStudioInputNow("studio_settings", next);
|
||||
syncSecondPassFromSchedule(updatedSamplers, quiet);
|
||||
}
|
||||
// The second pass is driven by ONE thing here: the schedule. Typing one turns it on and
|
||||
// pass 1 stops where that schedule starts; clearing it turns it off. The node's own
|
||||
// enable/cut widgets still exist for graph users with no schedule wired, but exposing
|
||||
// them alongside the schedule meant two controls saying the same thing in different
|
||||
// words ("starts at" vs "re-enters at"), which is exactly what made it confusing.
|
||||
// The second pass is driven by ONE thing: the schedule. Typing one turns it on; clearing
|
||||
// it turns it off. There is no cut and no re-entry point to configure — pass 1 always runs
|
||||
// the main schedule in full and pass 2 always runs this one in full.
|
||||
function syncSecondPassFromSchedule(samplers, quiet) {
|
||||
const raw = String(samplers?.low?.sigmas || "").replace(/;/g, ",");
|
||||
const vals = raw.split(",").map((v) => parseFloat(v.trim())).filter((v) => !isNaN(v));
|
||||
const set = quiet ? S.setSamplerInput : S.setSamplerInputNow;
|
||||
// Two real sigmas minimum — one number is not a schedule.
|
||||
if (vals.length >= 2 && vals[0] > 0 && vals[0] < 1) {
|
||||
set("second_pass", true);
|
||||
set("second_pass_sigma", vals[0]); // cut == entry -> an exact hand-over, no re-noise
|
||||
} else {
|
||||
S.unsetSamplerInput("second_pass");
|
||||
S.unsetSamplerInput("second_pass_sigma");
|
||||
}
|
||||
if (vals.length >= 2) set("second_pass", true); // one number is not a schedule
|
||||
else S.unsetSamplerInput("second_pass");
|
||||
}
|
||||
|
||||
try {
|
||||
window.SamplerPanel.render(box, samplers,
|
||||
(s) => persistSamplers(s, true),
|
||||
|
||||
@@ -347,13 +347,15 @@
|
||||
const spSig = textCtrl(s.low.sigmas, "e.g. 0.812, 0.6, 0.35, 0.15, 0.0", "sp-second-sig",
|
||||
(v) => { s.low.sigmas = v; mkSave(true)(); });
|
||||
row(container, "Second pass schedule", spSig);
|
||||
hint(container, "Fill this in and the scene is sampled in two passes; leave it empty and it "
|
||||
+ "isn't. Comma-separated floats. Pass 1 runs the main schedule down to the "
|
||||
+ "FIRST sigma listed here, then pass 2 takes over and runs this schedule to "
|
||||
+ "the end — so the second half can have its own step count and spacing. "
|
||||
+ "Nothing is added at the hand-over, so with a schedule that simply continues "
|
||||
+ "where the first left off the result matches a single pass; the cost is only "
|
||||
+ "whatever extra steps you list.");
|
||||
hint(container, "Fill this in and each scene is sampled in two passes; leave it empty and "
|
||||
+ "it isn't. Comma-separated floats, high to low, ending at 0. Pass 1 "
|
||||
+ "runs the main Sigmas schedule above in full, then pass 2 runs THIS "
|
||||
+ "one in full, so the total is simply the two added up (a 9-step main "
|
||||
+ "plus a 4-step second pass is 13). Pass 2 starts from the finished "
|
||||
+ "clip and re-noises it up to your first sigma, so that value is the "
|
||||
+ "strength dial — near 1.0 reworks the shot, low values only polish "
|
||||
+ "it — and the rest sets how many steps it gets. To make pass 1 "
|
||||
+ "shorter, shorten the schedule above; nothing here cuts it short.");
|
||||
}
|
||||
|
||||
window.SamplerPanel = { render, defaultSamplers, defaultPass };
|
||||
|
||||
+64
-116
@@ -2576,15 +2576,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
# builder's positional reference-workflow mapping (extract_widgets) stays aligned.
|
||||
"second_pass": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Stop the schedule partway and finish the scene in a SECOND sampling pass. Independent of anchor_shift (which runs its own two passes — when anchor_shift acts on a scene this is skipped, with a note). On its own the split is behaviour-neutral at second_pass_restart_sigma=0: pass 2 resumes from exactly the state pass 1 handed over, no noise added, same total step count, so the output matches a single pass. It becomes useful when the second half is made DIFFERENT — re-entering higher up the schedule (second_pass_restart_sigma) to let the model rework what it has, or finishing on a different schedule entirely (the second_pass_sigmas input). The i2v anchor stays pinned across the split, so the scene does not drift off its reference. Cost is exactly the extra steps you add: at restart 0 with no second schedule it is free; re-entering higher costs the replayed steps. Off by default.",
|
||||
}),
|
||||
"second_pass_sigma": ("FLOAT", {
|
||||
"default": 0.603, "min": 0.001, "max": 0.999, "step": 0.001,
|
||||
"tooltip": "Where pass 1 stops. The schedule is walked and the last pass-1 step is the one whose NEXT sigma has crossed this value, so the cut always lands on a real schedule step — only the sigmas actually in your schedule are reachable. Both halves need at least one step, so a value outside the schedule's range is refused with a reason rather than silently ignored.",
|
||||
}),
|
||||
"second_pass_restart_sigma": ("FLOAT", {
|
||||
"default": 0.0, "min": 0.0, "max": 0.999, "step": 0.001,
|
||||
"tooltip": "Where pass 2 re-enters, when no second_pass_sigmas schedule is connected. 0 (default) = carry straight on from the cut: nothing is added, total cost unchanged. A value ABOVE second_pass_sigma rewinds — the half-denoised latent is rebuilt at that higher sigma (exact for a mid-trajectory latent, not the naive img2img re-noise) and those steps run again, letting the model rework structure it has already committed to, at the cost of repeating them. BELOW the cut is refused: noise can be added back, never removed. Ignored when second_pass_sigmas is connected, since that schedule's own first sigma is the entry point.",
|
||||
"tooltip": "Sample each scene in TWO passes. Pass 1 runs the main sigmas schedule in full, exactly as written; pass 2 then runs the second_pass_sigmas schedule in full, exactly as written, starting from pass 1's finished clip. Nothing is cut short and nothing is derived — to make pass 1 shorter, shorten the main schedule. Total steps are simply the two schedules added up (a 9-step main plus a 4-step second pass is 13). Pass 2 re-enters the way any img2img refine does, by re-noising the finished clip up to its first sigma, so that first sigma controls how much it is allowed to change: nearer 1.0 reworks the shot, low values only polish it. Requires a second_pass_sigmas schedule; without one the pass is skipped with a note. Independent of anchor_shift, which runs its own two passes for a different reason — when it acts on a scene, this is skipped.",
|
||||
}),
|
||||
"second_pass_op": (["none", "sharpen", "upscale_2x"], {
|
||||
"default": "none",
|
||||
@@ -2593,7 +2585,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
# A connection socket, never a widget — safe at the end, and it must stay after
|
||||
# every widget above (see the widgets_values note at the top of this block).
|
||||
"second_pass_sigmas": ("SIGMAS", {
|
||||
"tooltip": "Optional SEPARATE schedule for pass 2. Wire any scheduler here and pass 2 runs that schedule end to end instead of continuing along the main one — so the second half can have a different step count, spacing or shift from the first (e.g. a short, fine tail schedule over a coarse first pass). Its FIRST sigma is where pass 2 re-enters, so it must start at or above second_pass_sigma (the state can be re-noised back up, never un-noised down); second_pass_restart_sigma is ignored when this is connected. The sampler is told this schedule is pass 2's real schedule, so its own phases (quality steps, final-correction window, AB2 ramp) are measured against it rather than against the main one.",
|
||||
"tooltip": "The schedule pass 2 runs — required for second_pass, and it is run EXACTLY as written, high to low, ending at 0. Wire any scheduler here, or type sigmas in the Editor. Pass 1 has already finished the main schedule by this point, so pass 2 starts from a clean clip and re-enters by re-noising it up to this schedule's FIRST sigma: that value is the strength dial (near 1.0 reworks the shot, low values only polish it), and the rest of the schedule sets how many steps it gets. A schedule that ascends, or that stops above 0, is refused with the reason — both would silently produce a distorted or under-denoised clip rather than fail loudly.",
|
||||
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
@@ -3170,70 +3163,35 @@ class FunPackLTXAVSceneChainSampler:
|
||||
# which the sampler needs to keep its phase boundaries where the schedule put them.
|
||||
return first, second, vals[cut], vals[start], resume, start
|
||||
|
||||
def _second_pass_plan(self, sigmas, cut_sigma, restart_sigma, alt_sigmas=None):
|
||||
"""Plan a general two-pass run, optionally finishing on a DIFFERENT schedule.
|
||||
def _second_pass_schedule(self, alt_sigmas):
|
||||
"""Validate the pass-2 schedule. Returns (sigmas, reason); one of them is None.
|
||||
|
||||
Returns (plan, reason). `plan` is None when the request cannot be honoured, and
|
||||
`reason` then says why and what to change — never a silent no-op.
|
||||
There is nothing to derive here and nothing to cut. Pass 1 runs the main schedule in
|
||||
full, exactly as written; pass 2 then runs THIS schedule in full, exactly as written.
|
||||
Pass 2 therefore starts from a FINISHED clip, which is the ordinary img2img re-entry
|
||||
(comfy's CONST scaling, x = s*noise + (1-s)*clean, valid precisely because the input
|
||||
is clean). "Continue from where pass 1 stopped" belongs to anchor_shift, which stops
|
||||
mid-trajectory on purpose; it has no meaning here. To make pass 1 shorter, shorten
|
||||
the main schedule.
|
||||
|
||||
plan = (pass1_sigmas, pass2_sigmas, cut_at, entry_at, resume, pass2_context)
|
||||
|
||||
Pass 1 always runs the top of `sigmas` down to the first sigma at or below
|
||||
`cut_sigma` (same walk as anchor_shift: the last pre-cut step is the one whose NEXT
|
||||
sigma crosses the threshold). Pass 2 is either a slice of the same schedule starting
|
||||
at `restart_sigma` (0 = continue straight on from the cut), or — when `alt_sigmas` is
|
||||
connected — that schedule in its entirety, entered at its own first sigma.
|
||||
|
||||
Either way pass 2 re-enters at a sigma at or above the cut, because _renoise_to_sigma
|
||||
can add noise back but never remove it. `pass2_context` is what the sampler must be
|
||||
told its real schedule is: the main schedule with an offset when pass 2 is a slice of
|
||||
it, or the alternate schedule at offset 0 when pass 2 IS that whole schedule.
|
||||
A hand-typed schedule is the one input that can be malformed, and both ways it can
|
||||
be are silent in the OUTPUT rather than loud at runtime, so they are caught here.
|
||||
"""
|
||||
if not isinstance(sigmas, torch.Tensor) or sigmas.numel() < 3:
|
||||
return None, "the main schedule needs at least 3 sigmas to be split"
|
||||
vals = [float(v) for v in sigmas.tolist()]
|
||||
eps = 1e-4 # SIGMAS are float32; see _anchor_shift_split_sigmas for why this matters
|
||||
|
||||
def _first_at_or_below(seq, s):
|
||||
return next((i for i, v in enumerate(seq) if v <= s + eps), None)
|
||||
|
||||
cut = _first_at_or_below(vals, float(cut_sigma))
|
||||
if cut is None or cut < 1:
|
||||
return None, (f"second_pass_sigma={float(cut_sigma):g} is not reached by the schedule "
|
||||
f"with a step left before it — pass 1 would have no step to run")
|
||||
first = sigmas[:cut + 1]
|
||||
if first.numel() < 2:
|
||||
return None, "pass 1 would have no step"
|
||||
cut_at = vals[cut]
|
||||
|
||||
if isinstance(alt_sigmas, torch.Tensor) and alt_sigmas.numel() >= 2:
|
||||
# Pass 2 runs its OWN schedule end to end. Its first sigma is where it re-enters,
|
||||
# so that is what the state has to be rebuilt at.
|
||||
second = alt_sigmas
|
||||
entry_at = float(alt_sigmas[0].item())
|
||||
ctx = {"sigmas": alt_sigmas, "offset": 0}
|
||||
if entry_at < cut_at - eps:
|
||||
return None, (f"second_pass_sigmas starts at {entry_at:g}, below "
|
||||
f"second_pass_sigma={cut_at:g} — pass 2 can be handed a noisier "
|
||||
f"latent than pass 1 left, never a cleaner one. Start the second "
|
||||
f"schedule at or above the cut, or lower second_pass_sigma")
|
||||
else:
|
||||
restart = float(restart_sigma)
|
||||
if restart <= 0:
|
||||
start = cut
|
||||
else:
|
||||
start = _first_at_or_below(vals, restart)
|
||||
if start is None or start > cut:
|
||||
return None, (f"second_pass_restart_sigma={restart:g} is below "
|
||||
f"second_pass_sigma={cut_at:g} — pass 2 can be handed a noisier "
|
||||
f"latent than pass 1 left, never a cleaner one. Use 0 to carry "
|
||||
f"straight on from the cut, or a value at or above it")
|
||||
second = sigmas[start:]
|
||||
entry_at = vals[start]
|
||||
ctx = {"sigmas": sigmas, "offset": start}
|
||||
if second.numel() < 2:
|
||||
return None, "pass 2 would have no step"
|
||||
return (first, second, cut_at, entry_at, abs(entry_at - cut_at) <= eps, ctx), None
|
||||
if not isinstance(alt_sigmas, torch.Tensor) or alt_sigmas.numel() < 2:
|
||||
return None, ("no second-pass schedule — set one (comma-separated sigmas, high to "
|
||||
"low, ending at 0) or turn the second pass off")
|
||||
vals = [float(v) for v in alt_sigmas.tolist()]
|
||||
eps = 1e-4
|
||||
if any(b > a + eps for a, b in zip(vals, vals[1:])):
|
||||
return None, ("second-pass schedule must descend — "
|
||||
f"{', '.join(f'{v:g}' for v in vals)} goes back up, which walks the "
|
||||
f"trajectory backwards. List the sigmas high to low")
|
||||
if vals[-1] > eps:
|
||||
return None, (f"second-pass schedule ends at {vals[-1]:g}, not 0 — the scene would "
|
||||
f"come out partially denoised (noise artefacts). End it at 0")
|
||||
if vals[0] <= eps:
|
||||
return None, "second-pass schedule starts at 0 — there is nothing for it to denoise"
|
||||
return alt_sigmas, None
|
||||
|
||||
def _second_pass_operate(self, latent, op, upsampler, vae):
|
||||
"""Apply the chosen latent-space operation between the two passes.
|
||||
@@ -5282,8 +5240,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
anchor_shift=False, anchor_shift_sigma=0.909, anchor_shift_frames=8,
|
||||
anchor_shift_restart_sigma=0.0, anchor_shift_tail="extend_last",
|
||||
anchor_shift_fresh_audio=True,
|
||||
second_pass=False, second_pass_sigma=0.603,
|
||||
second_pass_restart_sigma=0.0, second_pass_op="none", second_pass_sigmas=None,
|
||||
second_pass=False, second_pass_op="none", second_pass_sigmas=None,
|
||||
unique_id=None, prompt=None):
|
||||
if not isinstance(positive, list) or not positive:
|
||||
raise ValueError("positive conditioning must contain at least one scene entry.")
|
||||
@@ -5353,9 +5310,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
anchor_shift_restart_sigma=anchor_shift_restart_sigma,
|
||||
anchor_shift_tail=anchor_shift_tail,
|
||||
anchor_shift_fresh_audio=anchor_shift_fresh_audio,
|
||||
second_pass=second_pass, second_pass_sigma=second_pass_sigma,
|
||||
second_pass_restart_sigma=second_pass_restart_sigma,
|
||||
second_pass_op=second_pass_op, second_pass_sigmas=second_pass_sigmas,
|
||||
second_pass=second_pass, second_pass_op=second_pass_op,
|
||||
second_pass_sigmas=second_pass_sigmas,
|
||||
)
|
||||
|
||||
max_scene_count = max(1, int(max_scenes))
|
||||
@@ -5934,24 +5890,31 @@ class FunPackLTXAVSceneChainSampler:
|
||||
# General second pass. Reaching this branch means anchor_shift did not act
|
||||
# on this scene (it is either off, or was skipped with a reason above), so
|
||||
# the two features never nest — they stay independent by construction.
|
||||
_sp_plan = None
|
||||
#
|
||||
# The shape is deliberately plain: pass 1 runs the main schedule IN FULL,
|
||||
# exactly as written, then pass 2 runs its own schedule IN FULL, exactly as
|
||||
# written. Nothing is cut and nothing is "continued from" — those belong to
|
||||
# anchor_shift, which stops mid-trajectory on purpose. To make pass 1
|
||||
# shorter, shorten the main schedule. Pass 2 therefore starts from a
|
||||
# FINISHED clip, so it re-enters through comfy's ordinary CONST noise
|
||||
# scaling (continue_from_state stays off) — valid precisely because the
|
||||
# input is clean. Total steps are simply the two schedules added up.
|
||||
_sp_b = None
|
||||
if second_pass:
|
||||
_sp_plan, _sp_reason = self._second_pass_plan(
|
||||
sigmas, second_pass_sigma, second_pass_restart_sigma,
|
||||
second_pass_sigmas)
|
||||
if _sp_plan is None:
|
||||
_sp_b, _sp_reason = self._second_pass_schedule(second_pass_sigmas)
|
||||
if _sp_b is None:
|
||||
run_mechanisms.append(f"second_pass(SKIPPED: {_sp_reason})")
|
||||
if _sp_plan is not None:
|
||||
_sp_a, _sp_b, _sp_cut, _sp_entry, _sp_resume, _sp_ctx = _sp_plan
|
||||
_sp_p1 = self._sample_chunk(
|
||||
model, sampler, _sp_a, scene_seed, cfg, scene_positive,
|
||||
scene_negative, chunk,
|
||||
schedule_context={"sigmas": sigmas, "offset": 0}, **_sample_kwargs)
|
||||
_sp_state = _sp_p1
|
||||
_full = self._sample_chunk(
|
||||
model, sampler, sigmas, scene_seed, cfg, scene_positive,
|
||||
scene_negative, chunk, **_sample_kwargs)
|
||||
if _sp_b is None:
|
||||
sampled = _full
|
||||
else:
|
||||
_sp_state = _full
|
||||
if second_pass_op not in (None, "none", ""):
|
||||
# Operate BEFORE the re-noise: sharpening a latent that has just had
|
||||
# noise added back would sharpen the noise. Shares segmented
|
||||
# detailing's lazily-loaded upsampler — same model, same knob.
|
||||
# The op runs on the FINISHED pass-1 clip, which is the only state
|
||||
# it makes sense on: sharpening a half-denoised latent sharpens the
|
||||
# noise in it. Shares segmented detailing's lazily-loaded upsampler.
|
||||
if _detail_upsampler_model is None and _detail_disabled_reason is None:
|
||||
try:
|
||||
try:
|
||||
@@ -5970,17 +5933,12 @@ class FunPackLTXAVSceneChainSampler:
|
||||
run_mechanisms.append(
|
||||
f"second_pass_op={second_pass_op} skipped: upsampler "
|
||||
f"unavailable ({_detail_disabled_reason})")
|
||||
if not _sp_resume:
|
||||
# Rebuild the state at the higher entry sigma. Fresh field: reusing
|
||||
# scene_seed would re-apply the exact noise pass 1 just resolved.
|
||||
_sp_state = self._renoise_to_sigma(
|
||||
_sp_state, _sp_cut, _sp_entry, scene_seed + 4242)
|
||||
# Unlike anchor_shift, a plain second pass must KEEP the i2v anchor
|
||||
# pinned — otherwise pass 2 re-denoises the reference frame and the
|
||||
# scene drifts away from the image it was supposed to start from.
|
||||
_sp_before = _sp_state
|
||||
_sp_state = self._restore_pinned_prefix(_sp_state, chunk)
|
||||
if (_sp_state is _sp_before and self._anchor_pinned_frames(chunk) > 0):
|
||||
if _sp_state is _sp_before and self._anchor_pinned_frames(chunk) > 0:
|
||||
# Only happens when the op changed the resolution — say so, because
|
||||
# an unpinned pass 2 can drift off the reference image.
|
||||
run_mechanisms.append(
|
||||
@@ -5988,23 +5946,15 @@ class FunPackLTXAVSceneChainSampler:
|
||||
"because second_pass_op changed the latent resolution — pass 2 runs "
|
||||
"unpinned and may drift from the reference image.")
|
||||
_sp_kw = dict(_sample_kwargs)
|
||||
_sp_kw["step_offset"] = _sample_kwargs["step_offset"] + (_sp_a.numel() - 1)
|
||||
_sp_kw["continue_from_state"] = True
|
||||
_sp_kw["schedule_context"] = _sp_ctx
|
||||
_sp_kw["step_offset"] = _sample_kwargs["step_offset"] + (int(sigmas.numel()) - 1)
|
||||
sampled = self._sample_chunk(
|
||||
model, sampler, _sp_b, scene_seed + 4242, cfg, scene_positive,
|
||||
scene_negative, _sp_state, **_sp_kw)
|
||||
_sp_alt = (isinstance(second_pass_sigmas, torch.Tensor)
|
||||
and second_pass_sigmas.numel() >= 2)
|
||||
run_mechanisms.append(
|
||||
f"second_pass({'continue' if _sp_resume else 'rewind'} "
|
||||
f"cut@{_sp_cut:g} entry@{_sp_entry:g}, "
|
||||
f"{_sp_a.numel() - 1}+{_sp_b.numel() - 1} steps"
|
||||
f"{', own schedule' if _sp_alt else ''})")
|
||||
else:
|
||||
sampled = self._sample_chunk(
|
||||
model, sampler, sigmas, scene_seed, cfg, scene_positive,
|
||||
scene_negative, chunk, **_sample_kwargs)
|
||||
f"second_pass({int(sigmas.numel()) - 1} steps + "
|
||||
f"{int(_sp_b.numel()) - 1} steps = "
|
||||
f"{int(sigmas.numel()) + int(_sp_b.numel()) - 2} total, "
|
||||
f"pass 2 from {float(_sp_b[0].item()):g})")
|
||||
_scene_sample_s = _time.perf_counter() - _t_sample0
|
||||
_phase_sampling += _scene_sample_s
|
||||
if _plateau_stats is not None:
|
||||
@@ -6043,7 +5993,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
if _detail_upsampler_model is None:
|
||||
_resolved = _detailing.resolve_upsampler_name(detail_upsampler)
|
||||
_detail_upsampler_model = _detailing.load_latent_upsampler(_resolved)
|
||||
print(f"[FunPackSceneChain] segmented detailing upsampler: {_resolved}")
|
||||
print(f"[FunPackSceneChain] latent upsampler loaded: {_resolved}")
|
||||
_t_detail0 = _time.perf_counter()
|
||||
sampled, _detail_note = _detailing.detail_refine_scene(
|
||||
self, model, vae, sampler, scene_positive, scene_negative, sampled,
|
||||
@@ -6345,8 +6295,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
anchor_shift=False, anchor_shift_sigma=0.909, anchor_shift_frames=8,
|
||||
anchor_shift_restart_sigma=0.0, anchor_shift_tail="extend_last",
|
||||
anchor_shift_fresh_audio=True,
|
||||
second_pass=False, second_pass_sigma=0.603,
|
||||
second_pass_restart_sigma=0.0, second_pass_op="none",
|
||||
second_pass=False, second_pass_op="none",
|
||||
second_pass_sigmas=None):
|
||||
"""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.
|
||||
@@ -6412,9 +6361,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
anchor_shift_restart_sigma=anchor_shift_restart_sigma,
|
||||
anchor_shift_tail=anchor_shift_tail,
|
||||
anchor_shift_fresh_audio=anchor_shift_fresh_audio,
|
||||
second_pass=second_pass, second_pass_sigma=second_pass_sigma,
|
||||
second_pass_restart_sigma=second_pass_restart_sigma,
|
||||
second_pass_op=second_pass_op, second_pass_sigmas=second_pass_sigmas,
|
||||
second_pass=second_pass, second_pass_op=second_pass_op,
|
||||
second_pass_sigmas=second_pass_sigmas,
|
||||
unique_id=None, prompt=None,
|
||||
)
|
||||
last = out
|
||||
|
||||
+41
-78
@@ -4,14 +4,15 @@ anchor_shift built the machinery (stop the schedule, hand the state over, re-ent
|
||||
this exposes it on its own so a scene can be finished differently from how it was started.
|
||||
The invariants that matter here are the ones a split can silently get wrong:
|
||||
|
||||
1. Pass 1 stops on a REAL step of the user's schedule, found by walking it.
|
||||
2. Pass 2 re-enters at or above the cut — never below, because noise can be added back but
|
||||
never removed (the same rule anchor_shift's rewind lives by).
|
||||
3. A connected second schedule replaces pass 2 wholesale, and its own first sigma becomes
|
||||
the entry point, so the sampler's phases are measured against IT and not the main one.
|
||||
4. Nothing is ever a silent no-op: every refusal comes back with a reason naming the fix.
|
||||
5. The i2v anchor stays PINNED across the split — the opposite of anchor_shift, which
|
||||
deletes the pin on purpose. Without this pass 2 re-denoises the reference frame.
|
||||
1. BOTH schedules run in full, exactly as written. Pass 1 is not cut short and pass 2 is
|
||||
not derived from it — total steps are simply the two added up. (An earlier version cut
|
||||
pass 1 at pass 2's first sigma, which is anchor_shift's behaviour and does not belong
|
||||
here; it silently shortened the run and distorted the result.)
|
||||
2. A hand-typed schedule is the one malformed-able input, and both ways it can be wrong
|
||||
are silent in the OUTPUT rather than loud at runtime, so they are refused up front.
|
||||
3. Nothing is ever a silent no-op: every refusal comes back with a reason naming the fix.
|
||||
4. The i2v anchor stays PINNED for pass 2 — the opposite of anchor_shift, which deletes
|
||||
the pin on purpose. Without this pass 2 re-denoises the reference frame.
|
||||
"""
|
||||
import sys
|
||||
import types
|
||||
@@ -44,88 +45,50 @@ def _node():
|
||||
return samplers.FunPackLTXAVSceneChainSampler()
|
||||
|
||||
|
||||
def _plan(cut, restart=0.0, alt=None):
|
||||
return _node()._second_pass_plan(SCHEDULE, cut, restart, alt)
|
||||
# ── the pass-2 schedule ─────────────────────────────────────────────────────
|
||||
|
||||
def _sched(alt):
|
||||
return _node()._second_pass_schedule(alt)
|
||||
|
||||
|
||||
# ── the split ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_continue_splits_the_schedule_without_adding_a_step():
|
||||
"""restart 0 is behaviour-neutral: the two halves are the whole schedule, cut once, and
|
||||
pass 2 resumes from the exact handed-over state (resume=True -> no re-noise)."""
|
||||
plan, reason = _plan(0.603)
|
||||
assert reason is None
|
||||
first, second, cut_at, entry_at, resume, ctx = plan
|
||||
assert [round(v, 3) for v in first.tolist()] == [1.0, 0.955, 0.893, 0.812, 0.715, 0.603]
|
||||
assert [round(v, 3) for v in second.tolist()] == [0.603, 0.482, 0.241, 0.121, 0.0]
|
||||
assert abs(cut_at - 0.603) < 1e-5 and abs(entry_at - 0.603) < 1e-5
|
||||
assert resume is True
|
||||
# No step is run twice: 5 + 4 == the schedule's own 9.
|
||||
assert (first.numel() - 1) + (second.numel() - 1) == SCHEDULE.numel() - 1
|
||||
# Pass 2's phases must still be measured against the whole schedule.
|
||||
assert ctx["sigmas"] is SCHEDULE and ctx["offset"] == 5
|
||||
def test_a_well_formed_schedule_is_taken_exactly_as_written():
|
||||
"""Nothing is derived, cut or continued. Pass 1 runs the main schedule in full and pass 2
|
||||
runs this one in full, so on the user's 9-step main a 4-step second pass is 13 steps —
|
||||
not a re-slicing of the main schedule."""
|
||||
alt = torch.tensor([0.812, 0.6, 0.35, 0.15, 0.0])
|
||||
out, reason = _sched(alt)
|
||||
assert reason is None and out is alt
|
||||
assert (SCHEDULE.numel() - 1) + (out.numel() - 1) == 13
|
||||
|
||||
|
||||
def test_rewind_re_enters_higher_and_replays_those_steps():
|
||||
plan, reason = _plan(0.482, restart=0.812)
|
||||
assert reason is None
|
||||
first, second, cut_at, entry_at, resume, ctx = plan
|
||||
assert [round(v, 3) for v in first.tolist()] == [1.0, 0.955, 0.893, 0.812, 0.715, 0.603, 0.482]
|
||||
assert [round(v, 3) for v in second.tolist()] == [0.812, 0.715, 0.603, 0.482, 0.241, 0.121, 0.0]
|
||||
assert resume is False # the caller must rebuild the state at 0.812 first
|
||||
assert ctx["offset"] == 3
|
||||
def test_an_ascending_schedule_is_refused():
|
||||
"""Both malformed cases are silent in the OUTPUT rather than loud at runtime, so they
|
||||
have to be caught before sampling. An ascending pair walks the trajectory backwards."""
|
||||
out, reason = _sched(torch.tensor([0.812, 0.4, 0.55, 0.0]))
|
||||
assert out is None and "must descend" in reason
|
||||
|
||||
|
||||
def test_restart_below_the_cut_is_refused_with_the_fix_named():
|
||||
plan, reason = _plan(0.812, restart=0.482)
|
||||
assert plan is None
|
||||
assert "never a cleaner one" in reason and "second_pass_restart_sigma" in reason
|
||||
def test_a_schedule_that_stops_short_of_zero_is_refused():
|
||||
"""Stopping above 0 leaves the clip partially denoised — the noise-artefact symptom."""
|
||||
out, reason = _sched(torch.tensor([0.812, 0.5, 0.25]))
|
||||
assert out is None and "not 0" in reason
|
||||
|
||||
|
||||
def test_a_cut_outside_the_schedule_is_refused_not_ignored():
|
||||
for cut in (1.0, 0.9999, 0.0005):
|
||||
plan, reason = _plan(cut)
|
||||
assert plan is None and reason, cut
|
||||
def test_equal_neighbouring_sigmas_are_not_treated_as_ascending():
|
||||
out, reason = _sched(torch.tensor([0.812, 0.6, 0.6, 0.0]))
|
||||
assert reason is None and out is not None
|
||||
|
||||
|
||||
# ── a separate schedule for pass 2 ──────────────────────────────────────────
|
||||
|
||||
def test_a_connected_schedule_replaces_pass_two_wholesale():
|
||||
"""The point of the feature: the second half can have its own step count and spacing.
|
||||
Its first sigma is the entry point, and the sampler must be told THAT is its schedule —
|
||||
otherwise it measures its quality phase and correction window against the main one."""
|
||||
alt = torch.tensor([0.715, 0.5, 0.3, 0.15, 0.0])
|
||||
plan, reason = _plan(0.603, restart=0.0, alt=alt)
|
||||
assert reason is None
|
||||
first, second, cut_at, entry_at, resume, ctx = plan
|
||||
assert second is alt
|
||||
assert abs(entry_at - 0.715) < 1e-5 # the alt schedule's own first sigma
|
||||
assert resume is False # 0.603 -> 0.715 needs the state rebuilt
|
||||
assert ctx["sigmas"] is alt and ctx["offset"] == 0
|
||||
# ...and pass 1 is unaffected by it.
|
||||
assert [round(v, 3) for v in first.tolist()] == [1.0, 0.955, 0.893, 0.812, 0.715, 0.603]
|
||||
def test_a_missing_or_degenerate_schedule_is_refused_with_a_reason():
|
||||
"""second_pass with nothing to run must say so, not silently sample once."""
|
||||
for alt in (None, torch.tensor([]), torch.tensor([0.5]), "0.8, 0.0"):
|
||||
out, reason = _sched(alt)
|
||||
assert out is None and reason
|
||||
|
||||
|
||||
def test_a_connected_schedule_starting_at_the_cut_is_an_exact_handover():
|
||||
alt = torch.tensor([0.603, 0.4, 0.2, 0.0])
|
||||
plan, reason = _plan(0.603, alt=alt)
|
||||
assert reason is None and plan[4] is True # resume -> nothing added
|
||||
|
||||
|
||||
def test_a_connected_schedule_starting_below_the_cut_is_refused():
|
||||
alt = torch.tensor([0.3, 0.15, 0.0])
|
||||
plan, reason = _plan(0.603, alt=alt)
|
||||
assert plan is None
|
||||
assert "second_pass_sigmas starts at" in reason
|
||||
|
||||
|
||||
def test_a_degenerate_connected_schedule_falls_back_to_the_restart_sigma():
|
||||
"""A single-sigma (or empty) SIGMAS input is not a schedule — it must not be treated as
|
||||
one, and the restart sigma still governs."""
|
||||
for alt in (None, torch.tensor([0.5]), torch.tensor([])):
|
||||
plan, reason = _plan(0.603, restart=0.812, alt=alt)
|
||||
assert reason is None
|
||||
assert plan[1].tolist()[0] == SCHEDULE[3].item() # a slice of the MAIN schedule
|
||||
def test_a_schedule_starting_at_zero_is_refused():
|
||||
out, reason = _sched(torch.tensor([0.0, 0.0]))
|
||||
assert out is None and "nothing for it to denoise" in reason
|
||||
|
||||
|
||||
# ── the pin survives the split ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user