Spaces:
Paused
Paused
| # Copyright 2026 The HuggingFace Team. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import html | |
| import regex as re | |
| import torch | |
| from transformers import AutoTokenizer, UMT5EncoderModel | |
| from ...configuration_utils import FrozenDict | |
| from ...guiders import ClassifierFreeGuidance | |
| from ...models import AutoencoderKLWan | |
| from ...utils import is_ftfy_available, logging | |
| from ...video_processor import VideoProcessor | |
| from ..modular_pipeline import ModularPipelineBlocks, PipelineState | |
| from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam | |
| from .modular_pipeline import HeliosModularPipeline | |
| if is_ftfy_available(): | |
| import ftfy | |
| logger = logging.get_logger(__name__) # pylint: disable=invalid-name | |
| def basic_clean(text): | |
| text = ftfy.fix_text(text) | |
| text = html.unescape(html.unescape(text)) | |
| return text.strip() | |
| def whitespace_clean(text): | |
| text = re.sub(r"\s+", " ", text) | |
| text = text.strip() | |
| return text | |
| def prompt_clean(text): | |
| text = whitespace_clean(basic_clean(text)) | |
| return text | |
| def get_t5_prompt_embeds( | |
| text_encoder: UMT5EncoderModel, | |
| tokenizer: AutoTokenizer, | |
| prompt: str | list[str], | |
| max_sequence_length: int, | |
| device: torch.device, | |
| dtype: torch.dtype | None = None, | |
| ): | |
| """Encode text prompts into T5 embeddings for Helios. | |
| Args: | |
| text_encoder: The T5 text encoder model. | |
| tokenizer: The tokenizer for the text encoder. | |
| prompt: The prompt or prompts to encode. | |
| max_sequence_length: Maximum sequence length for tokenization. | |
| device: Device to place tensors on. | |
| dtype: Optional dtype override. Defaults to `text_encoder.dtype`. | |
| Returns: | |
| A tuple of `(prompt_embeds, attention_mask)` where `prompt_embeds` is the encoded text embeddings and | |
| `attention_mask` is a boolean mask. | |
| """ | |
| dtype = dtype or text_encoder.dtype | |
| prompt = [prompt] if isinstance(prompt, str) else prompt | |
| prompt = [prompt_clean(u) for u in prompt] | |
| text_inputs = tokenizer( | |
| prompt, | |
| padding="max_length", | |
| max_length=max_sequence_length, | |
| truncation=True, | |
| add_special_tokens=True, | |
| return_attention_mask=True, | |
| return_tensors="pt", | |
| ) | |
| text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask | |
| seq_lens = mask.gt(0).sum(dim=1).long() | |
| prompt_embeds = text_encoder(text_input_ids.to(device), mask.to(device)).last_hidden_state | |
| prompt_embeds = prompt_embeds.to(dtype=dtype, device=device) | |
| prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)] | |
| prompt_embeds = torch.stack( | |
| [torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))]) for u in prompt_embeds], dim=0 | |
| ) | |
| return prompt_embeds, text_inputs.attention_mask.bool() | |
| class HeliosTextEncoderStep(ModularPipelineBlocks): | |
| model_name = "helios" | |
| def description(self) -> str: | |
| return "Text Encoder step that generates text embeddings to guide the video generation" | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("text_encoder", UMT5EncoderModel), | |
| ComponentSpec("tokenizer", AutoTokenizer), | |
| ComponentSpec( | |
| "guider", | |
| ClassifierFreeGuidance, | |
| config=FrozenDict({"guidance_scale": 5.0}), | |
| default_creation_method="from_config", | |
| ), | |
| ] | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam.template("prompt"), | |
| InputParam.template("negative_prompt"), | |
| InputParam.template("max_sequence_length"), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam.template("prompt_embeds"), | |
| OutputParam.template("negative_prompt_embeds"), | |
| ] | |
| def check_inputs(prompt, negative_prompt): | |
| if prompt is not None and not isinstance(prompt, (str, list)): | |
| raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") | |
| if negative_prompt is not None and not isinstance(negative_prompt, (str, list)): | |
| raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}") | |
| if prompt is not None and negative_prompt is not None: | |
| prompt_list = [prompt] if isinstance(prompt, str) else prompt | |
| neg_list = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt | |
| if type(prompt_list) is not type(neg_list): | |
| raise TypeError( | |
| f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=" | |
| f" {type(prompt)}." | |
| ) | |
| if len(prompt_list) != len(neg_list): | |
| raise ValueError( | |
| f"`negative_prompt` has batch size {len(neg_list)}, but `prompt` has batch size" | |
| f" {len(prompt_list)}. Please make sure that passed `negative_prompt` matches" | |
| " the batch size of `prompt`." | |
| ) | |
| def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| prompt = block_state.prompt | |
| negative_prompt = block_state.negative_prompt | |
| max_sequence_length = block_state.max_sequence_length | |
| device = components._execution_device | |
| self.check_inputs(prompt, negative_prompt) | |
| # Encode prompt | |
| block_state.prompt_embeds, _ = get_t5_prompt_embeds( | |
| text_encoder=components.text_encoder, | |
| tokenizer=components.tokenizer, | |
| prompt=prompt, | |
| max_sequence_length=max_sequence_length, | |
| device=device, | |
| ) | |
| # Encode negative prompt | |
| block_state.negative_prompt_embeds = None | |
| if components.requires_unconditional_embeds: | |
| negative_prompt = negative_prompt or "" | |
| if isinstance(prompt, list) and isinstance(negative_prompt, str): | |
| negative_prompt = len(prompt) * [negative_prompt] | |
| block_state.negative_prompt_embeds, _ = get_t5_prompt_embeds( | |
| text_encoder=components.text_encoder, | |
| tokenizer=components.tokenizer, | |
| prompt=negative_prompt, | |
| max_sequence_length=max_sequence_length, | |
| device=device, | |
| ) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class HeliosImageVaeEncoderStep(ModularPipelineBlocks): | |
| """Encodes an input image into VAE latent space for image-to-video generation.""" | |
| model_name = "helios" | |
| def description(self) -> str: | |
| return ( | |
| "Image Encoder step that encodes an input image into VAE latent space, " | |
| "producing image_latents (first frame prefix) and fake_image_latents (history seed) " | |
| "for image-to-video generation." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("vae", AutoencoderKLWan), | |
| ComponentSpec( | |
| "video_processor", | |
| VideoProcessor, | |
| config=FrozenDict({"vae_scale_factor": 8}), | |
| default_creation_method="from_config", | |
| ), | |
| ] | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam.template("image"), | |
| InputParam.template("height", default=384), | |
| InputParam.template("width", default=640), | |
| InputParam( | |
| "num_latent_frames_per_chunk", | |
| default=9, | |
| type_hint=int, | |
| description="Number of latent frames per temporal chunk.", | |
| ), | |
| InputParam.template("generator"), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam.template("image_latents"), | |
| OutputParam( | |
| "fake_image_latents", type_hint=torch.Tensor, description="Fake image latents for history seeding" | |
| ), | |
| ] | |
| def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| vae = components.vae | |
| device = components._execution_device | |
| latents_mean = ( | |
| torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(vae.device, vae.dtype) | |
| ) | |
| latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to( | |
| vae.device, vae.dtype | |
| ) | |
| # Preprocess image to 4D tensor (B, C, H, W) | |
| image = components.video_processor.preprocess( | |
| block_state.image, height=block_state.height, width=block_state.width | |
| ) | |
| image_5d = image.unsqueeze(2).to(device=device, dtype=vae.dtype) # (B, C, 1, H, W) | |
| # Encode image to get image_latents | |
| image_latents = vae.encode(image_5d).latent_dist.sample(generator=block_state.generator) | |
| image_latents = (image_latents - latents_mean) * latents_std | |
| # Encode fake video to get fake_image_latents | |
| min_frames = (block_state.num_latent_frames_per_chunk - 1) * components.vae_scale_factor_temporal + 1 | |
| fake_video = image_5d.repeat(1, 1, min_frames, 1, 1) # (B, C, min_frames, H, W) | |
| fake_latents_full = vae.encode(fake_video).latent_dist.sample(generator=block_state.generator) | |
| fake_latents_full = (fake_latents_full - latents_mean) * latents_std | |
| fake_image_latents = fake_latents_full[:, :, -1:, :, :] | |
| block_state.image_latents = image_latents.to(device=device, dtype=torch.float32) | |
| block_state.fake_image_latents = fake_image_latents.to(device=device, dtype=torch.float32) | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| class HeliosVideoVaeEncoderStep(ModularPipelineBlocks): | |
| """Encodes an input video into VAE latent space for video-to-video generation. | |
| Produces `image_latents` (first frame) and `video_latents` (remaining frames encoded in chunks). | |
| """ | |
| model_name = "helios" | |
| def description(self) -> str: | |
| return ( | |
| "Video Encoder step that encodes an input video into VAE latent space, " | |
| "producing image_latents (first frame) and video_latents (chunked video frames) " | |
| "for video-to-video generation." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("vae", AutoencoderKLWan), | |
| ComponentSpec( | |
| "video_processor", | |
| VideoProcessor, | |
| config=FrozenDict({"vae_scale_factor": 8}), | |
| default_creation_method="from_config", | |
| ), | |
| ] | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam("video", required=True, description="Input video for video-to-video generation"), | |
| InputParam.template("height", default=384), | |
| InputParam.template("width", default=640), | |
| InputParam( | |
| "num_latent_frames_per_chunk", | |
| default=9, | |
| type_hint=int, | |
| description="Number of latent frames per temporal chunk.", | |
| ), | |
| InputParam.template("generator"), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam.template("image_latents"), | |
| OutputParam("video_latents", type_hint=torch.Tensor, description="Encoded video latents (chunked)"), | |
| ] | |
| def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState: | |
| block_state = self.get_block_state(state) | |
| vae = components.vae | |
| device = components._execution_device | |
| num_latent_frames_per_chunk = block_state.num_latent_frames_per_chunk | |
| latents_mean = ( | |
| torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(vae.device, vae.dtype) | |
| ) | |
| latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to( | |
| vae.device, vae.dtype | |
| ) | |
| # Preprocess video | |
| video = components.video_processor.preprocess_video( | |
| block_state.video, height=block_state.height, width=block_state.width | |
| ) | |
| video = video.to(device=device, dtype=vae.dtype) | |
| # Encode video into latents | |
| num_frames = video.shape[2] | |
| min_frames = (num_latent_frames_per_chunk - 1) * 4 + 1 | |
| num_chunks = num_frames // min_frames | |
| if num_chunks == 0: | |
| raise ValueError( | |
| f"Video must have at least {min_frames} frames " | |
| f"(got {num_frames} frames). " | |
| f"Required: (num_latent_frames_per_chunk - 1) * 4 + 1 = ({num_latent_frames_per_chunk} - 1) * 4 + 1 = {min_frames}" | |
| ) | |
| total_valid_frames = num_chunks * min_frames | |
| start_frame = num_frames - total_valid_frames | |
| # Encode first frame | |
| first_frame = video[:, :, 0:1, :, :] | |
| image_latents = vae.encode(first_frame).latent_dist.sample(generator=block_state.generator) | |
| image_latents = (image_latents - latents_mean) * latents_std | |
| # Encode remaining frames in chunks | |
| latents_chunks = [] | |
| for i in range(num_chunks): | |
| chunk_start = start_frame + i * min_frames | |
| chunk_end = chunk_start + min_frames | |
| video_chunk = video[:, :, chunk_start:chunk_end, :, :] | |
| chunk_latents = vae.encode(video_chunk).latent_dist.sample(generator=block_state.generator) | |
| chunk_latents = (chunk_latents - latents_mean) * latents_std | |
| latents_chunks.append(chunk_latents) | |
| video_latents = torch.cat(latents_chunks, dim=2) | |
| block_state.image_latents = image_latents.to(device=device, dtype=torch.float32) | |
| block_state.video_latents = video_latents.to(device=device, dtype=torch.float32) | |
| self.set_block_state(state, block_state) | |
| return components, state | |