1818import torch
1919
2020from ...models import LTXVideoTransformer3DModel
21- from ...pipelines .ltx .pipeline_ltx import LTXPipeline
2221from ...schedulers import FlowMatchEulerDiscreteScheduler
2322from ...utils import logging
2423from ...utils .torch_utils import randn_tensor
@@ -73,6 +72,43 @@ def retrieve_timesteps(
7372 return timesteps , num_inference_steps
7473
7574
75+ # Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._pack_latents
76+ def _pack_latents (latents : torch .Tensor , patch_size : int = 1 , patch_size_t : int = 1 ) -> torch .Tensor :
77+ # Unpacked latents of shape are [B, C, F, H, W] are patched into tokens of shape
78+ # [B, C, F // p_t, p_t, H // p, p, W // p, p].
79+ # The patch dimensions are then permuted and collapsed into the channel dimension of shape:
80+ # [B, F // p_t * H // p * W // p, C * p_t * p * p] (an ndim=3 tensor).
81+ # dim=0 is the batch size, dim=1 is the effective video sequence length,
82+ # dim=2 is the effective number of input features
83+ batch_size , num_channels , num_frames , height , width = latents .shape
84+ post_patch_num_frames = num_frames // patch_size_t
85+ post_patch_height = height // patch_size
86+ post_patch_width = width // patch_size
87+ latents = latents .reshape (
88+ batch_size ,
89+ - 1 ,
90+ post_patch_num_frames ,
91+ patch_size_t ,
92+ post_patch_height ,
93+ patch_size ,
94+ post_patch_width ,
95+ patch_size ,
96+ )
97+ latents = latents .permute (0 , 2 , 4 , 6 , 1 , 3 , 5 , 7 ).flatten (4 , 7 ).flatten (1 , 3 )
98+ return latents
99+
100+
101+ # Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._normalize_latents
102+ def _normalize_latents (
103+ latents : torch .Tensor , latents_mean : torch .Tensor , latents_std : torch .Tensor , scaling_factor : float = 1.0
104+ ) -> torch .Tensor :
105+ # Normalize latents across the channel dimension [B, C, F, H, W]
106+ latents_mean = latents_mean .view (1 , - 1 , 1 , 1 , 1 ).to (latents .device , latents .dtype )
107+ latents_std = latents_std .view (1 , - 1 , 1 , 1 , 1 ).to (latents .device , latents .dtype )
108+ latents = (latents - latents_mean ) * scaling_factor / latents_std
109+ return latents
110+
111+
76112class LTXTextInputStep (ModularPipelineBlocks ):
77113 model_name = "ltx"
78114
@@ -94,7 +130,6 @@ def expected_components(self) -> list[ComponentSpec]:
94130 def inputs (self ) -> list [InputParam ]:
95131 return [
96132 InputParam .template ("num_images_per_prompt" , name = "num_videos_per_prompt" ),
97- InputParam ("guidance_scale" , type_hint = float , default = 3.0 ),
98133 InputParam .template ("prompt_embeds" , required = True ),
99134 InputParam .template ("prompt_embeds_mask" , name = "prompt_attention_mask" ),
100135 InputParam .template ("negative_prompt_embeds" ),
@@ -112,11 +147,6 @@ def intermediate_outputs(self) -> list[OutputParam]:
112147 def __call__ (self , components : LTXModularPipeline , state : PipelineState ) -> PipelineState :
113148 block_state = self .get_block_state (state )
114149
115- # Set guidance_scale on guider so CFG is configured correctly
116- guidance_scale = getattr (block_state , "guidance_scale" , 3.0 )
117- if hasattr (components , "guider" ) and components .guider is not None :
118- components .guider .guidance_scale = guidance_scale
119-
120150 block_state .batch_size = block_state .prompt_embeds .shape [0 ]
121151 block_state .dtype = block_state .prompt_embeds .dtype
122152 num_videos = block_state .num_videos_per_prompt
@@ -257,7 +287,7 @@ def inputs(self) -> list[InputParam]:
257287 @property
258288 def intermediate_outputs (self ) -> list [OutputParam ]:
259289 return [
260- OutputParam . template ("latents" ),
290+ OutputParam ("latents" , type_hint = torch . Tensor ),
261291 ]
262292
263293 @torch .no_grad ()
@@ -279,7 +309,7 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
279309 block_state .latents = randn_tensor (
280310 shape , generator = block_state .generator , device = device , dtype = torch .float32
281311 )
282- block_state .latents = LTXPipeline . _pack_latents (
312+ block_state .latents = _pack_latents (
283313 block_state .latents ,
284314 components .transformer_spatial_patch_size ,
285315 components .transformer_temporal_patch_size ,
@@ -289,39 +319,19 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
289319 return components , state
290320
291321
292- # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
293- def retrieve_latents (
294- encoder_output : torch .Tensor , generator : torch .Generator | None = None , sample_mode : str = "sample"
295- ):
296- if hasattr (encoder_output , "latent_dist" ) and sample_mode == "sample" :
297- return encoder_output .latent_dist .sample (generator )
298- elif hasattr (encoder_output , "latent_dist" ) and sample_mode == "argmax" :
299- return encoder_output .latent_dist .mode ()
300- elif hasattr (encoder_output , "latents" ):
301- return encoder_output .latents
302- else :
303- raise AttributeError ("Could not access latents of provided encoder_output" )
304-
305-
306322class LTXImage2VideoPrepareLatentsStep (ModularPipelineBlocks ):
307323 model_name = "ltx"
308324
309325 @property
310326 def description (self ) -> str :
311- return "Prepare latents step for image-to-video: encodes the first frame and creates a conditioning mask"
312-
313- @property
314- def expected_components (self ) -> list [ComponentSpec ]:
315- from ...models import AutoencoderKLLTXVideo
316-
317- return [
318- ComponentSpec ("vae" , AutoencoderKLLTXVideo ),
319- ]
327+ return (
328+ "Prepare latents step for image-to-video: takes pre-encoded image latents and creates a conditioning mask"
329+ )
320330
321331 @property
322332 def inputs (self ) -> list [InputParam ]:
323333 return [
324- InputParam . template ( "image" ),
334+ InputParam ( "image_latents" , type_hint = torch . Tensor , required = True ),
325335 InputParam .template ("height" , default = 512 ),
326336 InputParam .template ("width" , default = 704 ),
327337 InputParam ("num_frames" , type_hint = int , default = 161 ),
@@ -335,7 +345,7 @@ def inputs(self) -> list[InputParam]:
335345 @property
336346 def intermediate_outputs (self ) -> list [OutputParam ]:
337347 return [
338- OutputParam . template ("latents" ),
348+ OutputParam ("latents" , type_hint = torch . Tensor ),
339349 OutputParam ("conditioning_mask" , type_hint = torch .Tensor ),
340350 ]
341351
@@ -355,7 +365,7 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
355365 if block_state .latents is not None :
356366 conditioning_mask = block_state .latents .new_zeros (mask_shape )
357367 conditioning_mask [:, :, 0 ] = 1.0
358- conditioning_mask = LTXPipeline . _pack_latents (
368+ conditioning_mask = _pack_latents (
359369 conditioning_mask ,
360370 components .transformer_spatial_patch_size ,
361371 components .transformer_temporal_patch_size ,
@@ -365,38 +375,9 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
365375 self .set_block_state (state , block_state )
366376 return components , state
367377
368- image = block_state .image
369- if not isinstance (image , torch .Tensor ):
370- from ...video_processor import VideoProcessor
371-
372- processor = VideoProcessor (vae_scale_factor = components .vae_spatial_compression_ratio )
373- image = processor .preprocess (image , height = block_state .height , width = block_state .width )
374- image = image .to (device = device , dtype = torch .float32 )
375-
376- vae_dtype = components .vae .dtype
377-
378- num_images = image .shape [0 ]
379- if isinstance (block_state .generator , list ):
380- init_latents = [
381- retrieve_latents (
382- components .vae .encode (image [i ].unsqueeze (0 ).unsqueeze (2 ).to (vae_dtype )), block_state .generator [i ]
383- )
384- for i in range (num_images )
385- ]
386- else :
387- init_latents = [
388- retrieve_latents (
389- components .vae .encode (img .unsqueeze (0 ).unsqueeze (2 ).to (vae_dtype )), block_state .generator
390- )
391- for img in image
392- ]
393-
394- init_latents = torch .cat (init_latents , dim = 0 ).to (torch .float32 )
378+ init_latents = block_state .image_latents .to (device = device , dtype = torch .float32 )
395379 if init_latents .shape [0 ] < batch_size :
396380 init_latents = init_latents .repeat_interleave (batch_size // init_latents .shape [0 ], dim = 0 )
397- init_latents = LTXPipeline ._normalize_latents (
398- init_latents , components .vae .latents_mean , components .vae .latents_std
399- )
400381 init_latents = init_latents .repeat (1 , 1 , num_frames , 1 , 1 )
401382
402383 actual_mask_shape = (
@@ -412,12 +393,12 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
412393 noise = randn_tensor (init_latents .shape , generator = block_state .generator , device = device , dtype = torch .float32 )
413394 latents = init_latents * conditioning_mask + noise * (1 - conditioning_mask )
414395
415- conditioning_mask = LTXPipeline . _pack_latents (
396+ conditioning_mask = _pack_latents (
416397 conditioning_mask ,
417398 components .transformer_spatial_patch_size ,
418399 components .transformer_temporal_patch_size ,
419400 ).squeeze (- 1 )
420- latents = LTXPipeline . _pack_latents (
401+ latents = _pack_latents (
421402 latents ,
422403 components .transformer_spatial_patch_size ,
423404 components .transformer_temporal_patch_size ,
0 commit comments