import json import math import os from typing import Optional, List import numpy as np import torch from accelerate import init_empty_weights from mmgp import offload from tqdm import tqdm import librosa import pyloudnorm as pyln import scipy.signal as ss import torch.nn.functional as F from transformers import AutoFeatureExtractor, Wav2Vec2FeatureExtractor, WhisperModel from shared.utils import files_locator as fl from ..wan.modules.t5 import T5EncoderModel from .modules.longcat_video_dit import LongCatVideoTransformer3DModel from .modules.avatar.longcat_video_dit_avatar import LongCatVideoAvatarTransformer3DModel from .modules.autoencoder_kl_wan import AutoencoderKLWan from .modules.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler from .audio_process.wav2vec2 import Wav2Vec2ModelWrapper from ..qwen.convert_diffusers_qwen_vae import convert_state_dict from shared.utils.text_encoder_cache import TextEncoderCache def _load_json_config(path): with open(path, "r", encoding="utf-8") as f: cfg = json.load(f) cfg.pop("_class_name", None) cfg.pop("_diffusers_version", None) cfg.pop("architectures", None) cfg.pop("model_max_length", None) return cfg def retrieve_latents( encoder_output: torch.Tensor, generator: Optional[torch.Generator] = None, sample_mode: str = "sample", ): if hasattr(encoder_output, "latent_dist") and sample_mode == "sample": return encoder_output.latent_dist.sample(generator) if hasattr(encoder_output, "latent_dist") and sample_mode == "argmax": return encoder_output.latent_dist.mode() if hasattr(encoder_output, "latents"): return encoder_output.latents raise AttributeError("Could not access latents of provided encoder_output") def optimized_scale(positive_flat, negative_flat): dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True) squared_norm = torch.sum(negative_flat ** 2, dim=1, keepdim=True) + 1e-8 return dot_product / squared_norm LONGCAT_AVATAR_TYPES = {"longcat_avatar", "longcat_avatar_v1_5"} class LongCatModel: def __init__( self, checkpoint_dir, model_filename=None, model_type=None, model_def=None, base_model_type=None, text_encoder_filename=None, quantizeTransformer=False, save_quantized=False, dtype=torch.bfloat16, VAE_dtype=torch.float32, mixed_precision_transformer=False, **kwargs, ): self.device = torch.device("cuda") self.dtype = dtype self.VAE_dtype = VAE_dtype self.model_def = model_def or {} self.base_model_type = base_model_type self.is_avatar = base_model_type in LONGCAT_AVATAR_TYPES self.is_avatar_v1_5 = base_model_type == "longcat_avatar_v1_5" self.audio_encoder_name = None self.sparse_attention_enabled = bool(self.model_def.get("sparse_attention", False)) self._interrupt = False self._reference_image = None text_encoder_path = text_encoder_filename or fl.locate_file( "umt5-xxl/models_t5_umt5-xxl-enc-bf16.safetensors", True ) text_encoder_folder = self.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_path) self.text_encoder = T5EncoderModel( text_len=512, dtype=dtype, device=torch.device("cpu"), checkpoint_path=text_encoder_path, tokenizer_path=tokenizer_path, ) self.text_encoder_cache = TextEncoderCache() transformer_config_path = self.model_def.get("transformer_config") if not transformer_config_path: transformer_config_path = ( "models/longcat/configs/longcat_avatar.json" if self.is_avatar else "models/longcat/configs/longcat_video.json" ) transformer_cfg = _load_json_config(transformer_config_path) if self.sparse_attention_enabled: transformer_cfg["enable_bsa"] = True sparse_params = self.model_def.get("sparse_attention_params") if isinstance(sparse_params, dict) and sparse_params: bsa_params = dict(transformer_cfg.get("bsa_params") or {}) bsa_params.update(sparse_params) transformer_cfg["bsa_params"] = bsa_params transformer_cls = ( LongCatVideoAvatarTransformer3DModel if self.is_avatar else LongCatVideoTransformer3DModel ) with init_empty_weights(include_buffers=True): transformer = transformer_cls(**transformer_cfg) model_path = model_filename[0] if isinstance(model_filename, (list, tuple)) else model_filename if model_path is None: raise ValueError("Missing LongCat transformer weights path.") offload.load_model_data(transformer, model_path, writable_tensors=False) transformer._model_dtype = dtype transformer.eval().requires_grad_(False) self.transformer = transformer if save_quantized: from wgp import save_quantized_model save_quantized_model(transformer, model_type, model_path, dtype, transformer_config_path) vae_cfg_path = "models/longcat/configs/longcat_vae.json" vae_weights = self.model_def.get("vae_URL") if vae_weights: vae_weights = fl.locate_file(vae_weights) else: for candidate in ["Wan2.1_VAE_bf16.safetensors", "Wan2.1_VAE.safetensors", "longcat_vae_bf16.safetensors"]: vae_weights = fl.locate_file(candidate, error_if_none=False) if vae_weights: break if not vae_weights: raise FileNotFoundError("Unable to locate a compatible VAE weights file for LongCat.") def preprocess_vae_sd(sd): return convert_state_dict(sd) self.vae = offload.fast_load_transformers_model( vae_weights, modelClass=AutoencoderKLWan, defaultConfigPath=vae_cfg_path, writable_tensors=False, preprocess_sd=preprocess_vae_sd, default_dtype=VAE_dtype, ) self.vae = self.vae.to(dtype=VAE_dtype, device="cpu") self.vae._model_dtype = VAE_dtype self.vae._dtype = VAE_dtype self.vae.eval().requires_grad_(False) scheduler_cfg = _load_json_config(self.model_def.get("scheduler_config", "models/longcat/configs/longcat_scheduler.json")) self.scheduler = FlowMatchEulerDiscreteScheduler(**scheduler_cfg) self.num_timesteps = 1000 self.num_distill_sample_steps = int(self.model_def.get("num_distill_sample_steps", 8 if self.is_avatar_v1_5 else 50)) if self.is_avatar: if self.is_avatar_v1_5: whisper_folder_name = self.model_def.get("audio_encoder_folder", "whisper-large-v3") whisper_folder = fl.locate_folder(whisper_folder_name) whisper_model_path = fl.locate_file(os.path.join(whisper_folder_name, "model.safetensors")) whisper_config_path = fl.locate_file(os.path.join(whisper_folder_name, "config.json")) fl.locate_file(os.path.join(whisper_folder_name, "generation_config.json")) fl.locate_file(os.path.join(whisper_folder_name, "preprocessor_config.json")) self.audio_encoder_name = "whisper-large-v3" self.audio_encoder = offload.fast_load_transformers_model( whisper_model_path, modelClass=WhisperModel, defaultConfigPath=whisper_config_path, modelPrefix="model", writable_tensors=False, default_dtype=dtype, ignore_unused_weights=True, ) if hasattr(self.audio_encoder, "decoder"): del self.audio_encoder.decoder self.audio_encoder._model_dtype = dtype self.audio_encoder.eval().requires_grad_(False) self.audio_feature_extractor = AutoFeatureExtractor.from_pretrained(whisper_folder, local_files_only=True) else: wav2vec_folder = fl.locate_folder("chinese-wav2vec2-base") self.audio_encoder_name = "wav2vec2" self.audio_encoder = Wav2Vec2ModelWrapper(wav2vec_folder) self.audio_encoder.eval().requires_grad_(False) if hasattr(self.audio_encoder, "feature_extractor"): self.audio_encoder.feature_extractor._freeze_parameters() self.audio_feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(wav2vec_folder, local_files_only=True) else: self.audio_encoder = None self.audio_feature_extractor = None self.vae_scale_factor_temporal = getattr(self.vae.config, "scale_factor_temporal", 4) self.vae_scale_factor_spatial = getattr(self.vae.config, "scale_factor_spatial", 8) self.transformer._interrupt_check = lambda: self._interrupt def get_loras_transformer(self, get_model_recursive_prop, **kwargs): if not self.is_avatar_v1_5: return [], [] sample_solver = kwargs.get("sample_solver") or self.model_def.get("sample_solver", "distill") lora_url = self.model_def.get("distill_lora_URL") if sample_solver == "distill" else None return ([lora_url], ["1.0"]) if lora_url else ([], []) def _clear_runtime_caches(self): clear = getattr(self.transformer, "clear_runtime_caches", None) if clear is not None: clear() def prepare_preview_payload(self, latents, preview_meta=None): if not torch.is_tensor(latents): return None return {"latents": latents.float()} def _apply_vae_tiling(self, VAE_tile_size): if not hasattr(self.vae, "enable_tiling"): return if VAE_tile_size is None or VAE_tile_size == 0: if hasattr(self.vae, "disable_tiling"): self.vae.disable_tiling() return if isinstance(VAE_tile_size, dict): tile = VAE_tile_size.get("tile_sample_min_size", None) else: tile = int(VAE_tile_size) if tile and tile > 0: stride = max(16, int(tile * 0.75)) self.vae.enable_tiling( tile_sample_min_height=tile, tile_sample_min_width=tile, tile_sample_stride_height=stride, tile_sample_stride_width=stride, ) def _validate_sparse_attention(self, latents): if not self.sparse_attention_enabled: return bsa_params = getattr(self.transformer.config, "bsa_params", None) or {} chunk_shape_q = bsa_params.get("chunk_3d_shape_q") chunk_shape_k = bsa_params.get("chunk_3d_shape_k") chunk_shape = chunk_shape_q or chunk_shape_k if not chunk_shape or latents.dim() != 5: self.transformer.disable_bsa() self.sparse_attention_enabled = False print("Sparse attention disabled: missing BSA parameters.") return attn_mode = offload.shared_state.get("_attention", "auto") require_grid_divisible = False if attn_mode == "flash": require_grid_divisible = True elif attn_mode == "auto": try: from shared.attention import flash_attn_bsa_3d except Exception: flash_attn_bsa_3d = None require_grid_divisible = flash_attn_bsa_3d is not None patch_t, patch_h, patch_w = self.transformer.config.patch_size n_t = latents.shape[2] // patch_t n_h = latents.shape[3] // patch_h n_w = latents.shape[4] // patch_w cp_split_hw = getattr(self.transformer.config, "cp_split_hw", None) if cp_split_hw: if n_h % cp_split_hw[0] != 0 or n_w % cp_split_hw[1] != 0: self.transformer.disable_bsa() self.sparse_attention_enabled = False print("Sparse attention disabled: cp_split_hw does not divide token grid.") return if require_grid_divisible: shape_q = chunk_shape_q or chunk_shape shape_k = chunk_shape_k or chunk_shape if ( n_t % shape_q[0] != 0 or n_h % shape_q[1] != 0 or n_w % shape_q[2] != 0 or n_t % shape_k[0] != 0 or n_h % shape_k[1] != 0 or n_w % shape_k[2] != 0 ): self.transformer.disable_bsa() self.sparse_attention_enabled = False print("Sparse attention disabled: flash BSA needs token grid divisible by chunk shape.") def _encode_prompt( self, prompt, negative_prompt, num_videos_per_prompt=1, max_length=512, device=None, dtype=None, ): device = device or self.device dtype = dtype or self.dtype def encode_fn(prompts): ids, mask = self.text_encoder.tokenizer( prompts, return_mask=True, add_special_tokens=True, ) ids = ids.to(device) mask = mask.to(device) prompt_embeds = self.text_encoder.model(ids, mask).to(dtype) return list(zip(prompt_embeds, mask)) prompt_list = [prompt] if isinstance(prompt, str) else prompt batch_size = len(prompt_list) prompt_contexts = self.text_encoder_cache.encode( encode_fn, prompt_list, device=device, ) prompt_embeds = torch.stack([ctx[0] for ctx in prompt_contexts], dim=0) mask = torch.stack([ctx[1] for ctx in prompt_contexts], dim=0) seq_len = prompt_embeds.shape[1] prompt_embeds = prompt_embeds.unsqueeze(1) if num_videos_per_prompt > 1: prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1, 1) prompt_embeds = prompt_embeds.view(batch_size * num_videos_per_prompt, 1, seq_len, -1) mask = mask.repeat(num_videos_per_prompt, 1) neg_embeds = None neg_mask = None if negative_prompt is not None: neg_list = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt if len(neg_list) == 1 and batch_size > 1: neg_list = neg_list * batch_size neg_contexts = self.text_encoder_cache.encode( encode_fn, neg_list, device=device, ) neg_embeds = torch.stack([ctx[0] for ctx in neg_contexts], dim=0) neg_mask = torch.stack([ctx[1] for ctx in neg_contexts], dim=0) neg_embeds = neg_embeds.unsqueeze(1) if num_videos_per_prompt > 1: neg_embeds = neg_embeds.repeat(1, num_videos_per_prompt, 1, 1) neg_embeds = neg_embeds.view(batch_size * num_videos_per_prompt, 1, seq_len, -1) neg_mask = neg_mask.repeat(num_videos_per_prompt, 1) return prompt_embeds, mask, neg_embeds, neg_mask def _prepare_latents( self, batch_size, num_channels_latents, height, width, num_frames, dtype, device, generator, latents=None, image=None, video=None, num_cond_frames=0, ): num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1 if latents is None: shape = ( batch_size, num_channels_latents, num_latent_frames, int(height) // self.vae_scale_factor_spatial, int(width) // self.vae_scale_factor_spatial, ) latents = torch.randn(shape, generator=generator, device=device, dtype=dtype) else: latents = latents.to(device=device, dtype=dtype) num_cond_latents = 0 if image is not None or video is not None: cond_latents = [] for i in range(batch_size): if image is not None: encoded_input = image[i].unsqueeze(0).unsqueeze(2) else: encoded_input = video[i][:, -num_cond_frames:].unsqueeze(0) latent = retrieve_latents( self.vae.encode(encoded_input), generator, sample_mode="argmax", ) cond_latents.append(latent) cond_latents = torch.cat(cond_latents, dim=0).to(dtype) cond_latents = self.normalize_latents(cond_latents) num_cond_latents = 1 + (num_cond_frames - 1) // self.vae_scale_factor_temporal latents[:, :, :num_cond_latents] = cond_latents[:, :, :num_cond_latents] return latents, num_cond_latents def normalize_latents(self, latents): latents_mean = ( torch.tensor(self.vae.config.latents_mean) .view(1, self.vae.config.z_dim, 1, 1, 1) .to(latents.device, latents.dtype) ) latents_std = ( torch.tensor(self.vae.config.latents_std) .view(1, self.vae.config.z_dim, 1, 1, 1) .to(latents.device, latents.dtype) ) return (latents - latents_mean) / latents_std def denormalize_latents(self, latents): latents_mean = ( torch.tensor(self.vae.config.latents_mean) .view(1, self.vae.config.z_dim, 1, 1, 1) .to(latents.device, latents.dtype) ) latents_std = ( torch.tensor(self.vae.config.latents_std) .view(1, self.vae.config.z_dim, 1, 1, 1) .to(latents.device, latents.dtype) ) return latents * latents_std + latents_mean def _loudness_norm(self, audio_array, sr=16000, lufs=-23, threshold=100): meter = pyln.Meter(sr) loudness = meter.integrated_loudness(audio_array) if not np.isfinite(loudness) or loudness > threshold: return audio_array return pyln.normalize.loudness(audio_array, loudness, lufs) def _add_noise_floor(self, audio_array, noise_level=0.0001): noise = np.random.normal(0, noise_level, size=audio_array.shape) return audio_array + noise def _smooth_transients(self, audio_array, sr=16000): b, a = ss.butter(3, 3000 / (sr / 2)) return ss.lfilter(b, a, audio_array) @staticmethod def _interpolate_audio_state(audio_state, target_len): if audio_state.shape[0] == target_len: return audio_state audio_state = audio_state.transpose(0, 1).unsqueeze(0).float() audio_state = F.interpolate(audio_state, size=target_len, mode="linear", align_corners=False) return audio_state.squeeze(0).transpose(0, 1) @torch.no_grad() def _get_audio_embedding_wav2vec(self, speech_array, fps=32, device="cpu", sample_rate=16000): audio_duration = len(speech_array) / sample_rate video_length = audio_duration * fps speech_array = self._loudness_norm(speech_array, sample_rate) speech_array = self._add_noise_floor(speech_array) speech_array = self._smooth_transients(speech_array, sample_rate) audio_feature = np.squeeze( self.audio_feature_extractor(speech_array, sampling_rate=sample_rate).input_values ) audio_feature = np.nan_to_num(audio_feature, nan=0.0, posinf=0.0, neginf=0.0) audio_feature = torch.from_numpy(audio_feature).float().to(device=device) audio_feature = audio_feature.unsqueeze(0) embeddings = self.audio_encoder(audio_feature, seq_len=int(video_length), output_hidden_states=True) audio_emb = torch.stack(embeddings.hidden_states[1:], dim=1).squeeze(0) audio_emb = audio_emb.permute(1, 0, 2).contiguous() audio_emb = torch.nan_to_num(audio_emb, nan=0.0, posinf=0.0, neginf=0.0) return audio_emb @torch.no_grad() def _get_audio_embedding_whisper(self, speech_array, fps=25, device=None, sample_rate=16000): device = device or self.device audio_duration = len(speech_array) / sample_rate video_length = max(int(round(audio_duration * fps)), 1) encoder_length = max(int(math.ceil(audio_duration * 50)), 1) speech_array = self._loudness_norm(speech_array, sample_rate) speech_array = self._add_noise_floor(speech_array) speech_array = self._smooth_transients(speech_array, sample_rate) layer_groups = [(0, 8), (8, 16), (16, 24), (24, 32), (32, 33)] audio_chunks = [[] for _ in layer_groups] mel_chunk = 750 * 640 for start in range(0, len(speech_array), mel_chunk): chunk = speech_array[start : start + mel_chunk] features = self.audio_feature_extractor( [chunk], return_tensors="pt", return_attention_mask=True, sampling_rate=sample_rate, ) input_features = features.input_features.to(device=device, dtype=self.dtype) attention_mask = getattr(features, "attention_mask", None) if attention_mask is not None: attention_mask = attention_mask.to(device=device) input_features = self.audio_encoder._mask_input_features(input_features, attention_mask=attention_mask) encoder_dtype = getattr(self.audio_encoder.encoder, "dtype", self.dtype) outputs = self.audio_encoder.encoder( input_features.to(encoder_dtype), head_mask=None, output_attentions=False, output_hidden_states=True, return_dict=True, ) chunk_states = outputs.hidden_states for group_idx, (layer_start, layer_end) in enumerate(layer_groups): layer_state = torch.stack(chunk_states[layer_start:layer_end], dim=0).mean(dim=0).squeeze(0) audio_chunks[group_idx].append(layer_state.detach().to("cpu")) del outputs, chunk_states, input_features, attention_mask, features audio_layers = [] for chunks in audio_chunks: layer_state = torch.cat(chunks, dim=0)[:encoder_length] layer_state = self._interpolate_audio_state(layer_state, video_length).to(dtype=self.dtype) audio_layers.append(layer_state) audio_emb = torch.stack(audio_layers, dim=1).contiguous() return torch.nan_to_num(audio_emb, nan=0.0, posinf=0.0, neginf=0.0) @torch.no_grad() def _get_audio_embedding(self, speech_array, fps=32, device="cpu", sample_rate=16000): if self.is_avatar_v1_5: return self._get_audio_embedding_whisper(speech_array, fps=fps, device=device, sample_rate=sample_rate) return self._get_audio_embedding_wav2vec(speech_array, fps=fps, device=device, sample_rate=sample_rate) def _build_audio_windows(self, audio_path, frame_num, fps, window_start_frame_no, audio_stride): speech_array, sr = librosa.load(audio_path, sr=16000) target_len = int((window_start_frame_no + frame_num) / fps * sr) if len(speech_array) < target_len: pad = target_len - len(speech_array) speech_array = np.pad(speech_array, (0, pad), mode="constant") audio_device = self.device if self.is_avatar_v1_5 else "cpu" full_audio_emb = self._get_audio_embedding(speech_array, fps=fps * audio_stride, device=audio_device, sample_rate=sr) if torch.isnan(full_audio_emb).any(): raise ValueError("Audio embedding contains NaNs.") audio_start_idx = window_start_frame_no * audio_stride audio_end_idx = audio_start_idx + audio_stride * frame_num window = self.transformer.audio_window if hasattr(self.transformer, "audio_window") else 5 offsets = torch.arange(window, device=full_audio_emb.device) - window // 2 centers = torch.arange(audio_start_idx, audio_end_idx, audio_stride, device=full_audio_emb.device).unsqueeze(1) + offsets.unsqueeze(0) centers = torch.clamp(centers, min=0, max=full_audio_emb.shape[0] - 1) audio_emb = full_audio_emb[centers][None, ...].to("cpu") del full_audio_emb return audio_emb def _build_ref_target_masks(self, height, width, speakers_bboxes=None): if not speakers_bboxes: speakers_bboxes = {"person1": [5, 10, 45, 90], "person2": [55, 10, 95, 90]} human_masks = [] background_mask = torch.zeros([height, width]) for _, person_bbox in speakers_bboxes.items(): y_min, x_min, y_max, x_max = person_bbox x_min, y_min, x_max, y_max = max(x_min, 5), max(y_min, 5), min(x_max, 95), min(y_max, 95) x_min, y_min, x_max, y_max = ( int(height * x_min / 100), int(width * y_min / 100), int(height * x_max / 100), int(width * y_max / 100), ) human_mask = torch.zeros([height, width]) human_mask[int(x_min) : int(x_max), int(y_min) : int(y_max)] = 1 background_mask += human_mask human_masks.append(human_mask) background_mask = torch.where(background_mask > 0, torch.tensor(0), torch.tensor(1)) human_masks.append(background_mask) return torch.stack(human_masks, dim=0) def get_timesteps_sigmas(self, sampling_steps, use_distill=False): if use_distill: distill_indices = torch.arange(1, self.num_distill_sample_steps + 1, dtype=torch.float32) distill_indices = (distill_indices * (self.num_timesteps // self.num_distill_sample_steps)).round().long() if self.is_avatar_v1_5: distill_indices = self.num_timesteps - distill_indices sigmas = torch.flip(torch.linspace(0, 1, self.num_timesteps), [0]) sigmas = torch.flip(sigmas[distill_indices], [0]).float() if sampling_steps != self.num_distill_sample_steps: inference_indices = np.linspace(0, self.num_distill_sample_steps, num=sampling_steps, endpoint=False) sigmas = sigmas[np.floor(inference_indices).astype(np.int64)] else: inference_indices = np.linspace(0, self.num_distill_sample_steps, num=sampling_steps, endpoint=False) inference_indices = np.floor(inference_indices).astype(np.int64) sigmas = torch.flip(distill_indices, [0])[inference_indices].float() / self.num_timesteps else: sigmas = torch.linspace(1, 0.001, sampling_steps, dtype=torch.float32) return sigmas.to(dtype=torch.float32, device="cpu") @torch.no_grad() def generate( self, seed=None, input_prompt="", n_prompt="", sampling_steps=50, input_ref_images=None, input_frames=None, input_frames2=None, input_masks=None, input_masks2=None, input_video=None, image_start=None, image_end=None, input_ref_masks=None, input_faces=None, input_custom=None, frame_num=93, batch_size=1, height=480, width=832, fit_into_canvas=None, alt_prompt=None, guide_scale=4.0, guide2_scale=None, guide3_scale=None, shift=None, audio_cfg_scale=None, joint_pass=False, VAE_tile_size=None, prefix_frames_count=0, conditioning_latents_size=0, callback=None, embedded_guidance_scale=None, enable_RIFLEx=None, cfg_star_switch=False, cfg_zero_step=-1, apg_switch=None, perturbation_switch=None, perturbation_layers=None, perturbation_start=None, perturbation_end=None, switch_threshold=None, switch2_threshold=None, guide_phases=None, model_switch_phase=None, alt_guide_scale=None, input_waveform=None, input_waveform_sample_rate=None, audio_guide=None, audio_guide2=None, audio_prompt_type=None, audio_proj=None, audio_scale=None, audio_context_lens=None, context_scale=None, control_scale_alt=None, alt_scale=None, motion_amplitude=None, model_mode=None, causal_block_size=None, causal_attention=None, fps=None, window_start_frame_no=0, sample_solver=None, reference_image_enabled=None, ref_img_index=None, mask_frame_range=None, overlapped_latents=None, return_latent_slice=None, speakers_bboxes=None, window_no=None, overlap_noise=None, overlap_size=None, color_correction_strength=None, input_video_is_hdr=False, lora_dir=None, keep_frames_parsed=None, model_filename=None, model_type=None, loras_slists=None, NAG_scale=None, NAG_tau=None, NAG_alpha=None, image_mode=None, video_prompt_type=None, offloadobj=None, set_header_text=None, pre_video_frame=None, prefix_video=None, original_input_ref_images=None, image_refs_relative_size=None, outpainting_dims=None, face_arc_embeds=None, custom_settings=None, **kwargs, ): if self._interrupt: return None if seed is None or seed == -1: seed = torch.seed() % (2**32 - 1) generator = torch.Generator(device=self.device) generator.manual_seed(seed) if fps is None or fps == 0: fps = self.model_def.get("fps", 15 if not self.is_avatar else 16) if frame_num % self.vae_scale_factor_temporal != 1: frame_num = frame_num // self.vae_scale_factor_temporal * self.vae_scale_factor_temporal + 1 frame_num = max(frame_num, 1) sample_solver = self.model_def.get("sample_solver", "auto") if sample_solver is None else sample_solver if sample_solver in (None, ""): sample_solver = "default" prompt_embeds, prompt_mask, neg_embeds, neg_mask = self._encode_prompt( input_prompt, n_prompt if n_prompt is not None else "", device=self.device, dtype=self.dtype, ) any_guidance = guide_scale is not None and guide_scale > 1 if self.is_avatar: if audio_cfg_scale is None: audio_cfg_scale = 1.0 any_guidance = any_guidance or audio_cfg_scale > 1 if reference_image_enabled is None: reference_image_enabled = self.model_def.get("reference_image_enabled", True) reference_image_enabled = self.is_avatar and bool(reference_image_enabled) reference_features_enabled = reference_image_enabled ref_img_index = self.model_def.get("ref_img_index", 10) if ref_img_index is None else ref_img_index mask_frame_range = self.model_def.get("mask_frame_range", 3) if mask_frame_range is None else mask_frame_range if not reference_features_enabled: ref_img_index = None mask_frame_range = None ref_image = None if reference_image_enabled: if input_ref_images is not None: ref_list = input_ref_images if isinstance(input_ref_images, list) else [input_ref_images] if len(ref_list) > 0: ref_image = ref_list[0] if window_no == 1: if ref_image is not None: self._reference_image = ( ref_image.detach().to("cpu") if torch.is_tensor(ref_image) else ref_image ) else: self._reference_image = None if ref_image is None and self._reference_image is not None: ref_image = self._reference_image cond_video = None num_cond_frames = 0 if input_video is not None: cond_video = input_video num_cond_frames = max(int(prefix_frames_count or 0), 0) self._apply_vae_tiling(VAE_tile_size) if cond_video is not None: cond_video = cond_video.to(device=self.device, dtype=self.VAE_dtype) if cond_video.dim() == 4: cond_video = cond_video.unsqueeze(0) cond_video_frames = cond_video.shape[2] if num_cond_frames <= 0: cond_video = None num_cond_frames = 0 else: num_cond_frames = min(num_cond_frames, cond_video_frames) if sample_solver not in ("auto", "default", "enhance_hf", "distill"): raise ValueError(f"Unsupported scheduler '{sample_solver}' for LongCat.") if self.model_def.get("distill_only", False) and sample_solver != "distill": raise ValueError("LongCat Avatar 1.5 currently supports the distilled scheduler only.") use_distill = sample_solver == "distill" enhance_hf = sample_solver == "enhance_hf" if sample_solver == "auto": enhance_hf = cond_video is not None and num_cond_frames > 1 if use_distill and enhance_hf: raise ValueError("distill and enhance_hf schedules cannot both be enabled.") image_cond = None ref_latent = None num_ref_latents = 0 if reference_image_enabled and ref_image is not None: if not torch.is_tensor(ref_image): ref_image = torch.from_numpy(np.array(ref_image)).float().div_(127.5).sub_(1.).movedim(-1, 0) ref_image = ref_image.to(device=self.device, dtype=self.VAE_dtype) if ref_image.dim() == 3: ref_image = ref_image.unsqueeze(0) if ref_image.dim() == 5 and ref_image.shape[2] == 1: ref_image = ref_image.squeeze(2) if ref_image.dim() != 4: raise ValueError("reference image must be CHW or BCHW for LongCat.") if ref_image.shape[0] == 1 and batch_size > 1: ref_image = ref_image.repeat(batch_size, 1, 1, 1) elif ref_image.shape[0] != batch_size: raise ValueError("reference image batch size does not match prompts.") if cond_video is None: image_cond = ref_image else: ref_image_5d = ref_image.unsqueeze(2) ref_latent = retrieve_latents(self.vae.encode(ref_image_5d), generator, sample_mode="argmax") ref_latent = self.normalize_latents(ref_latent).to(torch.float32) num_ref_latents = 1 if torch.is_tensor(overlapped_latents): if overlapped_latents.dim() == 4: overlapped_latents = overlapped_latents.unsqueeze(0) if overlapped_latents.dim() != 5: overlapped_latents = None else: overlapped_latents = None cond_image_frames = 1 if image_cond is not None else num_cond_frames expected_num_cond_latents = ( 1 + (cond_image_frames - 1) // self.vae_scale_factor_temporal if cond_image_frames > 0 else 0 ) use_overlap_latents = ( overlapped_latents is not None and expected_num_cond_latents > 0 and image_cond is None ) if use_overlap_latents: lat_h = int(height) // self.vae_scale_factor_spatial lat_w = int(width) // self.vae_scale_factor_spatial if ( overlapped_latents.shape[1] != self.transformer.config.in_channels or overlapped_latents.shape[3] != lat_h or overlapped_latents.shape[4] != lat_w ): use_overlap_latents = False if use_overlap_latents: num_latent_frames = (frame_num - 1) // self.vae_scale_factor_temporal + 1 shape = ( batch_size, self.transformer.config.in_channels, num_latent_frames, lat_h, lat_w, ) latents = torch.randn(shape, generator=generator, device=self.device, dtype=torch.float32) overlap_latents = overlapped_latents.to(device=self.device, dtype=torch.float32) if overlap_latents.shape[0] == 1 and batch_size > 1: overlap_latents = overlap_latents.repeat(batch_size, 1, 1, 1, 1) if overlap_latents.shape[2] > expected_num_cond_latents: overlap_latents = overlap_latents[:, :, -expected_num_cond_latents:] cond_latents = None if cond_video is not None and overlap_latents.shape[2] < expected_num_cond_latents: cond_latents_list = [] for i in range(batch_size): encoded_input = cond_video[i][:, -cond_image_frames:].unsqueeze(0) latent = retrieve_latents( self.vae.encode(encoded_input), generator, sample_mode="argmax", ) cond_latents_list.append(latent) cond_latents = torch.cat(cond_latents_list, dim=0).to(torch.float32) cond_latents = self.normalize_latents(cond_latents) overlap_len = min(overlap_latents.shape[2], cond_latents.shape[2]) if overlap_len > 0: cond_latents[:, :, -overlap_len:] = overlap_latents[:, :, -overlap_len:] else: cond_latents = overlap_latents num_cond_latents = min(cond_latents.shape[2], num_latent_frames) if cond_latents is not None else 0 if num_cond_latents > 0: latents[:, :, :num_cond_latents] = cond_latents[:, :, -num_cond_latents:] else: latents, num_cond_latents = self._prepare_latents( batch_size=batch_size, num_channels_latents=self.transformer.config.in_channels, height=height, width=width, num_frames=frame_num, dtype=torch.float32, device=self.device, generator=generator, latents=None, image=image_cond, video=None if image_cond is not None else cond_video, num_cond_frames=cond_image_frames, ) if reference_image_enabled and ref_latent is None and self.is_avatar and num_cond_latents > 1: ref_latent = latents[:, :, :1].clone() num_ref_latents = 1 if ref_latent is not None: num_cond_latents += num_ref_latents latents = torch.cat([ref_latent, latents], dim=2) self._validate_sparse_attention(latents) sigmas = self.get_timesteps_sigmas(sampling_steps, use_distill=use_distill) self.scheduler.set_timesteps(sampling_steps, sigmas=sigmas, device=self.device) timesteps = self.scheduler.timesteps if enhance_hf: num_tail_uniform_steps = max(3, min(15, int(len(timesteps) * 0.2))) tail_uniform_start = float(timesteps.max()) * 0.5 tail_uniform_end = 0 timesteps_uniform_tail = list( np.linspace( tail_uniform_start, tail_uniform_end, num_tail_uniform_steps, dtype=np.float32, endpoint=(tail_uniform_end != 0), ) ) timesteps_uniform_tail = [ torch.tensor(t, device=self.device, dtype=torch.float32).unsqueeze(0) for t in timesteps_uniform_tail ] filtered_timesteps = [ timestep.unsqueeze(0).to(self.device) for timestep in timesteps if timestep > tail_uniform_start ] timesteps = torch.cat(filtered_timesteps + timesteps_uniform_tail) self.scheduler.timesteps = timesteps self.scheduler.sigmas = torch.cat( [timesteps / self.num_timesteps, torch.zeros(1, device=timesteps.device)] ) audio_emb = None ref_target_masks = None if self.is_avatar: if audio_guide is None: raise ValueError("Audio guide is required for LongCat Avatar.") audio_stride = int(self.model_def.get("audio_stride", 1 if self.is_avatar_v1_5 else 2)) audio_emb = self._build_audio_windows( audio_guide, frame_num, fps, window_start_frame_no, audio_stride ) if audio_guide2 is not None or self.model_def.get("multi_speakers_only", False): if audio_guide2 is None: raise ValueError("Second audio guide is required for LongCat Avatar Multi.") audio_emb2 = self._build_audio_windows( audio_guide2, frame_num, fps, window_start_frame_no, audio_stride ) audio_emb = torch.cat([audio_emb, audio_emb2], dim=0) ref_target_masks = self._build_ref_target_masks(height, width, speakers_bboxes) if ref_target_masks is not None: ref_target_masks = ref_target_masks.to(self.device) if self.is_avatar_v1_5 and offloadobj is not None: offloadobj.unload_all() audio_emb = audio_emb.to(self.device, dtype=self.dtype) latents = latents.to(self.device, dtype=self.dtype) prompt_embeds = prompt_embeds.to(self.device) prompt_mask = prompt_mask.to(self.device) if neg_embeds is None: neg_embeds = prompt_embeds neg_mask = prompt_mask else: neg_embeds = neg_embeds.to(self.device) neg_mask = neg_mask.to(self.device) ref_kwargs = {} if self.is_avatar and num_ref_latents > 0: ref_kwargs = { "num_ref_latents": num_ref_latents, "ref_img_index": ref_img_index, "mask_frame_range": mask_frame_range, } callback(-1, None, True, override_num_inference_steps = len(timesteps)) with tqdm(total=len(timesteps), desc="Denoising") as progress_bar: for i, t in enumerate(timesteps): if self._interrupt: return None def _aborted(outputs): if outputs is None: return True if isinstance(outputs, (list, tuple)): return any(item is None for item in outputs) return outputs is None timestep = t.expand(latents.shape[0]).to(self.dtype) if num_cond_latents > 0: timestep = timestep[:, None].expand(-1, latents.shape[2]).clone() timestep[:, :num_cond_latents] = 0 if self.is_avatar and audio_emb is not None and any_guidance: audio_cond = audio_emb.to(self.device, dtype=self.dtype) audio_uncond = torch.zeros_like(audio_cond) x_list = [latents, latents, latents] ctx_list = [prompt_embeds, neg_embeds, neg_embeds] mask_list = [prompt_mask, neg_mask, neg_mask] audio_list = [audio_cond, audio_cond, audio_uncond] ref_list = [ref_target_masks, ref_target_masks, ref_target_masks] if joint_pass: outputs = self.transformer( hidden_states=x_list, timestep=[timestep] * len(x_list), encoder_hidden_states=ctx_list, encoder_attention_mask=mask_list, num_cond_latents=[num_cond_latents] * len(x_list), audio_embs=audio_list, ref_target_masks=ref_list, **ref_kwargs, ) if _aborted(outputs): return None else: outputs = [] for x_i, ctx_i, mask_i, audio_i, ref_i in zip( x_list, ctx_list, mask_list, audio_list, ref_list ): output = self.transformer( hidden_states=x_i, timestep=timestep, encoder_hidden_states=ctx_i, encoder_attention_mask=mask_i, num_cond_latents=num_cond_latents, audio_embs=audio_i, ref_target_masks=ref_i, **ref_kwargs, ) if _aborted(output): return None outputs.append(output) noise_pred_cond, noise_pred_uncond_text, noise_pred_uncond = outputs noise_pred = ( noise_pred_uncond + guide_scale * (noise_pred_cond - noise_pred_uncond_text) + audio_cfg_scale * (noise_pred_uncond_text - noise_pred_uncond) ) elif any_guidance: x_list = [latents, latents] ctx_list = [prompt_embeds, neg_embeds] mask_list = [prompt_mask, neg_mask] if joint_pass: outputs = self.transformer( hidden_states=x_list, timestep=[timestep] * len(x_list), encoder_hidden_states=ctx_list, encoder_attention_mask=mask_list, num_cond_latents=[num_cond_latents] * len(x_list), **ref_kwargs, ) if _aborted(outputs): return None else: outputs = [] for x_i, ctx_i, mask_i in zip(x_list, ctx_list, mask_list): output = self.transformer( hidden_states=x_i, timestep=timestep, encoder_hidden_states=ctx_i, encoder_attention_mask=mask_i, num_cond_latents=num_cond_latents, **ref_kwargs, ) if _aborted(output): return None outputs.append(output) noise_pred_cond, noise_pred_uncond = outputs if cfg_star_switch: positive_flat = noise_pred_cond.view(latents.shape[0], -1) negative_flat = noise_pred_uncond.view(latents.shape[0], -1) st_star = optimized_scale(positive_flat, negative_flat).view(latents.shape[0], 1, 1, 1) if cfg_zero_step >= 0 and i <= cfg_zero_step: noise_pred = noise_pred_cond * 0.0 else: noise_pred_uncond = noise_pred_uncond * st_star noise_pred = noise_pred_uncond + guide_scale * (noise_pred_cond - noise_pred_uncond) else: noise_pred = noise_pred_uncond + guide_scale * (noise_pred_cond - noise_pred_uncond) else: noise_pred = self.transformer( hidden_states=latents, timestep=timestep, encoder_hidden_states=prompt_embeds, encoder_attention_mask=prompt_mask, num_cond_latents=num_cond_latents, audio_embs=audio_emb if self.is_avatar else None, ref_target_masks=ref_target_masks if self.is_avatar else None, **ref_kwargs, ) if _aborted(noise_pred): return None noise_pred = -noise_pred if num_cond_latents > 0: latents[:, :, num_cond_latents:] = self.scheduler.step( noise_pred[:, :, num_cond_latents:], t, latents[:, :, num_cond_latents:], return_dict=False, )[0] else: latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0] if callback is not None: callback(i, latents.squeeze(0)) progress_bar.update() if num_ref_latents > 0: latents = latents[:, :, num_ref_latents:] num_cond_latents -= num_ref_latents latent_slice = None if return_latent_slice is not None: latent_slice = latents[:, :, return_latent_slice].detach().to("cpu") latents = latents.to(self.vae.dtype) latents = self.denormalize_latents(latents) video = self.vae.decode(latents, return_dict=False)[0].clamp(-1, 1) if video.dim() == 5: video = video[0] self._clear_runtime_caches() if latent_slice is not None: return {"x": video, "latent_slice": latent_slice} return video