Split the DFR pipeline into composable stages
Addresses the review on #14567: use the same compose pattern as the other LTX
two-stage pipelines, get ancestral Euler from an existing scheduler, and put the
temporal rounds in their own pipeline so the schedule is not switched mid-call.
`LTX2DFRPipeline.__call__` is now one denoise pass at `height` x `width`. Callers
compose stage 1, `LTX2LatentUpsamplePipeline`, stage 2 and each temporal round,
and a documented recipe is the copy-paste 1080p path. The recipe knobs
(`spatial_upscalings`, `temporal_upscalings`, `detailing_lora_adapter_name`) and
the required upsampler components are gone; `height`/`width` are this pass, not
the final output.
`ancestral_euler_step` is replaced by `LTXEulerAncestralRFScheduler.step` plus a
re-application of the conditioning blend, which ancestral noise would otherwise
erode on the strength-0.95 seam anchors. The new `LTX2DFRTemporalRefinePipeline`
owns that scheduler and one round; stage 1, stage 2 and the epilogue stay on
`FlowMatchEulerDiscreteScheduler`. It refuses any other scheduler rather than
silently taking a deterministic step and returning a softer canvas.
Pack/unpack, `prepare_latents`, `denoise` and `encode_conditions` move to
`LTX2DFRCoreMixin` so neither pipeline subclasses the other. Public latents are
raw on both sides of every boundary, `output_type="latent"` returns the untrimmed
canvas so a slot on the pad is not dropped, and `trim_canvas` does the trim
before decode. `LTX2DFRPipelineOutput` carries `keyframes` and
`keyframe_positions`, which cannot be re-derived after a round.
Verified against the pre-split implementation: bit-exact on dummy components in
fp32 and bf16, with and without the IC-LoRA reference, over one and two rounds;
and within one bf16 ulp on the real checkpoint, where the only difference is that
the split normalizes upsampled latents in fp32 rather than bf16.
Also in this pass:
- `__call__` takes `video_tiles` (the `epilogue_tiles` layout) instead of a
resolved token plan. Resolving one needs the RoPE coordinates that only exist
once `prepare_latents` has run, so a caller could not build the plan at all.
- `rebuild_epilogue_keyframes` is public and takes and returns raw latents. The
composed epilogue needs it, so it was public API in practice while named
private, and its normalized return forced callers into `_denormalize_latents`.
- Drop prompt enhancement and `num_videos_per_prompt` from the temporal pipeline.
Enhancement belongs to stage 1 -- re-running it would denoise the canvas under
a different prompt than the one that generated it -- and the batch is set by
the incoming latent canvas, so `num_videos_per_prompt > 1` only ever raised.
- Fix the docs recipe: `requested_frames` counted latent frames where
`trim_canvas` wants pixel frames, truncating a 241-frame render to 25; the
three pipelines share components, so place them together instead of offloading
one and leaving `temporal_latent_upsampler` off the device; and the detailing
IC-LoRA is applied at 0.5, the strength the reference hardcodes.
- Pass `crf=0` on the conditions in the refine-round test. The default CRF sends
the image through H.264 re-compression, which needs PyAV, so the test failed on
any environment without it while testing nothing about re-compression.