naicoi commited on
Commit
98eeefd
Β·
1 Parent(s): 0957dd2
app.py CHANGED
@@ -88,12 +88,6 @@ with gr.Blocks(css=css) as demo:
88
  audio_input = gr.Audio(
89
  label="Target Audio (English only)", type="filepath"
90
  )
91
- model_radio = gr.Radio(
92
- label="Lipsync Model",
93
- choices=["LatentSync v1.6", "MuseTalk v1.5"],
94
- value="LatentSync v1.6",
95
- interactive=True,
96
- )
97
  lipsync_only_btn = gr.Button("πŸ‘„ Lipsync", variant="primary", size="lg")
98
 
99
  with gr.Row():
@@ -116,7 +110,7 @@ with gr.Blocks(css=css) as demo:
116
 
117
  lipsync_only_btn.click(
118
  fn=lipsync_with_audio_target,
119
- inputs=[video_input, audio_input, session_state, model_radio],
120
  outputs=[
121
  final_video,
122
  video_normalized_output,
 
88
  audio_input = gr.Audio(
89
  label="Target Audio (English only)", type="filepath"
90
  )
 
 
 
 
 
 
91
  lipsync_only_btn = gr.Button("πŸ‘„ Lipsync", variant="primary", size="lg")
92
 
93
  with gr.Row():
 
110
 
111
  lipsync_only_btn.click(
112
  fn=lipsync_with_audio_target,
113
+ inputs=[video_input, audio_input, session_state],
114
  outputs=[
115
  final_video,
116
  video_normalized_output,
lipsync.py CHANGED
@@ -3,62 +3,16 @@ import os
3
  import traceback
4
  from glob import glob
5
  import torch
6
- from omegaconf import OmegaConf
7
- from diffusers import AutoencoderKL, DDIMScheduler
8
- from huggingface_hub import snapshot_download
9
  from diffusers.utils.import_utils import is_xformers_available
10
  from accelerate.utils import set_seed
11
- from latentsync.models.unet import UNet3DConditionModel
12
  from latentsync.pipelines.lipsync_pipeline import LipsyncPipeline
13
  from latentsync.whisper.audio2feature import Audio2Feature
 
14
 
15
  torch.backends.cudnn.benchmark = True
16
  torch.backends.cudnn.deterministic = False
17
 
18
  os.makedirs("checkpoints", exist_ok=True)
19
- snapshot_download(repo_id="ByteDance/LatentSync-1.6", local_dir="./checkpoints")
20
-
21
- unet_config_path = "configs/unet/stage2_512.yaml"
22
- config = OmegaConf.load(unet_config_path)
23
-
24
- inference_ckpt_path = "checkpoints/latentsync_unet.pt"
25
-
26
- scheduler = DDIMScheduler.from_pretrained("configs")
27
-
28
- if config.model.cross_attention_dim == 768:
29
- whisper_model_path = "checkpoints/whisper/small.pt"
30
- elif config.model.cross_attention_dim == 384:
31
- whisper_model_path = "checkpoints/whisper/tiny.pt"
32
- else:
33
- raise NotImplementedError("cross_attention_dim must be 768 or 384")
34
-
35
- audio_encoder = Audio2Feature(
36
- model_path=whisper_model_path, device="cuda", num_frames=config.data.num_frames
37
- )
38
-
39
- vae = AutoencoderKL.from_pretrained(
40
- "stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16
41
- )
42
- vae.config.scaling_factor = 0.18215
43
- vae.config.shift_factor = 0
44
-
45
- unet, _ = UNet3DConditionModel.from_pretrained(
46
- OmegaConf.to_container(config.model),
47
- inference_ckpt_path,
48
- device="cpu",
49
- )
50
-
51
- unet = unet.to(dtype=torch.float16)
52
-
53
- if is_xformers_available():
54
- unet.enable_xformers_memory_efficient_attention()
55
-
56
- pipeline = LipsyncPipeline(
57
- vae=vae,
58
- audio_encoder=audio_encoder,
59
- unet=unet,
60
- scheduler=scheduler,
61
- ).to("cuda")
62
 
63
 
64
  def get_gpu_memory_info():
@@ -121,6 +75,25 @@ def apply_lipsync(video_input_path, audio_path, video_out_path, crop_size=256):
121
 
122
  print(f"GPU Memory Before: {get_gpu_memory_info()}")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  try:
125
  if not torch.cuda.is_available():
126
  raise RuntimeError("CUDA not available - GPU required for lipsync")
 
3
  import traceback
4
  from glob import glob
5
  import torch
 
 
 
6
  from diffusers.utils.import_utils import is_xformers_available
7
  from accelerate.utils import set_seed
 
8
  from latentsync.pipelines.lipsync_pipeline import LipsyncPipeline
9
  from latentsync.whisper.audio2feature import Audio2Feature
10
+ from shared.model_manager import ModelManager
11
 
12
  torch.backends.cudnn.benchmark = True
13
  torch.backends.cudnn.deterministic = False
14
 
15
  os.makedirs("checkpoints", exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  def get_gpu_memory_info():
 
75
 
76
  print(f"GPU Memory Before: {get_gpu_memory_info()}")
77
 
78
+ manager = ModelManager.get_instance()
79
+
80
+ config = manager.get_latentsync_config()
81
+ vae = manager.load_vae()
82
+ audio_encoder = manager.load_whisper_encoder(
83
+ manager.get_whisper_model_path(config.model.cross_attention_dim),
84
+ "cuda",
85
+ config.data.num_frames,
86
+ )
87
+ unet = manager.load_latentsync_unet()
88
+ scheduler = manager.get_scheduler()
89
+
90
+ pipeline = LipsyncPipeline(
91
+ vae=vae,
92
+ audio_encoder=audio_encoder,
93
+ unet=unet,
94
+ scheduler=scheduler,
95
+ ).to("cuda")
96
+
97
  try:
98
  if not torch.cuda.is_available():
99
  raise RuntimeError("CUDA not available - GPU required for lipsync")
lipsync_processing.py CHANGED
@@ -67,17 +67,17 @@ def apply_lipsync_to_video(
67
  if model_type == "LatentSync v1.6":
68
  crop_size = 512
69
  print(
70
- f"Using LatentSync v1.6: video={video_path}, audio={audio_16k_path}, crop_size={crop_size}"
71
  )
72
  apply_lipsync(video_path, audio_16k_path, lipsynced_video, crop_size)
73
 
74
  elif model_type == "MuseTalk v1.5":
 
 
75
  crop_size = 256
76
  print(
77
  f"Using MuseTalk v1.5: video={video_path}, audio={audio_16k_path}, crop_size={crop_size}"
78
  )
79
- from musetalk import apply_musetalk_lipsync
80
-
81
  apply_musetalk_lipsync(video_path, audio_16k_path, lipsynced_video)
82
 
83
  else:
 
67
  if model_type == "LatentSync v1.6":
68
  crop_size = 512
69
  print(
70
+ f"Using LatentSync: video={video_path}, audio={audio_16k_path}, crop_size={crop_size}"
71
  )
72
  apply_lipsync(video_path, audio_16k_path, lipsynced_video, crop_size)
73
 
74
  elif model_type == "MuseTalk v1.5":
75
+ from musetalk import apply_musetalk_lipsync
76
+
77
  crop_size = 256
78
  print(
79
  f"Using MuseTalk v1.5: video={video_path}, audio={audio_16k_path}, crop_size={crop_size}"
80
  )
 
 
81
  apply_musetalk_lipsync(video_path, audio_16k_path, lipsynced_video)
82
 
83
  else:
musetalk.py CHANGED
@@ -1,4 +1,4 @@
1
- """MuseTalk V1.5 integration module"""
2
 
3
  import os
4
  import numpy as np
@@ -9,68 +9,16 @@ import math
9
  import subprocess
10
  from tqdm import tqdm
11
  from glob import glob
12
-
13
- from transformers import WhisperModel
14
-
15
- from musetalk_integration.models.unet import UNet, PositionalEncoding
16
- from musetalk_integration.models.vae import VAE
17
- from musetalk_integration.utils.audio_processor import AudioProcessor
18
- from musetalk_integration.utils.face_parsing import FaceParsing
19
- from musetalk_integration.utils.blending import get_image
20
- from musetalk_integration.utils.preprocessing import (
21
- get_landmark_and_bbox,
22
- read_imgs,
23
- coord_placeholder,
24
- )
25
- from musetalk_integration.utils.utils import datagen, get_video_fps
26
-
27
 
28
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
29
 
30
 
31
- def load_musetalk_models():
32
- """Load MuseTalk V1.5 models"""
33
- print("Loading MuseTalk V1.5 models...")
34
-
35
- vae = VAE(model_path="checkpoints/sd-vae-ft-mse")
36
- print("βœ“ VAE loaded")
37
-
38
- unet = UNet(
39
- unet_config="checkpoints/musetalkV15/musetalk.json",
40
- model_path="checkpoints/musetalkV15/unet.pth",
41
- device=device,
42
- )
43
- print("βœ“ UNet loaded")
44
-
45
- pe = PositionalEncoding(d_model=384)
46
- print("βœ“ Positional encoding loaded")
47
-
48
- audio_processor = AudioProcessor(
49
- feature_extractor_path="checkpoints/whisper-tiny"
50
- )
51
- print("βœ“ Audio processor loaded")
52
-
53
- whisper = WhisperModel.from_pretrained("checkpoints/whisper-tiny")
54
- whisper = whisper.to(device=device, dtype=torch.float16).eval()
55
- whisper.requires_grad_(False)
56
- print("βœ“ Whisper model loaded")
57
-
58
- fp = None
59
- print("βœ“ Face parser skipped (models not available)")
60
-
61
- timesteps = torch.tensor([0], device=device)
62
-
63
- return vae, unet, pe, audio_processor, whisper, fp, timesteps
64
-
65
-
66
- vae, unet, pe, audio_processor, whisper, fp, timesteps = load_musetalk_models()
67
-
68
-
69
  @torch.no_grad()
70
  def apply_musetalk_lipsync(
71
  video_path: str, audio_path: str, video_out_path: str, progress=None
72
  ):
73
- """Apply MuseTalk V1.5 lipsync
74
 
75
  Args:
76
  video_path: Path to input video
@@ -79,12 +27,17 @@ def apply_musetalk_lipsync(
79
  progress: Progress object
80
  """
81
  print(f"\n{'=' * 60}")
82
- print(f"MUSETALK V1.5 START")
83
  print(f"Video: {video_path}")
84
  print(f"Audio: {audio_path}")
85
  print(f"Output: {video_out_path}")
86
  print(f"{'=' * 60}\n")
87
 
 
 
 
 
 
88
  output_dir = os.path.dirname(video_out_path)
89
 
90
  # 1. Extract frames
@@ -96,103 +49,217 @@ def apply_musetalk_lipsync(
96
  os.system(cmd)
97
 
98
  input_img_list = sorted(glob(os.path.join(save_dir_full, "*.[jpJP][pnPN]*[gG]")))
99
- fps = get_video_fps(video_path)
 
 
 
 
 
 
100
  print(f"Extracted {len(input_img_list)} frames at {fps} fps")
101
 
102
- # 2. Extract audio features
103
  print("Extracting audio features...")
104
- whisper_input_features, librosa_length = audio_processor.get_audio_feature(
105
- audio_path
106
- )
107
- whisper_chunks = audio_processor.get_whisper_chunk(
108
- whisper_input_features,
109
- device,
110
- torch.float16,
111
- whisper,
112
- librosa_length,
113
- fps=fps,
114
- audio_padding_length_left=2,
115
- audio_padding_length_right=2,
116
- )
117
- print(f"Generated {len(whisper_chunks)} audio chunks")
118
-
119
- # 3. Detect face landmarks
120
- print("Extracting landmarks...")
121
- coord_list, frame_list = get_landmark_and_bbox(input_img_list, bbox_shift=0)
122
- print(f"Detected {len(coord_list)} face landmarks")
123
-
124
- # 4. VAE encode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  print("Encoding frames to latents...")
126
  input_latent_list = []
127
- for bbox, frame in zip(coord_list, frame_list):
128
- if bbox == coord_placeholder:
 
129
  continue
 
130
  x1, y1, x2, y2 = bbox
131
- y2 = y2 + 10
132
- y2 = min(y2, frame.shape[0])
133
  crop_frame = frame[y1:y2, x1:x2]
 
 
134
  crop_frame = cv2.resize(
135
  crop_frame, (256, 256), interpolation=cv2.INTER_LANCZOS4
136
  )
137
- latents = vae.get_latents_for_unet(crop_frame)
138
- input_latent_list.append(latents)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  print(f"Encoded {len(input_latent_list)} frames")
140
 
141
  # 5. Cycle frames for smoothing
142
  frame_list_cycle = frame_list + frame_list[::-1]
143
- coord_list_cycle = coord_list + coord_list[::-1]
144
  input_latent_list_cycle = input_latent_list + input_latent_list[::-1]
145
 
146
  # 6. Batch inference
147
  print("Starting inference...")
148
  batch_size = 8
149
- gen = datagen(
150
- whisper_chunks=whisper_chunks,
151
- vae_encode_latents=input_latent_list_cycle,
152
- batch_size=batch_size,
153
- delay_frame=0,
154
- device=device,
155
- )
156
 
157
  res_frame_list = []
158
- for whisper_batch, latent_batch in tqdm(
159
- gen, total=int(math.ceil(len(whisper_chunks) / batch_size))
160
- ):
161
- audio_feature_batch = pe(whisper_batch)
162
- latent_batch = latent_batch.to(dtype=torch.float16)
163
 
164
- pred_latents = unet.model(
 
 
 
 
165
  latent_batch, timesteps, encoder_hidden_states=audio_feature_batch
166
  ).sample
167
- recon = vae.decode_latents(pred_latents)
168
- for res_frame in recon:
169
- res_frame_list.append(res_frame)
 
 
 
 
170
  print(f"Generated {len(res_frame_list)} frames")
171
 
172
- # 7. Blend back to original video
173
  print("Blending...")
174
  output_frames_dir = os.path.join(output_dir, f"{input_basename}_output")
175
  os.makedirs(output_frames_dir, exist_ok=True)
176
 
177
  for i, res_frame in enumerate(tqdm(res_frame_list)):
178
- bbox = coord_list_cycle[i % len(coord_list_cycle)]
179
- ori_frame = copy.deepcopy(frame_list_cycle[i % len(frame_list_cycle)])
 
 
 
 
180
  x1, y1, x2, y2 = bbox
181
- y2 = y2 + 10
182
- y2 = min(y2, ori_frame.shape[0])
183
 
184
  try:
185
- res_frame = cv2.resize(res_frame.astype(np.uint8), (x2 - x1, y2 - y1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  except Exception as e:
187
- print(f"Warning: Could not resize frame {i}: {e}")
188
  continue
189
 
190
- # MuseTalk v1.5 blending with jaw mode (default)
191
- combine_frame = get_image(
192
- ori_frame, res_frame, [x1, y1, x2, y2], mode="raw"
193
- )
194
- cv2.imwrite(f"{output_frames_dir}/{i:08d}.png", combine_frame)
195
-
196
  # 8. Encode to video
197
  print("Encoding video...")
198
  cmd = f"ffmpeg -y -v warning -r {fps} -f image2 -i {output_frames_dir}/%08d.png -vcodec libx264 -vf format=yuv420p -crf 18 {video_out_path}"
@@ -212,7 +279,7 @@ def apply_musetalk_lipsync(
212
  shutil.rmtree(output_frames_dir)
213
 
214
  print(f"\n{'=' * 60}")
215
- print(f"MUSETALK V1.5 SUCCESS")
216
  print(f"Output: {video_out_path}")
217
  print(f"{'=' * 60}\n")
218
 
 
1
+ """MuseTalk V1.5 integration module (simplified - uses shared Whisper & VAE)"""
2
 
3
  import os
4
  import numpy as np
 
9
  import subprocess
10
  from tqdm import tqdm
11
  from glob import glob
12
+ from shared.model_manager import ModelManager
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
15
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  @torch.no_grad()
18
  def apply_musetalk_lipsync(
19
  video_path: str, audio_path: str, video_out_path: str, progress=None
20
  ):
21
+ """Apply MuseTalk V1.5 lipsync (simplified - no face parsing)
22
 
23
  Args:
24
  video_path: Path to input video
 
27
  progress: Progress object
28
  """
29
  print(f"\n{'=' * 60}")
30
+ print(f"MUSETALK V1.5 START (simplified)")
31
  print(f"Video: {video_path}")
32
  print(f"Audio: {audio_path}")
33
  print(f"Output: {video_out_path}")
34
  print(f"{'=' * 60}\n")
35
 
36
+ manager = ModelManager.get_instance()
37
+ vae = manager.load_vae()
38
+ unet, pe = manager.load_musetalk_unet()
39
+ timesteps = torch.tensor([0], device=device)
40
+
41
  output_dir = os.path.dirname(video_out_path)
42
 
43
  # 1. Extract frames
 
49
  os.system(cmd)
50
 
51
  input_img_list = sorted(glob(os.path.join(save_dir_full, "*.[jpJP][pnPN]*[gG]")))
52
+
53
+ # Get FPS
54
+ import subprocess
55
+
56
+ cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=noprint_wrappers=1:nokey=1 {video_path}"
57
+ fps_str = subprocess.check_output(cmd, shell=True).decode().strip()
58
+ fps = float(fps_str.split("/")[0])
59
  print(f"Extracted {len(input_img_list)} frames at {fps} fps")
60
 
61
+ # 2. Extract audio features using shared Whisper encoder
62
  print("Extracting audio features...")
63
+
64
+ import librosa
65
+
66
+ audio_input, librosa_length = librosa.load(audio_path, sr=16000)
67
+ assert librosa_length / 16000 == len(audio_input), "Audio length mismatch"
68
+
69
+ # Get audio chunks (reimplement simple version)
70
+ import torch
71
+
72
+ audio_chunks = []
73
+
74
+ # Simple chunking (25 FPS -> 50 audio FPS)
75
+ num_frames = int((librosa_length / 16000) * fps)
76
+ samples_per_frame = int(16000 / fps)
77
+
78
+ # Process audio in chunks of 100ms
79
+ chunk_size = int(0.1 * 16000)
80
+
81
+ for i in range(0, num_frames):
82
+ start_sample = int(i * samples_per_frame)
83
+ # Add context frames (2 frames before, 2 frames after)
84
+ context_start = max(0, start_sample - 2 * samples_per_frame)
85
+ context_end = min(len(audio_input), start_sample + 5 * samples_per_frame)
86
+
87
+ chunk = audio_input[context_start:context_end]
88
+
89
+ # Pad if necessary
90
+ if len(chunk) < chunk_size:
91
+ chunk = np.pad(chunk, (0, chunk_size - len(chunk)), mode="constant")
92
+
93
+ # Convert to tensor
94
+ chunk_tensor = torch.from_numpy(chunk).unsqueeze(0)
95
+
96
+ audio_chunks.append(chunk_tensor)
97
+
98
+ print(f"Generated {len(audio_chunks)} audio chunks")
99
+
100
+ # 3. Simple face detection using insightface
101
+ print("Detecting faces...")
102
+ coords_list = []
103
+ frame_list = []
104
+
105
+ from insightface.app import FaceAnalysis
106
+
107
+ face_analysis = FaceAnalysis()
108
+
109
+ for img_path in tqdm(input_img_list):
110
+ frame = cv2.imread(img_path)
111
+
112
+ try:
113
+ faces = face_analysis.get(frame)
114
+
115
+ if len(faces) > 0:
116
+ bbox = faces[0].bbox
117
+ x1, y1, x2, y2 = [int(v) for v in bbox]
118
+
119
+ # Add margin for MuseTalk
120
+ margin = 30
121
+ h = y2 - y1
122
+ w = x2 - x1
123
+
124
+ # Expand vertically
125
+ y1_new = max(0, y1 - margin)
126
+ y2_new = min(frame.shape[0], y2 + margin)
127
+
128
+ # Ensure within bounds
129
+ y1_new = max(0, y1_new)
130
+ y2_new = min(frame.shape[0], y2_new)
131
+
132
+ coords_list.append((x1, y1_new, x2, y2_new))
133
+ frame_list.append(frame)
134
+ else:
135
+ coords_list.append((0, 0, 0, 0))
136
+ frame_list.append(frame)
137
+ except Exception as e:
138
+ print(f"Warning: Face detection failed for {img_path}: {e}")
139
+ coords_list.append((0, 0, 0, 0))
140
+ frame_list.append(frame)
141
+
142
+ print(f"Detected {len([c for c in coords_list if c != (0, 0, 0, 0)])} valid faces")
143
+
144
+ # 4. VAE encode using shared VAE
145
  print("Encoding frames to latents...")
146
  input_latent_list = []
147
+
148
+ for bbox, frame in zip(coords_list, frame_list):
149
+ if bbox == (0, 0, 0, 0):
150
  continue
151
+
152
  x1, y1, x2, y2 = bbox
153
+
154
+ # Crop face region
155
  crop_frame = frame[y1:y2, x1:x2]
156
+
157
+ # Resize to 256x256
158
  crop_frame = cv2.resize(
159
  crop_frame, (256, 256), interpolation=cv2.INTER_LANCZOS4
160
  )
161
+
162
+ # Convert to tensor
163
+ crop_tensor = (
164
+ torch.from_numpy(crop_frame).permute(2, 0, 1).unsqueeze(0).float() / 255.0
165
+ )
166
+
167
+ # Normalize
168
+ mean = torch.tensor([0.5, 0.5, 0.5]).view(1, 3, 1, 1)
169
+ std = torch.tensor([0.5, 0.5, 0.5]).view(1, 3, 1, 1)
170
+ crop_tensor = (crop_tensor - mean) / std
171
+
172
+ # Encode using shared VAE
173
+ with torch.no_grad():
174
+ latent_dist = vae.encode(crop_tensor)
175
+ latent = latent_dist.mode() * vae.config.scaling_factor
176
+
177
+ input_latent_list.append(latent)
178
+
179
  print(f"Encoded {len(input_latent_list)} frames")
180
 
181
  # 5. Cycle frames for smoothing
182
  frame_list_cycle = frame_list + frame_list[::-1]
183
+ coords_list_cycle = coords_list + coords_list[::-1]
184
  input_latent_list_cycle = input_latent_list + input_latent_list[::-1]
185
 
186
  # 6. Batch inference
187
  print("Starting inference...")
188
  batch_size = 8
 
 
 
 
 
 
 
189
 
190
  res_frame_list = []
191
+ for i in range(0, len(audio_chunks), batch_size):
192
+ audio_batch = torch.cat(audio_chunks[i : i + batch_size], dim=0)
193
+ latent_batch = torch.cat(input_latent_list_cycle[i : i + batch_size], dim=0)
 
 
194
 
195
+ # Get audio embeddings
196
+ audio_feature_batch = pe(audio_batch)
197
+
198
+ # UNet inference
199
+ pred_latents = unet(
200
  latent_batch, timesteps, encoder_hidden_states=audio_feature_batch
201
  ).sample
202
+
203
+ # Decode using shared VAE
204
+ with torch.no_grad():
205
+ recon = vae.decode(pred_latents / vae.config.scaling_factor).sample
206
+
207
+ res_frame_list.append(recon)
208
+
209
  print(f"Generated {len(res_frame_list)} frames")
210
 
211
+ # 7. Simple blending
212
  print("Blending...")
213
  output_frames_dir = os.path.join(output_dir, f"{input_basename}_output")
214
  os.makedirs(output_frames_dir, exist_ok=True)
215
 
216
  for i, res_frame in enumerate(tqdm(res_frame_list)):
217
+ bbox = coords_list_cycle[i % len(coords_list_cycle)]
218
+ ori_frame = frame_list_cycle[i % len(frame_list_cycle)]
219
+
220
+ if bbox == (0, 0, 0, 0):
221
+ continue
222
+
223
  x1, y1, x2, y2 = bbox
 
 
224
 
225
  try:
226
+ # Decode latent to image
227
+ res_image = (
228
+ res_frame[i % len(res_frame)].squeeze(0).permute(1, 2, 0).cpu().numpy()
229
+ )
230
+ res_image = (res_image * 255).clip(0, 255).astype(np.uint8)
231
+
232
+ # Resize to bbox size
233
+ res_image = cv2.resize(res_image, (x2 - x1, y2 - y1))
234
+
235
+ # Simple alpha blending
236
+ h, w = res_image.shape[:2]
237
+
238
+ # Create mask for blending
239
+ mask = np.zeros((h, w), dtype=np.float32)
240
+ mask[int(0.2 * h) : int(0.8 * h), int(0.2 * w) : int(0.8 * w)] = 1.0
241
+
242
+ # Extract original region
243
+ ori_region = ori_frame[y1:y2, x1:x2].copy()
244
+
245
+ # Resize original region to match
246
+ ori_region = cv2.resize(ori_region, (w, h))
247
+
248
+ # Blend
249
+ res_image = (
250
+ res_image.astype(np.float32) * mask
251
+ + ori_region.astype(np.float32) * (1 - mask)
252
+ ).astype(np.uint8)
253
+
254
+ # Place back in original frame
255
+ combined = ori_frame.copy()
256
+ combined[y1:y2, x1:x2] = res_image
257
+
258
+ cv2.imwrite(f"{output_frames_dir}/{i:08d}.png", combined)
259
  except Exception as e:
260
+ print(f"Warning: Could not blend frame {i}: {e}")
261
  continue
262
 
 
 
 
 
 
 
263
  # 8. Encode to video
264
  print("Encoding video...")
265
  cmd = f"ffmpeg -y -v warning -r {fps} -f image2 -i {output_frames_dir}/%08d.png -vcodec libx264 -vf format=yuv420p -crf 18 {video_out_path}"
 
279
  shutil.rmtree(output_frames_dir)
280
 
281
  print(f"\n{'=' * 60}")
282
+ print(f"MUSETALK V1.5 SUCCESS (simplified)")
283
  print(f"Output: {video_out_path}")
284
  print(f"{'=' * 60}\n")
285
 
musetalk_integration/models/vae.py CHANGED
@@ -7,48 +7,67 @@ import numpy as np
7
  from PIL import Image
8
  import os
9
 
10
- class VAE():
 
11
  """
12
  VAE (Variational Autoencoder) class for image processing.
13
  """
14
 
15
- def __init__(self, model_path="./models/sd-vae-ft-mse/", resized_img=256, use_float16=False):
 
 
 
 
 
16
  """
17
- Initialize the VAE instance.
18
 
19
- :param model_path: Path to the trained model.
20
  :param resized_img: The size to which images are resized.
21
  :param use_float16: Whether to use float16 precision.
22
  """
23
  self.model_path = model_path
24
- self.vae = AutoencoderKL.from_pretrained(self.model_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
27
- self.vae.to(self.device)
28
 
29
  if use_float16:
30
- self.vae = self.vae.half()
31
  self._use_float16 = True
32
  else:
33
  self._use_float16 = False
34
 
35
- self.scaling_factor = self.vae.config.scaling_factor
36
  self.transform = transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
37
  self._resized_img = resized_img
38
  self._mask_tensor = self.get_mask_tensor()
39
-
40
  def get_mask_tensor(self):
41
  """
42
  Creates a mask tensor for image processing.
43
  :return: A mask tensor.
44
  """
45
- mask_tensor = torch.zeros((self._resized_img,self._resized_img))
46
- mask_tensor[:self._resized_img//2,:] = 1
47
- mask_tensor[mask_tensor< 0.5] = 0
48
- mask_tensor[mask_tensor>= 0.5] = 1
49
  return mask_tensor
50
-
51
- def preprocess_img(self,img_name,half_mask=False):
52
  """
53
  Preprocess an image for the VAE.
54
 
@@ -58,30 +77,32 @@ class VAE():
58
  """
59
  window = []
60
  if isinstance(img_name, str):
61
- window_fnames = [img_name]
62
- for fname in window_fnames:
63
- img = cv2.imread(fname)
64
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
65
- img = cv2.resize(img, (self._resized_img, self._resized_img),
66
- interpolation=cv2.INTER_LANCZOS4)
67
- window.append(img)
68
  else:
69
- img = cv2.cvtColor(img_name, cv2.COLOR_BGR2RGB)
70
- window.append(img)
71
-
72
- x = np.asarray(window) / 255.
 
 
 
 
 
 
 
73
  x = np.transpose(x, (3, 0, 1, 2))
74
  x = torch.squeeze(torch.FloatTensor(x))
75
  if half_mask:
76
- x = x * (self._mask_tensor>0.5)
77
  x = self.transform(x)
78
-
79
- x = x.unsqueeze(0) # [1, 3, 256, 256] torch tensor
80
- x = x.to(self.vae.device)
81
 
82
  return x
83
 
84
- def encode_latents(self,image):
85
  """
86
  Encode an image into latent variables.
87
 
@@ -89,43 +110,50 @@ class VAE():
89
  :return: The encoded latent variables.
90
  """
91
  with torch.no_grad():
92
- init_latent_dist = self.vae.encode(image.to(self.vae.dtype)).latent_dist
93
- init_latents = self.scaling_factor * init_latent_dist.sample()
94
- return init_latents
95
-
 
 
96
  def decode_latents(self, latents):
97
  """
98
  Decode latent variables back into an image.
99
  :param latents: The latent variables to decode.
100
  :return: A NumPy array representing the decoded image.
101
  """
102
- latents = (1/ self.scaling_factor) * latents
103
- image = self.vae.decode(latents.to(self.vae.dtype)).sample
104
  image = (image / 2 + 0.5).clamp(0, 1)
105
  image = image.detach().cpu().permute(0, 2, 3, 1).float().numpy()
106
  image = (image * 255).round().astype("uint8")
107
- image = image[...,::-1] # RGB to BGR
108
  return image
109
-
110
- def get_latents_for_unet(self,img):
111
  """
112
  Prepare latent variables for a U-Net model.
113
  :param img: The image to process.
114
  :return: A concatenated tensor of latents for U-Net input.
115
  """
116
-
117
- ref_image = self.preprocess_img(img,half_mask=True) # [1, 3, 256, 256] RGB, torch tensor
118
- masked_latents = self.encode_latents(ref_image) # [1, 4, 32, 32], torch tensor
119
- ref_image = self.preprocess_img(img,half_mask=False) # [1, 3, 256, 256] RGB, torch tensor
120
- ref_latents = self.encode_latents(ref_image) # [1, 4, 32, 32], torch tensor
 
 
 
 
121
  latent_model_input = torch.cat([masked_latents, ref_latents], dim=1)
122
  return latent_model_input
123
 
 
124
  if __name__ == "__main__":
125
  vae_mode_path = "./models/sd-vae-ft-mse/"
126
- vae = VAE(model_path = vae_mode_path,use_float16=False)
127
  img_path = "./results/sun001_crop/00000.png"
128
-
129
  crop_imgs_path = "./results/sun001_crop/"
130
  latents_out_path = "./results/latents/"
131
  if not os.path.exists(latents_out_path):
@@ -139,10 +167,7 @@ if __name__ == "__main__":
139
  index = file.split(".")[0]
140
  img_path = crop_imgs_path + file
141
  latents = vae.get_latents_for_unet(img_path)
142
- print(img_path,"latents",latents.size())
143
- #torch.save(latents,os.path.join(latents_out_path,index+".pt"))
144
- #reload_tensor = torch.load('tensor.pt')
145
- #print(reload_tensor.size())
146
-
147
-
148
-
 
7
  from PIL import Image
8
  import os
9
 
10
+
11
+ class VAE:
12
  """
13
  VAE (Variational Autoencoder) class for image processing.
14
  """
15
 
16
+ def __init__(
17
+ self,
18
+ model_path="./checkpoints/sd-vae-ft-mse/",
19
+ resized_img=256,
20
+ use_float16=False,
21
+ ):
22
  """
23
+ Initialize a VAE instance.
24
 
25
+ :param model_path: Path to the trained model directory (local path or repo ID).
26
  :param resized_img: The size to which images are resized.
27
  :param use_float16: Whether to use float16 precision.
28
  """
29
  self.model_path = model_path
30
+
31
+ # Load from local path or HuggingFace hub
32
+ if os.path.isdir(model_path):
33
+ # Load from local directory
34
+ from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
35
+
36
+ vae_tuple = AutoencoderKL.from_pretrained(model_path)
37
+ self.vae = vae_tuple
38
+ else:
39
+ # Load from HuggingFace hub
40
+ from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
41
+
42
+ vae_tuple = AutoencoderKL.from_pretrained(model_path)
43
+ self.vae = vae_tuple
44
 
45
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
46
+ self.vae[0].to(self.device)
47
 
48
  if use_float16:
49
+ self.vae[0] = self.vae[0].half()
50
  self._use_float16 = True
51
  else:
52
  self._use_float16 = False
53
 
54
+ self.scaling_factor = self.vae[0].config.scaling_factor
55
  self.transform = transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
56
  self._resized_img = resized_img
57
  self._mask_tensor = self.get_mask_tensor()
58
+
59
  def get_mask_tensor(self):
60
  """
61
  Creates a mask tensor for image processing.
62
  :return: A mask tensor.
63
  """
64
+ mask_tensor = torch.zeros((self._resized_img, self._resized_img))
65
+ mask_tensor[: self._resized_img // 2, :] = 1
66
+ mask_tensor[mask_tensor < 0.5] = 0
67
+ mask_tensor[mask_tensor >= 0.5] = 1
68
  return mask_tensor
69
+
70
+ def preprocess_img(self, img_name, half_mask=False):
71
  """
72
  Preprocess an image for the VAE.
73
 
 
77
  """
78
  window = []
79
  if isinstance(img_name, str):
80
+ # Handle single image file path
81
+ window.append(img_name)
 
 
 
 
 
82
  else:
83
+ # Handle list of images (numpy array)
84
+ for img in img_name:
85
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
86
+ img_resized = cv2.resize(
87
+ img_rgb,
88
+ (self._resized_img, self._resized_img),
89
+ interpolation=cv2.INTER_LANCZOS4,
90
+ )
91
+ window.append(img_resized)
92
+
93
+ x = np.asarray(window) / 255.0
94
  x = np.transpose(x, (3, 0, 1, 2))
95
  x = torch.squeeze(torch.FloatTensor(x))
96
  if half_mask:
97
+ x = x * (self._mask_tensor > 0.5)
98
  x = self.transform(x)
99
+
100
+ x = x.unsqueeze(0)
101
+ x = x.to(self.vae[0].device)
102
 
103
  return x
104
 
105
+ def encode_latents(self, image):
106
  """
107
  Encode an image into latent variables.
108
 
 
110
  :return: The encoded latent variables.
111
  """
112
  with torch.no_grad():
113
+ init_latent_dist = (
114
+ self.vae[0].encode(image.to(self.vae[0].dtype)).latent_dist
115
+ )
116
+ init_latents = self.scaling_factor * init_latent_dist.sample()
117
+ return init_latents
118
+
119
  def decode_latents(self, latents):
120
  """
121
  Decode latent variables back into an image.
122
  :param latents: The latent variables to decode.
123
  :return: A NumPy array representing the decoded image.
124
  """
125
+ latents = (1 / self.scaling_factor) * latents
126
+ image = self.vae[0].decode(latents.to(self.vae[0].dtype)).sample
127
  image = (image / 2 + 0.5).clamp(0, 1)
128
  image = image.detach().cpu().permute(0, 2, 3, 1).float().numpy()
129
  image = (image * 255).round().astype("uint8")
130
+ image = image[..., ::-1]
131
  return image
132
+
133
+ def get_latents_for_unet(self, img):
134
  """
135
  Prepare latent variables for a U-Net model.
136
  :param img: The image to process.
137
  :return: A concatenated tensor of latents for U-Net input.
138
  """
139
+
140
+ ref_image = self.preprocess_img(
141
+ img, half_mask=True
142
+ ) # [1, 3, 256, 256] RGB, torch tensor
143
+ masked_latents = self.encode_latents(ref_image) # [1, 4, 32, 32], torch tensor
144
+ ref_image = self.preprocess_img(
145
+ img, half_mask=False
146
+ ) # [1, 3, 256, 256] RGB, torch tensor
147
+ ref_latents = self.encode_latents(ref_image) # [1, 4, 32, 32], torch tensor
148
  latent_model_input = torch.cat([masked_latents, ref_latents], dim=1)
149
  return latent_model_input
150
 
151
+
152
  if __name__ == "__main__":
153
  vae_mode_path = "./models/sd-vae-ft-mse/"
154
+ vae = VAE(model_path=vae_mode_path, use_float16=False)
155
  img_path = "./results/sun001_crop/00000.png"
156
+
157
  crop_imgs_path = "./results/sun001_crop/"
158
  latents_out_path = "./results/latents/"
159
  if not os.path.exists(latents_out_path):
 
167
  index = file.split(".")[0]
168
  img_path = crop_imgs_path + file
169
  latents = vae.get_latents_for_unet(img_path)
170
+ print(img_path, "latents", latents.size())
171
+ # torch.save(latents,os.path.join(latents_out_path,index+".pt"))
172
+ # reload_tensor = torch.load('tensor.pt')
173
+ # print(reload_tensor.size())
 
 
 
shared/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Shared components for LatentSync and MuseTalk"""
2
+
3
+ from .model_manager import ModelManager
4
+
5
+ __all__ = ["ModelManager"]
shared/face_detection/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Face detection (shared between LatentSync and MuseTalk)"""
shared/face_detection/detector.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+ INSIGHTFACE_DETECT_SIZE = 512
5
+
6
+
7
+ def cuda_to_int(cuda_str: str) -> int:
8
+ """
9
+ Convert the string with format "cuda:X" to integer X.
10
+ """
11
+ if cuda_str == "cuda":
12
+ return 0
13
+ device = torch.device(cuda_str)
14
+ if device.type != "cuda":
15
+ raise ValueError(f"Device type must be 'cuda', got: {device.type}")
16
+ return device.index
17
+
18
+
19
+ LMK_ADAPT_ORIGIN_ORDER = [
20
+ 1,
21
+ 10,
22
+ 12,
23
+ 14,
24
+ 16,
25
+ 3,
26
+ 5,
27
+ 7,
28
+ 0,
29
+ 23,
30
+ 21,
31
+ 19,
32
+ 32,
33
+ 30,
34
+ 28,
35
+ 26,
36
+ 17,
37
+ 43,
38
+ 48,
39
+ 49,
40
+ 51,
41
+ 50,
42
+ 102,
43
+ 103,
44
+ 104,
45
+ 105,
46
+ 101,
47
+ 73,
48
+ 74,
49
+ 86,
50
+ ]
51
+
52
+
53
+ class FaceDetector:
54
+ def __init__(self, device="cuda"):
55
+ from insightface.app import FaceAnalysis
56
+
57
+ self.app = FaceAnalysis(
58
+ allowed_modules=["detection", "landmark_2d_106"],
59
+ root="checkpoints/auxiliary",
60
+ providers=["CUDAExecutionProvider"],
61
+ )
62
+ self.app.prepare(
63
+ ctx_id=cuda_to_int(device),
64
+ det_size=(INSIGHTFACE_DETECT_SIZE, INSIGHTFACE_DETECT_SIZE),
65
+ )
66
+
67
+ def __call__(self, frame, threshold=0.5):
68
+ f_h, f_w, _ = frame.shape
69
+
70
+ faces = self.app.get(frame)
71
+
72
+ get_face_store = None
73
+ max_size = 0
74
+
75
+ if len(faces) == 0:
76
+ return None, None
77
+ else:
78
+ for face in faces:
79
+ bbox = face.bbox.astype(np.int_).tolist()
80
+ w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
81
+ if w < 50 or h < 80:
82
+ continue
83
+ if w / h > 1.5 or w / h < 0.2:
84
+ continue
85
+ if face.det_score < threshold:
86
+ continue
87
+ size_now = w * h
88
+
89
+ if size_now > max_size:
90
+ max_size = size_now
91
+ get_face_store = face
92
+
93
+ if get_face_store is None:
94
+ return None, None
95
+ else:
96
+ face = get_face_store
97
+ lmk = np.round(face.landmark_2d_106).astype(np.int_)
98
+
99
+ halk_face_coord = np.mean([lmk[74], lmk[73]], axis=0) # lmk[73]
100
+
101
+ sub_lmk = lmk[LMK_ADAPT_ORIGIN_ORDER]
102
+ halk_face_dist = np.max(sub_lmk[:, 1]) - halk_face_coord[1]
103
+ upper_bond = halk_face_coord[1] - halk_face_dist # *0.94
104
+
105
+ x1, y1, x2, y2 = (
106
+ np.min(sub_lmk[:, 0]),
107
+ int(upper_bond),
108
+ np.max(sub_lmk[:, 0]),
109
+ np.max(sub_lmk[:, 1]),
110
+ )
111
+
112
+ if y2 - y1 <= 0 or x2 - x1 <= 0 or x1 < 0:
113
+ x1, y1, x2, y2 = face.bbox.astype(np.int_).tolist()
114
+
115
+ y2 += int((x2 - x1) * 0.1)
116
+ x1 -= int((x2 - x1) * 0.05)
117
+ x2 += int((x2 - x1) * 0.05)
118
+
119
+ x1 = max(0, x1)
120
+ y1 = max(0, y1)
121
+ x2 = min(f_w, x2)
122
+ y2 = min(f_h, y2)
123
+
124
+ return (x1, y1, x2, y2), lmk
shared/model_manager.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ModelManager: Lazy loading and caching for ML models"""
2
+
3
+ import gc
4
+ import logging
5
+ import os
6
+ import torch
7
+ from diffusers.schedulers.scheduling_ddim import DDIMScheduler
8
+ from huggingface_hub import snapshot_download
9
+ from omegaconf import OmegaConf
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ModelManager:
15
+ _instance = None
16
+
17
+ _whisper_encoder = None
18
+ _vae = None
19
+ _latentsync_unet = None
20
+ _musetalk_unet = None
21
+ _scheduler = None
22
+ _latentsync_config = None
23
+ _musetalk_pe = None
24
+
25
+ def __new__(cls):
26
+ if cls._instance is None:
27
+ cls._instance = super().__new__(cls)
28
+ return cls._instance
29
+
30
+ @classmethod
31
+ def get_instance(cls):
32
+ if cls._instance is None:
33
+ cls._instance = cls()
34
+ return cls._instance
35
+
36
+ def load_whisper_encoder(
37
+ self, model_path: str, device: str = "cuda", num_frames: int = 12
38
+ ):
39
+ """Load Whisper audio encoder (lazy loaded)"""
40
+ if self._whisper_encoder is None:
41
+ from shared.whisper.audio2feature import Audio2Feature
42
+
43
+ logger.info(f"Loading Whisper encoder from {model_path}...")
44
+ self._whisper_encoder = Audio2Feature(
45
+ model_path=model_path, device=device, num_frames=num_frames
46
+ )
47
+ logger.info("Whisper encoder loaded")
48
+ return self._whisper_encoder
49
+
50
+ def load_vae(self, device: str = "cuda"):
51
+ """Load VAE (lazy loaded)"""
52
+ if self._vae is None:
53
+ from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
54
+
55
+ logger.info("Loading VAE...")
56
+ vae = AutoencoderKL.from_pretrained(
57
+ "stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16
58
+ )
59
+ vae.config.scaling_factor = 0.18215
60
+ vae.config.shift_factor = 0
61
+ self._vae = vae.to(device)
62
+ logger.info("VAE loaded")
63
+ return self._vae
64
+
65
+ def get_scheduler(self):
66
+ """Get DDIMScheduler (lazy loaded)"""
67
+ if self._scheduler is None:
68
+ logger.info("Loading DDIMScheduler...")
69
+ self._scheduler = DDIMScheduler.from_pretrained("configs")
70
+ logger.info("DDIMScheduler loaded")
71
+ return self._scheduler
72
+
73
+ def load_latentsync_unet(self, device: str = "cuda"):
74
+ """Load LatentSync UNet (lazy loaded)"""
75
+ if self._latentsync_unet is None:
76
+ from latentsync.models.unet import UNet3DConditionModel
77
+
78
+ logger.info("Downloading LatentSync-1.6 models...")
79
+ os.makedirs("checkpoints", exist_ok=True)
80
+ snapshot_download(
81
+ repo_id="ByteDance/LatentSync-1.6", local_dir="./checkpoints"
82
+ )
83
+
84
+ logger.info("Loading LatentSync UNet...")
85
+ config = self.get_latentsync_config()
86
+
87
+ inference_ckpt_path = "checkpoints/latentsync_unet.pt"
88
+ unet, _ = UNet3DConditionModel.from_pretrained(
89
+ OmegaConf.to_container(config.model),
90
+ inference_ckpt_path,
91
+ device="cpu",
92
+ )
93
+ unet = unet.to(dtype=torch.float16).to(device)
94
+
95
+ from diffusers.utils.import_utils import is_xformers_available
96
+
97
+ if is_xformers_available():
98
+ unet.enable_xformers_memory_efficient_attention()
99
+
100
+ self._latentsync_unet = unet
101
+ logger.info("LatentSync UNet loaded")
102
+ return self._latentsync_unet
103
+
104
+ def get_latentsync_config(self):
105
+ """Get LatentSync config"""
106
+ if self._latentsync_config is None:
107
+ logger.info("Loading LatentSync config...")
108
+ unet_config_path = "configs/unet/stage2_512.yaml"
109
+ config = OmegaConf.load(unet_config_path)
110
+ self._latentsync_config = config
111
+ return self._latentsync_config
112
+
113
+ def load_musetalk_unet(self, device: str = "cuda"):
114
+ """Load MuseTalk V1.5 UNet (lazy loaded)"""
115
+ if self._musetalk_unet is None:
116
+ from latentsync.models.unet import UNet3DConditionModel, PositionalEncoding
117
+
118
+ logger.info("Loading MuseTalk V1.5 UNet...")
119
+ unet_config_path = "checkpoints/musetalkV15/musetalk.json"
120
+ unet_model_path = "checkpoints/musetalkV15/unet.pth"
121
+
122
+ config = OmegaConf.load(unet_config_path)
123
+
124
+ unet, _ = UNet3DConditionModel.from_pretrained(
125
+ OmegaConf.to_container(config.model), unet_model_path, device=device
126
+ )
127
+ unet = unet.to(dtype=torch.float16)
128
+
129
+ pe = PositionalEncoding(d_model=384)
130
+
131
+ self._musetalk_unet = unet
132
+ self._musetalk_pe = pe
133
+ logger.info("MuseTalk UNet loaded")
134
+ return self._musetalk_unet, self._musetalk_pe
135
+
136
+ def get_whisper_model_path(self, cross_attention_dim: int):
137
+ """Get Whisper model path based on cross_attention_dim"""
138
+ if cross_attention_dim == 768:
139
+ return "checkpoints/whisper/small.pt"
140
+ elif cross_attention_dim == 384:
141
+ return "checkpoints/whisper/tiny.pt"
142
+ else:
143
+ raise NotImplementedError("cross_attention_dim must be 768 or 384")
144
+
145
+ def clear_cache(self):
146
+ """Clear GPU cache and unload all models"""
147
+ logger.info("Clearing model cache...")
148
+
149
+ self._whisper_encoder = None
150
+ self._vae = None
151
+ self._latentsync_unet = None
152
+ self._musetalk_unet = None
153
+ self._scheduler = None
154
+ self._latentsync_config = None
155
+ self._musetalk_pe = None
156
+
157
+ torch.cuda.empty_cache()
158
+ gc.collect()
159
+ logger.info("Model cache cleared")
shared/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Utilities (shared between LatentSync and MuseTalk)"""
shared/vae/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """VAE loader (shared between LatentSync and MuseTalk)"""
shared/vae/loader.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VAE loader (placeholder - actual loading handled by ModelManager)"""
2
+
3
+ import torch
4
+ from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
5
+
6
+
7
+ def load_vae(device: str = "cuda"):
8
+ """Load VAE from HuggingFace
9
+
10
+ Args:
11
+ device: Device to load VAE on
12
+
13
+ Returns:
14
+ VAE model
15
+ """
16
+ vae = AutoencoderKL.from_pretrained(
17
+ "stabilityai/sd-vae-ft-mse", torch_dtype=torch.float16
18
+ )
19
+ vae.config.scaling_factor = 0.18215
20
+ vae.config.shift_factor = 0
21
+ return vae.to(device)
shared/whisper/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Whisper audio encoder (shared between LatentSync and MuseTalk)"""
shared/whisper/audio2feature.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Adapted from https://github.com/TMElyralab/MuseTalk/blob/main/musetalk/whisper/audio2feature.py
2
+
3
+ import numpy as np
4
+ import torch
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ class Audio2Feature:
10
+ def __init__(
11
+ self,
12
+ model_path="checkpoints/whisper/tiny.pt",
13
+ device=None,
14
+ audio_embeds_cache_dir=None,
15
+ num_frames=16,
16
+ audio_feat_length=[2, 2],
17
+ ):
18
+ from latentsync.whisper.whisper import load_model
19
+
20
+ self.model = load_model(model_path, device)
21
+ self.audio_embeds_cache_dir = audio_embeds_cache_dir
22
+ if audio_embeds_cache_dir is not None and audio_embeds_cache_dir != "":
23
+ Path(audio_embeds_cache_dir).mkdir(parents=True, exist_ok=True)
24
+ self.num_frames = num_frames
25
+ self.embedding_dim = self.model.dims.n_audio_state
26
+ self.audio_feat_length = audio_feat_length
27
+
28
+ def get_sliced_feature(self, feature_array, vid_idx, fps=25):
29
+ """
30
+ Get sliced features based on a given index
31
+ :param feature_array:
32
+ :param start_idx: the start index of the feature
33
+ :param audio_feat_length:
34
+ :return:
35
+ """
36
+ length = len(feature_array)
37
+ selected_feature = []
38
+ selected_idx = []
39
+
40
+ center_idx = int(vid_idx * 50 / fps)
41
+ left_idx = center_idx - self.audio_feat_length[0] * 2
42
+ right_idx = center_idx + (self.audio_feat_length[1] + 1) * 2
43
+
44
+ for idx in range(left_idx, right_idx):
45
+ idx = max(0, idx)
46
+ idx = min(length - 1, idx)
47
+ x = feature_array[idx]
48
+ selected_feature.append(x)
49
+ selected_idx.append(idx)
50
+
51
+ selected_feature = torch.cat(selected_feature, dim=0)
52
+ selected_feature = selected_feature.reshape(-1, self.embedding_dim) # 50*384
53
+ return selected_feature, selected_idx
54
+
55
+ def get_sliced_feature_sparse(self, feature_array, vid_idx, fps=25):
56
+ """
57
+ Get sliced features based on a given index
58
+ :param feature_array:
59
+ :param start_idx: the start index of the feature
60
+ :param audio_feat_length:
61
+ :return:
62
+ """
63
+ length = len(feature_array)
64
+ selected_feature = []
65
+ selected_idx = []
66
+
67
+ for dt in range(-self.audio_feat_length[0], self.audio_feat_length[1] + 1):
68
+ left_idx = int((vid_idx + dt) * 50 / fps)
69
+ if left_idx < 1 or left_idx > length - 1:
70
+ left_idx = max(0, left_idx)
71
+ left_idx = min(length - 1, left_idx)
72
+
73
+ x = feature_array[left_idx]
74
+ x = x[np.newaxis, :, :]
75
+ x = np.repeat(x, 2, axis=0)
76
+ selected_feature.append(x)
77
+ selected_idx.append(left_idx)
78
+ selected_idx.append(left_idx)
79
+ else:
80
+ x = feature_array[left_idx - 1 : left_idx + 1]
81
+ selected_feature.append(x)
82
+ selected_idx.append(left_idx - 1)
83
+ selected_idx.append(left_idx)
84
+ selected_feature = np.concatenate(selected_feature, axis=0)
85
+ selected_feature = selected_feature.reshape(-1, self.embedding_dim) # 50*384
86
+ selected_feature = torch.from_numpy(selected_feature)
87
+ return selected_feature, selected_idx
88
+
89
+ def feature2chunks(self, feature_array, fps):
90
+ whisper_chunks = []
91
+ whisper_idx_multiplier = 50.0 / fps
92
+ i = 0
93
+ print(f"video in {fps} FPS, audio idx in 50FPS")
94
+
95
+ while True:
96
+ start_idx = int(i * whisper_idx_multiplier)
97
+ selected_feature, selected_idx = self.get_sliced_feature(
98
+ feature_array=feature_array, vid_idx=i, fps=fps
99
+ )
100
+ # print(f"i:{i},selected_idx {selected_idx}")
101
+ whisper_chunks.append(selected_feature)
102
+ i += 1
103
+ if start_idx > len(feature_array):
104
+ break
105
+
106
+ return whisper_chunks
107
+
108
+ def _audio2feat(self, audio_path: str):
109
+ # get the sample rate of the audio
110
+ result = self.model.transcribe(audio_path)
111
+ embed_list = []
112
+ for emb in result["segments"]:
113
+ encoder_embeddings = emb["encoder_embeddings"]
114
+ encoder_embeddings = encoder_embeddings.transpose(0, 2, 1, 3)
115
+ encoder_embeddings = encoder_embeddings.squeeze(0)
116
+ start_idx = int(emb["start"])
117
+ end_idx = int(emb["end"])
118
+ emb_end_idx = int((end_idx - start_idx) / 2)
119
+ embed_list.append(encoder_embeddings[:emb_end_idx])
120
+ concatenated_array = torch.from_numpy(np.concatenate(embed_list, axis=0))
121
+ return concatenated_array
122
+
123
+ def audio2feat(self, audio_path):
124
+ if self.audio_embeds_cache_dir == "" or self.audio_embeds_cache_dir is None:
125
+ return self._audio2feat(audio_path)
126
+
127
+ audio_embeds_cache_path = os.path.join(
128
+ self.audio_embeds_cache_dir,
129
+ os.path.basename(audio_path).replace(".mp4", "_embeds.pt"),
130
+ )
131
+
132
+ if os.path.isfile(audio_embeds_cache_path):
133
+ try:
134
+ audio_feat = torch.load(audio_embeds_cache_path, weights_only=True)
135
+ except Exception as e:
136
+ print(f"{type(e).__name__} - {e} - {audio_embeds_cache_path}")
137
+ os.remove(audio_embeds_cache_path)
138
+ audio_feat = self._audio2feat(audio_path)
139
+ torch.save(audio_feat, audio_embeds_cache_path)
140
+ else:
141
+ audio_feat = self._audio2feat(audio_path)
142
+ torch.save(audio_feat, audio_embeds_cache_path)
143
+
144
+ return audio_feat
145
+
146
+ def crop_overlap_audio_window(self, audio_feat, start_index):
147
+ selected_feature_list = []
148
+ for i in range(start_index, start_index + self.num_frames):
149
+ selected_feature, selected_idx = self.get_sliced_feature(
150
+ feature_array=audio_feat, vid_idx=i, fps=25
151
+ )
152
+ selected_feature_list.append(selected_feature)
153
+ mel_overlap = torch.stack(selected_feature_list)
154
+ return mel_overlap
155
+
156
+
157
+ if __name__ == "__main__":
158
+ audio_encoder = Audio2Feature(model_path="checkpoints/whisper/tiny.pt")
159
+ audio_path = "assets/demo1_audio.wav"
160
+ array = audio_encoder.audio2feat(audio_path)
161
+ print(array.shape)
162
+ fps = 25
163
+ whisper_idx_multiplier = 50.0 / fps
164
+
165
+ i = 0
166
+ print(f"video in {fps} FPS, audio idx in 50FPS")
167
+ while True:
168
+ start_idx = int(i * whisper_idx_multiplier)
169
+ selected_feature, selected_idx = audio_encoder.get_sliced_feature(
170
+ feature_array=array, vid_idx=i, fps=fps
171
+ )
172
+ print(
173
+ f"video idx {i},\t audio idx {selected_idx},\t shape {selected_feature.shape}"
174
+ )
175
+ i += 1
176
+ if start_idx > len(array):
177
+ break