| # LTX-V2V Reference-Conditioned Video-to-Video |
|
|
| Video-to-video generation on top of [LTX-2](https://github.com/Lightricks/LTX-2) |
| (Lightricks' DiT-based video+audio diffusion model): given a **source video** |
| (structure/motion signal) and a **first-frame reference image** (target |
| identity/appearance), generate an **output video** that follows the source |
| video's motion while adopting the reference image's look. |
|
|
| This repo is a trimmed-down, sharable fork containing: |
| - `packages/ltx-core`, `packages/ltx-pipelines` β the LTX-2 inference stack, |
| with a small local patch to the reference-conditioning position logic (see |
| below) that is **not** in upstream `Lightricks/LTX-2`. |
| - `packages/ltx-trainer` β LTX-2's LoRA/IC-LoRA training stack, with a matching |
| patch on the training side, plus a `reference_image` dataset column so you |
| can train on (video, reference_image, caption) triples. |
| - `scripts/infer_v2v.py` β new inference script for the video+ref-image β |
| video workflow described above. |
| - `configs/*.yaml`, `scripts/train_ic_lora.sh` β training configs and launcher |
| for the two custom IC-LoRA variants this workflow needs. |
| - `data/` β dataset format documentation and example JSONL rows. |
|
|
| **Not included:** pretrained IC-LoRA checkpoints. The ones we trained |
| internally were trained on licensed movie footage, so we're not redistributing |
| the weights β train your own with `scripts/train_ic_lora.sh` on your own data |
| (see `data/README.md`). |
|
|
| ## Quick start |
|
|
| ```bash |
| ./setup.sh # uv sync + download ~90GB of base-model weights |
| source .venv/bin/activate |
| |
| # 1. Prepare your data (see data/README.md for the JSONL schema) |
| scripts/preprocess_dataset.sh data/dataset.jsonl "1920x1024x233" |
| |
| # 2. Train an IC-LoRA (defaults to configs/v2v_reference_ic_lora.yaml) |
| scripts/train_ic_lora.sh --config configs/v2v_reference_ic_lora.yaml |
| |
| # 3. Run inference with your trained checkpoint |
| python scripts/infer_v2v.py \ |
| --input-video my_source_video.mp4 \ |
| --ref-image my_reference.png \ |
| --prompt "A woman in a red dress walks through a garden, camera tracking steadily." \ |
| --structure-lora packages/ltx-trainer/outputs/v2v_reference_ic_lora/checkpoints/lora_weights_step_08000.safetensors \ |
| --output out.mp4 |
| ``` |
|
|
| `setup.sh` downloads gated/large weights from HuggingFace β if a download |
| 401s, run `huggingface-cli login` (or set `HF_TOKEN`) first. |
|
|
| ## How it works |
|
|
| ### Input video β output video |
|
|
| The source video is never fed to the transformer as raw pixels through a |
| special "V2V" code path β there isn't one. It goes through the exact same |
| `video_conditioning` mechanism IC-LoRA uses for any reference signal |
| (`ICLoraPipeline.__call__`'s `video_conditioning: list[tuple[path, strength]]` |
| parameter, in `packages/ltx-pipelines/src/ltx_pipelines/ic_lora.py`): it's |
| VAE-encoded, patchified into tokens exactly like the noisy video latent, and |
| **appended to the same self-attention token sequence** the denoiser attends |
| over (`VideoConditionByReferenceLatent`, |
| `packages/ltx-core/src/ltx_core/conditioning/types/reference_video_cond.py`). |
| A LoRA (`--structure-lora`) is what actually teaches the model to *use* those |
| extra tokens meaningfully β the base model has no opinion on what an arbitrary |
| appended video means. |
|
|
| Important: `reference_video`/`--input-video` is not necessarily raw source |
| footage β it's whatever structure/motion signal your LoRA was trained to |
| condition on (pose render, edge map, or the raw video itself, if that's what |
| you trained with). See `data/README.md` β "About `reference_video`". |
|
|
| ### Two ways to position-encode the reference image |
|
|
| This is the part that's easy to get subtly wrong, so it's exposed as an |
| explicit `--pos-mode` flag in `scripts/infer_v2v.py` rather than being baked in |
| silently: |
|
|
| **`--pos-mode first-frame` (default)** β the reference image **reuses** the |
| denoised video's own frame-0 RoPE position. Implemented via `ICLoraPipeline`'s |
| native `images: list[(path, frame_idx, strength)]` parameter β this is a |
| **base-model capability, no LoRA required**. Code path: |
| `combined_image_conditionings()` in |
| `packages/ltx-pipelines/src/ltx_pipelines/utils/helpers.py` β |
| `VideoConditionByLatentIndex` / `VideoConditionByKeyframeIndex` in |
| `packages/ltx-core/src/ltx_core/conditioning/types/`. Because the reference |
| image is placed at the exact same absolute time position as frame 0, RoPE's |
| relative-rotation attention naturally treats it as "this is what frame 0 looks |
| like" rather than as a separate signal. |
|
|
| **`--pos-mode reference`** β the reference image gets its **own, disjoint** |
| RoPE position range instead of sharing frame 0's. Implemented via |
| `video_conditioning` β `VideoConditionByReferenceLatent` |
| (`packages/ltx-core/src/ltx_core/conditioning/types/reference_video_cond.py`). |
| This is the path our custom IC-LoRAs (`configs/ref_image_ic_lora.yaml`, |
| `configs/v2v_reference_ic_lora.yaml`) are actually trained against, and is |
| generally the better-performing option **once you have a matching LoRA** β the |
| base model alone doesn't know what to do with a disjoint-position reference |
| block; it has to be taught. |
|
|
| Why "disjoint" instead of just also using t=0: giving the reference image the |
| same absolute position as the target's frame 0 means RoPE's relative rotation |
| between them is **zero** β the attention mechanism can't tell the reference |
| token and the target's own frame-0 token apart by position alone. The fix |
| (originally a local patch on top of upstream LTX-2, in |
| `reference_video_cond.py`): |
|
|
| ```python |
| # packages/ltx-core/src/ltx_core/conditioning/types/reference_video_cond.py |
| REF_POSITION_TICK_SECONDS = 1e-3 |
| current_min_time = latent_state.positions[:, 0, ...].min() |
| ref_max_time = positions[:, 0, ...].max() |
| positions[:, 0, ...] = positions[:, 0, ...] - ref_max_time + current_min_time - REF_POSITION_TICK_SECONDS |
| ``` |
|
|
| This shifts the whole reference block to end `1ms` **before** whatever's |
| already the earliest time position in the sequence β a strictly ordered, |
| non-overlapping range. The training side |
| (`packages/ltx-trainer/src/ltx_trainer/training_strategies/flexible.py`, |
| `_apply_reference_condition`) applies the **identical** tick-offset logic, so a |
| LoRA trained with this code sees the same position layout at train and |
| inference time. If you ever modify one side, modify the other β they must stay |
| in parity. |
|
|
| When two `reference`-type conditions are stacked (as in |
| `configs/v2v_reference_ic_lora.yaml`, which uses both `reference_video` and |
| `reference_image`), each additional block is slotted immediately before |
| whatever range is already occupied, so every block β target frames, structure |
| reference, appearance reference β ends up in its own strictly-ordered slice of |
| time, none of them aliasing. |
|
|
| ### Conditioning strength (separate from position mode) |
|
|
| Two independent knobs control how strongly a reference item affects the |
| output, orthogonal to which position mode you use: |
|
|
| - **`--ref-strength` / `--structure-strength`** (denoise strength, `strength` |
| in `ConditioningItem`): controls how much noise the conditioning latent |
| itself gets mixed with during denoising (`GaussianNoiser.torch.lerp`, in |
| `packages/ltx-core/src/ltx_core/components/noisers.py`). `1.0` = the |
| reference stays exactly at its clean VAE-encoded value; lower values let the |
| model deviate from it. |
| - **`--conditioning-attention-strength`**: scales how strongly conditioning |
| tokens attend to/from the noisy tokens in self-attention, independent of the |
| noise blending above. |
|
|
| ## Repository layout |
|
|
| ``` |
| setup.sh one-click env + weights setup |
| pyproject.toml, uv.lock workspace definition (uv) |
| packages/ |
| ltx-core/ LTX-2 model + conditioning + RoPE (patched) |
| ltx-pipelines/ ICLoraPipeline and friends |
| ltx-trainer/ LoRA / IC-LoRA training stack (patched) |
| configs/ |
| v2v_reference_ic_lora.yaml train on (video, reference_video, reference_image, caption) |
| ref_image_ic_lora.yaml simpler: train on (video, reference_image, caption) only |
| accelerate/ accelerate configs referenced by the trainer docs |
| scripts/ |
| train_ic_lora.sh resolves __REPO_ROOT__ in a config, launches training |
| preprocess_dataset.sh wraps packages/ltx-trainer/scripts/process_dataset.py |
| infer_v2v.py video + reference image -> video, --pos-mode switch |
| data/ |
| README.md dataset JSONL schema, resolution buckets, etc. |
| dataset.jsonl example rows: video + reference_video + reference_image + caption |
| dataset_image_only.jsonl example rows: video + reference_image + caption |
| weights/ populated by setup.sh (gitignored β large binaries) |
| ``` |
|
|
| ## Which config to train |
|
|
| - Have paired (source video, structure signal, reference image) data? Use |
| `configs/v2v_reference_ic_lora.yaml` β trains one LoRA that uses both |
| `--input-video` and `--ref-image` at inference. |
| - Only have (video, reference image) pairs, no structure video? Use |
| `configs/ref_image_ic_lora.yaml` β simpler, doesn't need `--input-video` at |
| all (pass an empty/no-op structure conditioning, or adapt |
| `scripts/infer_v2v.py` to drop `--structure-lora`/`--input-video` for this |
| case). |
|
|
| See `data/README.md` for the exact JSONL schema, column-to-encoder mapping, and |
| resolution-bucket rules. |
|
|
| ## Known limitations / things we couldn't fully verify |
|
|
| - We could not confirm from the LTX-2 repo alone whether the public |
| `LTX-2.3-22b-IC-LoRA-Union-Control` LoRA expects raw source video or a |
| preprocessed control map (depth/pose/edge) for its `video_conditioning` |
| input β its model card (not vendored here) would be the source of truth if |
| you want to use it instead of training your own structure-control LoRA. |
| - `reference_video`/`--input-video` preprocessing (pose/edge/depth extraction) |
| is out of scope for this repo β bring your own, or use the basic Canny-edge |
| `compute_reference()` in `packages/ltx-trainer/scripts/process_videos.py` as |
| a starting point. |
|
|