ibyteohdear commited on
Commit
cff4b2e
·
verified ·
1 Parent(s): 6f39c5a

Upload folder using huggingface_hub (part 2)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. packages/ltx-trainer/configs/a2v_lora.yaml +344 -0
  2. packages/ltx-trainer/configs/accelerate/ddp.yaml +16 -0
  3. packages/ltx-trainer/configs/accelerate/ddp_compile.yaml +21 -0
  4. packages/ltx-trainer/configs/accelerate/fsdp.yaml +29 -0
  5. packages/ltx-trainer/configs/accelerate/fsdp_compile.yaml +34 -0
  6. packages/ltx-trainer/configs/audio_extend_lora.yaml +343 -0
  7. packages/ltx-trainer/configs/audio_inpainting_lora.yaml +324 -0
  8. packages/ltx-trainer/configs/audio_suffix_lora.yaml +343 -0
  9. packages/ltx-trainer/configs/av2av_ic_lora.yaml +342 -0
  10. packages/ltx-trainer/configs/i2v_lora.yaml +348 -0
  11. packages/ltx-trainer/configs/t2a_lora.yaml +317 -0
  12. packages/ltx-trainer/configs/t2v_lora.yaml +327 -0
  13. packages/ltx-trainer/configs/t2v_lora_low_vram.yaml +339 -0
  14. packages/ltx-trainer/configs/v2a_lora.yaml +349 -0
  15. packages/ltx-trainer/configs/v2v_ic_lora.yaml +359 -0
  16. packages/ltx-trainer/configs/video_extend_lora.yaml +355 -0
  17. packages/ltx-trainer/configs/video_inpainting_lora.yaml +343 -0
  18. packages/ltx-trainer/configs/video_outpainting_lora.yaml +341 -0
  19. packages/ltx-trainer/configs/video_suffix_lora.yaml +355 -0
  20. packages/ltx-trainer/docs/configuration-reference.md +464 -0
  21. packages/ltx-trainer/docs/custom-training-strategies.md +515 -0
  22. packages/ltx-trainer/docs/dataset-preparation.md +489 -0
  23. packages/ltx-trainer/docs/quick-start.md +160 -0
  24. packages/ltx-trainer/docs/training-guide.md +215 -0
  25. packages/ltx-trainer/docs/training-modes.md +600 -0
  26. packages/ltx-trainer/docs/troubleshooting.md +314 -0
  27. packages/ltx-trainer/docs/utility-scripts.md +239 -0
  28. packages/ltx-trainer/pyproject.toml +98 -0
  29. packages/ltx-trainer/scripts/caption_videos.py +536 -0
  30. packages/ltx-trainer/scripts/compute_reference.py +304 -0
  31. packages/ltx-trainer/scripts/decode_latents.py +369 -0
  32. packages/ltx-trainer/scripts/process_captions.py +497 -0
  33. packages/ltx-trainer/scripts/process_dataset.py +432 -0
  34. packages/ltx-trainer/scripts/process_videos.py +1474 -0
  35. packages/ltx-trainer/scripts/serve_captioner.py +203 -0
  36. packages/ltx-trainer/scripts/split_scenes.py +417 -0
  37. packages/ltx-trainer/scripts/train.py +63 -0
  38. packages/ltx-trainer/src/ltx_trainer/__init__.py +44 -0
  39. packages/ltx-trainer/src/ltx_trainer/captioning.py +455 -0
  40. packages/ltx-trainer/src/ltx_trainer/config.py +833 -0
  41. packages/ltx-trainer/src/ltx_trainer/config_display.py +156 -0
  42. packages/ltx-trainer/src/ltx_trainer/datasets.py +315 -0
  43. packages/ltx-trainer/src/ltx_trainer/gemma_8bit.py +104 -0
  44. packages/ltx-trainer/src/ltx_trainer/gpu_utils.py +90 -0
  45. packages/ltx-trainer/src/ltx_trainer/hf_hub_utils.py +212 -0
  46. packages/ltx-trainer/src/ltx_trainer/model_loader.py +378 -0
  47. packages/ltx-trainer/src/ltx_trainer/progress.py +182 -0
  48. packages/ltx-trainer/src/ltx_trainer/quantization.py +195 -0
  49. packages/ltx-trainer/src/ltx_trainer/sigma_tracker.py +59 -0
  50. packages/ltx-trainer/src/ltx_trainer/timestep_samplers.py +160 -0
packages/ltx-trainer/configs/a2v_lora.yaml ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Audio-to-Video LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # audio-to-video generation. The model learns to generate videos conditioned
7
+ # on a frozen audio signal via the transformer's built-in cross-modal attention.
8
+ #
9
+ # In this mode, audio is provided as a frozen (clean, no noise, no loss)
10
+ # conditioning signal. The video modality is the only generated output.
11
+ # Audio influences video generation through the transformer's audio-to-video
12
+ # cross-attention mechanism.
13
+ #
14
+ # Use this configuration when you want to:
15
+ # - Generate videos driven by audio content (e.g., music visualizations)
16
+ # - Train audio-reactive video generation
17
+ # - Create models that synchronize video with given audio
18
+ #
19
+ # Dataset structure:
20
+ # preprocessed_data_root/
21
+ # ├── latents/ # Video latents (VAE-encoded videos)
22
+ # ├── conditions/ # Text embeddings for each video
23
+ # └── audio_latents/ # Audio latents (frozen conditioning input)
24
+ #
25
+ # =============================================================================
26
+
27
+ # -----------------------------------------------------------------------------
28
+ # Model Configuration
29
+ # -----------------------------------------------------------------------------
30
+ # Specifies the base model to fine-tune and the training mode.
31
+ model:
32
+ # Path to the LTX-2 model checkpoint (.safetensors file)
33
+ # This should be a local path to your downloaded model
34
+ model_path: "path/to/ltx-2-model.safetensors"
35
+
36
+ # Path to the text encoder model directory
37
+ # For LTX-2, this is typically the Gemma-based text encoder
38
+ text_encoder_path: "path/to/gemma-text-encoder"
39
+
40
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
41
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
42
+ training_mode: "lora"
43
+
44
+ # Optional: Path to resume training from a checkpoint
45
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
46
+ load_checkpoint: null
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # LoRA Configuration
50
+ # -----------------------------------------------------------------------------
51
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
52
+ lora:
53
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
54
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
55
+ rank: 32
56
+
57
+ # Alpha scaling factor (usually set equal to rank)
58
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
59
+ alpha: 32
60
+
61
+ # Dropout probability for LoRA layers (0.0 = no dropout)
62
+ # Can help with regularization if overfitting occurs
63
+ dropout: 0.0
64
+
65
+ # Which transformer modules to apply LoRA to
66
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
67
+ #
68
+ # VIDEO MODULES:
69
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
70
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
71
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
72
+ #
73
+ # AUDIO MODULES:
74
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
75
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
76
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
77
+ #
78
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
79
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
80
+ # (Q from video, K/V from audio - allows video to attend to audio features)
81
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
82
+ # (Q from audio, K/V from video - allows audio to attend to video features)
83
+ #
84
+ # Using short patterns like "to_k" matches ALL attention modules (video, audio, and cross-modal).
85
+ # For audio-video training, this is the recommended approach.
86
+ target_modules:
87
+ # Attention layers (matches both video and audio branches)
88
+ - "to_k"
89
+ - "to_q"
90
+ - "to_v"
91
+ - "to_out.0"
92
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
93
+ # - "ff.net.0.proj"
94
+ # - "ff.net.2"
95
+ # - "audio_ff.net.0.proj"
96
+ # - "audio_ff.net.2"
97
+
98
+ # -----------------------------------------------------------------------------
99
+ # Training Strategy Configuration
100
+ # -----------------------------------------------------------------------------
101
+ # Defines the audio-to-video training approach using the unified flexible strategy.
102
+ # Audio is frozen (no noise, sigma=0, excluded from loss) and conditions video
103
+ # generation via the transformer's built-in cross-modal attention.
104
+ training_strategy:
105
+ # Strategy name: "flexible" for the unified conditioning framework
106
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
107
+ # modality-specific configuration blocks.
108
+ name: "flexible"
109
+
110
+ # Video modality configuration
111
+ # Video is the generated (denoised) output
112
+ video:
113
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
114
+ is_generated: true
115
+ # Directory name (within preprocessed_data_root) containing video latents
116
+ latents_dir: "latents"
117
+
118
+ # Audio modality configuration
119
+ # Audio is frozen — it acts as conditioning for video generation
120
+ # Frozen modalities get sigma=0, timestep=0, no noise, and no loss
121
+ audio:
122
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
123
+ # When false, audio is passed through the transformer clean and influences
124
+ # video via cross-modal attention
125
+ is_generated: false
126
+ # Directory name (within preprocessed_data_root) containing audio latents
127
+ latents_dir: "audio_latents"
128
+
129
+ # -----------------------------------------------------------------------------
130
+ # Optimization Configuration
131
+ # -----------------------------------------------------------------------------
132
+ # Controls the training optimization parameters.
133
+ optimization:
134
+ # Learning rate for the optimizer
135
+ # Typical range for LoRA: 1e-5 to 1e-4
136
+ learning_rate: 1e-4
137
+
138
+ # Total number of training steps
139
+ steps: 2000
140
+
141
+ # Batch size per GPU
142
+ # Reduce if running out of memory
143
+ batch_size: 1
144
+
145
+ # Number of gradient accumulation steps
146
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
147
+ gradient_accumulation_steps: 1
148
+
149
+ # Maximum gradient norm for clipping (helps training stability)
150
+ max_grad_norm: 1.0
151
+
152
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
153
+ optimizer_type: "adamw"
154
+
155
+ # Learning rate scheduler type
156
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
157
+ scheduler_type: "linear"
158
+
159
+ # Additional scheduler parameters (depends on scheduler_type)
160
+ scheduler_params: { }
161
+
162
+ # Enable gradient checkpointing to reduce memory usage
163
+ # Recommended for training with limited GPU memory
164
+ enable_gradient_checkpointing: true
165
+
166
+ # -----------------------------------------------------------------------------
167
+ # Acceleration Configuration
168
+ # -----------------------------------------------------------------------------
169
+ # Hardware acceleration and memory optimization settings.
170
+ acceleration:
171
+ # Mixed precision training mode
172
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
173
+ mixed_precision_mode: "bf16"
174
+
175
+ # Model quantization for reduced memory usage
176
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
177
+ quantization: null
178
+
179
+ # Load text encoder in 8-bit precision to save memory
180
+ # Useful when GPU memory is limited
181
+ load_text_encoder_in_8bit: false
182
+
183
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
184
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
185
+ # LoRA). No effect under FSDP (sharded state).
186
+ offload_optimizer_during_validation: false
187
+
188
+
189
+ # -----------------------------------------------------------------------------
190
+ # Data Configuration
191
+ # -----------------------------------------------------------------------------
192
+ # Specifies the training data location and loading parameters.
193
+ data:
194
+ # Root directory containing preprocessed training data
195
+ # Should contain: latents/, audio_latents/, and conditions/
196
+ preprocessed_data_root: "/path/to/preprocessed/data"
197
+
198
+ # Number of worker processes for data loading
199
+ # Used for parallel data loading to speed up data loading
200
+ num_dataloader_workers: 2
201
+
202
+ # -----------------------------------------------------------------------------
203
+ # Validation Configuration
204
+ # -----------------------------------------------------------------------------
205
+ # Controls validation sampling during training.
206
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
207
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
208
+ validation:
209
+ # Validation samples — each sample describes a self-contained generation request.
210
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
211
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
212
+ samples:
213
+ - prompt: >-
214
+ A musician plays an acoustic guitar in a dimly lit recording studio, fingers moving
215
+ across the fretboard with practiced ease. The warm amber light from a desk lamp
216
+ illuminates the wooden guitar body. Sound-absorbing panels line the walls, and a
217
+ microphone stands nearby on a boom arm.
218
+ conditions:
219
+ - type: audio_to_video
220
+ audio: "/path/to/conditioning_audio_1.wav"
221
+ - prompt: >-
222
+ Rain falls steadily on a cobblestone street in a European town at dusk. Puddles form
223
+ between the stones, creating ripples as new drops land. Old brick buildings line both
224
+ sides of the narrow street, their facades glistening with moisture under warm
225
+ streetlights.
226
+ conditions:
227
+ - type: audio_to_video
228
+ audio: "/path/to/conditioning_audio_2.wav"
229
+
230
+ # Negative prompt to avoid unwanted artifacts
231
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
232
+
233
+ # Output video dimensions [width, height, frames]
234
+ # Width and height must be divisible by 32
235
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
236
+ video_dims: [ 576, 576, 89 ]
237
+
238
+ # Frame rate for generated videos
239
+ frame_rate: 25.0
240
+
241
+ # Random seed for reproducible validation outputs
242
+ seed: 42
243
+
244
+ # Number of denoising steps for validation inference
245
+ # Higher values = better quality but slower generation
246
+ inference_steps: 30
247
+
248
+ # Generate validation videos every N training steps
249
+ # Set to null to disable validation during training
250
+ interval: 100
251
+
252
+ # Classifier-free guidance scale
253
+ # Higher values = stronger adherence to prompt but may introduce artifacts
254
+ guidance_scale: 4.0
255
+
256
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
257
+ # STG is combined with CFG for better temporal coherence
258
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
259
+ stg_blocks: [29] # Recommended: single block 29
260
+ stg_mode: "stg_v" # "stg_av" perturbs both audio and video, "stg_v" video only
261
+
262
+ # Whether to generate audio in validation samples
263
+ # Independent of training_strategy.audio.is_generated - you can generate audio
264
+ # in validation even when not training the audio branch
265
+ # For this audio-to-video config, false matches frozen audio conditioning (video-only synthesis).
266
+ generate_audio: false
267
+
268
+ # Generate video from the frozen audio condition
269
+ generate_video: true
270
+
271
+ # Skip validation at the beginning of training (step 0)
272
+ skip_initial_validation: false
273
+
274
+ # -----------------------------------------------------------------------------
275
+ # Checkpoint Configuration
276
+ # -----------------------------------------------------------------------------
277
+ # Controls model checkpoint saving during training.
278
+ checkpoints:
279
+ # Save a checkpoint every N steps
280
+ # Set to null to disable intermediate checkpoints
281
+ interval: 250
282
+
283
+ # Number of most recent checkpoints to keep
284
+ # Set to -1 to keep all checkpoints
285
+ keep_last_n: -1
286
+
287
+ # Precision to use when saving checkpoint weights
288
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
289
+ precision: "bfloat16"
290
+
291
+ # -----------------------------------------------------------------------------
292
+ # Flow Matching Configuration
293
+ # -----------------------------------------------------------------------------
294
+ # Parameters for the flow matching training objective.
295
+ flow_matching:
296
+ # Timestep sampling mode
297
+ # "shifted_logit_normal" is recommended for LTX-2 models
298
+ timestep_sampling_mode: "shifted_logit_normal"
299
+
300
+ # Additional parameters for timestep sampling
301
+ timestep_sampling_params: { }
302
+
303
+ # -----------------------------------------------------------------------------
304
+ # Hugging Face Hub Configuration
305
+ # -----------------------------------------------------------------------------
306
+ # Settings for uploading trained models to the Hugging Face Hub.
307
+ hub:
308
+ # Whether to push the trained model to the Hub
309
+ push_to_hub: false
310
+
311
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
312
+ # Required if push_to_hub is true
313
+ hub_model_id: null
314
+
315
+ # -----------------------------------------------------------------------------
316
+ # Weights & Biases Configuration
317
+ # -----------------------------------------------------------------------------
318
+ # Settings for experiment tracking with W&B.
319
+ wandb:
320
+ # Enable W&B logging
321
+ enabled: false
322
+
323
+ # W&B project name
324
+ project: "ltx-2-trainer"
325
+
326
+ # W&B username or team (null uses default account)
327
+ entity: null
328
+
329
+ # Tags to help organize runs
330
+ tags: [ "ltx2", "lora", "a2v" ]
331
+
332
+ # Log validation media (video/audio) to W&B
333
+ log_validation_videos: true
334
+
335
+ # -----------------------------------------------------------------------------
336
+ # General Configuration
337
+ # -----------------------------------------------------------------------------
338
+ # Global settings for the training run.
339
+
340
+ # Random seed for reproducibility
341
+ seed: 42
342
+
343
+ # Directory to save outputs (checkpoints, validation videos, logs)
344
+ output_dir: "outputs/a2v_lora"
packages/ltx-trainer/configs/accelerate/ddp.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ compute_environment: LOCAL_MACHINE
2
+ debug: false
3
+ distributed_type: MULTI_GPU
4
+ downcast_bf16: 'no'
5
+ enable_cpu_affinity: false
6
+ machine_rank: 0
7
+ main_training_function: main
8
+ mixed_precision: bf16
9
+ num_machines: 1
10
+ num_processes: 4
11
+ rdzv_backend: static
12
+ same_network: true
13
+ tpu_env: []
14
+ tpu_use_cluster: false
15
+ tpu_use_sudo: false
16
+ use_cpu: false
packages/ltx-trainer/configs/accelerate/ddp_compile.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ compute_environment: LOCAL_MACHINE
2
+ dynamo_config:
3
+ dynamo_backend: INDUCTOR
4
+ dynamo_mode: default
5
+ dynamo_use_fullgraph: false
6
+ dynamo_use_dynamic: true
7
+ debug: false
8
+ distributed_type: MULTI_GPU
9
+ downcast_bf16: 'no'
10
+ enable_cpu_affinity: false
11
+ machine_rank: 0
12
+ main_training_function: main
13
+ mixed_precision: bf16
14
+ num_machines: 1
15
+ num_processes: 4
16
+ rdzv_backend: static
17
+ same_network: true
18
+ tpu_env: [ ]
19
+ tpu_use_cluster: false
20
+ tpu_use_sudo: false
21
+ use_cpu: false
packages/ltx-trainer/configs/accelerate/fsdp.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ compute_environment: LOCAL_MACHINE
2
+ debug: false
3
+ distributed_type: FSDP
4
+ downcast_bf16: 'no'
5
+ enable_cpu_affinity: false
6
+ fsdp_config:
7
+ fsdp_activation_checkpointing: false
8
+ fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
9
+ fsdp_backward_prefetch: BACKWARD_PRE
10
+ fsdp_cpu_ram_efficient_loading: true
11
+ fsdp_forward_prefetch: false
12
+ fsdp_offload_params: false
13
+ fsdp_reshard_after_forward: FULL_SHARD
14
+ fsdp_state_dict_type: SHARDED_STATE_DICT
15
+ fsdp_sync_module_states: true
16
+ fsdp_transformer_layer_cls_to_wrap: BasicAVTransformerBlock
17
+ fsdp_use_orig_params: true
18
+ fsdp_version: 1
19
+ machine_rank: 0
20
+ main_training_function: main
21
+ mixed_precision: bf16
22
+ num_machines: 1
23
+ num_processes: 4
24
+ rdzv_backend: static
25
+ same_network: true
26
+ tpu_env: []
27
+ tpu_use_cluster: false
28
+ tpu_use_sudo: false
29
+ use_cpu: false
packages/ltx-trainer/configs/accelerate/fsdp_compile.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ compute_environment: LOCAL_MACHINE
2
+ debug: false
3
+ distributed_type: FSDP
4
+ downcast_bf16: 'no'
5
+ dynamo_config:
6
+ dynamo_backend: INDUCTOR
7
+ dynamo_mode: default
8
+ dynamo_use_fullgraph: false
9
+ dynamo_use_dynamic: true
10
+ enable_cpu_affinity: false
11
+ fsdp_config:
12
+ fsdp_activation_checkpointing: false
13
+ fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
14
+ fsdp_backward_prefetch: BACKWARD_PRE
15
+ fsdp_cpu_ram_efficient_loading: true
16
+ fsdp_forward_prefetch: false
17
+ fsdp_offload_params: false
18
+ fsdp_reshard_after_forward: FULL_SHARD
19
+ fsdp_state_dict_type: SHARDED_STATE_DICT
20
+ fsdp_sync_module_states: true
21
+ fsdp_transformer_layer_cls_to_wrap: BasicAVTransformerBlock
22
+ fsdp_use_orig_params: true
23
+ fsdp_version: 1
24
+ machine_rank: 0
25
+ main_training_function: main
26
+ mixed_precision: bf16
27
+ num_machines: 1
28
+ num_processes: 4
29
+ rdzv_backend: static
30
+ same_network: true
31
+ tpu_env: []
32
+ tpu_use_cluster: false
33
+ tpu_use_sudo: false
34
+ use_cpu: false
packages/ltx-trainer/configs/audio_extend_lora.yaml ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Audio Extension (Forward) LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # forward audio extension. The model learns to continue audio forward in time
7
+ # by conditioning on a prefix of existing audio latent timesteps.
8
+ #
9
+ # Prefix conditioning works by providing the first N audio latent timesteps as
10
+ # clean conditioning signals during training — they receive no noise,
11
+ # timestep=0, and are excluded from the loss. The model learns to generate
12
+ # audio that seamlessly continues from the given prefix.
13
+ #
14
+ # This is an audio-only training mode — no video modality is configured.
15
+ #
16
+ # Use this configuration when you want to:
17
+ # - Train the model to extend/continue existing audio
18
+ # - Fine-tune audio temporal continuation capabilities
19
+ # - Create seamless audio extension models
20
+ #
21
+ # Dataset structure:
22
+ # preprocessed_data_root/
23
+ # ├── conditions/ # Text embeddings for each sample
24
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
25
+ #
26
+ # =============================================================================
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # Model Configuration
30
+ # -----------------------------------------------------------------------------
31
+ # Specifies the base model to fine-tune and the training mode.
32
+ model:
33
+ # Path to the LTX-2 model checkpoint (.safetensors file)
34
+ # This should be a local path to your downloaded model
35
+ model_path: "path/to/ltx-2-model.safetensors"
36
+
37
+ # Path to the text encoder model directory
38
+ # For LTX-2, this is typically the Gemma-based text encoder
39
+ text_encoder_path: "path/to/gemma-text-encoder"
40
+
41
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
42
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
43
+ training_mode: "lora"
44
+
45
+ # Optional: Path to resume training from a checkpoint
46
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
47
+ load_checkpoint: null
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # LoRA Configuration
51
+ # -----------------------------------------------------------------------------
52
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
53
+ lora:
54
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
55
+ # Typical values: 8, 16, 32, 64. Start with 32 for audio extension LoRA training.
56
+ rank: 32
57
+
58
+ # Alpha scaling factor (usually set equal to rank)
59
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
60
+ alpha: 32
61
+
62
+ # Dropout probability for LoRA layers (0.0 = no dropout)
63
+ # Can help with regularization if overfitting occurs
64
+ dropout: 0.0
65
+
66
+ # Which transformer modules to apply LoRA to
67
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
68
+ #
69
+ # VIDEO MODULES (not used for audio-only modes):
70
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
71
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
72
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
73
+ #
74
+ # AUDIO MODULES:
75
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
76
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
77
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
78
+ #
79
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (not used for audio-only modes):
80
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
81
+ # (Q from video, K/V from audio - allows video to attend to audio features)
82
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
83
+ # (Q from audio, K/V from video - allows audio to attend to video features)
84
+ #
85
+ # For audio-only extension, we explicitly target audio modules.
86
+ # Including audio FFN layers can increase the LoRA's capacity.
87
+ target_modules:
88
+ # Audio self-attention
89
+ - "audio_attn1.to_k"
90
+ - "audio_attn1.to_q"
91
+ - "audio_attn1.to_v"
92
+ - "audio_attn1.to_out.0"
93
+ # Audio cross-attention to text
94
+ - "audio_attn2.to_k"
95
+ - "audio_attn2.to_q"
96
+ - "audio_attn2.to_v"
97
+ - "audio_attn2.to_out.0"
98
+ # Audio feed-forward (often improves transformation quality)
99
+ - "audio_ff.net.0.proj"
100
+ - "audio_ff.net.2"
101
+
102
+ # -----------------------------------------------------------------------------
103
+ # Training Strategy Configuration
104
+ # -----------------------------------------------------------------------------
105
+ # Defines the audio extension training approach using the unified flexible
106
+ # strategy. Prefix conditioning provides the first N audio latent timesteps as
107
+ # clean conditioning, teaching the model to generate temporal continuations.
108
+ training_strategy:
109
+ name: "flexible"
110
+
111
+ # Audio modality configuration (audio-only, no video)
112
+ audio:
113
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
114
+ is_generated: true
115
+ # Directory name (within preprocessed_data_root) containing audio latents
116
+ latents_dir: "audio_latents"
117
+
118
+ # Conditions applied to the audio modality during training
119
+ conditions:
120
+ - type: prefix
121
+ # Number of audio latent timesteps to use as conditioning prefix
122
+ # Each audio latent timestep = 1 patchified token
123
+ temporal_boundary: 8
124
+ # Probability of applying prefix conditioning per training sample
125
+ # At 1.0, all training samples use audio extension mode
126
+ probability: 1.0
127
+
128
+ # -----------------------------------------------------------------------------
129
+ # Optimization Configuration
130
+ # -----------------------------------------------------------------------------
131
+ # Controls the training optimization parameters.
132
+ optimization:
133
+ # Learning rate for the optimizer
134
+ # Typical range for LoRA: 1e-5 to 1e-4
135
+ learning_rate: 2e-4
136
+
137
+ # Total number of training steps
138
+ steps: 3000
139
+
140
+ # Batch size per GPU
141
+ # Reduce if running out of memory
142
+ batch_size: 1
143
+
144
+ # Number of gradient accumulation steps
145
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
146
+ gradient_accumulation_steps: 1
147
+
148
+ # Maximum gradient norm for clipping (helps training stability)
149
+ max_grad_norm: 1.0
150
+
151
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
152
+ optimizer_type: "adamw"
153
+
154
+ # Learning rate scheduler type
155
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
156
+ scheduler_type: "linear"
157
+
158
+ # Additional scheduler parameters (depends on scheduler_type)
159
+ scheduler_params: { }
160
+
161
+ # Enable gradient checkpointing to reduce memory usage
162
+ # Recommended for training with limited GPU memory
163
+ enable_gradient_checkpointing: true
164
+
165
+ # -----------------------------------------------------------------------------
166
+ # Acceleration Configuration
167
+ # -----------------------------------------------------------------------------
168
+ # Hardware acceleration and memory optimization settings.
169
+ acceleration:
170
+ # Mixed precision training mode
171
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
172
+ mixed_precision_mode: "bf16"
173
+
174
+ # Model quantization for reduced memory usage
175
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
176
+ quantization: null
177
+
178
+ # Load text encoder in 8-bit precision to save memory
179
+ # Useful when GPU memory is limited
180
+ load_text_encoder_in_8bit: false
181
+
182
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
183
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
184
+ # LoRA). No effect under FSDP (sharded state).
185
+ offload_optimizer_during_validation: false
186
+
187
+
188
+ # -----------------------------------------------------------------------------
189
+ # Data Configuration
190
+ # -----------------------------------------------------------------------------
191
+ # Specifies the training data location and loading parameters.
192
+ data:
193
+ # Root directory containing preprocessed training data
194
+ # Should contain: conditions/ and audio_latents/ subdirectories
195
+ preprocessed_data_root: "/path/to/preprocessed/data"
196
+
197
+ # Number of worker processes for data loading
198
+ # Used for parallel data loading to speed up data loading
199
+ num_dataloader_workers: 2
200
+
201
+ # -----------------------------------------------------------------------------
202
+ # Validation Configuration
203
+ # -----------------------------------------------------------------------------
204
+ # Controls validation sampling during training.
205
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
206
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
207
+ validation:
208
+ # Validation samples — each sample describes a self-contained generation request.
209
+ # Each sample includes an audio prefix condition for forward audio extension.
210
+ samples:
211
+ - prompt: >-
212
+ A warm and soothing piano melody with soft ambient textures, reminiscent of a quiet
213
+ evening by the fireplace. Gentle reverb creates a sense of intimate space.
214
+ conditions:
215
+ - type: prefix
216
+ audio: "/path/to/prefix_audio_1.wav"
217
+ # Duration is in seconds; choose a value that covers a comparable audio context
218
+ # to the training temporal_boundary measured in audio latent timesteps.
219
+ duration: 1.0
220
+ - prompt: >-
221
+ Energetic electronic beats with pulsing synthesizer leads and crisp hi-hat patterns.
222
+ Deep bass tones provide a driving rhythm underneath bright melodic arpeggios.
223
+ conditions:
224
+ - type: prefix
225
+ audio: "/path/to/prefix_audio_2.wav"
226
+ # Duration is in seconds; choose a value that covers a comparable audio context
227
+ # to the training temporal_boundary measured in audio latent timesteps.
228
+ duration: 1.0
229
+
230
+ # Negative prompt to avoid unwanted artifacts
231
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
232
+
233
+ # Generation length control [width, height, frames]
234
+ # With generate_video=false, width/height are unused; frames and frame_rate set audio duration.
235
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
236
+ video_dims: [ 512, 512, 81 ]
237
+
238
+ # Frame rate for generated videos
239
+ frame_rate: 25.0
240
+
241
+ # Random seed for reproducible validation outputs
242
+ seed: 42
243
+
244
+ # Number of denoising steps for validation inference
245
+ # Higher values = better quality but slower generation
246
+ inference_steps: 30
247
+
248
+ # Generate validation videos every N training steps
249
+ # Set to null to disable validation during training
250
+ interval: 100
251
+
252
+ # Classifier-free guidance scale
253
+ # Higher values = stronger adherence to prompt but may introduce artifacts
254
+ guidance_scale: 4.0
255
+
256
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
257
+ # STG is combined with CFG for better temporal coherence
258
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
259
+ stg_blocks: [29] # Recommended: single block 29
260
+ stg_mode: "stg_av" # "stg_av" skips both audio and video self-attention (suited to audio validation)
261
+
262
+ # Whether to generate audio in validation samples
263
+ # Enabled because this audio-only config generates audio
264
+ generate_audio: true
265
+
266
+ # Whether to generate video in validation samples
267
+ # Disabled because no video modality is configured
268
+ generate_video: false
269
+
270
+ # Skip validation at the beginning of training (step 0)
271
+ skip_initial_validation: false
272
+
273
+ # -----------------------------------------------------------------------------
274
+ # Checkpoint Configuration
275
+ # -----------------------------------------------------------------------------
276
+ # Controls model checkpoint saving during training.
277
+ checkpoints:
278
+ # Save a checkpoint every N steps
279
+ # Set to null to disable intermediate checkpoints
280
+ interval: 250
281
+
282
+ # Number of most recent checkpoints to keep
283
+ # Set to -1 to keep all checkpoints
284
+ keep_last_n: 3
285
+
286
+ # Precision to use when saving checkpoint weights
287
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
288
+ precision: "bfloat16"
289
+
290
+ # -----------------------------------------------------------------------------
291
+ # Flow Matching Configuration
292
+ # -----------------------------------------------------------------------------
293
+ # Parameters for the flow matching training objective.
294
+ flow_matching:
295
+ # Timestep sampling mode
296
+ # "shifted_logit_normal" is recommended for LTX-2 models
297
+ timestep_sampling_mode: "shifted_logit_normal"
298
+
299
+ # Additional parameters for timestep sampling
300
+ timestep_sampling_params: { }
301
+
302
+ # -----------------------------------------------------------------------------
303
+ # Hugging Face Hub Configuration
304
+ # -----------------------------------------------------------------------------
305
+ # Settings for uploading trained models to the Hugging Face Hub.
306
+ hub:
307
+ # Whether to push the trained model to the Hub
308
+ push_to_hub: false
309
+
310
+ # Repository ID on Hugging Face Hub (e.g., "username/my-ic-lora-model")
311
+ # Required if push_to_hub is true
312
+ hub_model_id: null
313
+
314
+ # -----------------------------------------------------------------------------
315
+ # Weights & Biases Configuration
316
+ # -----------------------------------------------------------------------------
317
+ # Settings for experiment tracking with W&B.
318
+ wandb:
319
+ # Enable W&B logging
320
+ enabled: false
321
+
322
+ # W&B project name
323
+ project: "ltx-2-trainer"
324
+
325
+ # W&B username or team (null uses default account)
326
+ entity: null
327
+
328
+ # Tags to help organize runs
329
+ tags: [ "ltx2", "lora", "audio-extension", "audio-only" ]
330
+
331
+ # Log validation media (video/audio) to W&B
332
+ log_validation_videos: true
333
+
334
+ # -----------------------------------------------------------------------------
335
+ # General Configuration
336
+ # -----------------------------------------------------------------------------
337
+ # Global settings for the training run.
338
+
339
+ # Random seed for reproducibility
340
+ seed: 42
341
+
342
+ # Directory to save outputs (checkpoints, validation videos, logs)
343
+ output_dir: "outputs/audio_extend_lora"
packages/ltx-trainer/configs/audio_inpainting_lora.yaml ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Audio Inpainting LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # audio inpainting. The model learns to fill in masked regions of audio
7
+ # using per-sample binary masks that define which regions are conditioning
8
+ # (provided clean) and which regions the model must generate.
9
+ #
10
+ # Mask conditioning works by loading per-sample binary masks from disk.
11
+ # Masked regions receive clean latents (no noise, timestep=0) and are excluded
12
+ # from the loss. Unmasked regions are noised and trained normally.
13
+ #
14
+ # This is an audio-only training mode — no video modality is configured.
15
+ #
16
+ # Use this configuration when you want to:
17
+ # - Train the model to fill in or replace regions of existing audio
18
+ # - Fine-tune audio inpainting capabilities on custom datasets
19
+ # - Create audio restoration or editing models
20
+ #
21
+ # Dataset structure:
22
+ # preprocessed_data_root/
23
+ # ├── conditions/ # Text embeddings for each sample
24
+ # ├── audio_latents/ # Audio latents (VAE-encoded audio)
25
+ # └── audio_masks/ # Per-sample binary masks defining conditioning regions
26
+ #
27
+ # Dataset metadata columns: audio, audio_mask, caption
28
+ #
29
+ # =============================================================================
30
+
31
+ # -----------------------------------------------------------------------------
32
+ # Model Configuration
33
+ # -----------------------------------------------------------------------------
34
+ # Specifies the base model to fine-tune and the training mode.
35
+ model:
36
+ # Path to the LTX-2 model checkpoint (.safetensors file)
37
+ # This should be a local path to your downloaded model
38
+ model_path: "path/to/ltx-2-model.safetensors"
39
+
40
+ # Path to the text encoder model directory
41
+ # For LTX-2, this is typically the Gemma-based text encoder
42
+ text_encoder_path: "path/to/gemma-text-encoder"
43
+
44
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
45
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
46
+ training_mode: "lora"
47
+
48
+ # Optional: Path to resume training from a checkpoint
49
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
50
+ load_checkpoint: null
51
+
52
+ # -----------------------------------------------------------------------------
53
+ # LoRA Configuration
54
+ # -----------------------------------------------------------------------------
55
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
56
+ lora:
57
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
58
+ # Typical values: 8, 16, 32, 64. Start with 32 for audio inpainting LoRA training.
59
+ rank: 32
60
+
61
+ # Alpha scaling factor (usually set equal to rank)
62
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
63
+ alpha: 32
64
+
65
+ # Dropout probability for LoRA layers (0.0 = no dropout)
66
+ # Can help with regularization if overfitting occurs
67
+ dropout: 0.0
68
+
69
+ # For audio-only inpainting, we explicitly target audio modules.
70
+ # Including audio FFN layers can increase the LoRA's capacity.
71
+ target_modules:
72
+ # Audio self-attention
73
+ - "audio_attn1.to_k"
74
+ - "audio_attn1.to_q"
75
+ - "audio_attn1.to_v"
76
+ - "audio_attn1.to_out.0"
77
+ # Audio cross-attention to text
78
+ - "audio_attn2.to_k"
79
+ - "audio_attn2.to_q"
80
+ - "audio_attn2.to_v"
81
+ - "audio_attn2.to_out.0"
82
+ # Audio feed-forward (often improves transformation quality)
83
+ - "audio_ff.net.0.proj"
84
+ - "audio_ff.net.2"
85
+
86
+ # -----------------------------------------------------------------------------
87
+ # Training Strategy Configuration
88
+ # -----------------------------------------------------------------------------
89
+ # Defines the audio inpainting training approach using the unified flexible
90
+ # strategy. Per-sample binary masks define which audio regions are provided
91
+ # as clean conditioning and which regions the model must learn to generate.
92
+ training_strategy:
93
+ name: "flexible"
94
+
95
+ # Audio modality configuration (audio-only, no video)
96
+ audio:
97
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
98
+ is_generated: true
99
+ # Directory name (within preprocessed_data_root) containing audio latents
100
+ latents_dir: "audio_latents"
101
+
102
+ # Conditions applied to the audio modality during training
103
+ conditions:
104
+ - type: mask
105
+ # Directory name (within preprocessed_data_root) containing binary masks
106
+ # Each mask file corresponds to a training sample and defines the
107
+ # conditioning region (mask=1 means conditioning, mask=0 means generate)
108
+ mask_dir: "audio_masks"
109
+ # Probability of applying mask conditioning per training sample
110
+ # At 1.0, all training samples use inpainting mode
111
+ probability: 1.0
112
+
113
+ # -----------------------------------------------------------------------------
114
+ # Optimization Configuration
115
+ # -----------------------------------------------------------------------------
116
+ # Controls the training optimization parameters.
117
+ optimization:
118
+ # Learning rate for the optimizer
119
+ # Typical range for LoRA: 1e-5 to 1e-4
120
+ learning_rate: 2e-4
121
+
122
+ # Total number of training steps
123
+ steps: 3000
124
+
125
+ # Batch size per GPU
126
+ # Reduce if running out of memory
127
+ batch_size: 1
128
+
129
+ # Number of gradient accumulation steps
130
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
131
+ gradient_accumulation_steps: 1
132
+
133
+ # Maximum gradient norm for clipping (helps training stability)
134
+ max_grad_norm: 1.0
135
+
136
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
137
+ optimizer_type: "adamw"
138
+
139
+ # Learning rate scheduler type
140
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
141
+ scheduler_type: "linear"
142
+
143
+ # Additional scheduler parameters (depends on scheduler_type)
144
+ scheduler_params: { }
145
+
146
+ # Enable gradient checkpointing to reduce memory usage
147
+ # Recommended for training with limited GPU memory
148
+ enable_gradient_checkpointing: true
149
+
150
+ # -----------------------------------------------------------------------------
151
+ # Acceleration Configuration
152
+ # -----------------------------------------------------------------------------
153
+ # Hardware acceleration and memory optimization settings.
154
+ acceleration:
155
+ # Mixed precision training mode
156
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
157
+ mixed_precision_mode: "bf16"
158
+
159
+ # Model quantization for reduced memory usage
160
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
161
+ quantization: null
162
+
163
+ # Load text encoder in 8-bit precision to save memory
164
+ # Useful when GPU memory is limited
165
+ load_text_encoder_in_8bit: false
166
+
167
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
168
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
169
+ # LoRA). No effect under FSDP (sharded state).
170
+ offload_optimizer_during_validation: false
171
+
172
+
173
+ # -----------------------------------------------------------------------------
174
+ # Data Configuration
175
+ # -----------------------------------------------------------------------------
176
+ # Specifies the training data location and loading parameters.
177
+ data:
178
+ # Root directory containing preprocessed training data
179
+ # Should contain: conditions/, audio_latents/, and audio_masks/ subdirectories
180
+ preprocessed_data_root: "/path/to/preprocessed/data"
181
+
182
+ # Number of worker processes for data loading
183
+ # Used for parallel data loading to speed up data loading
184
+ num_dataloader_workers: 2
185
+
186
+ # -----------------------------------------------------------------------------
187
+ # Validation Configuration
188
+ # -----------------------------------------------------------------------------
189
+ # Controls validation sampling during training.
190
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
191
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
192
+ validation:
193
+ # Validation samples — each sample describes a self-contained generation request.
194
+ # For audio inpainting, each sample includes mask conditioning (audio + mask paths).
195
+ samples:
196
+ - prompt: >-
197
+ A warm and soothing piano melody with soft ambient textures, reminiscent of a quiet
198
+ evening by the fireplace. Gentle reverb creates a sense of intimate space.
199
+ conditions:
200
+ - type: mask
201
+ audio: "/path/to/inpainting_audio_1.wav"
202
+ mask: "/path/to/inpainting_mask_1.pt"
203
+ - prompt: >-
204
+ Energetic electronic beats with pulsing synthesizer leads and crisp hi-hat patterns.
205
+ Deep bass tones provide a driving rhythm underneath bright melodic arpeggios.
206
+ conditions:
207
+ - type: mask
208
+ audio: "/path/to/inpainting_audio_2.wav"
209
+ mask: "/path/to/inpainting_mask_2.pt"
210
+
211
+ # Negative prompt to avoid unwanted artifacts
212
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
213
+
214
+ # Generation length control [width, height, frames]
215
+ # With generate_video=false, width/height are unused; frames and frame_rate set audio duration.
216
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
217
+ video_dims: [ 512, 512, 81 ]
218
+
219
+ # Frame rate for generated videos
220
+ frame_rate: 25.0
221
+
222
+ # Random seed for reproducible validation outputs
223
+ seed: 42
224
+
225
+ # Number of denoising steps for validation inference
226
+ # Higher values = better quality but slower generation
227
+ inference_steps: 30
228
+
229
+ # Generate validation videos every N training steps
230
+ # Set to null to disable validation during training
231
+ interval: 100
232
+
233
+ # Classifier-free guidance scale
234
+ # Higher values = stronger adherence to prompt but may introduce artifacts
235
+ guidance_scale: 4.0
236
+
237
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
238
+ # STG is combined with CFG for better temporal coherence
239
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
240
+ stg_blocks: [29] # Recommended: single block 29
241
+ stg_mode: "stg_av" # "stg_av" skips both audio and video self-attention (suited to audio validation)
242
+
243
+ # Whether to generate audio in validation samples
244
+ # Enabled because this audio-only config generates audio
245
+ generate_audio: true
246
+
247
+ # Whether to generate video in validation samples
248
+ # Disabled because no video modality is configured
249
+ generate_video: false
250
+
251
+ # Skip validation at the beginning of training (step 0)
252
+ skip_initial_validation: false
253
+
254
+ # -----------------------------------------------------------------------------
255
+ # Checkpoint Configuration
256
+ # -----------------------------------------------------------------------------
257
+ # Controls model checkpoint saving during training.
258
+ checkpoints:
259
+ # Save a checkpoint every N steps
260
+ # Set to null to disable intermediate checkpoints
261
+ interval: 250
262
+
263
+ # Number of most recent checkpoints to keep
264
+ # Set to -1 to keep all checkpoints
265
+ keep_last_n: 3
266
+
267
+ # Precision to use when saving checkpoint weights
268
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
269
+ precision: "bfloat16"
270
+
271
+ # -----------------------------------------------------------------------------
272
+ # Flow Matching Configuration
273
+ # -----------------------------------------------------------------------------
274
+ # Parameters for the flow matching training objective.
275
+ flow_matching:
276
+ # Timestep sampling mode
277
+ # "shifted_logit_normal" is recommended for LTX-2 models
278
+ timestep_sampling_mode: "shifted_logit_normal"
279
+
280
+ # Additional parameters for timestep sampling
281
+ timestep_sampling_params: { }
282
+
283
+ # -----------------------------------------------------------------------------
284
+ # Hugging Face Hub Configuration
285
+ # -----------------------------------------------------------------------------
286
+ # Settings for uploading trained models to the Hugging Face Hub.
287
+ hub:
288
+ # Whether to push the trained model to the Hub
289
+ push_to_hub: false
290
+
291
+ # Repository ID on Hugging Face Hub (e.g., "username/my-ic-lora-model")
292
+ # Required if push_to_hub is true
293
+ hub_model_id: null
294
+
295
+ # -----------------------------------------------------------------------------
296
+ # Weights & Biases Configuration
297
+ # -----------------------------------------------------------------------------
298
+ # Settings for experiment tracking with W&B.
299
+ wandb:
300
+ # Enable W&B logging
301
+ enabled: false
302
+
303
+ # W&B project name
304
+ project: "ltx-2-trainer"
305
+
306
+ # W&B username or team (null uses default account)
307
+ entity: null
308
+
309
+ # Tags to help organize runs
310
+ tags: [ "ltx2", "lora", "audio-inpainting", "audio-only" ]
311
+
312
+ # Log validation media (video/audio) to W&B
313
+ log_validation_videos: true
314
+
315
+ # -----------------------------------------------------------------------------
316
+ # General Configuration
317
+ # -----------------------------------------------------------------------------
318
+ # Global settings for the training run.
319
+
320
+ # Random seed for reproducibility
321
+ seed: 42
322
+
323
+ # Directory to save outputs (checkpoints, validation videos, logs)
324
+ output_dir: "outputs/audio_inpainting_lora"
packages/ltx-trainer/configs/audio_suffix_lora.yaml ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Audio Extension (Backward) LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # backward audio extension. The model learns to generate audio content leading into existing audio
7
+ # by conditioning on a suffix of existing audio latent timesteps.
8
+ #
9
+ # Suffix conditioning works by providing the last N audio latent timesteps as
10
+ # clean conditioning signals during training — they receive no noise,
11
+ # timestep=0, and are excluded from the loss. The model learns to generate
12
+ # audio that seamlessly leads into the given suffix.
13
+ #
14
+ # This is an audio-only training mode — no video modality is configured.
15
+ #
16
+ # Use this configuration when you want to:
17
+ # - Train the model to generate preceding content for existing audio
18
+ # - Fine-tune audio temporal continuation capabilities
19
+ # - Create seamless audio extension models
20
+ #
21
+ # Dataset structure:
22
+ # preprocessed_data_root/
23
+ # ├── conditions/ # Text embeddings for each sample
24
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
25
+ #
26
+ # =============================================================================
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # Model Configuration
30
+ # -----------------------------------------------------------------------------
31
+ # Specifies the base model to fine-tune and the training mode.
32
+ model:
33
+ # Path to the LTX-2 model checkpoint (.safetensors file)
34
+ # This should be a local path to your downloaded model
35
+ model_path: "path/to/ltx-2-model.safetensors"
36
+
37
+ # Path to the text encoder model directory
38
+ # For LTX-2, this is typically the Gemma-based text encoder
39
+ text_encoder_path: "path/to/gemma-text-encoder"
40
+
41
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
42
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
43
+ training_mode: "lora"
44
+
45
+ # Optional: Path to resume training from a checkpoint
46
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
47
+ load_checkpoint: null
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # LoRA Configuration
51
+ # -----------------------------------------------------------------------------
52
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
53
+ lora:
54
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
55
+ # Typical values: 8, 16, 32, 64. Start with 32 for audio extension LoRA training.
56
+ rank: 32
57
+
58
+ # Alpha scaling factor (usually set equal to rank)
59
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
60
+ alpha: 32
61
+
62
+ # Dropout probability for LoRA layers (0.0 = no dropout)
63
+ # Can help with regularization if overfitting occurs
64
+ dropout: 0.0
65
+
66
+ # Which transformer modules to apply LoRA to
67
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
68
+ #
69
+ # VIDEO MODULES (not used for audio-only modes):
70
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
71
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
72
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
73
+ #
74
+ # AUDIO MODULES:
75
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
76
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
77
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
78
+ #
79
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (not used for audio-only modes):
80
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
81
+ # (Q from video, K/V from audio - allows video to attend to audio features)
82
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
83
+ # (Q from audio, K/V from video - allows audio to attend to video features)
84
+ #
85
+ # For audio-only extension, we explicitly target audio modules.
86
+ # Including audio FFN layers can increase the LoRA's capacity.
87
+ target_modules:
88
+ # Audio self-attention
89
+ - "audio_attn1.to_k"
90
+ - "audio_attn1.to_q"
91
+ - "audio_attn1.to_v"
92
+ - "audio_attn1.to_out.0"
93
+ # Audio cross-attention to text
94
+ - "audio_attn2.to_k"
95
+ - "audio_attn2.to_q"
96
+ - "audio_attn2.to_v"
97
+ - "audio_attn2.to_out.0"
98
+ # Audio feed-forward (often improves transformation quality)
99
+ - "audio_ff.net.0.proj"
100
+ - "audio_ff.net.2"
101
+
102
+ # -----------------------------------------------------------------------------
103
+ # Training Strategy Configuration
104
+ # -----------------------------------------------------------------------------
105
+ # Defines the backward audio extension training approach using the unified flexible
106
+ # strategy. Suffix conditioning provides the last N audio latent timesteps as
107
+ # clean conditioning, teaching the model to generate content leading into existing audio.
108
+ training_strategy:
109
+ name: "flexible"
110
+
111
+ # Audio modality configuration (audio-only, no video)
112
+ audio:
113
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
114
+ is_generated: true
115
+ # Directory name (within preprocessed_data_root) containing audio latents
116
+ latents_dir: "audio_latents"
117
+
118
+ # Conditions applied to the audio modality during training
119
+ conditions:
120
+ - type: suffix
121
+ # Number of audio latent timesteps to use as conditioning suffix
122
+ # Each audio latent timestep = 1 patchified token
123
+ temporal_boundary: 8
124
+ # Probability of applying suffix conditioning per training sample
125
+ # At 1.0, all training samples use audio extension mode
126
+ probability: 1.0
127
+
128
+ # -----------------------------------------------------------------------------
129
+ # Optimization Configuration
130
+ # -----------------------------------------------------------------------------
131
+ # Controls the training optimization parameters.
132
+ optimization:
133
+ # Learning rate for the optimizer
134
+ # Typical range for LoRA: 1e-5 to 1e-4
135
+ learning_rate: 2e-4
136
+
137
+ # Total number of training steps
138
+ steps: 3000
139
+
140
+ # Batch size per GPU
141
+ # Reduce if running out of memory
142
+ batch_size: 1
143
+
144
+ # Number of gradient accumulation steps
145
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
146
+ gradient_accumulation_steps: 1
147
+
148
+ # Maximum gradient norm for clipping (helps training stability)
149
+ max_grad_norm: 1.0
150
+
151
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
152
+ optimizer_type: "adamw"
153
+
154
+ # Learning rate scheduler type
155
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
156
+ scheduler_type: "linear"
157
+
158
+ # Additional scheduler parameters (depends on scheduler_type)
159
+ scheduler_params: { }
160
+
161
+ # Enable gradient checkpointing to reduce memory usage
162
+ # Recommended for training with limited GPU memory
163
+ enable_gradient_checkpointing: true
164
+
165
+ # -----------------------------------------------------------------------------
166
+ # Acceleration Configuration
167
+ # -----------------------------------------------------------------------------
168
+ # Hardware acceleration and memory optimization settings.
169
+ acceleration:
170
+ # Mixed precision training mode
171
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
172
+ mixed_precision_mode: "bf16"
173
+
174
+ # Model quantization for reduced memory usage
175
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
176
+ quantization: null
177
+
178
+ # Load text encoder in 8-bit precision to save memory
179
+ # Useful when GPU memory is limited
180
+ load_text_encoder_in_8bit: false
181
+
182
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
183
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
184
+ # LoRA). No effect under FSDP (sharded state).
185
+ offload_optimizer_during_validation: false
186
+
187
+
188
+ # -----------------------------------------------------------------------------
189
+ # Data Configuration
190
+ # -----------------------------------------------------------------------------
191
+ # Specifies the training data location and loading parameters.
192
+ data:
193
+ # Root directory containing preprocessed training data
194
+ # Should contain: conditions/ and audio_latents/ subdirectories
195
+ preprocessed_data_root: "/path/to/preprocessed/data"
196
+
197
+ # Number of worker processes for data loading
198
+ # Used for parallel data loading to speed up data loading
199
+ num_dataloader_workers: 2
200
+
201
+ # -----------------------------------------------------------------------------
202
+ # Validation Configuration
203
+ # -----------------------------------------------------------------------------
204
+ # Controls validation sampling during training.
205
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
206
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
207
+ validation:
208
+ # Validation samples — each sample describes a self-contained generation request.
209
+ # Each sample includes an audio suffix condition for backward audio extension.
210
+ samples:
211
+ - prompt: >-
212
+ A warm and soothing piano melody with soft ambient textures, reminiscent of a quiet
213
+ evening by the fireplace. Gentle reverb creates a sense of intimate space.
214
+ conditions:
215
+ - type: suffix
216
+ audio: "/path/to/suffix_audio_1.wav"
217
+ # Duration is in seconds; choose a value that covers a comparable audio context
218
+ # to the training temporal_boundary measured in audio latent timesteps.
219
+ duration: 1.0
220
+ - prompt: >-
221
+ Energetic electronic beats with pulsing synthesizer leads and crisp hi-hat patterns.
222
+ Deep bass tones provide a driving rhythm underneath bright melodic arpeggios.
223
+ conditions:
224
+ - type: suffix
225
+ audio: "/path/to/suffix_audio_2.wav"
226
+ # Duration is in seconds; choose a value that covers a comparable audio context
227
+ # to the training temporal_boundary measured in audio latent timesteps.
228
+ duration: 1.0
229
+
230
+ # Negative prompt to avoid unwanted artifacts
231
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
232
+
233
+ # Generation length control [width, height, frames]
234
+ # With generate_video=false, width/height are unused; frames and frame_rate set audio duration.
235
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
236
+ video_dims: [ 512, 512, 81 ]
237
+
238
+ # Frame rate for generated videos
239
+ frame_rate: 25.0
240
+
241
+ # Random seed for reproducible validation outputs
242
+ seed: 42
243
+
244
+ # Number of denoising steps for validation inference
245
+ # Higher values = better quality but slower generation
246
+ inference_steps: 30
247
+
248
+ # Generate validation videos every N training steps
249
+ # Set to null to disable validation during training
250
+ interval: 100
251
+
252
+ # Classifier-free guidance scale
253
+ # Higher values = stronger adherence to prompt but may introduce artifacts
254
+ guidance_scale: 4.0
255
+
256
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
257
+ # STG is combined with CFG for better temporal coherence
258
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
259
+ stg_blocks: [29] # Recommended: single block 29
260
+ stg_mode: "stg_av" # "stg_av" skips both audio and video self-attention (suited to audio validation)
261
+
262
+ # Whether to generate audio in validation samples
263
+ # Enabled because this audio-only config generates audio
264
+ generate_audio: true
265
+
266
+ # Whether to generate video in validation samples
267
+ # Disabled because no video modality is configured
268
+ generate_video: false
269
+
270
+ # Skip validation at the beginning of training (step 0)
271
+ skip_initial_validation: false
272
+
273
+ # -----------------------------------------------------------------------------
274
+ # Checkpoint Configuration
275
+ # -----------------------------------------------------------------------------
276
+ # Controls model checkpoint saving during training.
277
+ checkpoints:
278
+ # Save a checkpoint every N steps
279
+ # Set to null to disable intermediate checkpoints
280
+ interval: 250
281
+
282
+ # Number of most recent checkpoints to keep
283
+ # Set to -1 to keep all checkpoints
284
+ keep_last_n: 3
285
+
286
+ # Precision to use when saving checkpoint weights
287
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
288
+ precision: "bfloat16"
289
+
290
+ # -----------------------------------------------------------------------------
291
+ # Flow Matching Configuration
292
+ # -----------------------------------------------------------------------------
293
+ # Parameters for the flow matching training objective.
294
+ flow_matching:
295
+ # Timestep sampling mode
296
+ # "shifted_logit_normal" is recommended for LTX-2 models
297
+ timestep_sampling_mode: "shifted_logit_normal"
298
+
299
+ # Additional parameters for timestep sampling
300
+ timestep_sampling_params: { }
301
+
302
+ # -----------------------------------------------------------------------------
303
+ # Hugging Face Hub Configuration
304
+ # -----------------------------------------------------------------------------
305
+ # Settings for uploading trained models to the Hugging Face Hub.
306
+ hub:
307
+ # Whether to push the trained model to the Hub
308
+ push_to_hub: false
309
+
310
+ # Repository ID on Hugging Face Hub (e.g., "username/my-ic-lora-model")
311
+ # Required if push_to_hub is true
312
+ hub_model_id: null
313
+
314
+ # -----------------------------------------------------------------------------
315
+ # Weights & Biases Configuration
316
+ # -----------------------------------------------------------------------------
317
+ # Settings for experiment tracking with W&B.
318
+ wandb:
319
+ # Enable W&B logging
320
+ enabled: false
321
+
322
+ # W&B project name
323
+ project: "ltx-2-trainer"
324
+
325
+ # W&B username or team (null uses default account)
326
+ entity: null
327
+
328
+ # Tags to help organize runs
329
+ tags: [ "ltx2", "lora", "audio-suffix", "audio-only" ]
330
+
331
+ # Log validation media (video/audio) to W&B
332
+ log_validation_videos: true
333
+
334
+ # -----------------------------------------------------------------------------
335
+ # General Configuration
336
+ # -----------------------------------------------------------------------------
337
+ # Global settings for the training run.
338
+
339
+ # Random seed for reproducibility
340
+ seed: 42
341
+
342
+ # Directory to save outputs (checkpoints, validation videos, logs)
343
+ output_dir: "outputs/audio_suffix_lora"
packages/ltx-trainer/configs/av2av_ic_lora.yaml ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 AV2AV IC-LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training In-Context LoRA (IC-LoRA) adapters that
6
+ # enable joint audio-video-to-audio-video transformations. IC-LoRA learns to
7
+ # apply transformations to both the video and audio modalities simultaneously
8
+ # by conditioning on paired reference video and audio.
9
+ #
10
+ # Both modalities use reference conditioning: pre-encoded reference latents
11
+ # are concatenated to each modality's target sequence. Reference tokens
12
+ # participate in bidirectional self-attention but receive no noise and are
13
+ # excluded from the loss.
14
+ #
15
+ # Key differences from video-only IC-LoRA (v2v_ic_lora.yaml):
16
+ # - Both video AND audio have reference conditions
17
+ # - Requires preprocessed reference latents for BOTH modalities
18
+ # - LoRA targets all modules (video, audio, and cross-modal attention)
19
+ # - Validation uses both video and audio reference conditions
20
+ #
21
+ # Dataset structure:
22
+ # preprocessed_data_root/
23
+ # ├── latents/ # Target video latents
24
+ # ├── audio_latents/ # Target audio latents
25
+ # ├── conditions/ # Text embeddings
26
+ # ├── reference_latents/ # Reference video latents (conditioning input)
27
+ # └── reference_audio_latents/ # Reference audio latents (conditioning input)
28
+ #
29
+ # Dataset metadata columns: video, audio, reference_video, reference_audio, caption
30
+ #
31
+ # =============================================================================
32
+
33
+ # -----------------------------------------------------------------------------
34
+ # Model Configuration
35
+ # -----------------------------------------------------------------------------
36
+ # Specifies the base model to fine-tune and the training mode.
37
+ model:
38
+ # Path to the LTX-2 model checkpoint (.safetensors file)
39
+ # This should be a local path to your downloaded model
40
+ model_path: "path/to/ltx-2-model.safetensors"
41
+
42
+ # Path to the text encoder model directory
43
+ # For LTX-2, this is typically the Gemma-based text encoder
44
+ text_encoder_path: "path/to/gemma-text-encoder"
45
+
46
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
47
+ # IC-LoRA reference conditioning is intended for LoRA adapter training.
48
+ training_mode: "lora"
49
+
50
+ # Optional: Path to resume training from a checkpoint
51
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
52
+ load_checkpoint: null
53
+
54
+ # -----------------------------------------------------------------------------
55
+ # LoRA Configuration
56
+ # -----------------------------------------------------------------------------
57
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
58
+ lora:
59
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
60
+ # Typical values: 8, 16, 32, 64. Start with 16-32 for IC-LoRA.
61
+ rank: 32
62
+
63
+ # Alpha scaling factor (usually set equal to rank)
64
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
65
+ alpha: 32
66
+
67
+ # Dropout probability for LoRA layers (0.0 = no dropout)
68
+ # Can help with regularization if overfitting occurs
69
+ dropout: 0.0
70
+
71
+ # For AV2AV IC-LoRA, we target ALL modules — video, audio, and cross-modal attention.
72
+ # Using short patterns matches all branches simultaneously.
73
+ target_modules:
74
+ # Attention layers (matches video, audio, and cross-modal branches)
75
+ - "to_k"
76
+ - "to_q"
77
+ - "to_v"
78
+ - "to_out.0"
79
+
80
+ # -----------------------------------------------------------------------------
81
+ # Training Strategy Configuration
82
+ # -----------------------------------------------------------------------------
83
+ # Defines the AV2AV IC-LoRA training approach using the unified flexible
84
+ # strategy. Both video and audio modalities have reference conditioning,
85
+ # enabling joint audiovisual transformations.
86
+ training_strategy:
87
+ name: "flexible"
88
+
89
+ # Video modality configuration
90
+ video:
91
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
92
+ is_generated: true
93
+ # Directory name (within preprocessed_data_root) containing target video latents
94
+ latents_dir: "latents"
95
+
96
+ # Conditions applied to the video modality during training
97
+ conditions:
98
+ # Reference conditioning (IC-LoRA): concatenates pre-encoded reference video
99
+ # latents to the target sequence
100
+ - type: reference
101
+ latents_dir: "reference_latents"
102
+ probability: 1.0
103
+
104
+ # Audio modality configuration
105
+ audio:
106
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
107
+ is_generated: true
108
+ # Directory name (within preprocessed_data_root) containing target audio latents
109
+ latents_dir: "audio_latents"
110
+
111
+ # Conditions applied to the audio modality during training
112
+ conditions:
113
+ # Reference conditioning (IC-LoRA): concatenates pre-encoded reference audio
114
+ # latents to the target sequence
115
+ - type: reference
116
+ latents_dir: "reference_audio_latents"
117
+ probability: 1.0
118
+
119
+ # -----------------------------------------------------------------------------
120
+ # Optimization Configuration
121
+ # -----------------------------------------------------------------------------
122
+ # Controls the training optimization parameters.
123
+ optimization:
124
+ # Learning rate for the optimizer
125
+ # Typical range for LoRA: 1e-5 to 1e-4
126
+ learning_rate: 2e-4
127
+
128
+ # Total number of training steps
129
+ steps: 3000
130
+
131
+ # Batch size per GPU
132
+ # Reduce if running out of memory
133
+ batch_size: 1
134
+
135
+ # Number of gradient accumulation steps
136
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
137
+ gradient_accumulation_steps: 1
138
+
139
+ # Maximum gradient norm for clipping (helps training stability)
140
+ max_grad_norm: 1.0
141
+
142
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
143
+ optimizer_type: "adamw"
144
+
145
+ # Learning rate scheduler type
146
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
147
+ scheduler_type: "linear"
148
+
149
+ # Additional scheduler parameters (depends on scheduler_type)
150
+ scheduler_params: { }
151
+
152
+ # Enable gradient checkpointing to reduce memory usage
153
+ # Recommended for training with limited GPU memory
154
+ enable_gradient_checkpointing: true
155
+
156
+ # -----------------------------------------------------------------------------
157
+ # Acceleration Configuration
158
+ # -----------------------------------------------------------------------------
159
+ # Hardware acceleration and memory optimization settings.
160
+ acceleration:
161
+ # Mixed precision training mode
162
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
163
+ mixed_precision_mode: "bf16"
164
+
165
+ # Model quantization for reduced memory usage
166
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
167
+ quantization: null
168
+
169
+ # Load text encoder in 8-bit precision to save memory
170
+ # Useful when GPU memory is limited
171
+ load_text_encoder_in_8bit: false
172
+
173
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
174
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
175
+ # LoRA). No effect under FSDP (sharded state).
176
+ offload_optimizer_during_validation: false
177
+
178
+
179
+ # -----------------------------------------------------------------------------
180
+ # Data Configuration
181
+ # -----------------------------------------------------------------------------
182
+ # Specifies the training data location and loading parameters.
183
+ data:
184
+ # Root directory containing preprocessed training data
185
+ # Should contain: latents/, audio_latents/, conditions/, reference_latents/, and reference_audio_latents/
186
+ preprocessed_data_root: "/path/to/preprocessed/data"
187
+
188
+ # Number of worker processes for data loading
189
+ # Used for parallel data loading to speed up data loading
190
+ num_dataloader_workers: 2
191
+
192
+ # -----------------------------------------------------------------------------
193
+ # Validation Configuration
194
+ # -----------------------------------------------------------------------------
195
+ # Controls validation sampling during training.
196
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
197
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
198
+ validation:
199
+ # Validation samples — each sample describes a self-contained generation request.
200
+ # For AV2AV IC-LoRA, each sample includes reference video and reference audio conditions.
201
+ samples:
202
+ - prompt: >-
203
+ A man in a casual blue jacket walks along a winding path through a lush green park on a
204
+ bright sunny afternoon. Tall oak trees line the pathway, their leaves rustling gently in
205
+ the breeze. Dappled sunlight creates shifting patterns on the ground as he strolls at a
206
+ relaxed pace, occasionally looking up at the scenery around him. The audio captures
207
+ footsteps on gravel, birds singing in the trees, distant children playing, and the soft
208
+ whisper of wind through the foliage.
209
+ conditions:
210
+ - type: reference
211
+ video: "/path/to/reference_video_1.mp4"
212
+ downscale_factor: 1
213
+ temporal_scale_factor: 1
214
+ include_in_output: true
215
+ - type: reference
216
+ audio: "/path/to/reference_audio_1.wav"
217
+ - prompt: >-
218
+ A fluffy orange tabby cat sits perfectly still on a wooden windowsill, its green eyes
219
+ intently tracking small birds hopping on a branch just outside the glass. The cat's ears
220
+ twitch and rotate, following every movement. Warm afternoon light illuminates its fur,
221
+ creating a soft golden glow. Behind the cat, a cozy living room with a bookshelf and
222
+ houseplants is visible. The audio features gentle purring, occasional soft meows, muffled
223
+ bird chirps through the window, and quiet ambient room sounds.
224
+ conditions:
225
+ - type: reference
226
+ video: "/path/to/reference_video_2.mp4"
227
+ downscale_factor: 1
228
+ temporal_scale_factor: 1
229
+ include_in_output: true
230
+ - type: reference
231
+ audio: "/path/to/reference_audio_2.wav"
232
+
233
+ # Negative prompt to avoid unwanted artifacts
234
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
235
+
236
+ # Output video dimensions [width, height, frames]
237
+ # Width and height must be divisible by 32
238
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
239
+ video_dims: [ 512, 512, 81 ]
240
+
241
+ # Frame rate for generated videos
242
+ frame_rate: 25.0
243
+
244
+ # Random seed for reproducible validation outputs
245
+ seed: 42
246
+
247
+ # Number of denoising steps for validation inference
248
+ # Higher values = better quality but slower generation
249
+ inference_steps: 30
250
+
251
+ # Generate validation videos every N training steps
252
+ # Set to null to disable validation during training
253
+ interval: 100
254
+
255
+ # Classifier-free guidance scale
256
+ # Higher values = stronger adherence to prompt but may introduce artifacts
257
+ guidance_scale: 4.0
258
+
259
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
260
+ # STG is combined with CFG for better temporal coherence
261
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
262
+ stg_blocks: [29] # Recommended: single block 29
263
+ stg_mode: "stg_av" # Both video and audio modalities are trained
264
+
265
+ # Whether to generate audio in validation samples
266
+ # Can be enabled even when not training the audio branch
267
+ generate_audio: true
268
+
269
+ # Skip validation at the beginning of training (step 0)
270
+ skip_initial_validation: false
271
+
272
+ # -----------------------------------------------------------------------------
273
+ # Checkpoint Configuration
274
+ # -----------------------------------------------------------------------------
275
+ # Controls model checkpoint saving during training.
276
+ checkpoints:
277
+ # Save a checkpoint every N steps
278
+ # Set to null to disable intermediate checkpoints
279
+ interval: 250
280
+
281
+ # Number of most recent checkpoints to keep
282
+ # Set to -1 to keep all checkpoints
283
+ keep_last_n: 3
284
+
285
+ # Precision to use when saving checkpoint weights
286
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
287
+ precision: "bfloat16"
288
+
289
+ # -----------------------------------------------------------------------------
290
+ # Flow Matching Configuration
291
+ # -----------------------------------------------------------------------------
292
+ # Parameters for the flow matching training objective.
293
+ flow_matching:
294
+ # Timestep sampling mode
295
+ # "shifted_logit_normal" is recommended for LTX-2 models
296
+ timestep_sampling_mode: "shifted_logit_normal"
297
+
298
+ # Additional parameters for timestep sampling
299
+ timestep_sampling_params: { }
300
+
301
+ # -----------------------------------------------------------------------------
302
+ # Hugging Face Hub Configuration
303
+ # -----------------------------------------------------------------------------
304
+ # Settings for uploading trained models to the Hugging Face Hub.
305
+ hub:
306
+ # Whether to push the trained model to the Hub
307
+ push_to_hub: false
308
+
309
+ # Repository ID on Hugging Face Hub (e.g., "username/my-ic-lora-model")
310
+ # Required if push_to_hub is true
311
+ hub_model_id: null
312
+
313
+ # -----------------------------------------------------------------------------
314
+ # Weights & Biases Configuration
315
+ # -----------------------------------------------------------------------------
316
+ # Settings for experiment tracking with W&B.
317
+ wandb:
318
+ # Enable W&B logging
319
+ enabled: false
320
+
321
+ # W&B project name
322
+ project: "ltx-2-trainer"
323
+
324
+ # W&B username or team (null uses default account)
325
+ entity: null
326
+
327
+ # Tags to help organize runs
328
+ tags: [ "ltx2", "ic-lora", "av2av" ]
329
+
330
+ # Log validation media (video/audio) to W&B
331
+ log_validation_videos: true
332
+
333
+ # -----------------------------------------------------------------------------
334
+ # General Configuration
335
+ # -----------------------------------------------------------------------------
336
+ # Global settings for the training run.
337
+
338
+ # Random seed for reproducibility
339
+ seed: 42
340
+
341
+ # Directory to save outputs (checkpoints, validation videos, logs)
342
+ output_dir: "outputs/av2av_ic_lora"
packages/ltx-trainer/configs/i2v_lora.yaml ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Image-to-Video LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # image-to-video generation. The model learns to generate videos conditioned
7
+ # on a starting image (first frame), with optional audio generation.
8
+ #
9
+ # First-frame conditioning works by providing the first frame as a clean
10
+ # conditioning signal during training — it receives no noise, timestep=0,
11
+ # and is excluded from the loss. This teaches the model to animate from
12
+ # a given image.
13
+ #
14
+ # Use this configuration when you want to:
15
+ # - Train the model to generate videos starting from a given image
16
+ # - Fine-tune image-to-video capabilities on custom datasets
17
+ # - Create image animation models with optional audio
18
+ #
19
+ # Dataset structure:
20
+ # preprocessed_data_root/
21
+ # ├── latents/ # Video latents (VAE-encoded videos)
22
+ # ├── conditions/ # Text embeddings for each video
23
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
24
+ #
25
+ # =============================================================================
26
+
27
+ # -----------------------------------------------------------------------------
28
+ # Model Configuration
29
+ # -----------------------------------------------------------------------------
30
+ # Specifies the base model to fine-tune and the training mode.
31
+ model:
32
+ # Path to the LTX-2 model checkpoint (.safetensors file)
33
+ # This should be a local path to your downloaded model
34
+ model_path: "path/to/ltx-2-model.safetensors"
35
+
36
+ # Path to the text encoder model directory
37
+ # For LTX-2, this is typically the Gemma-based text encoder
38
+ text_encoder_path: "path/to/gemma-text-encoder"
39
+
40
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
41
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
42
+ training_mode: "lora"
43
+
44
+ # Optional: Path to resume training from a checkpoint
45
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
46
+ load_checkpoint: null
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # LoRA Configuration
50
+ # -----------------------------------------------------------------------------
51
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
52
+ lora:
53
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
54
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
55
+ rank: 32
56
+
57
+ # Alpha scaling factor (usually set equal to rank)
58
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
59
+ alpha: 32
60
+
61
+ # Dropout probability for LoRA layers (0.0 = no dropout)
62
+ # Can help with regularization if overfitting occurs
63
+ dropout: 0.0
64
+
65
+ # Which transformer modules to apply LoRA to
66
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
67
+ #
68
+ # VIDEO MODULES:
69
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
70
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
71
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
72
+ #
73
+ # AUDIO MODULES:
74
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
75
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
76
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
77
+ #
78
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
79
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
80
+ # (Q from video, K/V from audio - allows video to attend to audio features)
81
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
82
+ # (Q from audio, K/V from video - allows audio to attend to video features)
83
+ #
84
+ # Using short patterns like "to_k" matches ALL attention modules (video, audio, and cross-modal).
85
+ # For audio-video training, this is the recommended approach.
86
+ target_modules:
87
+ # Attention layers (matches both video and audio branches)
88
+ - "to_k"
89
+ - "to_q"
90
+ - "to_v"
91
+ - "to_out.0"
92
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
93
+ # - "ff.net.0.proj"
94
+ # - "ff.net.2"
95
+ # - "audio_ff.net.0.proj"
96
+ # - "audio_ff.net.2"
97
+
98
+ # -----------------------------------------------------------------------------
99
+ # Training Strategy Configuration
100
+ # -----------------------------------------------------------------------------
101
+ # Defines the image-to-video training approach using the unified flexible strategy.
102
+ # First-frame conditioning provides the first frame as clean conditioning signal
103
+ # during training, teaching the model to animate from a given image.
104
+ training_strategy:
105
+ # Strategy name: "flexible" for the unified conditioning framework
106
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
107
+ # modality-specific configuration blocks.
108
+ name: "flexible"
109
+
110
+ # Video modality configuration
111
+ video:
112
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
113
+ is_generated: true
114
+ # Directory name (within preprocessed_data_root) containing video latents
115
+ latents_dir: "latents"
116
+
117
+ # Conditions applied to the video modality during training
118
+ # First-frame conditioning: the first frame of each video is provided as a clean
119
+ # conditioning signal (no noise, timestep=0, excluded from loss)
120
+ conditions:
121
+ - type: first_frame
122
+ # Probability of applying first-frame conditioning per training sample
123
+ # At 0.5, half the training samples use I2V mode, half use pure T2V
124
+ # Higher values improve I2V quality but may reduce T2V diversity
125
+ probability: 0.5
126
+
127
+ # Audio modality configuration
128
+ audio:
129
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
130
+ is_generated: true
131
+ # Directory name (within preprocessed_data_root) containing audio latents
132
+ latents_dir: "audio_latents"
133
+
134
+ # -----------------------------------------------------------------------------
135
+ # Optimization Configuration
136
+ # -----------------------------------------------------------------------------
137
+ # Controls the training optimization parameters.
138
+ optimization:
139
+ # Learning rate for the optimizer
140
+ # Typical range for LoRA: 1e-5 to 1e-4
141
+ learning_rate: 1e-4
142
+
143
+ # Total number of training steps
144
+ steps: 2000
145
+
146
+ # Batch size per GPU
147
+ # Reduce if running out of memory
148
+ batch_size: 1
149
+
150
+ # Number of gradient accumulation steps
151
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
152
+ gradient_accumulation_steps: 1
153
+
154
+ # Maximum gradient norm for clipping (helps training stability)
155
+ max_grad_norm: 1.0
156
+
157
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
158
+ optimizer_type: "adamw"
159
+
160
+ # Learning rate scheduler type
161
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
162
+ scheduler_type: "linear"
163
+
164
+ # Additional scheduler parameters (depends on scheduler_type)
165
+ scheduler_params: { }
166
+
167
+ # Enable gradient checkpointing to reduce memory usage
168
+ # Recommended for training with limited GPU memory
169
+ enable_gradient_checkpointing: true
170
+
171
+ # -----------------------------------------------------------------------------
172
+ # Acceleration Configuration
173
+ # -----------------------------------------------------------------------------
174
+ # Hardware acceleration and memory optimization settings.
175
+ acceleration:
176
+ # Mixed precision training mode
177
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
178
+ mixed_precision_mode: "bf16"
179
+
180
+ # Model quantization for reduced memory usage
181
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
182
+ quantization: null
183
+
184
+ # Load text encoder in 8-bit precision to save memory
185
+ # Useful when GPU memory is limited
186
+ load_text_encoder_in_8bit: false
187
+
188
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
189
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
190
+ # LoRA). No effect under FSDP (sharded state).
191
+ offload_optimizer_during_validation: false
192
+
193
+
194
+ # -----------------------------------------------------------------------------
195
+ # Data Configuration
196
+ # -----------------------------------------------------------------------------
197
+ # Specifies the training data location and loading parameters.
198
+ data:
199
+ # Root directory containing preprocessed training data
200
+ # Should contain: latents/, conditions/, and audio_latents/
201
+ preprocessed_data_root: "/path/to/preprocessed/data"
202
+
203
+ # Number of worker processes for data loading
204
+ # Used for parallel data loading to speed up data loading
205
+ num_dataloader_workers: 2
206
+
207
+ # -----------------------------------------------------------------------------
208
+ # Validation Configuration
209
+ # -----------------------------------------------------------------------------
210
+ # Controls validation sampling during training.
211
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
212
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
213
+ validation:
214
+ # Validation samples — each sample describes a self-contained generation request.
215
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
216
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
217
+ samples:
218
+ - prompt: >-
219
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
220
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
221
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
222
+ ceramic mug, then continues working with focused concentration. The audio captures the
223
+ gentle clicking of keyboard keys, the soft rustle of papers, and ambient room tone with
224
+ occasional distant bird chirps from outside.
225
+ conditions:
226
+ - type: first_frame
227
+ image_or_video: "/path/to/conditioning_image_1.png"
228
+ - prompt: >-
229
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
230
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
231
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
232
+ various pots simmer on the stove behind him. The audio features the sizzling of pans,
233
+ the clinking of utensils against plates, and the ambient hum of kitchen ventilation.
234
+ conditions:
235
+ - type: first_frame
236
+ image_or_video: "/path/to/conditioning_image_2.png"
237
+
238
+ # Negative prompt to avoid unwanted artifacts
239
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
240
+
241
+ # Output video dimensions [width, height, frames]
242
+ # Width and height must be divisible by 32
243
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
244
+ video_dims: [ 576, 576, 89 ]
245
+
246
+ # Frame rate for generated videos
247
+ frame_rate: 25.0
248
+
249
+ # Random seed for reproducible validation outputs
250
+ seed: 42
251
+
252
+ # Number of denoising steps for validation inference
253
+ # Higher values = better quality but slower generation
254
+ inference_steps: 30
255
+
256
+ # Generate validation videos every N training steps
257
+ # Set to null to disable validation during training
258
+ interval: 100
259
+
260
+ # Classifier-free guidance scale
261
+ # Higher values = stronger adherence to prompt but may introduce artifacts
262
+ guidance_scale: 4.0
263
+
264
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
265
+ # STG is combined with CFG for better temporal coherence
266
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
267
+ stg_blocks: [29] # Recommended: single block 29
268
+ stg_mode: "stg_av" # "stg_av" perturbs both audio and video, "stg_v" video only
269
+
270
+ # Whether to generate audio in validation samples
271
+ # Independent of training_strategy.audio.is_generated - you can generate audio
272
+ # in validation even when not training the audio branch
273
+ generate_audio: true
274
+
275
+ # Skip validation at the beginning of training (step 0)
276
+ skip_initial_validation: false
277
+
278
+ # -----------------------------------------------------------------------------
279
+ # Checkpoint Configuration
280
+ # -----------------------------------------------------------------------------
281
+ # Controls model checkpoint saving during training.
282
+ checkpoints:
283
+ # Save a checkpoint every N steps
284
+ # Set to null to disable intermediate checkpoints
285
+ interval: 250
286
+
287
+ # Number of most recent checkpoints to keep
288
+ # Set to -1 to keep all checkpoints
289
+ keep_last_n: -1
290
+
291
+ # Precision to use when saving checkpoint weights
292
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
293
+ precision: "bfloat16"
294
+
295
+ # -----------------------------------------------------------------------------
296
+ # Flow Matching Configuration
297
+ # -----------------------------------------------------------------------------
298
+ # Parameters for the flow matching training objective.
299
+ flow_matching:
300
+ # Timestep sampling mode
301
+ # "shifted_logit_normal" is recommended for LTX-2 models
302
+ timestep_sampling_mode: "shifted_logit_normal"
303
+
304
+ # Additional parameters for timestep sampling
305
+ timestep_sampling_params: { }
306
+
307
+ # -----------------------------------------------------------------------------
308
+ # Hugging Face Hub Configuration
309
+ # -----------------------------------------------------------------------------
310
+ # Settings for uploading trained models to the Hugging Face Hub.
311
+ hub:
312
+ # Whether to push the trained model to the Hub
313
+ push_to_hub: false
314
+
315
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
316
+ # Required if push_to_hub is true
317
+ hub_model_id: null
318
+
319
+ # -----------------------------------------------------------------------------
320
+ # Weights & Biases Configuration
321
+ # -----------------------------------------------------------------------------
322
+ # Settings for experiment tracking with W&B.
323
+ wandb:
324
+ # Enable W&B logging
325
+ enabled: false
326
+
327
+ # W&B project name
328
+ project: "ltx-2-trainer"
329
+
330
+ # W&B username or team (null uses default account)
331
+ entity: null
332
+
333
+ # Tags to help organize runs
334
+ tags: [ "ltx2", "lora", "i2v" ]
335
+
336
+ # Log validation media (video/audio) to W&B
337
+ log_validation_videos: true
338
+
339
+ # -----------------------------------------------------------------------------
340
+ # General Configuration
341
+ # -----------------------------------------------------------------------------
342
+ # Global settings for the training run.
343
+
344
+ # Random seed for reproducibility
345
+ seed: 42
346
+
347
+ # Directory to save outputs (checkpoints, validation videos, logs)
348
+ output_dir: "outputs/i2v_lora"
packages/ltx-trainer/configs/t2a_lora.yaml ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Text-to-Audio LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # text-to-audio generation. The model learns to generate audio from text
7
+ # prompts without any additional conditioning.
8
+ #
9
+ # This is the simplest audio-only training mode — no reference audio, no
10
+ # video modality. Only the audio branch of the transformer is trained.
11
+ #
12
+ # Use this configuration when you want to:
13
+ # - Fine-tune audio generation for specific sound styles or domains
14
+ # - Train custom audio generation capabilities
15
+ # - Create audio LoRAs that can be combined with video LoRAs
16
+ #
17
+ # Dataset structure:
18
+ # preprocessed_data_root/
19
+ # ├── conditions/ # Text embeddings for each sample
20
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
21
+ #
22
+ # =============================================================================
23
+
24
+ # -----------------------------------------------------------------------------
25
+ # Model Configuration
26
+ # -----------------------------------------------------------------------------
27
+ # Specifies the base model to fine-tune and the training mode.
28
+ model:
29
+ # Path to the LTX-2 model checkpoint (.safetensors file)
30
+ # This should be a local path to your downloaded model
31
+ model_path: "path/to/ltx-2-model.safetensors"
32
+
33
+ # Path to the text encoder model directory
34
+ # For LTX-2, this is typically the Gemma-based text encoder
35
+ text_encoder_path: "path/to/gemma-text-encoder"
36
+
37
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
38
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
39
+ training_mode: "lora"
40
+
41
+ # Optional: Path to resume training from a checkpoint
42
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
43
+ load_checkpoint: null
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # LoRA Configuration
47
+ # -----------------------------------------------------------------------------
48
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
49
+ lora:
50
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
51
+ # Typical values: 8, 16, 32, 64. Start with 32 for general audio LoRA training.
52
+ rank: 32
53
+
54
+ # Alpha scaling factor (usually set equal to rank)
55
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
56
+ alpha: 32
57
+
58
+ # Dropout probability for LoRA layers (0.0 = no dropout)
59
+ # Can help with regularization if overfitting occurs
60
+ dropout: 0.0
61
+
62
+ # Which transformer modules to apply LoRA to
63
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
64
+ #
65
+ # VIDEO MODULES (not used for audio-only modes):
66
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
67
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
68
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
69
+ #
70
+ # AUDIO MODULES:
71
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
72
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
73
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
74
+ #
75
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (not used for audio-only modes):
76
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
77
+ # (Q from video, K/V from audio - allows video to attend to audio features)
78
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
79
+ # (Q from audio, K/V from video - allows audio to attend to video features)
80
+ #
81
+ # For audio-only training, we explicitly target audio modules.
82
+ # Including audio FFN layers can increase the LoRA's capacity.
83
+ target_modules:
84
+ # Audio self-attention
85
+ - "audio_attn1.to_k"
86
+ - "audio_attn1.to_q"
87
+ - "audio_attn1.to_v"
88
+ - "audio_attn1.to_out.0"
89
+ # Audio cross-attention to text
90
+ - "audio_attn2.to_k"
91
+ - "audio_attn2.to_q"
92
+ - "audio_attn2.to_v"
93
+ - "audio_attn2.to_out.0"
94
+ # Audio feed-forward (often improves transformation quality)
95
+ - "audio_ff.net.0.proj"
96
+ - "audio_ff.net.2"
97
+
98
+ # -----------------------------------------------------------------------------
99
+ # Training Strategy Configuration
100
+ # -----------------------------------------------------------------------------
101
+ # Defines the text-to-audio training approach using the unified flexible
102
+ # strategy. Audio-only training with no additional conditions — the model
103
+ # learns to generate audio purely from text prompts.
104
+ training_strategy:
105
+ name: "flexible"
106
+
107
+ # Audio modality configuration (audio-only, no video)
108
+ audio:
109
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
110
+ is_generated: true
111
+ # Directory name (within preprocessed_data_root) containing audio latents
112
+ latents_dir: "audio_latents"
113
+
114
+ # -----------------------------------------------------------------------------
115
+ # Optimization Configuration
116
+ # -----------------------------------------------------------------------------
117
+ # Controls the training optimization parameters.
118
+ optimization:
119
+ # Learning rate for the optimizer
120
+ # Typical range for LoRA: 1e-5 to 1e-4
121
+ learning_rate: 2e-4
122
+
123
+ # Total number of training steps
124
+ steps: 3000
125
+
126
+ # Batch size per GPU
127
+ # Reduce if running out of memory
128
+ batch_size: 1
129
+
130
+ # Number of gradient accumulation steps
131
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
132
+ gradient_accumulation_steps: 1
133
+
134
+ # Maximum gradient norm for clipping (helps training stability)
135
+ max_grad_norm: 1.0
136
+
137
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
138
+ optimizer_type: "adamw"
139
+
140
+ # Learning rate scheduler type
141
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
142
+ scheduler_type: "linear"
143
+
144
+ # Additional scheduler parameters (depends on scheduler_type)
145
+ scheduler_params: { }
146
+
147
+ # Enable gradient checkpointing to reduce memory usage
148
+ # Recommended for training with limited GPU memory
149
+ enable_gradient_checkpointing: true
150
+
151
+ # -----------------------------------------------------------------------------
152
+ # Acceleration Configuration
153
+ # -----------------------------------------------------------------------------
154
+ # Hardware acceleration and memory optimization settings.
155
+ acceleration:
156
+ # Mixed precision training mode
157
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
158
+ mixed_precision_mode: "bf16"
159
+
160
+ # Model quantization for reduced memory usage
161
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
162
+ quantization: null
163
+
164
+ # Load text encoder in 8-bit precision to save memory
165
+ # Useful when GPU memory is limited
166
+ load_text_encoder_in_8bit: false
167
+
168
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
169
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
170
+ # LoRA). No effect under FSDP (sharded state).
171
+ offload_optimizer_during_validation: false
172
+
173
+
174
+ # -----------------------------------------------------------------------------
175
+ # Data Configuration
176
+ # -----------------------------------------------------------------------------
177
+ # Specifies the training data location and loading parameters.
178
+ data:
179
+ # Root directory containing preprocessed training data
180
+ # Should contain: conditions/ and audio_latents/ subdirectories
181
+ preprocessed_data_root: "/path/to/preprocessed/data"
182
+
183
+ # Number of worker processes for data loading
184
+ # Used for parallel data loading to speed up data loading
185
+ num_dataloader_workers: 2
186
+
187
+ # -----------------------------------------------------------------------------
188
+ # Validation Configuration
189
+ # -----------------------------------------------------------------------------
190
+ # Controls validation sampling during training.
191
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
192
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
193
+ validation:
194
+ # Validation samples — each sample describes a self-contained generation request.
195
+ # Text-to-audio validation samples do not need additional conditions.
196
+ samples:
197
+ - prompt: >-
198
+ A warm and soothing piano melody with soft ambient textures, reminiscent of a quiet
199
+ evening by the fireplace. Gentle reverb creates a sense of intimate space.
200
+ - prompt: >-
201
+ Energetic electronic beats with pulsing synthesizer leads and crisp hi-hat patterns.
202
+ Deep bass tones provide a driving rhythm underneath bright melodic arpeggios.
203
+
204
+ # Negative prompt to avoid unwanted artifacts
205
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
206
+
207
+ # Generation length control [width, height, frames]
208
+ # With generate_video=false, width/height are unused; frames and frame_rate set audio duration.
209
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
210
+ video_dims: [ 512, 512, 81 ]
211
+
212
+ # Frame rate for generated videos
213
+ frame_rate: 25.0
214
+
215
+ # Random seed for reproducible validation outputs
216
+ seed: 42
217
+
218
+ # Number of denoising steps for validation inference
219
+ # Higher values = better quality but slower generation
220
+ inference_steps: 30
221
+
222
+ # Generate validation videos every N training steps
223
+ # Set to null to disable validation during training
224
+ interval: 100
225
+
226
+ # Classifier-free guidance scale
227
+ # Higher values = stronger adherence to prompt but may introduce artifacts
228
+ guidance_scale: 4.0
229
+
230
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
231
+ # STG is combined with CFG for better temporal coherence
232
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
233
+ stg_blocks: [29] # Recommended: single block 29
234
+ stg_mode: "stg_av" # "stg_av" skips both audio and video self-attention (suited to audio validation)
235
+
236
+ # Whether to generate audio in validation samples
237
+ # Enabled because this audio-only config generates audio
238
+ generate_audio: true
239
+
240
+ # Whether to generate video in validation samples
241
+ # Disabled because no video modality is configured
242
+ generate_video: false
243
+
244
+ # Skip validation at the beginning of training (step 0)
245
+ skip_initial_validation: false
246
+
247
+ # -----------------------------------------------------------------------------
248
+ # Checkpoint Configuration
249
+ # -----------------------------------------------------------------------------
250
+ # Controls model checkpoint saving during training.
251
+ checkpoints:
252
+ # Save a checkpoint every N steps
253
+ # Set to null to disable intermediate checkpoints
254
+ interval: 250
255
+
256
+ # Number of most recent checkpoints to keep
257
+ # Set to -1 to keep all checkpoints
258
+ keep_last_n: 3
259
+
260
+ # Precision to use when saving checkpoint weights
261
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
262
+ precision: "bfloat16"
263
+
264
+ # -----------------------------------------------------------------------------
265
+ # Flow Matching Configuration
266
+ # -----------------------------------------------------------------------------
267
+ # Parameters for the flow matching training objective.
268
+ flow_matching:
269
+ # Timestep sampling mode
270
+ # "shifted_logit_normal" is recommended for LTX-2 models
271
+ timestep_sampling_mode: "shifted_logit_normal"
272
+
273
+ # Additional parameters for timestep sampling
274
+ timestep_sampling_params: { }
275
+
276
+ # -----------------------------------------------------------------------------
277
+ # Hugging Face Hub Configuration
278
+ # -----------------------------------------------------------------------------
279
+ # Settings for uploading trained models to the Hugging Face Hub.
280
+ hub:
281
+ # Whether to push the trained model to the Hub
282
+ push_to_hub: false
283
+
284
+ # Repository ID on Hugging Face Hub (e.g., "username/my-ic-lora-model")
285
+ # Required if push_to_hub is true
286
+ hub_model_id: null
287
+
288
+ # -----------------------------------------------------------------------------
289
+ # Weights & Biases Configuration
290
+ # -----------------------------------------------------------------------------
291
+ # Settings for experiment tracking with W&B.
292
+ wandb:
293
+ # Enable W&B logging
294
+ enabled: false
295
+
296
+ # W&B project name
297
+ project: "ltx-2-trainer"
298
+
299
+ # W&B username or team (null uses default account)
300
+ entity: null
301
+
302
+ # Tags to help organize runs
303
+ tags: [ "ltx2", "lora", "t2a", "audio-only" ]
304
+
305
+ # Log validation media (video/audio) to W&B
306
+ log_validation_videos: true
307
+
308
+ # -----------------------------------------------------------------------------
309
+ # General Configuration
310
+ # -----------------------------------------------------------------------------
311
+ # Global settings for the training run.
312
+
313
+ # Random seed for reproducibility
314
+ seed: 42
315
+
316
+ # Directory to save outputs (checkpoints, validation videos, logs)
317
+ output_dir: "outputs/t2a_lora"
packages/ltx-trainer/configs/t2v_lora.yaml ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Text-to-Video LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # text-to-video generation with joint audio-video support.
7
+ #
8
+ # Use this configuration when you want to:
9
+ # - Fine-tune LTX-2 on your own video dataset
10
+ # - Train joint audio-video generation from text prompts
11
+ # - Create custom video generation styles or audiovisual concepts
12
+ #
13
+ # Dataset structure:
14
+ # preprocessed_data_root/
15
+ # ├── latents/ # Video latents (VAE-encoded videos)
16
+ # ├── conditions/ # Text embeddings for each video
17
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
18
+ #
19
+ # =============================================================================
20
+
21
+ # -----------------------------------------------------------------------------
22
+ # Model Configuration
23
+ # -----------------------------------------------------------------------------
24
+ # Specifies the base model to fine-tune and the training mode.
25
+ model:
26
+ # Path to the LTX-2 model checkpoint (.safetensors file)
27
+ # This should be a local path to your downloaded model
28
+ model_path: "path/to/ltx-2-model.safetensors"
29
+
30
+ # Path to the text encoder model directory
31
+ # For LTX-2, this is typically the Gemma-based text encoder
32
+ text_encoder_path: "path/to/gemma-text-encoder"
33
+
34
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
35
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
36
+ training_mode: "lora"
37
+
38
+ # Optional: Path to resume training from a checkpoint
39
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
40
+ load_checkpoint: null
41
+
42
+ # -----------------------------------------------------------------------------
43
+ # LoRA Configuration
44
+ # -----------------------------------------------------------------------------
45
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
46
+ lora:
47
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
48
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
49
+ rank: 32
50
+
51
+ # Alpha scaling factor (usually set equal to rank)
52
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
53
+ alpha: 32
54
+
55
+ # Dropout probability for LoRA layers (0.0 = no dropout)
56
+ # Can help with regularization if overfitting occurs
57
+ dropout: 0.0
58
+
59
+ # Which transformer modules to apply LoRA to
60
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
61
+ #
62
+ # VIDEO MODULES:
63
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
64
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
65
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
66
+ #
67
+ # AUDIO MODULES:
68
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
69
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
70
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
71
+ #
72
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
73
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
74
+ # (Q from video, K/V from audio - allows video to attend to audio features)
75
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
76
+ # (Q from audio, K/V from video - allows audio to attend to video features)
77
+ #
78
+ # Using short patterns like "to_k" matches ALL attention modules (video, audio, and cross-modal).
79
+ # For audio-video training, this is the recommended approach.
80
+ target_modules:
81
+ # Attention layers (matches both video and audio branches)
82
+ - "to_k"
83
+ - "to_q"
84
+ - "to_v"
85
+ - "to_out.0"
86
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
87
+ # - "ff.net.0.proj"
88
+ # - "ff.net.2"
89
+ # - "audio_ff.net.0.proj"
90
+ # - "audio_ff.net.2"
91
+
92
+ # -----------------------------------------------------------------------------
93
+ # Training Strategy Configuration
94
+ # -----------------------------------------------------------------------------
95
+ # Defines the training approach using the unified flexible strategy.
96
+ # This configuration trains both video and audio generation from text prompts.
97
+ training_strategy:
98
+ # Strategy name: "flexible" for the unified conditioning framework
99
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
100
+ # modality-specific configuration blocks.
101
+ name: "flexible"
102
+
103
+ # Video modality configuration
104
+ # When is_generated is true, the model learns to generate (denoise) video
105
+ video:
106
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
107
+ is_generated: true
108
+ # Directory name (within preprocessed_data_root) containing video latents
109
+ latents_dir: "latents"
110
+
111
+ # Audio modality configuration
112
+ # When is_generated is true, the model learns to generate (denoise) audio
113
+ audio:
114
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
115
+ is_generated: true
116
+ # Directory name (within preprocessed_data_root) containing audio latents
117
+ latents_dir: "audio_latents"
118
+
119
+ # -----------------------------------------------------------------------------
120
+ # Optimization Configuration
121
+ # -----------------------------------------------------------------------------
122
+ # Controls the training optimization parameters.
123
+ optimization:
124
+ # Learning rate for the optimizer
125
+ # Typical range for LoRA: 1e-5 to 1e-4
126
+ learning_rate: 1e-4
127
+
128
+ # Total number of training steps
129
+ steps: 2000
130
+
131
+ # Batch size per GPU
132
+ # Reduce if running out of memory
133
+ batch_size: 1
134
+
135
+ # Number of gradient accumulation steps
136
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
137
+ gradient_accumulation_steps: 1
138
+
139
+ # Maximum gradient norm for clipping (helps training stability)
140
+ max_grad_norm: 1.0
141
+
142
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
143
+ optimizer_type: "adamw"
144
+
145
+ # Learning rate scheduler type
146
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
147
+ scheduler_type: "linear"
148
+
149
+ # Additional scheduler parameters (depends on scheduler_type)
150
+ scheduler_params: { }
151
+
152
+ # Enable gradient checkpointing to reduce memory usage
153
+ # Recommended for training with limited GPU memory
154
+ enable_gradient_checkpointing: true
155
+
156
+ # -----------------------------------------------------------------------------
157
+ # Acceleration Configuration
158
+ # -----------------------------------------------------------------------------
159
+ # Hardware acceleration and memory optimization settings.
160
+ acceleration:
161
+ # Mixed precision training mode
162
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
163
+ mixed_precision_mode: "bf16"
164
+
165
+ # Model quantization for reduced memory usage
166
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
167
+ quantization: null
168
+
169
+ # Load text encoder in 8-bit precision to save memory
170
+ # Useful when GPU memory is limited
171
+ load_text_encoder_in_8bit: false
172
+
173
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
174
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
175
+ # LoRA). No effect under FSDP (sharded state).
176
+ offload_optimizer_during_validation: false
177
+
178
+
179
+ # -----------------------------------------------------------------------------
180
+ # Data Configuration
181
+ # -----------------------------------------------------------------------------
182
+ # Specifies the training data location and loading parameters.
183
+ data:
184
+ # Root directory containing preprocessed training data
185
+ # Should contain: latents/, conditions/, and audio_latents/
186
+ preprocessed_data_root: "/path/to/preprocessed/data"
187
+
188
+ # Number of worker processes for data loading
189
+ # Used for parallel data loading to speed up data loading
190
+ num_dataloader_workers: 2
191
+
192
+ # -----------------------------------------------------------------------------
193
+ # Validation Configuration
194
+ # -----------------------------------------------------------------------------
195
+ # Controls validation sampling during training.
196
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
197
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
198
+ validation:
199
+ # Validation samples — each sample describes a self-contained generation request.
200
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
201
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
202
+ samples:
203
+ - prompt: >-
204
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
205
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
206
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
207
+ ceramic mug, then continues working with focused concentration. The audio captures the
208
+ gentle clicking of keyboard keys, the soft rustle of papers, and ambient room tone with
209
+ occasional distant bird chirps from outside.
210
+ - prompt: >-
211
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
212
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
213
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
214
+ various pots simmer on the stove behind him. The audio features the sizzling of pans,
215
+ the clinking of utensils against plates, and the ambient hum of kitchen ventilation.
216
+
217
+ # Negative prompt to avoid unwanted artifacts
218
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
219
+
220
+ # Output video dimensions [width, height, frames]
221
+ # Width and height must be divisible by 32
222
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
223
+ video_dims: [ 576, 576, 89 ]
224
+
225
+ # Frame rate for generated videos
226
+ frame_rate: 25.0
227
+
228
+ # Random seed for reproducible validation outputs
229
+ seed: 42
230
+
231
+ # Number of denoising steps for validation inference
232
+ # Higher values = better quality but slower generation
233
+ inference_steps: 30
234
+
235
+ # Generate validation videos every N training steps
236
+ # Set to null to disable validation during training
237
+ interval: 100
238
+
239
+ # Classifier-free guidance scale
240
+ # Higher values = stronger adherence to prompt but may introduce artifacts
241
+ guidance_scale: 4.0
242
+
243
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
244
+ # STG is combined with CFG for better temporal coherence
245
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
246
+ stg_blocks: [29] # Recommended: single block 29
247
+ stg_mode: "stg_av" # "stg_av" perturbs both audio and video, "stg_v" video only
248
+
249
+ # Whether to generate audio in validation samples
250
+ # Independent of training_strategy.audio.is_generated - you can generate audio
251
+ # in validation even when not training the audio branch
252
+ generate_audio: true
253
+
254
+ # Skip validation at the beginning of training (step 0)
255
+ skip_initial_validation: false
256
+
257
+ # -----------------------------------------------------------------------------
258
+ # Checkpoint Configuration
259
+ # -----------------------------------------------------------------------------
260
+ # Controls model checkpoint saving during training.
261
+ checkpoints:
262
+ # Save a checkpoint every N steps
263
+ # Set to null to disable intermediate checkpoints
264
+ interval: 250
265
+
266
+ # Number of most recent checkpoints to keep
267
+ # Set to -1 to keep all checkpoints
268
+ keep_last_n: -1
269
+
270
+ # Precision to use when saving checkpoint weights
271
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
272
+ precision: "bfloat16"
273
+
274
+ # -----------------------------------------------------------------------------
275
+ # Flow Matching Configuration
276
+ # -----------------------------------------------------------------------------
277
+ # Parameters for the flow matching training objective.
278
+ flow_matching:
279
+ # Timestep sampling mode
280
+ # "shifted_logit_normal" is recommended for LTX-2 models
281
+ timestep_sampling_mode: "shifted_logit_normal"
282
+
283
+ # Additional parameters for timestep sampling
284
+ timestep_sampling_params: { }
285
+
286
+ # -----------------------------------------------------------------------------
287
+ # Hugging Face Hub Configuration
288
+ # -----------------------------------------------------------------------------
289
+ # Settings for uploading trained models to the Hugging Face Hub.
290
+ hub:
291
+ # Whether to push the trained model to the Hub
292
+ push_to_hub: false
293
+
294
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
295
+ # Required if push_to_hub is true
296
+ hub_model_id: null
297
+
298
+ # -----------------------------------------------------------------------------
299
+ # Weights & Biases Configuration
300
+ # -----------------------------------------------------------------------------
301
+ # Settings for experiment tracking with W&B.
302
+ wandb:
303
+ # Enable W&B logging
304
+ enabled: false
305
+
306
+ # W&B project name
307
+ project: "ltx-2-trainer"
308
+
309
+ # W&B username or team (null uses default account)
310
+ entity: null
311
+
312
+ # Tags to help organize runs
313
+ tags: [ "ltx2", "lora", "t2v" ]
314
+
315
+ # Log validation media (video/audio) to W&B
316
+ log_validation_videos: true
317
+
318
+ # -----------------------------------------------------------------------------
319
+ # General Configuration
320
+ # -----------------------------------------------------------------------------
321
+ # Global settings for the training run.
322
+
323
+ # Random seed for reproducibility
324
+ seed: 42
325
+
326
+ # Directory to save outputs (checkpoints, validation videos, logs)
327
+ output_dir: "outputs/t2v_lora"
packages/ltx-trainer/configs/t2v_lora_low_vram.yaml ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Text-to-Video LoRA Training Configuration (Low VRAM)
3
+ # =============================================================================
4
+ #
5
+ # This is a memory-optimized variant of the standard text-to-video LoRA config.
6
+ # It uses 8-bit optimizer, int8 quantization, and reduced LoRA rank to minimize
7
+ # GPU memory usage while maintaining good training quality.
8
+ #
9
+ # Memory optimizations applied:
10
+ # - 8-bit AdamW optimizer (reduces optimizer state memory by ~75%)
11
+ # - INT8 model quantization (reduces model memory by ~50%)
12
+ # - Lower LoRA rank (16 vs 32, reduces trainable parameters)
13
+ # - Gradient checkpointing enabled
14
+ #
15
+ # Recommended for GPUs with 32GB VRAM (e.g., RTX 5090).
16
+ #
17
+ # Use this configuration when you want to:
18
+ # - Fine-tune LTX-2 on your own video dataset with limited GPU memory
19
+ # - Train joint audio-video generation from text prompts
20
+ # - Create custom video generation styles or audiovisual concepts
21
+ #
22
+ # Dataset structure:
23
+ # preprocessed_data_root/
24
+ # ├── latents/ # Video latents (VAE-encoded videos)
25
+ # ├── conditions/ # Text embeddings for each video
26
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
27
+ #
28
+ # =============================================================================
29
+
30
+ # -----------------------------------------------------------------------------
31
+ # Model Configuration
32
+ # -----------------------------------------------------------------------------
33
+ # Specifies the base model to fine-tune and the training mode.
34
+ model:
35
+ # Path to the LTX-2 model checkpoint (.safetensors file)
36
+ # This should be a local path to your downloaded model
37
+ model_path: "path/to/ltx-2-model.safetensors"
38
+
39
+ # Path to the text encoder model directory
40
+ # For LTX-2, this is typically the Gemma-based text encoder
41
+ text_encoder_path: "path/to/gemma-text-encoder"
42
+
43
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
44
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
45
+ training_mode: "lora"
46
+
47
+ # Optional: Path to resume training from a checkpoint
48
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
49
+ load_checkpoint: null
50
+
51
+ # -----------------------------------------------------------------------------
52
+ # LoRA Configuration
53
+ # -----------------------------------------------------------------------------
54
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
55
+ # Using a lower rank (16) to reduce trainable parameters and memory usage.
56
+ # This still provides good capacity for many fine-tuning tasks.
57
+ lora:
58
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
59
+ # Typical values: 8, 16, 32, 64. Using 16 for low VRAM configuration.
60
+ rank: 16
61
+
62
+ # Alpha scaling factor (usually set equal to rank)
63
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
64
+ alpha: 16
65
+
66
+ # Dropout probability for LoRA layers (0.0 = no dropout)
67
+ # Can help with regularization if overfitting occurs
68
+ dropout: 0.0
69
+
70
+ # Which transformer modules to apply LoRA to
71
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
72
+ #
73
+ # VIDEO MODULES:
74
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
75
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
76
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
77
+ #
78
+ # AUDIO MODULES:
79
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
80
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
81
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
82
+ #
83
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
84
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
85
+ # (Q from video, K/V from audio - allows video to attend to audio features)
86
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
87
+ # (Q from audio, K/V from video - allows audio to attend to video features)
88
+ #
89
+ # Using short patterns like "to_k" matches ALL attention modules (video, audio, and cross-modal).
90
+ # For audio-video training, this is the recommended approach.
91
+ target_modules:
92
+ # Attention layers (matches both video and audio branches)
93
+ - "to_k"
94
+ - "to_q"
95
+ - "to_v"
96
+ - "to_out.0"
97
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
98
+ # - "ff.net.0.proj"
99
+ # - "ff.net.2"
100
+ # - "audio_ff.net.0.proj"
101
+ # - "audio_ff.net.2"
102
+
103
+ # -----------------------------------------------------------------------------
104
+ # Training Strategy Configuration
105
+ # -----------------------------------------------------------------------------
106
+ # Defines the training approach using the unified flexible strategy.
107
+ # This configuration trains both video and audio generation from text prompts.
108
+ training_strategy:
109
+ # Strategy name: "flexible" for the unified conditioning framework
110
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
111
+ # modality-specific configuration blocks.
112
+ name: "flexible"
113
+
114
+ # Video modality configuration
115
+ # When is_generated is true, the model learns to generate (denoise) video
116
+ video:
117
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
118
+ is_generated: true
119
+ # Directory name (within preprocessed_data_root) containing video latents
120
+ latents_dir: "latents"
121
+
122
+ # Audio modality configuration
123
+ # When is_generated is true, the model learns to generate (denoise) audio
124
+ audio:
125
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
126
+ is_generated: true
127
+ # Directory name (within preprocessed_data_root) containing audio latents
128
+ latents_dir: "audio_latents"
129
+
130
+ # -----------------------------------------------------------------------------
131
+ # Optimization Configuration
132
+ # -----------------------------------------------------------------------------
133
+ # Controls the training optimization parameters.
134
+ optimization:
135
+ # Learning rate for the optimizer
136
+ # Typical range for LoRA: 1e-5 to 1e-4
137
+ learning_rate: 1e-4
138
+
139
+ # Total number of training steps
140
+ steps: 2000
141
+
142
+ # Batch size per GPU
143
+ # Reduce if running out of memory
144
+ batch_size: 1
145
+
146
+ # Number of gradient accumulation steps
147
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
148
+ gradient_accumulation_steps: 1
149
+
150
+ # Maximum gradient norm for clipping (helps training stability)
151
+ max_grad_norm: 1.0
152
+
153
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
154
+ # Using 8-bit AdamW to reduce optimizer state memory by ~75%
155
+ optimizer_type: "adamw8bit"
156
+
157
+ # Learning rate scheduler type
158
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
159
+ scheduler_type: "linear"
160
+
161
+ # Additional scheduler parameters (depends on scheduler_type)
162
+ scheduler_params: { }
163
+
164
+ # Enable gradient checkpointing to reduce memory usage
165
+ # Recommended for training with limited GPU memory
166
+ enable_gradient_checkpointing: true
167
+
168
+ # -----------------------------------------------------------------------------
169
+ # Acceleration Configuration
170
+ # -----------------------------------------------------------------------------
171
+ # Hardware acceleration and memory optimization settings.
172
+ acceleration:
173
+ # Mixed precision training mode
174
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
175
+ mixed_precision_mode: "bf16"
176
+
177
+ # Model quantization for reduced memory usage
178
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
179
+ # Using INT8 quantization to reduce base model memory consumption by ~50%
180
+ quantization: "int8-quanto"
181
+
182
+ # Load text encoder in 8-bit precision to save memory
183
+ # Useful when GPU memory is limited
184
+ load_text_encoder_in_8bit: true
185
+
186
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
187
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
188
+ # LoRA). No effect under FSDP (sharded state).
189
+ offload_optimizer_during_validation: true
190
+
191
+ # -----------------------------------------------------------------------------
192
+ # Data Configuration
193
+ # -----------------------------------------------------------------------------
194
+ # Specifies the training data location and loading parameters.
195
+ data:
196
+ # Root directory containing preprocessed training data
197
+ # Should contain: latents/, conditions/, and audio_latents/
198
+ preprocessed_data_root: "/path/to/preprocessed/data"
199
+
200
+ # Number of worker processes for data loading
201
+ # Used for parallel data loading to speed up data loading
202
+ num_dataloader_workers: 2
203
+
204
+ # -----------------------------------------------------------------------------
205
+ # Validation Configuration
206
+ # -----------------------------------------------------------------------------
207
+ # Controls validation sampling during training.
208
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
209
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
210
+ validation:
211
+ # Validation samples — each sample describes a self-contained generation request.
212
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
213
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
214
+ samples:
215
+ - prompt: >-
216
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
217
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
218
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
219
+ ceramic mug, then continues working with focused concentration. The audio captures the
220
+ gentle clicking of keyboard keys, the soft rustle of papers, and ambient room tone with
221
+ occasional distant bird chirps from outside.
222
+ - prompt: >-
223
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
224
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
225
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
226
+ various pots simmer on the stove behind him. The audio features the sizzling of pans,
227
+ the clinking of utensils against plates, and the ambient hum of kitchen ventilation.
228
+
229
+ # Negative prompt to avoid unwanted artifacts
230
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
231
+
232
+ # Output video dimensions [width, height, frames]
233
+ # Width and height must be divisible by 32
234
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
235
+ video_dims: [ 576, 576, 49 ]
236
+
237
+ # Frame rate for generated videos
238
+ frame_rate: 25.0
239
+
240
+ # Random seed for reproducible validation outputs
241
+ seed: 42
242
+
243
+ # Number of denoising steps for validation inference
244
+ # Higher values = better quality but slower generation
245
+ inference_steps: 30
246
+
247
+ # Generate validation videos every N training steps
248
+ # Set to null to disable validation during training
249
+ interval: 100
250
+
251
+ # Classifier-free guidance scale
252
+ # Higher values = stronger adherence to prompt but may introduce artifacts
253
+ guidance_scale: 4.0
254
+
255
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
256
+ # STG is combined with CFG for better temporal coherence
257
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
258
+ stg_blocks: [ 29 ] # Recommended: single block 29
259
+ stg_mode: "stg_av" # "stg_av" perturbs both audio and video, "stg_v" video only
260
+
261
+ # Whether to generate audio in validation samples
262
+ # Independent of training_strategy audio modality settings — you can generate audio
263
+ # in validation even when not training the audio branch
264
+ generate_audio: true
265
+
266
+ # Skip validation at the beginning of training (step 0)
267
+ skip_initial_validation: false
268
+
269
+ # -----------------------------------------------------------------------------
270
+ # Checkpoint Configuration
271
+ # -----------------------------------------------------------------------------
272
+ # Controls model checkpoint saving during training.
273
+ checkpoints:
274
+ # Save a checkpoint every N steps
275
+ # Set to null to disable intermediate checkpoints
276
+ interval: 250
277
+
278
+ # Number of most recent checkpoints to keep
279
+ # Set to -1 to keep all checkpoints
280
+ keep_last_n: -1
281
+
282
+ # Precision to use when saving checkpoint weights
283
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
284
+ precision: "bfloat16"
285
+
286
+ # -----------------------------------------------------------------------------
287
+ # Flow Matching Configuration
288
+ # -----------------------------------------------------------------------------
289
+ # Parameters for the flow matching training objective.
290
+ flow_matching:
291
+ # Timestep sampling mode
292
+ # "shifted_logit_normal" is recommended for LTX-2 models
293
+ timestep_sampling_mode: "shifted_logit_normal"
294
+
295
+ # Additional parameters for timestep sampling
296
+ timestep_sampling_params: { }
297
+
298
+ # -----------------------------------------------------------------------------
299
+ # Hugging Face Hub Configuration
300
+ # -----------------------------------------------------------------------------
301
+ # Settings for uploading trained models to the Hugging Face Hub.
302
+ hub:
303
+ # Whether to push the trained model to the Hub
304
+ push_to_hub: false
305
+
306
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
307
+ # Required if push_to_hub is true
308
+ hub_model_id: null
309
+
310
+ # -----------------------------------------------------------------------------
311
+ # Weights & Biases Configuration
312
+ # -----------------------------------------------------------------------------
313
+ # Settings for experiment tracking with W&B.
314
+ wandb:
315
+ # Enable W&B logging
316
+ enabled: false
317
+
318
+ # W&B project name
319
+ project: "ltx-2-trainer"
320
+
321
+ # W&B username or team (null uses default account)
322
+ entity: null
323
+
324
+ # Tags to help organize runs
325
+ tags: [ "ltx2", "lora", "t2v", "low-vram" ]
326
+
327
+ # Log validation media (video/audio) to W&B
328
+ log_validation_videos: true
329
+
330
+ # -----------------------------------------------------------------------------
331
+ # General Configuration
332
+ # -----------------------------------------------------------------------------
333
+ # Global settings for the training run.
334
+
335
+ # Random seed for reproducibility
336
+ seed: 42
337
+
338
+ # Directory to save outputs (checkpoints, validation videos, logs)
339
+ output_dir: "outputs/t2v_lora_low_vram"
packages/ltx-trainer/configs/v2a_lora.yaml ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Video-to-Audio (Foley) LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # video-to-audio (Foley) generation. The model learns to generate audio
7
+ # conditioned on a frozen video signal via the transformer's built-in
8
+ # cross-modal attention.
9
+ #
10
+ # In this mode, video is provided as a frozen (clean, no noise, no loss)
11
+ # conditioning signal. The audio modality is the only generated output.
12
+ # Video influences audio generation through the transformer's video-to-audio
13
+ # cross-attention mechanism.
14
+ #
15
+ # Use this configuration when you want to:
16
+ # - Generate sound effects (Foley) for existing videos
17
+ # - Train audio generation conditioned on visual content
18
+ # - Create models that produce audio matching video content
19
+ #
20
+ # Dataset structure:
21
+ # preprocessed_data_root/
22
+ # ├── latents/ # Video latents (frozen conditioning input)
23
+ # ├── conditions/ # Text embeddings for each video
24
+ # └── audio_latents/ # Audio latents (VAE-encoded audio, generated output)
25
+ #
26
+ # =============================================================================
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # Model Configuration
30
+ # -----------------------------------------------------------------------------
31
+ # Specifies the base model to fine-tune and the training mode.
32
+ model:
33
+ # Path to the LTX-2 model checkpoint (.safetensors file)
34
+ # This should be a local path to your downloaded model
35
+ model_path: "path/to/ltx-2-model.safetensors"
36
+
37
+ # Path to the text encoder model directory
38
+ # For LTX-2, this is typically the Gemma-based text encoder
39
+ text_encoder_path: "path/to/gemma-text-encoder"
40
+
41
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
42
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
43
+ training_mode: "lora"
44
+
45
+ # Optional: Path to resume training from a checkpoint
46
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
47
+ load_checkpoint: null
48
+
49
+ # -----------------------------------------------------------------------------
50
+ # LoRA Configuration
51
+ # -----------------------------------------------------------------------------
52
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
53
+ lora:
54
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
55
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
56
+ rank: 32
57
+
58
+ # Alpha scaling factor (usually set equal to rank)
59
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
60
+ alpha: 32
61
+
62
+ # Dropout probability for LoRA layers (0.0 = no dropout)
63
+ # Can help with regularization if overfitting occurs
64
+ dropout: 0.0
65
+
66
+ # Which transformer modules to apply LoRA to
67
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
68
+ #
69
+ # VIDEO MODULES:
70
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
71
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
72
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
73
+ #
74
+ # AUDIO MODULES:
75
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
76
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
77
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
78
+ #
79
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
80
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
81
+ # (Q from video, K/V from audio - allows video to attend to audio features)
82
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
83
+ # (Q from audio, K/V from video - allows audio to attend to video features)
84
+ #
85
+ # For video-to-audio training, we target audio modules and the cross-modal
86
+ # attention that allows audio to attend to video features.
87
+ target_modules:
88
+ # Audio self-attention
89
+ - "audio_attn1.to_k"
90
+ - "audio_attn1.to_q"
91
+ - "audio_attn1.to_v"
92
+ - "audio_attn1.to_out.0"
93
+ # Audio cross-attention to text
94
+ - "audio_attn2.to_k"
95
+ - "audio_attn2.to_q"
96
+ - "audio_attn2.to_v"
97
+ - "audio_attn2.to_out.0"
98
+ # Audio feed-forward
99
+ - "audio_ff.net.0.proj"
100
+ - "audio_ff.net.2"
101
+ # Cross-modal attention: allows audio to attend to video features
102
+ - "video_to_audio_attn.to_k"
103
+ - "video_to_audio_attn.to_q"
104
+ - "video_to_audio_attn.to_v"
105
+ - "video_to_audio_attn.to_out.0"
106
+
107
+ # -----------------------------------------------------------------------------
108
+ # Training Strategy Configuration
109
+ # -----------------------------------------------------------------------------
110
+ # Defines the video-to-audio (Foley) training approach using the unified
111
+ # flexible strategy. Video is frozen (no noise, sigma=0, excluded from loss)
112
+ # and conditions audio generation via the transformer's cross-modal attention.
113
+ training_strategy:
114
+ # Strategy name: "flexible" for the unified conditioning framework
115
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
116
+ # modality-specific configuration blocks.
117
+ name: "flexible"
118
+
119
+ # Video modality configuration
120
+ # Video is frozen — it acts as conditioning for audio generation
121
+ # Frozen modalities get sigma=0, timestep=0, no noise, and no loss
122
+ video:
123
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
124
+ # When false, video is passed through the transformer clean and influences
125
+ # audio via cross-modal attention
126
+ is_generated: false
127
+ # Directory name (within preprocessed_data_root) containing video latents
128
+ latents_dir: "latents"
129
+
130
+ # Audio modality configuration
131
+ # Audio is the generated (denoised) output
132
+ audio:
133
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
134
+ is_generated: true
135
+ # Directory name (within preprocessed_data_root) containing audio latents
136
+ latents_dir: "audio_latents"
137
+
138
+ # -----------------------------------------------------------------------------
139
+ # Optimization Configuration
140
+ # -----------------------------------------------------------------------------
141
+ # Controls the training optimization parameters.
142
+ optimization:
143
+ # Learning rate for the optimizer
144
+ # Typical range for LoRA: 1e-5 to 1e-4
145
+ learning_rate: 1e-4
146
+
147
+ # Total number of training steps
148
+ steps: 2000
149
+
150
+ # Batch size per GPU
151
+ # Reduce if running out of memory
152
+ batch_size: 1
153
+
154
+ # Number of gradient accumulation steps
155
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
156
+ gradient_accumulation_steps: 1
157
+
158
+ # Maximum gradient norm for clipping (helps training stability)
159
+ max_grad_norm: 1.0
160
+
161
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
162
+ optimizer_type: "adamw"
163
+
164
+ # Learning rate scheduler type
165
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
166
+ scheduler_type: "linear"
167
+
168
+ # Additional scheduler parameters (depends on scheduler_type)
169
+ scheduler_params: { }
170
+
171
+ # Enable gradient checkpointing to reduce memory usage
172
+ # Recommended for training with limited GPU memory
173
+ enable_gradient_checkpointing: true
174
+
175
+ # -----------------------------------------------------------------------------
176
+ # Acceleration Configuration
177
+ # -----------------------------------------------------------------------------
178
+ # Hardware acceleration and memory optimization settings.
179
+ acceleration:
180
+ # Mixed precision training mode
181
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
182
+ mixed_precision_mode: "bf16"
183
+
184
+ # Model quantization for reduced memory usage
185
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
186
+ quantization: null
187
+
188
+ # Load text encoder in 8-bit precision to save memory
189
+ # Useful when GPU memory is limited
190
+ load_text_encoder_in_8bit: false
191
+
192
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
193
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
194
+ # LoRA). No effect under FSDP (sharded state).
195
+ offload_optimizer_during_validation: false
196
+
197
+
198
+ # -----------------------------------------------------------------------------
199
+ # Data Configuration
200
+ # -----------------------------------------------------------------------------
201
+ # Specifies the training data location and loading parameters.
202
+ data:
203
+ # Root directory containing preprocessed training data
204
+ # Should contain: latents/, audio_latents/, and conditions/
205
+ preprocessed_data_root: "/path/to/preprocessed/data"
206
+
207
+ # Number of worker processes for data loading
208
+ # Used for parallel data loading to speed up data loading
209
+ num_dataloader_workers: 2
210
+
211
+ # -----------------------------------------------------------------------------
212
+ # Validation Configuration
213
+ # -----------------------------------------------------------------------------
214
+ # Controls validation sampling during training.
215
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
216
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
217
+ validation:
218
+ # Validation samples — each sample describes a self-contained generation request.
219
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
220
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
221
+ samples:
222
+ - prompt: >-
223
+ The sound of ocean waves crashing against rocky cliffs, with seagulls calling in the
224
+ distance and wind whistling through coastal grass.
225
+ conditions:
226
+ - type: video_to_audio
227
+ video: "/path/to/conditioning_video_1.mp4"
228
+ - prompt: >-
229
+ Footsteps echo in a marble hallway as a person walks steadily, with the distant hum of
230
+ air conditioning and occasional door closing sounds.
231
+ conditions:
232
+ - type: video_to_audio
233
+ video: "/path/to/conditioning_video_2.mp4"
234
+
235
+ # Negative prompt to avoid unwanted artifacts
236
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
237
+
238
+ # Validation dimensions [width, height, frames]
239
+ # Width/height resize the frozen video condition; frames and frame_rate set audio duration.
240
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
241
+ video_dims: [ 576, 576, 89 ]
242
+
243
+ # Frame rate for generated videos
244
+ frame_rate: 25.0
245
+
246
+ # Random seed for reproducible validation outputs
247
+ seed: 42
248
+
249
+ # Number of denoising steps for validation inference
250
+ # Higher values = better quality but slower generation
251
+ inference_steps: 30
252
+
253
+ # Generate validation videos every N training steps
254
+ # Set to null to disable validation during training
255
+ interval: 100
256
+
257
+ # Classifier-free guidance scale
258
+ # Higher values = stronger adherence to prompt but may introduce artifacts
259
+ guidance_scale: 4.0
260
+
261
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
262
+ # STG is combined with CFG for better temporal coherence
263
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
264
+ stg_blocks: [29] # Recommended: single block 29
265
+ stg_mode: "stg_av" # "stg_av" perturbs both audio and video, "stg_v" video only
266
+
267
+ # Whether to generate audio in validation samples
268
+ # Independent of training_strategy.audio.is_generated - you can generate audio
269
+ # in validation even when not training the audio branch
270
+ generate_audio: true
271
+
272
+ # Whether to generate video in validation samples
273
+ # Disabled for V2A since video is frozen conditioning, not generated
274
+ generate_video: false
275
+
276
+ # Skip validation at the beginning of training (step 0)
277
+ skip_initial_validation: false
278
+
279
+ # -----------------------------------------------------------------------------
280
+ # Checkpoint Configuration
281
+ # -----------------------------------------------------------------------------
282
+ # Controls model checkpoint saving during training.
283
+ checkpoints:
284
+ # Save a checkpoint every N steps
285
+ # Set to null to disable intermediate checkpoints
286
+ interval: 250
287
+
288
+ # Number of most recent checkpoints to keep
289
+ # Set to -1 to keep all checkpoints
290
+ keep_last_n: -1
291
+
292
+ # Precision to use when saving checkpoint weights
293
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
294
+ precision: "bfloat16"
295
+
296
+ # -----------------------------------------------------------------------------
297
+ # Flow Matching Configuration
298
+ # -----------------------------------------------------------------------------
299
+ # Parameters for the flow matching training objective.
300
+ flow_matching:
301
+ # Timestep sampling mode
302
+ # "shifted_logit_normal" is recommended for LTX-2 models
303
+ timestep_sampling_mode: "shifted_logit_normal"
304
+
305
+ # Additional parameters for timestep sampling
306
+ timestep_sampling_params: { }
307
+
308
+ # -----------------------------------------------------------------------------
309
+ # Hugging Face Hub Configuration
310
+ # -----------------------------------------------------------------------------
311
+ # Settings for uploading trained models to the Hugging Face Hub.
312
+ hub:
313
+ # Whether to push the trained model to the Hub
314
+ push_to_hub: false
315
+
316
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
317
+ # Required if push_to_hub is true
318
+ hub_model_id: null
319
+
320
+ # -----------------------------------------------------------------------------
321
+ # Weights & Biases Configuration
322
+ # -----------------------------------------------------------------------------
323
+ # Settings for experiment tracking with W&B.
324
+ wandb:
325
+ # Enable W&B logging
326
+ enabled: false
327
+
328
+ # W&B project name
329
+ project: "ltx-2-trainer"
330
+
331
+ # W&B username or team (null uses default account)
332
+ entity: null
333
+
334
+ # Tags to help organize runs
335
+ tags: [ "ltx2", "lora", "v2a", "foley" ]
336
+
337
+ # Log validation media (video/audio) to W&B
338
+ log_validation_videos: true
339
+
340
+ # -----------------------------------------------------------------------------
341
+ # General Configuration
342
+ # -----------------------------------------------------------------------------
343
+ # Global settings for the training run.
344
+
345
+ # Random seed for reproducibility
346
+ seed: 42
347
+
348
+ # Directory to save outputs (checkpoints, validation videos, logs)
349
+ output_dir: "outputs/v2a_lora"
packages/ltx-trainer/configs/v2v_ic_lora.yaml ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Video-to-Video (IC-LoRA) Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training In-Context LoRA (IC-LoRA) adapters that
6
+ # enable video-to-video transformations. IC-LoRA learns to apply visual
7
+ # transformations (e.g., depth-to-video, pose control, style transfer, etc.)
8
+ # by conditioning on reference videos.
9
+ #
10
+ # Key differences from text-to-video LoRA:
11
+ # - Uses reference videos as conditioning input alongside text prompts
12
+ # - Requires preprocessed reference latents in addition to target latents
13
+ # - Validation requires reference videos to demonstrate the transformation
14
+ #
15
+ # Dataset structure for IC-LoRA training:
16
+ # preprocessed_data_root/
17
+ # ├── latents/ # Target video latents (what the model learns to generate)
18
+ # ├── conditions/ # Text embeddings for each video
19
+ # └── reference_latents/ # Reference video latents (conditioning input)
20
+ #
21
+ # Dataset metadata columns: video, reference_video, caption
22
+ #
23
+ # =============================================================================
24
+
25
+ # -----------------------------------------------------------------------------
26
+ # Model Configuration
27
+ # -----------------------------------------------------------------------------
28
+ # Specifies the base model to fine-tune and the training mode.
29
+ model:
30
+ # Path to the LTX-2 model checkpoint (.safetensors file)
31
+ # This should be a local path to your downloaded model
32
+ model_path: "path/to/ltx-2-model.safetensors"
33
+
34
+ # Path to the text encoder model directory
35
+ # For LTX-2, this is typically the Gemma-based text encoder
36
+ text_encoder_path: "path/to/gemma-text-encoder"
37
+
38
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
39
+ # IC-LoRA reference conditioning is intended for LoRA adapter training.
40
+ training_mode: "lora"
41
+
42
+ # Optional: Path to resume training from a checkpoint
43
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
44
+ load_checkpoint: null
45
+
46
+ # -----------------------------------------------------------------------------
47
+ # LoRA Configuration
48
+ # -----------------------------------------------------------------------------
49
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
50
+ lora:
51
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
52
+ # Typical values: 8, 16, 32, 64. Start with 16-32 for IC-LoRA.
53
+ rank: 32
54
+
55
+ # Alpha scaling factor (usually set equal to rank)
56
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
57
+ alpha: 32
58
+
59
+ # Dropout probability for LoRA layers (0.0 = no dropout)
60
+ # Can help with regularization if overfitting occurs
61
+ dropout: 0.0
62
+
63
+ # Which transformer modules to apply LoRA to
64
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
65
+ #
66
+ # VIDEO MODULES:
67
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
68
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
69
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
70
+ #
71
+ # AUDIO MODULES (not used for video-only IC-LoRA):
72
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
73
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
74
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
75
+ #
76
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction, not used for video-only IC-LoRA):
77
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
78
+ # (Q from video, K/V from audio - allows video to attend to audio features)
79
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
80
+ # (Q from audio, K/V from video - allows audio to attend to video features)
81
+ #
82
+ # For IC-LoRA (video-only), we explicitly target video modules.
83
+ # Including FFN layers often improves transformation quality.
84
+ target_modules:
85
+ # Video self-attention
86
+ - "attn1.to_k"
87
+ - "attn1.to_q"
88
+ - "attn1.to_v"
89
+ - "attn1.to_out.0"
90
+ # Video cross-attention
91
+ - "attn2.to_k"
92
+ - "attn2.to_q"
93
+ - "attn2.to_v"
94
+ - "attn2.to_out.0"
95
+ # Video feed-forward (often improves transformation quality)
96
+ - "ff.net.0.proj"
97
+ - "ff.net.2"
98
+
99
+ # -----------------------------------------------------------------------------
100
+ # Training Strategy Configuration
101
+ # -----------------------------------------------------------------------------
102
+ # Defines the video-to-video (IC-LoRA) training approach using the unified
103
+ # flexible strategy. Reference conditioning concatenates pre-encoded reference
104
+ # video latents to the target sequence. Reference tokens participate in
105
+ # bidirectional self-attention but receive no noise and are excluded from loss.
106
+ training_strategy:
107
+ # Strategy name: "flexible" for the unified conditioning framework
108
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
109
+ # modality-specific configuration blocks.
110
+ name: "flexible"
111
+
112
+ # Video modality configuration
113
+ video:
114
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
115
+ is_generated: true
116
+ # Directory name (within preprocessed_data_root) containing target video latents
117
+ latents_dir: "latents"
118
+
119
+ # Conditions applied to the video modality during training
120
+ conditions:
121
+ # Reference conditioning (IC-LoRA): concatenates pre-encoded reference video
122
+ # latents to the target sequence. The model learns to transform the reference
123
+ # into the target video based on the text prompt.
124
+ - type: reference
125
+ # Directory name (within preprocessed_data_root) containing reference video latents
126
+ # These are the conditioning inputs that guide the transformation
127
+ latents_dir: "reference_latents"
128
+ # Probability of applying reference conditioning per training sample
129
+ probability: 1.0
130
+
131
+ # Optional first-frame conditioning to improve I2V capabilities
132
+ # At low probability, this teaches the model to also accept first-frame input
133
+ - type: first_frame
134
+ probability: 0.2
135
+
136
+ # -----------------------------------------------------------------------------
137
+ # Optimization Configuration
138
+ # -----------------------------------------------------------------------------
139
+ # Controls the training optimization parameters.
140
+ optimization:
141
+ # Learning rate for the optimizer
142
+ # Typical range for LoRA: 1e-5 to 1e-4
143
+ learning_rate: 2e-4
144
+
145
+ # Total number of training steps
146
+ steps: 3000
147
+
148
+ # Batch size per GPU
149
+ # Reduce if running out of memory
150
+ batch_size: 1
151
+
152
+ # Number of gradient accumulation steps
153
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
154
+ gradient_accumulation_steps: 1
155
+
156
+ # Maximum gradient norm for clipping (helps training stability)
157
+ max_grad_norm: 1.0
158
+
159
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
160
+ optimizer_type: "adamw"
161
+
162
+ # Learning rate scheduler type
163
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
164
+ scheduler_type: "linear"
165
+
166
+ # Additional scheduler parameters (depends on scheduler_type)
167
+ scheduler_params: { }
168
+
169
+ # Enable gradient checkpointing to reduce memory usage
170
+ # Recommended for training with limited GPU memory
171
+ enable_gradient_checkpointing: true
172
+
173
+ # -----------------------------------------------------------------------------
174
+ # Acceleration Configuration
175
+ # -----------------------------------------------------------------------------
176
+ # Hardware acceleration and memory optimization settings.
177
+ acceleration:
178
+ # Mixed precision training mode
179
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
180
+ mixed_precision_mode: "bf16"
181
+
182
+ # Model quantization for reduced memory usage
183
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
184
+ quantization: null
185
+
186
+ # Load text encoder in 8-bit precision to save memory
187
+ # Useful when GPU memory is limited
188
+ load_text_encoder_in_8bit: false
189
+
190
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
191
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
192
+ # LoRA). No effect under FSDP (sharded state).
193
+ offload_optimizer_during_validation: false
194
+
195
+
196
+ # -----------------------------------------------------------------------------
197
+ # Data Configuration
198
+ # -----------------------------------------------------------------------------
199
+ # Specifies the training data location and loading parameters.
200
+ data:
201
+ # Root directory containing preprocessed training data
202
+ # Should contain: latents/, conditions/, and reference_latents/ subdirectories
203
+ preprocessed_data_root: "/path/to/preprocessed/data"
204
+
205
+ # Number of worker processes for data loading
206
+ # Used for parallel data loading to speed up data loading
207
+ num_dataloader_workers: 2
208
+
209
+ # -----------------------------------------------------------------------------
210
+ # Validation Configuration
211
+ # -----------------------------------------------------------------------------
212
+ # Controls validation sampling during training.
213
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
214
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
215
+ validation:
216
+ # Validation samples — each sample describes a self-contained generation request.
217
+ # For IC-LoRA, each sample includes a reference condition pointing to the conditioning video.
218
+ samples:
219
+ - prompt: >-
220
+ A man in a casual blue jacket walks along a winding path through a lush green park on a
221
+ bright sunny afternoon. Tall oak trees line the pathway, their leaves rustling gently in
222
+ the breeze. Dappled sunlight creates shifting patterns on the ground as he strolls at a
223
+ relaxed pace, occasionally looking up at the scenery around him. The audio captures
224
+ footsteps on gravel, birds singing in the trees, distant children playing, and the soft
225
+ whisper of wind through the foliage.
226
+ conditions:
227
+ - type: reference
228
+ video: "/path/to/reference_video_1.mp4"
229
+ # Set these to match --reference-downscale-factor / --reference-temporal-scale-factor
230
+ # if reference latents were preprocessed at reduced spatial or temporal resolution.
231
+ downscale_factor: 1
232
+ temporal_scale_factor: 1
233
+ include_in_output: true
234
+ - prompt: >-
235
+ A fluffy orange tabby cat sits perfectly still on a wooden windowsill, its green eyes
236
+ intently tracking small birds hopping on a branch just outside the glass. The cat's ears
237
+ twitch and rotate, following every movement. Warm afternoon light illuminates its fur,
238
+ creating a soft golden glow. Behind the cat, a cozy living room with a bookshelf and
239
+ houseplants is visible. The audio features gentle purring, occasional soft meows, muffled
240
+ bird chirps through the window, and quiet ambient room sounds.
241
+ conditions:
242
+ - type: reference
243
+ video: "/path/to/reference_video_2.mp4"
244
+ # Set these to match --reference-downscale-factor / --reference-temporal-scale-factor
245
+ # if reference latents were preprocessed at reduced spatial or temporal resolution.
246
+ downscale_factor: 1
247
+ temporal_scale_factor: 1
248
+ include_in_output: true
249
+
250
+ # Negative prompt to avoid unwanted artifacts
251
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
252
+
253
+ # Output video dimensions [width, height, frames]
254
+ # Width and height must be divisible by 32
255
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
256
+ video_dims: [ 512, 512, 81 ]
257
+
258
+ # Frame rate for generated videos
259
+ frame_rate: 25.0
260
+
261
+ # Random seed for reproducible validation outputs
262
+ seed: 42
263
+
264
+ # Number of denoising steps for validation inference
265
+ # Higher values = better quality but slower generation
266
+ inference_steps: 30
267
+
268
+ # Generate validation videos every N training steps
269
+ # Set to null to disable validation during training
270
+ interval: 100
271
+
272
+ # Classifier-free guidance scale
273
+ # Higher values = stronger adherence to prompt but may introduce artifacts
274
+ guidance_scale: 4.0
275
+
276
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
277
+ # STG is combined with CFG for better temporal coherence
278
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
279
+ stg_blocks: [29] # Recommended: single block 29
280
+ stg_mode: "stg_v" # "stg_v" for video-only (no audio training)
281
+
282
+ # Whether to generate audio in validation samples
283
+ # Can be enabled even when not training the audio branch
284
+ generate_audio: false
285
+
286
+ # Skip validation at the beginning of training (step 0)
287
+ skip_initial_validation: false
288
+
289
+ # -----------------------------------------------------------------------------
290
+ # Checkpoint Configuration
291
+ # -----------------------------------------------------------------------------
292
+ # Controls model checkpoint saving during training.
293
+ checkpoints:
294
+ # Save a checkpoint every N steps
295
+ # Set to null to disable intermediate checkpoints
296
+ interval: 250
297
+
298
+ # Number of most recent checkpoints to keep
299
+ # Set to -1 to keep all checkpoints
300
+ keep_last_n: 3
301
+
302
+ # Precision to use when saving checkpoint weights
303
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
304
+ precision: "bfloat16"
305
+
306
+ # -----------------------------------------------------------------------------
307
+ # Flow Matching Configuration
308
+ # -----------------------------------------------------------------------------
309
+ # Parameters for the flow matching training objective.
310
+ flow_matching:
311
+ # Timestep sampling mode
312
+ # "shifted_logit_normal" is recommended for LTX-2 models
313
+ timestep_sampling_mode: "shifted_logit_normal"
314
+
315
+ # Additional parameters for timestep sampling
316
+ timestep_sampling_params: { }
317
+
318
+ # -----------------------------------------------------------------------------
319
+ # Hugging Face Hub Configuration
320
+ # -----------------------------------------------------------------------------
321
+ # Settings for uploading trained models to the Hugging Face Hub.
322
+ hub:
323
+ # Whether to push the trained model to the Hub
324
+ push_to_hub: false
325
+
326
+ # Repository ID on Hugging Face Hub (e.g., "username/my-ic-lora-model")
327
+ # Required if push_to_hub is true
328
+ hub_model_id: null
329
+
330
+ # -----------------------------------------------------------------------------
331
+ # Weights & Biases Configuration
332
+ # -----------------------------------------------------------------------------
333
+ # Settings for experiment tracking with W&B.
334
+ wandb:
335
+ # Enable W&B logging
336
+ enabled: false
337
+
338
+ # W&B project name
339
+ project: "ltx-2-trainer"
340
+
341
+ # W&B username or team (null uses default account)
342
+ entity: null
343
+
344
+ # Tags to help organize runs
345
+ tags: [ "ltx2", "ic-lora", "v2v" ]
346
+
347
+ # Log validation media (video/audio) to W&B
348
+ log_validation_videos: true
349
+
350
+ # -----------------------------------------------------------------------------
351
+ # General Configuration
352
+ # -----------------------------------------------------------------------------
353
+ # Global settings for the training run.
354
+
355
+ # Random seed for reproducibility
356
+ seed: 42
357
+
358
+ # Directory to save outputs (checkpoints, validation videos, logs)
359
+ output_dir: "outputs/v2v_ic_lora"
packages/ltx-trainer/configs/video_extend_lora.yaml ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Video Extension LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # video extension (temporal continuation). The model learns to extend a video
7
+ # forward in time by conditioning on a prefix of existing frames.
8
+ #
9
+ # Prefix conditioning works by providing the first N temporal units as clean
10
+ # conditioning signals during training — they receive no noise, timestep=0,
11
+ # and are excluded from the loss. The model learns to generate continuation
12
+ # frames that seamlessly follow the given prefix.
13
+ #
14
+ # Use this configuration when you want to:
15
+ # - Train the model to extend/continue existing videos
16
+ # - Fine-tune temporal continuation capabilities on custom datasets
17
+ # - Create seamless video extension models with optional audio
18
+ #
19
+ # Dataset structure:
20
+ # preprocessed_data_root/
21
+ # ├── latents/ # Video latents (VAE-encoded videos)
22
+ # ├── conditions/ # Text embeddings for each video
23
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
24
+ #
25
+ # =============================================================================
26
+
27
+ # -----------------------------------------------------------------------------
28
+ # Model Configuration
29
+ # -----------------------------------------------------------------------------
30
+ # Specifies the base model to fine-tune and the training mode.
31
+ model:
32
+ # Path to the LTX-2 model checkpoint (.safetensors file)
33
+ # This should be a local path to your downloaded model
34
+ model_path: "path/to/ltx-2-model.safetensors"
35
+
36
+ # Path to the text encoder model directory
37
+ # For LTX-2, this is typically the Gemma-based text encoder
38
+ text_encoder_path: "path/to/gemma-text-encoder"
39
+
40
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
41
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
42
+ training_mode: "lora"
43
+
44
+ # Optional: Path to resume training from a checkpoint
45
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
46
+ load_checkpoint: null
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # LoRA Configuration
50
+ # -----------------------------------------------------------------------------
51
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
52
+ lora:
53
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
54
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
55
+ rank: 32
56
+
57
+ # Alpha scaling factor (usually set equal to rank)
58
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
59
+ alpha: 32
60
+
61
+ # Dropout probability for LoRA layers (0.0 = no dropout)
62
+ # Can help with regularization if overfitting occurs
63
+ dropout: 0.0
64
+
65
+ # Which transformer modules to apply LoRA to
66
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
67
+ #
68
+ # VIDEO MODULES:
69
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
70
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
71
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
72
+ #
73
+ # AUDIO MODULES:
74
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
75
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
76
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
77
+ #
78
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
79
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
80
+ # (Q from video, K/V from audio - allows video to attend to audio features)
81
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
82
+ # (Q from audio, K/V from video - allows audio to attend to video features)
83
+ #
84
+ # Using short patterns like "to_k" matches ALL attention modules (video, audio, and cross-modal).
85
+ # For audio-video training, this is the recommended approach.
86
+ target_modules:
87
+ # Attention layers (matches both video and audio branches)
88
+ - "to_k"
89
+ - "to_q"
90
+ - "to_v"
91
+ - "to_out.0"
92
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
93
+ # - "ff.net.0.proj"
94
+ # - "ff.net.2"
95
+ # - "audio_ff.net.0.proj"
96
+ # - "audio_ff.net.2"
97
+
98
+ # -----------------------------------------------------------------------------
99
+ # Training Strategy Configuration
100
+ # -----------------------------------------------------------------------------
101
+ # Defines the video extension training approach using the unified flexible strategy.
102
+ # Prefix conditioning provides the first N latent frames as clean conditioning,
103
+ # teaching the model to generate temporally coherent continuations.
104
+ training_strategy:
105
+ # Strategy name: "flexible" for the unified conditioning framework
106
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
107
+ # modality-specific configuration blocks.
108
+ name: "flexible"
109
+
110
+ # Video modality configuration
111
+ video:
112
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
113
+ is_generated: true
114
+ # Directory name (within preprocessed_data_root) containing video latents
115
+ latents_dir: "latents"
116
+
117
+ # Conditions applied to the video modality during training
118
+ # Prefix conditioning: the first N temporal units are provided as clean
119
+ # conditioning signals (no noise, timestep=0, excluded from loss)
120
+ conditions:
121
+ - type: prefix
122
+ # Number of latent frames to use as conditioning prefix
123
+ # For prefix conditioning, N latent frames correspond to (N - 1) * 8 + 1 pixel frames.
124
+ # temporal_boundary=8 means 57 pixel frames are used as prefix.
125
+ temporal_boundary: 8
126
+ # Probability of applying prefix conditioning per training sample
127
+ # At 1.0, all training samples use video extension mode
128
+ probability: 1.0
129
+
130
+ # Audio modality configuration
131
+ audio:
132
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
133
+ is_generated: true
134
+ # Directory name (within preprocessed_data_root) containing audio latents
135
+ latents_dir: "audio_latents"
136
+
137
+ # -----------------------------------------------------------------------------
138
+ # Optimization Configuration
139
+ # -----------------------------------------------------------------------------
140
+ # Controls the training optimization parameters.
141
+ optimization:
142
+ # Learning rate for the optimizer
143
+ # Typical range for LoRA: 1e-5 to 1e-4
144
+ learning_rate: 1e-4
145
+
146
+ # Total number of training steps
147
+ steps: 2000
148
+
149
+ # Batch size per GPU
150
+ # Reduce if running out of memory
151
+ batch_size: 1
152
+
153
+ # Number of gradient accumulation steps
154
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
155
+ gradient_accumulation_steps: 1
156
+
157
+ # Maximum gradient norm for clipping (helps training stability)
158
+ max_grad_norm: 1.0
159
+
160
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
161
+ optimizer_type: "adamw"
162
+
163
+ # Learning rate scheduler type
164
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
165
+ scheduler_type: "linear"
166
+
167
+ # Additional scheduler parameters (depends on scheduler_type)
168
+ scheduler_params: { }
169
+
170
+ # Enable gradient checkpointing to reduce memory usage
171
+ # Recommended for training with limited GPU memory
172
+ enable_gradient_checkpointing: true
173
+
174
+ # -----------------------------------------------------------------------------
175
+ # Acceleration Configuration
176
+ # -----------------------------------------------------------------------------
177
+ # Hardware acceleration and memory optimization settings.
178
+ acceleration:
179
+ # Mixed precision training mode
180
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
181
+ mixed_precision_mode: "bf16"
182
+
183
+ # Model quantization for reduced memory usage
184
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
185
+ quantization: null
186
+
187
+ # Load text encoder in 8-bit precision to save memory
188
+ # Useful when GPU memory is limited
189
+ load_text_encoder_in_8bit: false
190
+
191
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
192
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
193
+ # LoRA). No effect under FSDP (sharded state).
194
+ offload_optimizer_during_validation: false
195
+
196
+
197
+ # -----------------------------------------------------------------------------
198
+ # Data Configuration
199
+ # -----------------------------------------------------------------------------
200
+ # Specifies the training data location and loading parameters.
201
+ data:
202
+ # Root directory containing preprocessed training data
203
+ # Should contain: latents/, conditions/, and audio_latents/
204
+ preprocessed_data_root: "/path/to/preprocessed/data"
205
+
206
+ # Number of worker processes for data loading
207
+ # Used for parallel data loading to speed up data loading
208
+ num_dataloader_workers: 2
209
+
210
+ # -----------------------------------------------------------------------------
211
+ # Validation Configuration
212
+ # -----------------------------------------------------------------------------
213
+ # Controls validation sampling during training.
214
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
215
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
216
+ validation:
217
+ # Validation samples — each sample describes a self-contained generation request.
218
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
219
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
220
+ samples:
221
+ - prompt: >-
222
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
223
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
224
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
225
+ ceramic mug, then continues working with focused concentration. The audio captures the
226
+ gentle clicking of keyboard keys, the soft rustle of papers, and ambient room tone with
227
+ occasional distant bird chirps from outside.
228
+ conditions:
229
+ - type: prefix
230
+ video: "/path/to/prefix_video_1.mp4"
231
+ # Matches temporal_boundary=8 during training: (8 - 1) * 8 + 1 = 57 frames
232
+ num_frames: 57
233
+ - prompt: >-
234
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
235
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
236
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
237
+ various pots simmer on the stove behind him. The audio features the sizzling of pans,
238
+ the clinking of utensils against plates, and the ambient hum of kitchen ventilation.
239
+ conditions:
240
+ - type: prefix
241
+ video: "/path/to/prefix_video_2.mp4"
242
+ # Matches temporal_boundary=8 during training: (8 - 1) * 8 + 1 = 57 frames
243
+ num_frames: 57
244
+
245
+ # Negative prompt to avoid unwanted artifacts
246
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
247
+
248
+ # Output video dimensions [width, height, frames]
249
+ # Width and height must be divisible by 32
250
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
251
+ video_dims: [ 576, 576, 89 ]
252
+
253
+ # Frame rate for generated videos
254
+ frame_rate: 25.0
255
+
256
+ # Random seed for reproducible validation outputs
257
+ seed: 42
258
+
259
+ # Number of denoising steps for validation inference
260
+ # Higher values = better quality but slower generation
261
+ inference_steps: 30
262
+
263
+ # Generate validation videos every N training steps
264
+ # Set to null to disable validation during training
265
+ interval: 100
266
+
267
+ # Classifier-free guidance scale
268
+ # Higher values = stronger adherence to prompt but may introduce artifacts
269
+ guidance_scale: 4.0
270
+
271
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
272
+ # STG is combined with CFG for better temporal coherence
273
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
274
+ stg_blocks: [29] # Recommended: single block 29
275
+ stg_mode: "stg_av" # "stg_av" perturbs both audio and video, "stg_v" video only
276
+
277
+ # Whether to generate audio in validation samples
278
+ # Independent of training_strategy.audio.is_generated - you can generate audio
279
+ # in validation even when not training the audio branch
280
+ generate_audio: true
281
+
282
+ # Skip validation at the beginning of training (step 0)
283
+ skip_initial_validation: false
284
+
285
+ # -----------------------------------------------------------------------------
286
+ # Checkpoint Configuration
287
+ # -----------------------------------------------------------------------------
288
+ # Controls model checkpoint saving during training.
289
+ checkpoints:
290
+ # Save a checkpoint every N steps
291
+ # Set to null to disable intermediate checkpoints
292
+ interval: 250
293
+
294
+ # Number of most recent checkpoints to keep
295
+ # Set to -1 to keep all checkpoints
296
+ keep_last_n: -1
297
+
298
+ # Precision to use when saving checkpoint weights
299
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
300
+ precision: "bfloat16"
301
+
302
+ # -----------------------------------------------------------------------------
303
+ # Flow Matching Configuration
304
+ # -----------------------------------------------------------------------------
305
+ # Parameters for the flow matching training objective.
306
+ flow_matching:
307
+ # Timestep sampling mode
308
+ # "shifted_logit_normal" is recommended for LTX-2 models
309
+ timestep_sampling_mode: "shifted_logit_normal"
310
+
311
+ # Additional parameters for timestep sampling
312
+ timestep_sampling_params: { }
313
+
314
+ # -----------------------------------------------------------------------------
315
+ # Hugging Face Hub Configuration
316
+ # -----------------------------------------------------------------------------
317
+ # Settings for uploading trained models to the Hugging Face Hub.
318
+ hub:
319
+ # Whether to push the trained model to the Hub
320
+ push_to_hub: false
321
+
322
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
323
+ # Required if push_to_hub is true
324
+ hub_model_id: null
325
+
326
+ # -----------------------------------------------------------------------------
327
+ # Weights & Biases Configuration
328
+ # -----------------------------------------------------------------------------
329
+ # Settings for experiment tracking with W&B.
330
+ wandb:
331
+ # Enable W&B logging
332
+ enabled: false
333
+
334
+ # W&B project name
335
+ project: "ltx-2-trainer"
336
+
337
+ # W&B username or team (null uses default account)
338
+ entity: null
339
+
340
+ # Tags to help organize runs
341
+ tags: [ "ltx2", "lora", "video-extension" ]
342
+
343
+ # Log validation media (video/audio) to W&B
344
+ log_validation_videos: true
345
+
346
+ # -----------------------------------------------------------------------------
347
+ # General Configuration
348
+ # -----------------------------------------------------------------------------
349
+ # Global settings for the training run.
350
+
351
+ # Random seed for reproducibility
352
+ seed: 42
353
+
354
+ # Directory to save outputs (checkpoints, validation videos, logs)
355
+ output_dir: "outputs/video_extend_lora"
packages/ltx-trainer/configs/video_inpainting_lora.yaml ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Video Inpainting LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # video inpainting. The model learns to fill in masked regions of a video
7
+ # using per-sample binary masks that define which regions are conditioning
8
+ # (provided clean) and which regions the model must generate.
9
+ #
10
+ # Mask conditioning works by loading per-sample binary masks from disk.
11
+ # Masked regions receive clean latents (no noise, timestep=0) and are excluded
12
+ # from the loss. Unmasked regions are noised and trained normally.
13
+ #
14
+ # Use this configuration when you want to:
15
+ # - Train the model to fill in or replace regions of existing videos
16
+ # - Fine-tune video inpainting capabilities on custom datasets
17
+ # - Create object removal or region editing models
18
+ #
19
+ # Dataset structure:
20
+ # preprocessed_data_root/
21
+ # ├── latents/ # Video latents (VAE-encoded videos)
22
+ # ├── conditions/ # Text embeddings for each video
23
+ # └── video_masks/ # Per-sample binary masks defining conditioning regions
24
+ #
25
+ # Dataset metadata columns: video, video_mask, caption
26
+ #
27
+ # =============================================================================
28
+
29
+ # -----------------------------------------------------------------------------
30
+ # Model Configuration
31
+ # -----------------------------------------------------------------------------
32
+ # Specifies the base model to fine-tune and the training mode.
33
+ model:
34
+ # Path to the LTX-2 model checkpoint (.safetensors file)
35
+ # This should be a local path to your downloaded model
36
+ model_path: "path/to/ltx-2-model.safetensors"
37
+
38
+ # Path to the text encoder model directory
39
+ # For LTX-2, this is typically the Gemma-based text encoder
40
+ text_encoder_path: "path/to/gemma-text-encoder"
41
+
42
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
43
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
44
+ training_mode: "lora"
45
+
46
+ # Optional: Path to resume training from a checkpoint
47
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
48
+ load_checkpoint: null
49
+
50
+ # -----------------------------------------------------------------------------
51
+ # LoRA Configuration
52
+ # -----------------------------------------------------------------------------
53
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
54
+ lora:
55
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
56
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
57
+ rank: 32
58
+
59
+ # Alpha scaling factor (usually set equal to rank)
60
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
61
+ alpha: 32
62
+
63
+ # Dropout probability for LoRA layers (0.0 = no dropout)
64
+ # Can help with regularization if overfitting occurs
65
+ dropout: 0.0
66
+
67
+ # Which transformer modules to apply LoRA to
68
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
69
+ #
70
+ # VIDEO MODULES:
71
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
72
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
73
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
74
+ #
75
+ # AUDIO MODULES:
76
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
77
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
78
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
79
+ #
80
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
81
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
82
+ # (Q from video, K/V from audio - allows video to attend to audio features)
83
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
84
+ # (Q from audio, K/V from video - allows audio to attend to video features)
85
+ #
86
+ # This is a video-only config, so explicitly target video attention modules.
87
+ target_modules:
88
+ - "attn1.to_k"
89
+ - "attn1.to_q"
90
+ - "attn1.to_v"
91
+ - "attn1.to_out.0"
92
+ - "attn2.to_k"
93
+ - "attn2.to_q"
94
+ - "attn2.to_v"
95
+ - "attn2.to_out.0"
96
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
97
+ # - "ff.net.0.proj"
98
+ # - "ff.net.2"
99
+
100
+ # -----------------------------------------------------------------------------
101
+ # Training Strategy Configuration
102
+ # -----------------------------------------------------------------------------
103
+ # Defines the video inpainting training approach using the unified flexible
104
+ # strategy. Per-sample binary masks define which regions are provided as clean
105
+ # conditioning and which regions the model must learn to generate.
106
+ training_strategy:
107
+ # Strategy name: "flexible" for the unified conditioning framework
108
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
109
+ # modality-specific configuration blocks.
110
+ name: "flexible"
111
+
112
+ # Video modality configuration
113
+ video:
114
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
115
+ is_generated: true
116
+ # Directory name (within preprocessed_data_root) containing video latents
117
+ latents_dir: "latents"
118
+
119
+ # Conditions applied to the video modality during training
120
+ # Mask conditioning: per-sample binary masks loaded from disk define which
121
+ # tokens are conditioning (clean, timestep=0, no loss) vs generated
122
+ conditions:
123
+ - type: mask
124
+ # Directory name (within preprocessed_data_root) containing binary masks
125
+ # Each mask file corresponds to a training sample and defines the
126
+ # conditioning region (mask=1 means conditioning, mask=0 means generate)
127
+ mask_dir: "video_masks"
128
+ # Probability of applying mask conditioning per training sample
129
+ # At 1.0, all training samples use inpainting mode
130
+ probability: 1.0
131
+
132
+ # -----------------------------------------------------------------------------
133
+ # Optimization Configuration
134
+ # -----------------------------------------------------------------------------
135
+ # Controls the training optimization parameters.
136
+ optimization:
137
+ # Learning rate for the optimizer
138
+ # Typical range for LoRA: 1e-5 to 1e-4
139
+ learning_rate: 1e-4
140
+
141
+ # Total number of training steps
142
+ steps: 2000
143
+
144
+ # Batch size per GPU
145
+ # Reduce if running out of memory
146
+ batch_size: 1
147
+
148
+ # Number of gradient accumulation steps
149
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
150
+ gradient_accumulation_steps: 1
151
+
152
+ # Maximum gradient norm for clipping (helps training stability)
153
+ max_grad_norm: 1.0
154
+
155
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
156
+ optimizer_type: "adamw"
157
+
158
+ # Learning rate scheduler type
159
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
160
+ scheduler_type: "linear"
161
+
162
+ # Additional scheduler parameters (depends on scheduler_type)
163
+ scheduler_params: { }
164
+
165
+ # Enable gradient checkpointing to reduce memory usage
166
+ # Recommended for training with limited GPU memory
167
+ enable_gradient_checkpointing: true
168
+
169
+ # -----------------------------------------------------------------------------
170
+ # Acceleration Configuration
171
+ # -----------------------------------------------------------------------------
172
+ # Hardware acceleration and memory optimization settings.
173
+ acceleration:
174
+ # Mixed precision training mode
175
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
176
+ mixed_precision_mode: "bf16"
177
+
178
+ # Model quantization for reduced memory usage
179
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
180
+ quantization: null
181
+
182
+ # Load text encoder in 8-bit precision to save memory
183
+ # Useful when GPU memory is limited
184
+ load_text_encoder_in_8bit: false
185
+
186
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
187
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
188
+ # LoRA). No effect under FSDP (sharded state).
189
+ offload_optimizer_during_validation: false
190
+
191
+
192
+ # -----------------------------------------------------------------------------
193
+ # Data Configuration
194
+ # -----------------------------------------------------------------------------
195
+ # Specifies the training data location and loading parameters.
196
+ data:
197
+ # Root directory containing preprocessed training data
198
+ # Should contain: latents/, conditions/, and video_masks/
199
+ preprocessed_data_root: "/path/to/preprocessed/data"
200
+
201
+ # Number of worker processes for data loading
202
+ # Used for parallel data loading to speed up data loading
203
+ num_dataloader_workers: 2
204
+
205
+ # -----------------------------------------------------------------------------
206
+ # Validation Configuration
207
+ # -----------------------------------------------------------------------------
208
+ # Controls validation sampling during training.
209
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
210
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
211
+ validation:
212
+ # Validation samples — each sample describes a self-contained generation request.
213
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
214
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
215
+ samples:
216
+ - prompt: >-
217
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
218
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
219
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
220
+ ceramic mug, then continues working with focused concentration.
221
+ conditions:
222
+ - type: mask
223
+ video: "/path/to/inpainting_video_1.mp4"
224
+ mask: "/path/to/inpainting_mask_1.mp4"
225
+ - prompt: >-
226
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
227
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
228
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
229
+ various pots simmer on the stove behind him.
230
+ conditions:
231
+ - type: mask
232
+ video: "/path/to/inpainting_video_2.mp4"
233
+ mask: "/path/to/inpainting_mask_2.mp4"
234
+
235
+ # Negative prompt to avoid unwanted artifacts
236
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
237
+
238
+ # Output video dimensions [width, height, frames]
239
+ # Width and height must be divisible by 32
240
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
241
+ video_dims: [ 576, 576, 89 ]
242
+
243
+ # Frame rate for generated videos
244
+ frame_rate: 25.0
245
+
246
+ # Random seed for reproducible validation outputs
247
+ seed: 42
248
+
249
+ # Number of denoising steps for validation inference
250
+ # Higher values = better quality but slower generation
251
+ inference_steps: 30
252
+
253
+ # Generate validation videos every N training steps
254
+ # Set to null to disable validation during training
255
+ interval: 100
256
+
257
+ # Classifier-free guidance scale
258
+ # Higher values = stronger adherence to prompt but may introduce artifacts
259
+ guidance_scale: 4.0
260
+
261
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
262
+ # STG is combined with CFG for better temporal coherence
263
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
264
+ stg_blocks: [29] # Recommended: single block 29
265
+ stg_mode: "stg_v" # "stg_v" for video-only validation
266
+
267
+ # Video inpainting trains video only; do not generate validation audio.
268
+ generate_audio: false
269
+
270
+ # Skip validation at the beginning of training (step 0)
271
+ skip_initial_validation: false
272
+
273
+ # -----------------------------------------------------------------------------
274
+ # Checkpoint Configuration
275
+ # -----------------------------------------------------------------------------
276
+ # Controls model checkpoint saving during training.
277
+ checkpoints:
278
+ # Save a checkpoint every N steps
279
+ # Set to null to disable intermediate checkpoints
280
+ interval: 250
281
+
282
+ # Number of most recent checkpoints to keep
283
+ # Set to -1 to keep all checkpoints
284
+ keep_last_n: -1
285
+
286
+ # Precision to use when saving checkpoint weights
287
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
288
+ precision: "bfloat16"
289
+
290
+ # -----------------------------------------------------------------------------
291
+ # Flow Matching Configuration
292
+ # -----------------------------------------------------------------------------
293
+ # Parameters for the flow matching training objective.
294
+ flow_matching:
295
+ # Timestep sampling mode
296
+ # "shifted_logit_normal" is recommended for LTX-2 models
297
+ timestep_sampling_mode: "shifted_logit_normal"
298
+
299
+ # Additional parameters for timestep sampling
300
+ timestep_sampling_params: { }
301
+
302
+ # -----------------------------------------------------------------------------
303
+ # Hugging Face Hub Configuration
304
+ # -----------------------------------------------------------------------------
305
+ # Settings for uploading trained models to the Hugging Face Hub.
306
+ hub:
307
+ # Whether to push the trained model to the Hub
308
+ push_to_hub: false
309
+
310
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
311
+ # Required if push_to_hub is true
312
+ hub_model_id: null
313
+
314
+ # -----------------------------------------------------------------------------
315
+ # Weights & Biases Configuration
316
+ # -----------------------------------------------------------------------------
317
+ # Settings for experiment tracking with W&B.
318
+ wandb:
319
+ # Enable W&B logging
320
+ enabled: false
321
+
322
+ # W&B project name
323
+ project: "ltx-2-trainer"
324
+
325
+ # W&B username or team (null uses default account)
326
+ entity: null
327
+
328
+ # Tags to help organize runs
329
+ tags: [ "ltx2", "lora", "inpainting" ]
330
+
331
+ # Log validation outputs to W&B
332
+ log_validation_videos: true
333
+
334
+ # -----------------------------------------------------------------------------
335
+ # General Configuration
336
+ # -----------------------------------------------------------------------------
337
+ # Global settings for the training run.
338
+
339
+ # Random seed for reproducibility
340
+ seed: 42
341
+
342
+ # Directory to save outputs (checkpoints, validation videos, logs)
343
+ output_dir: "outputs/video_inpainting_lora"
packages/ltx-trainer/configs/video_outpainting_lora.yaml ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Video Outpainting LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # video outpainting (spatial extension). The model learns to generate content
7
+ # surrounding a known rectangular region of the video.
8
+ #
9
+ # Spatial crop conditioning works by providing a rectangular pixel region as
10
+ # clean conditioning during training — those tokens receive no noise,
11
+ # timestep=0, and are excluded from the loss. The model learns to generate
12
+ # the content outside the given region, seamlessly extending the scene.
13
+ #
14
+ # Use this configuration when you want to:
15
+ # - Extend video content beyond its original boundaries
16
+ # - Train spatial outpainting capabilities on custom datasets
17
+ # - Create video aspect ratio conversion models
18
+ #
19
+ # Dataset structure:
20
+ # preprocessed_data_root/
21
+ # ├── latents/ # Video latents (VAE-encoded videos)
22
+ # └── conditions/ # Text embeddings for each video
23
+ #
24
+ # =============================================================================
25
+
26
+ # -----------------------------------------------------------------------------
27
+ # Model Configuration
28
+ # -----------------------------------------------------------------------------
29
+ # Specifies the base model to fine-tune and the training mode.
30
+ model:
31
+ # Path to the LTX-2 model checkpoint (.safetensors file)
32
+ # This should be a local path to your downloaded model
33
+ model_path: "path/to/ltx-2-model.safetensors"
34
+
35
+ # Path to the text encoder model directory
36
+ # For LTX-2, this is typically the Gemma-based text encoder
37
+ text_encoder_path: "path/to/gemma-text-encoder"
38
+
39
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
40
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
41
+ training_mode: "lora"
42
+
43
+ # Optional: Path to resume training from a checkpoint
44
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
45
+ load_checkpoint: null
46
+
47
+ # -----------------------------------------------------------------------------
48
+ # LoRA Configuration
49
+ # -----------------------------------------------------------------------------
50
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
51
+ lora:
52
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
53
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
54
+ rank: 32
55
+
56
+ # Alpha scaling factor (usually set equal to rank)
57
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
58
+ alpha: 32
59
+
60
+ # Dropout probability for LoRA layers (0.0 = no dropout)
61
+ # Can help with regularization if overfitting occurs
62
+ dropout: 0.0
63
+
64
+ # Which transformer modules to apply LoRA to
65
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
66
+ #
67
+ # VIDEO MODULES:
68
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
69
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
70
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
71
+ #
72
+ # AUDIO MODULES:
73
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
74
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
75
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
76
+ #
77
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
78
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
79
+ # (Q from video, K/V from audio - allows video to attend to audio features)
80
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
81
+ # (Q from audio, K/V from video - allows audio to attend to video features)
82
+ #
83
+ # This is a video-only config, so explicitly target video attention modules.
84
+ target_modules:
85
+ - "attn1.to_k"
86
+ - "attn1.to_q"
87
+ - "attn1.to_v"
88
+ - "attn1.to_out.0"
89
+ - "attn2.to_k"
90
+ - "attn2.to_q"
91
+ - "attn2.to_v"
92
+ - "attn2.to_out.0"
93
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
94
+ # - "ff.net.0.proj"
95
+ # - "ff.net.2"
96
+
97
+ # -----------------------------------------------------------------------------
98
+ # Training Strategy Configuration
99
+ # -----------------------------------------------------------------------------
100
+ # Defines the video outpainting training approach using the unified flexible
101
+ # strategy. Spatial crop conditioning provides a rectangular pixel region as
102
+ # clean conditioning, teaching the model to generate surrounding content.
103
+ training_strategy:
104
+ # Strategy name: "flexible" for the unified conditioning framework
105
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
106
+ # modality-specific configuration blocks.
107
+ name: "flexible"
108
+
109
+ # Video modality configuration
110
+ video:
111
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
112
+ is_generated: true
113
+ # Directory name (within preprocessed_data_root) containing video latents
114
+ latents_dir: "latents"
115
+
116
+ # Conditions applied to the video modality during training
117
+ # Spatial crop conditioning provides a rectangular region as clean context;
118
+ # the model learns to generate the surrounding video tokens.
119
+ conditions:
120
+ - type: spatial_crop
121
+ # Rectangular pixel region provided as clean conditioning (y1, x1, y2, x2)
122
+ # Pixels within this region are conditioning (clean, timestep=0, no loss)
123
+ # Pixels outside this region are generated by the model
124
+ # Coordinates are in pixel space — automatically converted to latent space
125
+ spatial_region: [0, 0, 288, 576]
126
+ # Probability of applying spatial crop conditioning per training sample
127
+ # At 1.0, all training samples use outpainting mode
128
+ probability: 1.0
129
+
130
+ # -----------------------------------------------------------------------------
131
+ # Optimization Configuration
132
+ # -----------------------------------------------------------------------------
133
+ # Controls the training optimization parameters.
134
+ optimization:
135
+ # Learning rate for the optimizer
136
+ # Typical range for LoRA: 1e-5 to 1e-4
137
+ learning_rate: 1e-4
138
+
139
+ # Total number of training steps
140
+ steps: 2000
141
+
142
+ # Batch size per GPU
143
+ # Reduce if running out of memory
144
+ batch_size: 1
145
+
146
+ # Number of gradient accumulation steps
147
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
148
+ gradient_accumulation_steps: 1
149
+
150
+ # Maximum gradient norm for clipping (helps training stability)
151
+ max_grad_norm: 1.0
152
+
153
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
154
+ optimizer_type: "adamw"
155
+
156
+ # Learning rate scheduler type
157
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
158
+ scheduler_type: "linear"
159
+
160
+ # Additional scheduler parameters (depends on scheduler_type)
161
+ scheduler_params: { }
162
+
163
+ # Enable gradient checkpointing to reduce memory usage
164
+ # Recommended for training with limited GPU memory
165
+ enable_gradient_checkpointing: true
166
+
167
+ # -----------------------------------------------------------------------------
168
+ # Acceleration Configuration
169
+ # -----------------------------------------------------------------------------
170
+ # Hardware acceleration and memory optimization settings.
171
+ acceleration:
172
+ # Mixed precision training mode
173
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
174
+ mixed_precision_mode: "bf16"
175
+
176
+ # Model quantization for reduced memory usage
177
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
178
+ quantization: null
179
+
180
+ # Load text encoder in 8-bit precision to save memory
181
+ # Useful when GPU memory is limited
182
+ load_text_encoder_in_8bit: false
183
+
184
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
185
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
186
+ # LoRA). No effect under FSDP (sharded state).
187
+ offload_optimizer_during_validation: false
188
+
189
+
190
+ # -----------------------------------------------------------------------------
191
+ # Data Configuration
192
+ # -----------------------------------------------------------------------------
193
+ # Specifies the training data location and loading parameters.
194
+ data:
195
+ # Root directory containing preprocessed training data
196
+ # Should contain: latents/ and conditions/
197
+ preprocessed_data_root: "/path/to/preprocessed/data"
198
+
199
+ # Number of worker processes for data loading
200
+ # Used for parallel data loading to speed up data loading
201
+ num_dataloader_workers: 2
202
+
203
+ # -----------------------------------------------------------------------------
204
+ # Validation Configuration
205
+ # -----------------------------------------------------------------------------
206
+ # Controls validation sampling during training.
207
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
208
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
209
+ validation:
210
+ # Validation samples — each sample describes a self-contained generation request.
211
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
212
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
213
+ samples:
214
+ - prompt: >-
215
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
216
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
217
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
218
+ ceramic mug, then continues working with focused concentration.
219
+ conditions:
220
+ - type: spatial_crop
221
+ video: "/path/to/outpainting_video_1.mp4"
222
+ spatial_region: [0, 0, 288, 576]
223
+ - prompt: >-
224
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
225
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
226
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
227
+ various pots simmer on the stove behind him.
228
+ conditions:
229
+ - type: spatial_crop
230
+ video: "/path/to/outpainting_video_2.mp4"
231
+ spatial_region: [0, 0, 288, 576]
232
+
233
+ # Negative prompt to avoid unwanted artifacts
234
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
235
+
236
+ # Output video dimensions [width, height, frames]
237
+ # Width and height must be divisible by 32
238
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
239
+ video_dims: [ 576, 576, 89 ]
240
+
241
+ # Frame rate for generated videos
242
+ frame_rate: 25.0
243
+
244
+ # Random seed for reproducible validation outputs
245
+ seed: 42
246
+
247
+ # Number of denoising steps for validation inference
248
+ # Higher values = better quality but slower generation
249
+ inference_steps: 30
250
+
251
+ # Generate validation videos every N training steps
252
+ # Set to null to disable validation during training
253
+ interval: 100
254
+
255
+ # Classifier-free guidance scale
256
+ # Higher values = stronger adherence to prompt but may introduce artifacts
257
+ guidance_scale: 4.0
258
+
259
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
260
+ # STG is combined with CFG for better temporal coherence
261
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
262
+ stg_blocks: [29] # Recommended: single block 29
263
+ stg_mode: "stg_v" # "stg_v" for video-only validation
264
+
265
+ # Video outpainting trains video only; do not generate validation audio.
266
+ generate_audio: false
267
+
268
+ # Skip validation at the beginning of training (step 0)
269
+ skip_initial_validation: false
270
+
271
+ # -----------------------------------------------------------------------------
272
+ # Checkpoint Configuration
273
+ # -----------------------------------------------------------------------------
274
+ # Controls model checkpoint saving during training.
275
+ checkpoints:
276
+ # Save a checkpoint every N steps
277
+ # Set to null to disable intermediate checkpoints
278
+ interval: 250
279
+
280
+ # Number of most recent checkpoints to keep
281
+ # Set to -1 to keep all checkpoints
282
+ keep_last_n: -1
283
+
284
+ # Precision to use when saving checkpoint weights
285
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
286
+ precision: "bfloat16"
287
+
288
+ # -----------------------------------------------------------------------------
289
+ # Flow Matching Configuration
290
+ # -----------------------------------------------------------------------------
291
+ # Parameters for the flow matching training objective.
292
+ flow_matching:
293
+ # Timestep sampling mode
294
+ # "shifted_logit_normal" is recommended for LTX-2 models
295
+ timestep_sampling_mode: "shifted_logit_normal"
296
+
297
+ # Additional parameters for timestep sampling
298
+ timestep_sampling_params: { }
299
+
300
+ # -----------------------------------------------------------------------------
301
+ # Hugging Face Hub Configuration
302
+ # -----------------------------------------------------------------------------
303
+ # Settings for uploading trained models to the Hugging Face Hub.
304
+ hub:
305
+ # Whether to push the trained model to the Hub
306
+ push_to_hub: false
307
+
308
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
309
+ # Required if push_to_hub is true
310
+ hub_model_id: null
311
+
312
+ # -----------------------------------------------------------------------------
313
+ # Weights & Biases Configuration
314
+ # -----------------------------------------------------------------------------
315
+ # Settings for experiment tracking with W&B.
316
+ wandb:
317
+ # Enable W&B logging
318
+ enabled: false
319
+
320
+ # W&B project name
321
+ project: "ltx-2-trainer"
322
+
323
+ # W&B username or team (null uses default account)
324
+ entity: null
325
+
326
+ # Tags to help organize runs
327
+ tags: [ "ltx2", "lora", "outpainting", "spatial-crop" ]
328
+
329
+ # Log validation outputs to W&B
330
+ log_validation_videos: true
331
+
332
+ # -----------------------------------------------------------------------------
333
+ # General Configuration
334
+ # -----------------------------------------------------------------------------
335
+ # Global settings for the training run.
336
+
337
+ # Random seed for reproducibility
338
+ seed: 42
339
+
340
+ # Directory to save outputs (checkpoints, validation videos, logs)
341
+ output_dir: "outputs/video_outpainting_lora"
packages/ltx-trainer/configs/video_suffix_lora.yaml ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # LTX-2 Video Backward Extension (Suffix) LoRA Training Configuration
3
+ # =============================================================================
4
+ #
5
+ # This configuration is for training LoRA adapters on the LTX-2 model for
6
+ # backward video extension. The model learns to generate video content that
7
+ # leads into a given suffix of existing frames.
8
+ #
9
+ # Suffix conditioning works by providing the last N temporal units as clean
10
+ # conditioning signals during training — they receive no noise, timestep=0,
11
+ # and are excluded from the loss. The model learns to generate preceding
12
+ # frames that seamlessly lead into the given suffix.
13
+ #
14
+ # Use this configuration when you want to:
15
+ # - Train the model to generate "prequel" content for existing videos
16
+ # - Fine-tune backward temporal continuation capabilities
17
+ # - Create video backward extension models with optional audio
18
+ #
19
+ # Dataset structure:
20
+ # preprocessed_data_root/
21
+ # ├── latents/ # Video latents (VAE-encoded videos)
22
+ # ├── conditions/ # Text embeddings for each video
23
+ # └── audio_latents/ # Audio latents (VAE-encoded audio)
24
+ #
25
+ # =============================================================================
26
+
27
+ # -----------------------------------------------------------------------------
28
+ # Model Configuration
29
+ # -----------------------------------------------------------------------------
30
+ # Specifies the base model to fine-tune and the training mode.
31
+ model:
32
+ # Path to the LTX-2 model checkpoint (.safetensors file)
33
+ # This should be a local path to your downloaded model
34
+ model_path: "path/to/ltx-2-model.safetensors"
35
+
36
+ # Path to the text encoder model directory
37
+ # For LTX-2, this is typically the Gemma-based text encoder
38
+ text_encoder_path: "path/to/gemma-text-encoder"
39
+
40
+ # Training mode: "lora" for efficient adapter training, "full" for full fine-tuning
41
+ # LoRA is recommended for most use cases (faster, less memory, prevents overfitting)
42
+ training_mode: "lora"
43
+
44
+ # Optional: Path to resume training from a checkpoint
45
+ # Can be a checkpoint file (.safetensors) or directory (uses latest checkpoint)
46
+ load_checkpoint: null
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # LoRA Configuration
50
+ # -----------------------------------------------------------------------------
51
+ # Controls the Low-Rank Adaptation parameters for efficient fine-tuning.
52
+ lora:
53
+ # Rank of the LoRA matrices (higher = more capacity but more parameters)
54
+ # Typical values: 8, 16, 32, 64. Start with 32 for general fine-tuning.
55
+ rank: 32
56
+
57
+ # Alpha scaling factor (usually set equal to rank)
58
+ # The effective scaling is alpha/rank, so alpha=rank means scaling of 1.0
59
+ alpha: 32
60
+
61
+ # Dropout probability for LoRA layers (0.0 = no dropout)
62
+ # Can help with regularization if overfitting occurs
63
+ dropout: 0.0
64
+
65
+ # Which transformer modules to apply LoRA to
66
+ # The LTX-2 transformer has separate attention and FFN blocks for video and audio:
67
+ #
68
+ # VIDEO MODULES:
69
+ # - attn1.to_k, attn1.to_q, attn1.to_v, attn1.to_out.0 (video self-attention)
70
+ # - attn2.to_k, attn2.to_q, attn2.to_v, attn2.to_out.0 (video cross-attention to text)
71
+ # - ff.net.0.proj, ff.net.2 (video feed-forward)
72
+ #
73
+ # AUDIO MODULES:
74
+ # - audio_attn1.to_k, audio_attn1.to_q, audio_attn1.to_v, audio_attn1.to_out.0 (audio self-attention)
75
+ # - audio_attn2.to_k, audio_attn2.to_q, audio_attn2.to_v, audio_attn2.to_out.0 (audio cross-attention to text)
76
+ # - audio_ff.net.0.proj, audio_ff.net.2 (audio feed-forward)
77
+ #
78
+ # AUDIO-VIDEO CROSS-ATTENTION MODULES (for cross-modal interaction):
79
+ # - audio_to_video_attn.to_k, audio_to_video_attn.to_q, audio_to_video_attn.to_v, audio_to_video_attn.to_out.0
80
+ # (Q from video, K/V from audio - allows video to attend to audio features)
81
+ # - video_to_audio_attn.to_k, video_to_audio_attn.to_q, video_to_audio_attn.to_v, video_to_audio_attn.to_out.0
82
+ # (Q from audio, K/V from video - allows audio to attend to video features)
83
+ #
84
+ # Using short patterns like "to_k" matches ALL attention modules (video, audio, and cross-modal).
85
+ # For audio-video training, this is the recommended approach.
86
+ target_modules:
87
+ # Attention layers (matches both video and audio branches)
88
+ - "to_k"
89
+ - "to_q"
90
+ - "to_v"
91
+ - "to_out.0"
92
+ # Uncomment below to also train feed-forward layers (can increase the LoRA's capacity):
93
+ # - "ff.net.0.proj"
94
+ # - "ff.net.2"
95
+ # - "audio_ff.net.0.proj"
96
+ # - "audio_ff.net.2"
97
+
98
+ # -----------------------------------------------------------------------------
99
+ # Training Strategy Configuration
100
+ # -----------------------------------------------------------------------------
101
+ # Defines the backward video extension training approach using the unified
102
+ # flexible strategy. Suffix conditioning provides the last N latent frames as
103
+ # clean conditioning, teaching the model to generate content leading into them.
104
+ training_strategy:
105
+ # Strategy name: "flexible" for the unified conditioning framework
106
+ # Supports all training modes (T2V, I2V, V2V, A2V, V2A, etc.) through
107
+ # modality-specific configuration blocks.
108
+ name: "flexible"
109
+
110
+ # Video modality configuration
111
+ video:
112
+ # Whether the model generates video (true) or uses it as frozen conditioning (false)
113
+ is_generated: true
114
+ # Directory name (within preprocessed_data_root) containing video latents
115
+ latents_dir: "latents"
116
+
117
+ # Conditions applied to the video modality during training
118
+ # Suffix conditioning: the last N temporal units are provided as clean
119
+ # conditioning signals (no noise, timestep=0, excluded from loss)
120
+ conditions:
121
+ - type: suffix
122
+ # Number of latent frames to use as conditioning suffix
123
+ # For suffix conditioning, N latent frames correspond to N * 8 pixel frames.
124
+ # temporal_boundary=8 means the last 64 pixel frames are used as suffix.
125
+ temporal_boundary: 8
126
+ # Probability of applying suffix conditioning per training sample
127
+ # At 1.0, all training samples use backward extension mode
128
+ probability: 1.0
129
+
130
+ # Audio modality configuration
131
+ audio:
132
+ # Whether the model generates audio (true) or uses it as frozen conditioning (false)
133
+ is_generated: true
134
+ # Directory name (within preprocessed_data_root) containing audio latents
135
+ latents_dir: "audio_latents"
136
+
137
+ # -----------------------------------------------------------------------------
138
+ # Optimization Configuration
139
+ # -----------------------------------------------------------------------------
140
+ # Controls the training optimization parameters.
141
+ optimization:
142
+ # Learning rate for the optimizer
143
+ # Typical range for LoRA: 1e-5 to 1e-4
144
+ learning_rate: 1e-4
145
+
146
+ # Total number of training steps
147
+ steps: 2000
148
+
149
+ # Batch size per GPU
150
+ # Reduce if running out of memory
151
+ batch_size: 1
152
+
153
+ # Number of gradient accumulation steps
154
+ # Effective batch size = batch_size * gradient_accumulation_steps * num_gpus
155
+ gradient_accumulation_steps: 1
156
+
157
+ # Maximum gradient norm for clipping (helps training stability)
158
+ max_grad_norm: 1.0
159
+
160
+ # Optimizer type: "adamw" (standard) or "adamw8bit" (memory-efficient)
161
+ optimizer_type: "adamw"
162
+
163
+ # Learning rate scheduler type
164
+ # Options: "constant", "linear", "cosine", "cosine_with_restarts", "polynomial"
165
+ scheduler_type: "linear"
166
+
167
+ # Additional scheduler parameters (depends on scheduler_type)
168
+ scheduler_params: { }
169
+
170
+ # Enable gradient checkpointing to reduce memory usage
171
+ # Recommended for training with limited GPU memory
172
+ enable_gradient_checkpointing: true
173
+
174
+ # -----------------------------------------------------------------------------
175
+ # Acceleration Configuration
176
+ # -----------------------------------------------------------------------------
177
+ # Hardware acceleration and memory optimization settings.
178
+ acceleration:
179
+ # Mixed precision training mode
180
+ # Options: "no" (fp32), "fp16" (half precision), "bf16" (bfloat16, recommended)
181
+ mixed_precision_mode: "bf16"
182
+
183
+ # Model quantization for reduced memory usage
184
+ # Options: null (none), "int8-quanto", "int4-quanto", "int2-quanto", "fp8-quanto", "fp8uz-quanto"
185
+ quantization: null
186
+
187
+ # Load text encoder in 8-bit precision to save memory
188
+ # Useful when GPU memory is limited
189
+ load_text_encoder_in_8bit: false
190
+
191
+ # Offload optimizer state to CPU during validation video sampling and restore it after.
192
+ # Frees VRAM for the VAE decoder when optimizer state is large (full fine-tune, high-rank
193
+ # LoRA). No effect under FSDP (sharded state).
194
+ offload_optimizer_during_validation: false
195
+
196
+
197
+ # -----------------------------------------------------------------------------
198
+ # Data Configuration
199
+ # -----------------------------------------------------------------------------
200
+ # Specifies the training data location and loading parameters.
201
+ data:
202
+ # Root directory containing preprocessed training data
203
+ # Should contain: latents/, conditions/, and audio_latents/
204
+ preprocessed_data_root: "/path/to/preprocessed/data"
205
+
206
+ # Number of worker processes for data loading
207
+ # Used for parallel data loading to speed up data loading
208
+ num_dataloader_workers: 2
209
+
210
+ # -----------------------------------------------------------------------------
211
+ # Validation Configuration
212
+ # -----------------------------------------------------------------------------
213
+ # Controls validation sampling during training.
214
+ # NOTE: Validation sampling use simplified inference pipelines and prioritizes speed over
215
+ # maximum quality. For production-quality inference, use `packages/ltx-pipelines`.
216
+ validation:
217
+ # Validation samples — each sample describes a self-contained generation request.
218
+ # Use 'conditions' to add conditioning (first_frame, prefix, suffix, reference, video_to_audio, etc.)
219
+ # See docs/configuration-reference.md#validation-condition-types for the full list of condition types.
220
+ samples:
221
+ - prompt: >-
222
+ A woman with long brown hair sits at a wooden desk in a cozy home office, typing on a
223
+ laptop while occasionally glancing at notes beside her. Soft natural light streams through
224
+ a large window, casting warm shadows across the room. She pauses to take a sip from a
225
+ ceramic mug, then continues working with focused concentration. The audio captures the
226
+ gentle clicking of keyboard keys, the soft rustle of papers, and ambient room tone with
227
+ occasional distant bird chirps from outside.
228
+ conditions:
229
+ - type: suffix
230
+ video: "/path/to/suffix_video_1.mp4"
231
+ # Matches temporal_boundary=8 during training: 8 * 8 = 64 frames
232
+ num_frames: 64
233
+ - prompt: >-
234
+ A chef in a white uniform stands in a professional kitchen, carefully plating a gourmet
235
+ dish with precise movements. Steam rises from freshly cooked vegetables as he arranges
236
+ them with tweezers. The stainless steel surfaces gleam under bright overhead lights, and
237
+ various pots simmer on the stove behind him. The audio features the sizzling of pans,
238
+ the clinking of utensils against plates, and the ambient hum of kitchen ventilation.
239
+ conditions:
240
+ - type: suffix
241
+ video: "/path/to/suffix_video_2.mp4"
242
+ # Matches temporal_boundary=8 during training: 8 * 8 = 64 frames
243
+ num_frames: 64
244
+
245
+ # Negative prompt to avoid unwanted artifacts
246
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted"
247
+
248
+ # Output video dimensions [width, height, frames]
249
+ # Width and height must be divisible by 32
250
+ # Frames must satisfy: frames % 8 == 1 (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, ...)
251
+ video_dims: [ 576, 576, 89 ]
252
+
253
+ # Frame rate for generated videos
254
+ frame_rate: 25.0
255
+
256
+ # Random seed for reproducible validation outputs
257
+ seed: 42
258
+
259
+ # Number of denoising steps for validation inference
260
+ # Higher values = better quality but slower generation
261
+ inference_steps: 30
262
+
263
+ # Generate validation videos every N training steps
264
+ # Set to null to disable validation during training
265
+ interval: 100
266
+
267
+ # Classifier-free guidance scale
268
+ # Higher values = stronger adherence to prompt but may introduce artifacts
269
+ guidance_scale: 4.0
270
+
271
+ # STG (Spatio-Temporal Guidance) parameters for improved video quality
272
+ # STG is combined with CFG for better temporal coherence
273
+ stg_scale: 1.0 # Recommended: 1.0 (0.0 disables STG)
274
+ stg_blocks: [29] # Recommended: single block 29
275
+ stg_mode: "stg_av" # "stg_av" perturbs both audio and video, "stg_v" video only
276
+
277
+ # Whether to generate audio in validation samples
278
+ # Independent of training_strategy.audio.is_generated - you can generate audio
279
+ # in validation even when not training the audio branch
280
+ generate_audio: true
281
+
282
+ # Skip validation at the beginning of training (step 0)
283
+ skip_initial_validation: false
284
+
285
+ # -----------------------------------------------------------------------------
286
+ # Checkpoint Configuration
287
+ # -----------------------------------------------------------------------------
288
+ # Controls model checkpoint saving during training.
289
+ checkpoints:
290
+ # Save a checkpoint every N steps
291
+ # Set to null to disable intermediate checkpoints
292
+ interval: 250
293
+
294
+ # Number of most recent checkpoints to keep
295
+ # Set to -1 to keep all checkpoints
296
+ keep_last_n: -1
297
+
298
+ # Precision to use when saving checkpoint weights
299
+ # Options: "bfloat16" (default, smaller files) or "float32" (full precision)
300
+ precision: "bfloat16"
301
+
302
+ # -----------------------------------------------------------------------------
303
+ # Flow Matching Configuration
304
+ # -----------------------------------------------------------------------------
305
+ # Parameters for the flow matching training objective.
306
+ flow_matching:
307
+ # Timestep sampling mode
308
+ # "shifted_logit_normal" is recommended for LTX-2 models
309
+ timestep_sampling_mode: "shifted_logit_normal"
310
+
311
+ # Additional parameters for timestep sampling
312
+ timestep_sampling_params: { }
313
+
314
+ # -----------------------------------------------------------------------------
315
+ # Hugging Face Hub Configuration
316
+ # -----------------------------------------------------------------------------
317
+ # Settings for uploading trained models to the Hugging Face Hub.
318
+ hub:
319
+ # Whether to push the trained model to the Hub
320
+ push_to_hub: false
321
+
322
+ # Repository ID on Hugging Face Hub (e.g., "username/my-lora-model")
323
+ # Required if push_to_hub is true
324
+ hub_model_id: null
325
+
326
+ # -----------------------------------------------------------------------------
327
+ # Weights & Biases Configuration
328
+ # -----------------------------------------------------------------------------
329
+ # Settings for experiment tracking with W&B.
330
+ wandb:
331
+ # Enable W&B logging
332
+ enabled: false
333
+
334
+ # W&B project name
335
+ project: "ltx-2-trainer"
336
+
337
+ # W&B username or team (null uses default account)
338
+ entity: null
339
+
340
+ # Tags to help organize runs
341
+ tags: [ "ltx2", "lora", "video-suffix", "backward-extension" ]
342
+
343
+ # Log validation media (video/audio) to W&B
344
+ log_validation_videos: true
345
+
346
+ # -----------------------------------------------------------------------------
347
+ # General Configuration
348
+ # -----------------------------------------------------------------------------
349
+ # Global settings for the training run.
350
+
351
+ # Random seed for reproducibility
352
+ seed: 42
353
+
354
+ # Directory to save outputs (checkpoints, validation videos, logs)
355
+ output_dir: "outputs/video_suffix_lora"
packages/ltx-trainer/docs/configuration-reference.md ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration Reference
2
+
3
+ The trainer uses structured Pydantic models for configuration, making it easy to customize training parameters.
4
+ This guide covers all available configuration options and their usage.
5
+
6
+ ## 📋 Overview
7
+
8
+ The main configuration class is [`LtxTrainerConfig`](../src/ltx_trainer/config.py), which includes the following
9
+ sub-configurations:
10
+
11
+ - **ModelConfig**: Base model and training mode settings
12
+ - **LoraConfig**: LoRA training parameters
13
+ - **TrainingStrategyConfig**: Training strategy settings (flexible conditioning framework)
14
+ - **OptimizationConfig**: Learning rate, batch sizes, and scheduler settings
15
+ - **AccelerationConfig**: Mixed precision and quantization settings
16
+ - **DataConfig**: Data loading parameters
17
+ - **ValidationConfig**: Validation and inference settings
18
+ - **CheckpointsConfig**: Checkpoint saving frequency and retention settings
19
+ - **HubConfig**: Hugging Face Hub integration settings
20
+ - **WandbConfig**: Weights & Biases logging settings
21
+ - **FlowMatchingConfig**: Timestep sampling parameters
22
+
23
+ ## 📄 Example Configuration Files
24
+
25
+ Check out our example configurations in the `configs` directory:
26
+
27
+ - 📄 [Text-to-Video LoRA](../configs/t2v_lora.yaml) - Text-to-video LoRA training
28
+ - 📄 [Image-to-Video LoRA](../configs/i2v_lora.yaml) - Image-to-video LoRA training
29
+ - 📄 [IC-LoRA Video-to-Video](../configs/v2v_ic_lora.yaml) - IC-LoRA video-to-video training
30
+ - 📄 [Audio-to-Video LoRA](../configs/a2v_lora.yaml) - Audio-to-video LoRA training
31
+ - 📄 [Video-to-Audio LoRA](../configs/v2a_lora.yaml) - Video-to-audio (Foley) LoRA training
32
+ - 📄 [Video Extension LoRA](../configs/video_extend_lora.yaml) - Video extension (forward) LoRA training
33
+ - 📄 [Video Suffix LoRA](../configs/video_suffix_lora.yaml) - Video extension (backward) LoRA training
34
+ - 📄 [Video Inpainting LoRA](../configs/video_inpainting_lora.yaml) - Video inpainting LoRA training
35
+ - 📄 [Video Outpainting LoRA](../configs/video_outpainting_lora.yaml) - Video outpainting (spatial crop) LoRA training
36
+ - 📄 [Text-to-Audio LoRA](../configs/t2a_lora.yaml) - Text-to-audio LoRA training
37
+ - 📄 [Audio Extension LoRA](../configs/audio_extend_lora.yaml) - Audio extension (forward) LoRA training
38
+ - 📄 [Audio Suffix LoRA](../configs/audio_suffix_lora.yaml) - Audio extension (backward) LoRA training
39
+ - 📄 [Audio Inpainting LoRA](../configs/audio_inpainting_lora.yaml) - Audio inpainting LoRA training
40
+ - 📄 [Audio-to-Audio IC-LoRA](../configs/a2a_ic_lora.yaml) - Audio IC-LoRA transformation training
41
+ - 📄 [AV2AV IC-LoRA](../configs/av2av_ic_lora.yaml) - Audio+video IC-LoRA transformation training
42
+ - 📄 [T2V LoRA (Low VRAM)](../configs/t2v_lora_low_vram.yaml) - Memory-optimized config for 32GB GPUs
43
+
44
+ ## ⚙️ Configuration Sections
45
+
46
+ > [!NOTE]
47
+ > The YAML snippets below show **recommended starting values**, not necessarily the code defaults.
48
+ > Fields you omit from your config file will use the code defaults from [`config.py`](../src/ltx_trainer/config.py).
49
+
50
+ ### ModelConfig
51
+
52
+ Controls the base model and training mode settings.
53
+
54
+ ```yaml
55
+ model:
56
+ model_path: "/path/to/ltx-2-model.safetensors" # Local path to model checkpoint
57
+ text_encoder_path: "/path/to/gemma-model" # Path to Gemma text encoder directory
58
+ training_mode: "lora" # "lora" or "full"
59
+ load_checkpoint: null # Path to checkpoint to resume from
60
+ ```
61
+
62
+ **Key parameters:**
63
+
64
+ | Parameter | Description |
65
+ |---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
66
+ | `model_path` | **Required.** Local path to the LTX-2 model checkpoint (`.safetensors` file). URLs are not supported. |
67
+ | `text_encoder_path` | **Required.** Path to the Gemma text encoder model directory. Download from [HuggingFace](https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized/). |
68
+ | `training_mode` | Training approach - `"lora"` for LoRA training or `"full"` for full-rank fine-tuning. |
69
+ | `load_checkpoint` | Optional path to resume training from a checkpoint file or directory. |
70
+
71
+ > [!NOTE]
72
+ > LTX-2 requires both a model checkpoint and a Gemma text encoder. Both must be local paths.
73
+
74
+ ### LoraConfig
75
+
76
+ LoRA-specific fine-tuning parameters (only used when `training_mode: "lora"`).
77
+
78
+ ```yaml
79
+ lora:
80
+ rank: 32 # LoRA rank (higher = more parameters)
81
+ alpha: 32 # LoRA alpha scaling factor
82
+ dropout: 0.0 # Dropout probability (0.0-1.0)
83
+ target_modules: # Modules to apply LoRA to
84
+ - "to_k"
85
+ - "to_q"
86
+ - "to_v"
87
+ - "to_out.0"
88
+ ```
89
+
90
+ **Key parameters:**
91
+
92
+ | Parameter | Description |
93
+ |------------------|---------------------------------------------------------------------------------|
94
+ | `rank` | LoRA rank - higher values mean more trainable parameters (typical range: 8-128) |
95
+ | `alpha` | Alpha scaling factor - typically set equal to rank |
96
+ | `dropout` | Dropout probability for regularization |
97
+ | `target_modules` | List of transformer modules to apply LoRA adapters to (see below) |
98
+
99
+ #### Understanding Target Modules
100
+
101
+ The LTX-2 transformer has separate attention and feed-forward blocks for video and audio, as well as cross-attention
102
+ modules that enable the two modalities to exchange information. Choosing the right `target_modules` is critical for
103
+ achieving good results, especially when training with audio.
104
+
105
+ **Video-only modules:**
106
+
107
+ | Module Pattern | Description |
108
+ |------------------------------------------------------------|---------------------------------|
109
+ | `attn1.to_k`, `attn1.to_q`, `attn1.to_v`, `attn1.to_out.0` | Video self-attention |
110
+ | `attn2.to_k`, `attn2.to_q`, `attn2.to_v`, `attn2.to_out.0` | Video cross-attention (to text) |
111
+ | `ff.net.0.proj`, `ff.net.2` | Video feed-forward network |
112
+
113
+ **Audio-only modules:**
114
+
115
+ | Module Pattern | Description |
116
+ |------------------------------------------------------------------------------------|---------------------------------|
117
+ | `audio_attn1.to_k`, `audio_attn1.to_q`, `audio_attn1.to_v`, `audio_attn1.to_out.0` | Audio self-attention |
118
+ | `audio_attn2.to_k`, `audio_attn2.to_q`, `audio_attn2.to_v`, `audio_attn2.to_out.0` | Audio cross-attention (to text) |
119
+ | `audio_ff.net.0.proj`, `audio_ff.net.2` | Audio feed-forward network |
120
+
121
+ **Audio-video cross-attention modules:**
122
+
123
+ These modules enable bidirectional information flow between the audio and video modalities:
124
+
125
+ | Module Pattern | Description |
126
+ |--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|
127
+ | `audio_to_video_attn.to_k`, `audio_to_video_attn.to_q`, `audio_to_video_attn.to_v`, `audio_to_video_attn.to_out.0` | Video attends to audio (Q from video, K/V from audio) |
128
+ | `video_to_audio_attn.to_k`, `video_to_audio_attn.to_q`, `video_to_audio_attn.to_v`, `video_to_audio_attn.to_out.0` | Audio attends to video (Q from audio, K/V from video) |
129
+
130
+ **Recommended configurations:**
131
+
132
+ For **video-only training**, target the video attention layers:
133
+
134
+ ```yaml
135
+ target_modules:
136
+ - "attn1.to_k"
137
+ - "attn1.to_q"
138
+ - "attn1.to_v"
139
+ - "attn1.to_out.0"
140
+ - "attn2.to_k"
141
+ - "attn2.to_q"
142
+ - "attn2.to_v"
143
+ - "attn2.to_out.0"
144
+ ```
145
+
146
+ For **audio-video training**, use patterns that match both branches:
147
+
148
+ ```yaml
149
+ target_modules:
150
+ - "to_k"
151
+ - "to_q"
152
+ - "to_v"
153
+ - "to_out.0"
154
+ ```
155
+
156
+ > [!NOTE]
157
+ > Using shorter patterns like `"to_k"` will match all attention modules including `attn1.to_k`, `audio_attn1.to_k`,
158
+ > `audio_to_video_attn.to_k`, and `video_to_audio_attn.to_k`, effectively training video, audio, and cross-modal
159
+ > attention branches together.
160
+
161
+ > [!TIP]
162
+ > You can also target the feed-forward (FFN) modules (`ff.net.0.proj`, `ff.net.2` for video,
163
+ > `audio_ff.net.0.proj`, `audio_ff.net.2` for audio) to increase the LoRA's capacity and potentially
164
+ > help it capture the target distribution better.
165
+
166
+ ### TrainingStrategyConfig
167
+
168
+ Configures the training strategy. The recommended strategy is `"flexible"`, which supports all conditioning scenarios through configuration.
169
+
170
+ #### Flexible Strategy
171
+
172
+ The flexible strategy provides a unified conditioning framework. Each modality (video, audio) is configured
173
+ independently with its own latents directory, generation flag, and list of conditions.
174
+
175
+ ```yaml
176
+ training_strategy:
177
+ name: "flexible"
178
+ video:
179
+ is_generated: true # Video is denoised during training
180
+ latents_dir: "latents" # Directory containing precomputed video latents
181
+ conditions:
182
+ - type: first_frame # Use first frame as conditioning
183
+ probability: 0.5 # Apply this condition 50% of the time
184
+ audio:
185
+ is_generated: true # Audio is denoised during training
186
+ latents_dir: "audio_latents" # Directory containing precomputed audio latents
187
+ conditions: [] # No additional audio conditions (text-only)
188
+ ```
189
+
190
+ **ModalityConfig parameters:**
191
+
192
+ | Parameter | Description |
193
+ |----------------|------------------------------------------------------------------------------------------------------------------|
194
+ | `is_generated` | `true` = modality is denoised (contributes to loss). `false` = frozen conditioning (sigma=0, no loss). |
195
+ | `latents_dir` | Directory name within `preprocessed_data_root` containing precomputed latents for this modality. |
196
+ | `conditions` | List of conditioning configs applied during training (see condition types below). Text conditioning is implicit. |
197
+
198
+ **Condition types:**
199
+
200
+ | Type | Parameters | Description |
201
+ |----------------|-----------------------------------------------------|---------------------------------------------------------------------------------------|
202
+ | `first_frame` | `probability` | First latent frame is clean, excluded from loss. **Video only.** |
203
+ | `prefix` | `temporal_boundary`, `probability` | First N latent temporal units are clean. For extension forward. |
204
+ | `suffix` | `temporal_boundary`, `probability` | Last N latent temporal units are clean. For extension backward. |
205
+ | `spatial_crop` | `spatial_region` (y1, x1, y2, x2 in px), `probability` | Rectangular region is clean, excluded from loss. For outpainting. **Video only.** |
206
+ | `mask` | `mask_dir`, `probability` | Per-sample mask directory. Masks are thresholded at `0.5`; `1` means conditioning, `0` means generate. |
207
+ | `reference` | `latents_dir`, `probability` | IC-LoRA style concatenation. Reference tokens are prepended, clean (timestep=0), no loss. |
208
+
209
+ > [!NOTE]
210
+ > The `prefix`, `suffix`, `mask`, and `reference` condition types work on both video and audio modalities —
211
+ > place them in the `video.conditions` or `audio.conditions` list as appropriate.
212
+ > `first_frame` and `spatial_crop` are video-only conditions.
213
+
214
+ > [!NOTE]
215
+ > Training conditions reference **directories** of precomputed data (within `preprocessed_data_root`),
216
+ > while validation conditions reference **individual files** (images, videos, masks) that are encoded
217
+ > on-the-fly during validation. The condition `type` names are the same, but the fields differ.
218
+
219
+ > [!NOTE]
220
+ > The legacy `text_to_video` and `video_to_video` strategies are deprecated but remain forward-compatible.
221
+ > New configs should use `name: "flexible"`.
222
+
223
+ ### OptimizationConfig
224
+
225
+ Training optimization parameters including learning rates, batch sizes, and schedulers.
226
+
227
+ ```yaml
228
+ optimization:
229
+ learning_rate: 1e-4 # Learning rate
230
+ steps: 2000 # Total training steps
231
+ batch_size: 1 # Batch size per GPU
232
+ gradient_accumulation_steps: 1 # Steps to accumulate gradients
233
+ max_grad_norm: 1.0 # Gradient clipping threshold
234
+ optimizer_type: "adamw" # "adamw" or "adamw8bit"
235
+ scheduler_type: "linear" # Scheduler type
236
+ scheduler_params: { } # Additional scheduler parameters
237
+ enable_gradient_checkpointing: true # Memory optimization
238
+ ```
239
+
240
+ **Key parameters:**
241
+
242
+ | Parameter | Description |
243
+ |---------------------------------|----------------------------------------------------------------------------------------------|
244
+ | `learning_rate` | Learning rate for optimization (typical range: 1e-5 to 1e-3) |
245
+ | `steps` | Total number of training steps |
246
+ | `batch_size` | Batch size per GPU (reduce if running out of memory) |
247
+ | `gradient_accumulation_steps` | Accumulate gradients over multiple steps |
248
+ | `scheduler_type` | LR scheduler: `"constant"`, `"linear"`, `"cosine"`, `"cosine_with_restarts"`, `"polynomial"`, `"step"` |
249
+ | `enable_gradient_checkpointing` | Trade training speed for GPU memory savings (recommended for large models) |
250
+
251
+ ### AccelerationConfig
252
+
253
+ Hardware acceleration and compute optimization settings.
254
+
255
+ ```yaml
256
+ acceleration:
257
+ mixed_precision_mode: "bf16" # "no", "fp16", or "bf16"
258
+ quantization: null # Quantization options
259
+ load_text_encoder_in_8bit: false # Load text encoder in 8-bit
260
+ offload_optimizer_during_validation: false # Offload optimizer state to CPU during validation
261
+ ```
262
+
263
+ **Key parameters:**
264
+
265
+ | Parameter | Description |
266
+ |---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
267
+ | `mixed_precision_mode` | Precision mode - `"bf16"` recommended for modern GPUs |
268
+ | `quantization` | Model quantization: `null`, `"int8-quanto"`, `"int4-quanto"`, `"int2-quanto"`, `"fp8-quanto"`, or `"fp8uz-quanto"` |
269
+ | `load_text_encoder_in_8bit` | Load the Gemma text encoder in 8-bit to save GPU memory |
270
+ | `offload_optimizer_during_validation` | Move optimizer state to CPU before validation video sampling and back afterwards. Useful when validation OOMs because VAE decoder + transformer + optimizer state can't coexist on the GPU (full fine-tune, high-rank LoRA). No effect for FSDP. |
271
+
272
+ ### DataConfig
273
+
274
+ Data loading and processing configuration.
275
+
276
+ ```yaml
277
+ data:
278
+ preprocessed_data_root: "/path/to/preprocessed/data" # Path to precomputed dataset
279
+ num_dataloader_workers: 2 # Background data loading workers
280
+ ```
281
+
282
+ **Key parameters:**
283
+
284
+ | Parameter | Description |
285
+ |--------------------------|--------------------------------------------------------------------------------------------|
286
+ | `preprocessed_data_root` | Path to your preprocessed dataset directory produced by `process_dataset.py` (contains `latents/`, `conditions/`, etc.) |
287
+ | `num_dataloader_workers` | Number of parallel data loading processes (0 = synchronous loading, useful when debugging) |
288
+
289
+ ### ValidationConfig
290
+
291
+ Validation and inference settings for monitoring training progress. Validation samples use a self-describing
292
+ format where each sample specifies its own prompt and conditions.
293
+
294
+ ```yaml
295
+ validation:
296
+ samples:
297
+ - prompt: "A cat playing with a ball"
298
+ conditions:
299
+ - type: first_frame
300
+ image_or_video: "/path/to/image.png"
301
+ - prompt: "A dog running in a field"
302
+ video_dims: [576, 576, 89] # Output dimensions: [width, height, frames]
303
+ negative_prompt: "worst quality, inconsistent motion, blurry, jittery, distorted" # Negative prompt for all samples
304
+ frame_rate: 25.0 # Output video frame rate (fps)
305
+ seed: 42 # Random seed for reproducibility
306
+ inference_steps: 30 # Number of denoising steps
307
+ interval: 100 # Run validation every N steps (null to disable)
308
+ guidance_scale: 4.0 # CFG scale (higher = stronger prompt adherence)
309
+ stg_scale: 1.0 # STG scale (0.0 to disable)
310
+ stg_blocks: [29] # Transformer blocks to apply STG perturbation
311
+ stg_mode: "stg_av" # STG mode: "stg_av" (audio+video) or "stg_v" (video only)
312
+ generate_audio: true # Whether to generate audio during validation
313
+ generate_video: true # Whether to generate video during validation
314
+ skip_initial_validation: false # Skip validation at step 0
315
+ ```
316
+
317
+ **Key parameters:**
318
+
319
+ | Parameter | Description |
320
+ |--------------------------|--------------------------------------------------------------------------------------------------------------------------|
321
+ | `samples` | List of `ValidationSample` objects (see below). Replaces the legacy `prompts`/`images`/`reference_videos` fields. |
322
+ | `video_dims` | Output dimensions `[width, height, frames]`. Width/height must be divisible by 32, frames must satisfy `frames % 8 == 1` |
323
+ | `interval` | Steps between validation runs (set to `null` to disable) |
324
+ | `guidance_scale` | CFG (Classifier-Free Guidance) scale. Recommended: 4.0 |
325
+ | `stg_scale` | STG (Spatio-Temporal Guidance) scale. 0.0 disables STG. Recommended: 1.0 |
326
+ | `stg_blocks` | Transformer blocks to perturb for STG. Recommended: `[29]` (single block) |
327
+ | `stg_mode` | STG mode: `"stg_av"` perturbs both audio and video, `"stg_v"` perturbs video only |
328
+ | `generate_audio` | Whether to generate audio in validation samples |
329
+ | `generate_video` | Whether to generate video in validation samples. Set to `false` for V2A (video-to-audio) validation. Default: `true` |
330
+ | `skip_initial_validation`| Skip validation video sampling at step 0 (beginning of training) |
331
+
332
+ #### ValidationSample
333
+
334
+ Each sample in the `samples` list has:
335
+
336
+ | Field | Description |
337
+ |--------------|-------------------------------------------------------------------------------------------------|
338
+ | `prompt` | Text prompt for this validation sample. |
339
+ | `conditions` | List of validation conditions (see types below). Empty list = text-only generation. |
340
+ | `video_dims` | Optional per-sample override for `(width, height, frames)`. Inherits from `ValidationConfig` if not set. |
341
+ | `seed` | Optional per-sample override for random seed. Inherits from `ValidationConfig` if not set. |
342
+
343
+ #### Validation Condition Types
344
+
345
+ | Type | Parameters | Description |
346
+ |------------------|------------------------------------------------------------|-------------------------------------------------------------------------|
347
+ | `first_frame` | `image_or_video` (path) | Use the first frame of the image/video as conditioning. |
348
+ | `prefix` | `video` or `audio` (path), optional `num_frames`/`duration`| Use a video/audio clip as temporal prefix (for extension forward). |
349
+ | `suffix` | `video` or `audio` (path), optional `num_frames`/`duration`| Use a video/audio clip as temporal suffix (for extension backward). |
350
+ | `spatial_crop` | `video` (path), `spatial_region` (y1, x1, y2, x2) | Provide spatial context for outpainting. Video only. |
351
+ | `mask` | `video` or `audio` (path), `mask` (path) | Mask-based inpainting with a binary mask file. |
352
+ | `reference` | `video` or `audio` (path), optional video-reference `downscale_factor`, `temporal_scale_factor`, `include_in_output` | IC-LoRA style reference conditioning. |
353
+ | `video_to_audio` | `video` (path) | Freeze video, generate audio. For Foley/V2A tasks. |
354
+ | `audio_to_video` | `audio` (path) | Freeze audio, generate video. For audio-driven generation. |
355
+
356
+ For video `reference` validation conditions, `downscale_factor` is the spatial reference scale and
357
+ `temporal_scale_factor` is the temporal reference scale. Set both to match the factors used when
358
+ preprocessing video reference latents for training; validation media is encoded on the fly and cannot infer
359
+ those factors from the training dataset.
360
+
361
+ > [!NOTE]
362
+ > The legacy fields `prompts`, `images`, and `reference_videos` are deprecated but auto-converted to `samples`
363
+ > internally. New configs should use the `samples` format.
364
+
365
+ ### CheckpointsConfig
366
+
367
+ Model checkpointing configuration.
368
+
369
+ ```yaml
370
+ checkpoints:
371
+ interval: 250 # Steps between checkpoint saves (null = disabled)
372
+ keep_last_n: 3 # Number of recent checkpoints to retain
373
+ precision: bfloat16 # Precision for saved weights (bfloat16 or float32)
374
+ no_resume: false # Ignore saved state, start from step 0
375
+ save_training_state: "minimal" # "full", "minimal", or "off"
376
+ ```
377
+
378
+ **Key parameters:**
379
+
380
+ | Parameter | Description |
381
+ |---------------|-------------------------------------------------------------------------------|
382
+ | `interval` | Steps between intermediate checkpoint saves (set to `null` to disable) |
383
+ | `keep_last_n` | Number of most recent checkpoints to keep (-1 = keep all) |
384
+ | `precision` | Precision for saved checkpoint weights: `"bfloat16"` (default) or `"float32"` |
385
+ | `no_resume` | When `true`, ignore saved training state and start from step 0. Model weights from `load_checkpoint` are still loaded. |
386
+ | `save_training_state` | Save training state for resume: `"full"` (optimizer + scheduler + RNG), `"minimal"` (scheduler + RNG only, sufficient for LoRA), `"off"` (no resume). |
387
+
388
+ ### HubConfig
389
+
390
+ Hugging Face Hub integration for automatic model uploads.
391
+
392
+ ```yaml
393
+ hub:
394
+ push_to_hub: false # Enable Hub uploading
395
+ hub_model_id: "username/model-name" # Hub repository ID
396
+ ```
397
+
398
+ **Key parameters:**
399
+
400
+ | Parameter | Description |
401
+ |----------------|------------------------------------------------------------------|
402
+ | `push_to_hub` | Whether to automatically push trained models to Hugging Face Hub |
403
+ | `hub_model_id` | Repository ID in format `"username/repository-name"` |
404
+
405
+ ### WandbConfig
406
+
407
+ Weights & Biases logging configuration.
408
+
409
+ ```yaml
410
+ wandb:
411
+ enabled: false # Enable W&B logging
412
+ project: "ltx-2-trainer" # W&B project name
413
+ entity: null # W&B username or team
414
+ tags: [ ] # Tags for the run
415
+ log_validation_videos: true # Log validation videos to W&B
416
+ ```
417
+
418
+ **Key parameters:**
419
+
420
+ | Parameter | Description |
421
+ |-------------------------|--------------------------------------------------|
422
+ | `enabled` | Whether to enable W&B logging |
423
+ | `project` | W&B project name |
424
+ | `entity` | W&B username or team (null uses default account) |
425
+ | `log_validation_videos` | Whether to log validation videos to W&B |
426
+
427
+ ### FlowMatchingConfig
428
+
429
+ Flow matching training configuration for timestep sampling.
430
+
431
+ ```yaml
432
+ flow_matching:
433
+ timestep_sampling_mode: "shifted_logit_normal" # Timestep sampling strategy
434
+ timestep_sampling_params: { } # Additional sampling parameters
435
+ ```
436
+
437
+ **Key parameters:**
438
+
439
+ | Parameter | Description |
440
+ |----------------------------|------------------------------------------------------------|
441
+ | `timestep_sampling_mode` | Sampling strategy: `"uniform"` or `"shifted_logit_normal"` |
442
+ | `timestep_sampling_params` | Additional parameters for the sampling strategy |
443
+
444
+ ### General Configuration
445
+
446
+ Top-level settings for the training run.
447
+
448
+ ```yaml
449
+ seed: 42 # Random seed for reproducibility
450
+ output_dir: "outputs/my_training_run" # Directory for outputs (checkpoints, validation videos, logs)
451
+ ```
452
+
453
+ | Parameter | Description |
454
+ |--------------|----------------------------------------------------------|
455
+ | `seed` | Random seed for reproducibility (default: `42`) |
456
+ | `output_dir` | Directory to save outputs (default: `"outputs"`) |
457
+
458
+ ## 🚀 Next Steps
459
+
460
+ Once you've configured your training parameters:
461
+
462
+ - Set up your dataset using [Dataset Preparation](dataset-preparation.md)
463
+ - Choose your training approach in [Training Modes](training-modes.md)
464
+ - Start training with the [Training Guide](training-guide.md)
packages/ltx-trainer/docs/custom-training-strategies.md ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Implementing Custom Training Strategies
2
+
3
+ This guide explains how to implement your own training strategy for specialized recipes that cannot be expressed with
4
+ the built-in `flexible` strategy.
5
+
6
+ ## 📋 Overview
7
+
8
+ The trainer uses the **Strategy Pattern** to separate training logic from the core training loop. Each strategy defines:
9
+
10
+ 1. **What data is needed** - Which preprocessed data directories to load
11
+ 2. **How to prepare inputs** - Transform batch data into model inputs
12
+ 3. **How to compute loss** - Calculate the training objective
13
+
14
+ This architecture lets you implement new training modes without modifying the core trainer code.
15
+
16
+ ### When You Need a Custom Strategy
17
+
18
+ > [!NOTE]
19
+ > The built-in `flexible` strategy already supports most conditioning scenarios out of the box:
20
+ > first-frame conditioning, video extension (prefix/suffix), spatial crop (outpainting),
21
+ > mask-based inpainting, IC-LoRA reference conditioning, and frozen modality cross-conditioning
22
+ > (audio-to-video, video-to-audio). Only implement a custom strategy if your use case requires
23
+ > fundamentally different training logic that cannot be expressed through the flexible strategy's
24
+ > configuration.
25
+
26
+ Consider implementing a custom strategy when you need:
27
+
28
+ - **Custom loss computation** (e.g., weighted losses, auxiliary losses, perceptual losses)
29
+ - **Non-standard noise application** (e.g., noise schedules different from flow matching)
30
+ - **Novel conditioning mechanisms** not covered by the flexible strategy's condition types
31
+ - **Additional model outputs** beyond the standard video/audio predictions
32
+
33
+ ## 🏗️ Architecture Overview
34
+
35
+ ### How Strategies Fit Into the Trainer
36
+
37
+ The trainer delegates all training-mode-specific logic to the strategy:
38
+
39
+ 1. **Initialization** — The trainer calls `config.get_data_sources()` to determine which preprocessed data directories to load
40
+ 2. **Each training step:**
41
+ - Calls `prepare_training_inputs()` to transform the raw batch into model-ready inputs
42
+ - Runs the transformer forward pass
43
+ - Calls `compute_loss()` to compute the training objective
44
+
45
+ The trainer handles everything else: optimization, checkpointing, validation, and distributed training.
46
+
47
+ ### Key Components
48
+
49
+ | Component | Purpose |
50
+ |-----------------------------------------------------------------------------------------|--------------------------------------------------------------|
51
+ | [`TrainingStrategyConfigBase`](../src/ltx_trainer/training_strategies/base_strategy.py) | Base class for strategy configuration (Pydantic model) |
52
+ | [`TrainingStrategy`](../src/ltx_trainer/training_strategies/base_strategy.py) | Abstract base class defining the strategy interface |
53
+ | [`ModelInputs`](../src/ltx_trainer/training_strategies/base_strategy.py) | Dataclass containing prepared inputs for the transformer |
54
+ | [`Modality`](../../ltx-core/src/ltx_core/model/transformer/modality.py) | ltx-core dataclass representing video or audio modality data |
55
+
56
+ ## 📝 Step-by-Step Implementation
57
+
58
+ ### Step 1: Plan Your Strategy
59
+
60
+ Before writing code, answer these questions:
61
+
62
+ 1. **What additional data does your strategy need?**
63
+ - Example: A perceptual-loss strategy may need auxiliary feature targets
64
+ - Example: A novel conditioning mechanism may need an additional precomputed directory
65
+
66
+ 2. **What does conditioning look like?**
67
+ - Which tokens should be noised vs. kept clean?
68
+ - How should conditioning tokens be structured (e.g., first frame, reference video, mask)?
69
+
70
+ 3. **How should loss be computed?**
71
+ - Which tokens contribute to the loss?
72
+ - Are there multiple loss terms to combine?
73
+
74
+ ### Step 2: Extend Data Preprocessing (If Needed)
75
+
76
+ If your strategy requires additional preprocessed data beyond video latents, audio latents, and text embeddings, you'll
77
+ need to extend the preprocessing pipeline.
78
+
79
+ #### Option A: Modify `process_dataset.py`
80
+
81
+ For integrated preprocessing, add new arguments and processing steps to the main script. For example, to add mask
82
+ preprocessing:
83
+
84
+ ```python
85
+ # In process_dataset.py, add a new argument
86
+ @app.command()
87
+ def main(
88
+ # ... existing arguments ...
89
+ mask_column: str | None = typer.Option(
90
+ default=None,
91
+ help="Column name containing mask video paths (for inpainting)",
92
+ ),
93
+ ) -> None:
94
+ # ... existing processing ...
95
+
96
+ # Process masks if provided
97
+ if mask_column:
98
+ logger.info("Processing mask videos for inpainting training...")
99
+ mask_latents_dir = output_base / "mask_latents"
100
+
101
+ compute_latents(
102
+ dataset_file=dataset_path,
103
+ video_column=mask_column,
104
+ resolution_buckets=parsed_resolution_buckets,
105
+ output_dir=str(mask_latents_dir),
106
+ model_path=model_path,
107
+ # ... other args ...
108
+ )
109
+ ```
110
+
111
+ #### Option B: Create a Standalone Script
112
+
113
+ For complex preprocessing that doesn't fit naturally into the existing pipeline, create a dedicated script
114
+ (e.g., `scripts/process_masks.py`). Use [`scripts/compute_reference.py`](../scripts/compute_reference.py) as a
115
+ template - it shows how to process paired data and update the dataset JSON.
116
+
117
+ #### Expected Output Structure
118
+
119
+ Your preprocessing should create a directory structure that the strategy can reference:
120
+
121
+ ```
122
+ preprocessed_data_root/
123
+ ├── latents/ # Video latents (standard)
124
+ ├── conditions/ # Text embeddings (standard)
125
+ ├── audio_latents/ # Audio latents (if with_audio)
126
+ ├── mask_latents/ # Your custom data directory
127
+ └── reference_latents/ # Reference videos (for IC-LoRA)
128
+ ```
129
+
130
+ ### Step 3: Create the Strategy Configuration
131
+
132
+ Create a new file for your strategy (e.g., `src/ltx_trainer/training_strategies/inpainting.py`):
133
+
134
+ ```python
135
+ """Inpainting training strategy.
136
+
137
+ This strategy implements video inpainting training where:
138
+ - Mask latents indicate which regions to inpaint
139
+ - Loss is computed only on masked (inpainted) regions
140
+ """
141
+
142
+ from typing import Any, Literal
143
+
144
+ import torch
145
+ from pydantic import Field
146
+ from torch import Tensor
147
+
148
+ from ltx_core.model.transformer.modality import Modality
149
+ from ltx_trainer.timestep_samplers import TimestepSampler
150
+ from ltx_trainer.training_strategies.base_strategy import (
151
+ ModelInputs,
152
+ TrainingStrategy,
153
+ TrainingStrategyConfigBase,
154
+ )
155
+
156
+
157
+ class InpaintingConfig(TrainingStrategyConfigBase):
158
+ """Configuration for inpainting training strategy."""
159
+
160
+ # The 'name' field acts as a discriminator for the config union
161
+ name: Literal["inpainting"] = "inpainting"
162
+
163
+ mask_latents_dir: str = Field(
164
+ default="mask_latents",
165
+ description="Directory name for mask latents",
166
+ )
167
+
168
+ # Add any strategy-specific parameters
169
+ mask_threshold: float = Field(
170
+ default=0.5,
171
+ description="Threshold for binary mask conversion",
172
+ ge=0.0,
173
+ le=1.0,
174
+ )
175
+
176
+ def get_data_sources(self) -> dict[str, str]:
177
+ """Define which data directories to load.
178
+
179
+ Returns a mapping of directory names (under preprocessed_data_root) to
180
+ batch keys. The trainer loads .pt files from each directory and exposes
181
+ them in the batch under the specified key. The trainer also uses this
182
+ mapping to validate that all required directories exist.
183
+ """
184
+ return {
185
+ "latents": "latents", # -> batch["latents"]
186
+ "conditions": "conditions", # -> batch["conditions"]
187
+ self.mask_latents_dir: "masks", # -> batch["masks"]
188
+ }
189
+ ```
190
+
191
+ **Key points:**
192
+
193
+ - Inherit from `TrainingStrategyConfigBase`
194
+ - Use `Literal["your_strategy_name"]` for the `name` field - this enables automatic strategy selection
195
+ - Use Pydantic `Field` for validation and documentation
196
+ - Implement `get_data_sources()` on the config — it's the single source of truth for data directories (used for both dataset wiring and existence validation)
197
+
198
+ ### Step 4: Implement the Strategy Class
199
+
200
+ ```python
201
+ class InpaintingStrategy(TrainingStrategy):
202
+ """Inpainting training strategy.
203
+
204
+ Trains the model to fill in masked regions of videos while
205
+ keeping unmasked regions as conditioning.
206
+ """
207
+
208
+ config: InpaintingConfig
209
+
210
+ def __init__(self, config: InpaintingConfig):
211
+ super().__init__(config)
212
+
213
+ def prepare_training_inputs(
214
+ self,
215
+ batch: dict[str, Any],
216
+ timestep_sampler: TimestepSampler,
217
+ ) -> ModelInputs:
218
+ """Transform batch data into model inputs.
219
+
220
+ This is where the core training logic lives:
221
+ 1. Extract and patchify latents
222
+ 2. Sample noise and apply it appropriately
223
+ 3. Create conditioning masks
224
+ 4. Build Modality objects for the transformer
225
+ """
226
+ # Get video latents [B, C, F, H, W]
227
+ latents_data = batch["latents"]
228
+ video_latents = latents_data["latents"]
229
+
230
+ # Get dimensions
231
+ num_frames = latents_data["num_frames"][0].item()
232
+ height = latents_data["height"][0].item()
233
+ width = latents_data["width"][0].item()
234
+
235
+ # Patchify: [B, C, F, H, W] -> [B, seq_len, C]
236
+ video_latents = self._video_patchifier.patchify(video_latents)
237
+
238
+ batch_size, seq_len, _ = video_latents.shape
239
+ device = video_latents.device
240
+ dtype = video_latents.dtype
241
+
242
+ # Get mask latents and process them
243
+ mask_data = batch["masks"]
244
+ mask_latents = mask_data["latents"]
245
+ mask_latents = self._video_patchifier.patchify(mask_latents)
246
+
247
+ # Create binary mask: True = inpaint this region, False = keep original
248
+ inpaint_mask = mask_latents.mean(dim=-1) > self.config.mask_threshold
249
+
250
+ # Sample noise and sigmas
251
+ sigmas = timestep_sampler.sample_for(video_latents)
252
+ noise = torch.randn_like(video_latents)
253
+
254
+ # Apply noise only to inpaint regions
255
+ sigmas_expanded = sigmas.view(-1, 1, 1)
256
+ noisy_latents = (1 - sigmas_expanded) * video_latents + sigmas_expanded * noise
257
+
258
+ # Keep original latents for non-inpaint regions (conditioning)
259
+ inpaint_mask_expanded = inpaint_mask.unsqueeze(-1)
260
+ noisy_latents = torch.where(inpaint_mask_expanded, noisy_latents, video_latents)
261
+
262
+ # Create per-token timesteps
263
+ # Conditioning tokens (non-inpaint) get timestep=0
264
+ # Inpaint tokens get the sampled sigma
265
+ timesteps = self._create_per_token_timesteps(~inpaint_mask, sigmas.squeeze())
266
+
267
+ # Compute targets (velocity prediction: noise - clean)
268
+ targets = noise - video_latents
269
+
270
+ # Get text embeddings
271
+ conditions = batch["conditions"]
272
+ video_prompt_embeds = conditions["video_prompt_embeds"]
273
+ prompt_attention_mask = conditions["prompt_attention_mask"]
274
+
275
+ # Generate position embeddings
276
+ positions = self._get_video_positions(
277
+ num_frames=num_frames,
278
+ height=height,
279
+ width=width,
280
+ batch_size=batch_size,
281
+ fps=24.0, # Or get from latents_data
282
+ device=device,
283
+ )
284
+
285
+ # Create video Modality
286
+ video_modality = Modality(
287
+ enabled=True,
288
+ latent=noisy_latents,
289
+ sigma=sigmas,
290
+ timesteps=timesteps,
291
+ positions=positions,
292
+ context=video_prompt_embeds,
293
+ context_mask=prompt_attention_mask,
294
+ )
295
+
296
+ # Loss mask: only compute loss on inpaint regions
297
+ loss_mask = inpaint_mask
298
+
299
+ return ModelInputs(
300
+ video=video_modality,
301
+ audio=None,
302
+ video_targets=targets,
303
+ audio_targets=None,
304
+ video_loss_mask=loss_mask,
305
+ audio_loss_mask=None,
306
+ )
307
+
308
+ def compute_loss(
309
+ self,
310
+ video_pred: Tensor,
311
+ audio_pred: Tensor | None,
312
+ inputs: ModelInputs,
313
+ ) -> Tensor:
314
+ """Compute training loss on inpaint regions only. Returns [B,]."""
315
+ # MSE loss
316
+ loss = (video_pred - inputs.video_targets).pow(2)
317
+
318
+ # Apply loss mask and reduce to per-element [B,]
319
+ loss_mask = inputs.video_loss_mask.unsqueeze(-1).float()
320
+ masked = loss.mul(loss_mask)
321
+ return masked.mean(dim=[-2, -1]) / loss_mask.mean(dim=[-2, -1]).clamp(min=1e-8)
322
+ ```
323
+
324
+ ### Step 5: Register the Strategy
325
+
326
+ You need to register your strategy in two places:
327
+
328
+ **1. Update [`src/ltx_trainer/training_strategies/__init__.py`](../src/ltx_trainer/training_strategies/__init__.py):**
329
+
330
+ ```python
331
+ # Add import for your strategy
332
+ from ltx_trainer.training_strategies.inpainting import InpaintingConfig, InpaintingStrategy
333
+
334
+ # Add to the TrainingStrategyConfig type alias
335
+ TrainingStrategyConfig = TextToVideoConfig | VideoToVideoConfig | FlexibleStrategyConfig | InpaintingConfig
336
+
337
+ # Add to __all__
338
+ __all__ = [
339
+ # ... existing exports ...
340
+ "InpaintingConfig",
341
+ "InpaintingStrategy",
342
+ ]
343
+
344
+
345
+ # Add case in get_training_strategy()
346
+ def get_training_strategy(config: TrainingStrategyConfig) -> TrainingStrategy:
347
+ match config:
348
+ # ... existing cases ...
349
+ case InpaintingConfig():
350
+ strategy = InpaintingStrategy(config)
351
+ ```
352
+
353
+ **2. Update [`src/ltx_trainer/config.py`](../src/ltx_trainer/config.py):**
354
+
355
+ ```python
356
+ # Add import
357
+ from ltx_trainer.training_strategies.inpainting import InpaintingConfig
358
+
359
+ # Add to the TrainingStrategyConfig union with a Tag matching your strategy name
360
+ TrainingStrategyConfig = Annotated[
361
+ Annotated[TextToVideoConfig, Tag("text_to_video")]
362
+ | Annotated[VideoToVideoConfig, Tag("video_to_video")]
363
+ | Annotated[FlexibleStrategyConfig, Tag("flexible")]
364
+ | Annotated[InpaintingConfig, Tag("inpainting")],
365
+ Discriminator(_get_strategy_discriminator),
366
+ ]
367
+ ```
368
+
369
+ ### Step 6: Create a Configuration File
370
+
371
+ Create an example config in `configs/`:
372
+
373
+ ```yaml
374
+ # configs/custom_inpainting_lora.yaml
375
+
376
+ model:
377
+ model_path: "/path/to/ltx2.safetensors"
378
+ text_encoder_path: "/path/to/gemma"
379
+ training_mode: "lora"
380
+
381
+ training_strategy:
382
+ name: "inpainting" # Must match your Literal type
383
+ mask_latents_dir: "mask_latents"
384
+ mask_threshold: 0.5
385
+
386
+ lora:
387
+ rank: 32
388
+ alpha: 32
389
+ target_modules:
390
+ - "to_k"
391
+ - "to_q"
392
+ - "to_v"
393
+ - "to_out.0"
394
+
395
+ data:
396
+ preprocessed_data_root: "/path/to/preprocessed/dataset"
397
+
398
+ optimization:
399
+ learning_rate: 1e-4
400
+ steps: 2000
401
+ batch_size: 1
402
+
403
+ # ... other config sections ...
404
+ ```
405
+
406
+ ## 🔧 Helper Methods Reference
407
+
408
+ The base `TrainingStrategy` class provides these helper methods:
409
+
410
+ | Method | Purpose |
411
+ |----------------------------------------------|-------------------------------------------------|
412
+ | `_video_patchifier.patchify(latents)` | Convert `[B, C, F, H, W]` → `[B, seq_len, C]` |
413
+ | `_audio_patchifier.patchify(latents)` | Convert `[B, C, T, F]` → `[B, T, C*F]` |
414
+ | `_get_video_positions(...)` | Generate position embeddings for video |
415
+ | `_get_audio_positions(...)` | Generate position embeddings for audio |
416
+ | `_create_per_token_timesteps(conditioning_mask, sampled_sigma)` | Create timesteps with 0 for conditioning tokens |
417
+ | `_create_first_frame_conditioning_mask(...)` | Create mask for first-frame conditioning |
418
+
419
+ ## 📊 Understanding ModelInputs
420
+
421
+ The `ModelInputs` dataclass contains everything needed for the forward pass and loss computation:
422
+
423
+ ```python
424
+ @dataclass
425
+ class ModelInputs:
426
+ video: Modality | None # Video modality data
427
+ audio: Modality | None # Audio modality data
428
+
429
+ video_targets: Tensor | None # Target values for video loss (velocity)
430
+ audio_targets: Tensor | None # Target values for audio loss (velocity)
431
+
432
+ video_loss_mask: Tensor | None # Boolean loss mask for video tokens
433
+ audio_loss_mask: Tensor | None # Boolean loss mask for audio tokens
434
+ ```
435
+
436
+ ## 📊 Understanding Modality
437
+
438
+ The `Modality` dataclass (from ltx-core) represents a single modality's data:
439
+
440
+ ```python
441
+ @dataclass(frozen=True)
442
+ class Modality:
443
+ latent: Tensor # [B, T, D] — patchified latent tokens
444
+ sigma: Tensor # [B,] — per-batch noise level (for cross-attn conditioning)
445
+ timesteps: Tensor # [B, T] — per-token timestep embeddings
446
+ positions: Tensor # [B, 3, T, 2] for video, [B, 1, T, 2] for audio — positional bounds
447
+ context: Tensor # text conditioning embeddings
448
+ enabled: bool = True
449
+ context_mask: Tensor | None = None # attention mask for text context
450
+ attention_mask: Tensor | None = None # optional 2D self-attention mask [B, T, T]
451
+ ```
452
+
453
+ > [!NOTE]
454
+ > **Per-token timesteps:** Each token in the sequence has its own timestep. Conditioning tokens—those that should remain
455
+ > un-noised—must have `timestep=0`. This is how the model distinguishes clean reference tokens from tokens to denoise. Use
456
+ > `_create_per_token_timesteps(conditioning_mask, sampled_sigma)` to set this up correctly.
457
+
458
+ > [!NOTE]
459
+ > `Modality` is immutable (frozen dataclass). Use `dataclasses.replace()` to create modified copies.
460
+
461
+ ## ✅ Testing Your Strategy
462
+
463
+ 1. **Verify your training configuration is valid:**
464
+ ```bash
465
+ uv run python -c "
466
+ from ltx_trainer.config import LtxTrainerConfig
467
+ import yaml
468
+
469
+ with open('configs/custom_inpainting_lora.yaml') as f:
470
+ config = LtxTrainerConfig(**yaml.safe_load(f))
471
+ print(f'Strategy: {config.training_strategy.name}')
472
+ "
473
+ ```
474
+
475
+ 2. **Test strategy instantiation:**
476
+ ```bash
477
+ uv run python -c "
478
+ from ltx_trainer.training_strategies import get_training_strategy
479
+ from ltx_trainer.training_strategies.inpainting import InpaintingConfig
480
+
481
+ config = InpaintingConfig()
482
+ strategy = get_training_strategy(config)
483
+ print(f'Data sources: {config.get_data_sources()}')
484
+ "
485
+ ```
486
+
487
+ 3. **Run a short training test:**
488
+ ```bash
489
+ uv run python scripts/train.py configs/custom_inpainting_lora.yaml
490
+ ```
491
+
492
+ ## 💡 Tips and Best Practices
493
+
494
+ ### Debugging
495
+
496
+ - Set `data.num_dataloader_workers: 0` to get clearer error messages
497
+ - Use a small dataset and few steps for initial testing
498
+ - Check tensor shapes at each step with print statements
499
+
500
+ ## 🔗 Related Documentation
501
+
502
+ - [Training Modes](training-modes.md) - Overview of built-in training modes
503
+ - [Configuration Reference](configuration-reference.md) - All configuration options
504
+ - [Dataset Preparation](dataset-preparation.md) - Preprocessing workflow
505
+ - [ltx-core Documentation](../../ltx-core/README.md) - Core model components
506
+
507
+ ## 📚 Reference: Existing Strategies
508
+
509
+ Study these implementations for guidance:
510
+
511
+ | Strategy | Complexity | Key Features |
512
+ |----------|------------|--------------|
513
+ | [`FlexibleStrategy`](../src/ltx_trainer/training_strategies/flexible.py) | Medium | Unified conditioning framework — supports all built-in modes |
514
+ | [`TextToVideoStrategy`](../src/ltx_trainer/training_strategies/text_to_video.py) | Simple | First-frame conditioning, optional audio (deprecated) |
515
+ | [`VideoToVideoStrategy`](../src/ltx_trainer/training_strategies/video_to_video.py) | Medium | Reference video concatenation, split loss mask (deprecated) |
packages/ltx-trainer/docs/dataset-preparation.md ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dataset Preparation Guide
2
+
3
+ This guide covers the complete workflow for preparing and preprocessing your dataset for training.
4
+
5
+ ## 📋 Overview
6
+
7
+ The general dataset preparation workflow is:
8
+
9
+ 1. **(Optional)** Split long videos into scenes using `split_scenes.py`
10
+ 2. **(Optional)** Generate captions for your videos using `caption_videos.py`
11
+ 3. **Preprocess your dataset** using `process_dataset.py` to compute and cache video/audio latents and text embeddings
12
+ 4. **Run the trainer** with your preprocessed dataset
13
+
14
+ ## 🎬 Step 1: Split Scenes
15
+
16
+ If you're starting with raw, long-form videos (e.g., downloaded from YouTube), you should first split them into shorter, coherent scenes.
17
+
18
+ ```bash
19
+ uv run python scripts/split_scenes.py input.mp4 scenes_output_dir/ \
20
+ --filter-shorter-than 5s
21
+ ```
22
+
23
+ This will create multiple video clips in `scenes_output_dir`.
24
+ These clips will be the input for the captioning step, if you choose to use it.
25
+
26
+ The script supports many configuration options for scene detection (detector algorithms, thresholds, minimum scene lengths, etc.):
27
+
28
+ ```bash
29
+ uv run python scripts/split_scenes.py --help
30
+ ```
31
+
32
+ ## 📝 Step 2: Caption Videos
33
+
34
+ If your dataset doesn't include captions, you can automatically generate them using multimodal models that understand both video and audio.
35
+
36
+ The default `qwen_omni` backend talks to a local vLLM server, which you launch once in a separate terminal:
37
+
38
+ ```bash
39
+ # Terminal 1: start the captioner server (stays running)
40
+ uv run python scripts/serve_captioner.py
41
+ ```
42
+
43
+ ```bash
44
+ # Terminal 2: caption your videos
45
+ uv run python scripts/caption_videos.py scenes_output_dir/ \
46
+ --output scenes_output_dir/dataset.json
47
+ ```
48
+
49
+ This will create a `dataset.json` file containing video paths and their captions.
50
+
51
+ **Captioning options:**
52
+
53
+ | Option | Description |
54
+ | ------------------ | --------------------------------------------------------------- |
55
+ | `--captioner-type` | `qwen_omni` (default, local vLLM server) or `gemini_flash` (API) |
56
+ | `--vllm-url` | Base URL of the vLLM server (default `http://127.0.0.1:8001/v1`) |
57
+ | `--override` | Re-caption files that already have captions |
58
+ | `--api-key` | Gemini API key (else `GEMINI_API_KEY`/`GOOGLE_API_KEY`; with no key, uses gcloud/Vertex AI auth) |
59
+
60
+ **Caption format:**
61
+
62
+ Each caption is a single, detailed paragraph describing both the visual content and the audio (speech, music, ambient sounds) of the clip. See the [Utility Scripts Reference](utility-scripts.md#automatic-video-captioning) for backend setup and the full list of options.
63
+
64
+ > [!NOTE]
65
+ > The automatically generated captions may contain inaccuracies or hallucinated content.
66
+ > We recommend reviewing and correcting the generated captions in your `dataset.json` file before proceeding to preprocessing.
67
+
68
+ ## ⚡ Step 3: Dataset Preprocessing
69
+
70
+ This step preprocesses your video dataset by:
71
+
72
+ 1. Resizing and cropping videos to fit specified resolution buckets
73
+ 2. Computing and caching video latent representations
74
+ 3. Computing and caching text embeddings for captions
75
+ 4. Extracting and caching audio latents from videos (automatic, use `--skip-audio` to disable)
76
+
77
+ > [!WARNING]
78
+ > Very large videos (especially high spatial resolution and/or many frames) can cause GPU out-of-memory (OOM)
79
+ > during preprocessing/encoding.
80
+ > The simplest fix is to reduce the target resolution (spatially: width/height) and/or the number of frames
81
+ > (temporally) by using `--resolution-buckets` with smaller dimensions (lower width/height and/or fewer frames).
82
+
83
+ ### Basic Usage
84
+
85
+ ```bash
86
+ uv run python scripts/process_dataset.py dataset.json \
87
+ --resolution-buckets "960x544x49" \
88
+ --model-path /path/to/ltx-2-model.safetensors \
89
+ --text-encoder-path /path/to/gemma-model
90
+ ```
91
+
92
+ Audio latents are automatically extracted from video files — no extra flag is needed. Use `--skip-audio`
93
+ to disable this. For standalone audio files (`.wav`), use the `audio` column in your dataset instead
94
+ (see [Convention-Based Column Detection](#convention-based-column-detection) below).
95
+
96
+ ### 🚀 Multi-GPU Preprocessing
97
+
98
+ Preprocessing large datasets can take a while. To run it across multiple GPUs in parallel, wrap the command with
99
+ `accelerate launch` (for example `--num_processes 4`). Each process handles an interleaved slice of the dataset.
100
+ The same approach applies to `process_videos.py` and `process_captions.py` when you run them standalone.
101
+
102
+ ```bash
103
+ uv run accelerate launch --num_processes 4 scripts/process_dataset.py dataset.json \
104
+ --resolution-buckets "960x544x49" \
105
+ --model-path /path/to/ltx-2-model.safetensors \
106
+ --text-encoder-path /path/to/gemma-model
107
+ ```
108
+
109
+ Outputs are written atomically (via a per-process temporary file, then renamed), so an interrupted run leaves no
110
+ corrupt files. By default a rerun **resumes** — items whose output `.pt` already exists are skipped.
111
+
112
+ > [!IMPORTANT]
113
+ > Pass **`--overwrite`** when rerunning with changed parameters (different model checkpoint, resolution buckets,
114
+ > text encoder, `--lora-trigger`, etc.). Without it the script keeps the stale outputs from the previous run.
115
+ >
116
+ > ```bash
117
+ > uv run accelerate launch --num_processes 4 scripts/process_dataset.py dataset.json \
118
+ > --resolution-buckets "960x544x49" \
119
+ > --model-path /path/to/ltx-2.3-model.safetensors \
120
+ > --text-encoder-path /path/to/gemma-model \
121
+ > --overwrite
122
+ > ```
123
+
124
+ ### 📊 Dataset Format
125
+
126
+ The trainer supports videos, single images, or a mix of both in the same dataset.
127
+
128
+ > [!TIP]
129
+ > **Image Datasets:** When using images, follow the same preprocessing steps and format requirements as with videos,
130
+ > but use `1` for the frame count in the resolution bucket (e.g., `960x544x1`).
131
+
132
+ > [!NOTE]
133
+ > **Mixed image + video datasets:** Mixing stills and videos in a single dataset is supported, but requires some care:
134
+ >
135
+ > - Preprocess with **multiple resolution buckets** covering both frame counts — e.g.
136
+ > `--resolution-buckets "960x544x1;960x544x49"`. Images are automatically assigned to the `F=1` bucket and
137
+ > videos to an `F>1` bucket.
138
+ > - You **must** set `optimization.batch_size: 1` in your training config (see the warning under
139
+ > [Resolution Buckets](#resolution-buckets)), since samples with different shapes cannot be collated into a
140
+ > single batch. Use `gradient_accumulation_steps` if you need a larger effective batch.
141
+ > - Per-step cost differs substantially between a single-frame sample and a many-frame sample, which can lead to
142
+ > uneven gradient magnitudes across steps. Consider weighting the two subsets or tuning the learning rate if
143
+ > you observe instability.
144
+ > - If you prefer a fully officially-supported path, train two separate LoRAs (one on stills, one on video) and
145
+ > stack them at inference.
146
+
147
+ The dataset must be a CSV, JSON, or JSONL metadata file with columns for captions and media paths.
148
+
149
+ #### Convention-Based Column Detection
150
+
151
+ The preprocessing script automatically detects and processes columns based on their names. The following columns are recognized:
152
+
153
+ | Column | Output Dir | Description |
154
+ |--------|-----------|-------------|
155
+ | `video` (or legacy `media_path`) | `latents/` | Target video to encode |
156
+ | `audio` | `audio_latents/` | Explicit audio file (overrides auto-extraction from video) |
157
+ | `caption` | `conditions/` | Text caption for the sample |
158
+ | `reference_video` (or legacy `ref_media_path`) | `reference_latents/` | IC-LoRA reference video |
159
+ | `reference_audio` | `reference_audio_latents/` | IC-LoRA reference audio |
160
+ | `video_mask` | `video_masks/` | Binary mask for video inpainting |
161
+ | `audio_mask` | `audio_masks/` | Binary mask for audio inpainting |
162
+
163
+ > [!NOTE]
164
+ > **Legacy column names:** `media_path` and `ref_media_path` are accepted as aliases for `video` and `reference_video` respectively. Existing datasets using these names will continue to work without modification.
165
+
166
+ **JSON format example:**
167
+
168
+ ```json
169
+ [
170
+ {
171
+ "caption": "A cat playing with a ball of yarn",
172
+ "video": "videos/cat_playing.mp4"
173
+ },
174
+ {
175
+ "caption": "A dog running in the park",
176
+ "video": "videos/dog_running.mp4"
177
+ }
178
+ ]
179
+ ```
180
+
181
+ **JSONL format example:**
182
+
183
+ ```jsonl
184
+ {"caption": "A cat playing with a ball of yarn", "video": "videos/cat_playing.mp4"}
185
+ {"caption": "A dog running in the park", "video": "videos/dog_running.mp4"}
186
+ ```
187
+
188
+ **CSV format example:**
189
+
190
+ ```csv
191
+ caption,video
192
+ "A cat playing with a ball of yarn","videos/cat_playing.mp4"
193
+ "A dog running in the park","videos/dog_running.mp4"
194
+ ```
195
+
196
+ **Additional dataset format examples:**
197
+
198
+ Audio-only dataset:
199
+ ```json
200
+ {"audio": "song.wav", "caption": "piano melody"}
201
+ ```
202
+
203
+ V2V IC-LoRA with reference video:
204
+ ```json
205
+ {"video": "clip.mp4", "reference_video": "depth.mp4", "caption": "depth to video"}
206
+ ```
207
+
208
+ A2A IC-LoRA with reference audio:
209
+ ```json
210
+ {"video": "clip.mp4", "reference_audio": "ref.wav", "caption": "match this style"}
211
+ ```
212
+ This form auto-extracts the target audio from `clip.mp4`. For pure audio datasets, use `audio` plus
213
+ `reference_audio` columns and preprocess with `--audio-durations`.
214
+
215
+ Video inpainting with mask:
216
+ ```json
217
+ {"video": "clip.mp4", "video_mask": "mask.mp4", "caption": "fill the sky"}
218
+ ```
219
+
220
+ ### 📐 Resolution Buckets
221
+
222
+ Videos are organized into "buckets" of specific dimensions (width × height × frames).
223
+ Each video is assigned to the nearest matching bucket.
224
+ You can preprocess with one or multiple resolution buckets.
225
+ When training with multiple resolution buckets, you must use a batch size of 1.
226
+
227
+ The dimensions of each bucket must follow these constraints due to LTX-2's VAE architecture:
228
+
229
+ - **Spatial dimensions** (width and height) must be multiples of 32
230
+ - **Number of frames** must satisfy `frames % 8 == 1` (e.g., 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, 97, 121, etc.)
231
+
232
+ **Guidelines for choosing training resolution:**
233
+
234
+ - For high-quality, detailed videos: use larger spatial dimensions (e.g. 768x448) with fewer frames (e.g. 89)
235
+ - For longer, motion-focused videos: use smaller spatial dimensions (512×512) with more frames (121)
236
+ - Memory usage increases with both spatial and temporal dimensions
237
+
238
+ **Example usage:**
239
+
240
+ ```bash
241
+ uv run python scripts/process_dataset.py dataset.json \
242
+ --resolution-buckets "960x544x49" \
243
+ --model-path /path/to/ltx-2-model.safetensors \
244
+ --text-encoder-path /path/to/gemma-model
245
+ ```
246
+
247
+ Multiple buckets are supported by separating entries with `;`:
248
+
249
+ ```bash
250
+ uv run python scripts/process_dataset.py dataset.json \
251
+ --resolution-buckets "960x544x49;512x512x49" \
252
+ --model-path /path/to/ltx-2-model.safetensors \
253
+ --text-encoder-path /path/to/gemma-model
254
+ ```
255
+
256
+ **Video processing workflow:**
257
+
258
+ 1. Videos are **resized** maintaining aspect ratio until either width or height matches the target
259
+ 2. The larger dimension is **center cropped** to match the bucket's dimensions
260
+ 3. Only the **first X frames are taken** to match the bucket's frame count, remaining frames are ignored
261
+
262
+ > [!NOTE]
263
+ > The sequence length processed by the transformer model can be calculated as:
264
+ >
265
+ > ```
266
+ > sequence_length = (H/32) * (W/32) * ((F-1)/8 + 1)
267
+ > ```
268
+ >
269
+ > Where:
270
+ >
271
+ > - H = Height of video
272
+ > - W = Width of video
273
+ > - F = Number of frames
274
+ > - 32 = VAE's spatial downsampling factor
275
+ > - 8 = VAE's temporal downsampling factor
276
+ >
277
+ > For example, a 768×448×89 video would have sequence length:
278
+ >
279
+ > ```
280
+ > (768/32) * (448/32) * ((89-1)/8 + 1) = 24 * 14 * 12 = 4,032
281
+ > ```
282
+ >
283
+ > Keep this in mind when choosing video dimensions, as longer sequences require more GPU memory.
284
+
285
+ > [!WARNING]
286
+ > When training with multiple resolution buckets, you must use a batch size of 1
287
+ > (i.e., set `optimization.batch_size: 1` in your training config).
288
+
289
+ ### 📁 Output Structure
290
+
291
+ The preprocessed data is saved in a `.precomputed` directory:
292
+
293
+ ```
294
+ dataset/
295
+ └── .precomputed/
296
+ ├── latents/ # Video latents
297
+ ├── conditions/ # Text embeddings
298
+ ├── audio_latents/ # Audio latents (auto-extracted or explicit)
299
+ ├── reference_latents/ # Reference video latents (IC-LoRA)
300
+ ├── reference_audio_latents/ # Reference audio latents (audio IC-LoRA)
301
+ ├── video_masks/ # Video masks (inpainting)
302
+ └── audio_masks/ # Audio masks (audio inpainting)
303
+ ```
304
+
305
+ Set `data.preprocessed_data_root` in your training config to this `.precomputed` directory — the parent directory that
306
+ contains `latents/`, `conditions/`, and any mode-specific audio/reference/mask directories.
307
+
308
+ ## 🔊 Audio-Only Dataset Preprocessing
309
+
310
+ For datasets containing only audio files (no `video` column), use `--audio-durations` to specify duration buckets:
311
+
312
+ ```bash
313
+ uv run python scripts/process_dataset.py dataset.json \
314
+ --audio-durations "2.0;4.0;8.0" \
315
+ --model-path /path/to/ltx-2-model.safetensors \
316
+ --text-encoder-path /path/to/gemma-model
317
+ ```
318
+
319
+ The `--audio-durations` flag provides duration buckets (in seconds) for audio-only datasets. Since there is no video column to derive timing from, explicit duration buckets are required.
320
+
321
+ ## 🪄 IC-LoRA Reference Video Preprocessing
322
+
323
+ For IC-LoRA training, you need to preprocess datasets that include reference videos.
324
+ Reference videos provide the conditioning input while target videos represent the desired transformed output.
325
+
326
+ ### Dataset Format with Reference Videos
327
+
328
+ The `reference_video` column is automatically detected by convention — no extra CLI flags are needed.
329
+
330
+ **JSON format:**
331
+
332
+ ```json
333
+ [
334
+ {
335
+ "caption": "A cat playing with a ball of yarn",
336
+ "video": "videos/cat_playing.mp4",
337
+ "reference_video": "references/cat_playing_depth.mp4"
338
+ }
339
+ ]
340
+ ```
341
+
342
+ **JSONL format:**
343
+
344
+ ```jsonl
345
+ {"caption": "A cat playing with a ball of yarn", "video": "videos/cat_playing.mp4", "reference_video": "references/cat_playing_depth.mp4"}
346
+ {"caption": "A dog running in the park", "video": "videos/dog_running.mp4", "reference_video": "references/dog_running_depth.mp4"}
347
+ ```
348
+
349
+ ### Preprocessing with Reference Videos
350
+
351
+ Convention-based detection means you just need the `reference_video` column in your dataset, and `process_dataset.py` will automatically detect and process it. No `--reference-column` flag is needed:
352
+
353
+ ```bash
354
+ uv run python scripts/process_dataset.py dataset.json \
355
+ --resolution-buckets "960x544x49" \
356
+ --model-path /path/to/ltx-2-model.safetensors \
357
+ --text-encoder-path /path/to/gemma-model \
358
+ --reference-downscale-factor 2 \
359
+ --reference-temporal-scale-factor 1
360
+ ```
361
+
362
+ This will create an additional `reference_latents/` directory containing the preprocessed reference video latents.
363
+ Use `--reference-downscale-factor` for spatial subsampling and `--reference-temporal-scale-factor` for temporal
364
+ subsampling. Validation reference conditions should use matching `downscale_factor` and `temporal_scale_factor` values.
365
+
366
+ > [!NOTE]
367
+ > **Legacy column names:** If your dataset uses `ref_media_path`, it is accepted as an alias for `reference_video`.
368
+
369
+ ### Generating Reference Videos
370
+
371
+ **Dataset Requirements for IC-LoRA:**
372
+
373
+ - Your dataset must contain paired videos where each target video has a corresponding reference video
374
+ - Reference and target videos should cover the same content. Reference videos can optionally be lower spatial
375
+ resolution or temporally subsampled (see Scaled Reference Conditioning in [Training Modes](training-modes.md)).
376
+ - Both reference and target videos should be preprocessed together using the same target resolution buckets, plus any
377
+ reference scale factors you choose.
378
+
379
+ We provide an example script, `[scripts/compute_reference.py](../scripts/compute_reference.py)`, to generate reference
380
+ videos for a given dataset. The default implementation generates Canny edge reference videos.
381
+
382
+ ```bash
383
+ uv run python scripts/compute_reference.py scenes_output_dir/ \
384
+ --output scenes_output_dir/dataset.json
385
+ ```
386
+
387
+ The script accepts a JSON file as the dataset configuration and updates it in-place by adding the filenames of the generated reference videos.
388
+
389
+ > [!NOTE]
390
+ > `compute_reference.py` writes generated references to the `reference_video` column, which `process_dataset.py`
391
+ > detects automatically. The legacy `ref_media_path` column is also accepted.
392
+
393
+ If you want to generate a different type of condition (depth maps, pose skeletons, etc.), modify or replace the `compute_reference()` function within this script.
394
+
395
+ ### Example Dataset
396
+
397
+ For reference, see our **[Canny Control Dataset](https://huggingface.co/datasets/Lightricks/Canny-Control-Dataset)** which demonstrates proper IC-LoRA dataset structure with paired videos and Canny edge maps.
398
+
399
+ ## 🎭 Mask Preprocessing for Inpainting
400
+
401
+ For inpainting training with the `mask` condition type, provide `video_mask` or `audio_mask` columns in your dataset
402
+ metadata. These columns point to mask media files (for example a mask image/video for video inpainting, or a waveform or
403
+ `.pt` tensor for audio inpainting). `process_dataset.py` downsamples and thresholds them into per-sample `.pt` tensors
404
+ under `video_masks/` or `audio_masks/`.
405
+
406
+ ### Processed Video Mask Format
407
+
408
+ If you create masks manually instead of using `process_dataset.py`, save them as `.pt` files with the key `"mask"`
409
+ containing a tensor of shape `[F, H, W]` where:
410
+
411
+ - `F` = number of latent frames (temporal dimension)
412
+ - `H` = latent height (pixel height / 32)
413
+ - `W` = latent width (pixel width / 32)
414
+ - Values are thresholded at `0.5`: values `> 0.5` are conditioning tokens (clean, excluded from loss),
415
+ and values `<= 0.5` are generated tokens (noised, contributes to loss).
416
+
417
+ ### Audio Mask Format
418
+
419
+ Audio masks follow the same thresholding pattern as video masks but with shape `[T]` (temporal dimension only), where `T` is the number of audio latent frames. They are stored in `audio_masks/`.
420
+
421
+ ### Directory Structure
422
+
423
+ Place masks in a directory within your preprocessed data root:
424
+
425
+ ```
426
+ preprocessed_data_root/
427
+ ├── latents/ # Video latents
428
+ ├── conditions/ # Text embeddings
429
+ ├── video_masks/ # Video masks (one .pt per sample, matching latent filenames)
430
+ └── audio_masks/ # Audio masks (one .pt per sample, matching latent filenames)
431
+ ```
432
+
433
+ Then reference the mask directory in your training config:
434
+
435
+ ```yaml
436
+ training_strategy:
437
+ name: "flexible"
438
+ video:
439
+ is_generated: true
440
+ latents_dir: "latents"
441
+ conditions:
442
+ - type: mask
443
+ mask_dir: "video_masks"
444
+ ```
445
+
446
+ ## 🎯 LoRA Trigger Words
447
+
448
+ When training a LoRA, you can specify a trigger token that will be prepended to all captions:
449
+
450
+ ```bash
451
+ uv run python scripts/process_dataset.py dataset.json \
452
+ --resolution-buckets "960x544x49" \
453
+ --model-path /path/to/ltx-2-model.safetensors \
454
+ --text-encoder-path /path/to/gemma-model \
455
+ --lora-trigger "MYTRIGGER"
456
+ ```
457
+
458
+ This acts as a trigger word that activates the LoRA during inference when you include the same token in your prompts.
459
+
460
+ > [!NOTE]
461
+ > There is no need to manually insert the trigger word into your dataset JSON/JSONL/CSV file.
462
+ > The trigger word specified with `--lora-trigger` is automatically prepended to each caption during preprocessing.
463
+
464
+ ## 🔍 Decoding Videos for Verification
465
+
466
+ If you add the `--decode` flag, the script will VAE-decode the precomputed video latents and save the resulting videos
467
+ in `.precomputed/decoded_videos`. Reference video latents are decoded to `.precomputed/decoded_reference_videos` when
468
+ present. To inspect audio latents, run `scripts/decode_latents.py` with `--with-audio`.
469
+
470
+ ```bash
471
+ uv run python scripts/process_dataset.py dataset.json \
472
+ --resolution-buckets "960x544x49" \
473
+ --model-path /path/to/ltx-2-model.safetensors \
474
+ --text-encoder-path /path/to/gemma-model \
475
+ --decode
476
+ ```
477
+
478
+ For single-frame images, the decoded latents will be saved as PNG files rather than MP4 videos.
479
+
480
+ ## 🚀 Next Steps
481
+
482
+ Once your dataset is preprocessed, you can proceed to:
483
+
484
+ - Configure your training parameters in [Configuration Reference](configuration-reference.md)
485
+ - Choose your training approach in [Training Modes](training-modes.md)
486
+ - Start training with the [Training Guide](training-guide.md)
487
+
488
+ > [!TIP]
489
+ > The `flexible` strategy supports masks for inpainting (`mask` condition type) and spatial crop regions for outpainting (`spatial_crop` condition type) out of the box. For other custom preprocessing needs, see [Custom Training Strategies](custom-training-strategies.md).
packages/ltx-trainer/docs/quick-start.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Start Guide
2
+
3
+ Get up and running with LTX-2 training in just a few steps!
4
+
5
+ ## 📋 Prerequisites
6
+
7
+ Before you begin, ensure you have:
8
+
9
+ 1. **LTX-2 Model Checkpoint** - A local `.safetensors` file containing the LTX-2 model weights.
10
+ Download `ltx-2.3-22b-dev.safetensors` from: [HuggingFace Hub](https://huggingface.co/Lightricks/LTX-2.3)
11
+ The trainer supports LTX-2 and LTX-2.3 checkpoints through the same configuration API; version-specific components
12
+ are detected from the checkpoint.
13
+ 2. **Gemma Text Encoder** - A local directory containing the Gemma model (required for LTX-2).
14
+ Download from: [HuggingFace Hub](https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized/)
15
+ 3. **Linux with CUDA** - The trainer requires `triton` which is Linux-only; CUDA 13+ is recommended
16
+ 4. **GPU with sufficient VRAM** - 80GB recommended for the standard config. For GPUs with 32GB VRAM (e.g., RTX 5090),
17
+ use the [low VRAM config](../configs/t2v_lora_low_vram.yaml) which enables INT8 quantization and other
18
+ memory optimizations
19
+
20
+ ## ⚡ Installation
21
+
22
+ First, install [uv](https://docs.astral.sh/uv/getting-started/installation/) if you haven't already.
23
+ Then clone the repository and install the dependencies:
24
+
25
+ ```bash
26
+ git clone https://github.com/Lightricks/LTX-2
27
+ ```
28
+
29
+ The `ltx-trainer` package is part of the `LTX-2` monorepo. Install the dependencies from the repository root,
30
+ then navigate to the trainer package:
31
+
32
+ ```bash
33
+ # From the repository root
34
+ uv sync
35
+ cd packages/ltx-trainer
36
+ ```
37
+
38
+ > [!NOTE]
39
+ > The trainer depends on [`ltx-core`](../../ltx-core/) and [`ltx-pipelines`](../../ltx-pipelines/)
40
+ > packages which are automatically installed from the monorepo.
41
+
42
+ ## 🏋 Training Workflow
43
+
44
+ If you are using an agent-enabled environment with repository skills, you can ask for the
45
+ [`train-model`](../../../.claude/skills/train-model/SKILL.md) skill to run this workflow with you.
46
+ It creates a run workspace, confirms the training mode, prepares data, preprocesses latents,
47
+ launches training, and monitors the run while stopping for approval before expensive steps.
48
+
49
+ ### 1. Choose a Training Mode
50
+
51
+ Start with [`t2v_lora.yaml`](../configs/t2v_lora.yaml) for a first run with videos and captions. For modes such as
52
+ IC-LoRA, inpainting, or outpainting, check [Training Modes](training-modes.md) first because your metadata needs extra
53
+ columns such as `reference_video`, `video_mask`, or `audio_mask` before preprocessing.
54
+
55
+ ### 2. Prepare Your Dataset
56
+
57
+ Organize your videos and captions, then preprocess them:
58
+
59
+ ```bash
60
+ # Split long videos into scenes (optional)
61
+ uv run python scripts/split_scenes.py input.mp4 scenes_output_dir/ --filter-shorter-than 5s
62
+
63
+ # Generate captions for videos (optional)
64
+ uv run python scripts/caption_videos.py scenes_output_dir/ --output dataset.json
65
+
66
+ # Preprocess the dataset (compute latents and embeddings)
67
+ uv run python scripts/process_dataset.py dataset.json \
68
+ --resolution-buckets "960x544x49" \
69
+ --model-path /path/to/ltx-2-model.safetensors \
70
+ --text-encoder-path /path/to/gemma-model
71
+ ```
72
+
73
+ By default, preprocessing writes to `.precomputed/`. Use that directory as `data.preprocessed_data_root`
74
+ in your training config.
75
+
76
+ See [Dataset Preparation](dataset-preparation.md) for detailed instructions.
77
+
78
+ ### 3. Configure Training
79
+
80
+ Create or modify a configuration YAML file. Start with one of the example configs:
81
+
82
+ - [`configs/t2v_lora.yaml`](../configs/t2v_lora.yaml) - Text-to-video LoRA
83
+ - [`configs/t2v_lora_low_vram.yaml`](../configs/t2v_lora_low_vram.yaml) - Same as above, tuned for ~32GB VRAM (INT8 quantization and memory optimizations)
84
+ - [`configs/v2v_ic_lora.yaml`](../configs/v2v_ic_lora.yaml) - IC-LoRA video-to-video
85
+
86
+ Key settings to update:
87
+
88
+ ```yaml
89
+ model:
90
+ model_path: "/path/to/ltx-2-model.safetensors"
91
+ text_encoder_path: "/path/to/gemma-model"
92
+
93
+ data:
94
+ preprocessed_data_root: "/path/to/preprocessed/data"
95
+
96
+ output_dir: "outputs/my_training_run"
97
+ ```
98
+
99
+ See [Configuration Reference](configuration-reference.md) for all available options.
100
+
101
+ ### 4. Start Training
102
+
103
+ ```bash
104
+ uv run python scripts/train.py configs/t2v_lora.yaml
105
+ ```
106
+
107
+ For multi-GPU training:
108
+
109
+ ```bash
110
+ uv run accelerate launch scripts/train.py configs/t2v_lora.yaml
111
+ ```
112
+
113
+ See [Training Guide](training-guide.md) for distributed training and advanced options.
114
+
115
+ ## 🎯 Training Modes
116
+
117
+ > [!TIP]
118
+ > **First time?** Start with [`t2v_lora.yaml`](../configs/t2v_lora.yaml) — it's the simplest mode
119
+ > and only requires videos with captions. You can explore other modes once you've confirmed your
120
+ > setup works.
121
+
122
+ The trainer supports several training modes:
123
+
124
+ | Mode | Description | Example Config |
125
+ |-----------------------|--------------------------------------------|-------------------------------------------------------------------|
126
+ | **Text-to-Video** | Generate video+audio from text prompts | [`t2v_lora.yaml`](../configs/t2v_lora.yaml) |
127
+ | **Image-to-Video** | Animate from a starting image | [`i2v_lora.yaml`](../configs/i2v_lora.yaml) |
128
+ | **Video Extension** | Extend videos temporally (forward/backward)| [`video_extend_lora.yaml`](../configs/video_extend_lora.yaml), [`video_suffix_lora.yaml`](../configs/video_suffix_lora.yaml) |
129
+ | **IC-LoRA (V2V)** | Video-to-video transformations | [`v2v_ic_lora.yaml`](../configs/v2v_ic_lora.yaml) |
130
+ | **Audio-to-Video** | Generate video conditioned on audio | [`a2v_lora.yaml`](../configs/a2v_lora.yaml) |
131
+ | **Video-to-Audio** | Generate audio/foley from video | [`v2a_lora.yaml`](../configs/v2a_lora.yaml) |
132
+ | **Video Inpainting** | Fill in masked regions of video | [`video_inpainting_lora.yaml`](../configs/video_inpainting_lora.yaml) |
133
+ | **Video Outpainting** | Extend video spatially | [`video_outpainting_lora.yaml`](../configs/video_outpainting_lora.yaml) |
134
+ | **Text-to-Audio** | Generate audio from text prompts | [`t2a_lora.yaml`](../configs/t2a_lora.yaml) |
135
+ | **Audio Extension** | Extend audio temporally | [`audio_extend_lora.yaml`](../configs/audio_extend_lora.yaml), [`audio_suffix_lora.yaml`](../configs/audio_suffix_lora.yaml) |
136
+ | **Audio Inpainting** | Fill in masked regions of audio | [`audio_inpainting_lora.yaml`](../configs/audio_inpainting_lora.yaml) |
137
+ | **IC-LoRA (A2A)** | Audio-to-audio transformations | [`a2a_ic_lora.yaml`](../configs/a2a_ic_lora.yaml) |
138
+ | **AV2AV IC-LoRA** | Audio+video IC-LoRA transformations | [`av2av_ic_lora.yaml`](../configs/av2av_ic_lora.yaml) |
139
+ | **Full Fine-tuning** | Full model training (any mode above) | Set `model.training_mode: "full"` |
140
+
141
+ See [Training Modes](training-modes.md) for detailed explanations of each mode.
142
+
143
+ ## Next Steps
144
+
145
+ Once you've completed your first training run, you can:
146
+
147
+ - **Use your trained LoRA for inference** - The [`ltx-pipelines`](../../ltx-pipelines/) package provides
148
+ production-ready inference
149
+ pipelines for various use cases (T2V, I2V, IC-LoRA, etc.). See the package documentation for details.
150
+ - Learn more about [Dataset Preparation](dataset-preparation.md) for advanced preprocessing
151
+ - Explore different [Training Modes](training-modes.md)
152
+ - Dive deeper into [Training Configuration](configuration-reference.md)
153
+ - Understand the model architecture in [LTX-Core Documentation](../../ltx-core/README.md)
154
+
155
+ ## Need Help?
156
+
157
+ If you run into issues at any step, see the [Troubleshooting Guide](troubleshooting.md) for solutions to common
158
+ problems.
159
+
160
+ Join our [Discord community](https://discord.gg/ltxplatform) for real-time help and discussion!
packages/ltx-trainer/docs/training-guide.md ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Training Guide
2
+
3
+ This guide covers how to run training jobs, from basic single-GPU training to advanced distributed setups and automatic
4
+ model uploads.
5
+
6
+ ## ⚡ Basic Training (Single GPU)
7
+
8
+ After preprocessing your dataset and preparing a configuration file, you can start training using the trainer script:
9
+
10
+ ```bash
11
+ uv run python scripts/train.py configs/t2v_lora.yaml
12
+ ```
13
+
14
+ The trainer will:
15
+
16
+ 1. **Load your configuration** and validate all parameters
17
+ 2. **Initialize models** and apply optimizations
18
+ 3. **Run the training loop** with progress tracking
19
+ 4. **Generate validation videos** (if configured)
20
+ 5. **Save the trained weights** in your output directory
21
+
22
+ ### Agent-Assisted Training
23
+
24
+ If your environment supports repository skills, the
25
+ [`train-model`](../../../.claude/skills/train-model/SKILL.md) skill provides an end-to-end
26
+ orchestrator for this package. It asks what you want the model to learn, maps that intent to
27
+ one of the documented [training modes](training-modes.md), probes your filesystem and GPU,
28
+ prepares/preprocesses the dataset, writes a run-specific config, launches training, and
29
+ monitors the job. It uses the trainer docs as its source of truth and stops for approval before
30
+ captioning, preprocessing, or starting expensive training work.
31
+
32
+ ### Output Files
33
+
34
+ **For LoRA training:**
35
+
36
+ - `checkpoints/lora_weights_step_00000.safetensors` - LoRA checkpoint weights, with the current step in the filename
37
+ - `training_config.yaml` - Copy of training configuration
38
+ - `samples/` - Generated validation samples (if enabled)
39
+ - `checkpoints/training_state_step_00000.pt` - Optional resume state, depending on `checkpoints.save_training_state`
40
+
41
+ **For full model fine-tuning:**
42
+
43
+ - `checkpoints/model_weights_step_00000.safetensors` - Full model checkpoint weights, with the current step in the filename
44
+ - `training_config.yaml` - Copy of training configuration
45
+ - `samples/` - Generated validation samples (if enabled)
46
+ - `checkpoints/training_state_step_00000.pt` - Optional resume state, depending on `checkpoints.save_training_state`
47
+
48
+ ## 🖥️ Distributed / Multi-GPU Training
49
+
50
+ We use Hugging Face 🤗 [Accelerate](https://huggingface.co/docs/accelerate/index) for multi-GPU DDP and FSDP.
51
+
52
+ ### Configure Accelerate
53
+
54
+ Run the interactive wizard once to set up your environment (DDP / FSDP, GPU count, etc.):
55
+
56
+ ```bash
57
+ uv run accelerate config
58
+ ```
59
+
60
+ This stores your preferences in `~/.cache/huggingface/accelerate/default_config.yaml`.
61
+
62
+ ### Use the Provided Accelerate Configs (Recommended)
63
+
64
+ We include ready-to-use Accelerate config files in `configs/accelerate/`:
65
+
66
+ - [ddp.yaml](../configs/accelerate/ddp.yaml) — Standard DDP
67
+ - [ddp_compile.yaml](../configs/accelerate/ddp_compile.yaml) — DDP with `torch.compile` (Inductor)
68
+ - [fsdp.yaml](../configs/accelerate/fsdp.yaml) — Standard FSDP (auto-wraps `BasicAVTransformerBlock`)
69
+ - [fsdp_compile.yaml](../configs/accelerate/fsdp_compile.yaml) — FSDP with `torch.compile` (Inductor)
70
+
71
+ Launch with a specific config using `--config_file`:
72
+
73
+ ```bash
74
+ # DDP (2 GPUs shown as example)
75
+ CUDA_VISIBLE_DEVICES=0,1 \
76
+ uv run accelerate launch --config_file configs/accelerate/ddp.yaml \
77
+ scripts/train.py configs/t2v_lora.yaml
78
+
79
+ # DDP + torch.compile
80
+ CUDA_VISIBLE_DEVICES=0,1 \
81
+ uv run accelerate launch --config_file configs/accelerate/ddp_compile.yaml \
82
+ scripts/train.py configs/t2v_lora.yaml
83
+
84
+ # FSDP (4 GPUs shown as example)
85
+ CUDA_VISIBLE_DEVICES=0,1,2,3 \
86
+ uv run accelerate launch --config_file configs/accelerate/fsdp.yaml \
87
+ scripts/train.py configs/t2v_lora.yaml
88
+
89
+ # FSDP + torch.compile
90
+ CUDA_VISIBLE_DEVICES=0,1,2,3 \
91
+ uv run accelerate launch --config_file configs/accelerate/fsdp_compile.yaml \
92
+ scripts/train.py configs/t2v_lora.yaml
93
+ ```
94
+
95
+ **Notes:**
96
+
97
+ - The number of processes is taken from the Accelerate config (`num_processes`). Override with `--num_processes X` or
98
+ restrict GPUs with `CUDA_VISIBLE_DEVICES`.
99
+ - The compile variants enable `torch.compile` with the Inductor backend via Accelerate's `dynamo_config`.
100
+ - FSDP configs auto-wrap the transformer blocks (`fsdp_transformer_layer_cls_to_wrap: BasicAVTransformerBlock`).
101
+
102
+ ### Launch with Your Default Accelerate Config
103
+
104
+ If you prefer to use your default Accelerate profile:
105
+
106
+ ```bash
107
+ # Use settings from your default accelerate config
108
+ uv run accelerate launch scripts/train.py configs/t2v_lora.yaml
109
+
110
+ # Override number of processes on the fly (e.g., 2 GPUs)
111
+ uv run accelerate launch --num_processes 2 scripts/train.py configs/t2v_lora.yaml
112
+
113
+ # Select specific GPUs
114
+ CUDA_VISIBLE_DEVICES=0,1 uv run accelerate launch scripts/train.py configs/t2v_lora.yaml
115
+ ```
116
+
117
+ > [!TIP]
118
+ > You can disable the in-terminal progress bars with `--disable-progress-bars` flag in the trainer CLI if desired.
119
+
120
+ ### Benefits of Distributed Training
121
+
122
+ - **Faster training**: Distribute workload across multiple GPUs
123
+ - **Larger effective batch sizes**: Combine gradients from multiple GPUs
124
+ - **Memory efficiency**: Each GPU handles a portion of the batch
125
+
126
+ > [!NOTE]
127
+ > Distributed training requires that all GPUs have sufficient memory for the model and batch size. The effective batch
128
+ > size becomes `batch_size × num_processes`.
129
+
130
+ ## 🤗 Pushing Models to Hugging Face Hub
131
+
132
+ You can automatically push your trained models to the Hugging Face Hub by adding the following to your configuration:
133
+
134
+ ```yaml
135
+ hub:
136
+ push_to_hub: true
137
+ hub_model_id: "your-username/your-model-name"
138
+ ```
139
+
140
+ ### Prerequisites
141
+
142
+ Before pushing, make sure you:
143
+
144
+ 1. **Have a Hugging Face account** - Sign up at [huggingface.co](https://huggingface.co)
145
+ 2. **Are logged in** via `huggingface-cli login` or have set the `HUGGING_FACE_HUB_TOKEN` environment variable
146
+ 3. **Have write access** to the specified repository (it will be created if it doesn't exist)
147
+
148
+ ### Login Options
149
+
150
+ **Option 1: Interactive login**
151
+
152
+ ```bash
153
+ uv run huggingface-cli login
154
+ ```
155
+
156
+ **Option 2: Environment variable**
157
+
158
+ ```bash
159
+ export HUGGING_FACE_HUB_TOKEN="your_token_here"
160
+ ```
161
+
162
+ ### What Gets Uploaded
163
+
164
+ The trainer will automatically:
165
+
166
+ - **Create a model card** with training details and sample outputs
167
+ - **Upload model weights**
168
+ - **Push sample videos as GIFs** in the model card
169
+ - **Include training configuration and prompts**
170
+
171
+ ## 📊 Weights & Biases Logging
172
+
173
+ Enable experiment tracking with W&B by adding to your configuration:
174
+
175
+ ```yaml
176
+ wandb:
177
+ enabled: true
178
+ project: "ltx-2-trainer"
179
+ entity: null # Your W&B username or team
180
+ tags: [ "ltx2", "lora" ]
181
+ log_validation_videos: true
182
+ ```
183
+
184
+ This will log:
185
+
186
+ - Training loss and learning rate
187
+ - Validation videos
188
+ - Model configuration
189
+ - Training progress
190
+
191
+ ## 🚀 Next Steps
192
+
193
+ After training completes:
194
+
195
+ - **Run inference with your trained LoRA** - The [`ltx-pipelines`](../../ltx-pipelines/) package provides
196
+ production-ready inference
197
+ pipelines that support loading custom LoRAs. Available pipelines include text-to-video, image-to-video,
198
+ IC-LoRA video-to-video, and more. See the [`ltx-pipelines`](../../ltx-pipelines/) package for usage details.
199
+ - **Test your model** with validation prompts
200
+ - **Iterate and improve** based on validation results
201
+ - **Share your results** by pushing to Hugging Face Hub
202
+
203
+ ## 💡 Tips for Successful Training
204
+
205
+ - **Start small**: Begin with a small dataset and a few hundred steps to verify everything works
206
+ - **Monitor validation**: Keep an eye on validation samples to catch overfitting
207
+ - **Adjust learning rate**: Lower learning rates often produce better results
208
+ - **Use gradient checkpointing**: Essential for training with limited GPU memory
209
+ - **Save checkpoints**: Regular checkpoints help recover from interruptions
210
+
211
+ ## Need Help?
212
+
213
+ If you encounter issues during training, see the [Troubleshooting Guide](troubleshooting.md).
214
+
215
+ Join our [Discord community](https://discord.gg/ltxplatform) for real-time help!
packages/ltx-trainer/docs/training-modes.md ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Training Modes Guide
2
+
3
+ The trainer uses the **flexible** training strategy (`name: "flexible"`) — a unified conditioning framework that
4
+ supports all training modes through configuration. Every scenario is expressed by setting `is_generated` on each
5
+ modality and adding optional conditions, rather than choosing a separate strategy class.
6
+
7
+ ## Key Concepts
8
+
9
+ Before diving into individual modes, here are the core ideas behind the flexible strategy:
10
+
11
+ - **`is_generated: true`** — the modality is denoised during training and contributes to the loss. This is the
12
+ modality the model learns to generate.
13
+ - **`is_generated: false`** — the modality is frozen (sigma=0, no noise, no loss). It passes through the transformer
14
+ clean and acts as cross-modal conditioning for the generated modality.
15
+ - **At least one modality must have `is_generated: true`.**
16
+ - **Conditions** are per-modality and can be composed (e.g., `reference` + `first_frame` together on the video
17
+ modality).
18
+ - Audio does **not** support `first_frame` or `spatial_crop` conditions — only `prefix`, `suffix`, `mask`,
19
+ and `reference`.
20
+
21
+ > [!TIP]
22
+ > If you are using an agent-enabled environment with repository skills and are unsure which mode to choose,
23
+ > ask for the [`train-model`](../../../.claude/skills/train-model/SKILL.md) skill. It maps your intent to one of
24
+ > these configs and walks through dataset preparation, preprocessing, launch, and monitoring.
25
+
26
+ ## 📊 Quick Reference
27
+
28
+ | Mode | Video | Audio | Conditions | Config |
29
+ |-----------------------|-----------|-----------|---------------------|--------|
30
+ | **T2V** | Generated | Generated | — | [`t2v_lora`](../configs/t2v_lora.yaml) |
31
+ | **I2V** | Generated | Generated | `first_frame` | [`i2v_lora`](../configs/i2v_lora.yaml) |
32
+ | **Video Extension** | Generated | Generated | `prefix`/`suffix` | [`video_extend_lora`](../configs/video_extend_lora.yaml) |
33
+ | **V2V IC-LoRA** | Generated | — | `reference` | [`v2v_ic_lora`](../configs/v2v_ic_lora.yaml) |
34
+ | **A2V** | Generated | Frozen | — | [`a2v_lora`](../configs/a2v_lora.yaml) |
35
+ | **V2A (Foley)** | Frozen | Generated | — | [`v2a_lora`](../configs/v2a_lora.yaml) |
36
+ | **Video Inpainting** | Generated | — | `mask` | [`video_inpainting_lora`](../configs/video_inpainting_lora.yaml) |
37
+ | **Video Outpainting** | Generated | — | `spatial_crop` | [`video_outpainting_lora`](../configs/video_outpainting_lora.yaml) |
38
+ | **T2A** | — | Generated | — | [`t2a_lora`](../configs/t2a_lora.yaml) |
39
+ | **Audio Extension** | — | Generated | `prefix`/`suffix` | [`audio_extend_lora`](../configs/audio_extend_lora.yaml) |
40
+ | **Audio Inpainting** | — | Generated | `mask` | [`audio_inpainting_lora`](../configs/audio_inpainting_lora.yaml) |
41
+ | **A2A IC-LoRA** | — | Generated | `reference` | [`a2a_ic_lora`](../configs/a2a_ic_lora.yaml) |
42
+ | **AV2AV IC-LoRA** | Generated | Generated | `reference` (both) | [`av2av_ic_lora`](../configs/av2av_ic_lora.yaml) |
43
+
44
+ ---
45
+
46
+ ## 🎯 Text-to-Video (T2V)
47
+
48
+ Generate video and audio from text prompts. Both modalities are denoised with no additional conditions.
49
+
50
+ ```yaml
51
+ training_strategy:
52
+ name: "flexible"
53
+ video:
54
+ is_generated: true
55
+ latents_dir: "latents"
56
+ audio:
57
+ is_generated: true
58
+ latents_dir: "audio_latents"
59
+ ```
60
+
61
+ **Example config:** 📄 [t2v_lora.yaml](../configs/t2v_lora.yaml)
62
+
63
+ ---
64
+
65
+ ## 🖼️ Image-to-Video (I2V)
66
+
67
+ Generate video conditioned on a starting image. The first frame is provided as a clean conditioning signal — no noise,
68
+ timestep=0, excluded from loss. The `probability` parameter controls how often first-frame conditioning is applied;
69
+ remaining samples train in pure T2V mode.
70
+
71
+ ```yaml
72
+ training_strategy:
73
+ name: "flexible"
74
+ video:
75
+ is_generated: true
76
+ latents_dir: "latents"
77
+ conditions:
78
+ - type: first_frame
79
+ probability: 0.5
80
+ audio:
81
+ is_generated: true
82
+ latents_dir: "audio_latents"
83
+ ```
84
+
85
+ **Example config:** 📄 [i2v_lora.yaml](../configs/i2v_lora.yaml)
86
+
87
+ ---
88
+
89
+ ## ⏩ Video Extension
90
+
91
+ Extend a video forward (or backward) in time. Prefix or suffix conditioning provides a span of existing latent frames
92
+ as clean conditioning. The `temporal_boundary` sets the number of **latent frames** used as context (each latent frame
93
+ = 8 pixel frames due to temporal compression).
94
+
95
+ ```yaml
96
+ training_strategy:
97
+ name: "flexible"
98
+ video:
99
+ is_generated: true
100
+ latents_dir: "latents"
101
+ conditions:
102
+ - type: prefix # or "suffix" for backward extension
103
+ temporal_boundary: 8 # 8 latent frames = 64 pixel frames
104
+ probability: 1.0
105
+ audio:
106
+ is_generated: true
107
+ latents_dir: "audio_latents"
108
+ ```
109
+
110
+ > [!NOTE]
111
+ > The `prefix` and `suffix` conditions also work on the audio modality for audio extension.
112
+ > Set `temporal_boundary` on the audio modality's conditions list to condition on a prefix or suffix
113
+ > of the audio latents.
114
+
115
+ **Example configs:** 📄 [video_extend_lora.yaml](../configs/video_extend_lora.yaml) (forward), 📄 [video_suffix_lora.yaml](../configs/video_suffix_lora.yaml) (backward)
116
+
117
+ ---
118
+
119
+ ## 🔄 IC-LoRA / Video-to-Video (V2V)
120
+
121
+ In-Context LoRA learns transformations from paired videos. Pre-encoded reference latents are concatenated to the target
122
+ sequence — reference tokens participate in bidirectional self-attention but receive no noise and are excluded from loss.
123
+ This enables control adapters (depth, pose), style transfer, deblurring, colorization, and more.
124
+
125
+ ```yaml
126
+ training_strategy:
127
+ name: "flexible"
128
+ video:
129
+ is_generated: true
130
+ latents_dir: "latents"
131
+ conditions:
132
+ - type: reference
133
+ latents_dir: "reference_latents"
134
+ probability: 1.0
135
+ - type: first_frame # optional — composable with reference
136
+ probability: 0.2
137
+ ```
138
+
139
+ > [!NOTE]
140
+ > IC-LoRA is video-only by default (no audio modality block). Conditions can be composed — the example above also
141
+ > applies first-frame conditioning with 20% probability alongside the reference.
142
+ > Use [AV2AV IC-LoRA](#av2av-ic-lora) when both video and audio references should be trained jointly.
143
+
144
+ **Example config:** 📄 [v2v_ic_lora.yaml](../configs/v2v_ic_lora.yaml)
145
+
146
+ ### Dataset Requirements
147
+
148
+ - **Paired videos** — each target video has a corresponding reference video
149
+ - **Same frame count** between reference and target
150
+ - Reference videos can optionally be at **lower spatial resolution** (see [Scaled Reference](#scaled-reference-conditioning) below)
151
+ - Both must be **preprocessed** before training
152
+
153
+ **Dataset structure:**
154
+
155
+ ```
156
+ preprocessed_data_root/
157
+ ├── latents/ # Target video latents
158
+ ├── conditions/ # Text embeddings
159
+ └── reference_latents/ # Reference video latents (conditioning input)
160
+ ```
161
+
162
+ ### Generating Reference Videos
163
+
164
+ Use the `compute_reference.py` script to generate reference videos (e.g., Canny edge maps) for a dataset:
165
+
166
+ ```bash
167
+ uv run python scripts/compute_reference.py scenes_output_dir/ \
168
+ --output scenes_output_dir/dataset.json
169
+ ```
170
+
171
+ To compute a different condition (depth maps, pose skeletons, etc.), modify the `compute_reference()` function in the
172
+ script.
173
+
174
+ > [!NOTE]
175
+ > `compute_reference.py` writes generated references to the `reference_video` column, which
176
+ > `process_dataset.py` detects automatically. The legacy `ref_media_path` column is also accepted.
177
+
178
+ ### Scaled Reference Conditioning
179
+
180
+ For more efficient training and inference, use **downscaled reference videos** while keeping targets at full
181
+ resolution. During training, the strategy infers the spatial and temporal scale factors from the preprocessed
182
+ reference and target latents and adjusts positional encodings accordingly. This reduces conditioning tokens, leading to:
183
+
184
+ - **Faster training** — shorter sequence lengths
185
+ - **Faster inference** — reduced memory usage
186
+ - **Same aspect ratio** maintained between reference and target
187
+
188
+ Preprocess with the `--reference-downscale-factor` option:
189
+
190
+ ```bash
191
+ uv run python scripts/process_dataset.py dataset.json \
192
+ --resolution-buckets 768x768x25 \
193
+ --model-path /path/to/ltx2.safetensors \
194
+ --text-encoder-path /path/to/gemma \
195
+ --reference-downscale-factor 2
196
+ ```
197
+
198
+ > [!NOTE]
199
+ > The `reference_video` column is auto-detected by convention — no `--reference-column` flag needed.
200
+
201
+ Validation encodes reference media on the fly, so set `downscale_factor` and `temporal_scale_factor`
202
+ on each `reference` validation condition to match the preprocessing factors:
203
+
204
+ ```yaml
205
+ validation:
206
+ samples:
207
+ - prompt: "..."
208
+ conditions:
209
+ - type: reference
210
+ video: "/path/to/reference.mp4"
211
+ downscale_factor: 2
212
+ temporal_scale_factor: 1
213
+ include_in_output: true
214
+ ```
215
+
216
+ > [!NOTE]
217
+ > The scale factor must be a positive integer, and all dimensions must be divisible by 32.
218
+ > Common values are 1 (no scaling), 2 (half resolution), or 4 (quarter resolution).
219
+
220
+ ---
221
+
222
+ ## 🔊 Audio-to-Video (A2V)
223
+
224
+ Generate video conditioned on frozen audio. Audio passes through the transformer clean (sigma=0) and influences video
225
+ via the built-in cross-modal attention. Only video is denoised.
226
+
227
+ ```yaml
228
+ training_strategy:
229
+ name: "flexible"
230
+ video:
231
+ is_generated: true
232
+ latents_dir: "latents"
233
+ audio:
234
+ is_generated: false
235
+ latents_dir: "audio_latents"
236
+ ```
237
+
238
+ **Example config:** 📄 [a2v_lora.yaml](../configs/a2v_lora.yaml)
239
+
240
+ ---
241
+
242
+ ## 🎵 Video-to-Audio / Foley (V2A)
243
+
244
+ Generate audio (Foley) conditioned on frozen video. Video passes through the transformer clean (sigma=0) and
245
+ conditions audio via cross-modal attention. Only audio is denoised.
246
+
247
+ ```yaml
248
+ training_strategy:
249
+ name: "flexible"
250
+ video:
251
+ is_generated: false
252
+ latents_dir: "latents"
253
+ audio:
254
+ is_generated: true
255
+ latents_dir: "audio_latents"
256
+ ```
257
+
258
+ **Example config:** 📄 [v2a_lora.yaml](../configs/v2a_lora.yaml)
259
+
260
+ ---
261
+
262
+ ## 🎭 Video Inpainting
263
+
264
+ Fill in masked regions of a video. Per-sample masks loaded from disk define which tokens are conditioning and which
265
+ must be generated. Masks are thresholded at `0.5` to match validation/inference: tokens with `mask > 0.5` receive clean
266
+ latents and timestep=0 and are excluded from loss; tokens with `mask <= 0.5` are denoised normally and contribute to
267
+ loss.
268
+
269
+ ```yaml
270
+ training_strategy:
271
+ name: "flexible"
272
+ video:
273
+ is_generated: true
274
+ latents_dir: "latents"
275
+ conditions:
276
+ - type: mask
277
+ mask_dir: "video_masks"
278
+ probability: 1.0
279
+ ```
280
+
281
+ **Dataset structure:**
282
+
283
+ ```
284
+ preprocessed_data_root/
285
+ ├── latents/ # Video latents
286
+ ├── conditions/ # Text embeddings
287
+ └── video_masks/ # Per-sample binary masks (1 → conditioning, 0 → generate)
288
+ ```
289
+
290
+ In dataset metadata, provide mask media via the `video_mask` column; preprocessing converts it into `video_masks/`.
291
+
292
+ **Example config:** 📄 [video_inpainting_lora.yaml](../configs/video_inpainting_lora.yaml)
293
+
294
+ ---
295
+
296
+ ## 🌅 Video Outpainting
297
+
298
+ Extend a video spatially beyond its original boundaries. A rectangular pixel region is provided as clean conditioning
299
+ (no noise, timestep=0, excluded from loss) — the model learns to generate the surrounding content. The `spatial_region`
300
+ is specified in pixel coordinates `[y1, x1, y2, x2]` and automatically converted to latent space.
301
+
302
+ ```yaml
303
+ training_strategy:
304
+ name: "flexible"
305
+ video:
306
+ is_generated: true
307
+ latents_dir: "latents"
308
+ conditions:
309
+ - type: spatial_crop
310
+ spatial_region: [0, 0, 288, 576] # y1, x1, y2, x2 in pixels
311
+ probability: 1.0
312
+ ```
313
+
314
+ > [!NOTE]
315
+ > `spatial_crop` is a video-only condition — it is not supported on the audio modality.
316
+
317
+ **Example config:** 📄 [video_outpainting_lora.yaml](../configs/video_outpainting_lora.yaml)
318
+
319
+ ---
320
+
321
+ ## 🔈 Text-to-Audio (T2A)
322
+
323
+ Generate audio from text prompts with no video modality. Only the audio branch of the transformer is denoised. Since
324
+ no video modality is configured, this mode uses **audio-only LoRA targets** — explicitly targeting `audio_attn1`,
325
+ `audio_attn2`, and `audio_ff` modules.
326
+
327
+ ```yaml
328
+ training_strategy:
329
+ name: "flexible"
330
+ audio:
331
+ is_generated: true
332
+ latents_dir: "audio_latents"
333
+ ```
334
+
335
+ > [!NOTE]
336
+ > With no `video` block in the strategy, the trainer only loads audio latents and text embeddings. LoRA adapters
337
+ > should explicitly target audio modules (e.g., `audio_attn1.to_k`) rather than short patterns like `to_k` which
338
+ > would also match video modules. See [LoRA Target Modules Guidance](#lora-target-modules-guidance) below.
339
+
340
+ **Example config:** 📄 [t2a_lora.yaml](../configs/t2a_lora.yaml)
341
+
342
+ ---
343
+
344
+ ## 🔊 Audio Extension
345
+
346
+ Extend audio forward (prefix) or backward (suffix) in time — the audio equivalent of Video Extension. A span of
347
+ existing audio latent frames is provided as clean conditioning, and the model generates the continuation. The
348
+ `temporal_boundary` sets the number of latent frames used as context. This mode uses **audio-only LoRA targets**.
349
+
350
+ ```yaml
351
+ training_strategy:
352
+ name: "flexible"
353
+ audio:
354
+ is_generated: true
355
+ latents_dir: "audio_latents"
356
+ conditions:
357
+ - type: prefix # or "suffix" for backward extension
358
+ temporal_boundary: 8
359
+ probability: 1.0
360
+ ```
361
+
362
+ **Example configs:** 📄 [audio_extend_lora.yaml](../configs/audio_extend_lora.yaml), 📄 [audio_suffix_lora.yaml](../configs/audio_suffix_lora.yaml)
363
+
364
+ ---
365
+
366
+ ## 🎭 Audio Inpainting
367
+
368
+ Fill in masked regions of audio. Per-sample masks loaded from disk define which audio tokens are conditioning and
369
+ which must be generated — the audio equivalent of Video Inpainting. Masks are thresholded at `0.5` with the same
370
+ binary semantics as video inpainting. This mode uses **audio-only LoRA targets**.
371
+
372
+ ```yaml
373
+ training_strategy:
374
+ name: "flexible"
375
+ audio:
376
+ is_generated: true
377
+ latents_dir: "audio_latents"
378
+ conditions:
379
+ - type: mask
380
+ mask_dir: "audio_masks"
381
+ probability: 1.0
382
+ ```
383
+
384
+ **Dataset structure:**
385
+
386
+ ```
387
+ preprocessed_data_root/
388
+ ├── conditions/ # Text embeddings
389
+ ├── audio_latents/ # Audio latents
390
+ └── audio_masks/ # Per-sample binary masks (1 → conditioning, 0 → generate)
391
+ ```
392
+
393
+ In dataset metadata, provide mask media via the `audio_mask` column; preprocessing converts it into `audio_masks/`.
394
+
395
+ **Example config:** 📄 [audio_inpainting_lora.yaml](../configs/audio_inpainting_lora.yaml)
396
+
397
+ ---
398
+
399
+ ## 🔄 IC-LoRA / Audio-to-Audio (A2A)
400
+
401
+ In-Context LoRA for audio-to-audio transformations. Pre-encoded reference audio latents are concatenated to the target
402
+ sequence — reference tokens participate in bidirectional self-attention but receive no noise and are excluded from loss.
403
+ This enables audio style transfer, voice conversion, sound effect transformation, and more. This mode uses
404
+ **audio-only LoRA targets**.
405
+
406
+ ```yaml
407
+ training_strategy:
408
+ name: "flexible"
409
+ audio:
410
+ is_generated: true
411
+ latents_dir: "audio_latents"
412
+ conditions:
413
+ - type: reference
414
+ latents_dir: "reference_audio_latents"
415
+ probability: 1.0
416
+ ```
417
+
418
+ **Dataset structure:**
419
+
420
+ ```
421
+ preprocessed_data_root/
422
+ ├── conditions/ # Text embeddings
423
+ ├── audio_latents/ # Target audio latents
424
+ └── reference_audio_latents/ # Reference audio latents (conditioning input)
425
+ ```
426
+
427
+ **Example config:** 📄 [a2a_ic_lora.yaml](../configs/a2a_ic_lora.yaml)
428
+
429
+ ---
430
+
431
+ ## 🔄 AV2AV IC-LoRA
432
+
433
+ Joint audio-video In-Context LoRA — both modalities have reference conditioning. Pre-encoded reference latents are
434
+ concatenated to each modality's target sequence independently. This enables joint audiovisual transformations such as
435
+ synchronized style transfer across both video and audio.
436
+
437
+ ```yaml
438
+ training_strategy:
439
+ name: "flexible"
440
+ video:
441
+ is_generated: true
442
+ latents_dir: "latents"
443
+ conditions:
444
+ - type: reference
445
+ latents_dir: "reference_latents"
446
+ probability: 1.0
447
+ audio:
448
+ is_generated: true
449
+ latents_dir: "audio_latents"
450
+ conditions:
451
+ - type: reference
452
+ latents_dir: "reference_audio_latents"
453
+ probability: 1.0
454
+ ```
455
+
456
+ > [!NOTE]
457
+ > Unlike audio-only IC-LoRA (A2A), AV2AV uses short LoRA target patterns like `"to_k"` to match all branches
458
+ > (video, audio, and cross-modal attention), since both modalities are trained.
459
+
460
+ **Dataset structure:**
461
+
462
+ ```
463
+ preprocessed_data_root/
464
+ ├── latents/ # Target video latents
465
+ ├── audio_latents/ # Target audio latents
466
+ ├── conditions/ # Text embeddings
467
+ ├── reference_latents/ # Reference video latents (conditioning input)
468
+ └── reference_audio_latents/ # Reference audio latents (conditioning input)
469
+ ```
470
+
471
+ **Example config:** 📄 [av2av_ic_lora.yaml](../configs/av2av_ic_lora.yaml)
472
+
473
+ ---
474
+
475
+ ## 🔥 Full Model Fine-tuning
476
+
477
+ All modes above default to `training_mode: "lora"`. For full fine-tuning, set `training_mode: "full"` — this updates
478
+ all model parameters rather than adding LoRA adapters.
479
+
480
+ ```yaml
481
+ model:
482
+ training_mode: "full"
483
+
484
+ training_strategy:
485
+ name: "flexible"
486
+ video:
487
+ is_generated: true
488
+ latents_dir: "latents"
489
+ audio:
490
+ is_generated: true
491
+ latents_dir: "audio_latents"
492
+ ```
493
+
494
+ > [!IMPORTANT]
495
+ > Full fine-tuning requires multiple high-end GPUs (e.g., 4-8× H100 80GB) and distributed training with FSDP.
496
+ > See [Training Guide](training-guide.md) for multi-GPU setup instructions.
497
+
498
+ ---
499
+
500
+ ## 🎛️ LoRA Target Modules Guidance
501
+
502
+ The `target_modules` configuration determines which transformer modules receive LoRA adapters. The right choice depends
503
+ on whether your training involves cross-modal (audio ↔ video) interaction.
504
+
505
+ **For T2V, I2V, A2V, V2A, or any mode involving both modalities** — use short patterns to match all branches
506
+ (video, audio, and cross-modal attention):
507
+
508
+ ```yaml
509
+ target_modules:
510
+ - "to_k"
511
+ - "to_q"
512
+ - "to_v"
513
+ - "to_out.0"
514
+ ```
515
+
516
+ > [!IMPORTANT]
517
+ > Short patterns like `"to_k"` match video modules (`attn1.to_k`, `attn2.to_k`), audio modules
518
+ > (`audio_attn1.to_k`, `audio_attn2.to_k`), and cross-modal modules (`audio_to_video_attn.to_k`,
519
+ > `video_to_audio_attn.to_k`). The cross-modal attention modules enable bidirectional information flow between
520
+ > audio and video, which is critical for synchronized audiovisual generation.
521
+ > See [Understanding Target Modules](configuration-reference.md#understanding-target-modules) for detailed guidance.
522
+
523
+ **For video-only IC-LoRA** — explicitly target video modules (including FFN layers for better transformation quality):
524
+
525
+ ```yaml
526
+ target_modules:
527
+ - "attn1.to_k"
528
+ - "attn1.to_q"
529
+ - "attn1.to_v"
530
+ - "attn1.to_out.0"
531
+ - "attn2.to_k"
532
+ - "attn2.to_q"
533
+ - "attn2.to_v"
534
+ - "attn2.to_out.0"
535
+ - "ff.net.0.proj"
536
+ - "ff.net.2"
537
+ ```
538
+
539
+ **For audio-only modes (T2A, Audio Extension, Audio Inpainting, A2A IC-LoRA)** — explicitly target audio modules:
540
+
541
+ ```yaml
542
+ target_modules:
543
+ - "audio_attn1.to_k"
544
+ - "audio_attn1.to_q"
545
+ - "audio_attn1.to_v"
546
+ - "audio_attn1.to_out.0"
547
+ - "audio_attn2.to_k"
548
+ - "audio_attn2.to_q"
549
+ - "audio_attn2.to_v"
550
+ - "audio_attn2.to_out.0"
551
+ - "audio_ff.net.0.proj"
552
+ - "audio_ff.net.2"
553
+ ```
554
+
555
+ > [!NOTE]
556
+ > Audio-only modes have no `video` block in the strategy, so there is no need to train video or cross-modal
557
+ > attention modules. Targeting only `audio_*` modules keeps the LoRA small and focused.
558
+
559
+ ---
560
+
561
+ ## 🎬 Using Trained Models for Inference
562
+
563
+ After training, use the [`ltx-pipelines`](../../ltx-pipelines/) package for production inference with your trained
564
+ LoRAs:
565
+
566
+ | Training Mode | Recommended Pipeline |
567
+ |-------------------------|-------------------------------------------------------|
568
+ | T2V / I2V / A2V / Extension / Inpainting / Outpainting | `TI2VidOneStagePipeline` or `TI2VidTwoStagesPipeline` |
569
+ | IC-LoRA (V2V / A2A / AV2AV) | `ICLoraPipeline` |
570
+ | V2A (Foley) / T2A / Audio Extension / Audio Inpainting | `TI2VidOneStagePipeline` or `TI2VidTwoStagesPipeline` |
571
+
572
+ All pipelines support loading custom LoRAs via the `loras` parameter. See the [`ltx-pipelines`](../../ltx-pipelines/)
573
+ package documentation for detailed usage instructions.
574
+
575
+ > [!NOTE]
576
+ > You can generate audio during validation even if you're not training the audio branch.
577
+ > Set `validation.generate_audio: true` independently of whether audio has `is_generated: true`.
578
+
579
+ ---
580
+
581
+ ## 🔄 Migration from Legacy Strategies
582
+
583
+ Legacy `text_to_video` and `video_to_video` strategy configs are forward-compatible and will continue to work (with a
584
+ deprecation warning). We recommend migrating to `flexible` for access to all conditioning modes.
585
+
586
+ ---
587
+
588
+ ## 🚀 Next Steps
589
+
590
+ Once you've chosen your training mode:
591
+
592
+ - Set up your dataset using [Dataset Preparation](dataset-preparation.md)
593
+ - Configure your training parameters in [Configuration Reference](configuration-reference.md)
594
+ - Start training with the [Training Guide](training-guide.md)
595
+
596
+ > [!TIP]
597
+ > Need a training mode that's not covered here?
598
+ > First check whether it can be expressed by composing existing `flexible` conditions. Use
599
+ > [Implementing Custom Training Strategies](custom-training-strategies.md) only for custom losses,
600
+ > noising rules, model outputs, or preprocessing that cannot be represented by configuration.
packages/ltx-trainer/docs/troubleshooting.md ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Troubleshooting Guide
2
+
3
+ This guide covers common issues and solutions when training with the LTX-2 trainer.
4
+
5
+ ## 🔧 VRAM and Memory Issues
6
+
7
+ Memory management is crucial for successful training with LTX-2.
8
+
9
+ > [!TIP]
10
+ > For GPUs with 32GB VRAM, use the pre-configured low VRAM config:
11
+ > [`configs/t2v_lora_low_vram.yaml`](../configs/t2v_lora_low_vram.yaml)
12
+ > which combines 8-bit optimizer, INT8 quantization, and reduced LoRA rank.
13
+
14
+ ### Memory Optimization Techniques
15
+
16
+ #### 1. Enable Gradient Checkpointing
17
+
18
+ Gradient checkpointing trades training speed for memory savings. **Highly recommended** for most training runs:
19
+
20
+ ```yaml
21
+ optimization:
22
+ enable_gradient_checkpointing: true
23
+ ```
24
+
25
+ #### 2. Enable 8-bit Text Encoder
26
+
27
+ Load the Gemma text encoder in 8-bit precision to save GPU memory:
28
+
29
+ ```yaml
30
+ acceleration:
31
+ load_text_encoder_in_8bit: true
32
+ ```
33
+
34
+ #### 3. Reduce Batch Size
35
+
36
+ Lower the batch size if you encounter out-of-memory errors:
37
+
38
+ ```yaml
39
+ optimization:
40
+ batch_size: 1 # Start with 1 and increase gradually
41
+ ```
42
+
43
+ Use gradient accumulation to maintain a larger effective batch size:
44
+
45
+ ```yaml
46
+ optimization:
47
+ batch_size: 1
48
+ gradient_accumulation_steps: 4 # Effective batch size = 4
49
+ ```
50
+
51
+ #### 4. Use Lower Resolution
52
+
53
+ Reduce spatial or temporal dimensions to save memory:
54
+
55
+ ```bash
56
+ # Smaller spatial resolution
57
+ uv run python scripts/process_dataset.py dataset.json \
58
+ --resolution-buckets "512x512x49" \
59
+ --model-path /path/to/model.safetensors \
60
+ --text-encoder-path /path/to/gemma
61
+
62
+ # Fewer frames
63
+ uv run python scripts/process_dataset.py dataset.json \
64
+ --resolution-buckets "960x544x25" \
65
+ --model-path /path/to/model.safetensors \
66
+ --text-encoder-path /path/to/gemma
67
+ ```
68
+
69
+ #### 5. Enable Model Quantization
70
+
71
+ Use quantization to reduce memory usage:
72
+
73
+ ```yaml
74
+ acceleration:
75
+ quantization: "int8-quanto" # Options: int8-quanto, int4-quanto, fp8-quanto
76
+ ```
77
+
78
+ #### 6. Use 8-bit Optimizer
79
+
80
+ The 8-bit AdamW optimizer uses less memory:
81
+
82
+ ```yaml
83
+ optimization:
84
+ optimizer_type: "adamw8bit"
85
+ ```
86
+
87
+ #### 7. Offload Optimizer State During Validation
88
+
89
+ If you OOM specifically during validation video sampling — typically in
90
+ full fine-tunes or high-rank LoRA runs where AdamW state and the VAE decoder
91
+ can't coexist on the GPU — offload optimizer state to CPU during sampling:
92
+
93
+ ```yaml
94
+ acceleration:
95
+ offload_optimizer_during_validation: true
96
+ ```
97
+
98
+ The offload + reload happens once per validation interval, not per step.
99
+ No effect for FSDP (sharded state).
100
+
101
+ ---
102
+
103
+ ## ⚠️ Common Usage Issues
104
+
105
+ ### Issue: "No module named 'ltx_trainer'" Error
106
+
107
+ **Solution:**
108
+ Ensure you've installed the dependencies and are using `uv run` to execute scripts:
109
+
110
+ ```bash
111
+ # From the repository root
112
+ uv sync
113
+ cd packages/ltx-trainer
114
+ uv run python scripts/train.py configs/t2v_lora.yaml
115
+ ```
116
+
117
+ > [!TIP]
118
+ > Always use `uv run` to execute Python scripts. This automatically uses the correct virtual environment
119
+ > without requiring manual activation.
120
+
121
+ ### Issue: "Gemma model path is not a directory" Error
122
+
123
+ **Solution:**
124
+ The `text_encoder_path` must point to a directory containing the Gemma model, not a file:
125
+
126
+ ```yaml
127
+ model:
128
+ model_path: "/path/to/ltx-2-model.safetensors" # File path
129
+ text_encoder_path: "/path/to/gemma-model/" # Directory path
130
+ ```
131
+
132
+ ### Issue: "Model path does not exist" Error
133
+
134
+ **Solution:**
135
+ LTX-2 requires local model paths. URLs are not supported:
136
+
137
+ ```yaml
138
+ # ✅ Correct - local path
139
+ model:
140
+ model_path: "/path/to/ltx-2-model.safetensors"
141
+
142
+ # ❌ Wrong - URL not supported
143
+ model:
144
+ model_path: "https://huggingface.co/..."
145
+ ```
146
+
147
+ ### Issue: "Frames must satisfy frames % 8 == 1" Error
148
+
149
+ **Solution:**
150
+ LTX-2 requires the number of frames to satisfy `frames % 8 == 1`:
151
+
152
+ - ✅ Valid: 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, 97, 121
153
+ - ❌ Invalid: 24, 32, 48, 64, 100
154
+
155
+ ### Issue: Slow Training Speed
156
+
157
+ **Optimizations:**
158
+
159
+ 1. **Disable gradient checkpointing** (if you have enough VRAM):
160
+
161
+ ```yaml
162
+ optimization:
163
+ enable_gradient_checkpointing: false
164
+ ```
165
+
166
+
167
+ 2. **Use torch.compile** via Accelerate:
168
+
169
+ ```bash
170
+ uv run accelerate launch --config_file configs/accelerate/ddp_compile.yaml \
171
+ scripts/train.py configs/t2v_lora.yaml
172
+ ```
173
+
174
+ ### Issue: Poor Quality Validation Outputs
175
+
176
+ **Solutions:**
177
+
178
+ 1. **Use conditioned validation:** For more reliable validation, use image-to-video (first-frame conditioning) rather than pure text-to-video:
179
+
180
+ ```yaml
181
+ validation:
182
+ samples:
183
+ - prompt: "a professional portrait video of a person"
184
+ conditions:
185
+ - type: first_frame
186
+ image_or_video: "/path/to/first_frame.png"
187
+ ```
188
+
189
+ 2. **Increase inference steps:**
190
+
191
+ ```yaml
192
+ validation:
193
+ inference_steps: 30
194
+ ```
195
+
196
+ 3. **Adjust guidance settings:**
197
+
198
+ ```yaml
199
+ validation:
200
+ guidance_scale: 4.0 # CFG scale (recommended: 4.0)
201
+ stg_scale: 1.0 # STG scale for temporal coherence (recommended: 1.0)
202
+ stg_blocks: [29] # Transformer block to perturb
203
+ ```
204
+
205
+ 4. **Check caption quality:**
206
+ Review and manually edit captions for accuracy if using auto-generated captions.
207
+ LTX-2 prefers long, detailed captions that describe both visual content and audio (e.g., ambient sounds, speech,
208
+ music).
209
+
210
+ 5. **Check target modules:**
211
+ Ensure your `target_modules` configuration matches your training goals. For audio-video training,
212
+ use patterns that match both branches (e.g., `"to_k"` instead of `"attn1.to_k"`).
213
+ See [Understanding Target Modules](configuration-reference.md#understanding-target-modules) for details.
214
+
215
+ 6. **Adjust LoRA rank:**
216
+ Try higher values for more capacity:
217
+
218
+ ```yaml
219
+ lora:
220
+ rank: 64 # Or 128 for more capacity
221
+ ```
222
+
223
+ 7. **Increase training steps:**
224
+
225
+ ```yaml
226
+ optimization:
227
+ steps: 3000
228
+ ```
229
+
230
+ ---
231
+
232
+ ## 🔍 Debugging Tools
233
+
234
+ ### Monitor GPU Memory Usage
235
+
236
+ Track memory usage during training:
237
+
238
+ ```bash
239
+ # Watch GPU memory in real-time
240
+ watch -n 1 nvidia-smi
241
+
242
+ # Log memory usage to file
243
+ nvidia-smi --query-gpu=memory.used,memory.total --format=csv --loop=5 > memory_log.csv
244
+ ```
245
+
246
+ ### Verify Preprocessed Data
247
+
248
+ Decode latents to visualize the preprocessed videos:
249
+
250
+ ```bash
251
+ uv run python scripts/decode_latents.py dataset/.precomputed/latents debug_output \
252
+ --model-path /path/to/model.safetensors
253
+ ```
254
+
255
+ To also decode audio latents, add the `--with-audio` flag:
256
+
257
+ ```bash
258
+ uv run python scripts/decode_latents.py dataset/.precomputed/latents debug_output \
259
+ --model-path /path/to/model.safetensors \
260
+ --with-audio
261
+ ```
262
+
263
+ Compare decoded videos and audio with originals to ensure quality.
264
+
265
+ ---
266
+
267
+ ## 💡 Best Practices
268
+
269
+ ### Before Training
270
+
271
+ - [ ] Test preprocessing with a small subset first
272
+ - [ ] Verify all video files are accessible
273
+ - [ ] Check available GPU memory
274
+ - [ ] Review configuration against hardware capabilities
275
+ - [ ] Ensure model and text encoder paths are correct
276
+
277
+ ### During Training
278
+
279
+ - [ ] Monitor GPU memory usage
280
+ - [ ] Check loss convergence regularly
281
+ - [ ] Review validation samples periodically
282
+ - [ ] Save checkpoints frequently
283
+
284
+ ### After Training
285
+
286
+ - [ ] Test trained model with diverse prompts
287
+ - [ ] Document training parameters and results
288
+ - [ ] Archive training data and configs
289
+
290
+ ## 🆘 Getting Help
291
+
292
+ If you're still experiencing issues:
293
+
294
+ 1. **Check logs:** Review console output for error details
295
+ 2. **Search issues:** Look through GitHub issues for similar problems
296
+ 3. **Provide details:** When reporting issues, include:
297
+ - Hardware specifications (GPU model, VRAM)
298
+ - Configuration file used
299
+ - Complete error message
300
+ - Steps to reproduce the issue
301
+
302
+ ---
303
+
304
+ ## 🤝 Join the Community
305
+
306
+ Have questions, want to share your results, or need real-time help?
307
+ Join our [community Discord server](https://discord.gg/ltxplatform)
308
+ to connect with other users and the development team!
309
+
310
+ - Get troubleshooting help
311
+ - Share your training results and workflows
312
+ - Stay up to date with announcements and updates
313
+
314
+ We look forward to seeing you there!
packages/ltx-trainer/docs/utility-scripts.md ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Utility Scripts Reference
2
+
3
+ This guide covers the various utility scripts available for preprocessing, conversion, and debugging tasks.
4
+
5
+ ## 🎬 Dataset Processing Scripts
6
+
7
+ ### Video Scene Splitting
8
+
9
+ The `scripts/split_scenes.py` script automatically splits long videos into shorter, coherent scenes.
10
+
11
+ ```bash
12
+ # Basic scene splitting
13
+ uv run python scripts/split_scenes.py input.mp4 output_dir/ --filter-shorter-than 5s
14
+ ```
15
+
16
+ **Key features:**
17
+
18
+ - **Automatic scene detection**: Uses PySceneDetect for intelligent splitting
19
+ - **Multiple algorithms**: Content-based, adaptive, threshold, and histogram detection
20
+ - **Filtering options**: Remove scenes shorter than specified duration
21
+ - **Customizable parameters**: Thresholds, window sizes, and detection modes
22
+
23
+ **Common options:**
24
+
25
+ ```bash
26
+ # See all available options
27
+ uv run python scripts/split_scenes.py --help
28
+
29
+ # Use adaptive detection with custom threshold
30
+ uv run python scripts/split_scenes.py video.mp4 scenes/ --detector adaptive --threshold 30.0
31
+
32
+ # Limit to maximum number of scenes
33
+ uv run python scripts/split_scenes.py video.mp4 scenes/ --max-scenes 50
34
+ ```
35
+
36
+ ### Automatic Video Captioning
37
+
38
+ The `scripts/caption_videos.py` script generates a single, detailed combined audio-visual
39
+ caption per video as a continuous paragraph of prose. Two backends are available:
40
+
41
+ - **`qwen_omni` (default)** — Qwen3-Omni-30B-A3B-Thinking served via a local
42
+ [vLLM](https://docs.vllm.ai/) HTTP server (~1-3 s/video on H100). Highest quality, runs
43
+ fully offline once the model is downloaded.
44
+ - **`gemini_flash`** — Google Gemini (cloud, `gemini-3.5-flash`). No GPU required. Auth is
45
+ automatic: set `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) for the Developer API, or just have
46
+ Google Cloud credentials available (`gcloud auth` / an attached service account) and it
47
+ uses Vertex AI with no extra setup.
48
+
49
+ **Step 1 — launch the captioner server** (`qwen_omni` only, one-time).
50
+
51
+ `scripts/serve_captioner.py` runs vLLM in an isolated environment via `uvx`, so vLLM's heavy
52
+ CUDA dependencies never touch the trainer's venv. It defaults to dynamic FP8 quantization
53
+ (~31 GiB weights, fits on 40 GB GPUs, same speed as BF16 on H100):
54
+
55
+ ```bash
56
+ # Terminal 1 - stays running
57
+ uv run python packages/ltx-trainer/scripts/serve_captioner.py
58
+
59
+ # Useful variants:
60
+ # --print-cmd show the vLLM command without running it
61
+ # --quantization bf16 use BF16 instead (needs ~66 GiB free VRAM)
62
+ # --hf-home /mnt/disk override where the ~65 GB model is downloaded
63
+ ```
64
+
65
+ **Step 2 — caption your videos.**
66
+
67
+ ```bash
68
+ # Terminal 2 - default backend talks to the server above
69
+ uv run python packages/ltx-trainer/scripts/caption_videos.py videos_dir/ --output dataset.json
70
+
71
+ # Remote server: --vllm-url http://other-host:8001/v1
72
+ # Gemini (gemini-3.5-flash): --captioner-type gemini_flash (uses GEMINI_API_KEY, else gcloud/Vertex)
73
+ # Gemini, parallel calls: --captioner-type gemini_flash --num-workers 5
74
+ # Re-caption everything: --override
75
+ ```
76
+
77
+ Captioning is incremental (already-captioned files are skipped, progress saves every 5 videos)
78
+ and writes JSON, JSONL, CSV, or TXT based on the output extension.
79
+
80
+ Qwen3-Omni-Thinking can optionally emit a `<think>...</think>` chain-of-thought before the
81
+ caption (`--enable-thinking`). It is off by default, which is recommended for bulk captioning
82
+ (thinking is slower as it generates the reasoning trace first).
83
+
84
+ For Gemini, keep `--num-workers` at 3-5 (higher values may hit API rate limits).
85
+
86
+ ### Dataset Preprocessing
87
+
88
+ The `scripts/process_dataset.py` script processes videos and caches latents for training.
89
+
90
+ ```bash
91
+ # Basic preprocessing
92
+ uv run python scripts/process_dataset.py dataset.json \
93
+ --resolution-buckets "960x544x49" \
94
+ --model-path /path/to/ltx-2-model.safetensors \
95
+ --text-encoder-path /path/to/gemma-model
96
+
97
+ # With video decoding for verification
98
+ uv run python scripts/process_dataset.py dataset.json \
99
+ --resolution-buckets "960x544x49" \
100
+ --model-path /path/to/ltx-2-model.safetensors \
101
+ --text-encoder-path /path/to/gemma-model \
102
+ --decode
103
+ ```
104
+
105
+ Multiple resolution buckets can be specified, separated by `;`:
106
+
107
+ ```bash
108
+ uv run python scripts/process_dataset.py dataset.json \
109
+ --resolution-buckets "960x544x49;512x512x81" \
110
+ --model-path /path/to/ltx-2-model.safetensors \
111
+ --text-encoder-path /path/to/gemma-model
112
+ ```
113
+
114
+ > [!NOTE]
115
+ > When training with multiple resolution buckets, set `optimization.batch_size: 1`.
116
+
117
+ **Multi-GPU preprocessing.** Launch with `accelerate launch` to shard the dataset across processes. Reruns resume
118
+ by default (existing `.pt` outputs are skipped); writes are atomic so interrupted runs are safe. Pass `--overwrite`
119
+ when rerunning with changed parameters (different model, resolution buckets, text encoder, `--lora-trigger`, etc.)
120
+ so stale outputs are replaced. Use the same `accelerate launch` pattern (and `--overwrite` when needed) with
121
+ `process_videos.py` or `process_captions.py` when you run those scripts standalone.
122
+
123
+ ```bash
124
+ # Multi-GPU preprocessing
125
+ uv run accelerate launch --num_processes 4 scripts/process_dataset.py dataset.json \
126
+ --resolution-buckets "960x544x49" \
127
+ --model-path /path/to/ltx-2-model.safetensors \
128
+ --text-encoder-path /path/to/gemma-model
129
+
130
+ # Force re-encoding of all items (e.g. after switching model or resolution)
131
+ uv run accelerate launch --num_processes 4 scripts/process_dataset.py dataset.json \
132
+ --resolution-buckets "960x544x49" \
133
+ --model-path /path/to/ltx-2.3-model.safetensors \
134
+ --text-encoder-path /path/to/gemma-model \
135
+ --overwrite
136
+ ```
137
+
138
+ For detailed usage, see the [Dataset Preparation Guide](dataset-preparation.md).
139
+
140
+ ### Reference Video Generation
141
+
142
+ The `scripts/compute_reference.py` script provides a template for creating reference videos needed for IC-LoRA training.
143
+ The default implementation generates Canny edge reference videos.
144
+
145
+ ```bash
146
+ # Generate Canny edge reference videos
147
+ uv run python scripts/compute_reference.py videos_dir/ --output dataset.json
148
+ ```
149
+
150
+ **Key features:**
151
+
152
+ - **Canny edge detection**: Creates edge-based reference videos
153
+ - **In-place editing**: Updates existing dataset JSON files
154
+ - **Customizable**: Modify the `compute_reference()` function for different conditions (depth, pose, etc.)
155
+
156
+ > [!TIP]
157
+ > You can edit this script to generate other types of reference videos for IC-LoRA training,
158
+ > such as depth maps, segmentation masks, or any custom video transformation.
159
+
160
+ > [!NOTE]
161
+ > `compute_reference.py` writes generated references to the `reference_video` column, which
162
+ > `process_dataset.py` detects automatically.
163
+
164
+ ## 🔍 Debugging and Verification Scripts
165
+
166
+ ### Latents Decoding
167
+
168
+ The `scripts/decode_latents.py` script decodes precomputed video latents back into video files for visual inspection.
169
+
170
+ ```bash
171
+ # Basic usage
172
+ uv run python scripts/decode_latents.py /path/to/latents/dir \
173
+ --output-dir /path/to/output \
174
+ --model-path /path/to/ltx-2-model.safetensors
175
+
176
+ # With VAE tiling for large videos
177
+ uv run python scripts/decode_latents.py /path/to/latents/dir \
178
+ --output-dir /path/to/output \
179
+ --model-path /path/to/ltx-2-model.safetensors \
180
+ --vae-tiling
181
+
182
+ # Decode both video and audio latents
183
+ uv run python scripts/decode_latents.py /path/to/latents/dir \
184
+ --output-dir /path/to/output \
185
+ --model-path /path/to/ltx-2-model.safetensors \
186
+ --with-audio
187
+ ```
188
+
189
+ **The script will:**
190
+
191
+ 1. **Load the VAE model** from the specified path
192
+ 2. **Process all `.pt` latent files** in the input directory
193
+ 3. **Decode each latent** back into a video using the VAE
194
+ 4. **Save resulting videos** as MP4 files in the output directory
195
+
196
+ **When to use:**
197
+
198
+ - **Verify preprocessing quality**: Check that your videos were encoded correctly
199
+ - **Debug training data**: Visualize what the model actually sees during training
200
+ - **Quality assessment**: Ensure latent encoding preserves important visual details
201
+
202
+ ### Inference with Trained Models
203
+
204
+ For inference with trained LoRAs, use the [`ltx-pipelines`](../../ltx-pipelines/) package which provides
205
+ production-ready pipelines:
206
+
207
+ - **Text/Image-to-Video**: `TI2VidOneStagePipeline`, `TI2VidTwoStagesPipeline`
208
+ - **Distilled (fast) inference**: `DistilledPipeline`
209
+ - **IC-LoRA video-to-video**: `ICLoraPipeline`
210
+ - **Keyframe interpolation**: `KeyframeInterpolationPipeline`
211
+
212
+ All pipelines support loading custom LoRAs trained with this trainer.
213
+
214
+ ## 🚀 Training Scripts
215
+
216
+ ### Basic and Distributed Training
217
+
218
+ Use `scripts/train.py` for both single GPU and multi-GPU runs:
219
+
220
+ ```bash
221
+ # Single-GPU training
222
+ uv run python scripts/train.py configs/t2v_lora.yaml
223
+
224
+ # Multi-GPU (uses your accelerate config)
225
+ uv run accelerate launch scripts/train.py configs/t2v_lora.yaml
226
+
227
+ # Override number of processes
228
+ uv run accelerate launch --num_processes 4 scripts/train.py configs/t2v_lora.yaml
229
+ ```
230
+
231
+ For detailed usage, see the [Training Guide](training-guide.md).
232
+
233
+ ## 💡 Tips for Using Utility Scripts
234
+
235
+ - **Start with `--help`**: Always check available options for each script
236
+ - **Test on small datasets**: Verify workflows with a few files before processing large datasets
237
+ - **Use decode verification**: Always decode a few samples to verify preprocessing quality
238
+ - **Monitor VRAM usage**: Reach for quantization or lower-memory settings (e.g. FP8 for the captioner server) when running into memory issues
239
+ - **Keep backups**: Make copies of important dataset files before running conversion scripts
packages/ltx-trainer/pyproject.toml ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ltx-trainer"
3
+ version = "1.1.7"
4
+ description = "LTX-2 training, democratized."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Matan Ben-Yosef", email = "mbyosef@lightricks.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "ltx-core",
12
+ "accelerate>=1.2.1",
13
+ "av>=14.2.1",
14
+ "bitsandbytes >=0.45.2; sys_platform == 'linux'",
15
+ "google-genai>=2.0",
16
+ "huggingface-hub[hf-xet]>=0.31.4",
17
+ "imageio>=2.37.0",
18
+ "imageio-ffmpeg>=0.6.0",
19
+ "openai>=2.0",
20
+ "opencv-python>=4.11.0.86",
21
+ "optimum-quanto>=0.2.6",
22
+ "pandas>=2.2.3",
23
+ "peft>=0.14.0",
24
+ "pillow-heif>=0.21.0",
25
+ "pydantic>=2.10.4",
26
+ "rich>=13.9.4",
27
+ "safetensors>=0.5.0",
28
+ "scenedetect>=0.6.5.2",
29
+ "sentencepiece>=0.2.0",
30
+ "soundfile>=0.12.1",
31
+ "torch>=2.6.0",
32
+ "torchaudio>=2.7.0",
33
+ # torchcodec must match the torch version (it ships a torch-ABI C++ extension and declares
34
+ # no torch pin of its own); the 0.9 line matches torch 2.9. torchaudio>=2.9 routes
35
+ # torchaudio.load() through torchcodec, so audio preprocessing needs it installed.
36
+ "torchcodec>=0.8.1,<0.10",
37
+ "torchvision>=0.21.0",
38
+ "typer>=0.15.1",
39
+ "wandb>=0.27.0",
40
+ "setuptools>=79.0.0",
41
+ ]
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pre-commit>=4.0.1",
46
+ "ruff>=0.8.6",
47
+ ]
48
+
49
+
50
+ [build-system]
51
+ requires = ["hatchling"]
52
+ build-backend = "hatchling.build"
53
+
54
+
55
+
56
+ [tool.ruff]
57
+ target-version = "1.1.7"
58
+ line-length = 120
59
+ # Restrict isort first-party detection to src/ so stray dirs (e.g. wandb/ run output)
60
+ # next to pyproject.toml don't get classified as first-party packages. See ruff#10519.
61
+ src = ["src"]
62
+
63
+ [tool.ruff.lint]
64
+ select = [
65
+ "E", # pycodestyle
66
+ "F", # pyflakes
67
+ "W", # pycodestyle (warnings)
68
+ "I", # isort
69
+ "N", # pep8-naming
70
+ "ANN", # flake8-annotations
71
+ "B", # flake8-bugbear
72
+ "A", # flake8-builtins
73
+ "COM", # flake8-commas
74
+ "C4", # flake8-comprehensions
75
+ "DTZ", # flake8-datetimez
76
+ "EXE", # flake8-executable
77
+ "PIE", # flake8-pie
78
+ "T20", # flake8-print
79
+ "PT", # flake8-pytest
80
+ "SIM", # flake8-simplify
81
+ "ARG", # flake8-unused-arguments
82
+ "PTH", # flake8--use-pathlib
83
+ "ERA", # flake8-eradicate
84
+ "RUF", # ruff specific rules
85
+ "PL", # pylint
86
+ ]
87
+ ignore = [
88
+ "ANN002", # Missing type annotation for *args
89
+ "ANN003", # Missing type annotation for **kwargs
90
+ "ANN204", # Missing type annotation for special method
91
+ "COM812", # Missing trailing comma
92
+ "PTH123", # `open()` should be replaced by `Path.open()`
93
+ "PLR2004", # Magic value used in comparison, consider replacing with a constant variable
94
+ ]
95
+ [tool.ruff.lint.pylint]
96
+ max-args = 10
97
+ [tool.ruff.lint.isort]
98
+ known-first-party = ["ltx_trainer", "ltx_core", "ltx_pipelines"]
packages/ltx-trainer/scripts/caption_videos.py ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Auto-caption videos with audio using multimodal models.
5
+ Backends:
6
+ - Qwen3-Omni-30B-A3B-Thinking via a local vLLM HTTP server (default,
7
+ ``qwen_omni``). Launch the server once with ``scripts/serve_captioner.py``.
8
+ - Gemini Flash 3.5 via Google's API (``gemini_flash``).
9
+ The paths in the output file are RELATIVE to the output file's directory,
10
+ making the dataset portable.
11
+ Basic usage:
12
+ # Launch the captioner server once (separate terminal)
13
+ uv run python scripts/serve_captioner.py
14
+ # Caption a directory
15
+ caption_videos.py videos_dir/ --output captions.json
16
+ # Caption a single video with a custom prompt
17
+ caption_videos.py video.mp4 --output cap.json --instruction "Describe in detail."
18
+ Advanced usage:
19
+ # Use Gemini Flash 3.5 (cloud, requires GEMINI_API_KEY)
20
+ caption_videos.py videos_dir/ --captioner-type gemini_flash
21
+ # Gemini with parallel workers
22
+ caption_videos.py videos_dir/ --captioner-type gemini_flash --num-workers 5
23
+ # Talk to a remote vLLM server
24
+ caption_videos.py videos_dir/ --vllm-url http://192.168.1.10:8001/v1
25
+ # Enable Qwen3 chain-of-thought (slower, more detail)
26
+ caption_videos.py videos_dir/ --enable-thinking
27
+ """
28
+
29
+ import csv
30
+ import json
31
+ from concurrent.futures import ThreadPoolExecutor, as_completed
32
+ from enum import Enum
33
+ from pathlib import Path
34
+
35
+ import typer
36
+ from rich.console import Console
37
+ from rich.progress import (
38
+ BarColumn,
39
+ MofNCompleteColumn,
40
+ Progress,
41
+ SpinnerColumn,
42
+ TextColumn,
43
+ TimeElapsedColumn,
44
+ TimeRemainingColumn,
45
+ )
46
+
47
+ from ltx_trainer.captioning import (
48
+ DEFAULT_QWEN_MODEL,
49
+ DEFAULT_VLLM_BASE_URL,
50
+ CaptionerType,
51
+ MediaCaptioningModel,
52
+ create_captioner,
53
+ )
54
+
55
+ VIDEO_EXTENSIONS = ["mp4", "avi", "mov", "mkv", "webm"]
56
+ IMAGE_EXTENSIONS = ["jpg", "jpeg", "png"]
57
+ MEDIA_EXTENSIONS = VIDEO_EXTENSIONS + IMAGE_EXTENSIONS
58
+ SAVE_INTERVAL = 5
59
+
60
+ console = Console()
61
+ app = typer.Typer(
62
+ pretty_exceptions_enable=False,
63
+ no_args_is_help=True,
64
+ help="Auto-caption videos with audio using multimodal models.",
65
+ )
66
+
67
+
68
+ class OutputFormat(str, Enum):
69
+ """Available output formats for captions."""
70
+
71
+ TXT = "txt" # Separate files for captions and video paths, one caption / video path per line
72
+ CSV = "csv" # CSV file with video path and caption columns
73
+ JSON = "json" # JSON file with video paths as keys and captions as values
74
+ JSONL = "jsonl" # JSON Lines file with one JSON object per line
75
+
76
+
77
+ def caption_media(
78
+ input_path: Path,
79
+ output_path: Path,
80
+ captioner: MediaCaptioningModel,
81
+ extensions: list[str],
82
+ recursive: bool,
83
+ fps: int,
84
+ output_format: OutputFormat,
85
+ override: bool,
86
+ num_workers: int = 1,
87
+ ) -> None:
88
+ """Caption videos and images using the provided captioning model.
89
+ Args:
90
+ input_path: Path to input video file or directory
91
+ output_path: Path to output caption file
92
+ captioner: Media captioning model
93
+ extensions: List of media file extensions to include
94
+ recursive: Whether to search subdirectories recursively
95
+ fps: Frames per second to sample from videos (ignored for images)
96
+ output_format: Format to save the captions in
97
+ override: Whether to override existing captions
98
+ num_workers: Number of parallel workers (only for cloud-based captioners like Gemini)
99
+ """
100
+
101
+ # Get list of media files to process
102
+ media_files = _get_media_files(input_path, extensions, recursive)
103
+
104
+ if not media_files:
105
+ console.print("[bold yellow]No media files found to process.[/]")
106
+ return
107
+
108
+ console.print(f"Found [bold]{len(media_files)}[/] media files to process.")
109
+
110
+ # Load existing captions and determine which files need processing
111
+ base_dir = output_path.parent.resolve()
112
+ existing_captions = _load_existing_captions(output_path, output_format)
113
+ existing_abs_paths = {str((base_dir / p).resolve()) for p in existing_captions}
114
+
115
+ if override:
116
+ media_to_process = media_files
117
+ else:
118
+ media_to_process = [f for f in media_files if str(f.resolve()) not in existing_abs_paths]
119
+ if skipped := len(media_files) - len(media_to_process):
120
+ console.print(f"[bold yellow]Skipping {skipped} media that already have captions.[/]")
121
+
122
+ if not media_to_process:
123
+ console.print("[bold yellow]All media already have captions. Use --override to recaption.[/]")
124
+ return
125
+
126
+ if num_workers > 1:
127
+ console.print(f"Running with [bold cyan]{num_workers}[/] parallel workers.")
128
+
129
+ captions = existing_captions.copy()
130
+ successfully_captioned = 0
131
+ completed_since_save = 0
132
+
133
+ progress = Progress(
134
+ SpinnerColumn(),
135
+ TextColumn("{task.description}"),
136
+ BarColumn(bar_width=40),
137
+ MofNCompleteColumn(),
138
+ TimeElapsedColumn(),
139
+ TextColumn("•"),
140
+ TimeRemainingColumn(),
141
+ console=console,
142
+ )
143
+
144
+ def process_one(media_file: Path) -> tuple[str, str]:
145
+ """Caption a single media file and return (relative_path, caption)."""
146
+ caption = captioner.caption(
147
+ path=media_file,
148
+ fps=fps,
149
+ )
150
+ # Don't resolve the file itself, so a symlinked clip keeps its logical path under the
151
+ # dataset dir instead of jumping to its (possibly external) link target.
152
+ rel_path = str((media_file.parent.resolve() / media_file.name).relative_to(base_dir))
153
+ return rel_path, caption
154
+
155
+ with progress:
156
+ task = progress.add_task(
157
+ f"Captioning (workers: {num_workers})" if num_workers > 1 else "Captioning",
158
+ total=len(media_to_process),
159
+ )
160
+
161
+ with ThreadPoolExecutor(max_workers=num_workers) as executor:
162
+ futures = {executor.submit(process_one, f): f for f in media_to_process}
163
+
164
+ for future in as_completed(futures):
165
+ media_file = futures[future]
166
+ progress.update(task, description=f"Captioning [bold blue]{media_file.name}[/]")
167
+
168
+ try:
169
+ rel_path, caption = future.result()
170
+
171
+ captions[rel_path] = caption
172
+ successfully_captioned += 1
173
+ completed_since_save += 1
174
+
175
+ if completed_since_save >= SAVE_INTERVAL:
176
+ _save_captions(captions, output_path, output_format)
177
+ completed_since_save = 0
178
+
179
+ except Exception as e:
180
+ console.print(f"[bold red]Error captioning {media_file.name}: {e}[/]")
181
+
182
+ progress.advance(task)
183
+
184
+ # Final save with everything accumulated
185
+ _save_captions(captions, output_path, output_format)
186
+
187
+ # Print summary
188
+ console.print(
189
+ f"[bold green]✓[/] Captioned [bold]{successfully_captioned}/{len(media_to_process)}[/] media successfully.",
190
+ )
191
+
192
+
193
+ def _get_media_files(
194
+ input_path: Path,
195
+ extensions: list[str] = MEDIA_EXTENSIONS,
196
+ recursive: bool = False,
197
+ ) -> list[Path]:
198
+ """Get all media files from the input path."""
199
+ input_path = Path(input_path)
200
+ # Normalize extensions to lowercase without dots
201
+ extensions_set = {ext.lower().lstrip(".") for ext in extensions}
202
+
203
+ if input_path.is_file():
204
+ # If input is a file, check if it has a valid extension
205
+ if input_path.suffix.lstrip(".").lower() in extensions_set:
206
+ return [input_path]
207
+ else:
208
+ typer.echo(f"Warning: {input_path} is not a recognized media file. Skipping.")
209
+ return []
210
+ elif input_path.is_dir():
211
+ # Find all files and filter by extension case-insensitively
212
+ glob_pattern = "**/*" if recursive else "*"
213
+ media_files = [
214
+ f for f in input_path.glob(glob_pattern) if f.is_file() and f.suffix.lstrip(".").lower() in extensions_set
215
+ ]
216
+ return sorted(media_files)
217
+ else:
218
+ typer.echo(f"Error: {input_path} does not exist.")
219
+ raise typer.Exit(code=1)
220
+
221
+
222
+ def _save_captions(
223
+ captions: dict[str, str],
224
+ output_path: Path,
225
+ format_type: OutputFormat,
226
+ ) -> None:
227
+ """Save captions to a file in the specified format.
228
+ Args:
229
+ captions: Dictionary mapping media paths to captions
230
+ output_path: Path to save the output file
231
+ format_type: Format to save the captions in
232
+ """
233
+ # Create parent directories if they don't exist
234
+ output_path.parent.mkdir(parents=True, exist_ok=True)
235
+
236
+ console.print("[bold blue]Saving captions...[/]")
237
+
238
+ match format_type:
239
+ case OutputFormat.TXT:
240
+ # Create two separate files for captions and media paths
241
+ captions_file = output_path.with_stem(f"{output_path.stem}_captions")
242
+ paths_file = output_path.with_stem(f"{output_path.stem}_paths")
243
+
244
+ with captions_file.open("w", encoding="utf-8") as f:
245
+ for caption in captions.values():
246
+ f.write(f"{caption}\n")
247
+
248
+ with paths_file.open("w", encoding="utf-8") as f:
249
+ for media_path in captions:
250
+ f.write(f"{media_path}\n")
251
+
252
+ console.print(f"[bold green]✓[/] Captions saved to [cyan]{captions_file}[/]")
253
+ console.print(f"[bold green]✓[/] Media paths saved to [cyan]{paths_file}[/]")
254
+
255
+ case OutputFormat.CSV:
256
+ with output_path.open("w", encoding="utf-8", newline="") as f:
257
+ writer = csv.writer(f)
258
+ writer.writerow(["caption", "media_path"])
259
+ for media_path, caption in captions.items():
260
+ writer.writerow([caption, media_path])
261
+
262
+ console.print(f"[bold green]✓[/] Captions saved to [cyan]{output_path}[/]")
263
+
264
+ case OutputFormat.JSON:
265
+ # Format as list of dictionaries with caption and media_path keys
266
+ json_data = [{"caption": caption, "media_path": media_path} for media_path, caption in captions.items()]
267
+
268
+ with output_path.open("w", encoding="utf-8") as f:
269
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
270
+
271
+ console.print(f"[bold green]✓[/] Captions saved to [cyan]{output_path}[/]")
272
+
273
+ case OutputFormat.JSONL:
274
+ with output_path.open("w", encoding="utf-8") as f:
275
+ for media_path, caption in captions.items():
276
+ f.write(json.dumps({"caption": caption, "media_path": media_path}, ensure_ascii=False) + "\n")
277
+
278
+ console.print(f"[bold green]✓[/] Captions saved to [cyan]{output_path}[/]")
279
+
280
+ case _:
281
+ raise ValueError(f"Unsupported output format: {format_type}")
282
+
283
+
284
+ def _load_existing_captions( # noqa: PLR0912
285
+ output_path: Path,
286
+ format_type: OutputFormat,
287
+ ) -> dict[str, str]:
288
+ """Load existing captions from a file.
289
+ Args:
290
+ output_path: Path to the captions file
291
+ format_type: Format of the captions file
292
+ Returns:
293
+ Dictionary mapping media paths to captions, or empty dict if file doesn't exist
294
+ """
295
+ if not output_path.exists():
296
+ return {}
297
+
298
+ console.print(f"[bold blue]Loading existing captions from [cyan]{output_path}[/]...[/]")
299
+
300
+ existing_captions = {}
301
+
302
+ try:
303
+ match format_type:
304
+ case OutputFormat.TXT:
305
+ # For TXT format, we have two separate files
306
+ captions_file = output_path.with_stem(f"{output_path.stem}_captions")
307
+ paths_file = output_path.with_stem(f"{output_path.stem}_paths")
308
+
309
+ if captions_file.exists() and paths_file.exists():
310
+ captions = captions_file.read_text(encoding="utf-8").splitlines()
311
+ paths = paths_file.read_text(encoding="utf-8").splitlines()
312
+
313
+ if len(captions) == len(paths):
314
+ existing_captions = dict(zip(paths, captions, strict=False))
315
+
316
+ case OutputFormat.CSV:
317
+ with output_path.open("r", encoding="utf-8", newline="") as f:
318
+ reader = csv.reader(f)
319
+ # Skip header
320
+ next(reader, None)
321
+ for row in reader:
322
+ if len(row) >= 2:
323
+ caption, media_path = row[0], row[1]
324
+ existing_captions[media_path] = caption
325
+
326
+ case OutputFormat.JSON:
327
+ with output_path.open("r", encoding="utf-8") as f:
328
+ json_data = json.load(f)
329
+ for item in json_data:
330
+ if "caption" in item and "media_path" in item:
331
+ existing_captions[item["media_path"]] = item["caption"]
332
+
333
+ case OutputFormat.JSONL:
334
+ with output_path.open("r", encoding="utf-8") as f:
335
+ for line in f:
336
+ item = json.loads(line)
337
+ if "caption" in item and "media_path" in item:
338
+ existing_captions[item["media_path"]] = item["caption"]
339
+
340
+ case _:
341
+ raise ValueError(f"Unsupported output format: {format_type}")
342
+
343
+ console.print(f"[bold green]✓[/] Loaded [bold]{len(existing_captions)}[/] existing captions")
344
+ return existing_captions
345
+
346
+ except Exception as e:
347
+ console.print(f"[bold yellow]Warning: Could not load existing captions: {e}[/]")
348
+ return {}
349
+
350
+
351
+ @app.command()
352
+ def main( # noqa: PLR0913
353
+ input_path: Path = typer.Argument( # noqa: B008
354
+ ...,
355
+ help="Path to input video/image file or directory containing media files",
356
+ exists=True,
357
+ ),
358
+ output: Path | None = typer.Option( # noqa: B008
359
+ None,
360
+ "--output",
361
+ "-o",
362
+ help="Path to output file for captions. Format determined by file extension.",
363
+ ),
364
+ captioner_type: CaptionerType = typer.Option( # noqa: B008
365
+ CaptionerType.QWEN_OMNI,
366
+ "--captioner-type",
367
+ "-c",
368
+ help="Type of captioner to use. Valid values: 'qwen_omni' (local), 'gemini_flash' (API)",
369
+ case_sensitive=False,
370
+ ),
371
+ vllm_url: str = typer.Option(
372
+ DEFAULT_VLLM_BASE_URL,
373
+ "--vllm-url",
374
+ help=(
375
+ "Base URL of the vLLM OpenAI-compatible server (qwen_omni only). "
376
+ "Launch the server with `uv run python scripts/serve_captioner.py`."
377
+ ),
378
+ ),
379
+ vllm_model: str = typer.Option(
380
+ DEFAULT_QWEN_MODEL,
381
+ "--vllm-model",
382
+ help="Served model identifier on the vLLM server (qwen_omni only).",
383
+ ),
384
+ enable_thinking: bool = typer.Option(
385
+ False,
386
+ "--enable-thinking/--no-thinking",
387
+ help=(
388
+ "Let Qwen3-Omni produce a <think>...</think> chain-of-thought before the caption. "
389
+ "Off by default: ~5x slower with marginal quality benefit and occasional hallucinations."
390
+ ),
391
+ ),
392
+ max_tokens: int = typer.Option(
393
+ 4096,
394
+ "--max-tokens",
395
+ help="Maximum new tokens to generate per caption (qwen_omni only).",
396
+ ),
397
+ instruction: str | None = typer.Option(
398
+ None,
399
+ "--instruction",
400
+ "-i",
401
+ help="Custom instruction for the captioning model. If not provided, uses an appropriate default.",
402
+ ),
403
+ extensions: str = typer.Option(
404
+ ",".join(MEDIA_EXTENSIONS),
405
+ "--extensions",
406
+ "-e",
407
+ help="Comma-separated list of media file extensions to process",
408
+ ),
409
+ recursive: bool = typer.Option(
410
+ False,
411
+ "--recursive",
412
+ "-r",
413
+ help="Search for media files in subdirectories recursively",
414
+ ),
415
+ fps: int = typer.Option(
416
+ 2,
417
+ "--fps",
418
+ "-f",
419
+ help=(
420
+ "Frames per second to sample from videos. 2 is a typical default; "
421
+ "lower values use less compute per video. Ignored for images and for the "
422
+ "Gemini backend (which decides its own sampling rate)."
423
+ ),
424
+ ),
425
+ override: bool = typer.Option(
426
+ False,
427
+ "--override",
428
+ help="Whether to override existing captions for media",
429
+ ),
430
+ api_key: str | None = typer.Option(
431
+ None,
432
+ "--api-key",
433
+ envvar=["GOOGLE_API_KEY", "GEMINI_API_KEY"],
434
+ help="API key for Gemini Flash (can also use GOOGLE_API_KEY or GEMINI_API_KEY env var)",
435
+ ),
436
+ num_workers: int = typer.Option(
437
+ 1,
438
+ "--num-workers",
439
+ "-w",
440
+ min=1,
441
+ max=10,
442
+ help=(
443
+ "Number of parallel workers for captioning (1-10). "
444
+ "Values above 1 are only supported for cloud-based captioners (gemini_flash). "
445
+ "Using multiple workers with a local model will raise an error."
446
+ ),
447
+ ),
448
+ ) -> None:
449
+ """Auto-caption videos with audio using multimodal models.
450
+ Backends:
451
+ - ``qwen_omni`` (default): Qwen3-Omni-30B-A3B-Thinking via a local vLLM
452
+ HTTP server. Launch the server once in a separate terminal with
453
+ ``uv run python scripts/serve_captioner.py``. The server stays loaded
454
+ across script invocations.
455
+ - ``gemini_flash``: Google Gemini (``gemini-3.5-flash``) via the google-genai SDK.
456
+ Auth is automatic -- ``GEMINI_API_KEY``/``GOOGLE_API_KEY`` for the Developer API,
457
+ or Google Cloud credentials (gcloud / service account) for Vertex AI with no env vars.
458
+ The paths in the output file will be relative to the output file's directory.
459
+ Examples:
460
+ # Caption videos using the local vLLM server (default)
461
+ caption_videos.py videos_dir/ -o captions.json
462
+ # Point at a remote vLLM server
463
+ caption_videos.py videos_dir/ -o captions.json --vllm-url http://other-host:8001/v1
464
+ # Caption using Gemini Flash 3.5
465
+ caption_videos.py videos_dir/ -o captions.json -c gemini_flash
466
+ # Caption with custom instruction
467
+ caption_videos.py video.mp4 -o captions.json -i "Describe this video in detail"
468
+ """
469
+
470
+ # Parallel workers are only supported for the cloud Gemini backend; qwen_omni
471
+ # drives a single shared vLLM server and is captioned serially from here.
472
+ if num_workers > 1 and captioner_type != CaptionerType.GEMINI_FLASH:
473
+ console.print(
474
+ "[bold red]Error:[/] --num-workers > 1 is only supported with "
475
+ "[bold]--captioner-type gemini_flash[/]. Use --num-workers 1 (default) "
476
+ "for the qwen_omni backend."
477
+ )
478
+ raise typer.Exit(code=1)
479
+
480
+ # Parse extensions
481
+ ext_list = [ext.strip() for ext in extensions.split(",")]
482
+
483
+ # Determine output path and format
484
+ if output is None:
485
+ output_format = OutputFormat.JSON
486
+ if input_path.is_file(): # noqa: SIM108
487
+ # Default to a JSON file with the same name as the input media
488
+ output = input_path.with_suffix(".dataset.json")
489
+ else:
490
+ # Default to a JSON file in the input directory
491
+ output = input_path / "dataset.json"
492
+ else:
493
+ # Determine format from file extension
494
+ output_format = OutputFormat(Path(output).suffix.lstrip(".").lower())
495
+
496
+ # Ensure output path is absolute
497
+ output = Path(output).resolve()
498
+ console.print(f"Output will be saved to [bold blue]{output}[/]")
499
+
500
+ with console.status("Initializing captioner...", spinner="dots"):
501
+ if captioner_type == CaptionerType.QWEN_OMNI:
502
+ captioner = create_captioner(
503
+ captioner_type=captioner_type,
504
+ base_url=vllm_url,
505
+ model=vllm_model,
506
+ instruction=instruction,
507
+ max_tokens=max_tokens,
508
+ enable_thinking=enable_thinking,
509
+ )
510
+ elif captioner_type == CaptionerType.GEMINI_FLASH:
511
+ captioner = create_captioner(
512
+ captioner_type=captioner_type,
513
+ api_key=api_key,
514
+ instruction=instruction,
515
+ )
516
+ else:
517
+ raise ValueError(f"Unsupported captioner type: {captioner_type}")
518
+
519
+ console.print(f"[bold green]✓[/] {captioner_type.value} captioner ready")
520
+
521
+ # Caption media files
522
+ caption_media(
523
+ input_path=input_path,
524
+ output_path=output,
525
+ captioner=captioner,
526
+ extensions=ext_list,
527
+ recursive=recursive,
528
+ fps=fps,
529
+ output_format=output_format,
530
+ override=override,
531
+ num_workers=num_workers,
532
+ )
533
+
534
+
535
+ if __name__ == "__main__":
536
+ app()
packages/ltx-trainer/scripts/compute_reference.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Compute reference videos for IC-LoRA training.
3
+ This script provides a command-line interface for generating reference videos to be used for IC-LoRA training.
4
+ Note that it reads and writes to the same file (the output of caption_videos.py),
5
+ where it adds the "reference_video" field to the JSON.
6
+ Basic usage:
7
+ # Compute reference videos for all videos in a directory
8
+ compute_reference.py videos_dir/ --output videos_dir/captions.json
9
+ """
10
+
11
+ # Standard library imports
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ # Third-party imports
17
+ import cv2
18
+ import torch
19
+ import torchvision.transforms.functional as TF # noqa: N812
20
+ import typer
21
+ from rich.console import Console
22
+ from rich.progress import (
23
+ BarColumn,
24
+ MofNCompleteColumn,
25
+ Progress,
26
+ SpinnerColumn,
27
+ TextColumn,
28
+ TimeElapsedColumn,
29
+ TimeRemainingColumn,
30
+ )
31
+ from transformers.utils.logging import disable_progress_bar
32
+
33
+ # Local imports
34
+ from ltx_trainer.video_utils import read_video, save_video
35
+
36
+ # Initialize console and disable progress bars
37
+ console = Console()
38
+ disable_progress_bar()
39
+
40
+ VIDEO_COLUMNS = ("video", "media_path")
41
+ REFERENCE_VIDEO_COLUMN = "reference_video"
42
+ LEGACY_REFERENCE_COLUMN = "reference_path"
43
+
44
+
45
+ def compute_reference(
46
+ images: torch.Tensor,
47
+ ) -> torch.Tensor:
48
+ """Compute Canny edge detection on a batch of images.
49
+ Args:
50
+ images: Batch of images tensor of shape [B, C, H, W]
51
+ Returns:
52
+ Binary edge masks tensor of shape [B, H, W]
53
+ """
54
+ # Convert to grayscale if needed
55
+ if images.shape[1] == 3:
56
+ images = TF.rgb_to_grayscale(images)
57
+
58
+ # Ensure images are in [0, 1] range
59
+ if images.max() > 1.0:
60
+ images = images / 255.0
61
+
62
+ # Compute Canny edges
63
+ edge_masks = []
64
+ for image in images:
65
+ # Convert to numpy for OpenCV
66
+ image_np = (image.squeeze().cpu().numpy() * 255).astype("uint8")
67
+
68
+ # Apply Canny edge detection
69
+ edges = cv2.Canny(
70
+ image_np,
71
+ threshold1=100,
72
+ threshold2=200,
73
+ )
74
+
75
+ # Convert back to tensor
76
+ edge_mask = torch.from_numpy(edges).float()
77
+ edge_masks.append(edge_mask)
78
+
79
+ edges = torch.stack(edge_masks)
80
+ edges = torch.stack([edges] * 3, dim=1) # Convert to 3-channel
81
+ return edges
82
+
83
+
84
+ def _get_meta_data(
85
+ output_path: Path,
86
+ ) -> list[dict[str, Any]]:
87
+ """Get set of existing reference video paths without loading the actual files.
88
+ Args:
89
+ output_path: Path to the reference video paths file
90
+ Returns:
91
+ Dataset rows with media paths and captions
92
+ """
93
+ if not output_path.exists():
94
+ return []
95
+
96
+ console.print(f"[bold blue]Reading meta data from [cyan]{output_path}[/]...[/]")
97
+
98
+ try:
99
+ with output_path.open("r", encoding="utf-8") as f:
100
+ json_data = json.load(f)
101
+ return json_data
102
+
103
+ except Exception as e:
104
+ console.print(f"[bold yellow]Warning: Could not check meta data: {e}[/]")
105
+ return []
106
+
107
+
108
+ def _get_media_path(item: dict[str, Any]) -> str:
109
+ for column in VIDEO_COLUMNS:
110
+ if column in item:
111
+ return item[column]
112
+ raise KeyError(f"Dataset row must contain one of {VIDEO_COLUMNS}")
113
+
114
+
115
+ def _save_dataset_json(
116
+ reference_paths: dict[str, str],
117
+ output_path: Path,
118
+ ) -> None:
119
+ """Save dataset json with reference video paths.
120
+ Args:
121
+ reference_paths: Dictionary mapping media paths to reference video paths
122
+ output_path: Path to save the output file
123
+ """
124
+
125
+ with output_path.open("r", encoding="utf-8") as f:
126
+ json_data = json.load(f)
127
+ new_json_data = json_data.copy()
128
+ for i, item in enumerate(json_data):
129
+ media_path = _get_media_path(item)
130
+ reference_path = reference_paths[media_path]
131
+ new_json_data[i].pop(LEGACY_REFERENCE_COLUMN, None)
132
+ new_json_data[i][REFERENCE_VIDEO_COLUMN] = reference_path
133
+
134
+ with output_path.open("w", encoding="utf-8") as f:
135
+ json.dump(new_json_data, f, indent=2, ensure_ascii=False)
136
+
137
+ console.print(f"[bold green]✓[/] Reference video paths saved to [cyan]{output_path}[/]")
138
+ console.print("[bold yellow]Note:[/] Reference videos were written to the '[cyan]reference_video[/]' column.")
139
+ console.print(" [cyan]process_dataset.py[/] detects this column automatically for IC-LoRA preprocessing.")
140
+
141
+
142
+ def process_media(
143
+ input_path: Path,
144
+ output_path: Path,
145
+ override: bool,
146
+ batch_size: int = 100,
147
+ ) -> None:
148
+ """Process videos and images to compute condition on videos.
149
+ Args:
150
+ input_path: Path to input video/image file or directory
151
+ output_path: Path to output reference video file
152
+ override: Whether to override existing reference video files
153
+ """
154
+ if not output_path.exists():
155
+ raise FileNotFoundError(
156
+ f"Output file does not exist: {output_path}. This is also the input file for the dataset."
157
+ )
158
+
159
+ # Check for existing reference video files
160
+ meta_data = _get_meta_data(output_path)
161
+
162
+ base_dir = input_path.resolve()
163
+ console.print(f"Using [bold blue]{base_dir}[/] as base directory for relative paths")
164
+
165
+ # Filter media files
166
+ media_to_process = []
167
+ skipped_media = []
168
+
169
+ def media_path_to_reference_path(media_file: Path) -> Path:
170
+ return media_file.parent / (media_file.stem + "_reference" + media_file.suffix)
171
+
172
+ media_files = [base_dir / Path(_get_media_path(sample)) for sample in meta_data]
173
+ for media_file in media_files:
174
+ reference_path = media_path_to_reference_path(media_file)
175
+ media_to_process.append(media_file)
176
+
177
+ console.print(f"Processing [bold]{len(media_to_process)}[/] media.")
178
+
179
+ # Initialize progress tracking
180
+ progress = Progress(
181
+ SpinnerColumn(),
182
+ TextColumn("{task.description}"),
183
+ BarColumn(bar_width=40),
184
+ MofNCompleteColumn(),
185
+ TimeElapsedColumn(),
186
+ TextColumn("•"),
187
+ TimeRemainingColumn(),
188
+ console=console,
189
+ )
190
+
191
+ # Process media files
192
+ media_paths = [_get_media_path(item) for item in meta_data]
193
+ reference_paths = {rel_path: str(media_path_to_reference_path(Path(rel_path))) for rel_path in media_paths}
194
+
195
+ with progress:
196
+ task = progress.add_task("Computing condition on videos", total=len(media_to_process))
197
+
198
+ for media_file, rel_path in zip(media_to_process, media_paths, strict=True):
199
+ progress.update(task, description=f"Processing [bold blue]{media_file.name}[/]")
200
+
201
+ # Key by the original media-path string (matches the dict seeded above). Avoid
202
+ # resolve()/relative_to here — they crash on symlinked or absolute media paths.
203
+ reference_path = media_path_to_reference_path(media_file)
204
+ try:
205
+ ref_stored = str(reference_path.relative_to(base_dir))
206
+ except ValueError:
207
+ ref_stored = str(reference_path) # absolute/out-of-tree: keep it next to the source
208
+ reference_paths[rel_path] = ref_stored
209
+
210
+ if not reference_path.resolve().exists() or override:
211
+ try:
212
+ video, fps = read_video(media_file)
213
+
214
+ # Process frames in batches
215
+ condition_frames = []
216
+
217
+ for i in range(0, len(video), batch_size):
218
+ batch = video[i : i + batch_size]
219
+ condition_batch = compute_reference(batch)
220
+ condition_frames.append(condition_batch)
221
+
222
+ # Concatenate all edge frames
223
+ all_condition = torch.cat(condition_frames, dim=0)
224
+
225
+ # Save the edge video
226
+ save_video(all_condition, reference_path.resolve(), fps=fps)
227
+
228
+ except Exception as e:
229
+ console.print(f"[bold red]Error processing [bold blue]{media_file}[/]: {e}[/]")
230
+ reference_paths.pop(rel_path)
231
+ else:
232
+ skipped_media.append(media_file)
233
+
234
+ progress.advance(task)
235
+
236
+ # Save results
237
+ _save_dataset_json(reference_paths, output_path)
238
+
239
+ # Print summary
240
+ total_to_process = len(media_files) - len(skipped_media)
241
+ console.print(
242
+ f"[bold green]✓[/] Processed [bold]{total_to_process}/{len(media_files)}[/] media successfully.",
243
+ )
244
+
245
+
246
+ app = typer.Typer(
247
+ pretty_exceptions_enable=False,
248
+ no_args_is_help=True,
249
+ help="Compute reference videos for IC-LoRA training.",
250
+ )
251
+
252
+
253
+ @app.command()
254
+ def main(
255
+ input_path: Path = typer.Argument( # noqa: B008
256
+ ...,
257
+ help="Path to input video/image file or directory containing media files",
258
+ exists=True,
259
+ ),
260
+ output: Path = typer.Option( # noqa: B008
261
+ ...,
262
+ "--output",
263
+ "-o",
264
+ help="Path to json output file for reference video paths. "
265
+ "This is also the input file for the dataset, the output of compute_captions.py.",
266
+ ),
267
+ override: bool = typer.Option(
268
+ False,
269
+ "--override",
270
+ help="Whether to override existing reference video files",
271
+ ),
272
+ batch_size: int = typer.Option(
273
+ 100,
274
+ "--batch-size",
275
+ help="Batch size for processing videos",
276
+ ),
277
+ ) -> None:
278
+ """Compute reference videos for IC-LoRA training.
279
+ This script generates reference videos (e.g., Canny edge maps) for given videos.
280
+ The paths in the output file will be relative to the output file's directory.
281
+ Examples:
282
+ # Process all videos in a directory
283
+ compute_reference.py videos_dir/ -o videos_dir/captions.json
284
+ """
285
+
286
+ # Ensure output path is absolute
287
+ output = Path(output).resolve()
288
+ console.print(f"Output will be saved to [bold blue]{output}[/]")
289
+
290
+ # Verify output path exists
291
+ if not output.exists():
292
+ raise FileNotFoundError(f"Output file does not exist: {output}. This is also the input file for the dataset.")
293
+
294
+ # Process media files
295
+ process_media(
296
+ input_path=input_path,
297
+ output_path=output,
298
+ override=override,
299
+ batch_size=batch_size,
300
+ )
301
+
302
+
303
+ if __name__ == "__main__":
304
+ app()
packages/ltx-trainer/scripts/decode_latents.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Decode precomputed video latents back into videos using the VAE.
5
+ This script loads latent files saved during preprocessing and decodes them
6
+ back into video clips using the same VAE model.
7
+ Basic usage:
8
+ python scripts/decode_latents.py /path/to/latents/dir /path/to/output \
9
+ --model-source /path/to/ltx2.safetensors
10
+ """
11
+
12
+ from pathlib import Path
13
+
14
+ import torch
15
+ import torchaudio
16
+ import torchvision.utils
17
+ import typer
18
+ from einops import rearrange
19
+ from rich.console import Console
20
+ from rich.progress import (
21
+ BarColumn,
22
+ MofNCompleteColumn,
23
+ Progress,
24
+ SpinnerColumn,
25
+ TextColumn,
26
+ TimeElapsedColumn,
27
+ TimeRemainingColumn,
28
+ )
29
+ from transformers.utils.logging import disable_progress_bar
30
+
31
+ from ltx_core.model.video_vae import SpatialTilingConfig, TemporalTilingConfig, TilingConfig
32
+ from ltx_trainer import logger
33
+ from ltx_trainer.model_loader import load_audio_vae_decoder, load_video_vae_decoder, load_vocoder
34
+ from ltx_trainer.video_utils import save_video
35
+
36
+ DEFAULT_TILE_SIZE_PIXELS = 512 # Spatial tile size in pixels (must be ≥64 and divisible by 32)
37
+ DEFAULT_TILE_OVERLAP_PIXELS = 128 # Spatial tile overlap in pixels (must be divisible by 32)
38
+ DEFAULT_TILE_SIZE_FRAMES = 128 # Temporal tile size in frames (must be ≥16 and divisible by 8)
39
+ DEFAULT_TILE_OVERLAP_FRAMES = 24 # Temporal tile overlap in frames (must be divisible by 8)
40
+
41
+ disable_progress_bar()
42
+ console = Console()
43
+ app = typer.Typer(
44
+ pretty_exceptions_enable=False,
45
+ no_args_is_help=True,
46
+ help="Decode precomputed video latents back into videos using the VAE.",
47
+ )
48
+
49
+
50
+ class LatentsDecoder:
51
+ def __init__(
52
+ self,
53
+ model_path: str,
54
+ device: str = "cuda",
55
+ vae_tiling: bool = False,
56
+ with_audio: bool = False,
57
+ ):
58
+ """Initialize the decoder with model configuration.
59
+ Args:
60
+ model_path: Path to LTX-2 checkpoint (.safetensors)
61
+ device: Device to use for computation
62
+ vae_tiling: Whether to enable VAE tiling for larger video resolutions
63
+ with_audio: Whether to load audio VAE for audio decoding
64
+ """
65
+ self.device = torch.device(device)
66
+ self.model_path = model_path
67
+ self.vae = None
68
+ self.audio_vae = None
69
+ self.vocoder = None
70
+ self.vae_tiling = vae_tiling
71
+
72
+ self._load_model(model_path, with_audio)
73
+
74
+ def _load_model(self, model_path: str, with_audio: bool = False) -> None:
75
+ """Initialize and load the VAE model(s)."""
76
+ with console.status(f"[bold]Loading video VAE decoder from {model_path}...", spinner="dots"):
77
+ self.vae = load_video_vae_decoder(model_path, device=self.device, dtype=torch.bfloat16)
78
+
79
+ if with_audio:
80
+ with console.status(f"[bold]Loading audio VAE decoder from {model_path}...", spinner="dots"):
81
+ self.audio_vae = load_audio_vae_decoder(model_path, device=self.device, dtype=torch.bfloat16)
82
+
83
+ with console.status(f"[bold]Loading vocoder from {model_path}...", spinner="dots"):
84
+ self.vocoder = load_vocoder(model_path, device=self.device)
85
+
86
+ @torch.inference_mode()
87
+ def decode(self, latents_dir: Path, output_dir: Path, seed: int | None = None) -> None:
88
+ """Decode all latent files in the directory recursively.
89
+ Args:
90
+ latents_dir: Directory containing latent files (.pt)
91
+ output_dir: Directory to save decoded videos
92
+ seed: Optional random seed for noise generation
93
+ """
94
+ # Find all .pt files recursively
95
+ latent_files = list(latents_dir.rglob("*.pt"))
96
+
97
+ if not latent_files:
98
+ logger.warning(f"No .pt files found in {latents_dir}")
99
+ return
100
+
101
+ logger.info(f"Found {len(latent_files):,} latent files to decode")
102
+
103
+ # Process files with progress bar
104
+ with Progress(
105
+ SpinnerColumn(),
106
+ TextColumn("[progress.description]{task.description}"),
107
+ BarColumn(),
108
+ MofNCompleteColumn(),
109
+ TimeElapsedColumn(),
110
+ TimeRemainingColumn(),
111
+ console=console,
112
+ ) as progress:
113
+ task = progress.add_task("Decoding latents", total=len(latent_files))
114
+
115
+ for latent_file in latent_files:
116
+ # Calculate relative path to maintain directory structure
117
+ rel_path = latent_file.relative_to(latents_dir)
118
+ output_subdir = output_dir / rel_path.parent
119
+ output_subdir.mkdir(parents=True, exist_ok=True)
120
+
121
+ try:
122
+ self._process_file(latent_file, output_subdir, seed)
123
+ except Exception as e:
124
+ logger.error(f"Error processing {latent_file}: {e}")
125
+ continue
126
+
127
+ progress.advance(task)
128
+
129
+ logger.info(f"Decoding complete! Videos saved to {output_dir}")
130
+
131
+ @torch.inference_mode()
132
+ def decode_audio(self, latents_dir: Path, output_dir: Path) -> None:
133
+ """Decode all audio latent files in the directory recursively.
134
+ Args:
135
+ latents_dir: Directory containing audio latent files (.pt)
136
+ output_dir: Directory to save decoded audio files
137
+ """
138
+ # Check if audio VAE is loaded
139
+ if self.audio_vae is None or self.vocoder is None:
140
+ logger.warning("Audio VAE or vocoder not loaded. Skipping audio decoding.")
141
+ return
142
+
143
+ # Find all .pt files recursively
144
+ latent_files = list(latents_dir.rglob("*.pt"))
145
+
146
+ if not latent_files:
147
+ logger.warning(f"No .pt files found in {latents_dir}")
148
+ return
149
+
150
+ logger.info(f"Found {len(latent_files):,} audio latent files to decode")
151
+
152
+ # Process files with progress bar
153
+ with Progress(
154
+ SpinnerColumn(),
155
+ TextColumn("[progress.description]{task.description}"),
156
+ BarColumn(),
157
+ MofNCompleteColumn(),
158
+ TimeElapsedColumn(),
159
+ TimeRemainingColumn(),
160
+ console=console,
161
+ ) as progress:
162
+ task = progress.add_task("Decoding audio latents", total=len(latent_files))
163
+
164
+ for latent_file in latent_files:
165
+ # Calculate relative path to maintain directory structure
166
+ rel_path = latent_file.relative_to(latents_dir)
167
+ output_subdir = output_dir / rel_path.parent
168
+ output_subdir.mkdir(parents=True, exist_ok=True)
169
+
170
+ try:
171
+ self._process_audio_file(latent_file, output_subdir)
172
+ except Exception as e:
173
+ logger.error(f"Error processing audio {latent_file}: {e}")
174
+ continue
175
+
176
+ progress.advance(task)
177
+
178
+ logger.info(f"Audio decoding complete! Audio files saved to {output_dir}")
179
+
180
+ def _process_file(self, latent_file: Path, output_dir: Path, seed: int | None) -> None:
181
+ """Process a single latent file."""
182
+ # Load the latent data
183
+ data = torch.load(latent_file, map_location=self.device, weights_only=False)
184
+
185
+ # Get latents - handle both old patchified [seq_len, C] and new [C, F, H, W] formats
186
+ latents = data["latents"]
187
+ num_frames = data["num_frames"]
188
+ height = data["height"]
189
+ width = data["width"]
190
+
191
+ # Check if latents need reshaping (old patchified format)
192
+ if latents.dim() == 2:
193
+ # Old format: [seq_len, C] -> reshape to [C, F, H, W]
194
+ latents = rearrange(latents, "(f h w) c -> c f h w", f=num_frames, h=height, w=width)
195
+
196
+ # Add batch dimension: [C, F, H, W] -> [1, C, F, H, W]
197
+ latents = latents.unsqueeze(0).to(device=self.device, dtype=torch.bfloat16)
198
+
199
+ # Create generator only if seed is provided
200
+ generator = None
201
+ if seed is not None:
202
+ generator = torch.Generator(device=self.device)
203
+ generator.manual_seed(seed)
204
+
205
+ # Decode the video
206
+ video = self._decode_video(latents, generator)
207
+
208
+ # Determine output format and save
209
+ is_image = video.shape[0] == 1
210
+ if is_image:
211
+ # Save as PNG for single frame
212
+ output_path = output_dir / f"{latent_file.stem}.png"
213
+ torchvision.utils.save_image(
214
+ video[0], # [C, H, W] in [0, 1]
215
+ str(output_path),
216
+ )
217
+ else:
218
+ # Save as MP4 for video using PyAV-based save_video
219
+ output_path = output_dir / f"{latent_file.stem}.mp4"
220
+ fps = data.get("fps", 24) # Use stored FPS or default to 24
221
+ save_video(
222
+ video_tensor=video, # [F, C, H, W] in [0, 1]
223
+ output_path=output_path,
224
+ fps=fps,
225
+ )
226
+
227
+ def _decode_video(self, latents: torch.Tensor, generator: torch.Generator | None = None) -> torch.Tensor:
228
+ """Decode latents to video frames."""
229
+ if self.vae_tiling:
230
+ # Use tiled decoding for reduced VRAM
231
+ tiling_config = TilingConfig(
232
+ spatial_config=SpatialTilingConfig(
233
+ tile_size_in_pixels=DEFAULT_TILE_SIZE_PIXELS,
234
+ tile_overlap_in_pixels=DEFAULT_TILE_OVERLAP_PIXELS,
235
+ ),
236
+ temporal_config=TemporalTilingConfig(
237
+ tile_size_in_frames=DEFAULT_TILE_SIZE_FRAMES,
238
+ tile_overlap_in_frames=DEFAULT_TILE_OVERLAP_FRAMES,
239
+ ),
240
+ )
241
+ chunks = list(
242
+ self.vae.tiled_decode(
243
+ latents,
244
+ tiling_config=tiling_config,
245
+ generator=generator,
246
+ )
247
+ )
248
+ # Concatenate along temporal dimension
249
+ video = torch.cat(chunks, dim=2) # [B, C, F, H, W]
250
+ else:
251
+ # Standard full decoding
252
+ video = self.vae(latents, generator=generator) # [B, C, F, H, W]
253
+
254
+ # Convert to [F, C, H, W] format and normalize to [0, 1]
255
+ video = rearrange(video, "1 c f h w -> f c h w")
256
+ video = (video + 1) / 2 # Denormalize from [-1, 1] to [0, 1]
257
+ video = video.clamp(0, 1)
258
+
259
+ return video
260
+
261
+ def _process_audio_file(self, latent_file: Path, output_dir: Path) -> None:
262
+ """Process a single audio latent file."""
263
+ # Load the latent data
264
+ data = torch.load(latent_file, map_location=self.device, weights_only=False)
265
+
266
+ latents = data["latents"].to(device=self.device, dtype=torch.float32)
267
+ num_time_steps = data["num_time_steps"]
268
+ freq_bins = data["frequency_bins"]
269
+
270
+ # Handle both old patchified [seq_len, C] and new [C, T, F] formats
271
+ if latents.dim() == 2:
272
+ # Old format: [seq_len, channels] where seq_len = time * freq
273
+ # Reshape to [C, T, F]
274
+ latents = rearrange(latents, "(t f) c -> c t f", t=num_time_steps, f=freq_bins)
275
+
276
+ # Add batch dimension: [C, T, F] -> [1, C, T, F]
277
+ latents = latents.unsqueeze(0)
278
+
279
+ # Set correct dtype for audio VAE
280
+ latents = latents.to(dtype=torch.bfloat16)
281
+
282
+ # Decode audio using audio VAE decoder (produces mel spectrogram)
283
+ mel_spectrogram = self.audio_vae(latents)
284
+
285
+ # Convert mel spectrogram to waveform using vocoder
286
+ waveform = self.vocoder(mel_spectrogram)
287
+
288
+ # Save as WAV
289
+ output_path = output_dir / f"{latent_file.stem}.wav"
290
+ sample_rate = self.vocoder.output_sampling_rate
291
+ torchaudio.save(str(output_path), waveform[0].cpu(), sample_rate)
292
+
293
+
294
+ @app.command()
295
+ def main(
296
+ latents_dir: str = typer.Argument(
297
+ ...,
298
+ help="Directory containing the precomputed latent files (searched recursively)",
299
+ ),
300
+ output_dir: str = typer.Argument(
301
+ ...,
302
+ help="Directory to save the decoded videos (maintains same folder hierarchy as input)",
303
+ ),
304
+ model_path: str = typer.Option(
305
+ ...,
306
+ help="Path to LTX-2 checkpoint (.safetensors file)",
307
+ ),
308
+ device: str = typer.Option(
309
+ default="cuda",
310
+ help="Device to use for computation",
311
+ ),
312
+ vae_tiling: bool = typer.Option(
313
+ default=True,
314
+ help="Enable VAE tiling for larger video resolutions",
315
+ ),
316
+ seed: int | None = typer.Option(
317
+ default=None,
318
+ help="Random seed for noise generation during decoding",
319
+ ),
320
+ with_audio: bool = typer.Option(
321
+ default=False,
322
+ help="Also decode audio latents (requires audio_latents directory)",
323
+ ),
324
+ audio_latents_dir: str | None = typer.Option(
325
+ default=None,
326
+ help="Directory containing audio latent files (defaults to 'audio_latents' sibling of latents_dir)",
327
+ ),
328
+ ) -> None:
329
+ """Decode precomputed video latents back into videos using the VAE.
330
+ This script recursively searches for .pt latent files in the input directory
331
+ and decodes them to videos, maintaining the same folder hierarchy in the output.
332
+ Examples:
333
+ # Basic usage
334
+ python scripts/decode_latents.py /path/to/latents /path/to/videos \\
335
+ --model-path /path/to/ltx2.safetensors
336
+ # With VAE tiling for large videos
337
+ python scripts/decode_latents.py /path/to/latents /path/to/videos \\
338
+ --model-path /path/to/ltx2.safetensors --vae-tiling
339
+ # With audio decoding
340
+ python scripts/decode_latents.py /path/to/latents /path/to/videos \\
341
+ --model-path /path/to/ltx2.safetensors --with-audio
342
+ """
343
+ latents_path = Path(latents_dir)
344
+ output_path = Path(output_dir)
345
+
346
+ if not latents_path.exists() or not latents_path.is_dir():
347
+ raise typer.BadParameter(f"Latents directory does not exist: {latents_path}")
348
+
349
+ decoder = LatentsDecoder(
350
+ model_path=model_path,
351
+ device=device,
352
+ vae_tiling=vae_tiling,
353
+ with_audio=with_audio,
354
+ )
355
+ decoder.decode(latents_path, output_path, seed=seed)
356
+
357
+ # Decode audio if requested
358
+ if with_audio:
359
+ audio_path = Path(audio_latents_dir) if audio_latents_dir else latents_path.parent / "audio_latents"
360
+
361
+ if audio_path.exists():
362
+ audio_output_path = output_path.parent / "decoded_audio"
363
+ decoder.decode_audio(audio_path, audio_output_path)
364
+ else:
365
+ logger.warning(f"Audio latents directory not found: {audio_path}")
366
+
367
+
368
+ if __name__ == "__main__":
369
+ app()
packages/ltx-trainer/scripts/process_captions.py ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Compute text embeddings for video generation training.
5
+ This module provides functionality for processing text captions, including:
6
+ - Loading captions from various file formats (CSV, JSON, JSONL)
7
+ - Cleaning and preprocessing text (removing LLM prefixes, adding ID tokens)
8
+ - CaptionsDataset for caption-only preprocessing workflows
9
+ Can be used as a standalone script:
10
+ python scripts/process_captions.py dataset.json --output-dir /path/to/output \
11
+ --model-source /path/to/ltx2.safetensors --text-encoder-path /path/to/gemma
12
+ """
13
+
14
+ import json
15
+ import os
16
+ from collections.abc import Callable
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import pandas as pd
21
+ import torch
22
+ import typer
23
+ from accelerate import PartialState
24
+ from rich.console import Console
25
+ from rich.progress import (
26
+ BarColumn,
27
+ MofNCompleteColumn,
28
+ Progress,
29
+ SpinnerColumn,
30
+ TaskProgressColumn,
31
+ TextColumn,
32
+ TimeElapsedColumn,
33
+ TimeRemainingColumn,
34
+ )
35
+ from torch.utils.data import DataLoader, Dataset, Subset
36
+ from transformers.utils.logging import disable_progress_bar
37
+
38
+ from ltx_trainer import logger
39
+ from ltx_trainer.model_loader import load_embeddings_processor, load_text_encoder
40
+
41
+ # Disable tokenizers parallelism to avoid warnings
42
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
43
+
44
+ disable_progress_bar()
45
+
46
+ # Common phrases that LLMs often add to captions that we might want to remove
47
+ COMMON_BEGINNING_PHRASES: tuple[str, ...] = (
48
+ "This video",
49
+ "The video",
50
+ "This clip",
51
+ "The clip",
52
+ "The animation",
53
+ "This image",
54
+ "The image",
55
+ "This picture",
56
+ "The picture",
57
+ )
58
+
59
+ COMMON_CONTINUATION_WORDS: tuple[str, ...] = (
60
+ "shows",
61
+ "depicts",
62
+ "features",
63
+ "captures",
64
+ "highlights",
65
+ "introduces",
66
+ "presents",
67
+ )
68
+
69
+ COMMON_LLM_START_PHRASES: tuple[str, ...] = (
70
+ "In the video,",
71
+ "In this video,",
72
+ "In this video clip,",
73
+ "In the clip,",
74
+ "Caption:",
75
+ *(
76
+ f"{beginning} {continuation}"
77
+ for beginning in COMMON_BEGINNING_PHRASES
78
+ for continuation in COMMON_CONTINUATION_WORDS
79
+ ),
80
+ )
81
+
82
+ app = typer.Typer(
83
+ pretty_exceptions_enable=False,
84
+ no_args_is_help=True,
85
+ help="Process text captions and save embeddings for video generation training.",
86
+ )
87
+
88
+
89
+ class CaptionsDataset(Dataset):
90
+ """
91
+ Dataset for processing text captions only.
92
+ This dataset is designed for caption preprocessing workflows where you only need
93
+ to process text without loading videos. Useful for:
94
+ - Precomputing text embeddings
95
+ - Caption cleaning and preprocessing
96
+ - Text-only preprocessing pipelines
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ dataset_file: str | Path,
102
+ caption_column: str,
103
+ media_column: str = "media_path",
104
+ lora_trigger: str | None = None,
105
+ remove_llm_prefixes: bool = False,
106
+ ) -> None:
107
+ """
108
+ Initialize the captions dataset.
109
+ Args:
110
+ dataset_file: Path to CSV/JSON/JSONL metadata file
111
+ caption_column: Column name for captions in the metadata file
112
+ media_column: Column name for media paths (used for output naming)
113
+ lora_trigger: Optional trigger word to prepend to each caption
114
+ remove_llm_prefixes: Whether to remove common LLM-generated prefixes
115
+ """
116
+ super().__init__()
117
+
118
+ self.dataset_file = Path(dataset_file)
119
+ self.caption_column = caption_column
120
+ self.media_column = media_column
121
+ self.lora_trigger = f"{lora_trigger.strip()} " if lora_trigger else ""
122
+
123
+ # Load captions with their corresponding output embedding paths
124
+ self.caption_data = self._load_caption_data()
125
+
126
+ # Convert to lists for indexing
127
+ self.output_paths = list(self.caption_data.keys())
128
+ self.prompts = list(self.caption_data.values())
129
+
130
+ # Clean LLM start phrases if requested
131
+ if remove_llm_prefixes:
132
+ self._clean_llm_prefixes()
133
+
134
+ def __len__(self) -> int:
135
+ return len(self.prompts)
136
+
137
+ def __getitem__(self, index: int) -> dict[str, Any]:
138
+ """Get a single caption with optional trigger word prepended and output path."""
139
+ prompt = self.lora_trigger + self.prompts[index]
140
+ return {
141
+ "prompt": prompt,
142
+ "output_path": self.output_paths[index],
143
+ "index": index,
144
+ }
145
+
146
+ def _load_caption_data(self) -> dict[str, str]:
147
+ """Load captions and compute their output embedding paths."""
148
+ if self.dataset_file.suffix == ".csv":
149
+ return self._load_caption_data_from_csv()
150
+ elif self.dataset_file.suffix == ".json":
151
+ return self._load_caption_data_from_json()
152
+ elif self.dataset_file.suffix == ".jsonl":
153
+ return self._load_caption_data_from_jsonl()
154
+ else:
155
+ raise ValueError("Expected `dataset_file` to be a path to a CSV, JSON, or JSONL file.")
156
+
157
+ def _embedding_output_path(self, media_path: Path) -> str:
158
+ """Output `.pt` path relative to the dataset dir; mirrors `process_videos._output_relative`
159
+ so caption keys match video/audio latent keys (and absolute paths don't escape output_dir)."""
160
+ data_root = self.dataset_file.parent
161
+ resolved = data_root / media_path # pathlib: an absolute media_path overrides data_root
162
+ try:
163
+ rel = resolved.relative_to(data_root)
164
+ except ValueError:
165
+ rel = Path(*resolved.parts[1:]) if resolved.is_absolute() else resolved
166
+ return str(rel.with_suffix(".pt"))
167
+
168
+ def _load_caption_data_from_csv(self) -> dict[str, str]:
169
+ """Load captions from a CSV file and compute output embedding paths."""
170
+ df = pd.read_csv(self.dataset_file)
171
+
172
+ if self.caption_column not in df.columns:
173
+ raise ValueError(f"Column '{self.caption_column}' not found in CSV file")
174
+ if self.media_column not in df.columns:
175
+ raise ValueError(f"Column '{self.media_column}' not found in CSV file")
176
+
177
+ caption_data = {}
178
+ for _, row in df.iterrows():
179
+ media_path = Path(row[self.media_column].strip())
180
+ output_path = self._embedding_output_path(media_path)
181
+ caption_data[output_path] = row[self.caption_column]
182
+
183
+ return caption_data
184
+
185
+ def _load_caption_data_from_json(self) -> dict[str, str]:
186
+ """Load captions from a JSON file and compute output embedding paths."""
187
+ with open(self.dataset_file, "r", encoding="utf-8") as file:
188
+ data = json.load(file)
189
+
190
+ if not isinstance(data, list):
191
+ raise ValueError("JSON file must contain a list of objects")
192
+
193
+ caption_data = {}
194
+ for entry in data:
195
+ if self.caption_column not in entry:
196
+ raise ValueError(f"Key '{self.caption_column}' not found in JSON entry: {entry}")
197
+ if self.media_column not in entry:
198
+ raise ValueError(f"Key '{self.media_column}' not found in JSON entry: {entry}")
199
+
200
+ media_path = Path(entry[self.media_column].strip())
201
+ output_path = self._embedding_output_path(media_path)
202
+ caption_data[output_path] = entry[self.caption_column]
203
+
204
+ return caption_data
205
+
206
+ def _load_caption_data_from_jsonl(self) -> dict[str, str]:
207
+ """Load captions from a JSONL file and compute output embedding paths."""
208
+ caption_data = {}
209
+ with open(self.dataset_file, "r", encoding="utf-8") as file:
210
+ for line in file:
211
+ entry = json.loads(line)
212
+ if self.caption_column not in entry:
213
+ raise ValueError(f"Key '{self.caption_column}' not found in JSONL entry: {entry}")
214
+ if self.media_column not in entry:
215
+ raise ValueError(f"Key '{self.media_column}' not found in JSONL entry: {entry}")
216
+
217
+ media_path = Path(entry[self.media_column].strip())
218
+ output_path = self._embedding_output_path(media_path)
219
+ caption_data[output_path] = entry[self.caption_column]
220
+
221
+ return caption_data
222
+
223
+ def _clean_llm_prefixes(self) -> None:
224
+ """Remove common LLM-generated prefixes from captions."""
225
+ for i in range(len(self.prompts)):
226
+ self.prompts[i] = self.prompts[i].strip()
227
+ for phrase in COMMON_LLM_START_PHRASES:
228
+ if self.prompts[i].startswith(phrase):
229
+ self.prompts[i] = self.prompts[i].removeprefix(phrase).strip()
230
+ break
231
+
232
+
233
+ def compute_captions_embeddings( # noqa: PLR0913
234
+ dataset_file: str | Path,
235
+ output_dir: str,
236
+ model_path: str,
237
+ text_encoder_path: str,
238
+ caption_column: str = "caption",
239
+ media_column: str = "media_path",
240
+ lora_trigger: str | None = None,
241
+ remove_llm_prefixes: bool = False,
242
+ batch_size: int = 8,
243
+ device: str = "cuda",
244
+ load_in_8bit: bool = False,
245
+ overwrite: bool = False,
246
+ ) -> None:
247
+ """
248
+ Process captions and save text embeddings.
249
+ Under ``accelerate launch``, each process handles an interleaved shard of
250
+ the dataset (rank/world read from ``accelerate.PartialState``). Already-
251
+ computed ``.pt`` outputs are skipped unless ``overwrite=True``; writes are
252
+ atomic so an interrupted run is safe to resume.
253
+ Args:
254
+ dataset_file: Path to metadata file (CSV/JSON/JSONL) containing captions and media paths
255
+ output_dir: Directory to save embeddings
256
+ model_path: Path to LTX-2 checkpoint (.safetensors)
257
+ text_encoder_path: Path to Gemma text encoder directory
258
+ caption_column: Column name containing captions in the metadata file
259
+ media_column: Column name containing media paths (used for output naming)
260
+ lora_trigger: Optional trigger word to prepend to each caption
261
+ remove_llm_prefixes: Whether to remove common LLM-generated prefixes
262
+ batch_size: Batch size for processing
263
+ device: Device to use for computation
264
+ load_in_8bit: Whether to load the Gemma text encoder in 8-bit precision
265
+ overwrite: Re-encode every item even if its output exists. Use when rerunning with
266
+ changed parameters (different text encoder, lora_trigger, etc.) so stale
267
+ outputs are replaced.
268
+ """
269
+ console = Console()
270
+
271
+ dataset = CaptionsDataset(
272
+ dataset_file=dataset_file,
273
+ caption_column=caption_column,
274
+ media_column=media_column,
275
+ lora_trigger=lora_trigger,
276
+ remove_llm_prefixes=remove_llm_prefixes,
277
+ )
278
+ logger.info(f"Loaded {len(dataset):,} captions")
279
+
280
+ output_path = Path(output_dir)
281
+ output_path.mkdir(parents=True, exist_ok=True)
282
+
283
+ # TODO(batch-tokenization): The current Gemma tokenizer doesn't support batched tokenization.
284
+ if batch_size > 1:
285
+ logger.warning(
286
+ "Batch size greater than 1 is not currently supported with the Gemma tokenizer. "
287
+ "Overriding batch_size to 1. This will be fixed in a future update."
288
+ )
289
+ batch_size = 1
290
+
291
+ dataloader = _build_sharded_dataloader(
292
+ dataset,
293
+ batch_size=batch_size,
294
+ num_workers=2,
295
+ is_done=lambda idx: (output_path / dataset.output_paths[idx]).is_file(),
296
+ overwrite=overwrite,
297
+ )
298
+ if dataloader is None:
299
+ return
300
+
301
+ # Load text encoder and embeddings processor
302
+ with console.status("[bold]Loading Gemma text encoder...", spinner="dots"):
303
+ text_encoder = load_text_encoder(
304
+ text_encoder_path,
305
+ device=device,
306
+ dtype=torch.bfloat16,
307
+ load_in_8bit=load_in_8bit,
308
+ )
309
+ embeddings_processor = load_embeddings_processor(
310
+ model_path,
311
+ device=device,
312
+ dtype=torch.bfloat16,
313
+ )
314
+
315
+ logger.info("Text encoder and embeddings processor loaded successfully")
316
+ logger.info(f"Processing captions in {len(dataloader):,} batches...")
317
+
318
+ with Progress(
319
+ SpinnerColumn(),
320
+ TextColumn("[progress.description]{task.description}"),
321
+ BarColumn(),
322
+ TaskProgressColumn(),
323
+ MofNCompleteColumn(),
324
+ TimeElapsedColumn(),
325
+ TimeRemainingColumn(),
326
+ console=console,
327
+ ) as progress:
328
+ task = progress.add_task("Processing captions", total=len(dataloader))
329
+ for batch in dataloader:
330
+ # Encode prompts using text_encoder.encode() + feature_extractor
331
+ # (returns video/audio features before connector).
332
+ # The connector is applied during training via embeddings_processor
333
+ with torch.inference_mode():
334
+ # TODO(batch-tokenization): When tokenizer supports batching, encode all prompts at once.
335
+ # For now, process one at a time:
336
+ for i in range(len(batch["prompt"])):
337
+ encoded = text_encoder.encode([batch["prompt"][i]], padding_side="left")
338
+ hidden_states, prompt_attention_mask = encoded[0]
339
+ video_prompt_embeds, audio_prompt_embeds = embeddings_processor.feature_extractor(
340
+ hidden_states, prompt_attention_mask, "left"
341
+ )
342
+
343
+ output_rel_path = Path(batch["output_path"][i])
344
+
345
+ # Create output directory maintaining structure
346
+ output_dir_path = output_path / output_rel_path.parent
347
+ output_dir_path.mkdir(parents=True, exist_ok=True)
348
+
349
+ embedding_data = {
350
+ "video_prompt_embeds": video_prompt_embeds[0].cpu().contiguous(),
351
+ "prompt_attention_mask": prompt_attention_mask[0].cpu().contiguous(),
352
+ }
353
+ if audio_prompt_embeds is not None:
354
+ embedding_data["audio_prompt_embeds"] = audio_prompt_embeds[0].cpu().contiguous()
355
+
356
+ output_file = output_path / output_rel_path
357
+ _atomic_save(embedding_data, output_file)
358
+
359
+ progress.advance(task)
360
+
361
+ logger.info(f"Processed {len(dataloader.dataset):,} captions -> {output_path}") # type: ignore[arg-type]
362
+
363
+
364
+ def _atomic_save(data: Any, out: Path) -> None: # noqa: ANN401
365
+ """Save to ``out`` atomically via per-PID temp file + replace.
366
+ Crash mid-write leaves an orphan ``.tmp.<pid>`` file that the skip logic
367
+ ignores. The per-PID suffix makes concurrent writes from multiple ranks
368
+ collision-free.
369
+ """
370
+ tmp = out.with_suffix(f"{out.suffix}.tmp.{os.getpid()}")
371
+ torch.save(data, tmp)
372
+ tmp.replace(out)
373
+
374
+
375
+ def _build_sharded_dataloader(
376
+ dataset: Dataset,
377
+ *,
378
+ batch_size: int,
379
+ num_workers: int,
380
+ is_done: Callable[[int], bool],
381
+ overwrite: bool,
382
+ ) -> DataLoader | None:
383
+ """Return a DataLoader over this rank's interleaved shard of ``dataset``.
384
+ When ``overwrite`` is False, items whose outputs already exist (per
385
+ ``is_done``) are filtered out. Returns ``None`` if this rank has nothing
386
+ to do, so the caller can early-return without loading any models.
387
+ """
388
+ state = PartialState()
389
+ todo = [i for i in range(state.process_index, len(dataset), state.num_processes) if overwrite or not is_done(i)]
390
+ if not todo:
391
+ logger.info(f"Rank {state.process_index}/{state.num_processes}: nothing to do")
392
+ return None
393
+ logger.info(f"Rank {state.process_index}/{state.num_processes}: processing {len(todo):,} of {len(dataset):,} items")
394
+ return DataLoader(Subset(dataset, todo), batch_size=batch_size, shuffle=False, num_workers=num_workers)
395
+
396
+
397
+ @app.command()
398
+ def main( # noqa: PLR0913
399
+ dataset_file: str = typer.Argument(
400
+ ...,
401
+ help="Path to metadata file (CSV/JSON/JSONL) containing captions and media paths",
402
+ ),
403
+ output_dir: str = typer.Option(
404
+ ...,
405
+ help="Output directory to save text embeddings",
406
+ ),
407
+ model_path: str = typer.Option(
408
+ ...,
409
+ help="Path to LTX-2 checkpoint (.safetensors file)",
410
+ ),
411
+ text_encoder_path: str = typer.Option(
412
+ ...,
413
+ help="Path to Gemma text encoder directory",
414
+ ),
415
+ caption_column: str = typer.Option(
416
+ default="caption",
417
+ help="Column name containing captions in the dataset JSON/JSONL/CSV file",
418
+ ),
419
+ media_column: str = typer.Option(
420
+ default="media_path",
421
+ help="Column name in the dataset JSON/JSONL/CSV file containing media paths "
422
+ "(used for output file naming and folder structure)",
423
+ ),
424
+ batch_size: int = typer.Option(
425
+ default=8,
426
+ help="Batch size for processing",
427
+ ),
428
+ device: str = typer.Option(
429
+ default="cuda",
430
+ help="Device to use for computation",
431
+ ),
432
+ lora_trigger: str | None = typer.Option(
433
+ default=None,
434
+ help="Optional trigger word to prepend to each caption (activates the LoRA during inference)",
435
+ ),
436
+ remove_llm_prefixes: bool = typer.Option(
437
+ default=False,
438
+ help="Remove common LLM-generated prefixes from captions",
439
+ ),
440
+ load_text_encoder_in_8bit: bool = typer.Option(
441
+ default=False,
442
+ help="Load the Gemma text encoder in 8-bit precision to save GPU memory (requires bitsandbytes)",
443
+ ),
444
+ overwrite: bool = typer.Option(
445
+ default=False,
446
+ help="Re-encode every caption even if its output exists. Use when rerunning with "
447
+ "changed parameters (different text encoder, lora_trigger, etc.) so stale outputs are replaced.",
448
+ ),
449
+ ) -> None:
450
+ """Process text captions and save embeddings for video generation training.
451
+ For multi-GPU preprocessing, invoke under ``accelerate launch`` - each process
452
+ will handle an interleaved shard of the dataset.
453
+ This script processes captions from metadata files and saves text embeddings
454
+ that can be used for training video generation models. The output embeddings
455
+ will maintain the same folder structure and naming as the corresponding media files.
456
+ Note: This script is designed for LTX-2 models which use the Gemma text encoder.
457
+ Examples:
458
+ # Process captions with LTX-2 model
459
+ python scripts/process_captions.py dataset.json --output-dir ./embeddings \\
460
+ --model-path /path/to/ltx2_checkpoint.safetensors \\
461
+ --text-encoder-path /path/to/gemma
462
+ # Add a trigger word for LoRA training
463
+ python scripts/process_captions.py dataset.json --output-dir ./embeddings \\
464
+ --model-path /path/to/ltx2.safetensors --text-encoder-path /path/to/gemma \\
465
+ --lora-trigger "mytoken"
466
+ # Remove LLM-generated prefixes from captions
467
+ python scripts/process_captions.py dataset.json --output-dir ./embeddings \\
468
+ --model-path /path/to/ltx2.safetensors --text-encoder-path /path/to/gemma \\
469
+ --remove-llm-prefixes
470
+ """
471
+
472
+ # Validate dataset file
473
+ if not Path(dataset_file).is_file():
474
+ raise typer.BadParameter(f"Dataset file not found: {dataset_file}")
475
+
476
+ if lora_trigger:
477
+ logger.info(f'LoRA trigger word "{lora_trigger}" will be prepended to all captions')
478
+
479
+ # Process embeddings
480
+ compute_captions_embeddings(
481
+ dataset_file=dataset_file,
482
+ output_dir=output_dir,
483
+ model_path=model_path,
484
+ text_encoder_path=text_encoder_path,
485
+ caption_column=caption_column,
486
+ media_column=media_column,
487
+ lora_trigger=lora_trigger,
488
+ remove_llm_prefixes=remove_llm_prefixes,
489
+ batch_size=batch_size,
490
+ device=device,
491
+ load_in_8bit=load_text_encoder_in_8bit,
492
+ overwrite=overwrite,
493
+ )
494
+
495
+
496
+ if __name__ == "__main__":
497
+ app()
packages/ltx-trainer/scripts/process_dataset.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Preprocess a media dataset for LTX-2 training.
5
+ Automatically detects dataset columns and processes each according to a convention table.
6
+ Column names determine what gets encoded and where outputs go — no per-role CLI flags needed.
7
+ Convention table:
8
+ video → Video VAE → latents/
9
+ audio → Audio VAE → audio_latents/
10
+ reference_video → Video VAE → reference_latents/
11
+ reference_audio → Audio VAE → reference_audio_latents/
12
+ video_mask → (downsample) → video_masks/
13
+ audio_mask → (downsample) → audio_masks/
14
+ caption → Text encoder → conditions/
15
+ Legacy aliases: media_path → video, ref_media_path → reference_video
16
+ Basic usage:
17
+ python scripts/process_dataset.py /path/to/dataset.json --resolution-buckets 768x768x49 \\
18
+ --model-path /path/to/ltx2.safetensors --text-encoder-path /path/to/gemma
19
+ """
20
+
21
+ from pathlib import Path
22
+
23
+ import typer
24
+ from decode_latents import LatentsDecoder
25
+ from process_captions import compute_captions_embeddings
26
+ from process_videos import (
27
+ compute_audio_latents,
28
+ compute_audio_masks,
29
+ compute_latents,
30
+ compute_scaled_resolution_buckets,
31
+ compute_video_masks,
32
+ detect_dataset_columns,
33
+ parse_resolution_buckets,
34
+ )
35
+ from rich.console import Console
36
+
37
+ from ltx_trainer import logger
38
+ from ltx_trainer.gpu_utils import free_gpu_memory_context
39
+
40
+ console = Console()
41
+
42
+ app = typer.Typer(
43
+ pretty_exceptions_enable=False,
44
+ no_args_is_help=True,
45
+ help="Preprocess a media dataset for LTX-2 training. "
46
+ "Automatically detects columns (video, audio, reference_video, reference_audio, caption) "
47
+ "and processes each with the appropriate encoder.",
48
+ )
49
+
50
+ _KNOWN_ROLES = {"video", "audio", "reference_video", "reference_audio", "video_mask", "audio_mask", "caption"}
51
+ _LEGACY_ALIASES = {"media_path": "video", "ref_media_path": "reference_video"}
52
+
53
+
54
+ def preprocess_dataset( # noqa: PLR0912, PLR0913, PLR0915
55
+ dataset_file: str,
56
+ resolution_buckets: list[tuple[int, int, int]] | None,
57
+ model_path: str,
58
+ text_encoder_path: str,
59
+ device: str,
60
+ output_dir: str | None = None,
61
+ video_column: str | None = None,
62
+ caption_column: str | None = None,
63
+ batch_size: int = 1,
64
+ lora_trigger: str | None = None,
65
+ vae_tiling: bool = False,
66
+ decode: bool = False,
67
+ remove_llm_prefixes: bool = False,
68
+ reference_downscale_factor: int = 1,
69
+ reference_temporal_scale_factor: int = 1,
70
+ skip_audio: bool = False,
71
+ audio_durations: list[float] | None = None,
72
+ load_text_encoder_in_8bit: bool = False,
73
+ overwrite: bool = False,
74
+ ) -> None:
75
+ """Run the preprocessing pipeline with convention-based column detection."""
76
+ _validate_dataset_file(dataset_file)
77
+
78
+ # Detect columns and resolve roles
79
+ dataset_columns = detect_dataset_columns(dataset_file)
80
+ roles = _resolve_columns(dataset_columns, video_column, caption_column)
81
+
82
+ # Log detected roles
83
+ for role, col in sorted(roles.items()):
84
+ alias_note = f" (alias for '{role}')" if col != role else ""
85
+ logger.info(f"Detected column '{col}'{alias_note} → {role}")
86
+
87
+ # Validate: need at least caption
88
+ if "caption" not in roles:
89
+ raise ValueError(
90
+ f"No caption column found. Dataset has columns: {dataset_columns}. "
91
+ f"Expected 'caption' or use --caption-column to specify."
92
+ )
93
+
94
+ # Validate: need video or audio
95
+ has_video = "video" in roles
96
+ has_audio = "audio" in roles
97
+ if not has_video and not has_audio:
98
+ raise ValueError(
99
+ f"No media column found. Dataset has columns: {dataset_columns}. "
100
+ f"Expected 'video', 'audio', or 'media_path' (legacy)."
101
+ )
102
+
103
+ # Validate: video modes need resolution buckets
104
+ if has_video and not resolution_buckets:
105
+ raise ValueError("--resolution-buckets is required when the dataset has a video column.")
106
+
107
+ output_base = Path(output_dir) if output_dir else Path(dataset_file).parent / ".precomputed"
108
+
109
+ if lora_trigger:
110
+ logger.info(f'LoRA trigger word "{lora_trigger}" will be prepended to all captions')
111
+
112
+ # --- Phase 1: Text encoder ---
113
+ with free_gpu_memory_context():
114
+ compute_captions_embeddings(
115
+ dataset_file=dataset_file,
116
+ output_dir=str(output_base / "conditions"),
117
+ model_path=model_path,
118
+ text_encoder_path=text_encoder_path,
119
+ caption_column=roles["caption"],
120
+ media_column=roles.get("video") or roles.get("audio") or roles["caption"],
121
+ lora_trigger=lora_trigger,
122
+ remove_llm_prefixes=remove_llm_prefixes,
123
+ batch_size=batch_size,
124
+ device=device,
125
+ load_in_8bit=load_text_encoder_in_8bit,
126
+ overwrite=overwrite,
127
+ )
128
+
129
+ # --- Phase 2: Video VAE (video, reference_video) ---
130
+ if has_video and resolution_buckets:
131
+ # Determine if audio should be auto-extracted from video files
132
+ auto_audio = not skip_audio and "audio" not in roles
133
+
134
+ audio_latents_dir = str(output_base / "audio_latents") if auto_audio else None
135
+ if auto_audio:
136
+ logger.info("Audio will be auto-extracted from video files (use --skip-audio to disable)")
137
+
138
+ with free_gpu_memory_context():
139
+ compute_latents(
140
+ dataset_file=dataset_file,
141
+ video_column=roles["video"],
142
+ resolution_buckets=resolution_buckets,
143
+ output_dir=str(output_base / "latents"),
144
+ model_path=model_path,
145
+ batch_size=batch_size,
146
+ device=device,
147
+ vae_tiling=vae_tiling,
148
+ with_audio=auto_audio,
149
+ audio_output_dir=audio_latents_dir,
150
+ overwrite=overwrite,
151
+ )
152
+
153
+ # Process reference video if present
154
+ if "reference_video" in roles:
155
+ if reference_downscale_factor > 1 and len(resolution_buckets) > 1:
156
+ raise ValueError(
157
+ "When using --reference-downscale-factor > 1, only a single resolution bucket is supported."
158
+ )
159
+ if reference_temporal_scale_factor > 1 and len(resolution_buckets) > 1:
160
+ raise ValueError(
161
+ "When using --reference-temporal-scale-factor > 1, only a single resolution bucket is supported."
162
+ )
163
+
164
+ reference_buckets = compute_scaled_resolution_buckets(resolution_buckets, reference_downscale_factor)
165
+ if reference_downscale_factor > 1:
166
+ logger.info(f"Processing reference videos at 1/{reference_downscale_factor} resolution...")
167
+ if reference_temporal_scale_factor > 1:
168
+ logger.info(
169
+ f"Temporally subsampling reference videos by {reference_temporal_scale_factor}x "
170
+ f"(VAE-aligned pattern)..."
171
+ )
172
+
173
+ with free_gpu_memory_context():
174
+ compute_latents(
175
+ dataset_file=dataset_file,
176
+ main_media_column=roles["video"],
177
+ video_column=roles["reference_video"],
178
+ resolution_buckets=reference_buckets,
179
+ output_dir=str(output_base / "reference_latents"),
180
+ model_path=model_path,
181
+ batch_size=batch_size,
182
+ device=device,
183
+ vae_tiling=vae_tiling,
184
+ overwrite=overwrite,
185
+ temporal_subsample_factor=reference_temporal_scale_factor,
186
+ )
187
+
188
+ # --- Phase 2b: Masks (video_mask, audio_mask) — processed after video latents for alignment ---
189
+ if "video_mask" in roles and has_video:
190
+ compute_video_masks(
191
+ dataset_file=dataset_file,
192
+ mask_column=roles["video_mask"],
193
+ latents_dir=str(output_base / "latents"),
194
+ output_dir=str(output_base / "video_masks"),
195
+ main_media_column=roles["video"],
196
+ )
197
+
198
+ # --- Phase 3: Audio VAE (audio, reference_audio) ---
199
+ audio_roles_to_process = [
200
+ ("audio", "audio_latents"),
201
+ ("reference_audio", "reference_audio_latents"),
202
+ ]
203
+ active_audio_roles = [(role, subdir) for role, subdir in audio_roles_to_process if role in roles]
204
+
205
+ if active_audio_roles:
206
+ # Determine audio duration constraint: video bucket → max_duration, or explicit buckets
207
+ max_audio_duration = None
208
+ audio_duration_buckets = None
209
+ if has_video and resolution_buckets:
210
+ max_audio_duration = max(f for f, _h, _w in resolution_buckets) / 25.0
211
+ elif audio_durations:
212
+ audio_duration_buckets = audio_durations
213
+
214
+ for role, output_subdir in active_audio_roles:
215
+ with free_gpu_memory_context():
216
+ compute_audio_latents(
217
+ dataset_file=dataset_file,
218
+ audio_column=roles[role],
219
+ output_dir=str(output_base / output_subdir),
220
+ model_path=model_path,
221
+ main_media_column=roles.get("video"),
222
+ max_duration=max_audio_duration,
223
+ duration_buckets=audio_duration_buckets,
224
+ device=device,
225
+ overwrite=overwrite,
226
+ )
227
+
228
+ # --- Phase 4: Audio masks (after audio latents exist for temporal alignment) ---
229
+ if "audio_mask" in roles:
230
+ audio_latents_source = output_base / "audio_latents"
231
+ if audio_latents_source.exists():
232
+ compute_audio_masks(
233
+ dataset_file=dataset_file,
234
+ mask_column=roles["audio_mask"],
235
+ audio_latents_dir=str(audio_latents_source),
236
+ output_dir=str(output_base / "audio_masks"),
237
+ main_media_column=roles.get("video") or roles.get("audio"),
238
+ )
239
+ else:
240
+ logger.warning("audio_mask column found but no audio_latents/ — run with audio first")
241
+
242
+ # --- Decode for verification ---
243
+ if decode:
244
+ logger.info("Decoding latents for verification...")
245
+ decoder = LatentsDecoder(model_path=model_path, device=device, vae_tiling=vae_tiling, with_audio=has_audio)
246
+ if has_video:
247
+ decoder.decode(output_base / "latents", output_base / "decoded_videos")
248
+ if "reference_video" in roles and (output_base / "reference_latents").exists():
249
+ decoder.decode(output_base / "reference_latents", output_base / "decoded_reference_videos")
250
+
251
+ # --- Summary ---
252
+ logger.info(f"Dataset preprocessing complete! Results saved to {output_base}")
253
+ produced = [d.name for d in output_base.iterdir() if d.is_dir() and not d.name.startswith("decoded")]
254
+ logger.info(f"Output directories: {', '.join(sorted(produced))}")
255
+
256
+
257
+ def _validate_dataset_file(dataset_path: str) -> None:
258
+ """Validate that the dataset file exists and has the correct format."""
259
+ dataset_file = Path(dataset_path)
260
+ if not dataset_file.exists():
261
+ raise FileNotFoundError(f"Dataset file does not exist: {dataset_file}")
262
+ if not dataset_file.is_file():
263
+ raise ValueError(f"Dataset path must be a file, not a directory: {dataset_file}")
264
+ if dataset_file.suffix.lower() not in [".csv", ".json", ".jsonl"]:
265
+ raise ValueError(f"Dataset file must be CSV, JSON, or JSONL format: {dataset_file}")
266
+
267
+
268
+ def _resolve_columns(
269
+ dataset_columns: set[str],
270
+ video_column_override: str | None = None,
271
+ caption_column_override: str | None = None,
272
+ ) -> dict[str, str]:
273
+ """Map canonical role names to actual dataset column names.
274
+ Returns a dict of role → column_name for recognized roles found in the dataset.
275
+ """
276
+ roles: dict[str, str] = {}
277
+ for col in dataset_columns:
278
+ role = _LEGACY_ALIASES.get(col, col)
279
+ if role in _KNOWN_ROLES:
280
+ roles[role] = col
281
+
282
+ if video_column_override and video_column_override in dataset_columns:
283
+ roles["video"] = video_column_override
284
+ if caption_column_override and caption_column_override in dataset_columns:
285
+ roles["caption"] = caption_column_override
286
+
287
+ return roles
288
+
289
+
290
+ @app.command()
291
+ def main( # noqa: PLR0913
292
+ dataset_path: str = typer.Argument(
293
+ ...,
294
+ help="Path to metadata file (CSV/JSON/JSONL) with columns matching the convention table",
295
+ ),
296
+ resolution_buckets: str | None = typer.Option(
297
+ default=None,
298
+ help='Resolution buckets in format "WxHxF;WxHxF;..." (e.g. "768x768x25"). '
299
+ "Required when dataset has a video column.",
300
+ ),
301
+ model_path: str = typer.Option(
302
+ ...,
303
+ help="Path to LTX-2 checkpoint (.safetensors file)",
304
+ ),
305
+ text_encoder_path: str = typer.Option(
306
+ ...,
307
+ help="Path to Gemma text encoder directory",
308
+ ),
309
+ caption_column: str | None = typer.Option(
310
+ default=None,
311
+ help="Override: treat this column as 'caption' (default: auto-detect 'caption')",
312
+ ),
313
+ video_column: str | None = typer.Option(
314
+ default=None,
315
+ help="Override: treat this column as 'video' (default: auto-detect 'video' or 'media_path')",
316
+ ),
317
+ batch_size: int = typer.Option(
318
+ default=1,
319
+ help="Batch size for preprocessing",
320
+ ),
321
+ device: str = typer.Option(
322
+ default="cuda",
323
+ help="Device to use for computation",
324
+ ),
325
+ vae_tiling: bool = typer.Option(
326
+ default=False,
327
+ help="Enable VAE tiling for larger video resolutions",
328
+ ),
329
+ output_dir: str | None = typer.Option(
330
+ default=None,
331
+ help="Output directory (defaults to .precomputed in dataset directory)",
332
+ ),
333
+ lora_trigger: str | None = typer.Option(
334
+ default=None,
335
+ help="Optional trigger word to prepend to each caption",
336
+ ),
337
+ decode: bool = typer.Option(
338
+ default=False,
339
+ help="Decode and save latents after encoding for verification",
340
+ ),
341
+ remove_llm_prefixes: bool = typer.Option(
342
+ default=False,
343
+ help="Remove LLM prefixes from captions",
344
+ ),
345
+ skip_audio: bool = typer.Option(
346
+ default=False,
347
+ help="Don't extract audio from video files (audio extraction is on by default)",
348
+ ),
349
+ audio_durations: str | None = typer.Option(
350
+ default=None,
351
+ help='Audio duration buckets in seconds for audio-only datasets (e.g. "2.0;4.0;8.0"). '
352
+ "When set, audio files are trimmed to the best matching duration. "
353
+ "Not needed when a video column is present (audio duration derived from video bucket).",
354
+ ),
355
+ with_audio: bool = typer.Option(
356
+ default=False,
357
+ hidden=True,
358
+ help="[DEPRECATED: audio is now on by default, use --skip-audio to disable]",
359
+ ),
360
+ load_text_encoder_in_8bit: bool = typer.Option(
361
+ default=False,
362
+ help="Load the Gemma text encoder in 8-bit precision to save GPU memory",
363
+ ),
364
+ reference_downscale_factor: int = typer.Option(
365
+ default=1,
366
+ help="Downscale factor for reference video resolution (e.g., 2 = half resolution for IC-LoRA)",
367
+ ),
368
+ reference_temporal_scale_factor: int = typer.Option(
369
+ default=1,
370
+ help="Temporal subsampling factor for reference videos (e.g., 2 = half frame rate, "
371
+ "VAE-aligned: keeps frame 0, then every Nth frame from frame 1 onwards)",
372
+ ),
373
+ overwrite: bool = typer.Option(
374
+ default=False,
375
+ help="Re-compute every item even if its output exists. Use when rerunning with "
376
+ "changed parameters (different model, resolution, etc.) so stale outputs are replaced.",
377
+ ),
378
+ ) -> None:
379
+ """Preprocess a media dataset for LTX-2 training.
380
+ See module docstring for the convention table. Audio is auto-extracted from
381
+ video files by default — use --skip-audio to disable.
382
+ For multi-GPU preprocessing, invoke under ``accelerate launch`` -- each process
383
+ will handle an interleaved shard of the dataset.
384
+ """
385
+ # Handle deprecated --with-audio flag
386
+ if with_audio:
387
+ logger.warning(
388
+ "--with-audio is deprecated. Audio extraction is now on by default. Use --skip-audio to disable."
389
+ )
390
+
391
+ parsed_buckets = parse_resolution_buckets(resolution_buckets) if resolution_buckets else None
392
+
393
+ if parsed_buckets and len(parsed_buckets) > 1:
394
+ logger.warning("Using multiple resolution buckets. Training batch size must be 1.")
395
+
396
+ if reference_downscale_factor < 1:
397
+ raise typer.BadParameter("--reference-downscale-factor must be >= 1")
398
+
399
+ if reference_temporal_scale_factor < 1:
400
+ raise typer.BadParameter("--reference-temporal-scale-factor must be >= 1")
401
+
402
+ parsed_audio_durations = None
403
+ if audio_durations:
404
+ parsed_audio_durations = [float(d) for d in audio_durations.split(";")]
405
+ if any(d <= 0 for d in parsed_audio_durations):
406
+ raise typer.BadParameter("All audio durations must be positive")
407
+
408
+ preprocess_dataset(
409
+ dataset_file=dataset_path,
410
+ resolution_buckets=parsed_buckets,
411
+ model_path=model_path,
412
+ text_encoder_path=text_encoder_path,
413
+ device=device,
414
+ output_dir=output_dir,
415
+ video_column=video_column,
416
+ caption_column=caption_column,
417
+ batch_size=batch_size,
418
+ lora_trigger=lora_trigger,
419
+ vae_tiling=vae_tiling,
420
+ decode=decode,
421
+ remove_llm_prefixes=remove_llm_prefixes,
422
+ reference_downscale_factor=reference_downscale_factor,
423
+ reference_temporal_scale_factor=reference_temporal_scale_factor,
424
+ skip_audio=skip_audio,
425
+ audio_durations=parsed_audio_durations,
426
+ load_text_encoder_in_8bit=load_text_encoder_in_8bit,
427
+ overwrite=overwrite,
428
+ )
429
+
430
+
431
+ if __name__ == "__main__":
432
+ app()
packages/ltx-trainer/scripts/process_videos.py ADDED
@@ -0,0 +1,1474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Compute latent representations for video generation training.
5
+ This module provides functionality for processing video and image files, including:
6
+ - Loading videos/images from various file formats (CSV, JSON, JSONL)
7
+ - Resizing, cropping, and transforming media
8
+ - MediaDataset for video-only preprocessing workflows
9
+ - BucketSampler for grouping videos by resolution
10
+ Can be used as a standalone script:
11
+ python scripts/process_videos.py dataset.csv --resolution-buckets 768x768x25 \
12
+ --output-dir /path/to/output --model-source /path/to/ltx2.safetensors
13
+ """
14
+
15
+ import json
16
+ import math
17
+ import os
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+ import torch
25
+ import torchaudio
26
+ import typer
27
+ from accelerate import PartialState
28
+ from pillow_heif import register_heif_opener
29
+ from rich.console import Console
30
+ from rich.progress import (
31
+ BarColumn,
32
+ MofNCompleteColumn,
33
+ Progress,
34
+ SpinnerColumn,
35
+ TaskProgressColumn,
36
+ TextColumn,
37
+ TimeElapsedColumn,
38
+ TimeRemainingColumn,
39
+ )
40
+ from torch.utils.data import DataLoader, Dataset, Subset
41
+ from torchvision import transforms
42
+ from torchvision.transforms import InterpolationMode
43
+ from torchvision.transforms.functional import crop, resize, to_tensor
44
+ from torchvision.transforms.functional import resize as tv_resize
45
+ from transformers.utils.logging import disable_progress_bar
46
+
47
+ from ltx_core.model.audio_vae import AudioProcessor
48
+ from ltx_core.types import Audio
49
+ from ltx_trainer import logger
50
+ from ltx_trainer.model_loader import load_audio_vae_encoder, load_video_vae_encoder
51
+ from ltx_trainer.utils import open_image_as_srgb
52
+ from ltx_trainer.video_utils import get_video_frame_count, read_video
53
+
54
+ disable_progress_bar()
55
+
56
+ # Register HEIF/HEIC support
57
+ register_heif_opener()
58
+
59
+ # Constants for validation
60
+ VAE_SPATIAL_FACTOR = 32
61
+ VAE_TEMPORAL_FACTOR = 8
62
+
63
+ # Audio constants
64
+ AUDIO_LATENT_CHANNELS = 8
65
+ AUDIO_FREQUENCY_BINS = 16
66
+
67
+ DEFAULT_TILE_SIZE = 512 # Spatial tile size in pixels (must be ≥64 and divisible by 32)
68
+ DEFAULT_TILE_OVERLAP = 128 # Spatial tile overlap in pixels (must be divisible by 32)
69
+
70
+ app = typer.Typer(
71
+ pretty_exceptions_enable=False,
72
+ no_args_is_help=True,
73
+ help="Process videos/images and save latent representations for video generation training.",
74
+ )
75
+
76
+
77
+ def _clamp_01(x: torch.Tensor) -> torch.Tensor:
78
+ return x.clamp_(0, 1)
79
+
80
+
81
+ class MediaDataset(Dataset):
82
+ """
83
+ Dataset for processing video and image files.
84
+ This dataset is designed for media preprocessing workflows where you need to:
85
+ - Load and preprocess videos/images
86
+ - Apply resizing and cropping transformations
87
+ - Handle different resolution buckets
88
+ - Filter out invalid media files
89
+ - Optionally extract audio from video files
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ dataset_file: str | Path,
95
+ main_media_column: str,
96
+ video_column: str,
97
+ resolution_buckets: list[tuple[int, int, int]],
98
+ reshape_mode: str = "center",
99
+ with_audio: bool = False,
100
+ temporal_subsample_factor: int = 1,
101
+ ) -> None:
102
+ """
103
+ Initialize the media dataset.
104
+ Args:
105
+ dataset_file: Path to CSV/JSON/JSONL metadata file
106
+ video_column: Column name for video paths in the metadata file
107
+ resolution_buckets: List of (frames, height, width) tuples
108
+ reshape_mode: How to crop videos ("center", "random")
109
+ with_audio: Whether to extract audio from video files
110
+ temporal_subsample_factor: Factor for VAE-aligned temporal subsampling.
111
+ When > 1, keeps frame 0 then takes every Nth frame from frame 1 onwards.
112
+ """
113
+ super().__init__()
114
+
115
+ self.dataset_file = Path(dataset_file)
116
+ self.main_media_column = main_media_column
117
+ self.resolution_buckets = resolution_buckets
118
+ self.reshape_mode = reshape_mode
119
+ self.with_audio = with_audio
120
+ self.temporal_subsample_factor = temporal_subsample_factor
121
+
122
+ # First load main media paths
123
+ self.main_media_paths = self._load_video_paths(main_media_column)
124
+
125
+ # Then load reference video paths
126
+ self.video_paths = self._load_video_paths(video_column)
127
+
128
+ # Filter out videos with insufficient frames
129
+ self._filter_valid_videos()
130
+
131
+ self.max_target_frames = max(self.resolution_buckets, key=lambda x: x[0])[0]
132
+
133
+ # Set up video transforms
134
+ self.transforms = transforms.Compose(
135
+ [
136
+ transforms.Lambda(_clamp_01),
137
+ transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True),
138
+ ]
139
+ )
140
+
141
+ def __len__(self) -> int:
142
+ return len(self.video_paths)
143
+
144
+ def __getitem__(self, index: int) -> dict[str, Any]:
145
+ """Get a single video/image with metadata, and optionally audio."""
146
+ if isinstance(index, list):
147
+ # Special case for BucketSampler - return cached data
148
+ return index
149
+
150
+ video_path: Path = self.video_paths[index]
151
+
152
+ # Compute relative path of the video
153
+ data_root = self.dataset_file.parent
154
+ relative_path = str(_output_relative(video_path, data_root))
155
+ media_relative_path = str(_output_relative(self.main_media_paths[index], data_root))
156
+
157
+ if video_path.suffix.lower() in [".png", ".jpg", ".jpeg"]:
158
+ media_tensor = self._preprocess_image(video_path)
159
+ fps = 1.0
160
+ audio_data = None # Images don't have audio
161
+ else:
162
+ media_tensor, fps = self._preprocess_video(video_path)
163
+
164
+ # Extract audio if enabled
165
+ if self.with_audio:
166
+ # Calculate target duration from the processed video frames
167
+ # This ensures audio is trimmed to match the exact video duration
168
+ # media_tensor is [C, F, H, W] so shape[1] is num_frames
169
+ target_duration = media_tensor.shape[1] / fps
170
+ audio_data = self._extract_audio(video_path, target_duration)
171
+ else:
172
+ audio_data = None
173
+
174
+ # media_tensor is [C, F, H, W] format for VAE compatibility
175
+ _, num_frames, height, width = media_tensor.shape
176
+
177
+ result = {
178
+ "video": media_tensor,
179
+ "relative_path": relative_path,
180
+ "main_media_relative_path": media_relative_path,
181
+ "video_metadata": {
182
+ "num_frames": num_frames,
183
+ "height": height,
184
+ "width": width,
185
+ "fps": fps,
186
+ },
187
+ }
188
+
189
+ # Add audio data if available
190
+ if audio_data is not None:
191
+ result["audio"] = audio_data
192
+
193
+ return result
194
+
195
+ @staticmethod
196
+ def _extract_audio(video_path: Path, target_duration: float) -> dict[str, torch.Tensor | int] | None:
197
+ """Extract audio track from a video file, trimmed/padded to match video duration."""
198
+ audio = _load_audio_from_file(video_path, max_duration=target_duration)
199
+ if audio is None:
200
+ return None
201
+
202
+ # Pad if shorter than target (_load_audio_from_file only trims, doesn't pad)
203
+ target_samples = int(target_duration * audio.sampling_rate)
204
+ if audio.waveform.shape[-1] < target_samples:
205
+ padding = target_samples - audio.waveform.shape[-1]
206
+ waveform = torch.nn.functional.pad(audio.waveform, (0, padding))
207
+ logger.warning(f"Padded audio to {target_duration:.2f} seconds for {video_path}")
208
+ else:
209
+ waveform = audio.waveform
210
+
211
+ return {"waveform": waveform, "sample_rate": audio.sampling_rate}
212
+
213
+ def _load_video_paths(self, column: str) -> list[Path]:
214
+ """Load video paths from the specified data source, validating existence."""
215
+ paths = _load_paths_from_dataset(self.dataset_file, column)
216
+ invalid = [p for p in paths if not p.is_file()]
217
+ if invalid:
218
+ raise ValueError(f"Found {len(invalid)} invalid paths in '{column}'. First few: {invalid[:5]}")
219
+ return paths
220
+
221
+ def _filter_valid_videos(self) -> None:
222
+ """Filter out videos with insufficient frames."""
223
+ original_length = len(self.video_paths)
224
+ valid_video_paths = []
225
+ valid_main_media_paths = []
226
+ min_frames_required = min(self.resolution_buckets, key=lambda x: x[0])[0]
227
+
228
+ for i, video_path in enumerate(self.video_paths):
229
+ if video_path.suffix.lower() in [".png", ".jpg", ".jpeg"]:
230
+ valid_video_paths.append(video_path)
231
+ valid_main_media_paths.append(self.main_media_paths[i])
232
+ continue
233
+
234
+ try:
235
+ frame_count = get_video_frame_count(video_path)
236
+
237
+ if frame_count >= min_frames_required:
238
+ valid_video_paths.append(video_path)
239
+ valid_main_media_paths.append(self.main_media_paths[i])
240
+ else:
241
+ logger.warning(
242
+ f"Skipping video at {video_path} - has {frame_count} frames, "
243
+ f"which is less than the minimum required frames ({min_frames_required})"
244
+ )
245
+ except Exception as e:
246
+ logger.warning(f"Failed to read video at {video_path}: {e!s}")
247
+
248
+ # Update both path lists to maintain synchronization
249
+ self.video_paths = valid_video_paths
250
+ self.main_media_paths = valid_main_media_paths
251
+
252
+ if len(self.video_paths) < original_length:
253
+ logger.warning(
254
+ f"Filtered out {original_length - len(self.video_paths)} videos with insufficient frames. "
255
+ f"Proceeding with {len(self.video_paths)} valid videos."
256
+ )
257
+
258
+ def _preprocess_image(self, path: Path) -> torch.Tensor:
259
+ """Preprocess a single image by resizing and applying transforms."""
260
+ image = open_image_as_srgb(path)
261
+ image = to_tensor(image)
262
+ image = image.unsqueeze(0) # Add frame dimension [1, C, H, W] for bucket selection
263
+
264
+ # Find nearest resolution bucket and resize
265
+ nearest_bucket = self._get_resolution_bucket_for_item(image)
266
+ _, target_height, target_width = nearest_bucket
267
+ image_resized = self._resize_and_crop(image, target_height, target_width)
268
+ # _resize_and_crop returns [C, H, W] for single-frame input (squeeze removes dim 0)
269
+
270
+ # Apply transforms
271
+ image = self.transforms(image_resized) # [C, H, W] -> [C, H, W]
272
+
273
+ # Add frame dimension in VAE format: [C, H, W] -> [C, 1, H, W]
274
+ image = image.unsqueeze(1)
275
+ return image
276
+
277
+ def _preprocess_video(self, path: Path) -> tuple[torch.Tensor, float]:
278
+ """Preprocess a video by loading, resizing, and applying transforms.
279
+ Returns:
280
+ Tuple of (video tensor in [C, F, H, W] format, fps)
281
+ """
282
+ # Load video frames up to max_target_frames
283
+ video, fps = read_video(path, max_frames=self.max_target_frames)
284
+
285
+ nearest_bucket = self._get_resolution_bucket_for_item(video)
286
+ target_num_frames, target_height, target_width = nearest_bucket
287
+ frames_resized = self._resize_and_crop(video, target_height, target_width)
288
+
289
+ # Trim video to target number of frames
290
+ frames_resized = frames_resized[:target_num_frames]
291
+
292
+ # VAE-aligned temporal subsampling: keep frame 0, then every Nth frame
293
+ if self.temporal_subsample_factor > 1:
294
+ indices = _compute_temporal_subsample_indices(target_num_frames, self.temporal_subsample_factor)
295
+ frames_resized = frames_resized[indices]
296
+
297
+ # Apply transforms to each frame and stack
298
+ video = torch.stack([self.transforms(frame) for frame in frames_resized], dim=0)
299
+
300
+ # Permute [F,C,H,W] -> [C,F,H,W] for VAE compatibility
301
+ # After DataLoader batching, this becomes [B,C,F,H,W] which VAE expects
302
+ video = video.permute(1, 0, 2, 3).contiguous()
303
+
304
+ return video, fps
305
+
306
+ def _get_resolution_bucket_for_item(self, media_tensor: torch.Tensor) -> tuple[int, int, int]:
307
+ """Get the nearest resolution bucket for the given media tensor."""
308
+ num_frames, _, height, width = media_tensor.shape
309
+
310
+ def distance(bucket: tuple[int, int, int]) -> tuple:
311
+ bucket_num_frames, bucket_height, bucket_width = bucket
312
+ # Lexicographic key:
313
+ # 1) minimize aspect-ratio diff (in log-scale, for invariance to shorter/longer ARs)
314
+ # 2) prefer buckets with more frames (by using negative)
315
+ # 3) prefer buckets with larger spatial area (by using negative)
316
+ return (
317
+ abs(math.log(width / height) - math.log(bucket_width / bucket_height)),
318
+ -bucket_num_frames,
319
+ -(bucket_height * bucket_width),
320
+ )
321
+
322
+ # Keep only buckets with <= available frames
323
+ relevant_buckets = [b for b in self.resolution_buckets if b[0] <= num_frames]
324
+ if not relevant_buckets:
325
+ raise ValueError(f"No resolution buckets have <= {num_frames} frames. Available: {self.resolution_buckets}")
326
+
327
+ # Find the bucket with the minimal distance (according to the function above) to the media item's shape.
328
+ nearest_bucket = min(relevant_buckets, key=distance)
329
+
330
+ return nearest_bucket
331
+
332
+ def _resize_and_crop(self, media_tensor: torch.Tensor, target_height: int, target_width: int) -> torch.Tensor:
333
+ """Resize and crop tensor to target size."""
334
+ # Get current dimensions
335
+ current_height, current_width = media_tensor.shape[2], media_tensor.shape[3]
336
+
337
+ # Calculate aspect ratios to determine which dimension to resize first
338
+ current_aspect = current_width / current_height
339
+ target_aspect = target_width / target_height
340
+
341
+ # Resize while maintaining aspect ratio - scale to make the smaller dimension fit
342
+ if current_aspect > target_aspect:
343
+ # Current is wider than target, so scale by height
344
+ new_width = int(current_width * target_height / current_height)
345
+ media_tensor = resize(
346
+ media_tensor,
347
+ size=[target_height, new_width], # type: ignore
348
+ interpolation=InterpolationMode.BICUBIC,
349
+ )
350
+ else:
351
+ # Current is taller than target, so scale by width
352
+ new_height = int(current_height * target_width / current_width)
353
+ media_tensor = resize(
354
+ media_tensor,
355
+ size=[new_height, target_width],
356
+ interpolation=InterpolationMode.BICUBIC,
357
+ )
358
+
359
+ # Update dimensions after resize
360
+ current_height, current_width = media_tensor.shape[2], media_tensor.shape[3]
361
+ media_tensor = media_tensor.squeeze(0)
362
+
363
+ # Calculate how much we need to crop from each dimension
364
+ delta_h = current_height - target_height
365
+ delta_w = current_width - target_width
366
+
367
+ # Determine crop position based on reshape mode
368
+ if self.reshape_mode == "random":
369
+ # Random crop position
370
+ top = np.random.randint(0, delta_h + 1)
371
+ left = np.random.randint(0, delta_w + 1)
372
+ elif self.reshape_mode == "center":
373
+ # Center crop
374
+ top, left = delta_h // 2, delta_w // 2
375
+ else:
376
+ raise ValueError(f"Unsupported reshape mode: {self.reshape_mode}")
377
+
378
+ # Perform the final crop to exact target dimensions
379
+ media_tensor = crop(media_tensor, top=top, left=left, height=target_height, width=target_width)
380
+ return media_tensor
381
+
382
+
383
+ def _compute_temporal_subsample_indices(num_frames: int, factor: int) -> list[int]:
384
+ """Compute VAE-aligned temporal subsample indices.
385
+ Keeps frame 0 (the VAE's standalone first-frame latent), then takes every
386
+ ``factor``-th frame from frame 1 onwards. This ensures each resulting
387
+ 8-frame VAE group spans ``factor`` groups of the original video.
388
+ """
389
+ if factor == 1:
390
+ return list(range(num_frames))
391
+ return [0, *list(range(1, num_frames, factor))]
392
+
393
+
394
+ def compute_latents( # noqa: PLR0912, PLR0913, PLR0915
395
+ dataset_file: str | Path,
396
+ video_column: str,
397
+ resolution_buckets: list[tuple[int, int, int]],
398
+ output_dir: str,
399
+ model_path: str,
400
+ main_media_column: str | None = None,
401
+ reshape_mode: str = "center",
402
+ batch_size: int = 1,
403
+ device: str = "cuda",
404
+ vae_tiling: bool = False,
405
+ with_audio: bool = False,
406
+ audio_output_dir: str | None = None,
407
+ num_dataloader_workers: int = 4,
408
+ overwrite: bool = False,
409
+ temporal_subsample_factor: int = 1,
410
+ ) -> None:
411
+ """
412
+ Process videos and save latent representations.
413
+ Under ``accelerate launch``, each process handles an interleaved shard of
414
+ the dataset (rank/world read from ``accelerate.PartialState``). Already-
415
+ computed ``.pt`` outputs are skipped unless ``overwrite=True``; writes are
416
+ atomic so an interrupted run is safe to resume.
417
+ Args:
418
+ dataset_file: Path to metadata file (CSV/JSON/JSONL) containing video paths
419
+ video_column: Column name for video paths in the metadata file
420
+ resolution_buckets: List of (frames, height, width) tuples
421
+ output_dir: Directory to save video latents
422
+ model_path: Path to LTX-2 checkpoint (.safetensors)
423
+ reshape_mode: How to crop videos ("center", "random")
424
+ main_media_column: Column name for main media paths (if different from video_column)
425
+ batch_size: Batch size for processing
426
+ device: Device to use for computation
427
+ vae_tiling: Whether to enable VAE tiling
428
+ with_audio: Whether to extract and encode audio from videos
429
+ audio_output_dir: Directory to save audio latents (required if with_audio=True)
430
+ num_dataloader_workers: Number of DataLoader worker processes (0 for in-process loading)
431
+ overwrite: Re-process every item even if its output exists. Use when rerunning with
432
+ changed parameters (different model, resolution, etc.) so stale outputs are replaced.
433
+ temporal_subsample_factor: Factor for VAE-aligned temporal subsampling of reference videos
434
+ """
435
+ # Validate temporal subsampling compatibility with resolution buckets
436
+ if temporal_subsample_factor > 1:
437
+ for frames, _h, _w in resolution_buckets:
438
+ pixel_frames_minus_one = frames - 1
439
+ if pixel_frames_minus_one % temporal_subsample_factor != 0:
440
+ raise ValueError(
441
+ f"Frame count {frames} is not compatible with "
442
+ f"temporal_subsample_factor={temporal_subsample_factor}. "
443
+ f"(frames - 1) must be divisible by the factor."
444
+ )
445
+ subsampled = 1 + pixel_frames_minus_one // temporal_subsample_factor
446
+ if (subsampled - 1) % VAE_TEMPORAL_FACTOR != 0:
447
+ raise ValueError(
448
+ f"After temporal subsampling {frames} → {subsampled} frames, "
449
+ f"result does not satisfy (frames - 1) % {VAE_TEMPORAL_FACTOR} == 0."
450
+ )
451
+
452
+ if with_audio and audio_output_dir is None:
453
+ raise ValueError("audio_output_dir must be provided when with_audio=True")
454
+
455
+ console = Console()
456
+ torch_device = torch.device(device)
457
+
458
+ dataset = MediaDataset(
459
+ dataset_file=dataset_file,
460
+ main_media_column=main_media_column or video_column,
461
+ video_column=video_column,
462
+ resolution_buckets=resolution_buckets,
463
+ reshape_mode=reshape_mode,
464
+ with_audio=with_audio,
465
+ temporal_subsample_factor=temporal_subsample_factor,
466
+ )
467
+ logger.info(f"Loaded {len(dataset)} valid media files")
468
+
469
+ output_path = Path(output_dir)
470
+ output_path.mkdir(parents=True, exist_ok=True)
471
+
472
+ audio_output_path: Path | None = None
473
+ if with_audio:
474
+ audio_output_path = Path(audio_output_dir)
475
+ audio_output_path.mkdir(parents=True, exist_ok=True)
476
+
477
+ # Audio processing requires batch_size=1; must be applied before the dataloader is built.
478
+ if with_audio and batch_size > 1:
479
+ logger.warning("Audio processing requires batch_size=1. Overriding batch_size to 1.")
480
+ batch_size = 1
481
+
482
+ data_root = Path(dataset_file).parent
483
+
484
+ def _is_done(idx: int) -> bool:
485
+ rel = _output_relative(dataset.main_media_paths[idx], data_root).with_suffix(".pt")
486
+ if not (output_path / rel).is_file():
487
+ return False
488
+ return audio_output_path is None or (audio_output_path / rel).is_file()
489
+
490
+ dataloader = _build_sharded_dataloader(
491
+ dataset,
492
+ batch_size=batch_size,
493
+ num_workers=num_dataloader_workers,
494
+ is_done=_is_done,
495
+ overwrite=overwrite,
496
+ )
497
+ if dataloader is None:
498
+ return
499
+
500
+ with console.status(f"[bold]Loading video VAE encoder from [cyan]{model_path}[/]...", spinner="dots"):
501
+ vae = load_video_vae_encoder(model_path, device=torch_device, dtype=torch.bfloat16)
502
+
503
+ audio_vae_encoder = None
504
+ audio_processor = None
505
+ if with_audio:
506
+ with console.status(f"[bold]Loading audio VAE encoder from [cyan]{model_path}[/]...", spinner="dots"):
507
+ audio_vae_encoder = load_audio_vae_encoder(
508
+ checkpoint_path=model_path,
509
+ device=torch_device,
510
+ dtype=torch.float32, # Audio VAE needs float32 for quality. TODO: re-test with bfloat16.
511
+ )
512
+ audio_processor = AudioProcessor(
513
+ target_sample_rate=audio_vae_encoder.sample_rate,
514
+ mel_bins=audio_vae_encoder.mel_bins,
515
+ mel_hop_length=audio_vae_encoder.mel_hop_length,
516
+ n_fft=audio_vae_encoder.n_fft,
517
+ ).to(torch_device)
518
+
519
+ # Track audio statistics
520
+ audio_success_count = 0
521
+ audio_skip_count = 0
522
+
523
+ # Process batches
524
+ with Progress(
525
+ SpinnerColumn(),
526
+ TextColumn("[progress.description]{task.description}"),
527
+ BarColumn(),
528
+ TaskProgressColumn(),
529
+ MofNCompleteColumn(),
530
+ TimeElapsedColumn(),
531
+ TimeRemainingColumn(),
532
+ console=console,
533
+ ) as progress:
534
+ task = progress.add_task("Processing videos", total=len(dataloader))
535
+
536
+ for batch in dataloader:
537
+ # Get video tensor - shape is [B, F, C, H, W] from DataLoader
538
+ video = batch["video"]
539
+
540
+ # Encode video
541
+ with torch.inference_mode():
542
+ video_latent_data = _encode_video(vae=vae, video=video, use_tiling=vae_tiling)
543
+
544
+ # Save latents for each item in batch
545
+ for i in range(len(batch["relative_path"])):
546
+ output_rel_path = Path(batch["main_media_relative_path"][i]).with_suffix(".pt")
547
+ output_file = output_path / output_rel_path
548
+
549
+ # Create output directory maintaining structure
550
+ output_file.parent.mkdir(parents=True, exist_ok=True)
551
+
552
+ # Store the latent's effective fps (= source_fps / subsample factor).
553
+ # Downstream position math expects the rate the saved latents actually have.
554
+ effective_fps = batch["video_metadata"]["fps"][i].item() / temporal_subsample_factor
555
+ latent_data = {
556
+ "latents": video_latent_data["latents"][i].cpu().contiguous(), # [C, F', H', W']
557
+ "num_frames": video_latent_data["num_frames"],
558
+ "height": video_latent_data["height"],
559
+ "width": video_latent_data["width"],
560
+ "fps": effective_fps,
561
+ }
562
+
563
+ _atomic_save(latent_data, output_file)
564
+
565
+ # Process audio if enabled (audio is already extracted by the dataset)
566
+ if with_audio:
567
+ audio_batch = batch.get("audio")
568
+ if audio_batch is not None:
569
+ # Extract the i-th item from batched audio data
570
+ # DataLoader collates [channels, samples] -> [batch, channels, samples]
571
+ audio_data = Audio(
572
+ waveform=audio_batch["waveform"][i],
573
+ sampling_rate=audio_batch["sample_rate"][i].item(),
574
+ )
575
+
576
+ # Encode audio
577
+ with torch.inference_mode():
578
+ audio_latents = _encode_audio(audio_vae_encoder, audio_processor, audio_data)
579
+
580
+ # Save audio latents
581
+ audio_output_file = audio_output_path / output_rel_path
582
+ audio_output_file.parent.mkdir(parents=True, exist_ok=True)
583
+
584
+ audio_save_data = {
585
+ "latents": audio_latents["latents"].cpu().contiguous(),
586
+ "num_time_steps": audio_latents["num_time_steps"],
587
+ "frequency_bins": audio_latents["frequency_bins"],
588
+ "duration": audio_latents["duration"],
589
+ }
590
+
591
+ _atomic_save(audio_save_data, audio_output_file)
592
+ audio_success_count += 1
593
+ else:
594
+ # Video has no audio track
595
+ audio_skip_count += 1
596
+
597
+ progress.advance(task)
598
+
599
+ logger.info(f"Processed {len(dataloader.dataset)} videos -> {output_path}") # type: ignore[arg-type]
600
+ if with_audio:
601
+ logger.info(
602
+ f"Audio processing: {audio_success_count} videos with audio, "
603
+ f"{audio_skip_count} videos without audio (skipped)"
604
+ )
605
+
606
+
607
+ def _encode_video(
608
+ vae: torch.nn.Module,
609
+ video: torch.Tensor,
610
+ dtype: torch.dtype | None = None,
611
+ use_tiling: bool = False,
612
+ tile_size: int = DEFAULT_TILE_SIZE,
613
+ tile_overlap: int = DEFAULT_TILE_OVERLAP,
614
+ ) -> dict[str, torch.Tensor | int]:
615
+ """Encode video into non-patchified latent representation.
616
+ Args:
617
+ vae: Video VAE encoder model
618
+ video: Input tensor of shape [B, C, F, H, W] (batch, channels, frames, height, width)
619
+ This is the format expected by the VAE encoder.
620
+ dtype: Target dtype for output latents
621
+ use_tiling: Whether to use spatial tiling for memory efficiency
622
+ tile_size: Tile size in pixels (must be divisible by 32)
623
+ tile_overlap: Overlap between tiles in pixels (must be divisible by 32)
624
+ Returns:
625
+ Dict containing non-patchified latents and shape information:
626
+ {
627
+ "latents": Tensor[B, C, F', H', W'], # Non-patchified format with batch dim
628
+ "num_frames": int, # Latent frame count
629
+ "height": int, # Latent height
630
+ "width": int, # Latent width
631
+ }
632
+ """
633
+ device = next(vae.parameters()).device
634
+ vae_dtype = next(vae.parameters()).dtype
635
+
636
+ # Add batch dimension if needed
637
+ if video.ndim == 4:
638
+ video = video.unsqueeze(0) # [C, F, H, W] -> [B, C, F, H, W]
639
+
640
+ video = video.to(device=device, dtype=vae_dtype)
641
+
642
+ # Choose encoding method based on tiling flag
643
+ if use_tiling:
644
+ latents = _tiled_encode_video(
645
+ vae=vae,
646
+ video=video,
647
+ tile_size=tile_size,
648
+ tile_overlap=tile_overlap,
649
+ )
650
+ else:
651
+ # Encode video - VAE expects [B, C, F, H, W], returns [B, C, F', H', W']
652
+ latents = vae(video)
653
+
654
+ if dtype is not None:
655
+ latents = latents.to(dtype=dtype)
656
+
657
+ _, _, num_frames, height, width = latents.shape
658
+
659
+ return {
660
+ "latents": latents, # [B, C, F', H', W']
661
+ "num_frames": num_frames,
662
+ "height": height,
663
+ "width": width,
664
+ }
665
+
666
+
667
+ def _tiled_encode_video( # noqa: PLR0912, PLR0915
668
+ vae: torch.nn.Module,
669
+ video: torch.Tensor,
670
+ tile_size: int = DEFAULT_TILE_SIZE,
671
+ tile_overlap: int = DEFAULT_TILE_OVERLAP,
672
+ ) -> torch.Tensor:
673
+ """Encode video using spatial tiling for memory efficiency.
674
+ Splits the video into overlapping spatial tiles, encodes each tile separately,
675
+ and blends the results using linear feathering in the overlap regions.
676
+ Args:
677
+ vae: Video VAE encoder model
678
+ video: Input tensor of shape [B, C, F, H, W]
679
+ tile_size: Tile size in pixels (must be divisible by 32)
680
+ tile_overlap: Overlap between tiles in pixels (must be divisible by 32)
681
+ Returns:
682
+ Encoded latent tensor [B, C_latent, F_latent, H_latent, W_latent]
683
+ """
684
+ batch, _channels, frames, height, width = video.shape
685
+ device = video.device
686
+ dtype = video.dtype
687
+
688
+ # Validate tile parameters
689
+ if tile_size % VAE_SPATIAL_FACTOR != 0:
690
+ raise ValueError(f"tile_size must be divisible by {VAE_SPATIAL_FACTOR}, got {tile_size}")
691
+ if tile_overlap % VAE_SPATIAL_FACTOR != 0:
692
+ raise ValueError(f"tile_overlap must be divisible by {VAE_SPATIAL_FACTOR}, got {tile_overlap}")
693
+ if tile_overlap >= tile_size:
694
+ raise ValueError(f"tile_overlap ({tile_overlap}) must be less than tile_size ({tile_size})")
695
+
696
+ # If video fits in a single tile, use regular encoding
697
+ if height <= tile_size and width <= tile_size:
698
+ return vae(video)
699
+
700
+ # Calculate output dimensions
701
+ # VAE compresses: H -> H/32, W -> W/32, F -> 1 + (F-1)/8
702
+ output_height = height // VAE_SPATIAL_FACTOR
703
+ output_width = width // VAE_SPATIAL_FACTOR
704
+ output_frames = 1 + (frames - 1) // VAE_TEMPORAL_FACTOR
705
+
706
+ # Latent channels (128 for LTX-2)
707
+ # Get from a small test encode or assume 128
708
+ latent_channels = 128
709
+
710
+ # Initialize output and weight tensors
711
+ output = torch.zeros(
712
+ (batch, latent_channels, output_frames, output_height, output_width),
713
+ device=device,
714
+ dtype=dtype,
715
+ )
716
+ weights = torch.zeros(
717
+ (batch, 1, output_frames, output_height, output_width),
718
+ device=device,
719
+ dtype=dtype,
720
+ )
721
+
722
+ # Calculate tile positions with overlap
723
+ # Step size is tile_size - tile_overlap
724
+ step_h = tile_size - tile_overlap
725
+ step_w = tile_size - tile_overlap
726
+
727
+ h_positions = list(range(0, max(1, height - tile_overlap), step_h))
728
+ w_positions = list(range(0, max(1, width - tile_overlap), step_w))
729
+
730
+ # Ensure last tile covers the edge
731
+ if h_positions[-1] + tile_size < height:
732
+ h_positions.append(height - tile_size)
733
+ if w_positions[-1] + tile_size < width:
734
+ w_positions.append(width - tile_size)
735
+
736
+ # Remove duplicates and sort
737
+ h_positions = sorted(set(h_positions))
738
+ w_positions = sorted(set(w_positions))
739
+
740
+ # Overlap in latent space
741
+ overlap_out_h = tile_overlap // VAE_SPATIAL_FACTOR
742
+ overlap_out_w = tile_overlap // VAE_SPATIAL_FACTOR
743
+
744
+ # Process each tile
745
+ for h_pos in h_positions:
746
+ for w_pos in w_positions:
747
+ # Calculate tile boundaries in input space
748
+ h_start = max(0, h_pos)
749
+ w_start = max(0, w_pos)
750
+ h_end = min(h_start + tile_size, height)
751
+ w_end = min(w_start + tile_size, width)
752
+
753
+ # Ensure tile dimensions are divisible by VAE_SPATIAL_FACTOR
754
+ tile_h = ((h_end - h_start) // VAE_SPATIAL_FACTOR) * VAE_SPATIAL_FACTOR
755
+ tile_w = ((w_end - w_start) // VAE_SPATIAL_FACTOR) * VAE_SPATIAL_FACTOR
756
+
757
+ if tile_h < VAE_SPATIAL_FACTOR or tile_w < VAE_SPATIAL_FACTOR:
758
+ continue
759
+
760
+ # Adjust end positions
761
+ h_end = h_start + tile_h
762
+ w_end = w_start + tile_w
763
+
764
+ # Extract tile
765
+ tile = video[:, :, :, h_start:h_end, w_start:w_end]
766
+
767
+ # Encode tile
768
+ encoded_tile = vae(tile)
769
+
770
+ # Get actual encoded dimensions
771
+ _, _, tile_out_frames, tile_out_height, tile_out_width = encoded_tile.shape
772
+
773
+ # Calculate output positions
774
+ out_h_start = h_start // VAE_SPATIAL_FACTOR
775
+ out_w_start = w_start // VAE_SPATIAL_FACTOR
776
+ out_h_end = min(out_h_start + tile_out_height, output_height)
777
+ out_w_end = min(out_w_start + tile_out_width, output_width)
778
+
779
+ # Trim encoded tile if necessary
780
+ actual_tile_h = out_h_end - out_h_start
781
+ actual_tile_w = out_w_end - out_w_start
782
+ encoded_tile = encoded_tile[:, :, :, :actual_tile_h, :actual_tile_w]
783
+
784
+ # Create blending mask with linear feathering at edges
785
+ mask = torch.ones(
786
+ (1, 1, tile_out_frames, actual_tile_h, actual_tile_w),
787
+ device=device,
788
+ dtype=dtype,
789
+ )
790
+
791
+ # Apply feathering at edges (linear blend in overlap regions)
792
+ # Left edge
793
+ if h_pos > 0 and overlap_out_h > 0 and overlap_out_h < actual_tile_h:
794
+ fade_in = torch.linspace(0.0, 1.0, overlap_out_h + 2, device=device, dtype=dtype)[1:-1]
795
+ mask[:, :, :, :overlap_out_h, :] *= fade_in.view(1, 1, 1, -1, 1)
796
+
797
+ # Right edge (bottom in height dimension)
798
+ if h_end < height and overlap_out_h > 0 and overlap_out_h < actual_tile_h:
799
+ fade_out = torch.linspace(1.0, 0.0, overlap_out_h + 2, device=device, dtype=dtype)[1:-1]
800
+ mask[:, :, :, -overlap_out_h:, :] *= fade_out.view(1, 1, 1, -1, 1)
801
+
802
+ # Top edge (left in width dimension)
803
+ if w_pos > 0 and overlap_out_w > 0 and overlap_out_w < actual_tile_w:
804
+ fade_in = torch.linspace(0.0, 1.0, overlap_out_w + 2, device=device, dtype=dtype)[1:-1]
805
+ mask[:, :, :, :, :overlap_out_w] *= fade_in.view(1, 1, 1, 1, -1)
806
+
807
+ # Bottom edge (right in width dimension)
808
+ if w_end < width and overlap_out_w > 0 and overlap_out_w < actual_tile_w:
809
+ fade_out = torch.linspace(1.0, 0.0, overlap_out_w + 2, device=device, dtype=dtype)[1:-1]
810
+ mask[:, :, :, :, -overlap_out_w:] *= fade_out.view(1, 1, 1, 1, -1)
811
+
812
+ # Accumulate weighted results
813
+ output[:, :, :, out_h_start:out_h_end, out_w_start:out_w_end] += encoded_tile * mask
814
+ weights[:, :, :, out_h_start:out_h_end, out_w_start:out_w_end] += mask
815
+
816
+ # Normalize by weights (avoid division by zero)
817
+ output = output / (weights + 1e-8)
818
+
819
+ return output
820
+
821
+
822
+ def _encode_audio(
823
+ audio_vae_encoder: torch.nn.Module,
824
+ audio_processor: torch.nn.Module,
825
+ audio: Audio,
826
+ ) -> dict[str, torch.Tensor | int | float]:
827
+ """Encode audio waveform into latent representation.
828
+ Args:
829
+ audio_vae_encoder: Audio VAE encoder model from ltx-core
830
+ audio_processor: AudioProcessor for waveform-to-spectrogram conversion
831
+ audio: Audio container with waveform tensor and sampling rate.
832
+ Returns:
833
+ Dict containing audio latents and shape information:
834
+ {
835
+ "latents": Tensor[C, T, F], # Non-patchified format
836
+ "num_time_steps": int,
837
+ "frequency_bins": int,
838
+ "duration": float,
839
+ }
840
+ """
841
+ device = next(audio_vae_encoder.parameters()).device
842
+ dtype = next(audio_vae_encoder.parameters()).dtype
843
+
844
+ waveform = audio.waveform.to(device=device, dtype=dtype)
845
+
846
+ # Add batch dimension if needed: [channels, samples] -> [batch, channels, samples]
847
+ if waveform.dim() == 2:
848
+ waveform = waveform.unsqueeze(0)
849
+
850
+ # Convert to stereo if needed (audio VAE expects 2 channels)
851
+ # Channel order for surround: 5.1=[L,R,C,LFE,Ls,Rs], 7.1=[L,R,C,LFE,Ls,Rs,Lb,Rb]
852
+ num_channels = waveform.shape[1]
853
+ if num_channels == 1:
854
+ # Mono to stereo: duplicate the channel
855
+ waveform = waveform.repeat(1, 2, 1)
856
+ elif num_channels == 6:
857
+ # 5.1 downmix with normalized weights (sum to 1.0)
858
+ # Original: L = L + 0.707*C + 0.707*Ls, weights sum = 2.414
859
+ w_main = 1.0 / 2.414 # ~0.414
860
+ w_other = 0.707 / 2.414 # ~0.293
861
+ left = w_main * waveform[:, 0, :] + w_other * waveform[:, 2, :] + w_other * waveform[:, 4, :]
862
+ right = w_main * waveform[:, 1, :] + w_other * waveform[:, 2, :] + w_other * waveform[:, 5, :]
863
+ waveform = torch.stack([left, right], dim=1)
864
+ elif num_channels == 8:
865
+ # 7.1 downmix with normalized weights (sum to 1.0)
866
+ # Original: L = L + 0.707*C + 0.707*Ls + 0.707*Lb, weights sum = 3.121
867
+ w_main = 1.0 / 3.121 # ~0.320
868
+ w_other = 0.707 / 3.121 # ~0.227
869
+ center = waveform[:, 2, :]
870
+ left = w_main * waveform[:, 0, :] + w_other * (center + waveform[:, 4, :] + waveform[:, 6, :])
871
+ right = w_main * waveform[:, 1, :] + w_other * (center + waveform[:, 5, :] + waveform[:, 7, :])
872
+ waveform = torch.stack([left, right], dim=1)
873
+ elif num_channels > 2:
874
+ # Unknown layout: average all channels to mono, then duplicate to stereo
875
+ logger.warning(f"Unknown audio channel layout ({num_channels} channels), using mean downmix")
876
+ mono = waveform.mean(dim=1, keepdim=True)
877
+ waveform = mono.repeat(1, 2, 1)
878
+
879
+ # Calculate duration
880
+ duration = waveform.shape[-1] / audio.sampling_rate
881
+
882
+ # Convert waveform to mel spectrogram using AudioProcessor
883
+ mel_spectrogram = audio_processor.waveform_to_mel(Audio(waveform=waveform, sampling_rate=audio.sampling_rate))
884
+ mel_spectrogram = mel_spectrogram.to(dtype=dtype)
885
+
886
+ # Encode mel spectrogram to latents
887
+ latents = audio_vae_encoder(mel_spectrogram)
888
+
889
+ # latents shape: [batch, channels, time, freq] = [1, 8, T, 16]
890
+ _, _channels, time_steps, freq_bins = latents.shape
891
+
892
+ return {
893
+ "latents": latents.squeeze(0), # [C, T, F] - remove batch dim
894
+ "num_time_steps": time_steps,
895
+ "frequency_bins": freq_bins,
896
+ "duration": duration,
897
+ }
898
+
899
+
900
+ AUDIO_FILE_EXTENSIONS = {".wav", ".mp3", ".flac", ".ogg", ".aac", ".m4a"}
901
+ VIDEO_FILE_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".webm"}
902
+ IMAGE_FILE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".heic", ".heif", ".bmp", ".tiff", ".webp"}
903
+
904
+
905
+ def compute_video_masks(
906
+ dataset_file: str | Path,
907
+ mask_column: str,
908
+ latents_dir: str,
909
+ output_dir: str,
910
+ main_media_column: str | None = None,
911
+ overwrite: bool = False,
912
+ ) -> None:
913
+ """Preprocess video mask files to latent-space binary masks.
914
+ For each sample, loads the mask video/image, applies the same spatial
915
+ resize/crop as the target video (read from saved latent metadata), downsamples
916
+ to latent dimensions, binarizes, and saves as a .pt tensor.
917
+ Args:
918
+ dataset_file: Path to metadata file (CSV/JSON/JSONL).
919
+ mask_column: Column name containing mask video/image paths.
920
+ latents_dir: Directory containing the target video latents (for reading
921
+ spatial/temporal metadata to ensure mask alignment).
922
+ output_dir: Directory to save mask .pt files.
923
+ main_media_column: Column for output file naming (defaults to mask_column).
924
+ """
925
+ dataset_path = Path(dataset_file)
926
+ data_root = dataset_path.parent
927
+ latents_path = Path(latents_dir)
928
+ output_path = Path(output_dir)
929
+ output_path.mkdir(parents=True, exist_ok=True)
930
+
931
+ naming_column = main_media_column or mask_column
932
+ mask_paths = _load_paths_from_dataset(dataset_path, mask_column)
933
+ naming_paths = _load_paths_from_dataset(dataset_path, naming_column) if naming_column != mask_column else mask_paths
934
+
935
+ success = 0
936
+ for mask_file, naming_file in zip(mask_paths, naming_paths, strict=True):
937
+ rel_path = _output_relative(naming_file, data_root)
938
+ latent_file = latents_path / rel_path.with_suffix(".pt")
939
+ out_file = output_path / rel_path.with_suffix(".pt")
940
+
941
+ if not latent_file.exists():
942
+ logger.warning(f"No target latent found at {latent_file}, skipping mask {mask_file}")
943
+ continue
944
+
945
+ if not overwrite and out_file.is_file():
946
+ continue
947
+
948
+ target_meta = torch.load(latent_file, map_location="cpu", weights_only=True)
949
+ latent_f = target_meta["num_frames"]
950
+ latent_h = target_meta["height"]
951
+ latent_w = target_meta["width"]
952
+ pixel_h = latent_h * VAE_SPATIAL_FACTOR
953
+ pixel_w = latent_w * VAE_SPATIAL_FACTOR
954
+ pixel_f = (latent_f - 1) * VAE_TEMPORAL_FACTOR + 1
955
+
956
+ # Load mask as video or image
957
+ if mask_file.suffix.lower() in IMAGE_FILE_EXTENSIONS:
958
+ img = to_tensor(open_image_as_srgb(mask_file)).mean(dim=0, keepdim=True) # [1, H, W]
959
+ img = tv_resize(img.unsqueeze(0), [pixel_h, pixel_w]).squeeze(0) # [1, H, W]
960
+ mask_pixels = img.expand(pixel_f, -1, -1) # tile across frames → [F, H, W]
961
+ else:
962
+ frames, _ = read_video(str(mask_file), max_frames=pixel_f) # [F, C, H, W]
963
+ frames = frames[:pixel_f].mean(dim=1) # grayscale → [F, H, W]
964
+ frames = torch.nn.functional.interpolate(
965
+ frames.unsqueeze(1), size=(pixel_h, pixel_w), mode="nearest"
966
+ ).squeeze(1) # [F, H, W]
967
+ mask_pixels = frames
968
+
969
+ # Downsample to latent dims: [F, H, W] → [F', H', W']
970
+ mask_latent = torch.nn.functional.avg_pool2d(mask_pixels.unsqueeze(1), kernel_size=VAE_SPATIAL_FACTOR).squeeze(
971
+ 1
972
+ ) # [F, H', W'] → spatial done
973
+ # Temporal: max-pool over groups of VAE_TEMPORAL_FACTOR frames (any masked frame masks the group)
974
+ f_spatial = mask_latent.shape[0]
975
+ pad_f = (VAE_TEMPORAL_FACTOR - f_spatial % VAE_TEMPORAL_FACTOR) % VAE_TEMPORAL_FACTOR
976
+ if pad_f > 0:
977
+ mask_latent = torch.nn.functional.pad(mask_latent, (0, 0, 0, 0, 0, pad_f))
978
+ h_prime, w_prime = mask_latent.shape[1], mask_latent.shape[2]
979
+ mask_latent = mask_latent.reshape(-1, VAE_TEMPORAL_FACTOR, h_prime, w_prime).amax(dim=1)[:latent_f]
980
+
981
+ # Binarize
982
+ mask_latent = (mask_latent > 0.5).float()
983
+
984
+ out_file.parent.mkdir(parents=True, exist_ok=True)
985
+ _atomic_save({"mask": mask_latent}, out_file)
986
+ success += 1
987
+
988
+ logger.info(f"Mask preprocessing complete: {success} masks saved to {output_path}")
989
+
990
+
991
+ def compute_audio_masks(
992
+ dataset_file: str | Path,
993
+ mask_column: str,
994
+ audio_latents_dir: str,
995
+ output_dir: str,
996
+ main_media_column: str | None = None,
997
+ overwrite: bool = False,
998
+ ) -> None:
999
+ """Preprocess audio mask files to latent-space binary masks.
1000
+ For each sample, loads the mask (a 1D waveform-like signal or a simple tensor),
1001
+ resamples it to match the target audio latent temporal length, binarizes, and saves.
1002
+ Args:
1003
+ dataset_file: Path to metadata file (CSV/JSON/JSONL).
1004
+ mask_column: Column name containing mask file paths (.wav or .pt).
1005
+ audio_latents_dir: Directory containing the target audio latents (for reading
1006
+ temporal metadata to ensure mask alignment).
1007
+ output_dir: Directory to save mask .pt files.
1008
+ main_media_column: Column for output file naming (defaults to mask_column).
1009
+ """
1010
+ dataset_path = Path(dataset_file)
1011
+ data_root = dataset_path.parent
1012
+ audio_latents_path = Path(audio_latents_dir)
1013
+ output_path = Path(output_dir)
1014
+ output_path.mkdir(parents=True, exist_ok=True)
1015
+
1016
+ naming_column = main_media_column or mask_column
1017
+ mask_paths = _load_paths_from_dataset(dataset_path, mask_column)
1018
+ naming_paths = _load_paths_from_dataset(dataset_path, naming_column) if naming_column != mask_column else mask_paths
1019
+
1020
+ success = 0
1021
+ for mask_file, naming_file in zip(mask_paths, naming_paths, strict=True):
1022
+ rel_path = _output_relative(naming_file, data_root)
1023
+ latent_file = audio_latents_path / rel_path.with_suffix(".pt")
1024
+ out_file = output_path / rel_path.with_suffix(".pt")
1025
+
1026
+ if not latent_file.exists():
1027
+ logger.warning(f"No target audio latent found at {latent_file}, skipping mask {mask_file}")
1028
+ continue
1029
+
1030
+ if not overwrite and out_file.is_file():
1031
+ continue
1032
+
1033
+ target_meta = torch.load(latent_file, map_location="cpu", weights_only=True)
1034
+ latent_t = target_meta["num_time_steps"]
1035
+
1036
+ # Load mask: .pt file (raw tensor) or .wav (use amplitude envelope)
1037
+ if mask_file.suffix == ".pt":
1038
+ raw_mask = torch.load(mask_file, map_location="cpu", weights_only=True)
1039
+ if isinstance(raw_mask, dict):
1040
+ raw_mask = raw_mask.get("mask", next(iter(raw_mask.values())))
1041
+ raw_mask = raw_mask.float().flatten()
1042
+ else:
1043
+ audio = _load_audio_from_file(mask_file)
1044
+ if audio is None:
1045
+ logger.warning(f"Could not load audio mask from {mask_file}")
1046
+ continue
1047
+ raw_mask = audio.waveform.abs().mean(dim=0) # mono amplitude envelope
1048
+
1049
+ # Resample to target audio latent length
1050
+ mask_resampled = torch.nn.functional.interpolate(
1051
+ raw_mask.unsqueeze(0).unsqueeze(0), size=latent_t, mode="nearest"
1052
+ ).squeeze() # [latent_t]
1053
+
1054
+ mask_binary = (mask_resampled > 0.5).float()
1055
+
1056
+ out_file.parent.mkdir(parents=True, exist_ok=True)
1057
+ _atomic_save({"mask": mask_binary}, out_file)
1058
+ success += 1
1059
+
1060
+ logger.info(f"Audio mask preprocessing complete: {success} masks saved to {output_path}")
1061
+
1062
+
1063
+ def compute_audio_latents( # noqa: PLR0915
1064
+ dataset_file: str | Path,
1065
+ audio_column: str,
1066
+ output_dir: str,
1067
+ model_path: str,
1068
+ main_media_column: str | None = None,
1069
+ max_duration: float | None = None,
1070
+ duration_buckets: list[float] | None = None,
1071
+ device: str = "cuda",
1072
+ overwrite: bool = False,
1073
+ ) -> None:
1074
+ """Encode audio files into latent representations.
1075
+ Supports standalone audio files (.wav, .mp3, etc.) and audio tracks
1076
+ extracted from video files (.mp4, etc.).
1077
+ Args:
1078
+ dataset_file: Path to metadata file (CSV/JSON/JSONL).
1079
+ audio_column: Column name containing audio file paths.
1080
+ output_dir: Directory to save audio latents.
1081
+ model_path: Path to LTX-2 checkpoint (.safetensors).
1082
+ main_media_column: Column for output file naming (defaults to audio_column).
1083
+ Ensures alignment with other latent directories.
1084
+ max_duration: Maximum audio duration in seconds. Audio is trimmed if longer.
1085
+ Mutually exclusive with duration_buckets.
1086
+ duration_buckets: List of allowed durations in seconds (e.g. [2.0, 4.0, 8.0]).
1087
+ Each audio file is matched to the largest bucket that fits its duration,
1088
+ then trimmed to exactly that length. Files shorter than the smallest
1089
+ bucket are skipped. Ensures uniform lengths for batched training.
1090
+ device: Device to use for computation.
1091
+ """
1092
+ console = Console()
1093
+ torch_device = torch.device(device)
1094
+
1095
+ dataset_path = Path(dataset_file)
1096
+ data_root = dataset_path.parent
1097
+ output_path = Path(output_dir)
1098
+ output_path.mkdir(parents=True, exist_ok=True)
1099
+
1100
+ naming_column = main_media_column or audio_column
1101
+ audio_paths = _load_paths_from_dataset(dataset_path, audio_column)
1102
+ naming_paths = (
1103
+ _load_paths_from_dataset(dataset_path, naming_column) if naming_column != audio_column else audio_paths
1104
+ )
1105
+
1106
+ with console.status(f"[bold]Loading audio VAE encoder from [cyan]{model_path}[/]...", spinner="dots"):
1107
+ audio_vae_encoder = load_audio_vae_encoder(
1108
+ checkpoint_path=model_path,
1109
+ device=torch_device,
1110
+ dtype=torch.float32,
1111
+ )
1112
+ audio_processor = AudioProcessor(
1113
+ target_sample_rate=audio_vae_encoder.sample_rate,
1114
+ mel_bins=audio_vae_encoder.mel_bins,
1115
+ mel_hop_length=audio_vae_encoder.mel_hop_length,
1116
+ n_fft=audio_vae_encoder.n_fft,
1117
+ ).to(torch_device)
1118
+
1119
+ sorted_buckets = sorted(duration_buckets, reverse=True) if duration_buckets else None
1120
+ success_count = 0
1121
+ skip_count = 0
1122
+
1123
+ with Progress(
1124
+ SpinnerColumn(),
1125
+ TextColumn("[progress.description]{task.description}"),
1126
+ BarColumn(),
1127
+ TaskProgressColumn(),
1128
+ MofNCompleteColumn(),
1129
+ TimeElapsedColumn(),
1130
+ TimeRemainingColumn(),
1131
+ console=console,
1132
+ ) as progress:
1133
+ task = progress.add_task("Encoding audio", total=len(audio_paths))
1134
+
1135
+ for audio_path, naming_path in zip(audio_paths, naming_paths, strict=True):
1136
+ rel_path = _output_relative(naming_path, data_root)
1137
+ output_file = output_path / rel_path.with_suffix(".pt")
1138
+ output_file.parent.mkdir(parents=True, exist_ok=True)
1139
+
1140
+ if not overwrite and output_file.is_file():
1141
+ success_count += 1
1142
+ progress.advance(task)
1143
+ continue
1144
+
1145
+ # Load audio (no trimming yet — need full duration for bucket matching)
1146
+ audio = _load_audio_from_file(audio_path)
1147
+ if audio is None:
1148
+ skip_count += 1
1149
+ progress.advance(task)
1150
+ continue
1151
+
1152
+ file_duration = audio.waveform.shape[-1] / audio.sampling_rate
1153
+
1154
+ # Determine target duration: bucket matching, max_duration cap, or full file
1155
+ target_duration = file_duration
1156
+ if sorted_buckets:
1157
+ bucket = next((b for b in sorted_buckets if b <= file_duration), None)
1158
+ if bucket is None:
1159
+ logger.warning(
1160
+ f"Skipping {audio_path.name} ({file_duration:.1f}s) — shorter than "
1161
+ f"smallest bucket ({sorted_buckets[-1]:.1f}s)"
1162
+ )
1163
+ skip_count += 1
1164
+ progress.advance(task)
1165
+ continue
1166
+ target_duration = bucket
1167
+ elif max_duration is not None:
1168
+ target_duration = min(file_duration, max_duration)
1169
+
1170
+ # Trim to target duration
1171
+ target_samples = int(target_duration * audio.sampling_rate)
1172
+ trimmed_waveform = audio.waveform[:, :target_samples]
1173
+ audio = Audio(waveform=trimmed_waveform, sampling_rate=audio.sampling_rate)
1174
+
1175
+ with torch.inference_mode():
1176
+ audio_latents = _encode_audio(audio_vae_encoder, audio_processor, audio)
1177
+
1178
+ _atomic_save(
1179
+ {
1180
+ "latents": audio_latents["latents"].cpu().contiguous(),
1181
+ "num_time_steps": audio_latents["num_time_steps"],
1182
+ "frequency_bins": audio_latents["frequency_bins"],
1183
+ "duration": audio_latents["duration"],
1184
+ },
1185
+ output_file,
1186
+ )
1187
+ success_count += 1
1188
+ progress.advance(task)
1189
+
1190
+ logger.info(f"Audio encoding complete: {success_count} encoded, {skip_count} skipped. Saved to {output_path}")
1191
+
1192
+
1193
+ def _output_relative(path: Path, data_root: Path) -> Path:
1194
+ """Relative path used to name a sample's cached output, mirroring the input layout.
1195
+ Normally media lives under the dataset directory and this is just the path relative to it.
1196
+ If a media path is absolute or otherwise outside the dataset directory (e.g. a one-off
1197
+ metadata file that references media elsewhere), mirror its absolute structure under the
1198
+ output directory instead of raising, so out-of-tree media stays collision-free.
1199
+ """
1200
+ try:
1201
+ return path.relative_to(data_root)
1202
+ except ValueError:
1203
+ return Path(*path.parts[1:]) if path.is_absolute() else path
1204
+
1205
+
1206
+ def _load_paths_from_dataset(dataset_file: Path, column: str) -> list[Path]:
1207
+ """Load file paths from a dataset column, resolving relative to the dataset file's directory."""
1208
+ data_root = dataset_file.parent
1209
+
1210
+ if dataset_file.suffix == ".csv":
1211
+ df = pd.read_csv(dataset_file)
1212
+ if column not in df.columns:
1213
+ raise ValueError(f"Column '{column}' not found in CSV file")
1214
+ return [data_root / Path(str(v).strip()) for v in df[column].tolist()]
1215
+
1216
+ if dataset_file.suffix == ".json":
1217
+ with open(dataset_file, encoding="utf-8") as f:
1218
+ data = json.load(f)
1219
+ if not isinstance(data, list):
1220
+ raise ValueError("JSON file must contain a list of objects")
1221
+ return [data_root / Path(entry[column].strip()) for entry in data]
1222
+
1223
+ if dataset_file.suffix == ".jsonl":
1224
+ paths = []
1225
+ with open(dataset_file, encoding="utf-8") as f:
1226
+ for line in f:
1227
+ entry = json.loads(line)
1228
+ paths.append(data_root / Path(entry[column].strip()))
1229
+ return paths
1230
+
1231
+ raise ValueError(f"Unsupported dataset format: {dataset_file.suffix}")
1232
+
1233
+
1234
+ def _load_audio_from_file(audio_path: Path, max_duration: float | None = None) -> Audio | None:
1235
+ """Load audio from an audio or video file, optionally trimming to max_duration."""
1236
+ try:
1237
+ waveform, sample_rate = torchaudio.load(str(audio_path))
1238
+ except Exception:
1239
+ logger.debug(f"Could not load audio from {audio_path}")
1240
+ return None
1241
+
1242
+ if max_duration is not None:
1243
+ max_samples = int(max_duration * sample_rate)
1244
+ if waveform.shape[-1] > max_samples:
1245
+ waveform = waveform[:, :max_samples]
1246
+
1247
+ return Audio(waveform=waveform, sampling_rate=sample_rate)
1248
+
1249
+
1250
+ def detect_dataset_columns(dataset_file: str | Path) -> set[str]:
1251
+ """Read column names from a dataset file without loading all data."""
1252
+ path = Path(dataset_file)
1253
+ if path.suffix == ".csv":
1254
+ df = pd.read_csv(path, nrows=0)
1255
+ return set(df.columns)
1256
+ if path.suffix == ".json":
1257
+ with open(path, encoding="utf-8") as f:
1258
+ data = json.load(f)
1259
+ return set(data[0].keys()) if isinstance(data, list) and data else set()
1260
+ if path.suffix == ".jsonl":
1261
+ with open(path, encoding="utf-8") as f:
1262
+ return set(json.loads(f.readline()).keys())
1263
+ return set()
1264
+
1265
+
1266
+ def parse_resolution_buckets(resolution_buckets_str: str) -> list[tuple[int, int, int]]:
1267
+ """Parse resolution buckets from string format to list of tuples (frames, height, width)"""
1268
+ resolution_buckets = []
1269
+ for bucket_str in resolution_buckets_str.split(";"):
1270
+ w, h, f = map(int, bucket_str.split("x"))
1271
+
1272
+ if w % VAE_SPATIAL_FACTOR != 0 or h % VAE_SPATIAL_FACTOR != 0:
1273
+ raise typer.BadParameter(
1274
+ f"Width and height must be multiples of {VAE_SPATIAL_FACTOR}, got {w}x{h}",
1275
+ param_hint="resolution-buckets",
1276
+ )
1277
+
1278
+ if f % VAE_TEMPORAL_FACTOR != 1:
1279
+ raise typer.BadParameter(
1280
+ f"Number of frames must be a multiple of {VAE_TEMPORAL_FACTOR} plus 1, got {f}",
1281
+ param_hint="resolution-buckets",
1282
+ )
1283
+
1284
+ resolution_buckets.append((f, h, w))
1285
+ return resolution_buckets
1286
+
1287
+
1288
+ def compute_scaled_resolution_buckets(
1289
+ resolution_buckets: list[tuple[int, int, int]],
1290
+ scale_factor: int,
1291
+ ) -> list[tuple[int, int, int]]:
1292
+ """Compute scaled resolution buckets and validate the results."""
1293
+ if scale_factor == 1:
1294
+ return resolution_buckets
1295
+
1296
+ scaled_buckets = []
1297
+ for frames, height, width in resolution_buckets:
1298
+ # Validate that scale factor evenly divides the dimensions
1299
+ if height % scale_factor != 0:
1300
+ raise ValueError(
1301
+ f"Height {height} is not evenly divisible by scale factor {scale_factor}. "
1302
+ f"Choose a scale factor that divides {height} evenly."
1303
+ )
1304
+ if width % scale_factor != 0:
1305
+ raise ValueError(
1306
+ f"Width {width} is not evenly divisible by scale factor {scale_factor}. "
1307
+ f"Choose a scale factor that divides {width} evenly."
1308
+ )
1309
+
1310
+ scaled_height = height // scale_factor
1311
+ scaled_width = width // scale_factor
1312
+
1313
+ # Validate scaled dimensions are divisible by VAE spatial factor
1314
+ if scaled_height % VAE_SPATIAL_FACTOR != 0:
1315
+ raise ValueError(
1316
+ f"Scaled height {scaled_height} (from {height} / {scale_factor}) "
1317
+ f"is not divisible by {VAE_SPATIAL_FACTOR}. "
1318
+ f"Choose a different scale factor or adjust your resolution buckets."
1319
+ )
1320
+ if scaled_width % VAE_SPATIAL_FACTOR != 0:
1321
+ raise ValueError(
1322
+ f"Scaled width {scaled_width} (from {width} / {scale_factor}) "
1323
+ f"is not divisible by {VAE_SPATIAL_FACTOR}. "
1324
+ f"Choose a different scale factor or adjust your resolution buckets."
1325
+ )
1326
+
1327
+ scaled_buckets.append((frames, scaled_height, scaled_width))
1328
+
1329
+ return scaled_buckets
1330
+
1331
+
1332
+ def _atomic_save(data: Any, out: Path) -> None: # noqa: ANN401
1333
+ """Save to ``out`` atomically via per-PID temp file + replace.
1334
+ Crash mid-write leaves an orphan ``.tmp.<pid>`` file that the skip logic
1335
+ ignores. The per-PID suffix makes concurrent writes from multiple ranks
1336
+ collision-free.
1337
+ """
1338
+ tmp = out.with_suffix(f"{out.suffix}.tmp.{os.getpid()}")
1339
+ torch.save(data, tmp)
1340
+ tmp.replace(out)
1341
+
1342
+
1343
+ def _build_sharded_dataloader(
1344
+ dataset: Dataset,
1345
+ *,
1346
+ batch_size: int,
1347
+ num_workers: int,
1348
+ is_done: Callable[[int], bool],
1349
+ overwrite: bool,
1350
+ ) -> DataLoader | None:
1351
+ """Return a DataLoader over this rank's interleaved shard of ``dataset``.
1352
+ When ``overwrite`` is False, items whose outputs already exist (per
1353
+ ``is_done``) are filtered out. Returns ``None`` if this rank has nothing
1354
+ to do, so the caller can early-return without loading any models.
1355
+ """
1356
+ state = PartialState()
1357
+ todo = [i for i in range(state.process_index, len(dataset), state.num_processes) if overwrite or not is_done(i)]
1358
+ if not todo:
1359
+ logger.info(f"Rank {state.process_index}/{state.num_processes}: nothing to do")
1360
+ return None
1361
+ logger.info(f"Rank {state.process_index}/{state.num_processes}: processing {len(todo):,} of {len(dataset):,} items")
1362
+ return DataLoader(Subset(dataset, todo), batch_size=batch_size, shuffle=False, num_workers=num_workers)
1363
+
1364
+
1365
+ @app.command()
1366
+ def main( # noqa: PLR0913
1367
+ dataset_file: str = typer.Argument(
1368
+ ...,
1369
+ help="Path to metadata file (CSV/JSON/JSONL) containing video paths",
1370
+ ),
1371
+ resolution_buckets: str = typer.Option(
1372
+ ...,
1373
+ help='Resolution buckets in format "WxHxF;WxHxF;..." (e.g. "768x768x25;512x512x49")',
1374
+ ),
1375
+ output_dir: str = typer.Option(
1376
+ ...,
1377
+ help="Output directory to save video latents",
1378
+ ),
1379
+ model_path: str = typer.Option(
1380
+ ...,
1381
+ help="Path to LTX-2 checkpoint (.safetensors file)",
1382
+ ),
1383
+ video_column: str = typer.Option(
1384
+ default="media_path",
1385
+ help="Column name in the dataset JSON/JSONL/CSV file containing video paths",
1386
+ ),
1387
+ batch_size: int = typer.Option(
1388
+ default=1,
1389
+ help="Batch size for processing",
1390
+ ),
1391
+ device: str = typer.Option(
1392
+ default="cuda",
1393
+ help="Device to use for computation",
1394
+ ),
1395
+ vae_tiling: bool = typer.Option(
1396
+ default=False,
1397
+ help="Enable VAE tiling for larger video resolutions",
1398
+ ),
1399
+ reshape_mode: str = typer.Option(
1400
+ default="center",
1401
+ help="How to crop videos: 'center' or 'random'",
1402
+ ),
1403
+ with_audio: bool = typer.Option(
1404
+ default=False,
1405
+ help="Extract and encode audio from video files",
1406
+ ),
1407
+ audio_output_dir: str | None = typer.Option(
1408
+ default=None,
1409
+ help="Output directory for audio latents (required if --with-audio is set)",
1410
+ ),
1411
+ overwrite: bool = typer.Option(
1412
+ default=False,
1413
+ help="Re-encode every item even if its output exists. Use when rerunning with "
1414
+ "changed parameters (different model, resolution, etc.) so stale outputs are replaced.",
1415
+ ),
1416
+ ) -> None:
1417
+ """Process videos/images and save latent representations for video generation training.
1418
+ This script processes videos and images from metadata files and saves latent representations
1419
+ that can be used for training video generation models. The output latents will maintain
1420
+ the same folder structure and naming as the corresponding media files.
1421
+ For multi-GPU preprocessing, invoke under ``accelerate launch`` -- each process
1422
+ will handle an interleaved shard of the dataset.
1423
+ Examples:
1424
+ # Process videos from a CSV file
1425
+ python scripts/process_videos.py dataset.csv --resolution-buckets 768x768x25 \\
1426
+ --output-dir ./latents --model-path /path/to/ltx2.safetensors
1427
+ # Process videos from a JSON file with custom video column
1428
+ python scripts/process_videos.py dataset.json --resolution-buckets 768x768x25 \\
1429
+ --output-dir ./latents --model-path /path/to/ltx2.safetensors --video-column "video_path"
1430
+ # Enable VAE tiling to save GPU VRAM
1431
+ python scripts/process_videos.py dataset.csv --resolution-buckets 1024x1024x25 \\
1432
+ --output-dir ./latents --model-path /path/to/ltx2.safetensors --vae-tiling
1433
+ # Process videos with audio
1434
+ python scripts/process_videos.py dataset.csv --resolution-buckets 768x768x25 \\
1435
+ --output-dir ./latents --model-path /path/to/ltx2.safetensors \\
1436
+ --with-audio --audio-output-dir ./audio_latents
1437
+ """
1438
+
1439
+ # Validate dataset file exists
1440
+ if not Path(dataset_file).is_file():
1441
+ raise typer.BadParameter(f"Dataset file not found: {dataset_file}")
1442
+
1443
+ # Validate audio parameters
1444
+ if with_audio and audio_output_dir is None:
1445
+ raise typer.BadParameter("--audio-output-dir is required when --with-audio is set")
1446
+
1447
+ # Parse resolution buckets
1448
+ parsed_resolution_buckets = parse_resolution_buckets(resolution_buckets)
1449
+
1450
+ if len(parsed_resolution_buckets) > 1:
1451
+ logger.warning(
1452
+ "Using multiple resolution buckets. "
1453
+ "When training with multiple resolution buckets, you must use a batch size of 1."
1454
+ )
1455
+
1456
+ # Process latents
1457
+ compute_latents(
1458
+ dataset_file=dataset_file,
1459
+ video_column=video_column,
1460
+ resolution_buckets=parsed_resolution_buckets,
1461
+ output_dir=output_dir,
1462
+ model_path=model_path,
1463
+ reshape_mode=reshape_mode,
1464
+ batch_size=batch_size,
1465
+ device=device,
1466
+ vae_tiling=vae_tiling,
1467
+ with_audio=with_audio,
1468
+ audio_output_dir=audio_output_dir,
1469
+ overwrite=overwrite,
1470
+ )
1471
+
1472
+
1473
+ if __name__ == "__main__":
1474
+ app()
packages/ltx-trainer/scripts/serve_captioner.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """Launch a vLLM server for Qwen3-Omni captioning.
4
+ Runs the actual server via ``uvx`` so that vLLM and its CUDA-tied
5
+ dependencies live in their own isolated environment (no impact on this
6
+ package's dependency tree).
7
+ The captioning script (``caption_videos.py``) talks to the server over its
8
+ OpenAI-compatible HTTP API. Once the server is up it stays loaded across
9
+ captioning runs; no per-script model warmup cost.
10
+ Typical usage::
11
+ # Default: dynamic FP8 quantization, listen on 127.0.0.1:8001
12
+ uv run python scripts/serve_captioner.py
13
+ # Just print the chosen `uvx vllm serve ...` command without running it
14
+ uv run python scripts/serve_captioner.py --print-cmd
15
+ # Use full bf16 on a GPU with >= 66 GiB free VRAM (slightly more reliable
16
+ # numerics but 2x the weight memory)
17
+ uv run python scripts/serve_captioner.py --quantization bf16
18
+ # Use a different port or expose on all interfaces
19
+ uv run python scripts/serve_captioner.py --port 9000 --host 0.0.0.0
20
+ """
21
+
22
+ import os
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ import typer
29
+ from rich.console import Console
30
+
31
+ console = Console()
32
+
33
+ # Model identifier we serve. The captioner client must use the same string.
34
+ DEFAULT_MODEL = "Qwen/Qwen3-Omni-30B-A3B-Thinking"
35
+
36
+ # Pinned vLLM version known to support Qwen3-Omni on CUDA 12.x.
37
+ # vLLM 0.20+ requires CUDA 13. Update both as the environment evolves.
38
+ # The ``[audio]`` extra is required for Qwen3-Omni to decode audio at all.
39
+ DEFAULT_VLLM_SPEC = "vllm[audio]==0.11.2"
40
+
41
+ # Approximate disk needed for the model download (HF cache structure).
42
+ MODEL_DISK_GIB = 65.0
43
+
44
+
45
+ app = typer.Typer(
46
+ pretty_exceptions_enable=False,
47
+ no_args_is_help=False,
48
+ help="Launch a local vLLM server for Qwen3-Omni captioning.",
49
+ )
50
+
51
+
52
+ def _query_disk_free_gib(path: Path) -> float:
53
+ return shutil.disk_usage(str(path)).free / 1024**3
54
+
55
+
56
+ def _build_vllm_args(
57
+ *,
58
+ model: str,
59
+ host: str,
60
+ port: int,
61
+ quantization: str,
62
+ max_model_len: int,
63
+ gpu_memory_utilization: float,
64
+ extra_args: list[str],
65
+ ) -> list[str]:
66
+ """Construct the `vllm serve ...` argv."""
67
+ args = [
68
+ "vllm",
69
+ "serve",
70
+ model,
71
+ "--host",
72
+ host,
73
+ "--port",
74
+ str(port),
75
+ "--dtype",
76
+ "bfloat16",
77
+ "--max-model-len",
78
+ str(max_model_len),
79
+ "--gpu-memory-utilization",
80
+ str(gpu_memory_utilization),
81
+ # Let the server accept ``file://`` URLs pointing at local videos.
82
+ "--allowed-local-media-path",
83
+ "/",
84
+ # The model is a multimodal MoE; cap each input to one of each
85
+ # modality to match what our captioner sends.
86
+ "--limit-mm-per-prompt",
87
+ '{"image": 1, "video": 1, "audio": 1}',
88
+ # Small concurrent-sequence cap so KV cache headroom isn't fragmented.
89
+ "--max-num-seqs",
90
+ "4",
91
+ ]
92
+ if quantization == "fp8":
93
+ args += ["--quantization", "fp8"]
94
+ args += extra_args
95
+ return args
96
+
97
+
98
+ @app.command()
99
+ def main(
100
+ model: str = typer.Option(DEFAULT_MODEL, "--model", help="Model identifier to serve."),
101
+ host: str = typer.Option("127.0.0.1", "--host", help="Listen address. Use 0.0.0.0 for remote access."),
102
+ port: int = typer.Option(8001, "--port", help="HTTP port."),
103
+ quantization: str = typer.Option(
104
+ "fp8",
105
+ "--quantization",
106
+ "-q",
107
+ help=(
108
+ "Weight precision. 'fp8' (default, dynamic FP8 -- ~31 GiB weights) is "
109
+ "the recommended choice; it fits on 40 GiB GPUs and runs at the same "
110
+ "speed as bf16 on H100. 'bf16' uses ~60 GiB of weights -- pick it if "
111
+ "you have abundant VRAM and want minimal numerical drift."
112
+ ),
113
+ ),
114
+ max_model_len: int = typer.Option(
115
+ 32768,
116
+ "--max-model-len",
117
+ help="Maximum context length the server accepts (must fit input video tokens + max_tokens).",
118
+ ),
119
+ gpu_memory_utilization: float = typer.Option(
120
+ 0.9,
121
+ "--gpu-memory-utilization",
122
+ help="Fraction of GPU memory vLLM may reserve (model + KV cache).",
123
+ ),
124
+ hf_home: Path | None = typer.Option( # noqa: B008
125
+ None,
126
+ "--hf-home",
127
+ help=(
128
+ "Override HF_HOME (where the model is downloaded). The model is ~65 GB; "
129
+ "by default this follows your environment's HF_HOME or HuggingFace's default."
130
+ ),
131
+ ),
132
+ vllm_spec: str = typer.Option(
133
+ DEFAULT_VLLM_SPEC,
134
+ "--vllm-spec",
135
+ help="pip-style spec passed to `uvx --from`. Pin a version that matches your CUDA.",
136
+ ),
137
+ print_cmd: bool = typer.Option(
138
+ False,
139
+ "--print-cmd",
140
+ help="Print the chosen command without running it.",
141
+ ),
142
+ extra_args: list[str] | None = typer.Argument( # noqa: B008
143
+ None,
144
+ help="Additional args passed through to `vllm serve` after `--`.",
145
+ ),
146
+ ) -> None:
147
+ """Launch the vLLM server for Qwen3-Omni."""
148
+ extra = extra_args or []
149
+
150
+ if quantization not in ("bf16", "fp8"):
151
+ console.print(f"[red]--quantization must be 'bf16' or 'fp8'; got {quantization!r}.[/]")
152
+ raise typer.Exit(code=1)
153
+
154
+ # Disk check (only meaningful before first download).
155
+ cache_root = hf_home or Path(os.environ.get("HF_HOME", str(Path.home() / ".cache" / "huggingface")))
156
+ cache_root.mkdir(parents=True, exist_ok=True)
157
+ free_disk = _query_disk_free_gib(cache_root)
158
+ if free_disk < MODEL_DISK_GIB:
159
+ console.print(
160
+ f"[yellow]\u26a0 Only {free_disk:.1f} GiB free on disk under {cache_root} but the "
161
+ f"model needs ~{MODEL_DISK_GIB:.0f} GiB. Either free up space, set --hf-home "
162
+ f"to a larger volume, or expect the download to fail mid-way.[/]"
163
+ )
164
+
165
+ vllm_args = _build_vllm_args(
166
+ model=model,
167
+ host=host,
168
+ port=port,
169
+ quantization=quantization,
170
+ max_model_len=max_model_len,
171
+ gpu_memory_utilization=gpu_memory_utilization,
172
+ extra_args=extra,
173
+ )
174
+
175
+ # Use ``uvx --from vllm==...`` so vLLM lives in its own throwaway venv
176
+ # (or a cached tool venv). The `--` separates uvx args from the command's.
177
+ uvx_cmd = ["uvx", "--from", vllm_spec, *vllm_args]
178
+
179
+ env = os.environ.copy()
180
+ # vLLM 0.11.x requires the V0 engine for Qwen3-Omni's multimodal pipeline.
181
+ env.setdefault("VLLM_USE_V1", "0")
182
+ if hf_home is not None:
183
+ env["HF_HOME"] = str(hf_home)
184
+
185
+ console.print("\n[bold]Command:[/]")
186
+ console.print(" " + " ".join(uvx_cmd))
187
+ if hf_home is not None:
188
+ console.print(f" [dim](with HF_HOME={hf_home})[/]")
189
+
190
+ if print_cmd:
191
+ return
192
+
193
+ console.print("\n[dim]Launching... (first run downloads the model -- ~5 min on a fast link)[/]\n")
194
+ try:
195
+ completed = subprocess.run(uvx_cmd, env=env, check=False)
196
+ except KeyboardInterrupt:
197
+ console.print("\n[yellow]Interrupted.[/]")
198
+ return
199
+ sys.exit(completed.returncode)
200
+
201
+
202
+ if __name__ == "__main__":
203
+ app()
packages/ltx-trainer/scripts/split_scenes.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Split video into scenes using PySceneDetect.
5
+ This script provides a command-line interface for splitting videos into scenes using various detection algorithms.
6
+ It supports multiple detection methods, preview image generation, and customizable parameters for fine-tuning
7
+ the scene detection process.
8
+ Basic usage:
9
+ # Split video using default content-based detection
10
+ scenes_split.py input.mp4 output_dir/
11
+ # Save 3 preview images per scene
12
+ scenes_split.py input.mp4 output_dir/ --save-images 3
13
+ # Process specific duration and filter short scenes
14
+ scenes_split.py input.mp4 output_dir/ --duration 60s --filter-shorter-than 2s
15
+ Advanced usage:
16
+ # Content detection with minimum scene length and frame skip
17
+ scenes_split.py input.mp4 output_dir/ --detector content --min-scene-length 30 --frame-skip 2
18
+ # Use adaptive detection with custom detector and detector parameters
19
+ scenes_split.py input.mp4 output_dir/ --detector adaptive --threshold 3.0 --adaptive-window 10
20
+ """
21
+
22
+ from enum import Enum
23
+ from pathlib import Path
24
+ from typing import List, Optional, Tuple
25
+
26
+ import typer
27
+ from scenedetect import (
28
+ AdaptiveDetector,
29
+ ContentDetector,
30
+ HistogramDetector,
31
+ SceneManager,
32
+ ThresholdDetector,
33
+ open_video,
34
+ )
35
+ from scenedetect.frame_timecode import FrameTimecode
36
+ from scenedetect.scene_manager import SceneDetector, write_scene_list_html
37
+ from scenedetect.scene_manager import save_images as save_scene_images
38
+ from scenedetect.stats_manager import StatsManager
39
+ from scenedetect.video_splitter import split_video_ffmpeg
40
+
41
+ app = typer.Typer(no_args_is_help=True, help="Split video into scenes using PySceneDetect.")
42
+
43
+
44
+ class DetectorType(str, Enum):
45
+ """Available scene detection algorithms."""
46
+
47
+ CONTENT = "content" # Detects fast cuts using HSV color space
48
+ ADAPTIVE = "adaptive" # Detects fast two-phase cuts
49
+ THRESHOLD = "threshold" # Detects fast cuts/slow fades in from and out to a given threshold level
50
+ HISTOGRAM = "histogram" # Detects based on YUV histogram differences in adjacent frames
51
+
52
+
53
+ def create_detector(
54
+ detector_type: DetectorType,
55
+ threshold: Optional[float] = None,
56
+ min_scene_len: Optional[int] = None,
57
+ luma_only: Optional[bool] = None,
58
+ adaptive_window: Optional[int] = None,
59
+ fade_bias: Optional[float] = None,
60
+ ) -> SceneDetector:
61
+ """Create a scene detector based on the specified type and parameters.
62
+ Args:
63
+ detector_type: Type of detector to create
64
+ threshold: Detection threshold (meaning varies by detector)
65
+ min_scene_len: Minimum scene length in frames
66
+ luma_only: If True, only use brightness for content detection
67
+ adaptive_window: Window size for adaptive detection
68
+ fade_bias: Bias for fade in/out detection (-1.0 to 1.0)
69
+ Note: Parameters set to None will use the detector's built-in default values.
70
+ Returns:
71
+ Configured scene detector instance
72
+ """
73
+ # Set common arguments
74
+ kwargs = {}
75
+ if threshold is not None:
76
+ kwargs["threshold"] = threshold
77
+
78
+ if min_scene_len is not None:
79
+ kwargs["min_scene_len"] = min_scene_len
80
+
81
+ match detector_type:
82
+ case DetectorType.CONTENT:
83
+ if luma_only is not None:
84
+ kwargs["luma_only"] = luma_only
85
+ return ContentDetector(**kwargs)
86
+ case DetectorType.ADAPTIVE:
87
+ if adaptive_window is not None:
88
+ kwargs["window_width"] = adaptive_window
89
+ if luma_only is not None:
90
+ kwargs["luma_only"] = luma_only
91
+ if "threshold" in kwargs:
92
+ # Special case for adaptive detector which uses different param name
93
+ kwargs["adaptive_threshold"] = kwargs.pop("threshold")
94
+ return AdaptiveDetector(**kwargs)
95
+ case DetectorType.THRESHOLD:
96
+ if fade_bias is not None:
97
+ kwargs["fade_bias"] = fade_bias
98
+ return ThresholdDetector(**kwargs)
99
+ case DetectorType.HISTOGRAM:
100
+ return HistogramDetector(**kwargs)
101
+ case _:
102
+ raise ValueError(f"Unknown detector type: {detector_type}")
103
+
104
+
105
+ def validate_output_dir(output_dir: str) -> Path:
106
+ """Validate and create output directory if it doesn't exist.
107
+ Args:
108
+ output_dir: Path to the output directory
109
+ Returns:
110
+ Path object of the validated output directory
111
+ """
112
+ path = Path(output_dir)
113
+
114
+ if path.exists() and not path.is_dir():
115
+ raise typer.BadParameter(f"{output_dir} exists but is not a directory")
116
+
117
+ return path
118
+
119
+
120
+ def parse_timecode(video: any, time_str: Optional[str]) -> Optional[FrameTimecode]:
121
+ """Parse a timecode string into a FrameTimecode object.
122
+ Supports formats:
123
+ - Frames: '123'
124
+ - Seconds: '123s' or '123.45s'
125
+ - Timecode: '00:02:03' or '00:02:03.456'
126
+ Args:
127
+ video: Video object to get framerate from
128
+ time_str: String to parse, or None
129
+ Returns:
130
+ FrameTimecode object or None if input is None
131
+ """
132
+ if time_str is None:
133
+ return None
134
+
135
+ try:
136
+ if time_str.endswith("s"):
137
+ # Seconds format
138
+ seconds = float(time_str[:-1])
139
+ return FrameTimecode(timecode=seconds, fps=video.frame_rate)
140
+ elif ":" in time_str:
141
+ # Timecode format
142
+ return FrameTimecode(timecode=time_str, fps=video.frame_rate)
143
+ else:
144
+ # Frame number format
145
+ return FrameTimecode(timecode=int(time_str), fps=video.frame_rate)
146
+ except ValueError as e:
147
+ raise typer.BadParameter(
148
+ f"Invalid timecode format: {time_str}. Use frames (123), "
149
+ f"seconds (123s/123.45s), or timecode (HH:MM:SS[.nnn])",
150
+ ) from e
151
+
152
+
153
+ def detect_and_split_scenes( # noqa: PLR0913
154
+ video_path: str,
155
+ output_dir: Path,
156
+ detector_type: DetectorType,
157
+ threshold: Optional[float] = None,
158
+ min_scene_len: Optional[int] = None,
159
+ max_scenes: Optional[int] = None,
160
+ filter_shorter_than: Optional[str] = None,
161
+ skip_start: Optional[int] = None, # noqa: ARG001
162
+ skip_end: Optional[int] = None, # noqa: ARG001
163
+ save_images_per_scene: int = 0,
164
+ stats_file: Optional[str] = None,
165
+ luma_only: bool = False,
166
+ adaptive_window: Optional[int] = None,
167
+ fade_bias: Optional[float] = None,
168
+ downscale_factor: Optional[int] = None,
169
+ frame_skip: int = 0,
170
+ duration: Optional[str] = None,
171
+ ) -> List[Tuple[FrameTimecode, FrameTimecode]]:
172
+ """Detect and split scenes in a video using the specified parameters.
173
+ Args:
174
+ video_path: Path to input video.
175
+ output_dir: Directory to save output split scenes.
176
+ detector_type: Type of scene detector to use.
177
+ threshold: Detection threshold.
178
+ min_scene_len: Minimum scene length in frames.
179
+ max_scenes: Maximum number of scenes to detect.
180
+ filter_shorter_than: Filter out scenes shorter than this duration (frames/seconds/timecode)
181
+ skip_start: Number of frames to skip at start.
182
+ skip_end: Number of frames to skip at end.
183
+ save_images_per_scene: Number of images to save per scene (0 to disable).
184
+ stats_file: Path to save detection statistics (optional).
185
+ luma_only: Only use brightness for content detection.
186
+ adaptive_window: Window size for adaptive detection.
187
+ fade_bias: Bias for fade detection (-1.0 to 1.0).
188
+ downscale_factor: Factor to downscale frames by during detection.
189
+ frame_skip: Number of frames to skip (i.e. process every 1 in N+1 frames,
190
+ where N is frame_skip, processing only 1/N+1 percent of the video,
191
+ speeding up the detection time at the expense of accuracy).
192
+ frame_skip must be 0 (the default) when using a StatsManager.
193
+ duration: How much of the video to process from start position.
194
+ Can be specified as frames (123), seconds (123s/123.45s),
195
+ or timecode (HH:MM:SS[.nnn]).
196
+ Returns:
197
+ List of detected scenes as (start, end) FrameTimecode pairs.
198
+ """
199
+ # Create video stream
200
+ video = open_video(video_path, backend="opencv")
201
+
202
+ # Parse duration if specified
203
+ duration_tc = parse_timecode(video, duration)
204
+
205
+ # Parse filter_shorter_than if specified
206
+ filter_shorter_than_tc = parse_timecode(video, filter_shorter_than)
207
+
208
+ # Initialize scene manager with optional stats manager
209
+ stats_manager = StatsManager() if stats_file else None
210
+ scene_manager = SceneManager(stats_manager)
211
+
212
+ # Configure scene manager
213
+ if downscale_factor:
214
+ scene_manager.auto_downscale = False
215
+ scene_manager.downscale = downscale_factor
216
+
217
+ # Create and add detector
218
+ detector = create_detector(
219
+ detector_type=detector_type,
220
+ threshold=threshold,
221
+ min_scene_len=min_scene_len,
222
+ luma_only=luma_only,
223
+ adaptive_window=adaptive_window,
224
+ fade_bias=fade_bias,
225
+ )
226
+ scene_manager.add_detector(detector)
227
+
228
+ # Detect scenes
229
+ typer.echo("Detecting scenes...")
230
+ scene_manager.detect_scenes(
231
+ video=video,
232
+ show_progress=True,
233
+ frame_skip=frame_skip,
234
+ duration=duration_tc,
235
+ )
236
+
237
+ # Get scene list
238
+ scenes = scene_manager.get_scene_list()
239
+
240
+ # Filter out scenes that are too short if filter_shorter_than is specified
241
+ if filter_shorter_than_tc:
242
+ original_count = len(scenes)
243
+ scenes = [
244
+ (start, end)
245
+ for start, end in scenes
246
+ if (end.get_frames() - start.get_frames()) >= filter_shorter_than_tc.get_frames()
247
+ ]
248
+ if len(scenes) < original_count:
249
+ typer.echo(
250
+ f"Filtered out {original_count - len(scenes)} scenes shorter "
251
+ f"than {filter_shorter_than_tc.get_seconds():.1f} seconds "
252
+ f"({filter_shorter_than_tc.get_frames()} frames)",
253
+ )
254
+
255
+ # Apply max scenes limit if specified
256
+ if max_scenes and len(scenes) > max_scenes:
257
+ typer.echo(f"Dropping last {len(scenes) - max_scenes} scenes to meet max_scenes ({max_scenes}) limit")
258
+ scenes = scenes[:max_scenes]
259
+
260
+ # Print scene information
261
+ typer.echo(f"Found {len(scenes)} scenes:")
262
+ for i, (start, end) in enumerate(scenes, 1):
263
+ typer.echo(
264
+ f"Scene {i}: {start.get_timecode()} to {end.get_timecode()} "
265
+ f"({end.get_frames() - start.get_frames()} frames)",
266
+ )
267
+
268
+ # Save stats if requested
269
+ if stats_file:
270
+ typer.echo(f"Saving detection stats to {stats_file}")
271
+ stats_manager.save_to_csv(stats_file)
272
+
273
+ # Split video into scenes
274
+ typer.echo("Splitting video into scenes...")
275
+ try:
276
+ split_video_ffmpeg(
277
+ input_video_path=video_path,
278
+ scene_list=scenes,
279
+ output_dir=output_dir,
280
+ show_progress=True,
281
+ )
282
+ typer.echo(f"Scenes have been saved to: {output_dir}")
283
+ except Exception as e:
284
+ raise typer.BadParameter(f"Error splitting video: {e}") from e
285
+
286
+ # Save preview images if requested
287
+ if save_images_per_scene > 0:
288
+ typer.echo(f"Saving {save_images_per_scene} preview images per scene...")
289
+ image_filenames = save_scene_images(
290
+ scene_list=scenes,
291
+ video=video,
292
+ num_images=save_images_per_scene,
293
+ output_dir=str(output_dir),
294
+ show_progress=True,
295
+ )
296
+
297
+ # Generate HTML report with scene information and previews
298
+ html_path = output_dir / "scene_report.html"
299
+ write_scene_list_html(
300
+ output_html_filename=str(html_path),
301
+ scene_list=scenes,
302
+ image_filenames=image_filenames,
303
+ )
304
+ typer.echo(f"Scene report saved to: {html_path}")
305
+
306
+ return scenes
307
+
308
+
309
+ @app.command()
310
+ def main( # noqa: PLR0913
311
+ video_path: Path = typer.Argument( # noqa: B008
312
+ ...,
313
+ help="Path to the input video file",
314
+ exists=True,
315
+ dir_okay=False,
316
+ ),
317
+ output_dir: str = typer.Argument(
318
+ ...,
319
+ help="Directory where split scenes will be saved",
320
+ ),
321
+ detector: DetectorType = typer.Option( # noqa: B008
322
+ DetectorType.CONTENT,
323
+ help="Scene detection algorithm to use",
324
+ ),
325
+ threshold: Optional[float] = typer.Option(
326
+ None,
327
+ help="Detection threshold (meaning varies by detector)",
328
+ ),
329
+ max_scenes: Optional[int] = typer.Option(
330
+ None,
331
+ help="Maximum number of scenes to produce",
332
+ ),
333
+ min_scene_length: Optional[int] = typer.Option(
334
+ None,
335
+ help="Minimum scene length during detection. Forces the detector to make scenes at least this many frames. "
336
+ "This affects scene detection behavior but does not filter out short scenes.",
337
+ ),
338
+ filter_shorter_than: Optional[str] = typer.Option(
339
+ None,
340
+ help="Filter out scenes shorter than this duration. Can be specified as frames (123), "
341
+ "seconds (123s/123.45s), or timecode (HH:MM:SS[.nnn]). These scenes will be detected but not saved.",
342
+ ),
343
+ skip_start: Optional[int] = typer.Option(
344
+ None,
345
+ help="Number of frames to skip at the start of the video",
346
+ ),
347
+ skip_end: Optional[int] = typer.Option(
348
+ None,
349
+ help="Number of frames to skip at the end of the video",
350
+ ),
351
+ duration: Optional[str] = typer.Option(
352
+ None,
353
+ "-d",
354
+ help="How much of the video to process. Can be specified as frames (123), "
355
+ "seconds (123s/123.45s), or timecode (HH:MM:SS[.nnn])",
356
+ ),
357
+ save_images: int = typer.Option(
358
+ 0,
359
+ help="Number of preview images to save per scene (0 to disable)",
360
+ ),
361
+ stats_file: Optional[str] = typer.Option(
362
+ None,
363
+ help="Path to save detection statistics CSV",
364
+ ),
365
+ luma_only: bool = typer.Option(
366
+ False,
367
+ help="Only use brightness for content detection",
368
+ ),
369
+ adaptive_window: Optional[int] = typer.Option(
370
+ None,
371
+ help="Window size for adaptive detection",
372
+ ),
373
+ fade_bias: Optional[float] = typer.Option(
374
+ None,
375
+ help="Bias for fade detection (-1.0 to 1.0)",
376
+ ),
377
+ downscale: Optional[int] = typer.Option(
378
+ None,
379
+ help="Factor to downscale frames by during detection",
380
+ ),
381
+ frame_skip: int = typer.Option(
382
+ 0,
383
+ help="Number of frames to skip during processing",
384
+ ),
385
+ ) -> None:
386
+ """Split video into scenes using PySceneDetect."""
387
+ if skip_start or skip_end:
388
+ typer.echo("Skipping start and end frames is not supported yet.")
389
+ return
390
+
391
+ # Validate output directory
392
+ output_path = validate_output_dir(output_dir)
393
+
394
+ # Detect and split scenes
395
+ detect_and_split_scenes(
396
+ video_path=str(video_path),
397
+ output_dir=output_path,
398
+ detector_type=detector,
399
+ threshold=threshold,
400
+ min_scene_len=min_scene_length,
401
+ max_scenes=max_scenes,
402
+ filter_shorter_than=filter_shorter_than,
403
+ skip_start=skip_start,
404
+ skip_end=skip_end,
405
+ duration=duration,
406
+ save_images_per_scene=save_images,
407
+ stats_file=stats_file,
408
+ luma_only=luma_only,
409
+ adaptive_window=adaptive_window,
410
+ fade_bias=fade_bias,
411
+ downscale_factor=downscale,
412
+ frame_skip=frame_skip,
413
+ )
414
+
415
+
416
+ if __name__ == "__main__":
417
+ app()
packages/ltx-trainer/scripts/train.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Train LTXV models using configuration from YAML files.
5
+ This script provides a command-line interface for training LTXV models using
6
+ either LoRA fine-tuning or full model fine-tuning. It loads configuration from
7
+ a YAML file and passes it to the trainer.
8
+ Basic usage:
9
+ python scripts/train.py CONFIG_PATH [--disable-progress-bars]
10
+ Resume is automatic when a training state file exists next to the loaded checkpoint.
11
+ To start fresh, set `checkpoints.no_resume: true` in the YAML config.
12
+ For multi-GPU/FSDP training, configure and launch via Accelerate:
13
+ accelerate config
14
+ accelerate launch scripts/train.py CONFIG_PATH
15
+ """
16
+
17
+ from pathlib import Path
18
+
19
+ import typer
20
+ import yaml
21
+ from rich.console import Console
22
+
23
+ from ltx_trainer.config import LtxTrainerConfig
24
+ from ltx_trainer.trainer import LtxvTrainer
25
+
26
+ console = Console()
27
+ app = typer.Typer(
28
+ pretty_exceptions_enable=False,
29
+ no_args_is_help=True,
30
+ help="Train LTXV models using configuration from YAML files.",
31
+ )
32
+
33
+
34
+ @app.command()
35
+ def main(
36
+ config_path: str = typer.Argument(..., help="Path to YAML configuration file"),
37
+ disable_progress_bars: bool = typer.Option(
38
+ False,
39
+ "--disable-progress-bars",
40
+ help="Disable progress bars (useful for multi-process runs)",
41
+ ),
42
+ ) -> None:
43
+ """Train the model using the provided configuration file."""
44
+ config_path = Path(config_path)
45
+ if not config_path.exists():
46
+ typer.echo(f"Error: Configuration file {config_path} does not exist.")
47
+ raise typer.Exit(code=1)
48
+
49
+ with open(config_path, "r") as file:
50
+ config_data = yaml.safe_load(file)
51
+
52
+ try:
53
+ trainer_config = LtxTrainerConfig(**config_data)
54
+ except Exception as e:
55
+ typer.echo(f"Error: Invalid configuration data: {e}")
56
+ raise typer.Exit(code=1) from e
57
+
58
+ trainer = LtxvTrainer(trainer_config)
59
+ trainer.train(disable_progress_bars=disable_progress_bars)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ app()
packages/ltx-trainer/src/ltx_trainer/__init__.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import sys
4
+ from logging import getLogger
5
+ from pathlib import Path
6
+
7
+ from rich.logging import RichHandler
8
+
9
+ # Get the process rank
10
+ IS_MULTI_GPU = os.environ.get("LOCAL_RANK") is not None
11
+ RANK = int(os.environ.get("LOCAL_RANK", "0"))
12
+
13
+ # Configure with Rich
14
+ logging.basicConfig(
15
+ level="INFO",
16
+ format=f"\\[rank {RANK}] %(message)s" if IS_MULTI_GPU else "%(message)s",
17
+ handlers=[
18
+ RichHandler(
19
+ rich_tracebacks=True,
20
+ show_time=False,
21
+ markup=True,
22
+ )
23
+ ],
24
+ )
25
+
26
+ # Get the logger and configure it
27
+ logger = getLogger("ltxv_trainer")
28
+ logger.setLevel(logging.DEBUG)
29
+ logger.propagate = True
30
+
31
+ # Set level based on process
32
+ if RANK != 0:
33
+ logger.setLevel(logging.WARNING)
34
+
35
+ # Expose common logging functions directly
36
+ debug = logger.debug
37
+ info = logger.info
38
+ warning = logger.warning
39
+ error = logger.error
40
+ critical = logger.critical
41
+
42
+
43
+ # Add the root directory to the Python path so we can import from scripts.
44
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
packages/ltx-trainer/src/ltx_trainer/captioning.py ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio-visual media captioning using multimodal models.
3
+ This module provides captioning capabilities for videos with audio using:
4
+ - Qwen3-Omni via a local vLLM server (default)
5
+ - Gemini Flash 3.5 (cloud API)
6
+ Both produce a single combined English caption per video as a single
7
+ continuous paragraph of prose.
8
+ The Qwen3-Omni backend runs in a separately-launched vLLM server rather than
9
+ in-process, so vLLM's heavy CUDA dependencies stay out of this package. The
10
+ captioner talks to it over the OpenAI-compatible HTTP API.
11
+ Launch the server once (in an isolated environment) with:
12
+ .. code-block:: bash
13
+ uv run python scripts/serve_captioner.py
14
+ That helper picks BF16 vs FP8 dynamic quantization based on the GPU's free
15
+ memory and forwards everything else to ``vllm serve``. To check the recommended
16
+ command without running it, pass ``--print-cmd``.
17
+ To use Gemini instead, install ``google-genai`` and either set ``GEMINI_API_KEY``
18
+ (Gemini Developer API) or have Google Cloud credentials available (gcloud / an
19
+ attached service account), in which case it uses Vertex AI automatically.
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import re
25
+ import subprocess
26
+ import tempfile
27
+ from abc import ABC, abstractmethod
28
+ from enum import Enum
29
+ from pathlib import Path
30
+ from typing import ClassVar
31
+
32
+ DEFAULT_VIDEO_CAPTION_INSTRUCTION = """\
33
+ Analyze this video and produce a single detailed caption covering both its visual content and its audio. Be \
34
+ detailed enough that someone reading the caption could form an accurate mental picture of what happens on screen \
35
+ and what can be heard. Be exhaustive: include every meaningful detail you can see and hear, including small \
36
+ objects, textures, secondary movements, and minor background sounds.
37
+
38
+ Begin the caption directly with the action or visual detail; do not preface it with phrases like \
39
+ "The video opens with...", "The scene shows...", "We see...", or "There is...".
40
+
41
+ For every shot, include:
42
+ - The shot type and framing (extreme wide / wide / medium / medium close-up / close-up / extreme close-up) and any \
43
+ camera motion.
44
+ - Characters' clothing, appearance, posture, and movement (direction, speed, quality).
45
+ - The environment's materials, textures, lighting, and colors.
46
+ - All audio: spoken dialogue (quoted exactly in the original language), tone of voice, music (style, mood, \
47
+ volume changes), and environmental sounds. If a category is absent -- for example no music is playing, or no one is \
48
+ speaking -- state that explicitly. Do not invent specific instruments, music genres, moods, or ambient sounds \
49
+ that are not actually present.
50
+ - Any on-screen text (signs, titles, labels).
51
+
52
+ Describe only what is visible or audible. Do not infer emotions, intentions, or anything outside the segment. \
53
+ Refer to people descriptively (e.g., "the man in the blue jacket"). Narrate strictly in chronological order; if \
54
+ the video contains multiple shots, describe each one in turn.
55
+
56
+ Write everything as a single continuous paragraph of prose. Do not use section headers, bullet points, or labels \
57
+ like "Audio:" / "Visual:" / "Shot:". Integrate visual and audio details naturally within the same sentences.
58
+
59
+ Return a JSON object with exactly one key:
60
+
61
+ {"combined_caption_english": "<your caption here>"}"""
62
+
63
+
64
+ DEFAULT_IMAGE_CAPTION_INSTRUCTION = """\
65
+ Analyze this image and produce a single detailed caption of its visual content. Be detailed enough that \
66
+ someone reading the caption could form an accurate mental picture of the image. Be thorough: include every meaningful \
67
+ detail that is actually present, including small objects, textures, and background elements.
68
+
69
+ Begin the caption directly with the main subject or a visual detail; do not preface it with phrases like \
70
+ "The image shows...", "This is a photo of...", "We see...", or "There is...".
71
+
72
+ Include:
73
+ - The framing and composition (close-up / medium / wide / overhead, etc.) and the vantage point.
74
+ - The medium or style if distinctive (photograph, illustration, 3D render, painting).
75
+ - People's clothing, appearance, and posture, and what they are doing.
76
+ - The setting's materials, textures, lighting, and colors.
77
+ - Transcribe any visible text verbatim (signs, labels, titles, captions).
78
+
79
+ Describe only what is visible. Do not infer emotions or intentions, and do not describe sounds, motion, or \
80
+ events before or after the moment shown -- this is a single still image. When something is ambiguous, describe \
81
+ the visible cue (e.g., "warm low-angle light") rather than guessing the underlying fact (e.g., "sunrise"). \
82
+ Refer to people descriptively (e.g., "the man in the blue jacket").
83
+
84
+ Only describe what is present. Never state that something is absent or missing -- do not write phrases like \
85
+ "there is no text", "no people are present", or "no other objects". If a category such as people or text does \
86
+ not appear, simply leave it out.
87
+
88
+ Write everything as a single continuous paragraph of prose. Do not use section headers, bullet points, or \
89
+ labels.
90
+
91
+ Return a JSON object with exactly one key:
92
+
93
+ {"combined_caption_english": "<your caption here>"}"""
94
+
95
+
96
+ # Default model served by ``scripts/serve_captioner.py``. The captioner does not
97
+ # download or load this model itself -- it just sends requests to the vLLM
98
+ # server, which already has the model loaded.
99
+ DEFAULT_QWEN_MODEL = "Qwen/Qwen3-Omni-30B-A3B-Thinking"
100
+ DEFAULT_VLLM_BASE_URL = "http://127.0.0.1:8001/v1"
101
+
102
+ # Key the combined-caption prompt asks the model to return its caption under.
103
+ _CAPTION_JSON_KEY = "combined_caption_english"
104
+
105
+
106
+ class CaptionerType(str, Enum):
107
+ """Enum for different types of media captioners."""
108
+
109
+ QWEN_OMNI = "qwen_omni" # Qwen3-Omni via local vLLM HTTP server
110
+ GEMINI_FLASH = "gemini_flash" # Gemini Flash 3.5 cloud API
111
+
112
+
113
+ def create_captioner(captioner_type: CaptionerType, **kwargs) -> "MediaCaptioningModel":
114
+ """Factory function to create a media captioner."""
115
+ match captioner_type:
116
+ case CaptionerType.QWEN_OMNI:
117
+ return QwenOmniCaptioner(**kwargs)
118
+ case CaptionerType.GEMINI_FLASH:
119
+ return GeminiFlashCaptioner(**kwargs)
120
+ case _:
121
+ raise ValueError(f"Unsupported captioner type: {captioner_type}")
122
+
123
+
124
+ class MediaCaptioningModel(ABC):
125
+ """Abstract base class for audio-visual media captioning models."""
126
+
127
+ instruction: str | None = None
128
+
129
+ @abstractmethod
130
+ def caption(self, path: str | Path, **kwargs) -> str:
131
+ """Generate a caption for the given video or image."""
132
+
133
+ def _resolve_instruction(self, path: str | Path) -> str:
134
+ """Return the custom instruction, or the image/video default for this input."""
135
+ if self.instruction is not None:
136
+ return self.instruction
137
+ return DEFAULT_IMAGE_CAPTION_INSTRUCTION if self._is_image_file(path) else DEFAULT_VIDEO_CAPTION_INSTRUCTION
138
+
139
+ @staticmethod
140
+ def _is_image_file(path: str | Path) -> bool:
141
+ return str(path).lower().endswith((".png", ".jpg", ".jpeg", ".heic", ".heif", ".webp"))
142
+
143
+ @staticmethod
144
+ def _is_video_file(path: str | Path) -> bool:
145
+ return str(path).lower().endswith((".mp4", ".avi", ".mov", ".mkv", ".webm"))
146
+
147
+
148
+ class QwenOmniCaptioner(MediaCaptioningModel):
149
+ """Audio-visual captioning via a local vLLM server running Qwen3-Omni.
150
+ The vLLM server must already be running. See ``scripts/serve_captioner.py``
151
+ for a helper that launches one in an isolated environment (no impact on
152
+ this package's dependency tree).
153
+ The captioner uses the OpenAI-compatible chat completions API. It sends
154
+ a ``file://`` URL pointing at the local video, the default combined-caption
155
+ prompt, and parses the JSON-wrapped response.
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ base_url: str = DEFAULT_VLLM_BASE_URL,
161
+ model: str = DEFAULT_QWEN_MODEL,
162
+ api_key: str = "EMPTY",
163
+ instruction: str | None = None,
164
+ max_tokens: int = 4096,
165
+ enable_thinking: bool = False,
166
+ timeout_s: float = 600.0,
167
+ ):
168
+ """Initialize the Qwen3-Omni captioner.
169
+ Args:
170
+ base_url: Base URL of the vLLM OpenAI-compatible server (default
171
+ ``http://127.0.0.1:8001/v1``).
172
+ model: Model identifier the server is serving. Must match the
173
+ server's ``--served-model-name`` (defaults to the HuggingFace
174
+ model ID).
175
+ api_key: Token sent in the ``Authorization`` header. vLLM accepts
176
+ any value by default.
177
+ instruction: Custom instruction prompt. If ``None``, uses the
178
+ default combined-caption prompt.
179
+ max_tokens: Maximum new tokens to generate per caption. 4096 leaves
180
+ comfortable headroom for both ``enable_thinking`` modes.
181
+ enable_thinking: Whether to let the Thinking model produce a
182
+ ``<think>...</think>`` chain-of-thought before the caption.
183
+ Off by default: it makes captioning ~5x slower with little
184
+ quality benefit and occasionally introduces hallucinations
185
+ (e.g., inventing dialogue or background music).
186
+ timeout_s: Per-request HTTP timeout.
187
+ """
188
+ from openai import OpenAI # noqa: PLC0415
189
+
190
+ self.model = model
191
+ self.instruction = instruction
192
+ self.max_tokens = max_tokens
193
+ self.enable_thinking = enable_thinking
194
+ self._client = OpenAI(base_url=base_url, api_key=api_key, timeout=timeout_s)
195
+
196
+ def caption(
197
+ self,
198
+ path: str | Path,
199
+ fps: int = 2,
200
+ ) -> str:
201
+ """Generate a caption for the given video or image.
202
+ Args:
203
+ path: Path to the video/image file to caption.
204
+ fps: Frames per second to sample from the video. Passed through to
205
+ vLLM's multimodal processor (``mm_processor_kwargs.fps``).
206
+ Default 2 is a typical choice for video MLLMs at this resolution.
207
+ Ignored for image inputs.
208
+ Returns:
209
+ The extracted caption string.
210
+ """
211
+ path = Path(path)
212
+ is_image = self._is_image_file(path)
213
+ is_video = self._is_video_file(path)
214
+ if not (is_image or is_video):
215
+ raise ValueError(f"Unsupported media file: {path}")
216
+
217
+ instruction = self._resolve_instruction(path)
218
+
219
+ if is_image:
220
+ content = [
221
+ {"type": "image_url", "image_url": {"url": f"file://{path.resolve()}"}},
222
+ {"type": "text", "text": instruction},
223
+ ]
224
+ return _parse_caption_response(self._chat(content)).strip()
225
+
226
+ return self._caption_video(path, instruction, fps)
227
+
228
+ def _chat(self, content: list[dict], mm_kwargs: dict | None = None) -> str:
229
+ """Send one chat-completions request and return the raw response text."""
230
+ extra_body: dict = {
231
+ "repetition_penalty": 1.05,
232
+ "chat_template_kwargs": {"enable_thinking": self.enable_thinking},
233
+ }
234
+ if mm_kwargs:
235
+ extra_body["mm_processor_kwargs"] = mm_kwargs
236
+ response = self._client.chat.completions.create(
237
+ model=self.model,
238
+ messages=[{"role": "user", "content": content}],
239
+ max_tokens=self.max_tokens,
240
+ temperature=0.0,
241
+ extra_body=extra_body,
242
+ )
243
+ return response.choices[0].message.content or ""
244
+
245
+ def _caption_video(self, path: Path, instruction: str, fps: int) -> str:
246
+ """Caption a video, sending its audio track as a separate modality.
247
+ vLLM does not extract a video's audio on its own (and its
248
+ ``use_audio_in_video`` path is broken server-side), so we pull the audio
249
+ into a 16 kHz mono WAV and send it alongside the video -- otherwise the
250
+ model only sees frames and fabricates any spoken content.
251
+ """
252
+ with tempfile.TemporaryDirectory(prefix="qwencap_") as tmp:
253
+ work = Path(tmp)
254
+
255
+ # Best-effort: ffmpeg fails (and we send video only) if there's no audio.
256
+ audio_url: str | None = None
257
+ try:
258
+ wav = work / "audio.wav"
259
+ _extract_audio_wav(path, wav)
260
+ audio_url = f"file://{wav.resolve()}"
261
+ except subprocess.CalledProcessError:
262
+ pass
263
+
264
+ def content(video: Path) -> list[dict]:
265
+ parts: list[dict] = [{"type": "video_url", "video_url": {"url": f"file://{video.resolve()}"}}]
266
+ if audio_url:
267
+ parts.append({"type": "audio_url", "audio_url": {"url": audio_url}})
268
+ parts.append({"type": "text", "text": instruction})
269
+ return parts
270
+
271
+ mm_kwargs = {"fps": fps}
272
+ try:
273
+ raw = self._chat(content(path), mm_kwargs)
274
+ except Exception as e:
275
+ # Raw / variable-frame-rate videos over-report their frame count, which
276
+ # breaks the server's frame sampler ("... frames from video"). Re-encode
277
+ # to a constant frame rate and retry once.
278
+ if "frames from video" not in str(e):
279
+ raise
280
+ cfr = work / "video_cfr.mp4"
281
+ _transcode_cfr(path, cfr)
282
+ raw = self._chat(content(cfr), mm_kwargs)
283
+
284
+ return _parse_caption_response(raw).strip()
285
+
286
+
287
+ class GeminiFlashCaptioner(MediaCaptioningModel):
288
+ """Audio-visual captioning using Google's Gemini via the Google Gen AI SDK.
289
+ Uses the ``google-genai`` package (the current SDK; ``google-generativeai``
290
+ is deprecated). Auth is resolved automatically:
291
+ 1. If an API key is given (``api_key`` argument, or ``GEMINI_API_KEY`` /
292
+ ``GOOGLE_API_KEY`` in the environment) -> the Gemini Developer API (AI Studio).
293
+ 2. Otherwise, if Google Cloud Application Default Credentials are available
294
+ (an attached service account or ``gcloud auth application-default login``)
295
+ -> Vertex AI. The project comes from ADC (or ``GOOGLE_CLOUD_PROJECT``) and
296
+ the location defaults to ``global`` (override with ``GOOGLE_CLOUD_LOCATION``).
297
+ This means it "just works" on a gcloud-authed GCP VM with no env vars.
298
+ If neither is available, a clear error explains how to authenticate.
299
+ Media is sent inline (``Part.from_bytes``), which works on both backends.
300
+ """
301
+
302
+ MODEL_ID = "gemini-3.5-flash"
303
+
304
+ _MIME_TYPES: ClassVar[dict[str, str]] = {
305
+ ".jpg": "image/jpeg",
306
+ ".jpeg": "image/jpeg",
307
+ ".png": "image/png",
308
+ ".webp": "image/webp",
309
+ ".heic": "image/heic",
310
+ ".heif": "image/heif",
311
+ ".mp4": "video/mp4",
312
+ ".mov": "video/quicktime",
313
+ ".avi": "video/x-msvideo",
314
+ ".mkv": "video/x-matroska",
315
+ ".webm": "video/webm",
316
+ }
317
+
318
+ def __init__(
319
+ self,
320
+ api_key: str | None = None,
321
+ instruction: str | None = None,
322
+ model: str | None = None,
323
+ ):
324
+ """Initialize the Gemini captioner.
325
+ Args:
326
+ api_key: Gemini Developer API key. If ``None``, falls back to
327
+ ``GEMINI_API_KEY`` / ``GOOGLE_API_KEY``; if no key is set at all,
328
+ uses Vertex AI via Application Default Credentials.
329
+ instruction: Custom instruction prompt. If ``None``, uses the default
330
+ image or video prompt depending on the input.
331
+ model: Override the served model id (defaults to ``MODEL_ID``).
332
+ """
333
+ self.instruction = instruction
334
+ self.model = model or self.MODEL_ID
335
+ self._client = self._make_client(api_key)
336
+
337
+ def caption(
338
+ self,
339
+ path: str | Path,
340
+ fps: int = 2, # noqa: ARG002 - kept for API compatibility
341
+ ) -> str:
342
+ from google.genai import types # noqa: PLC0415
343
+
344
+ path = Path(path)
345
+ instruction = self._resolve_instruction(path)
346
+ media = types.Part.from_bytes(data=path.read_bytes(), mime_type=self._mime_type(path))
347
+ response = self._client.models.generate_content(
348
+ model=self.model,
349
+ contents=[media, instruction],
350
+ config=types.GenerateContentConfig(temperature=0.0),
351
+ )
352
+
353
+ # Gemini may also return JSON if it followed our prompt format.
354
+ return _parse_caption_response(response.text or "").strip()
355
+
356
+ @classmethod
357
+ def _mime_type(cls, path: Path) -> str:
358
+ try:
359
+ return cls._MIME_TYPES[path.suffix.lower()]
360
+ except KeyError:
361
+ raise ValueError(f"Unsupported media type for Gemini: {path.suffix}") from None
362
+
363
+ def _make_client(self, api_key: str | None): # noqa: ANN202 - genai.Client type is lazy-imported
364
+ from google import genai # noqa: PLC0415
365
+
366
+ # 1. API key (explicit arg or env) -> Gemini Developer API.
367
+ key = api_key or os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
368
+ if key:
369
+ return genai.Client(api_key=key)
370
+
371
+ # 2. No key -> Vertex AI via Application Default Credentials (gcloud / service account).
372
+ import google.auth # noqa: PLC0415
373
+
374
+ try:
375
+ _, adc_project = google.auth.default()
376
+ except Exception as e:
377
+ raise ValueError(
378
+ "No Gemini credentials found. Provide an API key (--api-key, or "
379
+ "GEMINI_API_KEY / GOOGLE_API_KEY), or set up Google Cloud credentials "
380
+ "for Vertex AI (e.g. `gcloud auth application-default login` or an "
381
+ "attached service account)."
382
+ ) from e
383
+
384
+ project = os.environ.get("GOOGLE_CLOUD_PROJECT") or adc_project
385
+ location = os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
386
+ return genai.Client(vertexai=True, project=project, location=location)
387
+
388
+
389
+ def _parse_caption_response(raw: str) -> str:
390
+ """Extract the caption text from a model response.
391
+ Backend-agnostic: works for any model that follows the combined-caption
392
+ prompt. Handles the formats a model may produce:
393
+ - Plain caption text
394
+ - JSON ``{"combined_caption_english": "..."}``
395
+ - ``<think>...</think>`` chain-of-thought followed by either of the above
396
+ - Truncated JSON (when generation hits a token limit mid-string)
397
+ """
398
+ text = re.sub(r"<think>[\s\S]*?</think>", "", raw).strip()
399
+
400
+ # Thinking models (e.g. Qwen3-Omni-*-Thinking) emit the reasoning trace
401
+ # without an opening ``<think>`` tag, because the chat template injects it
402
+ # for them -- so the response starts mid-thought and is terminated by a lone
403
+ # ``</think>`` before the real answer. Drop everything up to that closer.
404
+ if "</think>" in text:
405
+ text = text.rsplit("</think>", 1)[1].strip()
406
+
407
+ if not text:
408
+ return raw.strip()
409
+
410
+ try:
411
+ parsed = json.loads(text)
412
+ if isinstance(parsed, dict) and _CAPTION_JSON_KEY in parsed:
413
+ return parsed[_CAPTION_JSON_KEY]
414
+ except (json.JSONDecodeError, ValueError):
415
+ pass
416
+
417
+ match = re.search(rf"\{{[^{{}}]*\"{_CAPTION_JSON_KEY}\"[^{{}}]*\}}", text)
418
+ if match:
419
+ try:
420
+ parsed = json.loads(match.group())
421
+ if isinstance(parsed, dict) and _CAPTION_JSON_KEY in parsed:
422
+ return parsed[_CAPTION_JSON_KEY]
423
+ except (json.JSONDecodeError, ValueError):
424
+ pass
425
+
426
+ # Truncated JSON: extract the string value even if the closing quote/brace is missing.
427
+ match = re.search(rf'"{_CAPTION_JSON_KEY}"\s*:\s*"((?:[^"\\]|\\.)*)', text)
428
+ if match:
429
+ try:
430
+ return json.loads('"' + match.group(1) + '"')
431
+ except (json.JSONDecodeError, ValueError):
432
+ return match.group(1)
433
+
434
+ return text
435
+
436
+
437
+ def _run_ffmpeg(args: list[str]) -> None:
438
+ """Run the ffmpeg binary bundled with ``imageio-ffmpeg`` (a dependency)."""
439
+ import imageio_ffmpeg # noqa: PLC0415
440
+
441
+ cmd = [imageio_ffmpeg.get_ffmpeg_exe(), "-y", "-loglevel", "error", *args]
442
+ subprocess.run(cmd, check=True, capture_output=True)
443
+
444
+
445
+ def _extract_audio_wav(src: Path, dest: Path) -> None:
446
+ """Extract the audio track to a 16 kHz mono PCM WAV (matches pretraining).
447
+ Raises ``CalledProcessError`` when the video has no audio stream.
448
+ """
449
+ _run_ffmpeg(["-i", str(src), "-vn", "-ac", "1", "-ar", "16000", "-c:a", "pcm_s16le", str(dest)])
450
+
451
+
452
+ def _transcode_cfr(src: Path, dest: Path) -> None:
453
+ """Re-encode the video to a constant frame rate so the server's frame sampler can
454
+ read every requested index (raw / variable-frame-rate videos over-report frames)."""
455
+ _run_ffmpeg(["-i", str(src), "-fps_mode", "cfr", "-c:v", "libx264", "-pix_fmt", "yuv420p", "-an", str(dest)])
packages/ltx-trainer/src/ltx_trainer/config.py ADDED
@@ -0,0 +1,833 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import Annotated, Literal, Union
3
+
4
+ from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag, ValidationInfo, field_validator, model_validator
5
+
6
+ from ltx_trainer.quantization import QuantizationOptions
7
+ from ltx_trainer.training_strategies.base_strategy import TrainingStrategyConfigBase
8
+ from ltx_trainer.training_strategies.flexible import FlexibleStrategyConfig
9
+ from ltx_trainer.training_strategies.text_to_video import TextToVideoConfig
10
+ from ltx_trainer.training_strategies.video_to_video import VideoToVideoConfig
11
+
12
+
13
+ class ConfigBaseModel(BaseModel):
14
+ model_config = ConfigDict(extra="forbid")
15
+
16
+
17
+ # =============================================================================
18
+ # Validation Condition Types
19
+ # =============================================================================
20
+
21
+
22
+ class FirstFrameConditionConfig(ConfigBaseModel):
23
+ """First-frame conditioning (intrinsic, latent_idx=0). Always targets video.
24
+ If image_or_video points to a video file, the first frame is automatically extracted.
25
+ """
26
+
27
+ type: Literal["first_frame"] = "first_frame"
28
+ image_or_video: str | Path
29
+
30
+
31
+ class PrefixConditionConfig(ConfigBaseModel):
32
+ """Prefix conditioning for temporal extension (intrinsic). Exactly one of video/audio must be set."""
33
+
34
+ type: Literal["prefix"] = "prefix"
35
+ video: str | None = None
36
+ audio: str | None = None
37
+ num_frames: int | None = Field(
38
+ default=None,
39
+ ge=1,
40
+ description="Number of pixel frames for video prefix. Must satisfy num_frames %% 8 == 1.",
41
+ )
42
+ duration: float | None = Field(default=None, gt=0, description="Duration in seconds for audio prefix")
43
+
44
+ @model_validator(mode="after")
45
+ def validate_exactly_one_modality(self) -> "PrefixConditionConfig":
46
+ if (self.video is None) == (self.audio is None):
47
+ raise ValueError("Exactly one of 'video' or 'audio' must be set for prefix condition")
48
+ return self
49
+
50
+ @model_validator(mode="after")
51
+ def validate_num_frames_constraint(self) -> "PrefixConditionConfig":
52
+ if self.video is not None and self.num_frames is not None and self.num_frames % 8 != 1:
53
+ raise ValueError(
54
+ f"num_frames ({self.num_frames}) must satisfy num_frames % 8 == 1 "
55
+ f"for video prefix (e.g., 1, 9, 17, 25, ...)"
56
+ )
57
+ return self
58
+
59
+
60
+ class SuffixConditionConfig(ConfigBaseModel):
61
+ """Suffix conditioning for temporal extension (intrinsic). Exactly one of video/audio must be set."""
62
+
63
+ type: Literal["suffix"] = "suffix"
64
+ video: str | None = None
65
+ audio: str | None = None
66
+ num_frames: int | None = Field(
67
+ default=None,
68
+ ge=1,
69
+ description="Number of pixel frames for video suffix. Must satisfy num_frames %% 8 == 0.",
70
+ )
71
+ duration: float | None = Field(default=None, gt=0, description="Duration in seconds for audio suffix")
72
+
73
+ @model_validator(mode="after")
74
+ def validate_exactly_one_modality(self) -> "SuffixConditionConfig":
75
+ if (self.video is None) == (self.audio is None):
76
+ raise ValueError("Exactly one of 'video' or 'audio' must be set for suffix condition")
77
+ return self
78
+
79
+ @model_validator(mode="after")
80
+ def validate_num_frames_constraint(self) -> "SuffixConditionConfig":
81
+ if self.video is not None and self.num_frames is not None and self.num_frames % 8 != 0:
82
+ raise ValueError(
83
+ f"num_frames ({self.num_frames}) must satisfy num_frames % 8 == 0 "
84
+ f"for video suffix (e.g., 8, 16, 24, 32, ...)"
85
+ )
86
+ return self
87
+
88
+
89
+ class SpatialCropConditionConfig(ConfigBaseModel):
90
+ """Spatial crop conditioning for outpainting (intrinsic, video only)."""
91
+
92
+ type: Literal["spatial_crop"] = "spatial_crop"
93
+ video: str
94
+ spatial_region: tuple[int, int, int, int] = Field(
95
+ ..., description="Spatial crop region as (y1, x1, y2, x2) in pixel coordinates"
96
+ )
97
+
98
+
99
+ class MaskConditionConfig(ConfigBaseModel):
100
+ """Mask-based conditioning for inpainting (intrinsic). Exactly one of video/audio must be set."""
101
+
102
+ type: Literal["mask"] = "mask"
103
+ video: str | None = None
104
+ audio: str | None = None
105
+ mask: str
106
+
107
+ @model_validator(mode="after")
108
+ def validate_exactly_one_modality(self) -> "MaskConditionConfig":
109
+ if (self.video is None) == (self.audio is None):
110
+ raise ValueError("Exactly one of 'video' or 'audio' must be set for mask condition")
111
+ return self
112
+
113
+
114
+ class ReferenceConditionConfig(ConfigBaseModel):
115
+ """Reference conditioning (IC-LoRA style concatenation). Exactly one of video/audio must be set."""
116
+
117
+ type: Literal["reference"] = "reference"
118
+ video: str | None = None
119
+ audio: str | None = None
120
+ downscale_factor: int = Field(default=1, ge=1)
121
+ temporal_scale_factor: int = Field(default=1, ge=1)
122
+ include_in_output: bool = False
123
+
124
+ @model_validator(mode="after")
125
+ def validate_exactly_one_modality(self) -> "ReferenceConditionConfig":
126
+ if (self.video is None) == (self.audio is None):
127
+ raise ValueError("Exactly one of 'video' or 'audio' must be set for reference condition")
128
+ return self
129
+
130
+
131
+ class VideoToAudioConditionConfig(ConfigBaseModel):
132
+ """Video-to-audio — video is provided as frozen cross-modal conditioning.
133
+ The video is kept clean (sigma=0) and influences audio generation via cross-modal attention.
134
+ """
135
+
136
+ type: Literal["video_to_audio"] = "video_to_audio"
137
+ video: str
138
+
139
+
140
+ class AudioToVideoConditionConfig(ConfigBaseModel):
141
+ """Audio-to-video — audio is provided as frozen cross-modal conditioning.
142
+ The audio is kept clean (sigma=0) and influences video generation via cross-modal attention.
143
+ """
144
+
145
+ type: Literal["audio_to_video"] = "audio_to_video"
146
+ audio: str
147
+
148
+
149
+ ValidationCondition = Annotated[
150
+ Union[
151
+ FirstFrameConditionConfig,
152
+ PrefixConditionConfig,
153
+ SuffixConditionConfig,
154
+ SpatialCropConditionConfig,
155
+ MaskConditionConfig,
156
+ ReferenceConditionConfig,
157
+ VideoToAudioConditionConfig,
158
+ AudioToVideoConditionConfig,
159
+ ],
160
+ Field(discriminator="type"),
161
+ ]
162
+
163
+
164
+ def _condition_targets_video(cond: ValidationCondition) -> bool:
165
+ """Check if a validation condition targets the video modality."""
166
+ if cond.type in ("first_frame", "spatial_crop", "video_to_audio"):
167
+ return True
168
+ if cond.type in ("prefix", "suffix", "mask", "reference"):
169
+ return getattr(cond, "video", None) is not None
170
+ return False
171
+
172
+
173
+ def _condition_targets_audio(cond: ValidationCondition) -> bool:
174
+ """Check if a validation condition targets the audio modality."""
175
+ if cond.type == "audio_to_video":
176
+ return True
177
+ if cond.type in ("prefix", "suffix", "mask", "reference"):
178
+ return getattr(cond, "audio", None) is not None
179
+ return False
180
+
181
+
182
+ class ValidationSample(ConfigBaseModel):
183
+ """Configuration for a single validation sample — fully self-describing."""
184
+
185
+ prompt: str
186
+ conditions: list[ValidationCondition] = Field(default_factory=list)
187
+
188
+ video_dims: tuple[int, int, int] | None = Field(
189
+ default=None,
190
+ description="Per-sample override for (width, height, frames). None = inherit from ValidationConfig.",
191
+ )
192
+ seed: int | None = Field(
193
+ default=None,
194
+ description="Per-sample override for random seed. None = inherit from ValidationConfig.",
195
+ )
196
+
197
+ @field_validator("video_dims")
198
+ @classmethod
199
+ def validate_video_dims(cls, v: tuple[int, int, int] | None) -> tuple[int, int, int] | None:
200
+ if v is None:
201
+ return v
202
+ width, height, frames = v
203
+ if width % 32 != 0:
204
+ raise ValueError(f"Width ({width}) must be divisible by 32")
205
+ if height % 32 != 0:
206
+ raise ValueError(f"Height ({height}) must be divisible by 32")
207
+ if frames % 8 != 1:
208
+ raise ValueError(f"Frames ({frames}) must satisfy frames % 8 == 1 for LTX-2 (e.g., 1, 9, 17, 25, ...)")
209
+ return v
210
+
211
+ @model_validator(mode="after")
212
+ def validate_frozen_modality_conflicts(self) -> "ValidationSample":
213
+ frozen_types = {c.type for c in self.conditions if c.type in ("video_to_audio", "audio_to_video")}
214
+
215
+ if "video_to_audio" in frozen_types and "audio_to_video" in frozen_types:
216
+ raise ValueError(
217
+ "Cannot have both video_to_audio and audio_to_video conditions — nothing would be generated"
218
+ )
219
+
220
+ if "video_to_audio" in frozen_types:
221
+ for c in self.conditions:
222
+ if c.type != "video_to_audio" and _condition_targets_video(c):
223
+ raise ValueError(
224
+ f"Cannot use video-targeting '{c.type}' condition when video is frozen (video_to_audio)"
225
+ )
226
+
227
+ if "audio_to_video" in frozen_types:
228
+ for c in self.conditions:
229
+ if c.type != "audio_to_video" and _condition_targets_audio(c):
230
+ raise ValueError(
231
+ f"Cannot use audio-targeting '{c.type}' condition when audio is frozen (audio_to_video)"
232
+ )
233
+
234
+ return self
235
+
236
+
237
+ class ModelConfig(ConfigBaseModel):
238
+ """Configuration for the base model and training mode"""
239
+
240
+ model_path: str | Path = Field(
241
+ ...,
242
+ description="Model path - local path to safetensors checkpoint file",
243
+ )
244
+
245
+ text_encoder_path: str | Path | None = Field(
246
+ default=None,
247
+ description="Path to text encoder (required for LTX-2/Gemma models, optional for LTXV/T5 models)",
248
+ )
249
+
250
+ training_mode: Literal["lora", "full"] = Field(
251
+ default="lora",
252
+ description="Training mode - either LoRA fine-tuning or full model fine-tuning",
253
+ )
254
+
255
+ load_checkpoint: str | Path | None = Field(
256
+ default=None,
257
+ description="Path to a checkpoint file or directory to load from. "
258
+ "If a directory is provided, the latest checkpoint will be used.",
259
+ )
260
+
261
+ @field_validator("model_path")
262
+ @classmethod
263
+ def validate_model_path(cls, v: str | Path) -> str | Path:
264
+ """Validate that model_path is either a valid URL or an existing local path."""
265
+ is_url = str(v).startswith(("http://", "https://"))
266
+
267
+ if is_url:
268
+ raise ValueError(f"Model path cannot be a URL: {v}")
269
+
270
+ if not Path(v).exists():
271
+ raise ValueError(f"Model path does not exist: {v}")
272
+
273
+ return v
274
+
275
+
276
+ class LoraConfig(ConfigBaseModel):
277
+ """Configuration for LoRA fine-tuning"""
278
+
279
+ rank: int = Field(
280
+ default=64,
281
+ description="Rank of LoRA adaptation",
282
+ ge=2,
283
+ )
284
+
285
+ alpha: int = Field(
286
+ default=64,
287
+ description="Alpha scaling factor for LoRA",
288
+ ge=1,
289
+ )
290
+
291
+ dropout: float = Field(
292
+ default=0.0,
293
+ description="Dropout probability for LoRA layers",
294
+ ge=0.0,
295
+ le=1.0,
296
+ )
297
+
298
+ target_modules: list[str] = Field(
299
+ default=["to_k", "to_q", "to_v", "to_out.0"],
300
+ description="List of modules to target with LoRA",
301
+ )
302
+
303
+
304
+ def _get_strategy_discriminator(v: dict | TrainingStrategyConfigBase) -> str:
305
+ """Discriminator function for strategy config union."""
306
+ if isinstance(v, dict):
307
+ return v.get("name", "text_to_video")
308
+ return v.name
309
+
310
+
311
+ # Union type for all strategy configs with discriminator
312
+ TrainingStrategyConfig = Annotated[
313
+ Annotated[TextToVideoConfig, Tag("text_to_video")]
314
+ | Annotated[VideoToVideoConfig, Tag("video_to_video")]
315
+ | Annotated[FlexibleStrategyConfig, Tag("flexible")],
316
+ Discriminator(_get_strategy_discriminator),
317
+ ]
318
+
319
+
320
+ class OptimizationConfig(ConfigBaseModel):
321
+ """Configuration for optimization parameters"""
322
+
323
+ learning_rate: float = Field(
324
+ default=5e-4,
325
+ description="Learning rate for optimization",
326
+ )
327
+
328
+ steps: int = Field(
329
+ default=3000,
330
+ description="Number of training steps",
331
+ )
332
+
333
+ batch_size: int = Field(
334
+ default=2,
335
+ description="Batch size for training",
336
+ )
337
+
338
+ gradient_accumulation_steps: int = Field(
339
+ default=1,
340
+ description="Number of steps to accumulate gradients",
341
+ )
342
+
343
+ max_grad_norm: float = Field(
344
+ default=1.0,
345
+ description="Maximum gradient norm for clipping",
346
+ )
347
+
348
+ optimizer_type: Literal["adamw", "adamw8bit"] = Field(
349
+ default="adamw",
350
+ description="Type of optimizer to use for training",
351
+ )
352
+
353
+ scheduler_type: Literal[
354
+ "constant",
355
+ "linear",
356
+ "cosine",
357
+ "cosine_with_restarts",
358
+ "polynomial",
359
+ "step",
360
+ ] = Field(
361
+ default="linear",
362
+ description="Type of scheduler to use for training",
363
+ )
364
+
365
+ scheduler_params: dict = Field(
366
+ default_factory=dict,
367
+ description="Parameters for the scheduler",
368
+ )
369
+
370
+ enable_gradient_checkpointing: bool = Field(
371
+ default=False,
372
+ description="Enable gradient checkpointing to save memory at the cost of slower training",
373
+ )
374
+
375
+
376
+ class AccelerationConfig(ConfigBaseModel):
377
+ """Configuration for hardware acceleration and compute optimization"""
378
+
379
+ mixed_precision_mode: Literal["no", "fp16", "bf16"] | None = Field(
380
+ default="bf16",
381
+ description="Mixed precision training mode",
382
+ )
383
+
384
+ quantization: QuantizationOptions | None = Field(
385
+ default=None,
386
+ description="Quantization precision to use",
387
+ )
388
+
389
+ load_text_encoder_in_8bit: bool = Field(
390
+ default=False,
391
+ description="Whether to load the text encoder in 8-bit precision to save memory",
392
+ )
393
+
394
+ offload_optimizer_during_validation: bool = Field(
395
+ default=False,
396
+ description="Offload optimizer state to CPU before validation video sampling and reload "
397
+ "it afterwards, to free VRAM for inference. Useful when optimizer state is large "
398
+ "(e.g. AdamW for full fine-tuning or high-rank LoRA) and validation OOMs because the "
399
+ "VAE decoder + transformer + optimizer state cannot coexist on the GPU. Has no effect "
400
+ "for FSDP (sharded state). Disabled by default.",
401
+ )
402
+
403
+
404
+ class DataConfig(ConfigBaseModel):
405
+ """Configuration for data loading and processing"""
406
+
407
+ preprocessed_data_root: str = Field(
408
+ description="Path to folder containing preprocessed training data",
409
+ )
410
+
411
+ num_dataloader_workers: int = Field(
412
+ default=2,
413
+ description="Number of background processes for data loading (0 means synchronous loading)",
414
+ ge=0,
415
+ )
416
+
417
+ @field_validator("preprocessed_data_root")
418
+ @classmethod
419
+ def validate_preprocessed_data_root(cls, v: str) -> str:
420
+ """Validate that preprocessed_data_root exists."""
421
+ path = Path(v).expanduser().resolve()
422
+ if not path.exists():
423
+ raise ValueError(f"Dataset path does not exist: {v}")
424
+ if not path.is_dir():
425
+ raise ValueError(f"Dataset path is not a directory: {v}")
426
+ return str(path)
427
+
428
+
429
+ class ValidationConfig(ConfigBaseModel):
430
+ """Configuration for validation during training"""
431
+
432
+ # Per-sample configuration (new format — preferred)
433
+ samples: list[ValidationSample] = Field(
434
+ default_factory=list,
435
+ description="List of validation samples. Each sample is fully self-describing with its own "
436
+ "prompt, conditions, and optional overrides. Replaces prompts/images/reference_videos.",
437
+ )
438
+
439
+ # Legacy fields (deprecated — converted to samples internally via convert_legacy_format)
440
+ prompts: list[str] = Field(
441
+ default_factory=list,
442
+ description="[DEPRECATED: use 'samples' instead] List of prompts to use for validation",
443
+ )
444
+
445
+ negative_prompt: str = Field(
446
+ default="worst quality, inconsistent motion, blurry, jittery, distorted",
447
+ description="Negative prompt to use for validation examples",
448
+ )
449
+
450
+ images: list[str] | None = Field(
451
+ default=None,
452
+ description="[DEPRECATED: use 'samples' with first_frame conditions] "
453
+ "List of image paths to use for validation. "
454
+ "One image path must be provided for each validation prompt",
455
+ )
456
+
457
+ reference_videos: list[str] | None = Field(
458
+ default=None,
459
+ description="[DEPRECATED: use 'samples' with reference conditions] "
460
+ "List of reference video paths to use for validation. "
461
+ "One video path must be provided for each validation prompt",
462
+ )
463
+
464
+ reference_downscale_factor: int = Field(
465
+ default=1,
466
+ description="[DEPRECATED: use downscale_factor on ReferenceCondition] "
467
+ "Downscale factor for reference videos in IC-LoRA validation. "
468
+ "When > 1, reference videos are processed at 1/n resolution (e.g., 2 means half resolution). "
469
+ "Must match the factor used during dataset preprocessing.",
470
+ ge=1,
471
+ )
472
+
473
+ video_dims: tuple[int, int, int] = Field(
474
+ default=(960, 544, 97),
475
+ description="Dimensions of validation videos (width, height, frames). "
476
+ "Width and height must be divisible by 32. Frames must satisfy frames % 8 == 1 for LTX-2.",
477
+ )
478
+
479
+ @field_validator("video_dims")
480
+ @classmethod
481
+ def validate_video_dims(cls, v: tuple[int, int, int]) -> tuple[int, int, int]:
482
+ """Validate video dimensions for LTX-2 compatibility."""
483
+ width, height, frames = v
484
+
485
+ if width % 32 != 0:
486
+ raise ValueError(f"Width ({width}) must be divisible by 32")
487
+ if height % 32 != 0:
488
+ raise ValueError(f"Height ({height}) must be divisible by 32")
489
+ if frames % 8 != 1:
490
+ raise ValueError(f"Frames ({frames}) must satisfy frames % 8 == 1 for LTX-2 (e.g., 1, 9, 17, 25, ...)")
491
+
492
+ return v
493
+
494
+ frame_rate: float = Field(
495
+ default=25.0,
496
+ description="Frame rate for validation videos",
497
+ gt=0,
498
+ )
499
+
500
+ seed: int = Field(
501
+ default=42,
502
+ description="Random seed used when sampling validation videos",
503
+ )
504
+
505
+ inference_steps: int = Field(
506
+ default=50,
507
+ description="Number of inference steps for validation",
508
+ gt=0,
509
+ )
510
+
511
+ interval: int | None = Field(
512
+ default=100,
513
+ description="Number of steps between validation runs. If None, validation is disabled.",
514
+ gt=0,
515
+ )
516
+
517
+ guidance_scale: float = Field(
518
+ default=4.0,
519
+ description="CFG guidance scale to use during validation",
520
+ ge=1.0,
521
+ )
522
+
523
+ stg_scale: float = Field(
524
+ default=1.0,
525
+ description="STG (Spatio-Temporal Guidance) scale. 0.0 disables STG. "
526
+ "Recommended value is 1.0. STG is combined with CFG for improved video quality.",
527
+ ge=0.0,
528
+ )
529
+
530
+ stg_blocks: list[int] | None = Field(
531
+ default=[29],
532
+ description="Which transformer blocks to perturb for STG. "
533
+ "None means all blocks are perturbed. Recommended for LTX-2: [29].",
534
+ )
535
+
536
+ stg_mode: Literal["stg_av", "stg_v"] = Field(
537
+ default="stg_av",
538
+ description="STG mode: 'stg_av' skips both audio and video self-attention, "
539
+ "'stg_v' skips only video self-attention.",
540
+ )
541
+
542
+ generate_audio: bool = Field(
543
+ default=True,
544
+ description="Whether to generate audio in validation samples. "
545
+ "Independent of training strategy setting - you can generate audio "
546
+ "in validation even when not training the audio branch.",
547
+ )
548
+
549
+ generate_video: bool = Field(
550
+ default=True,
551
+ description="Whether to generate video in validation samples. "
552
+ "Set to False for audio-only or v2a validation to save VRAM by skipping video VAE decoder loading. "
553
+ "When False, validation will only generate audio (requires generate_audio=True).",
554
+ )
555
+
556
+ skip_initial_validation: bool = Field(
557
+ default=False,
558
+ description="Skip validation video sampling at step 0 (beginning of training)",
559
+ )
560
+
561
+ include_reference_in_output: bool = Field(
562
+ default=False,
563
+ description="[DEPRECATED: use include_in_output on ReferenceCondition] "
564
+ "For video-to-video training: concatenate the original reference video side-by-side "
565
+ "with the generated output. The reference comes from the input video, not from the model's output.",
566
+ )
567
+
568
+ @field_validator("images")
569
+ @classmethod
570
+ def validate_images(cls, v: list[str] | None, info: ValidationInfo) -> list[str] | None:
571
+ """Validate that number of images (if provided) matches number of prompts."""
572
+ if v is None:
573
+ return None
574
+
575
+ num_prompts = len(info.data.get("prompts", []))
576
+ if v is not None and len(v) != num_prompts:
577
+ raise ValueError(f"Number of images ({len(v)}) must match number of prompts ({num_prompts})")
578
+
579
+ for image_path in v:
580
+ if not Path(image_path).exists():
581
+ raise ValueError(f"Image path '{image_path}' does not exist")
582
+
583
+ return v
584
+
585
+ @field_validator("reference_videos")
586
+ @classmethod
587
+ def validate_reference_videos(cls, v: list[str] | None, info: ValidationInfo) -> list[str] | None:
588
+ """Validate that number of reference videos (if provided) matches number of prompts."""
589
+ if v is None:
590
+ return None
591
+
592
+ num_prompts = len(info.data.get("prompts", []))
593
+ if v is not None and len(v) != num_prompts:
594
+ raise ValueError(f"Number of reference videos ({len(v)}) must match number of prompts ({num_prompts})")
595
+
596
+ for video_path in v:
597
+ if not Path(video_path).exists():
598
+ raise ValueError(f"Reference video path '{video_path}' does not exist")
599
+
600
+ return v
601
+
602
+ @model_validator(mode="after")
603
+ def convert_legacy_format(self) -> "ValidationConfig":
604
+ """Convert deprecated prompts/images/reference_videos to the new samples format."""
605
+ if self.prompts and not self.samples:
606
+ samples = []
607
+ for i, prompt in enumerate(self.prompts):
608
+ conditions: list[ValidationCondition] = []
609
+ if self.images and i < len(self.images):
610
+ conditions.append(FirstFrameConditionConfig(image_or_video=self.images[i]))
611
+ if self.reference_videos and i < len(self.reference_videos):
612
+ conditions.append(
613
+ ReferenceConditionConfig(
614
+ video=self.reference_videos[i],
615
+ downscale_factor=self.reference_downscale_factor,
616
+ include_in_output=self.include_reference_in_output,
617
+ )
618
+ )
619
+ samples.append(ValidationSample(prompt=prompt, conditions=conditions))
620
+ self.samples = samples
621
+ return self
622
+
623
+ @model_validator(mode="after")
624
+ def validate_scaled_reference_dimensions(self) -> "ValidationConfig":
625
+ """Validate that scaled reference dimensions are valid when reference_downscale_factor > 1."""
626
+ if self.reference_downscale_factor > 1:
627
+ width, height, _frames = self.video_dims
628
+
629
+ if width % self.reference_downscale_factor != 0:
630
+ raise ValueError(
631
+ f"Width {width} is not evenly divisible by reference_downscale_factor "
632
+ f"{self.reference_downscale_factor}. Choose a downscale factor that divides {width} evenly."
633
+ )
634
+ if height % self.reference_downscale_factor != 0:
635
+ raise ValueError(
636
+ f"Height {height} is not evenly divisible by reference_downscale_factor "
637
+ f"{self.reference_downscale_factor}. Choose a downscale factor that divides {height} evenly."
638
+ )
639
+
640
+ scaled_width = width // self.reference_downscale_factor
641
+ scaled_height = height // self.reference_downscale_factor
642
+
643
+ if scaled_width % 32 != 0:
644
+ raise ValueError(
645
+ f"Scaled reference width {scaled_width} (from {width} / {self.reference_downscale_factor}) "
646
+ f"is not divisible by 32. Choose a different downscale factor or adjust video_dims."
647
+ )
648
+ if scaled_height % 32 != 0:
649
+ raise ValueError(
650
+ f"Scaled reference height {scaled_height} (from {height} / {self.reference_downscale_factor}) "
651
+ f"is not divisible by 32. Choose a different downscale factor or adjust video_dims."
652
+ )
653
+
654
+ return self
655
+
656
+ @model_validator(mode="after")
657
+ def validate_output_modality_requirements(self) -> "ValidationConfig":
658
+ """Validate output modality settings when validation is configured."""
659
+ has_validation = bool(self.prompts) or bool(self.samples)
660
+ if has_validation and not self.generate_video and not self.generate_audio:
661
+ raise ValueError(
662
+ "At least one of generate_video or generate_audio must be True when validation is configured."
663
+ )
664
+ return self
665
+
666
+
667
+ class CheckpointsConfig(ConfigBaseModel):
668
+ """Configuration for model checkpointing during training"""
669
+
670
+ interval: int | None = Field(
671
+ default=None,
672
+ description="Number of steps between checkpoint saves. If None, intermediate checkpoints are disabled.",
673
+ gt=0,
674
+ )
675
+
676
+ keep_last_n: int = Field(
677
+ default=1,
678
+ description="Number of most recent checkpoints to keep. Set to -1 to keep all checkpoints.",
679
+ ge=-1,
680
+ )
681
+
682
+ precision: Literal["bfloat16", "float32"] = Field(
683
+ default="bfloat16",
684
+ description="Precision to use when saving checkpoint weights. Options: 'bfloat16' or 'float32'.",
685
+ )
686
+
687
+ no_resume: bool = Field(
688
+ default=False,
689
+ description="When True, ignore any saved training state and start from step 0. "
690
+ "Model weights from load_checkpoint are still loaded, but optimizer/scheduler "
691
+ "state and step counter are reset.",
692
+ )
693
+
694
+ save_training_state: Literal["full", "minimal", "off"] = Field(
695
+ default="minimal",
696
+ description="Save training state alongside checkpoints for resume. "
697
+ "'full': optimizer + scheduler + RNG + step (~800MB for LoRA, much larger for full fine-tuning). "
698
+ "'minimal': scheduler + RNG + step only (~few KB, sufficient for LoRA). "
699
+ "'off': nothing saved, resume not possible.",
700
+ )
701
+
702
+
703
+ class HubConfig(ConfigBaseModel):
704
+ """Configuration for Hugging Face Hub integration"""
705
+
706
+ push_to_hub: bool = Field(default=False, description="Whether to push the model weights to the Hugging Face Hub")
707
+ hub_model_id: str | None = Field(
708
+ default=None, description="Hugging Face Hub repository ID (e.g., 'username/repo-name')"
709
+ )
710
+
711
+ @model_validator(mode="after")
712
+ def validate_hub_config(self) -> "HubConfig":
713
+ """Validate that hub_model_id is not None when push_to_hub is True."""
714
+ if self.push_to_hub and not self.hub_model_id:
715
+ raise ValueError("hub_model_id must be specified when push_to_hub is True")
716
+ return self
717
+
718
+
719
+ class WandbConfig(ConfigBaseModel):
720
+ """Configuration for Weights & Biases logging"""
721
+
722
+ enabled: bool = Field(
723
+ default=False,
724
+ description="Whether to enable W&B logging",
725
+ )
726
+
727
+ project: str = Field(
728
+ default="ltxv-trainer",
729
+ description="W&B project name",
730
+ )
731
+
732
+ entity: str | None = Field(
733
+ default=None,
734
+ description="W&B username or team",
735
+ )
736
+
737
+ tags: list[str] = Field(
738
+ default_factory=list,
739
+ description="Tags to add to the W&B run",
740
+ )
741
+
742
+ log_validation_videos: bool = Field(
743
+ default=True,
744
+ description="Whether to log validation videos to W&B",
745
+ )
746
+
747
+
748
+ class FlowMatchingConfig(ConfigBaseModel):
749
+ """Configuration for flow matching training"""
750
+
751
+ timestep_sampling_mode: Literal["uniform", "shifted_logit_normal"] = Field(
752
+ default="shifted_logit_normal",
753
+ description="Mode to use for timestep sampling",
754
+ )
755
+
756
+ timestep_sampling_params: dict = Field(
757
+ default_factory=dict,
758
+ description="Parameters for timestep sampling",
759
+ )
760
+
761
+
762
+ class LtxTrainerConfig(ConfigBaseModel):
763
+ """Unified configuration for LTXV training"""
764
+
765
+ # Sub-configurations
766
+ model: ModelConfig = Field(default_factory=ModelConfig)
767
+ lora: LoraConfig | None = Field(default=None)
768
+ training_strategy: TrainingStrategyConfig = Field(
769
+ default_factory=TextToVideoConfig,
770
+ description="Training strategy configuration. Determines the training mode and its parameters.",
771
+ )
772
+ optimization: OptimizationConfig = Field(default_factory=OptimizationConfig)
773
+ acceleration: AccelerationConfig = Field(default_factory=AccelerationConfig)
774
+ data: DataConfig
775
+ validation: ValidationConfig = Field(default_factory=ValidationConfig)
776
+ checkpoints: CheckpointsConfig = Field(default_factory=CheckpointsConfig)
777
+ hub: HubConfig = Field(default_factory=HubConfig)
778
+ flow_matching: FlowMatchingConfig = Field(default_factory=FlowMatchingConfig)
779
+ wandb: WandbConfig = Field(default_factory=WandbConfig)
780
+
781
+ # General configuration
782
+ seed: int = Field(
783
+ default=42,
784
+ description="Random seed for reproducibility",
785
+ )
786
+
787
+ output_dir: str = Field(
788
+ default="outputs",
789
+ description="Directory to save model outputs",
790
+ )
791
+
792
+ # noinspection PyNestedDecorators
793
+ @field_validator("output_dir")
794
+ @classmethod
795
+ def expand_output_path(cls, v: str) -> str:
796
+ """Expand user home directory in output path."""
797
+ return str(Path(v).expanduser().resolve())
798
+
799
+ def _validate_data_dirs_exist(self) -> None:
800
+ """Verify that every directory declared by the training strategy exists under the data root."""
801
+ data_root = Path(self.data.preprocessed_data_root)
802
+ for dir_name in self.training_strategy.get_data_sources():
803
+ dir_path = data_root / dir_name
804
+ if not dir_path.is_dir():
805
+ raise ValueError(
806
+ f"Required data directory '{dir_name}' does not exist under preprocessed_data_root: {dir_path}"
807
+ )
808
+
809
+ @model_validator(mode="after")
810
+ def validate_strategy_compatibility(self) -> "LtxTrainerConfig":
811
+ """Validate that training strategy and other configurations are compatible."""
812
+ self._validate_data_dirs_exist()
813
+
814
+ # Check that reference videos are provided when using video_to_video strategy
815
+ if self.training_strategy.name == "video_to_video" and self.validation.interval:
816
+ has_reference = bool(self.validation.reference_videos) or any(
817
+ cond.type == "reference" for sample in self.validation.samples for cond in sample.conditions
818
+ )
819
+ if not has_reference:
820
+ raise ValueError(
821
+ "reference_videos or samples with reference conditions must be provided "
822
+ "in validation config when using video_to_video strategy"
823
+ )
824
+
825
+ # Check that LoRA config is provided when training mode is lora
826
+ if self.model.training_mode == "lora" and self.lora is None:
827
+ raise ValueError("LoRA configuration must be provided when training_mode is 'lora'")
828
+
829
+ # Check that LoRA config is provided when using video_to_video strategy
830
+ if self.training_strategy.name == "video_to_video" and self.model.training_mode != "lora":
831
+ raise ValueError("Training mode must be 'lora' when using video_to_video strategy")
832
+
833
+ return self
packages/ltx-trainer/src/ltx_trainer/config_display.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Display utilities for training configuration.
2
+ This module provides formatted console output for LtxTrainerConfig.
3
+ """
4
+
5
+ from rich import box
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from ltx_trainer.config import LtxTrainerConfig
10
+
11
+
12
+ def print_config(config: LtxTrainerConfig) -> None:
13
+ """Print configuration as a nicely formatted table with sections."""
14
+
15
+ def fmt(v: object, max_len: int = 55) -> str:
16
+ """Format any value for display."""
17
+ if v is None:
18
+ return "[dim]—[/]"
19
+ if isinstance(v, bool):
20
+ return "[green]✓[/]" if v else "[dim]✗[/]"
21
+ if isinstance(v, (list, tuple)):
22
+ if not v:
23
+ return "[dim]—[/]"
24
+ return ", ".join(str(x) for x in v)
25
+ s = str(v)
26
+ return s[: max_len - 3] + "..." if len(s) > max_len else s
27
+
28
+ cfg = config
29
+ opt = cfg.optimization
30
+ val = cfg.validation
31
+ accel = cfg.acceleration
32
+
33
+ # Build sections: list of (section_title, [(key, value), ...])
34
+ sections: list[tuple[str, list[tuple[str, str]]]] = [
35
+ (
36
+ "🎬 Model",
37
+ [
38
+ ("Base", fmt(cfg.model.model_path)),
39
+ ("Text Encoder", fmt(cfg.model.text_encoder_path) or "[dim]Built-in[/]"),
40
+ ("Training Mode", f"[bold green]{cfg.model.training_mode.upper()}[/]"),
41
+ ("Load Checkpoint", fmt(cfg.model.load_checkpoint) if cfg.model.load_checkpoint else "[dim]—[/]"),
42
+ ],
43
+ ),
44
+ ]
45
+
46
+ if cfg.lora:
47
+ sections.append(
48
+ (
49
+ "🔗 LoRA",
50
+ [
51
+ ("Rank / Alpha", f"{cfg.lora.rank} / {cfg.lora.alpha}"),
52
+ ("Dropout", str(cfg.lora.dropout)),
53
+ ("Target Modules", fmt(cfg.lora.target_modules)),
54
+ ],
55
+ )
56
+ )
57
+
58
+ # Strategy section - include strategy-specific fields
59
+ strategy_items: list[tuple[str, str]] = [("Name", cfg.training_strategy.name)]
60
+ if hasattr(cfg.training_strategy, "with_audio"):
61
+ strategy_items.append(("Audio", fmt(cfg.training_strategy.with_audio)))
62
+ if hasattr(cfg.training_strategy, "first_frame_conditioning_p"):
63
+ strategy_items.append(("First Frame Cond P", str(cfg.training_strategy.first_frame_conditioning_p)))
64
+
65
+ sections.append(("🎯 Strategy", strategy_items))
66
+
67
+ sections.extend(
68
+ [
69
+ (
70
+ "⚡ Optimization",
71
+ [
72
+ ("Steps", f"[bold]{opt.steps:,}[/]"),
73
+ ("Learning Rate", f"{opt.learning_rate:.2e}"),
74
+ ("Batch Size", str(opt.batch_size)),
75
+ ("Grad Accumulation", str(opt.gradient_accumulation_steps)),
76
+ ("Optimizer", opt.optimizer_type),
77
+ ("Scheduler", opt.scheduler_type),
78
+ ("Max Grad Norm", str(opt.max_grad_norm)),
79
+ ("Grad Checkpointing", fmt(opt.enable_gradient_checkpointing)),
80
+ ],
81
+ ),
82
+ (
83
+ "🚀 Acceleration",
84
+ [
85
+ ("Mixed Precision", accel.mixed_precision_mode or "[dim]—[/]"),
86
+ ("Quantization", str(accel.quantization) if accel.quantization else "[dim]—[/]"),
87
+ ("Text Encoder 8bit", fmt(accel.load_text_encoder_in_8bit)),
88
+ ("Optimizer CPU Offload", fmt(accel.offload_optimizer_during_validation)),
89
+ ],
90
+ ),
91
+ (
92
+ "🎥 Validation",
93
+ [
94
+ ("Prompts", f"{len(val.prompts)} prompt(s)" if val.prompts else "[dim]—[/]"),
95
+ ("Interval", f"Every {val.interval} steps" if val.interval else "[dim]Disabled[/]"),
96
+ ("Video Dims", f"{val.video_dims[0]}x{val.video_dims[1]}, {val.video_dims[2]} frames"),
97
+ ("Frame Rate", f"{val.frame_rate} fps"),
98
+ ("Inference Steps", str(val.inference_steps)),
99
+ ("CFG Scale", str(val.guidance_scale)),
100
+ (
101
+ "STG",
102
+ f"scale={val.stg_scale}; blocks={fmt(val.stg_blocks)}; mode={val.stg_mode}"
103
+ if val.stg_scale > 0
104
+ else "[dim]Disabled[/]",
105
+ ),
106
+ ("Seed", str(val.seed)),
107
+ ],
108
+ ),
109
+ (
110
+ "📂 Data & Output",
111
+ [
112
+ ("Dataset", fmt(cfg.data.preprocessed_data_root)),
113
+ ("Dataloader Workers", str(cfg.data.num_dataloader_workers)),
114
+ ("Output Dir", fmt(cfg.output_dir)),
115
+ ("Seed", str(cfg.seed)),
116
+ ],
117
+ ),
118
+ (
119
+ "🔌 Integrations",
120
+ [
121
+ (
122
+ "Checkpoints",
123
+ f"Every {cfg.checkpoints.interval} steps (keep {cfg.checkpoints.keep_last_n})"
124
+ if cfg.checkpoints.interval
125
+ else "[dim]Disabled[/]",
126
+ ),
127
+ ("W&B", f"{cfg.wandb.project}" if cfg.wandb.enabled else "[dim]Disabled[/]"),
128
+ ("HF Hub", cfg.hub.hub_model_id if cfg.hub.push_to_hub else "[dim]Disabled[/]"),
129
+ ],
130
+ ),
131
+ ]
132
+ )
133
+
134
+ # Build table with section headers
135
+ table = Table(
136
+ title="[bold]⚙️ Training Configuration[/]",
137
+ show_header=False,
138
+ box=box.ROUNDED,
139
+ border_style="bright_blue",
140
+ padding=(0, 1),
141
+ title_style="bold bright_blue",
142
+ )
143
+ table.add_column("Key", style="white", width=20)
144
+ table.add_column("Value", style="cyan")
145
+
146
+ for i, (section_title, items) in enumerate(sections):
147
+ if i > 0:
148
+ table.add_row("", "") # Blank line between sections
149
+ table.add_row(f"[bold yellow]{section_title}[/]", "")
150
+ for key, value in items:
151
+ table.add_row(f" {key}", value)
152
+
153
+ console = Console()
154
+ console.print()
155
+ console.print(table)
156
+ console.print()
packages/ltx-trainer/src/ltx_trainer/datasets.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from concurrent.futures import ThreadPoolExecutor
2
+ from pathlib import Path
3
+
4
+ import torch
5
+ from einops import rearrange
6
+ from torch import Tensor
7
+ from torch.utils.data import Dataset
8
+
9
+ from ltx_trainer import logger
10
+
11
+ # Constants for precomputed data directories
12
+ PRECOMPUTED_DIR_NAME = ".precomputed"
13
+
14
+
15
+ class DummyDataset(Dataset):
16
+ """Produce random latents and prompt embeddings. For minimal demonstration and benchmarking purposes"""
17
+
18
+ def __init__(
19
+ self,
20
+ width: int = 1024,
21
+ height: int = 1024,
22
+ num_frames: int = 25,
23
+ fps: int = 24,
24
+ dataset_length: int = 200,
25
+ latent_dim: int = 128,
26
+ latent_spatial_compression_ratio: int = 32,
27
+ latent_temporal_compression_ratio: int = 8,
28
+ prompt_embed_dim: int = 4096,
29
+ prompt_sequence_length: int = 256,
30
+ ) -> None:
31
+ if width % 32 != 0:
32
+ raise ValueError(f"Width must be divisible by 32, got {width=}")
33
+
34
+ if height % 32 != 0:
35
+ raise ValueError(f"Height must be divisible by 32, got {height=}")
36
+
37
+ if num_frames % 8 != 1:
38
+ raise ValueError(f"Number of frames must have a remainder of 1 when divided by 8, got {num_frames=}")
39
+
40
+ self.width = width
41
+ self.height = height
42
+ self.num_frames = num_frames
43
+ self.fps = fps
44
+ self.dataset_length = dataset_length
45
+ self.latent_dim = latent_dim
46
+ self.num_latent_frames = (num_frames - 1) // latent_temporal_compression_ratio + 1
47
+ self.latent_height = height // latent_spatial_compression_ratio
48
+ self.latent_width = width // latent_spatial_compression_ratio
49
+ self.latent_sequence_length = self.num_latent_frames * self.latent_height * self.latent_width
50
+ self.prompt_embed_dim = prompt_embed_dim
51
+ self.prompt_sequence_length = prompt_sequence_length
52
+
53
+ def __len__(self) -> int:
54
+ return self.dataset_length
55
+
56
+ def __getitem__(self, idx: int) -> dict[str, dict[str, Tensor]]:
57
+ return {
58
+ "latent_conditions": {
59
+ "latents": torch.randn(
60
+ self.latent_dim,
61
+ self.num_latent_frames,
62
+ self.latent_height,
63
+ self.latent_width,
64
+ ),
65
+ "num_frames": self.num_latent_frames,
66
+ "height": self.latent_height,
67
+ "width": self.latent_width,
68
+ "fps": self.fps,
69
+ },
70
+ "text_conditions": {
71
+ "video_prompt_embeds": torch.randn(
72
+ self.prompt_sequence_length,
73
+ self.prompt_embed_dim,
74
+ ),
75
+ "audio_prompt_embeds": torch.randn(
76
+ self.prompt_sequence_length,
77
+ self.prompt_embed_dim,
78
+ ),
79
+ "prompt_attention_mask": torch.ones(
80
+ self.prompt_sequence_length,
81
+ dtype=torch.bool,
82
+ ),
83
+ },
84
+ }
85
+
86
+
87
+ class PrecomputedDataset(Dataset):
88
+ def __init__(self, data_root: str, data_sources: dict[str, str] | list[str] | None = None) -> None:
89
+ """
90
+ Generic dataset for loading precomputed data from multiple sources.
91
+ Args:
92
+ data_root: Root directory containing preprocessed data
93
+ data_sources: Either:
94
+ - Dict mapping directory names to output keys
95
+ - List of directory names (keys will equal values)
96
+ - None (defaults to ["latents", "conditions"])
97
+ Example:
98
+ # Standard mode (list)
99
+ dataset = PrecomputedDataset("data/", ["latents", "conditions"])
100
+ # Standard mode (dict)
101
+ dataset = PrecomputedDataset("data/", {"latents": "latent_conditions", "conditions": "text_conditions"})
102
+ # IC-LoRA mode
103
+ dataset = PrecomputedDataset("data/", ["latents", "conditions", "reference_latents"])
104
+ Note:
105
+ Latents are always returned in non-patchified format [C, F, H, W].
106
+ Legacy patchified format [seq_len, C] is automatically converted.
107
+ """
108
+ super().__init__()
109
+
110
+ self.data_root = self._setup_data_root(data_root)
111
+ self.data_sources = self._normalize_data_sources(data_sources)
112
+ self.source_paths = self._setup_source_paths()
113
+ self.sample_files = self._discover_samples()
114
+ self._validate_setup()
115
+
116
+ @staticmethod
117
+ def _setup_data_root(data_root: str) -> Path:
118
+ """Setup and validate the data root directory."""
119
+ data_root = Path(data_root).expanduser().resolve()
120
+
121
+ if not data_root.exists():
122
+ raise FileNotFoundError(f"Data root directory does not exist: {data_root}")
123
+
124
+ # If the given path is the dataset root, use the precomputed subdirectory
125
+ if (data_root / PRECOMPUTED_DIR_NAME).exists():
126
+ data_root = data_root / PRECOMPUTED_DIR_NAME
127
+
128
+ return data_root
129
+
130
+ @staticmethod
131
+ def _normalize_data_sources(data_sources: dict[str, str] | list[str] | None) -> dict[str, str]:
132
+ """Normalize data_sources input to a consistent dict format."""
133
+ if data_sources is None:
134
+ # Default sources
135
+ return {"latents": "latent_conditions", "conditions": "text_conditions"}
136
+ elif isinstance(data_sources, list):
137
+ # Convert list to dict where keys equal values
138
+ return {source: source for source in data_sources}
139
+ elif isinstance(data_sources, dict):
140
+ return data_sources.copy()
141
+ else:
142
+ raise TypeError(f"data_sources must be dict, list, or None, got {type(data_sources)}")
143
+
144
+ def _setup_source_paths(self) -> dict[str, Path]:
145
+ """Map data source names to their actual directory paths."""
146
+ source_paths = {}
147
+
148
+ for dir_name in self.data_sources:
149
+ source_path = self.data_root / dir_name
150
+ source_paths[dir_name] = source_path
151
+
152
+ # Check that all sources exist.
153
+ if not source_path.exists():
154
+ raise FileNotFoundError(f"Required {dir_name} directory does not exist: {source_path}")
155
+
156
+ return source_paths
157
+
158
+ def _discover_samples(self) -> dict[str, list[Path]]:
159
+ """Discover all valid sample files across all data sources.
160
+ Uses a fast two-pass approach: first globs all sources in parallel to build
161
+ full-path sets in memory, then checks expected paths via set membership.
162
+ This avoids O(N * num_sources) stat calls on networked filesystems while
163
+ correctly handling path remapping (e.g. latent_X.pt -> condition_X.pt).
164
+ """
165
+ if not self.data_sources:
166
+ raise ValueError("No data sources configured")
167
+
168
+ data_key = "latents" if "latents" in self.data_sources else next(iter(self.data_sources.keys()))
169
+ data_path = self.source_paths[data_key]
170
+
171
+ # Pass 1: Glob all sources in parallel, build full-path sets
172
+ def _glob_source(dir_name: str) -> tuple[list[Path], set[str]]:
173
+ source_path = self.source_paths[dir_name]
174
+ paths = list(source_path.glob("**/*.pt"))
175
+ path_set = {str(p) for p in paths}
176
+ return paths, path_set
177
+
178
+ with ThreadPoolExecutor(max_workers=len(self.data_sources)) as executor:
179
+ glob_results = dict(
180
+ zip(
181
+ self.data_sources.keys(),
182
+ executor.map(_glob_source, self.data_sources.keys()),
183
+ strict=True,
184
+ )
185
+ )
186
+
187
+ # Get primary source files (cached from glob, no second scan)
188
+ data_files, _ = glob_results[data_key]
189
+ if not data_files:
190
+ raise ValueError(f"No data files found in {data_path}")
191
+ data_files.sort()
192
+
193
+ # Log source sizes
194
+ for dir_name, (paths, _) in glob_results.items():
195
+ logger.debug(f"Source {dir_name}: {len(paths)} files")
196
+
197
+ # Build path sets for non-primary sources
198
+ other_path_sets = {
199
+ dir_name: path_set for dir_name, (_, path_set) in glob_results.items() if dir_name != data_key
200
+ }
201
+
202
+ # Pass 2: For each primary file, check if expected paths exist in other sources' sets
203
+ sample_files: dict[str, list[Path]] = {output_key: [] for output_key in self.data_sources.values()}
204
+ valid_count = 0
205
+
206
+ for data_file in data_files:
207
+ rel_path = data_file.relative_to(data_path)
208
+
209
+ # Check all other sources via set lookup (O(1) per source, no stat calls)
210
+ all_exist = True
211
+ for dir_name, path_set in other_path_sets.items():
212
+ expected = self._get_expected_file_path(dir_name, data_file, rel_path)
213
+ if str(expected) not in path_set:
214
+ logger.debug(f"Skipping {data_file.name}: no matching {dir_name} file at {expected}")
215
+ all_exist = False
216
+ break
217
+
218
+ if all_exist:
219
+ self._fill_sample_data_files(data_file, rel_path, sample_files)
220
+ valid_count += 1
221
+
222
+ skipped = len(data_files) - valid_count
223
+ if skipped > 0:
224
+ logger.info(f"Fast index: {valid_count} valid samples from {len(data_files)} total ({skipped} skipped)")
225
+ else:
226
+ logger.debug(f"Fast index: {valid_count} valid samples from {len(data_files)} total")
227
+
228
+ return sample_files
229
+
230
+ def _get_expected_file_path(self, dir_name: str, data_file: Path, rel_path: Path) -> Path:
231
+ """Get the expected file path for a given data source."""
232
+ source_path = self.source_paths[dir_name]
233
+
234
+ # For conditions, handle legacy naming where latent_X.pt maps to condition_X.pt
235
+ if dir_name == "conditions" and data_file.name.startswith("latent_"):
236
+ return source_path / f"condition_{data_file.stem[7:]}.pt"
237
+
238
+ return source_path / rel_path
239
+
240
+ def _fill_sample_data_files(self, data_file: Path, rel_path: Path, sample_files: dict[str, list[Path]]) -> None:
241
+ """Add a valid sample to the sample_files tracking."""
242
+ for dir_name, output_key in self.data_sources.items():
243
+ expected_path = self._get_expected_file_path(dir_name, data_file, rel_path)
244
+ sample_files[output_key].append(expected_path.relative_to(self.source_paths[dir_name]))
245
+
246
+ def _validate_setup(self) -> None:
247
+ """Validate that the dataset setup is correct."""
248
+ sample_counts = {key: len(files) for key, files in self.sample_files.items()}
249
+ if not sample_counts or all(count == 0 for count in sample_counts.values()):
250
+ raise ValueError(
251
+ f"No valid samples found in {self.data_root} - all configured data sources "
252
+ f"({list(self.data_sources)}) must have matching files (per-source counts: {sample_counts})"
253
+ )
254
+
255
+ # Verify all output keys have the same number of samples
256
+ if len(set(sample_counts.values())) > 1:
257
+ raise ValueError(f"Mismatched sample counts across sources: {sample_counts}")
258
+
259
+ def __len__(self) -> int:
260
+ # Use the first output key as reference count
261
+ first_key = next(iter(self.sample_files.keys()))
262
+ return len(self.sample_files[first_key])
263
+
264
+ def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
265
+ result = {}
266
+
267
+ for dir_name, output_key in self.data_sources.items():
268
+ source_path = self.source_paths[dir_name]
269
+ file_rel_path = self.sample_files[output_key][index]
270
+ file_path = source_path / file_rel_path
271
+
272
+ try:
273
+ data = torch.load(file_path, map_location="cpu", weights_only=True)
274
+
275
+ # Normalize video latent format if this is a latent source
276
+ if "latent" in dir_name.lower():
277
+ data = self._normalize_video_latents(data)
278
+
279
+ result[output_key] = data
280
+ except Exception as e:
281
+ raise RuntimeError(f"Failed to load {output_key} from {file_path}: {e}") from e
282
+
283
+ # Add index for debugging
284
+ result["idx"] = index
285
+ return result
286
+
287
+ @staticmethod
288
+ def _normalize_video_latents(data: dict) -> dict:
289
+ """
290
+ Normalize video latents to non-patchified format [C, F, H, W].
291
+ Used for keeping backward compatibility with legacy datasets.
292
+ """
293
+ latents = data["latents"]
294
+
295
+ # Check if latents are in legacy patchified format [seq_len, C]
296
+ if latents.dim() == 2:
297
+ # Legacy format: [seq_len, C] where seq_len = F * H * W
298
+ num_frames = data["num_frames"]
299
+ height = data["height"]
300
+ width = data["width"]
301
+
302
+ # Unpatchify: [seq_len, C] -> [C, F, H, W]
303
+ latents = rearrange(
304
+ latents,
305
+ "(f h w) c -> c f h w",
306
+ f=num_frames,
307
+ h=height,
308
+ w=width,
309
+ )
310
+
311
+ # Update the data dict with unpatchified latents
312
+ data = data.copy()
313
+ data["latents"] = latents
314
+
315
+ return data
packages/ltx-trainer/src/ltx_trainer/gemma_8bit.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ruff: noqa: PLC0415
2
+
3
+ """
4
+ 8-bit Gemma text encoder loading utilities.
5
+ This module provides functionality for loading the Gemma text encoder in 8-bit precision
6
+ using bitsandbytes, which significantly reduces GPU memory usage.
7
+ Example usage:
8
+ from ltx_trainer.gemma_8bit import load_8bit_gemma
9
+ text_encoder = load_8bit_gemma(gemma_model_path="/path/to/gemma")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ from collections.abc import Generator
17
+ from contextlib import contextmanager
18
+ from pathlib import Path
19
+
20
+ import torch
21
+
22
+ from ltx_core.text_encoders.gemma.encoders.base_encoder import GemmaTextEncoder
23
+ from ltx_core.text_encoders.gemma.tokenizer import LTXVGemmaTokenizer
24
+
25
+
26
+ def load_8bit_gemma(
27
+ gemma_model_path: str | Path,
28
+ dtype: torch.dtype = torch.bfloat16,
29
+ device: torch.device | str | int | None = None,
30
+ ) -> GemmaTextEncoder:
31
+ """Load the Gemma text encoder in 8-bit precision using bitsandbytes.
32
+ Only the Gemma LLM backbone is loaded here. The embeddings processor
33
+ (feature extractor + connectors) should be loaded separately via
34
+ :func:`ltx_trainer.model_loader.load_embeddings_processor`.
35
+ Args:
36
+ gemma_model_path: Path to Gemma model directory
37
+ dtype: Data type for non-quantized model weights
38
+ device: Device to place the quantized model on. When ``None`` (default),
39
+ the device is inferred from ``LOCAL_RANK`` if CUDA is available, so
40
+ multi-process launches put each rank's encoder on its own GPU
41
+ instead of all colliding on ``cuda:0``.
42
+ Returns:
43
+ GemmaTextEncoder with 8-bit quantized Gemma backbone
44
+ Raises:
45
+ ImportError: If bitsandbytes is not installed
46
+ FileNotFoundError: If required model files are not found
47
+ """
48
+ try:
49
+ from transformers import BitsAndBytesConfig, Gemma3ForConditionalGeneration
50
+ except ImportError as e:
51
+ raise ImportError(
52
+ "8-bit text encoder loading requires bitsandbytes. Install it with: uv pip install bitsandbytes"
53
+ ) from e
54
+
55
+ gemma_path = _find_gemma_subpath(gemma_model_path, "model*.safetensors")
56
+ tokenizer_path = _find_gemma_subpath(gemma_model_path, "tokenizer.model")
57
+
58
+ # Pin the entire model to a single device. `device_map="auto"` collides on cuda:0
59
+ # in multi-process launches because every rank picks the same default device.
60
+ device_map: str | dict[str, int | str | torch.device]
61
+ if device is not None:
62
+ device_map = {"": device}
63
+ elif torch.cuda.is_available():
64
+ device_map = {"": int(os.environ.get("LOCAL_RANK", "0"))}
65
+ else:
66
+ device_map = "auto"
67
+
68
+ quantization_config = BitsAndBytesConfig(load_in_8bit=True)
69
+ with _suppress_accelerate_memory_warnings():
70
+ gemma_model = Gemma3ForConditionalGeneration.from_pretrained(
71
+ gemma_path,
72
+ quantization_config=quantization_config,
73
+ torch_dtype=torch.bfloat16,
74
+ device_map=device_map,
75
+ local_files_only=True,
76
+ )
77
+
78
+ tokenizer = LTXVGemmaTokenizer(tokenizer_path, 1024)
79
+
80
+ return GemmaTextEncoder(
81
+ tokenizer=tokenizer,
82
+ model=gemma_model,
83
+ dtype=dtype,
84
+ )
85
+
86
+
87
+ def _find_gemma_subpath(root_path: str | Path, pattern: str) -> str:
88
+ """Find a file matching a glob pattern and return its parent directory."""
89
+ matches = list(Path(root_path).rglob(pattern))
90
+ if not matches:
91
+ raise FileNotFoundError(f"No files matching pattern '{pattern}' found under {root_path}")
92
+ return str(matches[0].parent)
93
+
94
+
95
+ @contextmanager
96
+ def _suppress_accelerate_memory_warnings() -> Generator[None, None, None]:
97
+ """Temporarily suppress INFO warnings from accelerate about memory allocation."""
98
+ accelerate_logger = logging.getLogger("accelerate.utils.modeling")
99
+ old_level = accelerate_logger.level
100
+ accelerate_logger.setLevel(logging.WARNING)
101
+ try:
102
+ yield
103
+ finally:
104
+ accelerate_logger.setLevel(old_level)
packages/ltx-trainer/src/ltx_trainer/gpu_utils.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GPU memory management utilities for training and inference."""
2
+
3
+ import functools
4
+ import gc
5
+ import subprocess
6
+ from typing import Callable, TypeVar
7
+
8
+ import torch
9
+
10
+ from ltx_trainer import logger
11
+
12
+ F = TypeVar("F", bound=Callable)
13
+
14
+
15
+ def free_gpu_memory(log: bool = False) -> None:
16
+ """Free GPU memory by running garbage collection and emptying CUDA cache.
17
+ Args:
18
+ log: If True, log memory stats after clearing
19
+ """
20
+ gc.collect()
21
+ if torch.cuda.is_available():
22
+ torch.cuda.empty_cache()
23
+ if log:
24
+ allocated = torch.cuda.memory_allocated() / 1024**3
25
+ reserved = torch.cuda.memory_reserved() / 1024**3
26
+ logger.debug(f"GPU memory freed. Allocated: {allocated:.2f}GB, Reserved: {reserved:.2f}GB")
27
+
28
+
29
+ class free_gpu_memory_context: # noqa: N801
30
+ """Context manager and decorator to free GPU memory before and/or after execution.
31
+ Can be used as a decorator:
32
+ @free_gpu_memory_context(after=True)
33
+ def my_function():
34
+ ...
35
+ Or as a context manager:
36
+ with free_gpu_memory_context():
37
+ heavy_operation()
38
+ Args:
39
+ before: Free memory before execution (default: False)
40
+ after: Free memory after execution (default: True)
41
+ log: Log memory stats when freeing (default: False)
42
+ """
43
+
44
+ def __init__(self, *, before: bool = False, after: bool = True, log: bool = False) -> None:
45
+ self.before = before
46
+ self.after = after
47
+ self.log = log
48
+
49
+ def __enter__(self) -> "free_gpu_memory_context":
50
+ if self.before:
51
+ free_gpu_memory(log=self.log)
52
+ return self
53
+
54
+ def __exit__(self, exc_type: type | None, exc_val: Exception | None, exc_tb: object) -> None:
55
+ if self.after:
56
+ free_gpu_memory(log=self.log)
57
+
58
+ def __call__(self, func: F) -> F:
59
+ @functools.wraps(func)
60
+ def wrapper(*args, **kwargs) -> object:
61
+ with self:
62
+ return func(*args, **kwargs)
63
+
64
+ return wrapper # type: ignore
65
+
66
+
67
+ def get_gpu_memory_gb(device: torch.device) -> float:
68
+ """Get current GPU memory usage in GB using nvidia-smi.
69
+ Args:
70
+ device: torch.device to get memory usage for
71
+ Returns:
72
+ Current GPU memory usage in GB
73
+ """
74
+ try:
75
+ device_id = device.index if device.index is not None else 0
76
+ result = subprocess.check_output(
77
+ [
78
+ "nvidia-smi",
79
+ "--query-gpu=memory.used",
80
+ "--format=csv,nounits,noheader",
81
+ "-i",
82
+ str(device_id),
83
+ ],
84
+ encoding="utf-8",
85
+ )
86
+ return float(result.strip()) / 1024 # Convert MB to GB
87
+ except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
88
+ logger.error(f"Failed to get GPU memory from nvidia-smi: {e}")
89
+ # Fallback to torch
90
+ return torch.cuda.memory_allocated(device) / 1024**3
packages/ltx-trainer/src/ltx_trainer/hf_hub_utils.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import List, Optional, Union
5
+
6
+ import imageio
7
+ from huggingface_hub import HfApi, create_repo
8
+ from huggingface_hub.utils import are_progress_bars_disabled, disable_progress_bars, enable_progress_bars
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+
11
+ from ltx_trainer import logger
12
+ from ltx_trainer.config import LtxTrainerConfig
13
+
14
+
15
+ def push_to_hub(
16
+ weights_path: Path,
17
+ sampled_videos_paths: Optional[List[Path]],
18
+ config: LtxTrainerConfig,
19
+ ) -> None:
20
+ """Push the trained LoRA weights to HuggingFace Hub."""
21
+ if not config.hub.hub_model_id:
22
+ logger.warning("⚠️ HuggingFace hub_model_id not specified, skipping push to hub")
23
+ return
24
+
25
+ api = HfApi()
26
+
27
+ # Save original progress bar state
28
+ original_progress_state = are_progress_bars_disabled()
29
+ disable_progress_bars() # Disable during our custom progress tracking
30
+
31
+ try:
32
+ # Try to create repo if it doesn't exist
33
+ try:
34
+ repo = create_repo(
35
+ repo_id=config.hub.hub_model_id,
36
+ repo_type="model",
37
+ exist_ok=True, # Don't raise error if repo exists
38
+ )
39
+ repo_id = repo.repo_id
40
+ logger.info(f"🤗 Successfully created HuggingFace model repository at: {repo.url}")
41
+ except Exception as e:
42
+ logger.error(f"❌ Failed to create HuggingFace model repository: {e}")
43
+ return
44
+
45
+ # Create a single temporary directory for all files
46
+ with tempfile.TemporaryDirectory() as temp_dir:
47
+ temp_path = Path(temp_dir)
48
+
49
+ with Progress(
50
+ SpinnerColumn(),
51
+ TextColumn("[progress.description]{task.description}"),
52
+ transient=True,
53
+ ) as progress:
54
+ try:
55
+ # Copy weights
56
+ task_copy = progress.add_task("Copying weights...", total=None)
57
+ weights_dest = temp_path / weights_path.name
58
+ shutil.copy2(weights_path, weights_dest)
59
+ progress.update(task_copy, description="✓ Weights copied")
60
+
61
+ # Create model card and save samples
62
+ task_card = progress.add_task("Creating model card and samples...", total=None)
63
+ _create_model_card(
64
+ output_dir=temp_path,
65
+ videos=sampled_videos_paths,
66
+ config=config,
67
+ )
68
+ progress.update(task_card, description="✓ Model card and samples created")
69
+
70
+ # Upload everything at once
71
+ task_upload = progress.add_task("Pushing files to HuggingFace Hub...", total=None)
72
+ api.upload_folder(
73
+ folder_path=str(temp_path),
74
+ repo_id=repo_id,
75
+ repo_type="model",
76
+ )
77
+ progress.update(task_upload, description="✓ Files pushed to HuggingFace Hub")
78
+ logger.info("✅ Successfully pushed files to HuggingFace Hub")
79
+
80
+ except Exception as e:
81
+ logger.error(f"❌ Failed to process and push files to HuggingFace Hub: {e}")
82
+ raise # Re-raise to handle in outer try block
83
+
84
+ finally:
85
+ # Restore original progress bar state
86
+ if not original_progress_state:
87
+ enable_progress_bars()
88
+
89
+
90
+ def convert_video_to_gif(video_path: Path, output_path: Path) -> None:
91
+ """Convert a video file to GIF format."""
92
+ try:
93
+ # Read the video file
94
+ reader = imageio.get_reader(str(video_path))
95
+ fps = reader.get_meta_data()["fps"]
96
+
97
+ # Write GIF file with infinite loop
98
+ writer = imageio.get_writer(
99
+ str(output_path),
100
+ fps=min(fps, 15), # Cap FPS at 15 for reasonable file size
101
+ loop=0, # 0 means infinite loop
102
+ )
103
+
104
+ for frame in reader:
105
+ writer.append_data(frame)
106
+
107
+ writer.close()
108
+ reader.close()
109
+ except Exception as e:
110
+ logger.error(f"Failed to convert video to GIF: {e}")
111
+
112
+
113
+ def _create_model_card(
114
+ output_dir: Union[str, Path],
115
+ videos: Optional[List[Path]],
116
+ config: LtxTrainerConfig,
117
+ ) -> Path:
118
+ """Generate and save a model card for the trained model."""
119
+
120
+ repo_id = config.hub.hub_model_id
121
+ pretrained_model_name_or_path = config.model.model_path
122
+ validation_prompts = config.validation.prompts
123
+ output_dir = Path(output_dir)
124
+ template_path = Path(__file__).parent.parent.parent / "templates" / "model_card.md"
125
+
126
+ # Read the template
127
+ template = template_path.read_text()
128
+
129
+ # Get model name from repo_id
130
+ model_name = repo_id.split("/")[-1]
131
+
132
+ # Get base model information
133
+ base_model_link = str(pretrained_model_name_or_path)
134
+ model_path_str = str(pretrained_model_name_or_path)
135
+ is_url = model_path_str.startswith(("http://", "https://"))
136
+
137
+ # For URLs, extract the filename from the URL. For local paths, use the filename stem
138
+ base_model_name = model_path_str.split("/")[-1] if is_url else Path(pretrained_model_name_or_path).name
139
+
140
+ # Format validation prompts and create grid layout
141
+ prompts_text = ""
142
+ sample_grid = []
143
+
144
+ if validation_prompts and videos:
145
+ prompts_text = "Example prompts used during validation:\n\n"
146
+
147
+ # Create samples directory
148
+ samples_dir = output_dir / "samples"
149
+ samples_dir.mkdir(exist_ok=True, parents=True)
150
+
151
+ # Process videos and create cells
152
+ cells = []
153
+ for i, (prompt, video) in enumerate(zip(validation_prompts, videos, strict=False)):
154
+ if video.exists():
155
+ # Add prompt to text section
156
+ prompts_text += f"- `{prompt}`\n"
157
+
158
+ # Convert video to GIF
159
+ gif_path = samples_dir / f"sample_{i}.gif"
160
+ try:
161
+ convert_video_to_gif(video, gif_path)
162
+
163
+ # Create grid cell with collapsible description
164
+ cell = (
165
+ f"![example{i + 1}](./samples/sample_{i}.gif)"
166
+ "<br>"
167
+ '<details style="max-width: 300px; margin: auto;">'
168
+ f"<summary>Prompt</summary>"
169
+ f"{prompt}"
170
+ "</details>"
171
+ )
172
+ cells.append(cell)
173
+ except Exception as e:
174
+ logger.error(f"Failed to process video {video}: {e}")
175
+
176
+ # Calculate optimal grid dimensions
177
+ num_cells = len(cells)
178
+ if num_cells > 0:
179
+ # Aim for a roughly square grid, with max 4 columns
180
+ num_cols = min(4, num_cells)
181
+ num_rows = (num_cells + num_cols - 1) // num_cols # Ceiling division
182
+
183
+ # Create grid rows
184
+ for row in range(num_rows):
185
+ start_idx = row * num_cols
186
+ end_idx = min(start_idx + num_cols, num_cells)
187
+ row_cells = cells[start_idx:end_idx]
188
+ # Properly format the row with table markers and exact number of cells
189
+ formatted_row = "| " + " | ".join(row_cells) + " |"
190
+ sample_grid.append(formatted_row)
191
+
192
+ # Join grid rows with just the content, no headers needed
193
+ grid_text = "\n".join(sample_grid) if sample_grid else ""
194
+
195
+ # Fill in the template
196
+ model_card_content = template.format(
197
+ base_model=base_model_name,
198
+ base_model_link=base_model_link,
199
+ model_name=model_name,
200
+ training_type="LoRA fine-tuning" if config.model.training_mode == "lora" else "Full model fine-tuning",
201
+ training_steps=config.optimization.steps,
202
+ learning_rate=config.optimization.learning_rate,
203
+ batch_size=config.optimization.batch_size,
204
+ validation_prompts=prompts_text,
205
+ sample_grid=grid_text,
206
+ )
207
+
208
+ # Save the model card directly
209
+ model_card_path = output_dir / "README.md"
210
+ model_card_path.write_text(model_card_content)
211
+
212
+ return model_card_path
packages/ltx-trainer/src/ltx_trainer/model_loader.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ruff: noqa: PLC0415
2
+
3
+ """
4
+ Model loader for LTX-2 trainer using the new ltx-core package.
5
+ This module provides a unified interface for loading LTX-2 model components
6
+ for training, using SingleGPUModelBuilder from ltx-core.
7
+ Example usage:
8
+ # Load individual components
9
+ vae_encoder = load_video_vae_encoder("/path/to/checkpoint.safetensors", device="cuda")
10
+ vae_decoder = load_video_vae_decoder("/path/to/checkpoint.safetensors", device="cuda")
11
+ text_encoder = load_text_encoder("/path/to/gemma", device="cuda")
12
+ # Load all components at once
13
+ components = load_model("/path/to/checkpoint.safetensors", text_encoder_path="/path/to/gemma")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+
22
+ import torch
23
+
24
+ from ltx_trainer import logger
25
+
26
+ # Type alias for device specification
27
+ Device = str | torch.device
28
+
29
+ # Type checking imports (not loaded at runtime)
30
+ if TYPE_CHECKING:
31
+ from ltx_core.components.schedulers import LTX2Scheduler
32
+ from ltx_core.model.audio_vae import AudioDecoder, AudioEncoder, Vocoder
33
+ from ltx_core.model.transformer import LTXModel
34
+ from ltx_core.model.video_vae import VideoDecoder, VideoEncoder
35
+ from ltx_core.text_encoders.gemma import GemmaTextEncoder
36
+ from ltx_core.text_encoders.gemma.embeddings_processor import EmbeddingsProcessor
37
+
38
+
39
+ def _to_torch_device(device: Device) -> torch.device:
40
+ """Convert device specification to torch.device."""
41
+ return torch.device(device) if isinstance(device, str) else device
42
+
43
+
44
+ # =============================================================================
45
+ # Individual Component Loaders
46
+ # =============================================================================
47
+
48
+
49
+ def load_transformer(
50
+ checkpoint_path: str | Path,
51
+ device: Device = "cpu",
52
+ dtype: torch.dtype = torch.bfloat16,
53
+ ) -> "LTXModel":
54
+ """Load the LTX transformer model.
55
+ Args:
56
+ checkpoint_path: Path to the safetensors checkpoint file
57
+ device: Device to load model on
58
+ dtype: Data type for model weights
59
+ Returns:
60
+ Loaded LTXModel transformer
61
+ """
62
+ from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
63
+ from ltx_core.model.transformer.model_configurator import (
64
+ LTXV_MODEL_COMFY_RENAMING_MAP,
65
+ LTXModelConfigurator,
66
+ )
67
+
68
+ return SingleGPUModelBuilder(
69
+ model_path=str(checkpoint_path),
70
+ model_class_configurator=LTXModelConfigurator,
71
+ model_sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP,
72
+ ).build(device=_to_torch_device(device), dtype=dtype)
73
+
74
+
75
+ def load_video_vae_encoder(
76
+ checkpoint_path: str | Path,
77
+ device: Device = "cpu",
78
+ dtype: torch.dtype = torch.bfloat16,
79
+ ) -> "VideoEncoder":
80
+ """Load the video VAE encoder (for preprocessing).
81
+ Args:
82
+ checkpoint_path: Path to the safetensors checkpoint file
83
+ device: Device to load model on
84
+ dtype: Data type for model weights
85
+ Returns:
86
+ Loaded VideoEncoder
87
+ """
88
+ from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
89
+ from ltx_core.model.video_vae import VAE_ENCODER_COMFY_KEYS_FILTER, VideoEncoderConfigurator
90
+
91
+ return SingleGPUModelBuilder(
92
+ model_path=str(checkpoint_path),
93
+ model_class_configurator=VideoEncoderConfigurator,
94
+ model_sd_ops=VAE_ENCODER_COMFY_KEYS_FILTER,
95
+ ).build(device=_to_torch_device(device), dtype=dtype)
96
+
97
+
98
+ def load_video_vae_decoder(
99
+ checkpoint_path: str | Path,
100
+ device: Device = "cpu",
101
+ dtype: torch.dtype = torch.bfloat16,
102
+ ) -> "VideoDecoder":
103
+ """Load the video VAE decoder (for inference/validation).
104
+ Args:
105
+ checkpoint_path: Path to the safetensors checkpoint file
106
+ device: Device to load model on
107
+ dtype: Data type for model weights
108
+ Returns:
109
+ Loaded VideoDecoder
110
+ """
111
+ from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
112
+ from ltx_core.model.video_vae import VAE_DECODER_COMFY_KEYS_FILTER, VideoDecoderConfigurator
113
+
114
+ return SingleGPUModelBuilder(
115
+ model_path=str(checkpoint_path),
116
+ model_class_configurator=VideoDecoderConfigurator,
117
+ model_sd_ops=VAE_DECODER_COMFY_KEYS_FILTER,
118
+ ).build(device=_to_torch_device(device), dtype=dtype)
119
+
120
+
121
+ def load_audio_vae_encoder(
122
+ checkpoint_path: str | Path,
123
+ device: Device = "cpu",
124
+ dtype: torch.dtype = torch.bfloat16,
125
+ ) -> "AudioEncoder":
126
+ """Load the audio VAE encoder (for preprocessing).
127
+ Args:
128
+ checkpoint_path: Path to the safetensors checkpoint file
129
+ device: Device to load model on
130
+ dtype: Data type for model weights (default bfloat16, but float32 recommended for quality)
131
+ Returns:
132
+ Loaded AudioEncoder
133
+ """
134
+ from ltx_core.loader import SingleGPUModelBuilder
135
+ from ltx_core.model.audio_vae import AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER, AudioEncoderConfigurator
136
+
137
+ return SingleGPUModelBuilder(
138
+ model_path=str(checkpoint_path),
139
+ model_class_configurator=AudioEncoderConfigurator,
140
+ model_sd_ops=AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER,
141
+ ).build(device=_to_torch_device(device), dtype=dtype)
142
+
143
+
144
+ def load_audio_vae_decoder(
145
+ checkpoint_path: str | Path,
146
+ device: Device = "cpu",
147
+ dtype: torch.dtype = torch.bfloat16,
148
+ ) -> "AudioDecoder":
149
+ """Load the audio VAE decoder.
150
+ Args:
151
+ checkpoint_path: Path to the safetensors checkpoint file
152
+ device: Device to load model on
153
+ dtype: Data type for model weights
154
+ Returns:
155
+ Loaded AudioDecoder
156
+ """
157
+ from ltx_core.loader import SingleGPUModelBuilder
158
+ from ltx_core.model.audio_vae import AUDIO_VAE_DECODER_COMFY_KEYS_FILTER, AudioDecoderConfigurator
159
+
160
+ return SingleGPUModelBuilder(
161
+ model_path=str(checkpoint_path),
162
+ model_class_configurator=AudioDecoderConfigurator,
163
+ model_sd_ops=AUDIO_VAE_DECODER_COMFY_KEYS_FILTER,
164
+ ).build(device=_to_torch_device(device), dtype=dtype)
165
+
166
+
167
+ def load_vocoder(
168
+ checkpoint_path: str | Path,
169
+ device: Device = "cpu",
170
+ dtype: torch.dtype = torch.bfloat16,
171
+ ) -> "Vocoder":
172
+ """Load the vocoder (for audio waveform generation).
173
+ Args:
174
+ checkpoint_path: Path to the safetensors checkpoint file
175
+ device: Device to load model on
176
+ dtype: Data type for model weights
177
+ Returns:
178
+ Loaded Vocoder
179
+ """
180
+ from ltx_core.loader import SingleGPUModelBuilder
181
+ from ltx_core.model.audio_vae import VOCODER_COMFY_KEYS_FILTER, VocoderConfigurator
182
+
183
+ return SingleGPUModelBuilder(
184
+ model_path=str(checkpoint_path),
185
+ model_class_configurator=VocoderConfigurator,
186
+ model_sd_ops=VOCODER_COMFY_KEYS_FILTER,
187
+ ).build(device=_to_torch_device(device), dtype=dtype)
188
+
189
+
190
+ def load_text_encoder(
191
+ gemma_model_path: str | Path,
192
+ device: Device = "cpu",
193
+ dtype: torch.dtype = torch.bfloat16,
194
+ load_in_8bit: bool = False,
195
+ ) -> "GemmaTextEncoder":
196
+ """Load the Gemma text encoder.
197
+ Args:
198
+ gemma_model_path: Path to Gemma model directory
199
+ device: Device to load model on
200
+ dtype: Data type for model weights
201
+ load_in_8bit: Whether to load the Gemma model in 8-bit precision using bitsandbytes.
202
+ Returns:
203
+ Loaded GemmaTextEncoder
204
+ """
205
+ if not Path(gemma_model_path).is_dir():
206
+ raise ValueError(f"Gemma model path is not a directory: {gemma_model_path}")
207
+
208
+ # Use 8-bit loading path if requested
209
+ if load_in_8bit:
210
+ from ltx_trainer.gemma_8bit import load_8bit_gemma
211
+
212
+ return load_8bit_gemma(gemma_model_path, dtype, device=device)
213
+
214
+ # Standard loading path
215
+ from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
216
+ from ltx_core.text_encoders.gemma import (
217
+ GEMMA_LLM_KEY_OPS,
218
+ GEMMA_MODEL_OPS,
219
+ GemmaTextEncoderConfigurator,
220
+ module_ops_from_gemma_root,
221
+ )
222
+ from ltx_core.utils import find_matching_file
223
+
224
+ torch_device = _to_torch_device(device)
225
+
226
+ gemma_model_folder = find_matching_file(str(gemma_model_path), "model*.safetensors").parent
227
+ gemma_weight_paths = [str(p) for p in gemma_model_folder.rglob("*.safetensors")]
228
+
229
+ text_encoder = SingleGPUModelBuilder(
230
+ model_path=tuple(gemma_weight_paths),
231
+ model_class_configurator=GemmaTextEncoderConfigurator,
232
+ model_sd_ops=GEMMA_LLM_KEY_OPS,
233
+ module_ops=(GEMMA_MODEL_OPS, *module_ops_from_gemma_root(str(gemma_model_path))),
234
+ ).build(device=torch_device, dtype=dtype)
235
+
236
+ return text_encoder
237
+
238
+
239
+ def load_embeddings_processor(
240
+ checkpoint_path: str | Path,
241
+ device: Device = "cpu",
242
+ dtype: torch.dtype = torch.bfloat16,
243
+ ) -> "EmbeddingsProcessor":
244
+ """Load the embeddings processor (feature extractor + video/audio connectors).
245
+ Args:
246
+ checkpoint_path: Path to the LTX-2 safetensors checkpoint file
247
+ device: Device to load model on
248
+ dtype: Data type for model weights
249
+ Returns:
250
+ Loaded EmbeddingsProcessor with feature extractor and connectors
251
+ """
252
+ from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
253
+ from ltx_core.text_encoders.gemma import (
254
+ EMBEDDINGS_PROCESSOR_KEY_OPS,
255
+ EmbeddingsProcessorConfigurator,
256
+ )
257
+
258
+ torch_device = _to_torch_device(device)
259
+
260
+ return SingleGPUModelBuilder(
261
+ model_path=str(checkpoint_path),
262
+ model_class_configurator=EmbeddingsProcessorConfigurator,
263
+ model_sd_ops=EMBEDDINGS_PROCESSOR_KEY_OPS,
264
+ ).build(device=torch_device, dtype=dtype)
265
+
266
+
267
+ # =============================================================================
268
+ # Combined Component Loader
269
+ # =============================================================================
270
+
271
+
272
+ @dataclass
273
+ class LtxModelComponents:
274
+ """Container for all LTX-2 model components."""
275
+
276
+ transformer: "LTXModel"
277
+ video_vae_encoder: "VideoEncoder | None" = None
278
+ video_vae_decoder: "VideoDecoder | None" = None
279
+ audio_vae_decoder: "AudioDecoder | None" = None
280
+ vocoder: "Vocoder | None" = None
281
+ text_encoder: "GemmaTextEncoder | None" = None
282
+ scheduler: "LTX2Scheduler | None" = None
283
+
284
+
285
+ def load_model(
286
+ checkpoint_path: str | Path,
287
+ text_encoder_path: str | Path | None = None,
288
+ device: Device = "cpu",
289
+ dtype: torch.dtype = torch.bfloat16,
290
+ with_video_vae_encoder: bool = False,
291
+ with_video_vae_decoder: bool = True,
292
+ with_audio_vae_decoder: bool = True,
293
+ with_vocoder: bool = True,
294
+ with_text_encoder: bool = True,
295
+ ) -> LtxModelComponents:
296
+ """
297
+ Load LTX-2 model components from a safetensors checkpoint.
298
+ This is a convenience function that loads multiple components at once.
299
+ For loading individual components, use the dedicated functions:
300
+ - load_transformer()
301
+ - load_video_vae_encoder()
302
+ - load_video_vae_decoder()
303
+ - load_audio_vae_decoder()
304
+ - load_vocoder()
305
+ - load_text_encoder()
306
+ Args:
307
+ checkpoint_path: Path to the safetensors checkpoint file
308
+ text_encoder_path: Path to Gemma model directory (required if with_text_encoder=True)
309
+ device: Device to load models on ("cuda", "cpu", etc.)
310
+ dtype: Data type for model weights
311
+ with_video_vae_encoder: Whether to load the video VAE encoder (for preprocessing)
312
+ with_video_vae_decoder: Whether to load the video VAE decoder (for inference/validation)
313
+ with_audio_vae_decoder: Whether to load the audio VAE decoder
314
+ with_vocoder: Whether to load the vocoder
315
+ with_text_encoder: Whether to load the text encoder
316
+ Returns:
317
+ LtxModelComponents containing all loaded model components
318
+ """
319
+ from ltx_core.components.schedulers import LTX2Scheduler
320
+
321
+ checkpoint_path = Path(checkpoint_path)
322
+
323
+ # Validate checkpoint exists
324
+ if not checkpoint_path.exists():
325
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
326
+
327
+ logger.info(f"Loading LTX-2 model from {checkpoint_path}")
328
+
329
+ torch_device = _to_torch_device(device)
330
+
331
+ # Load transformer
332
+ logger.debug("Loading transformer...")
333
+ transformer = load_transformer(checkpoint_path, torch_device, dtype)
334
+
335
+ # Load video VAE encoder
336
+ video_vae_encoder = None
337
+ if with_video_vae_encoder:
338
+ logger.debug("Loading video VAE encoder...")
339
+ video_vae_encoder = load_video_vae_encoder(checkpoint_path, torch_device, dtype)
340
+
341
+ # Load video VAE decoder
342
+ video_vae_decoder = None
343
+ if with_video_vae_decoder:
344
+ logger.debug("Loading video VAE decoder...")
345
+ video_vae_decoder = load_video_vae_decoder(checkpoint_path, torch_device, dtype)
346
+
347
+ # Load audio VAE decoder
348
+ audio_vae_decoder = None
349
+ if with_audio_vae_decoder:
350
+ logger.debug("Loading audio VAE decoder...")
351
+ audio_vae_decoder = load_audio_vae_decoder(checkpoint_path, torch_device, dtype)
352
+
353
+ # Load vocoder
354
+ vocoder = None
355
+ if with_vocoder:
356
+ logger.debug("Loading vocoder...")
357
+ vocoder = load_vocoder(checkpoint_path, torch_device, dtype)
358
+
359
+ # Load text encoder
360
+ text_encoder = None
361
+ if with_text_encoder:
362
+ if text_encoder_path is None:
363
+ raise ValueError("text_encoder_path must be provided when with_text_encoder=True")
364
+ logger.debug("Loading Gemma text encoder...")
365
+ text_encoder = load_text_encoder(text_encoder_path, torch_device, dtype)
366
+
367
+ # Create scheduler (stateless, no loading needed)
368
+ scheduler = LTX2Scheduler()
369
+
370
+ return LtxModelComponents(
371
+ transformer=transformer,
372
+ video_vae_encoder=video_vae_encoder,
373
+ video_vae_decoder=video_vae_decoder,
374
+ audio_vae_decoder=audio_vae_decoder,
375
+ vocoder=vocoder,
376
+ text_encoder=text_encoder,
377
+ scheduler=scheduler,
378
+ )
packages/ltx-trainer/src/ltx_trainer/progress.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Progress tracking for LTX training.
2
+ This module provides a unified progress display for training and validation sampling,
3
+ encapsulating all Rich progress bar logic in one place.
4
+ """
5
+
6
+ from rich.progress import (
7
+ BarColumn,
8
+ Progress,
9
+ TaskID,
10
+ TextColumn,
11
+ TimeElapsedColumn,
12
+ TimeRemainingColumn,
13
+ )
14
+
15
+
16
+ class SamplingContext:
17
+ """Context for validation sampling progress tracking.
18
+ Provides a unified progress display showing current video and denoising step.
19
+ Display format: "Sampling X/Y [████████████] step Z/W"
20
+ The progress bar shows the denoising progress for the current video.
21
+ """
22
+
23
+ def __init__(self, progress: Progress | None, task: TaskID | None, num_prompts: int, num_steps: int):
24
+ self._progress = progress
25
+ self._task = task
26
+ self._num_prompts = num_prompts
27
+ self._num_steps = num_steps
28
+
29
+ def start_video(self, video_idx: int) -> None:
30
+ """Start tracking a new video (resets step progress)."""
31
+ if self._progress is None or self._task is None:
32
+ return
33
+ # Reset task for new video: completed=0, total=num_steps
34
+ self._progress.reset(self._task, total=self._num_steps)
35
+ self._progress.update(
36
+ self._task,
37
+ completed=0,
38
+ video=f"{video_idx + 1}/{self._num_prompts}",
39
+ info=f"step 0/{self._num_steps}",
40
+ )
41
+
42
+ def advance_step(self) -> None:
43
+ """Advance the denoising step by one."""
44
+ if self._progress is None or self._task is None:
45
+ return
46
+ self._progress.advance(self._task)
47
+ completed = int(self._progress.tasks[self._task].completed)
48
+ self._progress.update(self._task, info=f"step {completed}/{self._num_steps}")
49
+
50
+ def cleanup(self) -> None:
51
+ """Hide sampling task when done."""
52
+ if self._progress is None or self._task is None:
53
+ return
54
+ self._progress.update(self._task, visible=False)
55
+
56
+
57
+ class TrainingProgress:
58
+ """Manages Rich progress display for training and validation.
59
+ This class encapsulates all progress bar logic, providing a clean interface
60
+ for the trainer to update progress without dealing with Rich internals.
61
+ Usage:
62
+ with TrainingProgress(enabled=True, total_steps=1000) as progress:
63
+ for step in range(1000):
64
+ # ... training step ...
65
+ progress.update_training(loss=0.1, lr=1e-4, step_time=0.5)
66
+ if should_validate:
67
+ sampling_ctx = progress.start_sampling(num_prompts=3, num_steps=30)
68
+ sampler = ValidationSampler(..., sampling_context=sampling_ctx)
69
+ for prompt_idx, prompt in enumerate(prompts):
70
+ sampling_ctx.start_video(prompt_idx)
71
+ sampler.generate(...)
72
+ sampling_ctx.cleanup()
73
+ """
74
+
75
+ def __init__(self, enabled: bool, total_steps: int):
76
+ """Initialize progress tracking.
77
+ Args:
78
+ enabled: Whether to display progress bars (False for non-main processes)
79
+ total_steps: Total number of training steps
80
+ """
81
+ self._enabled = enabled
82
+ self._total_steps = total_steps
83
+ self._train_task: TaskID | None = None
84
+
85
+ if not enabled:
86
+ self._progress = None
87
+ return
88
+
89
+ # Single Progress instance with flexible columns
90
+ self._progress = Progress(
91
+ TextColumn("[progress.description]{task.description}"),
92
+ TextColumn("{task.fields[video]}", style="magenta"),
93
+ BarColumn(bar_width=40, style="blue"),
94
+ TextColumn("{task.fields[info]}", style="cyan"),
95
+ TimeElapsedColumn(),
96
+ TextColumn("ETA:"),
97
+ TimeRemainingColumn(compact=True),
98
+ )
99
+
100
+ def __enter__(self) -> "TrainingProgress":
101
+ """Enter the progress context, starting the live display."""
102
+ if self._progress is not None:
103
+ self._progress.__enter__()
104
+ self._train_task = self._progress.add_task(
105
+ "Training",
106
+ total=self._total_steps,
107
+ video=f"0/{self._total_steps}",
108
+ info="Starting...",
109
+ )
110
+ return self
111
+
112
+ def __exit__(self, *args) -> None:
113
+ """Exit the progress context, stopping the live display."""
114
+ if self._progress is not None:
115
+ self._progress.__exit__(*args)
116
+
117
+ @property
118
+ def enabled(self) -> bool:
119
+ """Whether progress display is enabled."""
120
+ return self._enabled
121
+
122
+ def update_training(
123
+ self,
124
+ *,
125
+ loss: float,
126
+ lr: float,
127
+ step_time: float,
128
+ advance: bool = True,
129
+ ) -> None:
130
+ """Update the training progress display.
131
+ Args:
132
+ loss: Current training loss
133
+ lr: Current learning rate
134
+ step_time: Time taken for this step in seconds
135
+ advance: Whether to advance the progress by one step
136
+ """
137
+ if self._progress is None or self._train_task is None:
138
+ return
139
+
140
+ info = f"Loss: {loss:.4f} | LR: {lr:.2e} | {step_time:.2f}s/step"
141
+ self._progress.update(
142
+ self._train_task,
143
+ advance=1 if advance else 0,
144
+ info=info,
145
+ )
146
+ # Update step count in video column
147
+ completed = int(self._progress.tasks[self._train_task].completed)
148
+ self._progress.update(self._train_task, video=f"{completed}/{self._total_steps}")
149
+
150
+ def start_sampling(self, num_prompts: int, num_steps: int) -> SamplingContext:
151
+ """Start validation sampling progress tracking.
152
+ Creates a task that shows current video and denoising step progress.
153
+ Format: "Sampling X/Y [████████████] step Z/W"
154
+ Args:
155
+ num_prompts: Number of validation prompts to sample
156
+ num_steps: Number of denoising steps per sample
157
+ Returns:
158
+ SamplingContext for tracking progress (no-op if progress is disabled)
159
+ """
160
+ if self._progress is None:
161
+ # Return a no-op context when progress is disabled
162
+ return SamplingContext(
163
+ progress=None,
164
+ task=None,
165
+ num_prompts=num_prompts,
166
+ num_steps=num_steps,
167
+ )
168
+
169
+ task = self._progress.add_task(
170
+ "Sampling",
171
+ total=num_steps,
172
+ completed=0,
173
+ video=f"0/{num_prompts}",
174
+ info=f"step 0/{num_steps}",
175
+ )
176
+
177
+ return SamplingContext(
178
+ progress=self._progress,
179
+ task=task,
180
+ num_prompts=num_prompts,
181
+ num_steps=num_steps,
182
+ )
packages/ltx-trainer/src/ltx_trainer/quantization.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Adapted from: https://github.com/bghira/SimpleTuner
2
+ # With improvements from: https://github.com/ostris/ai-toolkit
3
+ from typing import Literal
4
+
5
+ import torch
6
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
7
+
8
+ from ltx_trainer import logger
9
+
10
+ QuantizationOptions = Literal[
11
+ "int8-quanto",
12
+ "int4-quanto",
13
+ "int2-quanto",
14
+ "fp8-quanto",
15
+ "fp8uz-quanto",
16
+ ]
17
+
18
+ # Modules to exclude from quantization.
19
+ # These are glob patterns passed to quanto's `exclude` parameter.
20
+ # When quantizing the full model at once, these patterns match against full module paths.
21
+ # When quantizing block-by-block, we also use SKIP_ROOT_MODULES for top-level modules.
22
+ EXCLUDE_PATTERNS = [
23
+ # Input/output projection layers
24
+ "patchify_proj",
25
+ "audio_patchify_proj",
26
+ "proj_out",
27
+ "audio_proj_out",
28
+ # Timestep embedding layers - int4 tinygemm requires strict bfloat16 input
29
+ # and these receive float32 sinusoidal embeddings that are cast to bfloat16
30
+ "*adaln*",
31
+ "time_proj",
32
+ "timestep_embedder*",
33
+ # Caption/text projection layers
34
+ "caption_projection*",
35
+ "audio_caption_projection*",
36
+ # Normalization layers (usually excluded from quantization)
37
+ "*norm*",
38
+ ]
39
+
40
+ # Top-level modules to skip entirely during block-by-block quantization.
41
+ # These are exact matches against model.named_children() names.
42
+ # (Needed because quanto's exclude patterns don't work when calling quantize() directly on a module)
43
+ SKIP_ROOT_MODULES = {
44
+ "patchify_proj",
45
+ "audio_patchify_proj",
46
+ "proj_out",
47
+ "audio_proj_out",
48
+ "audio_caption_projection",
49
+ }
50
+
51
+
52
+ def quantize_model(
53
+ model: torch.nn.Module,
54
+ precision: QuantizationOptions,
55
+ quantize_activations: bool = False,
56
+ device: torch.device | str | None = None,
57
+ ) -> torch.nn.Module:
58
+ """
59
+ Quantize a model using optimum-quanto.
60
+ For large models with transformer_blocks, this function quantizes block-by-block
61
+ on GPU then moves back to CPU, which is much faster than quantizing on CPU and
62
+ uses less peak VRAM than loading the entire model to GPU at once.
63
+ Args:
64
+ model: The model to quantize.
65
+ precision: The quantization precision (e.g. "int8-quanto", "fp8-quanto").
66
+ quantize_activations: Whether to quantize activations in addition to weights.
67
+ device: Device to use for quantization. If None, uses CUDA if available, else CPU.
68
+ Returns:
69
+ The quantized model.
70
+ """
71
+ from optimum.quanto import freeze, quantize # noqa: PLC0415
72
+
73
+ if device is None:
74
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
75
+ elif isinstance(device, str):
76
+ device = torch.device(device)
77
+
78
+ weight_quant = _get_quanto_dtype(precision)
79
+
80
+ if quantize_activations:
81
+ logger.debug("Quantizing model weights and activations")
82
+ activations_quant = weight_quant
83
+ else:
84
+ activations_quant = None
85
+
86
+ # Remember original device to restore after quantization
87
+ original_device = next(model.parameters()).device
88
+
89
+ # Check if model has transformer_blocks for block-by-block quantization
90
+ if hasattr(model, "transformer_blocks"):
91
+ logger.debug("Quantizing model using block-by-block approach for memory efficiency")
92
+ _quantize_blockwise(
93
+ model,
94
+ weight_quant=weight_quant,
95
+ activations_quant=activations_quant,
96
+ device=device,
97
+ )
98
+ else:
99
+ # Fallback: quantize entire model at once
100
+ model.to(device)
101
+ quantize(model, weights=weight_quant, activations=activations_quant, exclude=EXCLUDE_PATTERNS)
102
+ freeze(model)
103
+
104
+ # Restore model to original device
105
+ model.to(original_device)
106
+
107
+ return model
108
+
109
+
110
+ def _quantize_blockwise(
111
+ model: torch.nn.Module,
112
+ weight_quant: torch.dtype,
113
+ activations_quant: torch.dtype | None,
114
+ device: torch.device,
115
+ ) -> None:
116
+ """Quantize a model block-by-block using optimum-quanto.
117
+ This approach:
118
+ 1. Moves each transformer block to GPU
119
+ 2. Quantizes on GPU (fast!)
120
+ 3. Freezes the quantized weights
121
+ 4. Moves back to CPU
122
+ This is much faster than quantizing on CPU and uses less peak VRAM
123
+ than loading the entire model to GPU.
124
+ """
125
+ from optimum.quanto import freeze, quantize # noqa: PLC0415
126
+
127
+ original_dtype = next(model.parameters()).dtype
128
+ transformer_blocks = list(model.transformer_blocks)
129
+
130
+ with Progress(
131
+ SpinnerColumn(),
132
+ TextColumn("[progress.description]{task.description}"),
133
+ BarColumn(),
134
+ TaskProgressColumn(),
135
+ transient=True,
136
+ ) as progress:
137
+ task = progress.add_task("Quantizing transformer blocks", total=len(transformer_blocks))
138
+
139
+ for block in transformer_blocks:
140
+ # Move block to GPU
141
+ block.to(device, dtype=original_dtype, non_blocking=True)
142
+
143
+ # Quantize on GPU
144
+ quantize(block, weights=weight_quant, activations=activations_quant, exclude=EXCLUDE_PATTERNS)
145
+ freeze(block)
146
+
147
+ # Move back to CPU to free up VRAM for next block
148
+ block.to("cpu", non_blocking=True)
149
+
150
+ progress.advance(task)
151
+
152
+ # Quantize remaining non-transformer-block modules (e.g., embeddings, timestep projections)
153
+ # Skip modules that should not be quantized (patchify_proj, proj_out, etc.)
154
+ logger.debug("Quantizing remaining model components")
155
+
156
+ for name, module in model.named_children():
157
+ if name == "transformer_blocks":
158
+ continue # Already quantized
159
+
160
+ if name in SKIP_ROOT_MODULES:
161
+ logger.debug(f"Skipping quantization for module: {name}")
162
+ continue # Don't quantize these modules
163
+
164
+ # Move to device, quantize, freeze, move back
165
+ module.to(device, dtype=original_dtype, non_blocking=True)
166
+ quantize(module, weights=weight_quant, activations=activations_quant, exclude=EXCLUDE_PATTERNS)
167
+ freeze(module)
168
+ module.to("cpu", non_blocking=True)
169
+
170
+
171
+ def _get_quanto_dtype(precision: QuantizationOptions) -> torch.dtype:
172
+ """Map precision string to quanto dtype."""
173
+ from optimum.quanto import ( # noqa: PLC0415
174
+ qfloat8,
175
+ qfloat8_e4m3fnuz,
176
+ qint2,
177
+ qint4,
178
+ qint8,
179
+ )
180
+
181
+ if precision == "int2-quanto":
182
+ return qint2
183
+ elif precision == "int4-quanto":
184
+ return qint4
185
+ elif precision == "int8-quanto":
186
+ return qint8
187
+ elif precision in ("fp8-quanto", "fp8uz-quanto"):
188
+ if torch.backends.mps.is_available():
189
+ raise ValueError("FP8 quantization is not supported on MPS devices. Use int2, int4, or int8 instead.")
190
+ if precision == "fp8-quanto":
191
+ return qfloat8
192
+ elif precision == "fp8uz-quanto":
193
+ return qfloat8_e4m3fnuz
194
+
195
+ raise ValueError(f"Invalid quantization precision: {precision}")
packages/ltx-trainer/src/ltx_trainer/sigma_tracker.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sigma-bucketed loss tracking.
2
+ Maps each training step's per-element sigmas and losses to buckets.
3
+ Smoothing is left to wandb's UI.
4
+ """
5
+
6
+ import bisect
7
+ from collections import defaultdict
8
+
9
+
10
+ class SigmaBucketTracker:
11
+ """Map per-element sigma values to named buckets for per-bucket loss logging.
12
+ By default, partitions [0, 1] into four equal-width buckets.
13
+ Custom boundaries can be provided for non-uniform bucketing.
14
+ Each call to update() receives per-element sigmas and losses (both [B,]),
15
+ buckets each element, and computes the mean loss per bucket. This gives
16
+ accurate per-sigma loss tracking even for batch_size > 1.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ bucket_boundaries: list[float] | None = None,
22
+ ) -> None:
23
+ if bucket_boundaries is None:
24
+ bucket_boundaries = [0.0, 0.25, 0.5, 0.75, 1.0]
25
+ if len(bucket_boundaries) < 2:
26
+ raise ValueError("bucket_boundaries must have at least 2 elements")
27
+ if any(bucket_boundaries[i] >= bucket_boundaries[i + 1] for i in range(len(bucket_boundaries) - 1)):
28
+ raise ValueError("bucket_boundaries must be strictly increasing")
29
+ self._boundaries = list(bucket_boundaries)
30
+ self._num_buckets = len(bucket_boundaries) - 1
31
+ self._bucket_labels = [
32
+ f"{bucket_boundaries[i]:.2f}-{bucket_boundaries[i + 1]:.2f}" for i in range(self._num_buckets)
33
+ ]
34
+ self._last_metrics: dict[str, float] = {}
35
+
36
+ def _get_bucket_index(self, sigma: float) -> int:
37
+ """Map sigma value to bucket index."""
38
+ idx = bisect.bisect_right(self._boundaries, sigma) - 1
39
+ return max(0, min(idx, self._num_buckets - 1))
40
+
41
+ def update(self, sigmas: list[float], losses: list[float]) -> None:
42
+ """Record per-element losses into their sigma buckets.
43
+ Args:
44
+ sigmas: Per-element sigma values, one per batch element.
45
+ losses: Per-element losses, one per batch element.
46
+ """
47
+ if not sigmas:
48
+ self._last_metrics = {}
49
+ return
50
+ bucket_losses: dict[int, list[float]] = defaultdict(list)
51
+ for sigma, loss in zip(sigmas, losses, strict=True):
52
+ bucket_losses[self._get_bucket_index(sigma)].append(loss)
53
+ self._last_metrics = {self._bucket_labels[b]: sum(vals) / len(vals) for b, vals in bucket_losses.items()}
54
+
55
+ def get_metrics(self, prefix: str = "train") -> dict[str, float]:
56
+ """Return the mean loss for each bucket hit on the last update.
57
+ Wandb handles smoothing in the UI.
58
+ """
59
+ return {f"{prefix}/loss_sigma_{label}": loss for label, loss in self._last_metrics.items()}
packages/ltx-trainer/src/ltx_trainer/timestep_samplers.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ class TimestepSampler:
5
+ """Base class for timestep samplers.
6
+ Timestep samplers are used to sample timesteps for diffusion models.
7
+ They should implement both sample() and sample_for() methods.
8
+ """
9
+
10
+ def sample(self, batch_size: int, seq_length: int | None = None, device: torch.device = None) -> torch.Tensor:
11
+ """Sample timesteps for a batch.
12
+ Args:
13
+ batch_size: Number of timesteps to sample
14
+ seq_length: (optional) Length of the sequence being processed
15
+ device: Device to place the samples on
16
+ Returns:
17
+ Tensor of shape (batch_size,) containing timesteps
18
+ """
19
+ raise NotImplementedError
20
+
21
+ def sample_for(self, batch: torch.Tensor) -> torch.Tensor:
22
+ """Sample timesteps for a specific batch tensor.
23
+ Args:
24
+ batch: Input tensor of shape (batch_size, seq_length, ...)
25
+ Returns:
26
+ Tensor of shape (batch_size,) containing timesteps
27
+ """
28
+ raise NotImplementedError
29
+
30
+
31
+ class UniformTimestepSampler(TimestepSampler):
32
+ """Samples timesteps uniformly between min_value and max_value (default 0 and 1)."""
33
+
34
+ def __init__(self, min_value: float = 0.0, max_value: float = 1.0):
35
+ self.min_value = min_value
36
+ self.max_value = max_value
37
+
38
+ def sample(self, batch_size: int, seq_length: int | None = None, device: torch.device = None) -> torch.Tensor: # noqa: ARG002
39
+ return torch.rand(batch_size, device=device) * (self.max_value - self.min_value) + self.min_value
40
+
41
+ def sample_for(self, batch: torch.Tensor) -> torch.Tensor:
42
+ if batch.ndim != 3:
43
+ raise ValueError(f"Batch should have 3 dimensions, got {batch.ndim}")
44
+
45
+ return self.sample(batch.shape[0], device=batch.device)
46
+
47
+
48
+ class ShiftedLogitNormalTimestepSampler(TimestepSampler):
49
+ """
50
+ Samples timesteps from a stretched shifted logit-normal distribution,
51
+ where the shift is determined by the sequence length.
52
+ The stretching normalizes samples between percentile bounds to ensure
53
+ the distribution covers [0, 1] more evenly. A uniform fallback prevents
54
+ collapse at high token counts.
55
+ """
56
+
57
+ def __init__(self, std: float = 1.0, eps: float = 1e-3, uniform_prob: float = 0.1):
58
+ self.std = std
59
+ self.eps = eps
60
+ self.uniform_prob = uniform_prob
61
+ # Percentile values for stretching (scaled by std)
62
+ # 99.9th percentile of standard normal ≈ 3.0902
63
+ # 0.5th percentile of standard normal ≈ -2.5758
64
+ self.normal_999_percentile = 3.0902 * std
65
+ self.normal_005_percentile = -2.5758 * std
66
+
67
+ def sample(self, batch_size: int, seq_length: int, device: torch.device = None) -> torch.Tensor:
68
+ """Sample timesteps for a batch from a stretched shifted logit-normal distribution.
69
+ Args:
70
+ batch_size: Number of timesteps to sample
71
+ seq_length: Length of the sequence being processed, used to determine the shift
72
+ device: Device to place the samples on
73
+ Returns:
74
+ Tensor of shape (batch_size,) containing timesteps sampled from a stretched
75
+ shifted logit-normal distribution, where the shift is determined by seq_length
76
+ """
77
+ mu = self._get_shift_for_sequence_length(seq_length)
78
+
79
+ # Sample from shifted logit-normal
80
+ normal_samples = torch.randn((batch_size,), device=device) * self.std + mu
81
+ logitnormal_samples = torch.sigmoid(normal_samples)
82
+
83
+ # Compute percentile bounds for stretching
84
+ percentile_999 = torch.sigmoid(torch.tensor(mu + self.normal_999_percentile, device=device))
85
+ percentile_005 = torch.sigmoid(torch.tensor(mu + self.normal_005_percentile, device=device))
86
+
87
+ # Stretch to [0, 1] range by normalizing between percentiles
88
+ zero_terminal_raw = (logitnormal_samples - percentile_005) / (percentile_999 - percentile_005)
89
+
90
+ # Reflect small values around eps for numerical stability
91
+ stretched_logit = torch.where(
92
+ zero_terminal_raw >= self.eps,
93
+ zero_terminal_raw,
94
+ 2 * self.eps - zero_terminal_raw,
95
+ )
96
+ stretched_logit = torch.clamp(stretched_logit, 0, 1)
97
+
98
+ # Mix with uniform samples (uniform_prob of the time)
99
+ uniform = (1 - self.eps) * torch.rand((batch_size,), device=device) + self.eps
100
+ prob = torch.rand((batch_size,), device=device)
101
+
102
+ return torch.where(prob > self.uniform_prob, stretched_logit, uniform)
103
+
104
+ def sample_for(self, batch: torch.Tensor) -> torch.Tensor:
105
+ """Sample timesteps for a specific batch tensor.
106
+ Args:
107
+ batch: Input tensor of shape (batch_size, seq_length, ...)
108
+ Returns:
109
+ Tensor of shape (batch_size,) containing timesteps sampled from a shifted
110
+ logit-normal distribution, where the shift is determined by the sequence length
111
+ of the input batch
112
+ Raises:
113
+ ValueError: If the input batch does not have 3 dimensions
114
+ """
115
+ if batch.ndim != 3:
116
+ raise ValueError(f"Batch should have 3 dimensions, got {batch.ndim}")
117
+
118
+ batch_size, seq_length, _ = batch.shape
119
+ return self.sample(batch_size, seq_length, device=batch.device)
120
+
121
+ @staticmethod
122
+ def _get_shift_for_sequence_length(
123
+ seq_length: int,
124
+ min_tokens: int = 1024,
125
+ max_tokens: int = 4096,
126
+ min_shift: float = 0.95,
127
+ max_shift: float = 2.05,
128
+ ) -> float:
129
+ # Calculate the shift value for a given sequence length using linear interpolation
130
+ # between min_shift and max_shift based on sequence length.
131
+ m = (max_shift - min_shift) / (max_tokens - min_tokens) # Calculate slope
132
+ b = min_shift - m * min_tokens # Calculate y-intercept
133
+ shift = m * seq_length + b # Apply linear equation y = mx + b
134
+ return shift
135
+
136
+
137
+ SAMPLERS = {
138
+ "uniform": UniformTimestepSampler,
139
+ "shifted_logit_normal": ShiftedLogitNormalTimestepSampler,
140
+ }
141
+
142
+
143
+ def example() -> None:
144
+ # noinspection PyUnresolvedReferences
145
+ import matplotlib.pyplot as plt # noqa: PLC0415
146
+
147
+ sampler = ShiftedLogitNormalTimestepSampler()
148
+ for seq_length in [1024, 2048, 4096, 8192]:
149
+ samples = sampler.sample(batch_size=1_000_000, seq_length=seq_length)
150
+
151
+ # plot the histogram of the samples
152
+ plt.hist(samples.numpy(), bins=100, density=True)
153
+ plt.title(f"Timestep Samples for Sequence Length {seq_length}")
154
+ plt.xlabel("Timestep")
155
+ plt.ylabel("Density")
156
+ plt.show()
157
+
158
+
159
+ if __name__ == "__main__":
160
+ example()