Spaces:
Runtime error
Runtime error
update
Browse files- app.py +1 -7
- lipsync.py +20 -47
- lipsync_processing.py +3 -3
- musetalk.py +183 -116
- musetalk_integration/models/vae.py +79 -54
- shared/__init__.py +5 -0
- shared/face_detection/__init__.py +1 -0
- shared/face_detection/detector.py +124 -0
- shared/model_manager.py +159 -0
- shared/utils/__init__.py +1 -0
- shared/vae/__init__.py +1 -0
- shared/vae/loader.py +21 -0
- shared/whisper/__init__.py +1 -0
- shared/whisper/audio2feature.py +177 -0
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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
print(f"Extracted {len(input_img_list)} frames at {fps} fps")
|
| 101 |
|
| 102 |
-
# 2. Extract audio features
|
| 103 |
print("Extracting audio features...")
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
#
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
print("Encoding frames to latents...")
|
| 126 |
input_latent_list = []
|
| 127 |
-
|
| 128 |
-
|
|
|
|
| 129 |
continue
|
|
|
|
| 130 |
x1, y1, x2, y2 = bbox
|
| 131 |
-
|
| 132 |
-
|
| 133 |
crop_frame = frame[y1:y2, x1:x2]
|
|
|
|
|
|
|
| 134 |
crop_frame = cv2.resize(
|
| 135 |
crop_frame, (256, 256), interpolation=cv2.INTER_LANCZOS4
|
| 136 |
)
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
audio_feature_batch = pe(whisper_batch)
|
| 162 |
-
latent_batch = latent_batch.to(dtype=torch.float16)
|
| 163 |
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
latent_batch, timesteps, encoder_hidden_states=audio_feature_batch
|
| 166 |
).sample
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
print(f"Generated {len(res_frame_list)} frames")
|
| 171 |
|
| 172 |
-
# 7.
|
| 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 =
|
| 179 |
-
ori_frame =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
x1, y1, x2, y2 = bbox
|
| 181 |
-
y2 = y2 + 10
|
| 182 |
-
y2 = min(y2, ori_frame.shape[0])
|
| 183 |
|
| 184 |
try:
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
except Exception as e:
|
| 187 |
-
print(f"Warning: Could not
|
| 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 |
-
|
|
|
|
| 11 |
"""
|
| 12 |
VAE (Variational Autoencoder) class for image processing.
|
| 13 |
"""
|
| 14 |
|
| 15 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
"""
|
| 17 |
-
Initialize
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 62 |
-
|
| 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 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 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 =
|
| 93 |
-
|
| 94 |
-
|
| 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/
|
| 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]
|
| 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(
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|