Update README.md
Browse files
README.md
CHANGED
@@ -30,301 +30,5 @@ The tranining code can be found [here](https://github.com/dashtoon/hunyuan-video
|
|
30 |
HunyuanVideo Keyframe Control Lora can be used directly from Diffusers. Install the latest version of Diffusers.
|
31 |
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
import cv2
|
37 |
-
import numpy as np
|
38 |
-
import safetensors.torch
|
39 |
-
import torch
|
40 |
-
import torchvision.transforms.v2 as transforms
|
41 |
-
from diffusers import FlowMatchEulerDiscreteScheduler, HunyuanVideoPipeline
|
42 |
-
from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
|
43 |
-
from diffusers.loaders import HunyuanVideoLoraLoaderMixin
|
44 |
-
from diffusers.models import AutoencoderKLHunyuanVideo, HunyuanVideoTransformer3DModel
|
45 |
-
from diffusers.models.attention import Attention
|
46 |
-
from diffusers.models.embeddings import apply_rotary_emb
|
47 |
-
from diffusers.models.transformers.transformer_hunyuan_video import HunyuanVideoPatchEmbed, HunyuanVideoTransformer3DModel
|
48 |
-
from diffusers.pipelines.hunyuan_video.pipeline_hunyuan_video import DEFAULT_PROMPT_TEMPLATE, retrieve_timesteps
|
49 |
-
from diffusers.pipelines.hunyuan_video.pipeline_output import HunyuanVideoPipelineOutput
|
50 |
-
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
51 |
-
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
|
52 |
-
from diffusers.utils import export_to_video, is_torch_xla_available, load_image, logging, replace_example_docstring
|
53 |
-
from diffusers.utils.state_dict_utils import convert_state_dict_to_diffusers, convert_unet_state_dict_to_peft
|
54 |
-
from diffusers.utils.torch_utils import randn_tensor
|
55 |
-
from diffusers.video_processor import VideoProcessor
|
56 |
-
from peft import LoraConfig, get_peft_model_state_dict, set_peft_model_state_dict
|
57 |
-
from PIL import Image
|
58 |
-
|
59 |
-
video_transforms = transforms.Compose(
|
60 |
-
[
|
61 |
-
transforms.Lambda(lambda x: x / 255.0),
|
62 |
-
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], inplace=True),
|
63 |
-
]
|
64 |
-
)
|
65 |
-
|
66 |
-
|
67 |
-
def resize_image_to_bucket(image: Union[Image.Image, np.ndarray], bucket_reso: tuple[int, int]) -> np.ndarray:
|
68 |
-
"""
|
69 |
-
Resize the image to the bucket resolution.
|
70 |
-
"""
|
71 |
-
is_pil_image = isinstance(image, Image.Image)
|
72 |
-
if is_pil_image:
|
73 |
-
image_width, image_height = image.size
|
74 |
-
else:
|
75 |
-
image_height, image_width = image.shape[:2]
|
76 |
-
|
77 |
-
if bucket_reso == (image_width, image_height):
|
78 |
-
return np.array(image) if is_pil_image else image
|
79 |
-
|
80 |
-
bucket_width, bucket_height = bucket_reso
|
81 |
-
|
82 |
-
scale_width = bucket_width / image_width
|
83 |
-
scale_height = bucket_height / image_height
|
84 |
-
scale = max(scale_width, scale_height)
|
85 |
-
image_width = int(image_width * scale + 0.5)
|
86 |
-
image_height = int(image_height * scale + 0.5)
|
87 |
-
|
88 |
-
if scale > 1:
|
89 |
-
image = Image.fromarray(image) if not is_pil_image else image
|
90 |
-
image = image.resize((image_width, image_height), Image.LANCZOS)
|
91 |
-
image = np.array(image)
|
92 |
-
else:
|
93 |
-
image = np.array(image) if is_pil_image else image
|
94 |
-
image = cv2.resize(image, (image_width, image_height), interpolation=cv2.INTER_AREA)
|
95 |
-
|
96 |
-
# crop the image to the bucket resolution
|
97 |
-
crop_left = (image_width - bucket_width) // 2
|
98 |
-
crop_top = (image_height - bucket_height) // 2
|
99 |
-
image = image[crop_top : crop_top + bucket_height, crop_left : crop_left + bucket_width]
|
100 |
-
|
101 |
-
return image
|
102 |
-
|
103 |
-
|
104 |
-
model_id = "hunyuanvideo-community/HunyuanVideo"
|
105 |
-
transformer = HunyuanVideoTransformer3DModel.from_pretrained(model_id, subfolder="transformer", torch_dtype=torch.bfloat16)
|
106 |
-
pipe = HunyuanVideoPipeline.from_pretrained(model_id, transformer=transformer, torch_dtype=torch.bfloat16)
|
107 |
-
|
108 |
-
pipe.to("cuda")
|
109 |
-
pipe.vae.enable_tiling()
|
110 |
-
pipe.vae.enable_slicing()
|
111 |
-
|
112 |
-
with torch.no_grad(): # enable image inputs
|
113 |
-
initial_input_channels = pipe.transformer.config.in_channels
|
114 |
-
new_img_in = HunyuanVideoPatchEmbed(
|
115 |
-
patch_size=(pipe.transformer.config.patch_size_t, pipe.transformer.config.patch_size, pipe.transformer.config.patch_size),
|
116 |
-
in_chans=pipe.transformer.config.in_channels * 2,
|
117 |
-
embed_dim=pipe.transformer.config.num_attention_heads * pipe.transformer.config.attention_head_dim,
|
118 |
-
)
|
119 |
-
new_img_in = new_img_in.to(pipe.device, dtype=pipe.dtype)
|
120 |
-
new_img_in.proj.weight.zero_()
|
121 |
-
new_img_in.proj.weight[:, :initial_input_channels].copy_(pipe.transformer.x_embedder.proj.weight)
|
122 |
-
|
123 |
-
if pipe.transformer.x_embedder.proj.bias is not None:
|
124 |
-
new_img_in.proj.bias.copy_(pipe.transformer.x_embedder.proj.bias)
|
125 |
-
|
126 |
-
pipe.transformer.x_embedder = new_img_in
|
127 |
-
|
128 |
-
LORA_PATH = "<PATH_TO_CONTROL_LORA_SAFETENSORS>"
|
129 |
-
lora_state_dict = pipe.lora_state_dict(LORA_PATH)
|
130 |
-
transformer_lora_state_dict = {f'{k.replace("transformer.", "")}': v for k, v in lora_state_dict.items() if k.startswith("transformer.") and "lora" in k}
|
131 |
-
pipe.load_lora_into_transformer(transformer_lora_state_dict, transformer=pipe.transformer, adapter_name="i2v", _pipeline=pipe)
|
132 |
-
pipe.set_adapters(["i2v"], adapter_weights=[1.0])
|
133 |
-
pipe.fuse_lora(components=["transformer"], lora_scale=1.0, adapter_names=["i2v"])
|
134 |
-
pipe.unload_lora_weights()
|
135 |
-
|
136 |
-
n_frames, height, width = 77, 1280, 720
|
137 |
-
prompt = "a woman"
|
138 |
-
cond_frame1 = load_image("https://content.dashtoon.ai/stability-images/e524013d-55d4-483a-b80a-dfc51d639158.png")
|
139 |
-
cond_frame1 = resize_image_to_bucket(cond_frame1, bucket_reso=(width, height))
|
140 |
-
|
141 |
-
cond_frame2 = load_image("https://content.dashtoon.ai/stability-images/0b29c296-0a90-4b92-96b9-1ed0ae21e480.png")
|
142 |
-
cond_frame2 = resize_image_to_bucket(cond_frame2, bucket_reso=(width, height))
|
143 |
-
|
144 |
-
cond_video = np.zeros(shape=(n_frames, height, width, 3))
|
145 |
-
cond_video[0], cond_video[-1] = np.array(cond_frame1), np.array(cond_frame2)
|
146 |
-
|
147 |
-
cond_video = torch.from_numpy(cond_video.copy()).permute(0, 3, 1, 2)
|
148 |
-
cond_video = torch.stack([video_transforms(x) for x in cond_video], dim=0).unsqueeze(0)
|
149 |
-
|
150 |
-
with torch.no_grad():
|
151 |
-
image_or_video = cond_video.to(device="cuda", dtype=pipe.dtype)
|
152 |
-
image_or_video = image_or_video.permute(0, 2, 1, 3, 4).contiguous() # [B, F, C, H, W] -> [B, C, F, H, W]
|
153 |
-
cond_latents = pipe.vae.encode(image_or_video).latent_dist.sample()
|
154 |
-
cond_latents = cond_latents * pipe.vae.config.scaling_factor
|
155 |
-
cond_latents = cond_latents.to(dtype=pipe.dtype)
|
156 |
-
|
157 |
-
|
158 |
-
@torch.no_grad()
|
159 |
-
def call_pipe(
|
160 |
-
pipe,
|
161 |
-
prompt: Union[str, List[str]] = None,
|
162 |
-
prompt_2: Union[str, List[str]] = None,
|
163 |
-
height: int = 720,
|
164 |
-
width: int = 1280,
|
165 |
-
num_frames: int = 129,
|
166 |
-
num_inference_steps: int = 50,
|
167 |
-
sigmas: List[float] = None,
|
168 |
-
guidance_scale: float = 6.0,
|
169 |
-
num_videos_per_prompt: Optional[int] = 1,
|
170 |
-
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
171 |
-
latents: Optional[torch.Tensor] = None,
|
172 |
-
prompt_embeds: Optional[torch.Tensor] = None,
|
173 |
-
pooled_prompt_embeds: Optional[torch.Tensor] = None,
|
174 |
-
prompt_attention_mask: Optional[torch.Tensor] = None,
|
175 |
-
output_type: Optional[str] = "pil",
|
176 |
-
return_dict: bool = True,
|
177 |
-
attention_kwargs: Optional[Dict[str, Any]] = None,
|
178 |
-
callback_on_step_end: Optional[Union[Callable[[int, int, Dict], None], PipelineCallback, MultiPipelineCallbacks]] = None,
|
179 |
-
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
|
180 |
-
prompt_template: Dict[str, Any] = DEFAULT_PROMPT_TEMPLATE,
|
181 |
-
max_sequence_length: int = 256,
|
182 |
-
image_latents: Optional[torch.Tensor] = None,
|
183 |
-
):
|
184 |
-
|
185 |
-
if isinstance(callback_on_step_end, (PipelineCallback, MultiPipelineCallbacks)):
|
186 |
-
callback_on_step_end_tensor_inputs = callback_on_step_end.tensor_inputs
|
187 |
-
|
188 |
-
# 1. Check inputs. Raise error if not correct
|
189 |
-
pipe.check_inputs(
|
190 |
-
prompt,
|
191 |
-
prompt_2,
|
192 |
-
height,
|
193 |
-
width,
|
194 |
-
prompt_embeds,
|
195 |
-
callback_on_step_end_tensor_inputs,
|
196 |
-
prompt_template,
|
197 |
-
)
|
198 |
-
|
199 |
-
pipe._guidance_scale = guidance_scale
|
200 |
-
pipe._attention_kwargs = attention_kwargs
|
201 |
-
pipe._current_timestep = None
|
202 |
-
pipe._interrupt = False
|
203 |
-
|
204 |
-
device = pipe._execution_device
|
205 |
-
|
206 |
-
# 2. Define call parameters
|
207 |
-
if prompt is not None and isinstance(prompt, str):
|
208 |
-
batch_size = 1
|
209 |
-
elif prompt is not None and isinstance(prompt, list):
|
210 |
-
batch_size = len(prompt)
|
211 |
-
else:
|
212 |
-
batch_size = prompt_embeds.shape[0]
|
213 |
-
|
214 |
-
# 3. Encode input prompt
|
215 |
-
prompt_embeds, pooled_prompt_embeds, prompt_attention_mask = pipe.encode_prompt(
|
216 |
-
prompt=prompt,
|
217 |
-
prompt_2=prompt_2,
|
218 |
-
prompt_template=prompt_template,
|
219 |
-
num_videos_per_prompt=num_videos_per_prompt,
|
220 |
-
prompt_embeds=prompt_embeds,
|
221 |
-
pooled_prompt_embeds=pooled_prompt_embeds,
|
222 |
-
prompt_attention_mask=prompt_attention_mask,
|
223 |
-
device=device,
|
224 |
-
max_sequence_length=max_sequence_length,
|
225 |
-
)
|
226 |
-
|
227 |
-
transformer_dtype = pipe.transformer.dtype
|
228 |
-
prompt_embeds = prompt_embeds.to(transformer_dtype)
|
229 |
-
prompt_attention_mask = prompt_attention_mask.to(transformer_dtype)
|
230 |
-
if pooled_prompt_embeds is not None:
|
231 |
-
pooled_prompt_embeds = pooled_prompt_embeds.to(transformer_dtype)
|
232 |
-
|
233 |
-
# 4. Prepare timesteps
|
234 |
-
sigmas = np.linspace(1.0, 0.0, num_inference_steps + 1)[:-1] if sigmas is None else sigmas
|
235 |
-
timesteps, num_inference_steps = retrieve_timesteps(
|
236 |
-
pipe.scheduler,
|
237 |
-
num_inference_steps,
|
238 |
-
device,
|
239 |
-
sigmas=sigmas,
|
240 |
-
)
|
241 |
-
|
242 |
-
# 5. Prepare latent variables
|
243 |
-
num_channels_latents = pipe.transformer.config.in_channels
|
244 |
-
num_latent_frames = (num_frames - 1) // pipe.vae_scale_factor_temporal + 1
|
245 |
-
latents = pipe.prepare_latents(
|
246 |
-
batch_size * num_videos_per_prompt,
|
247 |
-
num_channels_latents,
|
248 |
-
height,
|
249 |
-
width,
|
250 |
-
num_latent_frames,
|
251 |
-
torch.float32,
|
252 |
-
device,
|
253 |
-
generator,
|
254 |
-
latents,
|
255 |
-
)
|
256 |
-
|
257 |
-
# 6. Prepare guidance condition
|
258 |
-
guidance = torch.tensor([guidance_scale] * latents.shape[0], dtype=transformer_dtype, device=device) * 1000.0
|
259 |
-
|
260 |
-
# 7. Denoising loop
|
261 |
-
num_warmup_steps = len(timesteps) - num_inference_steps * pipe.scheduler.order
|
262 |
-
pipe._num_timesteps = len(timesteps)
|
263 |
-
|
264 |
-
with pipe.progress_bar(total=num_inference_steps) as progress_bar:
|
265 |
-
for i, t in enumerate(timesteps):
|
266 |
-
if pipe.interrupt:
|
267 |
-
continue
|
268 |
-
|
269 |
-
pipe._current_timestep = t
|
270 |
-
latent_model_input = latents.to(transformer_dtype)
|
271 |
-
timestep = t.expand(latents.shape[0]).to(latents.dtype)
|
272 |
-
|
273 |
-
noise_pred = pipe.transformer(
|
274 |
-
hidden_states=torch.cat([latent_model_input, image_latents], dim=1),
|
275 |
-
timestep=timestep,
|
276 |
-
encoder_hidden_states=prompt_embeds,
|
277 |
-
encoder_attention_mask=prompt_attention_mask,
|
278 |
-
pooled_projections=pooled_prompt_embeds,
|
279 |
-
guidance=guidance,
|
280 |
-
attention_kwargs=attention_kwargs,
|
281 |
-
return_dict=False,
|
282 |
-
)[0]
|
283 |
-
|
284 |
-
# compute the previous noisy sample x_t -> x_t-1
|
285 |
-
latents = pipe.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
286 |
-
|
287 |
-
if callback_on_step_end is not None:
|
288 |
-
callback_kwargs = {}
|
289 |
-
for k in callback_on_step_end_tensor_inputs:
|
290 |
-
callback_kwargs[k] = locals()[k]
|
291 |
-
callback_outputs = callback_on_step_end(pipe, i, t, callback_kwargs)
|
292 |
-
|
293 |
-
latents = callback_outputs.pop("latents", latents)
|
294 |
-
prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
|
295 |
-
|
296 |
-
# call the callback, if provided
|
297 |
-
if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % pipe.scheduler.order == 0):
|
298 |
-
progress_bar.update()
|
299 |
-
pipe._current_timestep = None
|
300 |
-
|
301 |
-
if not output_type == "latent":
|
302 |
-
latents = latents.to(pipe.vae.dtype) / pipe.vae.config.scaling_factor
|
303 |
-
video = pipe.vae.decode(latents, return_dict=False)[0]
|
304 |
-
video = pipe.video_processor.postprocess_video(video, output_type=output_type)
|
305 |
-
else:
|
306 |
-
video = latents
|
307 |
-
|
308 |
-
# Offload all models
|
309 |
-
pipe.maybe_free_model_hooks()
|
310 |
-
|
311 |
-
if not return_dict:
|
312 |
-
return (video,)
|
313 |
-
|
314 |
-
return HunyuanVideoPipelineOutput(frames=video)
|
315 |
-
|
316 |
-
|
317 |
-
video = call_pipe(
|
318 |
-
pipe,
|
319 |
-
prompt=prompt,
|
320 |
-
num_frames=n_frames,
|
321 |
-
num_inference_steps=50,
|
322 |
-
image_latents=cond_latents,
|
323 |
-
width=width,
|
324 |
-
height=height,
|
325 |
-
guidance_scale=6.0,
|
326 |
-
generator=torch.Generator(device="cuda").manual_seed(0),
|
327 |
-
).frames[0]
|
328 |
-
|
329 |
-
export_to_video(video, "output.mp4", fps=24)
|
330 |
-
```
|
|
|
30 |
HunyuanVideo Keyframe Control Lora can be used directly from Diffusers. Install the latest version of Diffusers.
|
31 |
|
32 |
|
33 |
+
## Inference
|
34 |
+
While the included `inference.py` script can be used to run inference. We would encourage folks to visit out [github repo](https://github.com/dashtoon/hunyuan-video-keyframe-control-lora/blob/main/hv_control_lora_inference.py) which contains a much optimized version of this inference script.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|