Upsampler: chunk_join=stepped replaces continue; fix pin-strength shadowing
Browse fileschunk_join=stepped: the chunks are re-drawn together, as spatial tiles
always were. The film starts as the upscaled chunks crossfaded into one
latent; each sigma step runs every chunk on its slice of that one state
(step 1 with its slice of one film-wide noise field) and merges the
results, each chunk weighted down linearly across the steps it shares, so
neighbours converge on one face and a chunk's off-tone last step counts
least. Every chunk is still drawn from the source: nothing accumulates.
Whole-frame chunks only; tiled or out-of-VRAM runs fall back to crossfade.
Removed chunk_join=continue: on a GPU it made a visible seam at the
hand-over (traced to a tone step at the held/free boundary) and did not
keep the face closer.
Fix: with the original (non-Regen2K) recipe and structure_lock > 0, chunk
2+ crashed -- the per-chunk lock anchor shadowed upsample()'s pin-strength
argument. Introduced on this branch; regression test added.
- CHANGELOG.md +2 -1
- tiled_upsampler.py +131 -46
- workflows/HandTieClips_Upsample_Video.json +1 -1
- workflows/HandTieClips_Upsample_Video_File.json +1 -1
|
@@ -15,12 +15,13 @@ pack — the published package excludes them.
|
|
| 15 |
- **New node: HTC Video Frames (lean)** (`HTCVideoFrames`). A drop-in for Get Video Components after Load Video, with the same images / audio / fps plus `frame_count` and the VIDEO. `start_seconds`, `max_frames`, `frame_rate` (24 fps nearest-frame re-time) and `resize` (megapixels or long edge, never enlarging) are applied while decoding, and the IMAGE is filled once from 8-bit frames: peak RAM was 1.08x the output against 2.01x for core-style decoding. It replaces Get Video Components in `workflows/HandTieClips_Upsample_Video.json`.
|
| 16 |
- **New node: HTC Load Video (path)** (`HTCLoadVideoPath`). It reads a video where it is, so there's no upload, no copy into `input/` and no 100 MB upload limit (no `--max-upload-size` needed). Paste a path (Copy as path quotes are fine) or a name relative to `input/`. Outputs VIDEO, fps, frame_count and an info line (resolution, fps with variable-rate warning, length, codec, bit depth, audio), and the node re-runs only when the file changes.
|
| 17 |
- **Fix (file to file, Video Frames):** an audio track FFmpeg can't decode (e.g. an iPhone's spatial-audio APAC track) is now skipped, as core does. Decoding one crashes the process.
|
| 18 |
-
- **Upsampler: `attention`, `chunk_join`, `chunking`, `tiling`, `lock_anchor`** (standalone nodes and `HTC Upsample Settings`). `attention=sparse` applies ComfyUI's Model Sparse Attention (sol-attn) to every re-draw step, and runs dense on a ComfyUI without it. `chunk_join=
|
| 19 |
- **Fix: the 3D latent upscale's CPU fallback no longer sticks.** One out-of-memory error used to run every remaining chunk on the CPU in float32. Now only that chunk does, and the next one goes back to the GPU.
|
| 20 |
- Conditioning rows (references) now count against `tile_tokens` when tiles are cut (`_cond_extra_tokens` was unused).
|
| 21 |
- **Upsampler: `tile_tokens = 0` learns the machine** (new default on the standalone nodes; `upsample_tile_tokens` 0 in `HandTieClips`, whose stock value stays 70000). It starts from the budget this GPU learned last time (70000 the first time) and measures each chunk's activation memory (peak minus what stays resident, since ComfyUI's dynamic loading fills spare VRAM with weights). It then stores the next run's starting budget in `ComfyUI/user/htc_upsampler_memory.json`: grown toward 85% of the card when there was room (at most 2x a run), or the budget that worked after an out-of-memory error, never returning near the failing value. It's keyed by GPU, VRAM, attention mode and the low-VRAM chunking. A fixed number behaves exactly as before and teaches nothing.
|
| 22 |
- **Draft: `tile_context = global pass`** (standalone nodes and `HTC Upsample Settings`; default `none`). For chunks that must be tiled, the chunk is first re-drawn whole-frame at the largest size that fits the budget, with the same steps. The tiles then hold their large shapes to that result (`structure_lock` at full strength, at `lock_sigma`). A tile only sees its own window, and that is where invention comes from; this way the layout comes from a pass that saw everything, and the tiles add detail. Untiled chunks are unaffected. Not yet measured on a GPU.
|
| 23 |
- **H3 Video Upsampler: `redraw_start`** (default 0.8 = the recipe). It rebuilds Regen2K's 3-step shift-6 grid from another starting noise. A side-by-side test showed the face drift comes from the re-draw itself, not from the 3D upscaler (denoise 0 matched the source), nor the seed, the LoRA strength or a text prompt. Starting from less noise keeps faces closer to the source at some cost in sharpening. It is the last widget on both nodes, so existing graphs keep their values.
|
|
|
|
| 24 |
|
| 25 |
## 3.2.0 — 2026-10-30
|
| 26 |
|
|
|
|
| 15 |
- **New node: HTC Video Frames (lean)** (`HTCVideoFrames`). A drop-in for Get Video Components after Load Video, with the same images / audio / fps plus `frame_count` and the VIDEO. `start_seconds`, `max_frames`, `frame_rate` (24 fps nearest-frame re-time) and `resize` (megapixels or long edge, never enlarging) are applied while decoding, and the IMAGE is filled once from 8-bit frames: peak RAM was 1.08x the output against 2.01x for core-style decoding. It replaces Get Video Components in `workflows/HandTieClips_Upsample_Video.json`.
|
| 16 |
- **New node: HTC Load Video (path)** (`HTCLoadVideoPath`). It reads a video where it is, so there's no upload, no copy into `input/` and no 100 MB upload limit (no `--max-upload-size` needed). Paste a path (Copy as path quotes are fine) or a name relative to `input/`. Outputs VIDEO, fps, frame_count and an info line (resolution, fps with variable-rate warning, length, codec, bit depth, audio), and the node re-runs only when the file changes.
|
| 17 |
- **Fix (file to file, Video Frames):** an audio track FFmpeg can't decode (e.g. an iPhone's spatial-audio APAC track) is now skipped, as core does. Decoding one crashes the process.
|
| 18 |
+
- **Upsampler: `attention`, `chunk_join`, `chunking`, `tiling`, `lock_anchor`** (standalone nodes and `HTC Upsample Settings`). `attention=sparse` applies ComfyUI's Model Sparse Attention (sol-attn) to every re-draw step, and runs dense on a ComfyUI without it. `chunk_join=stepped` re-draws the chunks together, as spatial tiles always were: after each step, the frames two chunks share are merged into one state, starting from one film-wide noise field, so neighbouring chunks converge on one face and a chunk's off-tone last step counts least. Whole-frame chunks only, and it falls back to crossfade otherwise. (A `continue` mode, which held the shared frames to the previous chunk, was tried and removed: in a side-by-side test it made a visible seam at the hand-over and did not keep the face any closer.) `chunking` / `tiling` (both on, as before) each *allow* a split when the clip doesn't fit `tile_tokens`. Time is cut before space, chunks are never under 68 frames (Regen2K), and both off re-draws the clip in one whole-frame pass. Chunking off on a clip over 4x `tile_tokens` (about 18 s at 2 MP; in `HandTieClips` that is usually the whole film) chunks anyway. Tiling off never tiles, and running out of VRAM is an error that says what to change. `lock_anchor=source` (new default; it only matters with `structure_lock` > 0) holds the large shapes to the clip's own latent, enlarged by plain interpolation, instead of the 3D upscaler's output, so the upscaler's own errors are pulled back too. Defaults are otherwise unchanged.
|
| 19 |
- **Fix: the 3D latent upscale's CPU fallback no longer sticks.** One out-of-memory error used to run every remaining chunk on the CPU in float32. Now only that chunk does, and the next one goes back to the GPU.
|
| 20 |
- Conditioning rows (references) now count against `tile_tokens` when tiles are cut (`_cond_extra_tokens` was unused).
|
| 21 |
- **Upsampler: `tile_tokens = 0` learns the machine** (new default on the standalone nodes; `upsample_tile_tokens` 0 in `HandTieClips`, whose stock value stays 70000). It starts from the budget this GPU learned last time (70000 the first time) and measures each chunk's activation memory (peak minus what stays resident, since ComfyUI's dynamic loading fills spare VRAM with weights). It then stores the next run's starting budget in `ComfyUI/user/htc_upsampler_memory.json`: grown toward 85% of the card when there was room (at most 2x a run), or the budget that worked after an out-of-memory error, never returning near the failing value. It's keyed by GPU, VRAM, attention mode and the low-VRAM chunking. A fixed number behaves exactly as before and teaches nothing.
|
| 22 |
- **Draft: `tile_context = global pass`** (standalone nodes and `HTC Upsample Settings`; default `none`). For chunks that must be tiled, the chunk is first re-drawn whole-frame at the largest size that fits the budget, with the same steps. The tiles then hold their large shapes to that result (`structure_lock` at full strength, at `lock_sigma`). A tile only sees its own window, and that is where invention comes from; this way the layout comes from a pass that saw everything, and the tiles add detail. Untiled chunks are unaffected. Not yet measured on a GPU.
|
| 23 |
- **H3 Video Upsampler: `redraw_start`** (default 0.8 = the recipe). It rebuilds Regen2K's 3-step shift-6 grid from another starting noise. A side-by-side test showed the face drift comes from the re-draw itself, not from the 3D upscaler (denoise 0 matched the source), nor the seed, the LoRA strength or a text prompt. Starting from less noise keeps faces closer to the source at some cost in sharpening. It is the last widget on both nodes, so existing graphs keep their values.
|
| 24 |
+
- **Fix:** with the original (non-Regen2K) recipe and `structure_lock` > 0, chunk 2 onward crashed, because the lock's anchor latent shadowed the first-frame pin strength. This was introduced on this branch, and it is now fixed and covered by a test.
|
| 25 |
|
| 26 |
## 3.2.0 — 2026-10-30
|
| 27 |
|
|
@@ -102,15 +102,15 @@ LOCK_SIGMA = 2.0
|
|
| 102 |
# tile_tokens; neither forces one. chunking cuts time into 102-frame chunks
|
| 103 |
# (no shorter than 68 -- Regen2K's 3 steps from 0.8 invent on short pieces);
|
| 104 |
# tiling cuts space. Time is cut before space: a tile sees part of the frame,
|
| 105 |
-
# which is where invention comes from, while a join in time
|
| 106 |
-
#
|
| 107 |
-
#
|
| 108 |
# lock_anchor: what structure_lock holds the large shapes to. source = the
|
| 109 |
# clip's own latent, enlarged by plain interpolation (nothing invented, so
|
| 110 |
# errors the 3D upscaler adds -- a blown patch, a colour shift -- are pulled
|
| 111 |
# back too); upscaler = the 3D upscaler's output.
|
| 112 |
ATTENTION_MODES = ("dense", "sparse")
|
| 113 |
-
JOIN_MODES = ("crossfade", "
|
| 114 |
LOCK_ANCHORS = ("source", "upscaler")
|
| 115 |
# tile_context (draft): what a tile knows about the rest of its frame. none =
|
| 116 |
# only its own window (and structure_lock's anchor). global pass = before the
|
|
@@ -1143,15 +1143,12 @@ def _apply_low_vram_patches(model, attn_chunks=4, ffn_chunks=2):
|
|
| 1143 |
return model
|
| 1144 |
|
| 1145 |
|
| 1146 |
-
def sample_tile(model, cond, sampler, sigmas, video, audio, noise, seed, lock=None
|
| 1147 |
-
known_steps=0):
|
| 1148 |
"""One tile through `sigmas`, audio frozen. -> the tile's video latent.
|
| 1149 |
|
| 1150 |
`noise` is a (video, audio) pair to use as is. SamplerCustomAdvanced's
|
| 1151 |
path, with a positive-only guider at cfg 1 and the x0 preview. `lock` is
|
| 1152 |
-
a `structure_lock_fn` for this tile, or None.
|
| 1153 |
-
latent steps of `video` are held as given (the sampler keeps them at
|
| 1154 |
-
every sigma) and the rest is drawn after them.
|
| 1155 |
"""
|
| 1156 |
import comfy.model_management as mm
|
| 1157 |
import comfy.nested_tensor as nt
|
|
@@ -1162,13 +1159,9 @@ def sample_tile(model, cond, sampler, sigmas, video, audio, noise, seed, lock=No
|
|
| 1162 |
|
| 1163 |
samples = comfy.sample.fix_empty_latent_channels(
|
| 1164 |
model, nt.NestedTensor((video, audio)), None, None)
|
| 1165 |
-
vmask = torch.ones((1, 1, int(video.shape[2]) if known_steps else 1,
|
| 1166 |
-
video.shape[3], video.shape[4]),
|
| 1167 |
-
device=video.device, dtype=torch.float32)
|
| 1168 |
-
if known_steps:
|
| 1169 |
-
vmask[:, :, :int(known_steps)] = 0.0
|
| 1170 |
mask = nt.NestedTensor((
|
| 1171 |
-
|
|
|
|
| 1172 |
torch.zeros((1, 32, 2, audio.shape[-1]),
|
| 1173 |
device=audio.device, dtype=audio.dtype)))
|
| 1174 |
guider = comfy.samplers.CFGGuider(model)
|
|
@@ -1348,7 +1341,7 @@ def global_pass(model, cond, sampler, sigmas, video, audio, seed, budget,
|
|
| 1348 |
def redraw_chunk(model, cond, sampler, sigmas, video, audio, seed, factor=1,
|
| 1349 |
budget=TOKEN_BUDGET, overlap_px=TILE_OVERLAP_PX,
|
| 1350 |
detail_sigma=0.0, noise_video=None, structure_lock=STRUCTURE_LOCK,
|
| 1351 |
-
lock_sigma=LOCK_SIGMA,
|
| 1352 |
tile_context="none"):
|
| 1353 |
"""Every tile stepped together, merged under the feather after each step.
|
| 1354 |
|
|
@@ -1359,10 +1352,7 @@ def redraw_chunk(model, cond, sampler, sigmas, video, audio, seed, factor=1,
|
|
| 1359 |
so low-frequency tile-grid shifts are stripped while fine detail is kept.
|
| 1360 |
`structure_lock` > 0 holds every tile's large shapes to its slice of
|
| 1361 |
`video` (the upscaled source) at every step; see `structure_lock_fn`.
|
| 1362 |
-
`
|
| 1363 |
-
its first `steps` latent steps are held to `latent` and the rest is drawn
|
| 1364 |
-
after them. Whole-frame chunks only; a tiled chunk ignores it (the
|
| 1365 |
-
caller's crossfade then joins it as before). `anchor` replaces `video`
|
| 1366 |
as what structure_lock holds the large shapes to. `allow_tiles` False
|
| 1367 |
keeps the chunk whole-frame whatever the budget, and an OOM is an error
|
| 1368 |
instead of a retry in tiles. `tile_context` "global pass" re-draws the
|
|
@@ -1432,22 +1422,13 @@ def redraw_chunk(model, cond, sampler, sigmas, video, audio, seed, factor=1,
|
|
| 1432 |
# DynamicVRAM DiT on every step.
|
| 1433 |
r0, c0, rows, cols, tcond, _m = prepared[0]
|
| 1434 |
xt = x[:, :, :, r0:r0 + rows, c0:c0 + cols].contiguous()
|
| 1435 |
-
ks = 0
|
| 1436 |
-
if known is not None and int(known[0]) > 0:
|
| 1437 |
-
ks = min(int(known[0]), t - 1)
|
| 1438 |
-
xt = xt.clone() # a whole-frame slice may be `video` itself
|
| 1439 |
-
xt[:, :, :ks] = known[1][:, :, :ks].to(xt.device, xt.dtype)
|
| 1440 |
noise = (nv[:, :, :, r0:r0 + rows, c0:c0 + cols].contiguous(), na)
|
| 1441 |
ot = sample_tile(model, tcond, sampler, sigmas, xt, audio, noise, seed,
|
| 1442 |
-
lock=locks[0]
|
| 1443 |
_calls += 1
|
| 1444 |
x = ot.to(device=dev, dtype=dt)
|
| 1445 |
del ot, xt
|
| 1446 |
else:
|
| 1447 |
-
if known is not None and int(known[0]) > 0:
|
| 1448 |
-
print(f"[{TAG}] chunk_join=continue needs a whole-frame chunk; this one is "
|
| 1449 |
-
f"{grid[0]}x{grid[1]} tiles, so it is crossfaded", flush=True)
|
| 1450 |
-
known = None
|
| 1451 |
if str(tile_context) == "global pass":
|
| 1452 |
g, g_note = global_pass(
|
| 1453 |
model, cond, sampler, sigmas, video, audio, seed,
|
|
@@ -1514,13 +1495,78 @@ def redraw_chunk(model, cond, sampler, sigmas, video, audio, seed, factor=1,
|
|
| 1514 |
|
| 1515 |
if float(detail_sigma or 0.0) > 0.0:
|
| 1516 |
x = _highpass_detail_merge(video, x, sigma=float(detail_sigma))
|
| 1517 |
-
if known is not None and int(known[0]) > 0:
|
| 1518 |
-
ks = min(int(known[0]), t - 1)
|
| 1519 |
-
x[:, :, :ks] = known[1][:, :, :ks].to(x.device, x.dtype)
|
| 1520 |
_LAST_BUDGET[0] = cur_budget
|
| 1521 |
return x, cur_budget
|
| 1522 |
|
| 1523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1524 |
def _crossfade(a, b, dim):
|
| 1525 |
n = a.shape[dim]
|
| 1526 |
wgt = torch.linspace(0.0, 1.0, n, device=a.device, dtype=a.dtype)
|
|
@@ -1781,7 +1827,49 @@ def upsample(model, conditioning, latent, sampler, upscaler, megapixels,
|
|
| 1781 |
_oom_total = 0
|
| 1782 |
init_budget = cur_budget = int(budget)
|
| 1783 |
mem_samples, _peak_all = [], 0
|
| 1784 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1785 |
a0, a1 = audio_span(f0, f1)
|
| 1786 |
cv = upscaled_cvs[i]
|
| 1787 |
upscaled_cvs[i] = None
|
|
@@ -1816,13 +1904,8 @@ def upsample(model, conditioning, latent, sampler, upscaler, megapixels,
|
|
| 1816 |
print(f"[{TAG}] chunk {i + 1}/{len(plan)}: keeping lowered budget "
|
| 1817 |
f"{cur_budget} ({_gc[0]}x{_gc[1]} tiles; would fit "
|
| 1818 |
f"{_gi[0]}x{_gi[1]} at {init_budget})", flush=True)
|
| 1819 |
-
|
| 1820 |
anchors[i] = None
|
| 1821 |
-
known = None
|
| 1822 |
-
if str(chunk_join) == "continue" and i > 0 and end_v > k0:
|
| 1823 |
-
_ks = min(end_v - k0, int(cv.shape[2]) - 1)
|
| 1824 |
-
if _ks > 0:
|
| 1825 |
-
known = (_ks, out_v[:, :, k0:k0 + _ks])
|
| 1826 |
_tr = time.perf_counter()
|
| 1827 |
if torch.cuda.is_available():
|
| 1828 |
torch.cuda.reset_peak_memory_stats()
|
|
@@ -1831,7 +1914,7 @@ def upsample(model, conditioning, latent, sampler, upscaler, megapixels,
|
|
| 1831 |
noise_video=(None if film_nv is None
|
| 1832 |
else film_nv[:, :, k0:k1]),
|
| 1833 |
structure_lock=structure_lock, lock_sigma=lock_sigma,
|
| 1834 |
-
|
| 1835 |
tile_context=tile_context)
|
| 1836 |
rv, cur_budget = (_rc if isinstance(_rc, tuple) else (_rc, cur_budget))
|
| 1837 |
_redraw_s += time.perf_counter() - _tr
|
|
@@ -2169,11 +2252,13 @@ SETTINGS_TYPE = "HTC_UPSAMPLE"
|
|
| 2169 |
ATTENTION_TIP = ("dense = exact attention. sparse = ComfyUI's Model Sparse Attention (sol-attn) "
|
| 2170 |
"on every step: much faster on long sequences (rdh measured ~4x on a 2K "
|
| 2171 |
"refine), close to dense but approximate -- compare faces before relying on it.")
|
| 2172 |
-
JOIN_TIP = ("crossfade =
|
| 2173 |
-
"
|
| 2174 |
-
"
|
| 2175 |
-
"
|
| 2176 |
-
"
|
|
|
|
|
|
|
| 2177 |
CHUNKING_TIP = ("On = the clip may be cut into 102-frame chunks (never under 68) when it does "
|
| 2178 |
"not fit tile_tokens. Off = the whole clip in one pass: no joins, VRAM grows with "
|
| 2179 |
"its length. Clips over 4x tile_tokens (~18 s at 2 MP and 70000; in HandTieClips "
|
|
|
|
| 102 |
# tile_tokens; neither forces one. chunking cuts time into 102-frame chunks
|
| 103 |
# (no shorter than 68 -- Regen2K's 3 steps from 0.8 invent on short pieces);
|
| 104 |
# tiling cuts space. Time is cut before space: a tile sees part of the frame,
|
| 105 |
+
# which is where invention comes from, while a join in time can be stepped
|
| 106 |
+
# together (chunk_join=stepped). Both off = the whole clip in one whole-frame
|
| 107 |
+
# pass.
|
| 108 |
# lock_anchor: what structure_lock holds the large shapes to. source = the
|
| 109 |
# clip's own latent, enlarged by plain interpolation (nothing invented, so
|
| 110 |
# errors the 3D upscaler adds -- a blown patch, a colour shift -- are pulled
|
| 111 |
# back too); upscaler = the 3D upscaler's output.
|
| 112 |
ATTENTION_MODES = ("dense", "sparse")
|
| 113 |
+
JOIN_MODES = ("crossfade", "stepped")
|
| 114 |
LOCK_ANCHORS = ("source", "upscaler")
|
| 115 |
# tile_context (draft): what a tile knows about the rest of its frame. none =
|
| 116 |
# only its own window (and structure_lock's anchor). global pass = before the
|
|
|
|
| 1143 |
return model
|
| 1144 |
|
| 1145 |
|
| 1146 |
+
def sample_tile(model, cond, sampler, sigmas, video, audio, noise, seed, lock=None):
|
|
|
|
| 1147 |
"""One tile through `sigmas`, audio frozen. -> the tile's video latent.
|
| 1148 |
|
| 1149 |
`noise` is a (video, audio) pair to use as is. SamplerCustomAdvanced's
|
| 1150 |
path, with a positive-only guider at cfg 1 and the x0 preview. `lock` is
|
| 1151 |
+
a `structure_lock_fn` for this tile, or None.
|
|
|
|
|
|
|
| 1152 |
"""
|
| 1153 |
import comfy.model_management as mm
|
| 1154 |
import comfy.nested_tensor as nt
|
|
|
|
| 1159 |
|
| 1160 |
samples = comfy.sample.fix_empty_latent_channels(
|
| 1161 |
model, nt.NestedTensor((video, audio)), None, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1162 |
mask = nt.NestedTensor((
|
| 1163 |
+
torch.ones((1, 1, 1, video.shape[3], video.shape[4]),
|
| 1164 |
+
device=video.device, dtype=torch.float32),
|
| 1165 |
torch.zeros((1, 32, 2, audio.shape[-1]),
|
| 1166 |
device=audio.device, dtype=audio.dtype)))
|
| 1167 |
guider = comfy.samplers.CFGGuider(model)
|
|
|
|
| 1341 |
def redraw_chunk(model, cond, sampler, sigmas, video, audio, seed, factor=1,
|
| 1342 |
budget=TOKEN_BUDGET, overlap_px=TILE_OVERLAP_PX,
|
| 1343 |
detail_sigma=0.0, noise_video=None, structure_lock=STRUCTURE_LOCK,
|
| 1344 |
+
lock_sigma=LOCK_SIGMA, anchor=None, allow_tiles=True,
|
| 1345 |
tile_context="none"):
|
| 1346 |
"""Every tile stepped together, merged under the feather after each step.
|
| 1347 |
|
|
|
|
| 1352 |
so low-frequency tile-grid shifts are stripped while fine detail is kept.
|
| 1353 |
`structure_lock` > 0 holds every tile's large shapes to its slice of
|
| 1354 |
`video` (the upscaled source) at every step; see `structure_lock_fn`.
|
| 1355 |
+
`anchor` replaces `video`
|
|
|
|
|
|
|
|
|
|
| 1356 |
as what structure_lock holds the large shapes to. `allow_tiles` False
|
| 1357 |
keeps the chunk whole-frame whatever the budget, and an OOM is an error
|
| 1358 |
instead of a retry in tiles. `tile_context` "global pass" re-draws the
|
|
|
|
| 1422 |
# DynamicVRAM DiT on every step.
|
| 1423 |
r0, c0, rows, cols, tcond, _m = prepared[0]
|
| 1424 |
xt = x[:, :, :, r0:r0 + rows, c0:c0 + cols].contiguous()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1425 |
noise = (nv[:, :, :, r0:r0 + rows, c0:c0 + cols].contiguous(), na)
|
| 1426 |
ot = sample_tile(model, tcond, sampler, sigmas, xt, audio, noise, seed,
|
| 1427 |
+
lock=locks[0])
|
| 1428 |
_calls += 1
|
| 1429 |
x = ot.to(device=dev, dtype=dt)
|
| 1430 |
del ot, xt
|
| 1431 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1432 |
if str(tile_context) == "global pass":
|
| 1433 |
g, g_note = global_pass(
|
| 1434 |
model, cond, sampler, sigmas, video, audio, seed,
|
|
|
|
| 1495 |
|
| 1496 |
if float(detail_sigma or 0.0) > 0.0:
|
| 1497 |
x = _highpass_detail_merge(video, x, sigma=float(detail_sigma))
|
|
|
|
|
|
|
|
|
|
| 1498 |
_LAST_BUDGET[0] = cur_budget
|
| 1499 |
return x, cur_budget
|
| 1500 |
|
| 1501 |
|
| 1502 |
+
def _step_weights(plan, i, device):
|
| 1503 |
+
"""Chunk i's weight per latent step in a stepped merge: 1 inside, ramping
|
| 1504 |
+
linearly to near 0 across the steps it shares with each neighbour, so the
|
| 1505 |
+
end of a chunk -- where it is weakest -- counts least."""
|
| 1506 |
+
k0, _f0, k1, _f1 = plan[i]
|
| 1507 |
+
w = torch.ones(k1 - k0, dtype=torch.float32, device=device)
|
| 1508 |
+
if i > 0:
|
| 1509 |
+
ov = max(0, plan[i - 1][2] - k0)
|
| 1510 |
+
if ov:
|
| 1511 |
+
w[:ov] = torch.arange(1, ov + 1, dtype=torch.float32, device=device) / (ov + 1)
|
| 1512 |
+
if i + 1 < len(plan):
|
| 1513 |
+
ov = max(0, k1 - plan[i + 1][0])
|
| 1514 |
+
if ov:
|
| 1515 |
+
w[-ov:] = torch.minimum(w[-ov:], torch.arange(ov, 0, -1, dtype=torch.float32,
|
| 1516 |
+
device=device) / (ov + 1))
|
| 1517 |
+
return w.view(1, 1, -1, 1, 1)
|
| 1518 |
+
|
| 1519 |
+
|
| 1520 |
+
def redraw_stepped(model, plan, videos, audios, conds, sampler, sigmas, seed,
|
| 1521 |
+
locks=None, mem_samples=None):
|
| 1522 |
+
"""Every chunk stepped TOGETHER (chunk_join=stepped). -> the film's video latent.
|
| 1523 |
+
|
| 1524 |
+
`videos` are the chunks' upscaled latents (plan order), `audios` their
|
| 1525 |
+
audio slices, `conds` their conditioning, `locks` their structure locks.
|
| 1526 |
+
The film starts as the chunks crossfaded into one latent; each sigma step
|
| 1527 |
+
runs every chunk from its slice of that one state (the first step with its
|
| 1528 |
+
slice of one film-wide noise field) and merges the results under
|
| 1529 |
+
`_step_weights` before the next step -- what spatial tiles have always
|
| 1530 |
+
done, along time. Whole-frame chunks only (the caller checks)."""
|
| 1531 |
+
c, h, w = int(videos[0].shape[1]), int(videos[0].shape[3]), int(videos[0].shape[4])
|
| 1532 |
+
tv = int(plan[-1][2])
|
| 1533 |
+
dev, dt = videos[0].device, videos[0].dtype
|
| 1534 |
+
film = torch.zeros((1, c, tv, h, w), dtype=dt, device=dev)
|
| 1535 |
+
dummy = torch.zeros((1, 32, 2, 1), dtype=dt, device=dev)
|
| 1536 |
+
end = 0
|
| 1537 |
+
for (k0, f0, k1, _f1), v in zip(plan, videos):
|
| 1538 |
+
end, _ = _join_into(film, dummy, end, 0, v.to(dev, dt), dummy[..., :0], k0, f0)
|
| 1539 |
+
nv = torch.randn((1, c, tv, h, w), generator=torch.Generator().manual_seed(int(seed)),
|
| 1540 |
+
dtype=torch.float32, device="cpu").to(dev, dt)
|
| 1541 |
+
weights = [_step_weights(plan, i, dev) for i in range(len(plan))]
|
| 1542 |
+
x = film
|
| 1543 |
+
for step in range(len(sigmas) - 1):
|
| 1544 |
+
two = sigmas[step:step + 2]
|
| 1545 |
+
acc = torch.zeros_like(x, dtype=torch.float32)
|
| 1546 |
+
wsum = torch.zeros((1, 1, tv, 1, 1), dtype=torch.float32, device=dev)
|
| 1547 |
+
for i, (k0, _f0, k1, _f1) in enumerate(plan):
|
| 1548 |
+
import comfy.model_management as mm
|
| 1549 |
+
mm.throw_exception_if_processing_interrupted()
|
| 1550 |
+
xt = x[:, :, k0:k1].contiguous()
|
| 1551 |
+
noise = ((nv[:, :, k0:k1].contiguous() if step == 0 else torch.zeros_like(xt)),
|
| 1552 |
+
torch.zeros_like(audios[i]))
|
| 1553 |
+
if torch.cuda.is_available():
|
| 1554 |
+
torch.cuda.reset_peak_memory_stats()
|
| 1555 |
+
ot = sample_tile(model, conds[i], sampler, two, xt, audios[i], noise, seed,
|
| 1556 |
+
lock=(locks[i] if locks else None))
|
| 1557 |
+
if mem_samples is not None and torch.cuda.is_available():
|
| 1558 |
+
mem_samples.append((seq_len(w * SPATIAL, h * SPATIAL, k1 - k0),
|
| 1559 |
+
int(torch.cuda.max_memory_allocated()
|
| 1560 |
+
- torch.cuda.memory_allocated()),
|
| 1561 |
+
int(torch.cuda.memory_allocated())))
|
| 1562 |
+
acc[:, :, k0:k1] += ot.to(device=dev, dtype=torch.float32) * weights[i]
|
| 1563 |
+
wsum[:, :, k0:k1] += weights[i]
|
| 1564 |
+
del ot, xt
|
| 1565 |
+
x = (acc / wsum.clamp(min=1e-8)).to(dtype=dt)
|
| 1566 |
+
del acc
|
| 1567 |
+
return x
|
| 1568 |
+
|
| 1569 |
+
|
| 1570 |
def _crossfade(a, b, dim):
|
| 1571 |
n = a.shape[dim]
|
| 1572 |
wgt = torch.linspace(0.0, 1.0, n, device=a.device, dtype=a.dtype)
|
|
|
|
| 1827 |
_oom_total = 0
|
| 1828 |
init_budget = cur_budget = int(budget)
|
| 1829 |
mem_samples, _peak_all = [], 0
|
| 1830 |
+
stepped = False
|
| 1831 |
+
if str(chunk_join) == "stepped" and sigmas is not None and len(plan) > 1:
|
| 1832 |
+
shape = upscaled_cvs[0].shape
|
| 1833 |
+
fits = all(tile_grid(int(shape[3]), int(shape[4]), int(k1 - k0), factor,
|
| 1834 |
+
max(4096, int(budget)), overlap_px)[1][:2] == (1, 1)
|
| 1835 |
+
for (k0, _f0, k1, _f1) in plan) if tiling else True
|
| 1836 |
+
if factor > 1 or not fits:
|
| 1837 |
+
print(f"[{TAG}] chunk_join=stepped needs whole-frame chunks"
|
| 1838 |
+
+ (" (and no h3upscale guide)" if factor > 1 else "")
|
| 1839 |
+
+ "; crossfading instead", flush=True)
|
| 1840 |
+
else:
|
| 1841 |
+
conds, auds, locks = [], [], []
|
| 1842 |
+
for i, (k0, f0, k1, f1) in enumerate(plan):
|
| 1843 |
+
a0, a1 = audio_span(f0, f1)
|
| 1844 |
+
auds.append(audio[:, :, :, a0:min(a1, ta)].contiguous())
|
| 1845 |
+
cv = upscaled_cvs[i]
|
| 1846 |
+
conds.append(cond_for_chunk(clean_refs(conditioning(f0, f1)) if per_chunk
|
| 1847 |
+
else cond, f0, f1, cv.shape[3], cv.shape[4]))
|
| 1848 |
+
locks.append(structure_lock_fn(anchors[i] if anchors[i] is not None else cv,
|
| 1849 |
+
structure_lock, lock_sigma)
|
| 1850 |
+
if float(structure_lock) > 0.0 else None)
|
| 1851 |
+
_tr = time.perf_counter()
|
| 1852 |
+
try:
|
| 1853 |
+
out_v = redraw_stepped(model, plan, upscaled_cvs, auds, conds, sampler,
|
| 1854 |
+
sigmas, seed, locks, mem_samples)
|
| 1855 |
+
stepped = True
|
| 1856 |
+
except Exception as exc:
|
| 1857 |
+
if not _is_oom(exc):
|
| 1858 |
+
raise
|
| 1859 |
+
print(f"[{TAG}] chunk_join=stepped ran out of VRAM ({exc}); crossfading "
|
| 1860 |
+
"chunk by chunk instead", flush=True)
|
| 1861 |
+
_free_all_gpu()
|
| 1862 |
+
mem_samples.clear()
|
| 1863 |
+
_redraw_s += time.perf_counter() - _tr
|
| 1864 |
+
if stepped:
|
| 1865 |
+
print(f"[{TAG}] chunk_join=stepped: {len(plan)} chunks x {len(sigmas) - 1} "
|
| 1866 |
+
f"steps in {len(plan) * (len(sigmas) - 1)} sampler calls, "
|
| 1867 |
+
f"{_redraw_s:.1f} s", flush=True)
|
| 1868 |
+
if torch.cuda.is_available() and mem_samples:
|
| 1869 |
+
_peak_all = max(a + r for _t, a, r in mem_samples)
|
| 1870 |
+
out_a[...] = audio[..., :int(total_a)]
|
| 1871 |
+
end_v, end_a = int(tv), int(total_a)
|
| 1872 |
+
for i, (k0, f0, k1, f1) in enumerate(plan if not stepped else []):
|
| 1873 |
a0, a1 = audio_span(f0, f1)
|
| 1874 |
cv = upscaled_cvs[i]
|
| 1875 |
upscaled_cvs[i] = None
|
|
|
|
| 1904 |
print(f"[{TAG}] chunk {i + 1}/{len(plan)}: keeping lowered budget "
|
| 1905 |
f"{cur_budget} ({_gc[0]}x{_gc[1]} tiles; would fit "
|
| 1906 |
f"{_gi[0]}x{_gi[1]} at {init_budget})", flush=True)
|
| 1907 |
+
lock_anchor_t = anchors[i] # not `anchor`: that is the pin strength above
|
| 1908 |
anchors[i] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1909 |
_tr = time.perf_counter()
|
| 1910 |
if torch.cuda.is_available():
|
| 1911 |
torch.cuda.reset_peak_memory_stats()
|
|
|
|
| 1914 |
noise_video=(None if film_nv is None
|
| 1915 |
else film_nv[:, :, k0:k1]),
|
| 1916 |
structure_lock=structure_lock, lock_sigma=lock_sigma,
|
| 1917 |
+
anchor=lock_anchor_t, allow_tiles=tiling,
|
| 1918 |
tile_context=tile_context)
|
| 1919 |
rv, cur_budget = (_rc if isinstance(_rc, tuple) else (_rc, cur_budget))
|
| 1920 |
_redraw_s += time.perf_counter() - _tr
|
|
|
|
| 2252 |
ATTENTION_TIP = ("dense = exact attention. sparse = ComfyUI's Model Sparse Attention (sol-attn) "
|
| 2253 |
"on every step: much faster on long sequences (rdh measured ~4x on a 2K "
|
| 2254 |
"refine), close to dense but approximate -- compare faces before relying on it.")
|
| 2255 |
+
JOIN_TIP = ("crossfade = every chunk is re-drawn on its own and the 17 frames neighbours share "
|
| 2256 |
+
"are blended. stepped = the chunks are stepped TOGETHER, the way spatial tiles "
|
| 2257 |
+
"are: after each of the re-draw's steps, the frames two chunks share are merged "
|
| 2258 |
+
"into one state before either takes the next step, from one film-wide noise "
|
| 2259 |
+
"field. Two chunks then converge on one face instead of meeting as two, and a "
|
| 2260 |
+
"chunk's last step (which tends to come out off-tone) weighs least. Same compute, "
|
| 2261 |
+
"one sampler call per chunk per step. Whole-frame chunks only.")
|
| 2262 |
CHUNKING_TIP = ("On = the clip may be cut into 102-frame chunks (never under 68) when it does "
|
| 2263 |
"not fit tile_tokens. Off = the whole clip in one pass: no joins, VRAM grows with "
|
| 2264 |
"its length. Clips over 4x tile_tokens (~18 s at 2 MP and 70000; in HandTieClips "
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
"Node name for S&R": "MarkdownNote"
|
| 25 |
},
|
| 26 |
"widgets_values": [
|
| 27 |
-
"## H3 Video Upsampler: upscale a finished clip\n\n1. **Load Video**: pick the clip (put it in `ComfyUI/input`). Any size; 24 fps is what H3\n assumes, other rates work but the soundtrack conditions the re-draw loosely.\n2. **Queue.** HTC Save Video writes it to `video/HANDTIECLIPS/upsampled` at the clip's own fps,\n with its own soundtrack untouched (NVENC when there is a GPU). Set its `frame_gen` to x2 for\n double the frame rate (RTX GPU + `nvidia-vfx`).\n\n### The node\n- **megapixels**: output size, aspect kept (2 = about 1920x1080).\n- **denoise**: Regen2K LoRA strength for the 3-step Euler re-draw on `[0.8, 24/35, 0.48, 0]` (0.8 is the default; 0 skips the re-draw and only enlarges).\n- **lora**: the Open-H3-Regenerate-2K LoRA (`open_h3_regenerate_2k_r256pp_bf16.safetensors` by default).\n- **tile_tokens**: 0 = auto (default): learned on this GPU run by run, starting at 70000, grown when there is room, lowered for good after running out of VRAM. A number fixes it (lower = shorter chunks, then tiles).\n- **redraw_seed**: noise seed for the Regen2K re-draw (42 by default).\n- **structure_lock**: 0 = off. Holds the re-draw to the clip's large shapes (faces, hands, line work) at every step so chunks and tiles stop inventing; try 0.5-1, higher for animation.\n- **lock_sigma**: what structure_lock counts as large, in latent cells (2 = 32 px). 1-1.5 also holds eyes and mouths on medium shots.\n- **fps** is wired from Get Video Components: H3 runs on a 24 fps clock, so for other rates **off_24_audio** retimes what the re-draw hears (retime) or conditions on silence. The output soundtrack is always the original. A 48 fps clip interpolated from a 24 fps render: upscale the 24 fps frames and use frame_gen x2 instead (half the work, exact sync).\n- **conditioning** (optional): a CLIPTextEncode of what the clip shows, typed or written by a VLM. Unwired = zeroed-out conditioning.\n- **audio_vae** wired = the re-draw pins the real soundtrack; unwired = it pins silence.\n\nMouths and motion come from the clip; only fine detail is re-drawn.\n\n**Video Frames (lean)** replaces Get Video Components: same images / audio / fps, about half the RAM, and `start_seconds`, `max_frames`, `frame_rate` and `resize` applied while decoding. For long clips use the file-to-file workflow instead.\n\n**Speed and joins:** `attention` sparse = ComfyUI's sparse attention (much faster on long sequences, approximate: compare faces). `chunk_join`
|
| 28 |
]
|
| 29 |
},
|
| 30 |
{
|
|
|
|
| 24 |
"Node name for S&R": "MarkdownNote"
|
| 25 |
},
|
| 26 |
"widgets_values": [
|
| 27 |
+
"## H3 Video Upsampler: upscale a finished clip\n\n1. **Load Video**: pick the clip (put it in `ComfyUI/input`). Any size; 24 fps is what H3\n assumes, other rates work but the soundtrack conditions the re-draw loosely.\n2. **Queue.** HTC Save Video writes it to `video/HANDTIECLIPS/upsampled` at the clip's own fps,\n with its own soundtrack untouched (NVENC when there is a GPU). Set its `frame_gen` to x2 for\n double the frame rate (RTX GPU + `nvidia-vfx`).\n\n### The node\n- **megapixels**: output size, aspect kept (2 = about 1920x1080).\n- **denoise**: Regen2K LoRA strength for the 3-step Euler re-draw on `[0.8, 24/35, 0.48, 0]` (0.8 is the default; 0 skips the re-draw and only enlarges).\n- **lora**: the Open-H3-Regenerate-2K LoRA (`open_h3_regenerate_2k_r256pp_bf16.safetensors` by default).\n- **tile_tokens**: 0 = auto (default): learned on this GPU run by run, starting at 70000, grown when there is room, lowered for good after running out of VRAM. A number fixes it (lower = shorter chunks, then tiles).\n- **redraw_seed**: noise seed for the Regen2K re-draw (42 by default).\n- **structure_lock**: 0 = off. Holds the re-draw to the clip's large shapes (faces, hands, line work) at every step so chunks and tiles stop inventing; try 0.5-1, higher for animation.\n- **lock_sigma**: what structure_lock counts as large, in latent cells (2 = 32 px). 1-1.5 also holds eyes and mouths on medium shots.\n- **fps** is wired from Get Video Components: H3 runs on a 24 fps clock, so for other rates **off_24_audio** retimes what the re-draw hears (retime) or conditions on silence. The output soundtrack is always the original. A 48 fps clip interpolated from a 24 fps render: upscale the 24 fps frames and use frame_gen x2 instead (half the work, exact sync).\n- **conditioning** (optional): a CLIPTextEncode of what the clip shows, typed or written by a VLM. Unwired = zeroed-out conditioning.\n- **audio_vae** wired = the re-draw pins the real soundtrack; unwired = it pins silence.\n\nMouths and motion come from the clip; only fine detail is re-drawn.\n\n**Video Frames (lean)** replaces Get Video Components: same images / audio / fps, about half the RAM, and `start_seconds`, `max_frames`, `frame_rate` and `resize` applied while decoding. For long clips use the file-to-file workflow instead.\n\n**Speed and joins:** `attention` sparse = ComfyUI's sparse attention (much faster on long sequences, approximate: compare faces). `chunk_join` stepped = neighbouring chunks are re-drawn together and merged after every step, so they converge on one face instead of meeting as two. `chunking` / `tiling` allow a split when the clip doesn't fit `tile_tokens` (chunks are shortened first, never under 68 frames); both off = the whole clip in one whole-frame pass, for short clips on big cards. `lock_anchor` source holds `structure_lock` to the clip itself, so the 3D upscaler's own errors are pulled back too."
|
| 28 |
]
|
| 29 |
},
|
| 30 |
{
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
"Node name for S&R": "MarkdownNote"
|
| 25 |
},
|
| 26 |
"widgets_values": [
|
| 27 |
-
"## H3 Video Upsampler (file to file)\n\nThe same re-draw as the IMAGE workflow, for clips of any length on any machine: the video is read, encoded, re-drawn, decoded and saved **a chunk at a time**, so RAM stays flat however long the film is (the IMAGE path holds the whole clip twice; a minute at 48 fps is over 100 GB).\n\n1. **Load Video**: pick the clip. Frame rate and soundtrack come from the file.\n2. **Queue.** The MP4 lands in `video/H3_upsampled` with the original soundtrack.\n\n### The node\n- **megapixels / denoise / lora / redraw_seed**: as in the IMAGE workflow. **tile_tokens** 0 = auto: learned on this GPU run by run.\n- **structure_lock** (0 = off) holds faces, hands and line work to the clip; try 0.5-1. **lock_sigma** 1-1.5 also holds eyes and mouths on medium shots.\n- **off_24_audio**: H3 runs on a 24 fps clock; for other rates `retime` keeps the re-draw's lip sync.\n- **frame_gen / codec / quality**: as in H3 Save Video. x2 doubles the frame rate (RTX + `nvidia-vfx`).\n- **conditioning** (optional): a CLIPTextEncode of the clip's look. Unwired = zeroed out.\n- **frame_rate**: `24 (H3 native)` re-times the clip to 24 fps while reading (nearest frame), so the soundtrack lines up exactly and a 48 fps clip costs half the re-draw; add `frame_gen` x2 to save 48 again.\n- **reuse_encoded**: the next run on the same clip skips reading and encoding (kept in ComfyUI's temp folder until restart). Handy for A/B runs of structure_lock or seeds.\n\n**Speed and joins:** `attention` sparse = ComfyUI's sparse attention (much faster on long sequences, approximate: compare faces). `chunk_join`
|
| 28 |
]
|
| 29 |
},
|
| 30 |
{
|
|
|
|
| 24 |
"Node name for S&R": "MarkdownNote"
|
| 25 |
},
|
| 26 |
"widgets_values": [
|
| 27 |
+
"## H3 Video Upsampler (file to file)\n\nThe same re-draw as the IMAGE workflow, for clips of any length on any machine: the video is read, encoded, re-drawn, decoded and saved **a chunk at a time**, so RAM stays flat however long the film is (the IMAGE path holds the whole clip twice; a minute at 48 fps is over 100 GB).\n\n1. **Load Video**: pick the clip. Frame rate and soundtrack come from the file.\n2. **Queue.** The MP4 lands in `video/H3_upsampled` with the original soundtrack.\n\n### The node\n- **megapixels / denoise / lora / redraw_seed**: as in the IMAGE workflow. **tile_tokens** 0 = auto: learned on this GPU run by run.\n- **structure_lock** (0 = off) holds faces, hands and line work to the clip; try 0.5-1. **lock_sigma** 1-1.5 also holds eyes and mouths on medium shots.\n- **off_24_audio**: H3 runs on a 24 fps clock; for other rates `retime` keeps the re-draw's lip sync.\n- **frame_gen / codec / quality**: as in H3 Save Video. x2 doubles the frame rate (RTX + `nvidia-vfx`).\n- **conditioning** (optional): a CLIPTextEncode of the clip's look. Unwired = zeroed out.\n- **frame_rate**: `24 (H3 native)` re-times the clip to 24 fps while reading (nearest frame), so the soundtrack lines up exactly and a 48 fps clip costs half the re-draw; add `frame_gen` x2 to save 48 again.\n- **reuse_encoded**: the next run on the same clip skips reading and encoding (kept in ComfyUI's temp folder until restart). Handy for A/B runs of structure_lock or seeds.\n\n**Speed and joins:** `attention` sparse = ComfyUI's sparse attention (much faster on long sequences, approximate: compare faces). `chunk_join` stepped = neighbouring chunks are re-drawn together and merged after every step, so they converge on one face instead of meeting as two. `chunking` / `tiling` allow a split when the clip doesn't fit `tile_tokens` (chunks are shortened first, never under 68 frames); both off = the whole clip in one whole-frame pass, for short clips on big cards. `lock_anchor` source holds `structure_lock` to the clip itself, so the 3D upscaler's own errors are pulled back too."
|
| 28 |
]
|
| 29 |
},
|
| 30 |
{
|