Swan SelfLift Transition Lift (H3)
The Node Between Your Two Samplers, and the rho Knob That Does the Work
- lowres_latent
- highres_latent
- vae
- upscaler
- highres_latent
Generating video at 480p and upscaling is the production pipeline, not a compromise. What people usually mean by it, though, is decode the frames, run SeedVR2 or a tile upscaler, encode them back, and hope the second diffusion pass doesn't fight the first one. This node is the version that stays in latent space and does the boring, correct thing at the seam between two samplers.
It takes the fully denoised output of your low-res sampler and produces a clean latent on the target grid. That's it for the output. The re-noising isn't its job - the next SamplerCustom does that with add_noise = True and the high half of the schedule from Swan Sigmas Split: Low Runs to Zero. If you don't have both of those pieces in your graph, this node has nothing sensible to hand off to.
The mechanism, in the order the code does it
H3 samples video and audio together as a nested latent, and the first thing this node does is split that nested structure into streams and find the video one. Video gets lifted; the audio stream is copied through untouched, so it can sit in an AV graph without mangling your audio.
Then it decides the output grid. Connect highres_latent - an empty latent of the size you actually want - and the node reads H/W off it and uses that exactly. Otherwise it multiplies the video latent's dimensions by target_scale and rounds to even latent dims, half-up: a 32px-alignment thing, because plain rounding silently shaved a row off a 1280x704 target. If the computed grid already equals the input, the node returns your latent unchanged - a pass-through, not an error, which is worth knowing before you spend twenty minutes wondering why target_scale did nothing.
Now the interesting part. The low-res endpoint gets lifted twice:
- a direct lift in latent space - through the learned H3 upscaler if you connected one, otherwise plain interpolation (
nearestorbilinear); - a pixel anchor - decode through the VAE, bicubic upscale in pixel space with antialiasing, re-encode. Frames are processed 32 at a time, because that decode is the memory spike.
Those two disagree wherever the upscale invented something the pixels don't support. So the node measures the residual between them per spatial location, takes the top rho fraction by that magnitude, and nudges those locations toward the pixel anchor - weakly where the residual is small (w_min), up to w_max at the worst offenders. Everything below the threshold is left exactly as the direct lift produced it. That's the whole idea of SelfLift-zero (arXiv:2609.02036, mode "zero"): don't average two lifts, trust one and patch its worst spots.
Setting rho = 0 skips the pixel route entirely and gives you the raw direct lift, no VAE needed. rho = 1 with both weights at 1 is just the pixel anchor.
What to actually set
direct_lift defaults to learned, which needs the upscaler handle from the pack's loader node. If nothing's connected it falls back to nearest and prints a line to the console - it won't error on you, but the fallback is visibly softer than the learned path, so check that print before blaming your sampler.
rho defaults to 0.3. The author's tooltip carries H3-specific advice worth following: 0.6 with w_min/w_max at 1/1 as a starting point. The w_min/w_max ramp is a fine-tuning knob; start them both at 1 so every risky location gets fully corrected, and soften w_min only if you see the correction itself causing seams.
vae is required whenever rho > 0. Use the VAE of the sampled model - re-encoding through a mismatched VAE inverts your latent into something the high-res sampler can't read.
Output is a single highres_latent, which goes into the second SamplerCustom. You also need an empty H3 AV latent for that second sampler, matching the clip's frame count.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/swan7-py/ComfyUI_Swan_Bits
# restart ComfyUI
Or Manager, searching Swan_Bits. No pip step - the pack ships no requirements file and imports only torch and einops. For direct_lift: learned you want an H3 checkpoint in ComfyUI/models/latent_upscale_models/; nearest with rho = 0 runs with nothing downloaded, which is a cheap way to sanity-check your graph.
Where it bites
Rank errors on the audio stream. The nested AV latent has to arrive intact. Split or merge it with generic latent nodes and you'll get a long error explaining that H3 audio must stay [B, 32, 2, T] next to video [B, 24, T, H, W], and pointing at the fix: build the low-res stage from a second Empty MiniMax H3 AV Latent - same frame count, smaller width/height. Don't downscale a latent by hand.
VRAM, during the pixel route. The VAE pass is the expensive half, and it runs before the high-res sampler has allocated anything. Turn on offload_after_upscale on the loader if you're squeezing.
Neither this node nor SelfLift has a community footprint yet - no threads, no shared workflows. You're early, and the tooltips are the best documentation that exists.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| lowres_latent | LATENT | Fully denoised low-resolution latent (the low-res SamplerCustom output). | |
| target_scale | FLOAT | 1.431–4 | Linear width/height lift factor. Ignored when highres_latent is connected. |
| direct_lift | COMBO | learned | learned requires the connected upscaler handle, otherwise falls back to nearest. |
| rho | FLOAT | 0.300–1 | Fraction of highest-risk locations corrected toward the VAE pixel anchor. H3 testing suggests 0.6 with w=1/1 as a starting point; 0 skips the pixel route. |
| w_min | FLOAT | 0.500–1 | — |
| w_max | FLOAT | 1.000–1 | — |
| highres_latentopt | LATENT | Optional empty target latent; its H/W define the output grid exactly. | |
| vaeopt | VAE | Required for rho > 0 (pixel-anchor route). Use the VAE of the sampled model. | |
| upscaleropt | H3_LATENT_UPSCALER | Provider handle from the Swan H3 upscaler loader node. | |
| keep_audioopt | BOOLEAN | false | Protect the audio stream in the high-res stage (audio mask = 0). The fused SelfLift sampler carries audio across the boundary; a split graph re-noises it unless protected. Turn on for audio-driven / digital-human workflows. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| highres_latent | LATENT | — |