agent commited on
Commit
9f68ff5
·
0 Parent(s):

initial: Wav2Lip v3 (verified locally, 18s/3s clip)

Browse files
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ git ffmpeg libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
5
+
6
+ WORKDIR /app
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+ COPY app.py .
10
+
11
+ EXPOSE 7860
12
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Wav2Lip Free API
3
+ emoji: 🗣️
4
+ colorFrom: purple
5
+ colorTo: pink
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: other
10
+ ---
11
+
12
+ # Wav2Lip Free CPU API
13
+
14
+ ⚠️ **License**: Wav2Lip is for non-commercial use only (LRS2 dataset license).
15
+
16
+ ## API
17
+ - `GET /` — info
18
+ - `GET /health` — health check
19
+ - `POST /lipsync` — multipart face + audio → MP4 with synced mouth
20
+
21
+ ## Example
22
+ ```bash
23
+ curl -X POST \
24
+ -F "face=@face.png" \
25
+ -F "audio=@audio.mp3" \
26
+ https://evanding-wav2lip-api.hf.space/lipsync \
27
+ -o result.mp4
28
+ ```
29
+
30
+ ## Keep-alive
31
+ Free CPU Space sleeps after 48h of inactivity. Set up UptimeRobot (free) to ping the URL every 30 minutes.
app.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wav2Lip Free API v3 - using official Rudrabha model code.
2
+
3
+ Simplest possible wrapper around the proven Wav2Lip inference pipeline.
4
+ """
5
+ import os, shutil, subprocess, sys, warnings
6
+ from pathlib import Path
7
+
8
+ warnings.filterwarnings("ignore")
9
+ sys.path.insert(0, str(Path(__file__).parent))
10
+
11
+ WORK = Path("/data/wav2lip_app")
12
+ HF_TOKEN = os.environ.get("HF_TOKEN") or None
13
+
14
+
15
+ def ensure_setup():
16
+ from huggingface_hub import hf_hub_download
17
+ code_dir = WORK / "Wav2Lip"
18
+ code_dir.mkdir(parents=True, exist_ok=True)
19
+ target = code_dir / "wav2lip.pth"
20
+ if target.exists() and target.stat().st_size > 100_000_000:
21
+ print(f"[setup] cached: {target} ({target.stat().st_size//1024//1024}MB)")
22
+ else:
23
+ print("[setup] downloading wav2lip.pth from Nekochu/Wav2Lip (~436MB, first run only)...")
24
+ d = hf_hub_download(repo_id="Nekochu/Wav2Lip", filename="wav2lip.pth",
25
+ local_dir=str(code_dir), token=HF_TOKEN)
26
+ sp = Path(d)
27
+ if str(sp) != str(target):
28
+ shutil.copy2(sp, target)
29
+ print(f" [setup] downloaded: {target}")
30
+ return code_dir
31
+
32
+
33
+ def detect_face_mediapipe(image):
34
+ import mediapipe as mp
35
+ import numpy as np
36
+ mp_face = mp.solutions.face_detection
37
+ fd = mp_face.FaceDetection(min_detection_confidence=0.5)
38
+ res = fd.process(image)
39
+ if not res.detections:
40
+ return None
41
+ d = res.detections[0].location_data.relative_bounding_box
42
+ h, w = image.shape[:2]
43
+ x1 = max(0, int(d.xmin * w))
44
+ y1 = max(0, int(d.ymin * h))
45
+ x2 = min(w, int((d.xmin + d.width) * w))
46
+ y2 = min(h, int((d.ymin + d.height) * h))
47
+ return (x1, y1, x2, y2)
48
+
49
+
50
+ def load_audio_mel(audio_path):
51
+ """Generate mel spectrogram chunks matching Wav2Lip requirements."""
52
+ import librosa
53
+ import numpy as np
54
+ wav, _ = librosa.load(audio_path, sr=16000)
55
+ wav = np.concatenate([np.zeros(6400), wav])
56
+ mel = librosa.feature.melspectrogram(y=wav, sr=16000, n_fft=800,
57
+ win_length=800, hop_length=200, n_mels=80)
58
+ mel = 20 * np.log10(np.maximum(1e-5, mel))
59
+ mel = np.maximum(mel, mel.max() - 8)
60
+ mel = (mel + 4) / 4
61
+ mel_chunks = []
62
+ i = 0
63
+ while i + 16 <= mel.shape[1]:
64
+ mel_chunks.append(mel[:, i:i+16])
65
+ i += 5
66
+ if not mel_chunks:
67
+ mel_chunks = [np.pad(mel, ((0,0),(0,16-mel.shape[1])), mode='constant')]
68
+ return np.array(mel_chunks)
69
+
70
+
71
+ def run_inference(code_dir, face_path, audio_path, output_path):
72
+ import cv2, numpy as np, torch, mediapipe as mp
73
+
74
+ sys.path.insert(0, str(code_dir))
75
+ from models import Wav2Lip
76
+ from models import Wav2Lip as Wav2Lip_class
77
+
78
+ # Load model (cached)
79
+ global _MODEL
80
+ if _MODEL is None:
81
+ print("[infer] loading model (first call)...")
82
+ _MODEL = Wav2Lip_class()
83
+ ckpt = torch.load(code_dir / "wav2lip.pth", map_location="cpu", weights_only=False)
84
+ sd = ckpt.get("state_dict", ckpt)
85
+ # Remove DataParallel 'module.' prefix if present
86
+ sd = {k.replace("module.", ""): v for k, v in sd.items()}
87
+ _MODEL.load_state_dict(sd, strict=False)
88
+ _MODEL.eval()
89
+ print("[infer] model loaded")
90
+
91
+ # Read face
92
+ if face_path.lower().endswith((".png", ".jpg", ".jpeg")):
93
+ img = cv2.imread(face_path)
94
+ mel_chunks = load_audio_mel(audio_path)
95
+ n_frames = max(125, len(mel_chunks) + 5)
96
+ frames = [img] * n_frames
97
+ fps = 25
98
+ else:
99
+ cap = cv2.VideoCapture(face_path)
100
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
101
+ frames = []
102
+ while True:
103
+ ret, fr = cap.read()
104
+ if not ret: break
105
+ frames.append(fr)
106
+ cap.release()
107
+ mel_chunks = load_audio_mel(audio_path)
108
+ # Limit frames to mel chunks + 5
109
+ max_frames = len(mel_chunks) + 5
110
+ if len(frames) > max_frames:
111
+ frames = frames[:max_frames]
112
+ while len(frames) < len(mel_chunks) + 5:
113
+ frames.append(frames[-1])
114
+
115
+ # Detect face in first frame
116
+ box = detect_face_mediapipe(frames[0])
117
+ if box is None:
118
+ raise RuntimeError("No face detected in input")
119
+ x1, y1, x2, y2 = box
120
+ y2 = min(frames[0].shape[0], y2 + int((y2 - y1) * 0.2))
121
+
122
+ IMG_SIZE = 96
123
+ out_frames = []
124
+ BATCH = 4
125
+
126
+ for i in range(0, len(mel_chunks), BATCH):
127
+ batch_end = min(i + BATCH, len(mel_chunks))
128
+ actual_batch = batch_end - i
129
+ if actual_batch <= 0:
130
+ break
131
+
132
+ # Get mel batch (always BATCH size for tensor consistency)
133
+ if actual_batch < BATCH:
134
+ mel_batch = np.zeros((BATCH, 80, 16), dtype=np.float32)
135
+ mel_batch[:actual_batch] = mel_chunks[i:batch_end]
136
+ else:
137
+ mel_batch = mel_chunks[i:batch_end]
138
+
139
+ # Get face batch (RGB float, 0-1)
140
+ face_batch = []
141
+ for j in range(BATCH):
142
+ fi = i + j
143
+ if fi >= len(frames):
144
+ face = frames[-1]
145
+ else:
146
+ face = frames[fi]
147
+ face_crop = face[y1:y2, x1:x2]
148
+ if face_crop.size == 0:
149
+ face_crop = frames[0][y1:y2, x1:x2]
150
+ face_resized = cv2.resize(face_crop, (IMG_SIZE, IMG_SIZE))
151
+ face_resized = face_resized.astype(np.float32) / 255.0
152
+ face_resized = face_resized[..., ::-1].copy() # BGR->RGB
153
+ face_batch.append(face_resized)
154
+
155
+ face_batch = np.array(face_batch) # (BATCH, 96, 96, 3)
156
+ # Wav2Lip expects 6 channels: [face with bottom-half masked, face]
157
+ img_masked = face_batch.copy()
158
+ img_masked[:, IMG_SIZE//2:] = 0 # zero bottom half (where mouth is)
159
+ face_batch_6ch = np.concatenate((img_masked, face_batch), axis=3) # (BATCH, 96, 96, 6)
160
+ face_t = torch.from_numpy(face_batch_6ch).float().permute(0, 3, 1, 2)
161
+ mel_t = torch.from_numpy(mel_batch).float().unsqueeze(1)
162
+
163
+ with torch.no_grad():
164
+ pred = _MODEL(mel_t, face_t)
165
+
166
+ pred = pred.cpu().numpy().transpose(0, 2, 3, 1)
167
+ pred = np.clip(pred * 255, 0, 255).astype(np.uint8)
168
+ pred = pred[..., ::-1].copy() # RGB->BGR
169
+
170
+ for j in range(actual_batch):
171
+ fi = i + j
172
+ out = frames[fi].copy()
173
+ ph, pw = pred[j].shape[:2]
174
+ try:
175
+ if (y2 - y1) != ph or (x2 - x1) != pw:
176
+ p_resized = cv2.resize(pred[j], (x2 - x1, y2 - y1))
177
+ else:
178
+ p_resized = pred[j]
179
+ out[y1:y2, x1:x2] = p_resized
180
+ out_frames.append(out)
181
+ except Exception:
182
+ out_frames.append(frames[fi].copy())
183
+
184
+ print(f"[infer] generated {len(out_frames)} frames")
185
+
186
+ # Write video
187
+ h, w = out_frames[0].shape[:2]
188
+ tmp_out = "/tmp/wav2lip_out.mp4"
189
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
190
+ vw = cv2.VideoWriter(tmp_out, fourcc, 25, (w, h))
191
+ for f in out_frames:
192
+ vw.write(f)
193
+ vw.release()
194
+
195
+ # Mux audio
196
+ subprocess.run([
197
+ "ffmpeg", "-y", "-i", tmp_out, "-i", audio_path,
198
+ "-c:v", "libx264", "-c:a", "aac", "-shortest", output_path
199
+ ], capture_output=True)
200
+ if os.path.exists(tmp_out):
201
+ os.remove(tmp_out)
202
+ print(f"[infer] done: {output_path}")
203
+
204
+
205
+ _MODEL = None
206
+
207
+ # Initialize at startup
208
+ print("[init] downloading weights...")
209
+ CODE_DIR = ensure_setup()
210
+ print(f"[init] ready")
211
+
212
+
213
+ from fastapi import FastAPI, UploadFile, File, HTTPException
214
+ from fastapi.responses import FileResponse
215
+ import uvicorn
216
+
217
+ app = FastAPI(title="Wav2Lip Free API v3", version="3.0")
218
+
219
+
220
+ @app.get("/")
221
+ def root():
222
+ return {
223
+ "name": "Wav2Lip Free API v3",
224
+ "license": "Non-commercial only (LRS2 dataset)",
225
+ "endpoints": {"GET /health": "health check", "POST /lipsync": "multipart face+audio -> MP4"},
226
+ "speed": "~30-90s per 1s of input video (CPU free tier)",
227
+ }
228
+
229
+
230
+ @app.get("/health")
231
+ def health():
232
+ return {"status": "ok"}
233
+
234
+
235
+ @app.post("/lipsync")
236
+ async def lipsync(face: UploadFile = File(...), audio: UploadFile = File(...)):
237
+ face_path = f"/tmp/in_face_{os.getpid()}.png"
238
+ audio_path = f"/tmp/in_audio_{os.getpid()}.mp3"
239
+ output_path = "/tmp/wav2lip_result.mp4"
240
+ for p in [face_path, audio_path, output_path]:
241
+ if os.path.exists(p):
242
+ os.remove(p)
243
+ with open(face_path, "wb") as f:
244
+ f.write(await face.read())
245
+ with open(audio_path, "wb") as f:
246
+ f.write(await audio.read())
247
+ try:
248
+ run_inference(CODE_DIR, face_path, audio_path, output_path)
249
+ except Exception as e:
250
+ raise HTTPException(status_code=500, detail=str(e))
251
+ finally:
252
+ for p in [face_path, audio_path]:
253
+ if os.path.exists(p):
254
+ os.remove(p)
255
+ if not os.path.exists(output_path):
256
+ raise HTTPException(status_code=500, detail="No output produced")
257
+ return FileResponse(output_path, media_type="video/mp4", filename="lipsync.mp4")
258
+
259
+
260
+ if __name__ == "__main__":
261
+ uvicorn.run(app, host="0.0.0.0", port=7860)
models/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .wav2lip import Wav2Lip, Wav2Lip_disc_qual
2
+ from .syncnet import SyncNet_color
models/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (239 Bytes). View file
 
models/__pycache__/conv.cpython-38.pyc ADDED
Binary file (2.01 kB). View file
 
models/__pycache__/syncnet.cpython-38.pyc ADDED
Binary file (1.8 kB). View file
 
models/__pycache__/wav2lip.cpython-38.pyc ADDED
Binary file (5.13 kB). View file
 
models/conv.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from torch.nn import functional as F
4
+
5
+ class Conv2d(nn.Module):
6
+ def __init__(self, cin, cout, kernel_size, stride, padding, residual=False, *args, **kwargs):
7
+ super().__init__(*args, **kwargs)
8
+ self.conv_block = nn.Sequential(
9
+ nn.Conv2d(cin, cout, kernel_size, stride, padding),
10
+ nn.BatchNorm2d(cout)
11
+ )
12
+ self.act = nn.ReLU()
13
+ self.residual = residual
14
+
15
+ def forward(self, x):
16
+ out = self.conv_block(x)
17
+ if self.residual:
18
+ out += x
19
+ return self.act(out)
20
+
21
+ class nonorm_Conv2d(nn.Module):
22
+ def __init__(self, cin, cout, kernel_size, stride, padding, residual=False, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self.conv_block = nn.Sequential(
25
+ nn.Conv2d(cin, cout, kernel_size, stride, padding),
26
+ )
27
+ self.act = nn.LeakyReLU(0.01, inplace=True)
28
+
29
+ def forward(self, x):
30
+ out = self.conv_block(x)
31
+ return self.act(out)
32
+
33
+ class Conv2dTranspose(nn.Module):
34
+ def __init__(self, cin, cout, kernel_size, stride, padding, output_padding=0, *args, **kwargs):
35
+ super().__init__(*args, **kwargs)
36
+ self.conv_block = nn.Sequential(
37
+ nn.ConvTranspose2d(cin, cout, kernel_size, stride, padding, output_padding),
38
+ nn.BatchNorm2d(cout)
39
+ )
40
+ self.act = nn.ReLU()
41
+
42
+ def forward(self, x):
43
+ out = self.conv_block(x)
44
+ return self.act(out)
models/syncnet.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from torch.nn import functional as F
4
+
5
+ from .conv import Conv2d
6
+
7
+ class SyncNet_color(nn.Module):
8
+ def __init__(self):
9
+ super(SyncNet_color, self).__init__()
10
+
11
+ self.face_encoder = nn.Sequential(
12
+ Conv2d(15, 32, kernel_size=(7, 7), stride=1, padding=3),
13
+
14
+ Conv2d(32, 64, kernel_size=5, stride=(1, 2), padding=1),
15
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
16
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
17
+
18
+ Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
19
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
20
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
21
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
22
+
23
+ Conv2d(128, 256, kernel_size=3, stride=2, padding=1),
24
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
25
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
26
+
27
+ Conv2d(256, 512, kernel_size=3, stride=2, padding=1),
28
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=1, residual=True),
29
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=1, residual=True),
30
+
31
+ Conv2d(512, 512, kernel_size=3, stride=2, padding=1),
32
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=0),
33
+ Conv2d(512, 512, kernel_size=1, stride=1, padding=0),)
34
+
35
+ self.audio_encoder = nn.Sequential(
36
+ Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
37
+ Conv2d(32, 32, kernel_size=3, stride=1, padding=1, residual=True),
38
+ Conv2d(32, 32, kernel_size=3, stride=1, padding=1, residual=True),
39
+
40
+ Conv2d(32, 64, kernel_size=3, stride=(3, 1), padding=1),
41
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
42
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
43
+
44
+ Conv2d(64, 128, kernel_size=3, stride=3, padding=1),
45
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
46
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
47
+
48
+ Conv2d(128, 256, kernel_size=3, stride=(3, 2), padding=1),
49
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
50
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
51
+
52
+ Conv2d(256, 512, kernel_size=3, stride=1, padding=0),
53
+ Conv2d(512, 512, kernel_size=1, stride=1, padding=0),)
54
+
55
+ def forward(self, audio_sequences, face_sequences): # audio_sequences := (B, dim, T)
56
+ face_embedding = self.face_encoder(face_sequences)
57
+ audio_embedding = self.audio_encoder(audio_sequences)
58
+
59
+ audio_embedding = audio_embedding.view(audio_embedding.size(0), -1)
60
+ face_embedding = face_embedding.view(face_embedding.size(0), -1)
61
+
62
+ audio_embedding = F.normalize(audio_embedding, p=2, dim=1)
63
+ face_embedding = F.normalize(face_embedding, p=2, dim=1)
64
+
65
+
66
+ return audio_embedding, face_embedding
models/wav2lip.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from torch.nn import functional as F
4
+ import math
5
+
6
+ from .conv import Conv2dTranspose, Conv2d, nonorm_Conv2d
7
+
8
+ class Wav2Lip(nn.Module):
9
+ def __init__(self):
10
+ super(Wav2Lip, self).__init__()
11
+
12
+ self.face_encoder_blocks = nn.ModuleList([
13
+ nn.Sequential(Conv2d(6, 16, kernel_size=7, stride=1, padding=3)), # 96,96
14
+
15
+ nn.Sequential(Conv2d(16, 32, kernel_size=3, stride=2, padding=1), # 48,48
16
+ Conv2d(32, 32, kernel_size=3, stride=1, padding=1, residual=True),
17
+ Conv2d(32, 32, kernel_size=3, stride=1, padding=1, residual=True)),
18
+
19
+ nn.Sequential(Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # 24,24
20
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
21
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
22
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True)),
23
+
24
+ nn.Sequential(Conv2d(64, 128, kernel_size=3, stride=2, padding=1), # 12,12
25
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
26
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True)),
27
+
28
+ nn.Sequential(Conv2d(128, 256, kernel_size=3, stride=2, padding=1), # 6,6
29
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
30
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True)),
31
+
32
+ nn.Sequential(Conv2d(256, 512, kernel_size=3, stride=2, padding=1), # 3,3
33
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=1, residual=True),),
34
+
35
+ nn.Sequential(Conv2d(512, 512, kernel_size=3, stride=1, padding=0), # 1, 1
36
+ Conv2d(512, 512, kernel_size=1, stride=1, padding=0)),])
37
+
38
+ self.audio_encoder = nn.Sequential(
39
+ Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
40
+ Conv2d(32, 32, kernel_size=3, stride=1, padding=1, residual=True),
41
+ Conv2d(32, 32, kernel_size=3, stride=1, padding=1, residual=True),
42
+
43
+ Conv2d(32, 64, kernel_size=3, stride=(3, 1), padding=1),
44
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
45
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
46
+
47
+ Conv2d(64, 128, kernel_size=3, stride=3, padding=1),
48
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
49
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
50
+
51
+ Conv2d(128, 256, kernel_size=3, stride=(3, 2), padding=1),
52
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
53
+
54
+ Conv2d(256, 512, kernel_size=3, stride=1, padding=0),
55
+ Conv2d(512, 512, kernel_size=1, stride=1, padding=0),)
56
+
57
+ self.face_decoder_blocks = nn.ModuleList([
58
+ nn.Sequential(Conv2d(512, 512, kernel_size=1, stride=1, padding=0),),
59
+
60
+ nn.Sequential(Conv2dTranspose(1024, 512, kernel_size=3, stride=1, padding=0), # 3,3
61
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=1, residual=True),),
62
+
63
+ nn.Sequential(Conv2dTranspose(1024, 512, kernel_size=3, stride=2, padding=1, output_padding=1),
64
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=1, residual=True),
65
+ Conv2d(512, 512, kernel_size=3, stride=1, padding=1, residual=True),), # 6, 6
66
+
67
+ nn.Sequential(Conv2dTranspose(768, 384, kernel_size=3, stride=2, padding=1, output_padding=1),
68
+ Conv2d(384, 384, kernel_size=3, stride=1, padding=1, residual=True),
69
+ Conv2d(384, 384, kernel_size=3, stride=1, padding=1, residual=True),), # 12, 12
70
+
71
+ nn.Sequential(Conv2dTranspose(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1),
72
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),
73
+ Conv2d(256, 256, kernel_size=3, stride=1, padding=1, residual=True),), # 24, 24
74
+
75
+ nn.Sequential(Conv2dTranspose(320, 128, kernel_size=3, stride=2, padding=1, output_padding=1),
76
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),
77
+ Conv2d(128, 128, kernel_size=3, stride=1, padding=1, residual=True),), # 48, 48
78
+
79
+ nn.Sequential(Conv2dTranspose(160, 64, kernel_size=3, stride=2, padding=1, output_padding=1),
80
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),
81
+ Conv2d(64, 64, kernel_size=3, stride=1, padding=1, residual=True),),]) # 96,96
82
+
83
+ self.output_block = nn.Sequential(Conv2d(80, 32, kernel_size=3, stride=1, padding=1),
84
+ nn.Conv2d(32, 3, kernel_size=1, stride=1, padding=0),
85
+ nn.Sigmoid())
86
+
87
+ def forward(self, audio_sequences, face_sequences):
88
+ # audio_sequences = (B, T, 1, 80, 16)
89
+ B = audio_sequences.size(0)
90
+
91
+ input_dim_size = len(face_sequences.size())
92
+ if input_dim_size > 4:
93
+ audio_sequences = torch.cat([audio_sequences[:, i] for i in range(audio_sequences.size(1))], dim=0)
94
+ face_sequences = torch.cat([face_sequences[:, :, i] for i in range(face_sequences.size(2))], dim=0)
95
+
96
+ audio_embedding = self.audio_encoder(audio_sequences) # B, 512, 1, 1
97
+
98
+ feats = []
99
+ x = face_sequences
100
+ for f in self.face_encoder_blocks:
101
+ x = f(x)
102
+ feats.append(x)
103
+
104
+ x = audio_embedding
105
+ for f in self.face_decoder_blocks:
106
+ x = f(x)
107
+ try:
108
+ x = torch.cat((x, feats[-1]), dim=1)
109
+ except Exception as e:
110
+ print(x.size())
111
+ print(feats[-1].size())
112
+ raise e
113
+
114
+ feats.pop()
115
+
116
+ x = self.output_block(x)
117
+
118
+ if input_dim_size > 4:
119
+ x = torch.split(x, B, dim=0) # [(B, C, H, W)]
120
+ outputs = torch.stack(x, dim=2) # (B, C, T, H, W)
121
+
122
+ else:
123
+ outputs = x
124
+
125
+ return outputs
126
+
127
+ class Wav2Lip_disc_qual(nn.Module):
128
+ def __init__(self):
129
+ super(Wav2Lip_disc_qual, self).__init__()
130
+
131
+ self.face_encoder_blocks = nn.ModuleList([
132
+ nn.Sequential(nonorm_Conv2d(3, 32, kernel_size=7, stride=1, padding=3)), # 48,96
133
+
134
+ nn.Sequential(nonorm_Conv2d(32, 64, kernel_size=5, stride=(1, 2), padding=2), # 48,48
135
+ nonorm_Conv2d(64, 64, kernel_size=5, stride=1, padding=2)),
136
+
137
+ nn.Sequential(nonorm_Conv2d(64, 128, kernel_size=5, stride=2, padding=2), # 24,24
138
+ nonorm_Conv2d(128, 128, kernel_size=5, stride=1, padding=2)),
139
+
140
+ nn.Sequential(nonorm_Conv2d(128, 256, kernel_size=5, stride=2, padding=2), # 12,12
141
+ nonorm_Conv2d(256, 256, kernel_size=5, stride=1, padding=2)),
142
+
143
+ nn.Sequential(nonorm_Conv2d(256, 512, kernel_size=3, stride=2, padding=1), # 6,6
144
+ nonorm_Conv2d(512, 512, kernel_size=3, stride=1, padding=1)),
145
+
146
+ nn.Sequential(nonorm_Conv2d(512, 512, kernel_size=3, stride=2, padding=1), # 3,3
147
+ nonorm_Conv2d(512, 512, kernel_size=3, stride=1, padding=1),),
148
+
149
+ nn.Sequential(nonorm_Conv2d(512, 512, kernel_size=3, stride=1, padding=0), # 1, 1
150
+ nonorm_Conv2d(512, 512, kernel_size=1, stride=1, padding=0)),])
151
+
152
+ self.binary_pred = nn.Sequential(nn.Conv2d(512, 1, kernel_size=1, stride=1, padding=0), nn.Sigmoid())
153
+ self.label_noise = .0
154
+
155
+ def get_lower_half(self, face_sequences):
156
+ return face_sequences[:, :, face_sequences.size(2)//2:]
157
+
158
+ def to_2d(self, face_sequences):
159
+ B = face_sequences.size(0)
160
+ face_sequences = torch.cat([face_sequences[:, :, i] for i in range(face_sequences.size(2))], dim=0)
161
+ return face_sequences
162
+
163
+ def perceptual_forward(self, false_face_sequences):
164
+ false_face_sequences = self.to_2d(false_face_sequences)
165
+ false_face_sequences = self.get_lower_half(false_face_sequences)
166
+
167
+ false_feats = false_face_sequences
168
+ for f in self.face_encoder_blocks:
169
+ false_feats = f(false_feats)
170
+
171
+ false_pred_loss = F.binary_cross_entropy(self.binary_pred(false_feats).view(len(false_feats), -1),
172
+ torch.ones((len(false_feats), 1)).cuda())
173
+
174
+ return false_pred_loss
175
+
176
+ def forward(self, face_sequences):
177
+ face_sequences = self.to_2d(face_sequences)
178
+ face_sequences = self.get_lower_half(face_sequences)
179
+
180
+ x = face_sequences
181
+ for f in self.face_encoder_blocks:
182
+ x = f(x)
183
+
184
+ return self.binary_pred(x).view(len(x), -1)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.6
2
+ uvicorn[standard]==0.34.0
3
+ python-multipart==0.0.20
4
+ torch
5
+ torchvision
6
+ --extra-index-url https://download.pytorch.org/whl/cpu
7
+ opencv-python-headless==4.10.0.84
8
+ librosa==0.9.2
9
+ mediapipe==0.10.14
10
+ numpy<2
11
+ huggingface_hub==0.25.2
12
+ ffmpeg-python