Nodes/ComfyUI-DiffHDR/DiffHDR (Image / Video)
ComfyUI Node

DiffHDR (Image / Video)

Get the window back out of a blown-out clip

By claussteinmassl·Created 2 days ago·Updated a day ago· 5
DiffHDR (Image / Video)
  • model
  • vae
  • images
  • clip
  • mask
  • reference_image
  • hdr
  • mask
presetfast
prompt
reference_ev5.0
resize_modecrop_to_720p
width1280
height720
steps20
seed10
samplerres_multistep
schedulersimple
shift8.0
mask_overexposedtrue
mask_underexposedfalse
window_size33
window_stride16
use_prev_window_referencetrue
attentionauto
vae_precisionfp32

You have footage where the sky is a flat white shape and the room is a flat black one. Nothing in those pixels is recoverable with a curve, because the data isn't there - it was clipped when it was shot or encoded. DiffHDR (Image / Video) is the node that goes and invents it back with a video diffusion model, and hands you scene-referred linear HDR at the other end instead of another 8-bit PNG.

That's the whole pitch. It's not an upscaler and not a grade, and it's not a wrapper around somebody else's inference engine: it's a port of Eyeline Labs' DiffHDR onto ComfyUI's native Wan2.1-VACE-14B objects, so it runs on the same checkpoints, VAE and loaders as your other VACE graphs.

How it works

DiffHDR treats LDR-to-HDR as generative radiance inpainting in the video model's latent space. Inside this node:

  1. Frames are converted to linear light and squeezed through a log-gamma curve (log(2.2·x + 1), normalised to 0–1) so several stops of highlight fit in a sane range.
  2. A mask detector finds the over-exposed regions - soft luma threshold, plus a nudge for pixels pinned in two or more channels - with a temporal pass so the mask doesn't strobe.
  3. Those frames go in as the VACE control video with that mask as control_masks, and the DiffHDR LoRA (auto-downloaded, ~60 MB, 80 patches) is applied to the model.
  4. VACE samples at cfg 1 with the trained text embedding baked in; the latent is decoded and the log curve is inverted. What comes out is float linear Rec.709 with values legitimately above 1.0.

One frame in → image mode. Note that "image mode" is not cheaper: the single frame is replicated up to the trained 33-frame window, so you pay the same sampling cost as a 33-frame video. That surprises people.

The inputs you actually touch

  • preset (default fast) and steps (20). fast is res_multistep / simple / shift 8; original reproduces the reference implementation (euler / simple / shift 5) and needs twice the steps to match it. 20 steps is the shipped default, 10 is fine, 6 is for seed hunting - and sampler / scheduler / shift only do anything when preset is custom.
  • resize_mode - crop_to_720p (default) centre-crops to the 1280×720 training resolution, native keeps your size floored to multiples of 16, custom uses width/height. The model was trained at 720p on 33-frame windows; everything else is extrapolation, though it generally behaves.
  • mask_overexposed (on) and mask_underexposed (off) pick what gets regenerated. The optional mask input overrides detection entirely - white means regenerate.
  • reference_image + reference_ev (default 5 stops): feed the node a shot of what was behind the glass and it will guide the reconstruction toward it.
  • Long clips: frames past window_size (33, must be 4n+1) get sampled as overlapping sliding windows every window_stride (16) and blended. Leave use_prev_window_reference on - it's on by default since 0.3.0 and it's the difference between a background that stays put and one that cross-fades into a different invention every window. attention / vae_precision: leave them at auto and fp32. as_loaded saves memory and can band your highlights.

Outputs are hdr (float linear, values > 1.0) and mask. Wire hdr to DiffHDR Save EXR, or to DiffHDR Tonemap Preview if you just want to look at it. Do not feed hdr straight to a SaveImage - an 8-bit save clips exactly the data you paid for.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/claussteinmassl/ComfyUI-DiffHDR.git
pip install -r ComfyUI-DiffHDR/requirements.txt   # OpenEXR + huggingface_hub

Or search DiffHDR in ComfyUI Manager. Then drop wan2.1_vace_14B_fp16.safetensors and wan_2.1_vae.safetensors from Comfy-Org/Wan_2.1_ComfyUI_repackaged into models/diffusion_models and models/vae, and restart. The text encoder is optional - DiffHDR was trained with an empty prompt and its embeddings ship in the repo, so leaving clip unconnected saves you the ~11 GB umT5 download. workflows/diffhdr_video.json is the two-loaders-and-done graph.

