| import os |
|
|
| import torch |
| import logging |
| from textwrap import indent |
| import torch.nn as nn |
| from tqdm import tqdm |
| from .ovi.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler |
| from diffusers import FlowMatchEulerDiscreteScheduler |
| from .ovi.utils.fm_solvers import (FlowDPMSolverMultistepScheduler, |
| get_sampling_sigmas, retrieve_timesteps) |
| from shared.utils import files_locator as fl |
| from .modules.vae2_2 import Wan2_2_VAE |
| from .modules.t5 import T5EncoderModel |
| from .ovi.modules.mmaudio.features_utils import FeaturesUtils |
| from .ovi.modules.fusion import FusionModel |
| import json |
| from mmgp import offload |
| from shared.utils.loras_mutipliers import update_loras_slists, get_model_switch_steps |
|
|
| def init_fusion_score_model_ovi(): |
| config_root = os.path.join("models", "wan", "ovi", "configs") |
| video_config_path = os.path.join(config_root , "video.json") |
| audio_config_path = os.path.join(config_root , "audio.json") |
|
|
| with open(video_config_path, encoding="utf-8") as f: |
| video_config = json.load(f) |
|
|
| with open(audio_config_path, encoding="utf-8") as f: |
| audio_config = json.load(f) |
|
|
| with torch.device("meta"): |
| fusion_model = FusionModel(video_config, audio_config) |
| |
| return fusion_model, video_config, audio_config |
|
|
| def init_mmaudio_vae(): |
| tod_path = fl.locate_file( os.path.join("mmaudio", "v1-16.pth")) |
| bigvgan_path = fl.locate_file(os.path.join("mmaudio", "best_netG.pt")) |
| |
| vae_config = { |
| "mode": "16k", |
| "need_vae_encoder": True, |
| "tod_vae_ckpt": str(tod_path), |
| "bigvgan_vocoder_ckpt": str(bigvgan_path), |
| } |
|
|
| return FeaturesUtils(**vae_config).to("cpu") |
|
|
| class OviFusionEngine: |
| def __init__(self, |
| device="cuda", |
| model_filename = None, |
| text_encoder_filename = None, |
| VAE_dtype = torch.bfloat16, |
| dtype = torch.bfloat16, |
| model_def = None, |
| **any): |
| |
| self.device = "cpu" |
| self.dtype = dtype |
| self.sr = 16000 |
| self.fps = model_def.get("fps", 24) |
| self._interrupt = False |
| self.last_audio = None |
|
|
| |
| self.device = device |
| self.target_dtype = torch.bfloat16 |
| model, video_config, audio_config = init_fusion_score_model_ovi() |
| |
| offload.load_model_data(model.video_model, model_filename[0], writable_tensors=False) |
| offload.load_model_data(model.audio_model, model_filename[1], writable_tensors=False) |
| offload.change_dtype(model, dtype, True) |
| model = model.eval() |
| |
| self.model = model |
|
|
|
|
| |
| |
|
|
| |
| |
|
|
|
|
| self.vae_stride = (4, 16, 16) |
| vae_checkpoint = "Wan2.2_VAE.safetensors" |
| self.vae = Wan2_2_VAE( vae_pth=fl.locate_file(vae_checkpoint), dtype= VAE_dtype, device="cpu") |
| self.vae.device = self.device |
| self.vae.model.requires_grad_(False).eval() |
|
|
| vae_model_audio = init_mmaudio_vae() |
| vae_model_audio.requires_grad_(False).eval() |
| self.audio_vae = vae_model_audio.bfloat16() |
| |
| text_encoder_folder = model_def.get("text_encoder_folder") |
| if text_encoder_folder: |
| tokenizer_path = fl.locate_folder(text_encoder_folder) |
| else: |
| tokenizer_path = os.path.dirname(text_encoder_filename) |
| self.text_encoder = T5EncoderModel( |
| text_len=512, |
| dtype=torch.bfloat16, |
| device=torch.device('cpu'), |
| checkpoint_path=text_encoder_filename, |
| tokenizer_path=tokenizer_path) |
| |
|
|
|
|
| |
| self.image_model = None |
| |
| |
| |
| |
| |
|
|
| |
| self.audio_latent_channel = audio_config.get("in_dim") |
| self.video_latent_channel = video_config.get("in_dim") |
| self.audio_latent_length = 157 |
| self.video_latent_length = 31 |
|
|
| logging.info(f"OVI Fusion Engine initialized, GPU VRAM allocated: {torch.cuda.memory_allocated(device)/1e9:.2f} GB, reserved: {torch.cuda.memory_reserved(device)/1e9:.2f} GB") |
|
|
|
|
| @torch.no_grad() |
| def generate(self, |
| input_prompt, |
| image_start=None, |
| input_video = None, |
| width = 1280, |
| height = 720, |
| frame_num = 121, |
| seed=100, |
| solver_name="unipc", |
| sampling_steps=50, |
| shift=5.0, |
| guide_scale=5.0, |
| audio_cfg_scalecale=4.0, |
| perturbation_layers=[11], |
| perturbation_start = 0.0, |
| perturbation_end = 1.0, |
| n_prompt="", |
| audio_negative_prompt="", |
| loras_slists = None, |
| callback = None, |
| block_size = 0, |
| VAE_tile_size = 0, |
| joint_pass = False, |
| **bbkwargs, |
| ): |
|
|
| if len(n_prompt) == 0: |
| n_prompt = "jitter, bad hands, blur, distortion" |
| if len(audio_negative_prompt) == 0: |
| audio_negative_prompt= "robotic, muffled, echo, distorted" |
|
|
| perturbation_layer = None |
| if isinstance(perturbation_layers, (list, tuple)) and perturbation_layers: |
| perturbation_layer = int(perturbation_layers[0]) |
| elif isinstance(perturbation_layers, (int, float)): |
| perturbation_layer = int(perturbation_layers) |
| if perturbation_layer is None: |
| perturbation_layer = 11 |
|
|
| video_frame_height_width=(height, width) |
|
|
| scheduler_video, timesteps_video = self.get_scheduler_time_steps( |
| sampling_steps=sampling_steps, |
| device=self.device, |
| solver_name=solver_name, |
| shift=shift |
| ) |
| scheduler_audio, timesteps_audio = self.get_scheduler_time_steps( |
| sampling_steps=sampling_steps, |
| device=self.device, |
| solver_name=solver_name, |
| shift=shift |
| ) |
|
|
| if self._interrupt: |
| return None |
|
|
|
|
| if input_video is not None: |
| first_frame = input_video |
| is_i2v = True |
| else: |
| first_frame = None |
| is_i2v = False |
|
|
| if callback != None: |
| callback(-1, None, True) |
|
|
| text_embeddings = self.text_encoder([input_prompt, n_prompt, audio_negative_prompt], device= self.device) |
| text_embeddings = [emb.to(self.target_dtype).to(self.device) for emb in text_embeddings] |
| |
| text_embeddings_audio_pos = text_embeddings[0] |
| text_embeddings_video_pos = text_embeddings[0] |
|
|
| text_embeddings_video_neg = text_embeddings[1] |
| text_embeddings_audio_neg = text_embeddings[2] |
|
|
| if is_i2v: |
| with torch.no_grad(): |
| latents_images = self.vae.encode([first_frame], VAE_tile_size)[0].to(self.target_dtype) |
| latents_images = latents_images.to(self.target_dtype) |
| video_latent_h, video_latent_w = latents_images.shape[2], latents_images.shape[3] |
| else: |
| video_h, video_w = video_frame_height_width |
| video_latent_h, video_latent_w = video_h // 16, video_w // 16 |
|
|
| if frame_num == 121: |
| video_latent_length = 31 |
| audio_latent_length = 157 |
| else: |
| video_latent_length = 61 |
| audio_latent_length = 314 |
|
|
|
|
| |
| from .modules.posemb_layers import get_rotary_pos_embed, get_nd_rotary_pos_embed |
|
|
| video_freqs = get_nd_rotary_pos_embed((0, 0, 0 ), (video_latent_length, video_latent_h//2, video_latent_w//2 )) |
| |
| audio_freqs = self.model.audio_model.get_audio_rope_params() |
| video_noise = torch.randn((self.video_latent_channel, video_latent_length, video_latent_h, video_latent_w), device=self.device, dtype=self.target_dtype, generator=torch.Generator(device=self.device).manual_seed(seed)) |
| audio_noise = torch.randn((audio_latent_length, self.audio_latent_channel), device=self.device, dtype=self.target_dtype, generator=torch.Generator(device=self.device).manual_seed(seed)) |
| def ret(): |
| return None |
| |
| |
| max_seq_len_audio = audio_noise.shape[0] |
| _patch_size_h, _patch_size_w = self.model.video_model.patch_size[1], self.model.video_model.patch_size[2] |
| max_seq_len_video = video_noise.shape[1] * video_noise.shape[2] * video_noise.shape[3] // (_patch_size_h*_patch_size_w) |
|
|
| update_loras_slists(self.model.video_model, loras_slists, len(timesteps_video)) |
| kwargs = { |
| 'vid_seq_len': max_seq_len_video, |
| 'audio_seq_len': max_seq_len_audio, |
| 'first_frame_is_clean': is_i2v, |
| 'callback' : callback, |
| 'pipeline': self, |
| 'video_freqs': video_freqs, |
| 'audio_freqs': audio_freqs, |
| } |
|
|
| |
| with torch.amp.autocast('cuda', enabled=self.target_dtype != torch.float32, dtype=self.target_dtype): |
| for i, (t_v, t_a) in tqdm(enumerate(zip(timesteps_video, timesteps_audio)), total=min(len(timesteps_video), len(timesteps_audio))): |
| timestep_input = torch.full((1,), t_v, device=self.device) |
| kwargs.update({ |
| "vid": video_noise, |
| "audio" : audio_noise, |
| "t": timestep_input, |
| }) |
| offload.set_step_no_for_lora(self.model.video_model, i) |
| if is_i2v: |
| video_noise[:, :1] = latents_images |
| computed_perturbation_layers = perturbation_layers if int(perturbation_start * sampling_steps) <= i < int(perturbation_end * sampling_steps) else None |
| any_guidance = not (guide_scale == 1 and audio_cfg_scalecale ==1) |
|
|
| if any_guidance and not joint_pass: |
| pred_vid_pos, pred_audio_pos = self.model( |
| audio_context= [text_embeddings_audio_pos], |
| vid_context= [text_embeddings_video_pos], |
| x_id_list =[0], |
| **kwargs |
| ) |
| if pred_vid_pos is None: |
| return ret() |
| |
| pred_vid_neg, pred_audio_neg = self.model( |
| audio_context= [text_embeddings_audio_neg], |
| vid_context =[text_embeddings_video_neg], |
| x_id_list =[1], |
| computed_perturbation_layers = computed_perturbation_layers, |
| **kwargs |
| ) |
| if pred_vid_neg is None: |
| return ret() |
| else: |
| vid, audio = self.model( |
| audio_context= [text_embeddings_audio_pos, text_embeddings_audio_neg], |
| vid_context= [text_embeddings_video_pos, text_embeddings_video_neg], |
| computed_perturbation_layers = computed_perturbation_layers, |
| x_id_list =[0,1], |
| **kwargs |
| ) |
| if vid is None: |
| return ret() |
| pred_vid_pos, pred_vid_neg = vid |
| pred_audio_pos, pred_audio_neg = audio |
| vid = audio = None |
| |
| pred_video_guided = pred_vid_neg + guide_scale * (pred_vid_pos - pred_vid_neg) |
| pred_audio_guided = pred_audio_neg + audio_cfg_scalecale * (pred_audio_pos - pred_audio_neg) |
| pred_audio_neg = pred_audio_pos = pred_vid_neg = pred_vid_pos = None |
| |
| video_noise = scheduler_video.step( |
| pred_video_guided.unsqueeze(0), t_v, video_noise.unsqueeze(0), return_dict=False |
| )[0].squeeze(0) |
| pred_video_guided = None |
| audio_noise = scheduler_audio.step( |
| pred_audio_guided.unsqueeze(0), t_a, audio_noise.unsqueeze(0), return_dict=False |
| )[0].squeeze(0) |
| pred_audio_guided = None |
| if callback is not None: |
| latents_preview = video_noise |
| callback(i, latents_preview, False ) |
| latents_preview = None |
| ret() |
|
|
| if is_i2v: |
| video_noise[:, :1] = latents_images |
|
|
| |
| audio_latents_for_vae = audio_noise.unsqueeze(0).transpose(1, 2) |
| generated_audio = self.audio_vae.wrapped_decode(audio_latents_for_vae) |
| generated_audio = generated_audio.squeeze().cpu().float().numpy() |
| |
| |
| generated_video = self.vae.decode_to_cpu_uint8([video_noise], VAE_tile_size, target_frames=frame_num, target_height=height, target_width=width)[0] |
|
|
| |
| output = {"x": generated_video, "audio": generated_audio} |
| return output |
|
|
|
|
| |
| def get_scheduler_time_steps(self, sampling_steps, solver_name='unipc', device=0, shift=5.0): |
| torch.manual_seed(4) |
|
|
| if solver_name == 'unipc': |
| sample_scheduler = FlowUniPCMultistepScheduler( |
| num_train_timesteps=1000, |
| shift=1, |
| use_dynamic_shifting=False) |
| sample_scheduler.set_timesteps( |
| sampling_steps, device=device, shift=shift) |
| timesteps = sample_scheduler.timesteps |
|
|
| elif solver_name == 'dpm++': |
| sample_scheduler = FlowDPMSolverMultistepScheduler( |
| num_train_timesteps=1000, |
| shift=1, |
| use_dynamic_shifting=False) |
| sampling_sigmas = get_sampling_sigmas(sampling_steps, shift=shift) |
| timesteps, _ = retrieve_timesteps( |
| sample_scheduler, |
| device=device, |
| sigmas=sampling_sigmas) |
| |
| elif solver_name == 'euler': |
| sample_scheduler = FlowMatchEulerDiscreteScheduler( |
| shift=shift |
| ) |
| timesteps, sampling_steps = retrieve_timesteps( |
| sample_scheduler, |
| sampling_steps, |
| device=device, |
| ) |
| |
| else: |
| raise NotImplementedError("Unsupported solver.") |
| |
| return sample_scheduler, timesteps |
| |
| def custom_compile(self, **compile_kwargs): |
| self.model.custom_compile(compile_kwargs) |
|
|
| def get_trans_lora(self): |
| return self.model.video_model, None |
|
|