H3 Join Latents (obvpm)
One latent for the whole timeline, so texture is decided across the joins
- latent
- layout
The honest summary of upscaling a chain, before this node existed: do it per clip and the seams get worse. A per-clip refine re-invents fine texture from its own prior and noise, and no amount of held neighbour makes it copy what the neighbour invented. The pack measured it - a refine moves 74% of the high-pass texture away from its upscaled prior, per clip, per seed, and that was with exact holds, matched noise levels, one noise field, keyframe references, continuous upscaled latents and a 27-step hold.
A single long clip doesn't have that problem, because every row is sampled in one pass with every other row in view. H3 Join Latents is how a chain gets the same treatment: it lays every clip of your timeline onto ONE raw AV latent at its true position, held windows coinciding, so the whole sequence can be upscaled as one, sampled as one, and rendered as one.
Inputs and outputs
One input - sequence, the Timeline's sequence output. It's a string, one output-relative clip per line, and wiring the Timeline's own sequence output rather than retyping the list is the point: the refine pass reads the SAME list the cut was built from, so the two can't drift apart.
Two outputs. latent is the joint raw AV latent (video + audio) - wire it to the latent upscaler, and also to H3 Joint VAE Decode and Save's source_audio when the sound gets re-sampled. layout is where each clip sits on that latent: one wire goes to H3 Joint Conditioning, another to H3 Joint VAE Decode and Save (for provenance, and as the check that refuses a latent that isn't the timeline the joint describes).
The wiring order
The joint refine is five nodes, and this is the first of them:
Timeline.sequence ─> H3 Join Latents ─> upscaler (temporal chunking ON)
─> H3 Joint Audio Mask ─> sampler.latent_image
layout ─> H3 Joint Conditioning ─> sampler.conditioning
sampler ─> H3 Joint VAE Decode and Save
The point of the whole arrangement, in the source's own words: the sampled timeline is the cut - every row is the clip the cut shows there - so there's no reassembly step, no profile folder, no per-clip take to stitch back together. You used to get refined clips nobody edits again, at the price of a loop in the graph. Not any more.
Two practical constraints. Every clip in a continuation chain must share one resolution, because latents can't be resized - mismatches are refused loudly. And the latent upscaler's temporal chunking must be on: upscaling the whole timeline in one pass produces a prior the refine hallucinates on, at every strength and under every sampling scheme. That one is counterintuitive enough that people hit it and blame the refine.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/chanon/comfyui-obvpm-timeline
Restart ComfyUI. No Python dependencies - the pyproject declares an empty list deliberately, since torch, torchaudio, numpy, safetensors and av already ship with ComfyUI. You need ComfyUI 2026-08-13 or later for core H3 support. In Manager, search comfyui-obvpm-timeline; every node in the pack ends in (obvpm), so obvpm in the node menu finds all of them.
For the refine branch specifically you also want the companion packs the README lists - the MiniMax H3 latent upscaler (Comfyui_Minimax_h3_latent_Upscaler) is the one this pass is built around, and ComfyUI-KJNodes provides the Set/Get plumbing the workflow uses. Manager's Install Missing Custom Nodes resolves them from the bundled workflow.
Troubleshooting
- "A latent that is not the timeline the joint describes is refused." That message comes from the render node and usually means the sequence changed between building this latent and rendering it - a take was added or reordered mid-run. Re-run the whole refine pass.
- Every clip must be the same resolution. Non-negotiable. Latents can't be resized, so a mixed-resolution timeline can't be joined; fix it before this node rather than after.
- Memory. This node itself is cheap; it's laying latents down. The cost lands on the upscaler and the sampler - see H3 Context Windowing for the window sizing that keeps it in VRAM.
- Clip list empty or unreadable. The
sequencestring is verbatim - comments included - and readers strip what they don't need. A malformed entry fails loudly; a mispointedbase_foldershows up as clips that can't be found. - You wanted a per-clip upscale. That's the other branch of the workflow, and it's a different tradeoff: faster to iterate, but the seams pay for it.
Generate at low resolution for fast iteration, then run this pass once at the end. That's the intended shape of the workflow, and it's the reason the refine exists as a separate mode rather than a setting.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| sequence | STRING | The timeline, one output-relative clip per line -- the Timeline's sequence output. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| latent | LATENT | The joint raw AV latent (video + audio), for the upscaler -- and for H3 Joint VAE Decode and Save's source_audio when the sound is re-sampled. |
| layout | OBVPM_H3_JOINT | Where each clip sits on it. Wire to H3 Joint Conditioning and H3 Joint VAE Decode and Save. |