Where people get burned

  • Wrong model. Connect anything that isn't Wan2.1-VACE-14B - the 1.3B VACE, a plain Wan 2.1, Wan 2.2 - and you get a ValueError. The node checks for 8 VACE blocks and a hidden size of 5120.
  • VRAM. Measured peaks for a 720p 33-frame run: 50.9 GB at plain fp16, 34.9 GB at fp8_e4m3fn, 28.7 GB as GGUF Q4_K_M via city96's ComfyUI-GGUF (UnetLoaderGGUF). This is not a 8 GB-card node. GGUF Q8 is the sweet spot if fp16 doesn't fit; Q4_K_M is the last stop.
  • Turbo / speed LoRAs don't work here. Thirteen Wan 2.1 step-distillation LoRAs were tested: they load fine but shift the value of the log-encoded output, and the log curve turns that into exponentially blown highlights - one measured +3,894 % highlight energy at 6 steps. They also save zero time per step. Leave them out.
  • It feels slow. Every execution logs a DiffHDR timing [...] line. If masks or control dwarfs sample, your PyTorch thread pool is sized from the host's advertised cores - set OMP_NUM_THREADS to what you actually have before launching ComfyUI.
  • The nodes don't appear at all. This pack registers through ComfyUI's newer comfy_api V3 node interface. On an older ComfyUI it imports, silently registers nothing, and you'd swear the install failed. Update ComfyUI first.
  • The Windows GPU path is untested by the author - Linux and macOS CI only.

And set expectations: there is no ground-truth HDR here. Two 50-step runs of the same clip, differing only in seed, sit 25–32 dB apart inside the mask.

CategoryDiffHDR

Inputs (24)

NameTypeDefaultDescription
presetCOMBOfastfast = res_multistep / simple / shift 8 (recommended, measured on Wan2.1-VACE-14B: matches the 50-step reference at 10-20 steps, 4-7x faster). original = euler / simple / shift 5 (the reference implementation's sampler). custom = use the sampler / scheduler / shift widgets below.
modelMODELWan2.1-VACE-14B diffusion model (bf16, fp8 or GGUF). The DiffHDR LoRA is downloaded and applied automatically.
vaeVAEWan 2.1 VAE. Used in float32 by default to avoid banding in the log-encoded output.
imagesIMAGEsRGB LDR image or frame batch.
promptSTRINGOptional prompt (DiffHDR was trained with an empty prompt). Only used when a CLIP is connected.
reference_evFLOAT5.0-10–10Exposure boost in stops applied to the reference image.
resize_modeCOMBOcrop_to_720pcrop_to_720p: centre-crop and resize to 1280x720 (training resolution; 720x1280 for portrait). native: keep size, floored to multiples of 16. custom: use width/height.
widthINT128016–8192Width for resize_mode=custom.
heightINT72016–8192Height for resize_mode=custom.
stepsINT201–200Sampling steps. 20 is the tuned default; 10 is enough with the fast preset; 50 = reference-implementation default.
seedINT100–18446744073709550000Noise seed. Long videos use the same seed for every window.
samplerCOMBOres_multistepSampler, used when preset = custom. res_multistep and dpmpp_2m are equivalent and reach the 50-step reference in far fewer steps than euler.
schedulerCOMBOsimpleScheduler, used when preset = custom. Only simple was measured; beta is deliberately not offered because it crushes highlights.
shiftFLOAT8.01–12Flow-matching shift, used when preset = custom. 8 measured best with every sampler; 5 is the reference implementation's value.
mask_overexposedBOOLEANtrueDetect and regenerate over-exposed (clipped) regions.
mask_underexposedBOOLEANfalseAlso detect and regenerate under-exposed (crushed) regions.
window_sizeINT335–129Frames per window (4n+1). 33 is the training length.
window_strideINT161–128Frames between window starts for long videos. Must be smaller than window_size.
use_prev_window_referenceBOOLEANtrueLong videos: feed each sliding window the previous window's output frame (at the next window's start) as its reference image, so the windows reconstruct the same content in the clipped regions. Measured to reduce the window-to-window change of the reconstructed background a lot. Off: every window is reconstructed on its own and the blend can visibly cross-fade between different reconstructions.
attentionCOMBOautoauto: SageAttention, then flash-attn if installed, else ComfyUI's default. Unavailable backends fall back to PyTorch SDPA. SageAttention is quantised attention: choose sdpa or flash_attn for bit-reproducible results.
vae_precisionCOMBOfp32fp32 is recommended. as_loaded saves memory but can cause banding in highlights.
clipoptCLIPOptional umT5-xxl text encoder. If not connected, the bundled DiffHDR embeddings are used and the prompt is ignored.
maskoptMASKOptional mask of regions to regenerate (white = regenerate). Overrides automatic detection.
reference_imageoptIMAGEOptional sRGB reference guiding the content of over-exposed regions.

Outputs (2)

NameTypeDescription
hdrIMAGELinear scene-referred HDR (Rec.709 primaries, float32, values above 1.0). Do not route through 8-bit save nodes.
maskMASKMask of regenerated regions.