Spaces:
Paused
Paused
Upload 8 files
Browse files- README.md +27 -49
- VideoSwapping.py +8 -77
- app.py +130 -822
- benchmark.py +21 -0
- download_model.py +19 -0
- optimized_swapper.py +243 -0
- packages.txt +1 -0
- requirements.txt +14 -7
README.md
CHANGED
|
@@ -1,57 +1,35 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
## What changed vs the original
|
| 19 |
-
|
| 20 |
-
- Explicit `CUDAExecutionProvider` preference via onnxruntime
|
| 21 |
-
- Robust Gradio video path handling (fixes all video tabs)
|
| 22 |
-
- Cleaned frame extraction + resume logic
|
| 23 |
-
- Better error reporting in the Log box
|
| 24 |
-
- Workdir isolation under `workdir/`
|
| 25 |
-
- `opencv-python-headless` (safer on servers)
|
| 26 |
-
- Proper `demo.queue()` for concurrent jobs
|
| 27 |
-
- `ssr_mode=False` to avoid Gradio SSR 405 errors
|
| 28 |
-
- ffmpeg system package declared
|
| 29 |
-
|
| 30 |
-
## Requirements on the rented machine
|
| 31 |
-
|
| 32 |
-
- NVIDIA GPU + drivers
|
| 33 |
-
- CUDA-compatible onnxruntime-gpu (already in requirements)
|
| 34 |
-
- ~4–8 GB VRAM recommended for 1080p video
|
| 35 |
-
- ffmpeg installed (packages.txt)
|
| 36 |
-
|
| 37 |
-
## Local / rented GPU launch
|
| 38 |
|
| 39 |
```bash
|
| 40 |
-
|
| 41 |
-
python app.py --share # creates a public link
|
| 42 |
-
# or
|
| 43 |
-
python app.py --server-port 7860
|
| 44 |
```
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
- On a modern GPU expect 5–15 fps effective (depends on resolution & number of faces).
|
| 53 |
-
- Resume is automatic: if the process is killed, re-run and already-swapped frames are skipped.
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
|
|
|
| 1 |
+
# FaceSwapAll — Optimized
|
| 2 |
+
|
| 3 |
+
Optimized InsightFace/ONNX Runtime face-swap Space for NVIDIA CUDA GPUs, with CPU fallback.
|
| 4 |
+
|
| 5 |
+
## Main optimizations
|
| 6 |
+
- Persistent InsightFace models
|
| 7 |
+
- Source-face detection/embedding cached per job
|
| 8 |
+
- In-memory video frame processing
|
| 9 |
+
- Reduced filesystem I/O
|
| 10 |
+
- Configurable detector size and detection interval
|
| 11 |
+
- CUDAExecutionProvider when available
|
| 12 |
+
- FFmpeg/OpenCV video output
|
| 13 |
+
- Original audio preservation when FFmpeg is available
|
| 14 |
+
|
| 15 |
+
## Hugging Face Space
|
| 16 |
+
The original project contains `inswapper_128.onnx` as a 554 MB Xet/LFS file. This ZIP intentionally does not duplicate that large binary. After uploading these files to the Space, run:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
```bash
|
| 19 |
+
python download_model.py
|
|
|
|
|
|
|
|
|
|
| 20 |
```
|
| 21 |
|
| 22 |
+
or keep the existing `inswapper_128.onnx` in the repository root.
|
| 23 |
|
| 24 |
+
The original repository and model are at:
|
| 25 |
+
`dosesnrolls1/FaceSwapAll`
|
| 26 |
|
| 27 |
+
## Git LFS
|
| 28 |
+
If you want the model tracked directly in Git, install Git LFS and add:
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
```bash
|
| 31 |
+
git lfs install
|
| 32 |
+
git add inswapper_128.onnx
|
| 33 |
+
```
|
| 34 |
|
| 35 |
+
The included `.gitattributes` already marks ONNX files for LFS.
|
VideoSwapping.py
CHANGED
|
@@ -1,81 +1,12 @@
|
|
| 1 |
-
import cv2
|
| 2 |
-
import os
|
| 3 |
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
"""
|
| 7 |
-
Extract frames with resume support.
|
| 8 |
-
Returns ordered list of frame paths.
|
| 9 |
-
"""
|
| 10 |
-
os.makedirs(frames_dir, exist_ok=True)
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
)
|
| 16 |
-
last_idx = -1
|
| 17 |
-
if existing:
|
| 18 |
-
try:
|
| 19 |
-
last_idx = max(int(f.split("_")[1].split(".")[0]) for f in existing)
|
| 20 |
-
except Exception:
|
| 21 |
-
last_idx = -1
|
| 22 |
-
|
| 23 |
-
cap = cv2.VideoCapture(video_path)
|
| 24 |
-
if not cap.isOpened():
|
| 25 |
-
raise RuntimeError(f"Cannot open video: {video_path}")
|
| 26 |
-
|
| 27 |
-
frame_paths = []
|
| 28 |
-
idx = 0
|
| 29 |
-
while True:
|
| 30 |
-
if max_frames is not None and idx >= max_frames:
|
| 31 |
-
break
|
| 32 |
-
ret, frame = cap.read()
|
| 33 |
-
if not ret:
|
| 34 |
-
break
|
| 35 |
-
frame_path = os.path.join(frames_dir, f"frame_{idx:05d}.jpg")
|
| 36 |
-
if idx > last_idx:
|
| 37 |
-
cv2.imwrite(frame_path, frame)
|
| 38 |
-
frame_paths.append(frame_path)
|
| 39 |
-
idx += 1
|
| 40 |
-
cap.release()
|
| 41 |
-
|
| 42 |
-
if not frame_paths:
|
| 43 |
-
raise RuntimeError("No frames could be extracted from the video")
|
| 44 |
-
return frame_paths
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def frames_to_video(frames_dir, output_video_path, fps, fourcc="mp4v"):
|
| 48 |
-
"""
|
| 49 |
-
Rebuild video from sorted frame_*.jpg or swapped_*.jpg files.
|
| 50 |
-
"""
|
| 51 |
-
frames = sorted(
|
| 52 |
-
[
|
| 53 |
-
os.path.join(frames_dir, f)
|
| 54 |
-
for f in os.listdir(frames_dir)
|
| 55 |
-
if f.endswith(".jpg") and (f.startswith("frame_") or f.startswith("swapped_"))
|
| 56 |
-
]
|
| 57 |
-
)
|
| 58 |
-
if not frames:
|
| 59 |
-
raise RuntimeError(f"No frames found in {frames_dir}")
|
| 60 |
-
|
| 61 |
-
first = cv2.imread(frames[0])
|
| 62 |
-
if first is None:
|
| 63 |
-
raise RuntimeError(f"Cannot read first frame: {frames[0]}")
|
| 64 |
-
height, width = first.shape[:2]
|
| 65 |
-
|
| 66 |
-
writer = cv2.VideoWriter(
|
| 67 |
-
output_video_path,
|
| 68 |
-
cv2.VideoWriter_fourcc(*fourcc),
|
| 69 |
-
fps,
|
| 70 |
-
(width, height)
|
| 71 |
-
)
|
| 72 |
-
written = 0
|
| 73 |
-
for p in frames:
|
| 74 |
-
img = cv2.imread(p)
|
| 75 |
-
if img is not None:
|
| 76 |
-
writer.write(img)
|
| 77 |
-
written += 1
|
| 78 |
-
writer.release()
|
| 79 |
-
|
| 80 |
-
if written == 0 or not os.path.exists(output_video_path) or os.path.getsize(output_video_path) < 1000:
|
| 81 |
-
raise RuntimeError(f"Failed to write video: {output_video_path}")
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
"""Backward-compatible wrapper around the optimized video engine."""
|
| 3 |
+
from optimized_swapper import FaceSwapEngine
|
| 4 |
|
| 5 |
+
_ENGINE = FaceSwapEngine()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
def swap_video(source_img, video_path, source_face_idx=1, target_face_idx=1,
|
| 8 |
+
det_size=320, detection_interval=1):
|
| 9 |
+
return _ENGINE.swap_video(
|
| 10 |
+
source_img, source_face_idx, video_path, target_face_idx,
|
| 11 |
+
det_size=det_size, detection_interval=detection_interval
|
| 12 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,843 +1,151 @@
|
|
| 1 |
-
|
| 2 |
import os
|
| 3 |
-
import cv2
|
| 4 |
-
import numpy as np
|
| 5 |
-
import shutil
|
| 6 |
-
import subprocess
|
| 7 |
import time
|
| 8 |
-
|
| 9 |
-
import
|
| 10 |
-
|
| 11 |
-
# ------------------------------------------------------------------
|
| 12 |
-
# Global swapper (GPU if available)
|
| 13 |
-
# ------------------------------------------------------------------
|
| 14 |
-
print("Initializing FaceSwapper (this may take a moment on first run)...")
|
| 15 |
-
swapper = FaceSwapper(det_size=(640, 640), ctx_id=0)
|
| 16 |
-
print("FaceSwapper ready.")
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
# Helpers
|
| 21 |
-
# ------------------------------------------------------------------
|
| 22 |
-
def add_audio_to_video(original_video_path, video_no_audio_path, output_path):
|
| 23 |
-
"""Mux original audio onto the silent swapped video using ffmpeg."""
|
| 24 |
-
cmd = [
|
| 25 |
-
"ffmpeg", "-y",
|
| 26 |
-
"-i", video_no_audio_path,
|
| 27 |
-
"-i", original_video_path,
|
| 28 |
-
"-c:v", "copy",
|
| 29 |
-
"-c:a", "aac",
|
| 30 |
-
"-map", "0:v:0",
|
| 31 |
-
"-map", "1:a:0?",
|
| 32 |
-
"-shortest",
|
| 33 |
-
output_path
|
| 34 |
-
]
|
| 35 |
-
try:
|
| 36 |
-
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 37 |
-
return True, ""
|
| 38 |
-
except subprocess.CalledProcessError as e:
|
| 39 |
-
return False, e.stderr.decode(errors="ignore")
|
| 40 |
|
|
|
|
| 41 |
|
| 42 |
-
def
|
| 43 |
try:
|
| 44 |
-
|
| 45 |
-
shutil.rmtree(path)
|
| 46 |
except Exception:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
if
|
| 60 |
-
|
| 61 |
-
if
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
start = time.time()
|
| 76 |
-
try:
|
| 77 |
-
progress(0, desc="Preparing")
|
| 78 |
-
src_path = "workdir/SinglePhoto/data_src.jpg"
|
| 79 |
-
dst_path = "workdir/SinglePhoto/data_dst.jpg"
|
| 80 |
-
out_path = "workdir/SinglePhoto/output_swapped.jpg"
|
| 81 |
-
ensure_dirs(os.path.dirname(src_path), os.path.dirname(out_path))
|
| 82 |
-
|
| 83 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 84 |
-
cv2.imwrite(dst_path, cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR))
|
| 85 |
-
log += "Saved source & destination\n"
|
| 86 |
-
|
| 87 |
-
progress(0.4, desc="Swapping on GPU")
|
| 88 |
-
result = swapper.swap_faces(src_path, int(src_idx), dst_path, int(dst_idx))
|
| 89 |
-
cv2.imwrite(out_path, result)
|
| 90 |
-
log += f"Saved result → {out_path}\n"
|
| 91 |
-
|
| 92 |
-
for p in (src_path, dst_path):
|
| 93 |
-
if os.path.exists(p):
|
| 94 |
-
os.remove(p)
|
| 95 |
-
|
| 96 |
-
progress(1, desc="Done")
|
| 97 |
-
log += f"Elapsed: {time.time()-start:.2f}s\n"
|
| 98 |
-
return out_path, log
|
| 99 |
-
except Exception as e:
|
| 100 |
-
log += f"ERROR: {e}\n"
|
| 101 |
-
return None, log
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def swap_single_src_multi_dst(src_img, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)):
|
| 105 |
-
log = ""
|
| 106 |
-
results = []
|
| 107 |
-
base = "workdir/SingleSrcMultiDst"
|
| 108 |
-
src_dir = f"{base}/src"
|
| 109 |
-
dst_dir = f"{base}/dst"
|
| 110 |
-
out_dir = f"{base}/output"
|
| 111 |
-
ensure_dirs(src_dir, dst_dir, out_dir)
|
| 112 |
-
|
| 113 |
-
try:
|
| 114 |
-
if isinstance(src_img, tuple):
|
| 115 |
-
src_img = src_img[0]
|
| 116 |
-
src_path = os.path.join(src_dir, "data_src.jpg")
|
| 117 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 118 |
-
log += "Saved source image\n"
|
| 119 |
-
|
| 120 |
-
if isinstance(dst_indices, str):
|
| 121 |
-
idx_list = [int(x.strip()) for x in dst_indices.split(",") if x.strip().isdigit()]
|
| 122 |
-
else:
|
| 123 |
-
idx_list = [int(x) for x in dst_indices]
|
| 124 |
-
|
| 125 |
-
total = max(1, len(dst_imgs))
|
| 126 |
-
for j, dst_img in enumerate(dst_imgs):
|
| 127 |
-
if isinstance(dst_img, tuple):
|
| 128 |
-
dst_img = dst_img[0]
|
| 129 |
-
if dst_img is None:
|
| 130 |
-
results.append(None)
|
| 131 |
-
continue
|
| 132 |
-
dst_path = os.path.join(dst_dir, f"data_dst_{j}.jpg")
|
| 133 |
-
out_path = os.path.join(out_dir, f"output_swapped_{j}.jpg")
|
| 134 |
-
cv2.imwrite(dst_path, cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR))
|
| 135 |
-
try:
|
| 136 |
-
dst_idx = idx_list[j] if j < len(idx_list) else 1
|
| 137 |
-
result = swapper.swap_faces(src_path, 1, dst_path, dst_idx)
|
| 138 |
-
cv2.imwrite(out_path, result)
|
| 139 |
-
results.append(out_path)
|
| 140 |
-
log += f"OK: src → dst[{j}] (face {dst_idx})\n"
|
| 141 |
-
except Exception as e:
|
| 142 |
-
results.append(None)
|
| 143 |
-
log += f"FAIL dst[{j}]: {e}\n"
|
| 144 |
-
progress((j + 1) / total, desc=f"Dst {j+1}/{total}")
|
| 145 |
-
return results, log
|
| 146 |
-
except Exception as e:
|
| 147 |
-
log += f"FATAL: {e}\n"
|
| 148 |
-
return results, log
|
| 149 |
-
|
| 150 |
|
| 151 |
-
def
|
| 152 |
-
|
|
|
|
| 153 |
results = []
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
for i, src_img in enumerate(src_imgs):
|
| 169 |
-
if isinstance(src_img, tuple):
|
| 170 |
-
src_img = src_img[0]
|
| 171 |
-
if src_img is None:
|
| 172 |
-
results.append(None)
|
| 173 |
-
continue
|
| 174 |
-
src_path = os.path.join(src_dir, f"data_src_{i}.jpg")
|
| 175 |
-
out_path = os.path.join(out_dir, f"output_swapped_{i}.jpg")
|
| 176 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 177 |
-
try:
|
| 178 |
-
result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx))
|
| 179 |
-
cv2.imwrite(out_path, result)
|
| 180 |
-
results.append(out_path)
|
| 181 |
-
log += f"OK: src[{i}] → dst\n"
|
| 182 |
-
except Exception as e:
|
| 183 |
-
results.append(None)
|
| 184 |
-
log += f"FAIL src[{i}]: {e}\n"
|
| 185 |
-
progress((i + 1) / total, desc=f"Src {i+1}/{total}")
|
| 186 |
-
return results, log
|
| 187 |
-
except Exception as e:
|
| 188 |
-
log += f"FATAL: {e}\n"
|
| 189 |
-
return results, log
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def swap_multi_src_multi_dst(src_imgs, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)):
|
| 193 |
-
log = ""
|
| 194 |
results = []
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
for
|
| 210 |
-
if
|
| 211 |
-
src_img = src_img[0]
|
| 212 |
-
if src_img is None:
|
| 213 |
-
continue
|
| 214 |
-
src_path = os.path.join(src_dir, f"data_src_{i}.jpg")
|
| 215 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 216 |
-
|
| 217 |
-
for j, dst_img in enumerate(dst_imgs):
|
| 218 |
-
if isinstance(dst_img, tuple):
|
| 219 |
-
dst_img = dst_img[0]
|
| 220 |
-
if dst_img is None:
|
| 221 |
-
continue
|
| 222 |
-
dst_path = os.path.join(dst_dir, f"data_dst_{j}.jpg")
|
| 223 |
-
out_path = os.path.join(out_dir, f"output_swapped_{i}_{j}.jpg")
|
| 224 |
-
cv2.imwrite(dst_path, cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR))
|
| 225 |
-
try:
|
| 226 |
-
dst_idx = idx_list[j] if j < len(idx_list) else 1
|
| 227 |
-
result = swapper.swap_faces(src_path, 1, dst_path, dst_idx)
|
| 228 |
-
cv2.imwrite(out_path, result)
|
| 229 |
-
results.append(out_path)
|
| 230 |
-
log += f"OK src[{i}]→dst[{j}]\n"
|
| 231 |
-
except Exception as e:
|
| 232 |
-
results.append(None)
|
| 233 |
-
log += f"FAIL {i}/{j}: {e}\n"
|
| 234 |
-
count += 1
|
| 235 |
-
progress(count / total, desc=f"{count}/{total}")
|
| 236 |
-
return results, log
|
| 237 |
-
except Exception as e:
|
| 238 |
-
log += f"FATAL: {e}\n"
|
| 239 |
-
return results, log
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
def swap_faces_custom(src_imgs, dst_img, mapping_str, progress=gr.Progress(track_tqdm=True)):
|
| 243 |
-
log = ""
|
| 244 |
-
start = time.time()
|
| 245 |
-
base = "workdir/CustomSwap"
|
| 246 |
-
src_dir = f"{base}/src"
|
| 247 |
-
temp_dir = f"{base}/temp"
|
| 248 |
-
dst_path = f"{base}/data_dst.jpg"
|
| 249 |
-
out_path = f"{base}/output_swapped.jpg"
|
| 250 |
-
ensure_dirs(src_dir, temp_dir)
|
| 251 |
-
|
| 252 |
-
try:
|
| 253 |
-
cv2.imwrite(dst_path, cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR))
|
| 254 |
-
log += "Saved destination\n"
|
| 255 |
-
|
| 256 |
-
src_paths = []
|
| 257 |
-
for i, src_img in enumerate(src_imgs):
|
| 258 |
-
if isinstance(src_img, tuple):
|
| 259 |
-
src_img = src_img[0]
|
| 260 |
-
if src_img is None:
|
| 261 |
-
continue
|
| 262 |
-
p = os.path.join(src_dir, f"data_src_{i+1}.jpg")
|
| 263 |
-
cv2.imwrite(p, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 264 |
-
src_paths.append(p)
|
| 265 |
-
log += f"Saved source {i+1}\n"
|
| 266 |
-
|
| 267 |
-
try:
|
| 268 |
-
mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()]
|
| 269 |
-
except Exception as e:
|
| 270 |
-
return None, f"Bad mapping: {e}"
|
| 271 |
-
|
| 272 |
-
temp_dst = os.path.join(temp_dir, "temp_dst.jpg")
|
| 273 |
-
shutil.copy(dst_path, temp_dst)
|
| 274 |
-
|
| 275 |
-
for face_idx, src_idx in enumerate(mapping, start=1):
|
| 276 |
-
if src_idx < 1 or src_idx > len(src_paths):
|
| 277 |
-
log += f"Skip invalid src index {src_idx} for face {face_idx}\n"
|
| 278 |
-
continue
|
| 279 |
try:
|
| 280 |
-
|
| 281 |
-
cv2.imwrite(temp_dst, swapped)
|
| 282 |
-
log += f"Swapped face {face_idx} ← source {src_idx}\n"
|
| 283 |
except Exception as e:
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
# ------------------------------------------------------------------
|
| 296 |
-
# Video functions – robust Gradio path handling
|
| 297 |
-
# ------------------------------------------------------------------
|
| 298 |
-
def swap_video(src_img, src_idx, video, dst_idx,
|
| 299 |
-
delete_frames_dir=True, add_audio=True,
|
| 300 |
-
progress=gr.Progress()):
|
| 301 |
-
log = ""
|
| 302 |
-
start = time.time()
|
| 303 |
-
base = "workdir/VideoSwapping"
|
| 304 |
-
src_path = f"{base}/data_src.jpg"
|
| 305 |
-
dst_video = f"{base}/data_dst.mp4"
|
| 306 |
-
frames_dir = f"{base}/video_frames"
|
| 307 |
-
swapped_dir = f"{base}/swapped_frames"
|
| 308 |
-
tmp_video = f"{base}/tmp_no_audio.mp4"
|
| 309 |
-
final_video = f"{base}/output_with_audio.mp4"
|
| 310 |
-
|
| 311 |
-
ensure_dirs(base, frames_dir, swapped_dir)
|
| 312 |
-
|
| 313 |
-
try:
|
| 314 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 315 |
-
log += "Saved source image\n"
|
| 316 |
-
progress(0.05, desc="Source ready")
|
| 317 |
-
|
| 318 |
-
video_path = _resolve_video_path(video)
|
| 319 |
-
shutil.copy(video_path, dst_video)
|
| 320 |
-
log += f"Copied video → {dst_video}\n"
|
| 321 |
-
|
| 322 |
-
from VideoSwapping import extract_frames, frames_to_video
|
| 323 |
-
|
| 324 |
-
frame_paths = extract_frames(dst_video, frames_dir)
|
| 325 |
-
total = len(frame_paths)
|
| 326 |
-
log += f"Extracted {total} frames\n"
|
| 327 |
-
progress(0.12, desc=f"Extracted {total} frames")
|
| 328 |
-
|
| 329 |
-
already = set(os.listdir(swapped_dir))
|
| 330 |
-
loop_start = time.time()
|
| 331 |
-
|
| 332 |
-
for idx, frame_path in enumerate(frame_paths):
|
| 333 |
-
name = f"swapped_{idx:05d}.jpg"
|
| 334 |
-
out_p = os.path.join(swapped_dir, name)
|
| 335 |
-
if name in already and os.path.exists(out_p):
|
| 336 |
-
pass
|
| 337 |
-
else:
|
| 338 |
-
try:
|
| 339 |
-
swapped = swapper.swap_faces(
|
| 340 |
-
src_path, int(src_idx), frame_path, int(dst_idx)
|
| 341 |
-
)
|
| 342 |
-
cv2.imwrite(out_p, swapped)
|
| 343 |
-
except ValueError as ve:
|
| 344 |
-
if "Target image contains" in str(ve) and int(dst_idx) != 1:
|
| 345 |
-
swapped = swapper.swap_faces(
|
| 346 |
-
src_path, int(src_idx), frame_path, 1
|
| 347 |
-
)
|
| 348 |
-
cv2.imwrite(out_p, swapped)
|
| 349 |
-
log += f"Frame {idx}: fell back to face 1\n"
|
| 350 |
-
else:
|
| 351 |
-
shutil.copy(frame_path, out_p)
|
| 352 |
-
log += f"Frame {idx} failed: {ve}\n"
|
| 353 |
-
except Exception as e:
|
| 354 |
-
shutil.copy(frame_path, out_p)
|
| 355 |
-
log += f"Frame {idx} failed, kept original: {e}\n"
|
| 356 |
-
|
| 357 |
-
elapsed = time.time() - loop_start
|
| 358 |
-
avg = elapsed / (idx + 1)
|
| 359 |
-
remain = avg * (total - idx - 1)
|
| 360 |
-
m, s = divmod(int(remain), 60)
|
| 361 |
-
progress(
|
| 362 |
-
0.12 + 0.7 * (idx + 1) / total,
|
| 363 |
-
desc=f"GPU swap {idx+1}/{total} | ETA {m:02d}:{s:02d}"
|
| 364 |
-
)
|
| 365 |
-
|
| 366 |
-
cap = cv2.VideoCapture(dst_video)
|
| 367 |
-
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
| 368 |
-
cap.release()
|
| 369 |
-
frames_to_video(swapped_dir, tmp_video, fps)
|
| 370 |
-
log += f"Rebuilt silent video @ {fps:.2f} fps\n"
|
| 371 |
-
progress(0.88, desc="Muxing audio")
|
| 372 |
-
|
| 373 |
-
if add_audio:
|
| 374 |
-
ok, err = add_audio_to_video(dst_video, tmp_video, final_video)
|
| 375 |
-
if ok:
|
| 376 |
-
log += "Audio muxed successfully\n"
|
| 377 |
-
result_path = final_video
|
| 378 |
-
else:
|
| 379 |
-
log += f"Audio mux failed: {err}\n"
|
| 380 |
-
result_path = tmp_video
|
| 381 |
-
else:
|
| 382 |
-
result_path = tmp_video
|
| 383 |
-
log += "Audio skipped by user\n"
|
| 384 |
-
|
| 385 |
-
if os.path.exists(src_path):
|
| 386 |
-
os.remove(src_path)
|
| 387 |
-
if delete_frames_dir:
|
| 388 |
-
safe_rmtree(frames_dir)
|
| 389 |
-
log += "Deleted frames dir\n"
|
| 390 |
-
safe_rmtree(swapped_dir)
|
| 391 |
-
|
| 392 |
-
log += f"Total time: {time.time()-start:.1f}s\n"
|
| 393 |
-
progress(1, desc="Done")
|
| 394 |
-
return result_path, log
|
| 395 |
-
|
| 396 |
-
except Exception as e:
|
| 397 |
-
log += f"FATAL: {e}\n"
|
| 398 |
-
return None, log
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
def swap_video_all_faces(src_img, video, num_faces_to_swap,
|
| 402 |
-
delete_frames_dir=True, add_audio=True,
|
| 403 |
-
progress=gr.Progress()):
|
| 404 |
-
"""Swap the same source face onto the first N faces of every frame."""
|
| 405 |
-
log = ""
|
| 406 |
-
start = time.time()
|
| 407 |
-
base = "workdir/VideoAllFaces"
|
| 408 |
-
src_path = f"{base}/data_src.jpg"
|
| 409 |
-
dst_video = f"{base}/data_dst.mp4"
|
| 410 |
-
frames_dir = f"{base}/video_frames"
|
| 411 |
-
swapped_dir = f"{base}/swapped_frames"
|
| 412 |
-
temp_dir = f"{base}/temp"
|
| 413 |
-
tmp_video = f"{base}/tmp_no_audio.mp4"
|
| 414 |
-
final_video = f"{base}/output_with_audio.mp4"
|
| 415 |
-
|
| 416 |
-
ensure_dirs(base, frames_dir, swapped_dir, temp_dir)
|
| 417 |
-
|
| 418 |
-
try:
|
| 419 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 420 |
-
log += "Saved source\n"
|
| 421 |
-
|
| 422 |
-
video_path = _resolve_video_path(video)
|
| 423 |
-
shutil.copy(video_path, dst_video)
|
| 424 |
-
log += f"Copied video → {dst_video}\n"
|
| 425 |
-
|
| 426 |
-
from VideoSwapping import extract_frames, frames_to_video
|
| 427 |
-
frame_paths = extract_frames(dst_video, frames_dir)
|
| 428 |
-
total = len(frame_paths)
|
| 429 |
-
log += f"Extracted {total} frames\n"
|
| 430 |
-
progress(0.1, desc=f"{total} frames")
|
| 431 |
-
|
| 432 |
-
already = set(os.listdir(swapped_dir))
|
| 433 |
-
loop_start = time.time()
|
| 434 |
-
n_faces = max(1, int(num_faces_to_swap))
|
| 435 |
-
|
| 436 |
-
for idx, frame_path in enumerate(frame_paths):
|
| 437 |
-
name = f"swapped_{idx:05d}.jpg"
|
| 438 |
-
out_p = os.path.join(swapped_dir, name)
|
| 439 |
-
if name in already and os.path.exists(out_p):
|
| 440 |
-
pass
|
| 441 |
-
else:
|
| 442 |
-
temp_frame = os.path.join(temp_dir, "t.jpg")
|
| 443 |
-
shutil.copy(frame_path, temp_frame)
|
| 444 |
-
for face_i in range(1, n_faces + 1):
|
| 445 |
-
try:
|
| 446 |
-
swapped = swapper.swap_faces(src_path, 1, temp_frame, face_i)
|
| 447 |
-
cv2.imwrite(temp_frame, swapped)
|
| 448 |
-
except Exception as e:
|
| 449 |
-
log += f"Frame {idx} face {face_i}: {e}\n"
|
| 450 |
-
break
|
| 451 |
-
shutil.copy(temp_frame, out_p)
|
| 452 |
-
if os.path.exists(temp_frame):
|
| 453 |
-
os.remove(temp_frame)
|
| 454 |
-
|
| 455 |
-
elapsed = time.time() - loop_start
|
| 456 |
-
avg = elapsed / (idx + 1)
|
| 457 |
-
remain = avg * (total - idx - 1)
|
| 458 |
-
m, s = divmod(int(remain), 60)
|
| 459 |
-
progress(0.1 + 0.75 * (idx + 1) / total,
|
| 460 |
-
desc=f"{idx+1}/{total} | ETA {m:02d}:{s:02d}")
|
| 461 |
-
|
| 462 |
-
safe_rmtree(temp_dir)
|
| 463 |
-
|
| 464 |
-
cap = cv2.VideoCapture(dst_video)
|
| 465 |
-
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
| 466 |
-
cap.release()
|
| 467 |
-
frames_to_video(swapped_dir, tmp_video, fps)
|
| 468 |
-
|
| 469 |
-
if add_audio:
|
| 470 |
-
ok, err = add_audio_to_video(dst_video, tmp_video, final_video)
|
| 471 |
-
result_path = final_video if ok else tmp_video
|
| 472 |
-
if not ok:
|
| 473 |
-
log += f"Audio fail: {err}\n"
|
| 474 |
-
else:
|
| 475 |
-
result_path = tmp_video
|
| 476 |
-
|
| 477 |
-
if delete_frames_dir:
|
| 478 |
-
safe_rmtree(frames_dir)
|
| 479 |
-
safe_rmtree(swapped_dir)
|
| 480 |
-
|
| 481 |
-
log += f"Total: {time.time()-start:.1f}s\n"
|
| 482 |
-
progress(1, desc="Done")
|
| 483 |
-
return result_path, log
|
| 484 |
-
|
| 485 |
-
except Exception as e:
|
| 486 |
-
log += f"FATAL: {e}\n"
|
| 487 |
-
return None, log
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
def swap_video_custom_mapping(src_imgs, video, mapping_str,
|
| 491 |
-
delete_frames_dir=True, add_audio=True,
|
| 492 |
-
progress=gr.Progress()):
|
| 493 |
-
log = ""
|
| 494 |
-
start = time.time()
|
| 495 |
-
base = "workdir/CustomVideo"
|
| 496 |
-
src_dir = f"{base}/src"
|
| 497 |
-
frames_dir = f"{base}/frames"
|
| 498 |
-
swapped_dir = f"{base}/swapped"
|
| 499 |
-
temp_dir = f"{base}/temp"
|
| 500 |
-
dst_video = f"{base}/data_dst.mp4"
|
| 501 |
-
tmp_video = f"{base}/tmp_no_audio.mp4"
|
| 502 |
-
final_video = f"{base}/output_with_audio.mp4"
|
| 503 |
-
|
| 504 |
-
ensure_dirs(src_dir, frames_dir, swapped_dir, temp_dir)
|
| 505 |
-
|
| 506 |
-
try:
|
| 507 |
-
# save sources
|
| 508 |
-
src_paths = []
|
| 509 |
-
for i, img in enumerate(src_imgs):
|
| 510 |
-
if isinstance(img, tuple):
|
| 511 |
-
img = img[0]
|
| 512 |
-
if img is None:
|
| 513 |
-
continue
|
| 514 |
-
p = os.path.join(src_dir, f"src_{i+1}.jpg")
|
| 515 |
-
cv2.imwrite(p, cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
|
| 516 |
-
src_paths.append(p)
|
| 517 |
-
log += f"Saved {len(src_paths)} source faces\n"
|
| 518 |
-
|
| 519 |
-
try:
|
| 520 |
-
mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()]
|
| 521 |
-
except Exception as e:
|
| 522 |
-
return None, f"Bad mapping: {e}"
|
| 523 |
-
|
| 524 |
-
video_path = _resolve_video_path(video)
|
| 525 |
-
shutil.copy(video_path, dst_video)
|
| 526 |
-
log += f"Copied video → {dst_video}\n"
|
| 527 |
-
|
| 528 |
-
from VideoSwapping import extract_frames, frames_to_video
|
| 529 |
-
frame_paths = extract_frames(dst_video, frames_dir)
|
| 530 |
-
total = len(frame_paths)
|
| 531 |
-
log += f"Extracted {total} frames\n"
|
| 532 |
-
progress(0.08, desc=f"{total} frames")
|
| 533 |
-
|
| 534 |
-
already = set(os.listdir(swapped_dir))
|
| 535 |
-
loop_start = time.time()
|
| 536 |
-
temp_frame = os.path.join(temp_dir, "t.jpg")
|
| 537 |
-
|
| 538 |
-
for idx, frame_path in enumerate(frame_paths):
|
| 539 |
-
name = f"swapped_{idx:05d}.jpg"
|
| 540 |
-
out_p = os.path.join(swapped_dir, name)
|
| 541 |
-
if name in already and os.path.exists(out_p):
|
| 542 |
-
pass
|
| 543 |
-
else:
|
| 544 |
-
shutil.copy(frame_path, temp_frame)
|
| 545 |
-
for face_idx, src_idx in enumerate(mapping, start=1):
|
| 546 |
-
if src_idx < 1 or src_idx > len(src_paths):
|
| 547 |
-
continue
|
| 548 |
-
try:
|
| 549 |
-
swapped = swapper.swap_faces(
|
| 550 |
-
src_paths[src_idx-1], 1, temp_frame, face_idx
|
| 551 |
-
)
|
| 552 |
-
cv2.imwrite(temp_frame, swapped)
|
| 553 |
-
except Exception as e:
|
| 554 |
-
log += f"F{idx} face{face_idx}: {e}\n"
|
| 555 |
-
shutil.copy(temp_frame, out_p)
|
| 556 |
-
|
| 557 |
-
elapsed = time.time() - loop_start
|
| 558 |
-
avg = elapsed / (idx + 1)
|
| 559 |
-
remain = avg * (total - idx - 1)
|
| 560 |
-
m, s = divmod(int(remain), 60)
|
| 561 |
-
progress(0.08 + 0.75 * (idx + 1) / total,
|
| 562 |
-
desc=f"{idx+1}/{total} | ETA {m:02d}:{s:02d}")
|
| 563 |
-
|
| 564 |
-
safe_rmtree(temp_dir)
|
| 565 |
-
|
| 566 |
-
cap = cv2.VideoCapture(dst_video)
|
| 567 |
-
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
| 568 |
-
cap.release()
|
| 569 |
-
frames_to_video(swapped_dir, tmp_video, fps)
|
| 570 |
-
|
| 571 |
-
if add_audio:
|
| 572 |
-
ok, err = add_audio_to_video(dst_video, tmp_video, final_video)
|
| 573 |
-
result_path = final_video if ok else tmp_video
|
| 574 |
-
if not ok:
|
| 575 |
-
log += f"Audio fail: {err}\n"
|
| 576 |
-
else:
|
| 577 |
-
result_path = tmp_video
|
| 578 |
-
|
| 579 |
-
if delete_frames_dir:
|
| 580 |
-
safe_rmtree(frames_dir)
|
| 581 |
-
safe_rmtree(swapped_dir)
|
| 582 |
-
|
| 583 |
-
log += f"Total: {time.time()-start:.1f}s\n"
|
| 584 |
-
progress(1, desc="Done")
|
| 585 |
-
return result_path, log
|
| 586 |
-
|
| 587 |
-
except Exception as e:
|
| 588 |
-
log += f"FATAL: {e}\n"
|
| 589 |
-
return None, log
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
def swap_single_src_multi_video(src_img, dst_videos, dst_indices,
|
| 593 |
-
delete_frames_dir=True, add_audio=True,
|
| 594 |
-
progress=gr.Progress(track_tqdm=True)):
|
| 595 |
-
log = ""
|
| 596 |
-
results = []
|
| 597 |
-
start = time.time()
|
| 598 |
-
base = "workdir/SingleSrcMultiVideo"
|
| 599 |
-
ensure_dirs(base)
|
| 600 |
-
|
| 601 |
-
try:
|
| 602 |
-
if isinstance(dst_indices, str):
|
| 603 |
-
idx_list = [int(x.strip()) for x in dst_indices.split(",") if x.strip().isdigit()]
|
| 604 |
-
else:
|
| 605 |
-
idx_list = [int(x) for x in dst_indices]
|
| 606 |
-
|
| 607 |
-
src_path = os.path.join(base, "data_src.jpg")
|
| 608 |
-
cv2.imwrite(src_path, cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR))
|
| 609 |
-
log += "Saved source\n"
|
| 610 |
-
|
| 611 |
-
from VideoSwapping import extract_frames, frames_to_video
|
| 612 |
-
|
| 613 |
-
# Gradio File(file_count="multiple") can return list of paths or list of objects
|
| 614 |
-
if not isinstance(dst_videos, (list, tuple)):
|
| 615 |
-
dst_videos = [dst_videos]
|
| 616 |
-
|
| 617 |
-
n_videos = len(dst_videos)
|
| 618 |
-
|
| 619 |
-
for i, video in enumerate(dst_videos):
|
| 620 |
-
dst_idx = idx_list[i] if i < len(idx_list) else 1
|
| 621 |
-
v_base = os.path.join(base, f"v{i}")
|
| 622 |
-
frames_dir = os.path.join(v_base, "frames")
|
| 623 |
-
swapped_dir = os.path.join(v_base, "swapped")
|
| 624 |
-
dst_v = os.path.join(v_base, "dst.mp4")
|
| 625 |
-
tmp_v = os.path.join(v_base, "tmp.mp4")
|
| 626 |
-
final_v = os.path.join(v_base, "out.mp4")
|
| 627 |
-
ensure_dirs(frames_dir, swapped_dir)
|
| 628 |
-
|
| 629 |
-
try:
|
| 630 |
-
video_path = _resolve_video_path(video)
|
| 631 |
-
shutil.copy(video_path, dst_v)
|
| 632 |
-
except Exception as e:
|
| 633 |
-
log += f"Video {i}: cannot resolve path: {e}\n"
|
| 634 |
-
results.append(None)
|
| 635 |
-
continue
|
| 636 |
-
|
| 637 |
-
frame_paths = extract_frames(dst_v, frames_dir)
|
| 638 |
-
total = len(frame_paths)
|
| 639 |
-
log += f"Video {i}: {total} frames\n"
|
| 640 |
-
progress(i / max(1, n_videos), desc=f"Video {i+1}/{n_videos}")
|
| 641 |
-
|
| 642 |
-
for idx, fp in enumerate(frame_paths):
|
| 643 |
-
out_p = os.path.join(swapped_dir, f"swapped_{idx:05d}.jpg")
|
| 644 |
-
try:
|
| 645 |
-
swapped = swapper.swap_faces(src_path, 1, fp, dst_idx)
|
| 646 |
-
cv2.imwrite(out_p, swapped)
|
| 647 |
-
except Exception:
|
| 648 |
-
shutil.copy(fp, out_p)
|
| 649 |
-
|
| 650 |
-
cap = cv2.VideoCapture(dst_v)
|
| 651 |
-
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
| 652 |
-
cap.release()
|
| 653 |
-
frames_to_video(swapped_dir, tmp_v, fps)
|
| 654 |
-
|
| 655 |
-
if add_audio:
|
| 656 |
-
ok, _ = add_audio_to_video(dst_v, tmp_v, final_v)
|
| 657 |
-
results.append(final_v if ok else tmp_v)
|
| 658 |
-
else:
|
| 659 |
-
results.append(tmp_v)
|
| 660 |
-
|
| 661 |
-
if delete_frames_dir:
|
| 662 |
-
safe_rmtree(frames_dir)
|
| 663 |
-
safe_rmtree(swapped_dir)
|
| 664 |
-
|
| 665 |
-
log += f"All videos done in {time.time()-start:.1f}s\n"
|
| 666 |
-
return results, log
|
| 667 |
-
|
| 668 |
-
except Exception as e:
|
| 669 |
-
log += f"FATAL: {e}\n"
|
| 670 |
-
return results, log
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
# ------------------------------------------------------------------
|
| 674 |
-
# Gradio UI
|
| 675 |
-
# ------------------------------------------------------------------
|
| 676 |
-
welcome = """
|
| 677 |
-
# Face Swapping Suite (GPU-ready)
|
| 678 |
-
All-in-one face swapping for photos **and videos**.
|
| 679 |
-
Optimised for rented GPUs (CUDA). First run downloads models (~300 MB).
|
| 680 |
-
"""
|
| 681 |
-
|
| 682 |
-
with gr.Blocks(title="FaceSwap GPU") as demo:
|
| 683 |
-
gr.Markdown(welcome)
|
| 684 |
|
| 685 |
with gr.Tab("Single Photo"):
|
| 686 |
-
gr.
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
gr.
|
| 690 |
-
|
| 691 |
-
gr.Image(
|
| 692 |
-
gr.Number(value=1, label="
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
)
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
with gr.Tab("Multi Src → Single Dst"):
|
| 717 |
-
gr.Interface(
|
| 718 |
-
fn=swap_multi_src_single_dst,
|
| 719 |
-
inputs=[
|
| 720 |
-
gr.Gallery(label="Source Images", columns=3, type="numpy"),
|
| 721 |
-
gr.Image(label="Destination Image", type="numpy"),
|
| 722 |
-
gr.Number(value=1, label="Destination Face Index"),
|
| 723 |
-
],
|
| 724 |
-
outputs=[
|
| 725 |
-
gr.Gallery(label="Results"),
|
| 726 |
-
gr.Textbox(label="Log", lines=6, interactive=False)
|
| 727 |
-
],
|
| 728 |
-
api_name="multi_src_single_dst"
|
| 729 |
-
)
|
| 730 |
-
|
| 731 |
-
with gr.Tab("Multi Src → Multi Dst"):
|
| 732 |
-
gr.Interface(
|
| 733 |
-
fn=swap_multi_src_multi_dst,
|
| 734 |
-
inputs=[
|
| 735 |
-
gr.Gallery(label="Source Images", columns=3, type="numpy"),
|
| 736 |
-
gr.Gallery(label="Destination Images", columns=3, type="numpy"),
|
| 737 |
-
gr.Textbox(label="Dst face indices (comma-sep)", value="1"),
|
| 738 |
-
],
|
| 739 |
-
outputs=[
|
| 740 |
-
gr.Gallery(label="Results"),
|
| 741 |
-
gr.Textbox(label="Log", lines=6, interactive=False)
|
| 742 |
-
],
|
| 743 |
-
api_name="multi_src_multi_dst"
|
| 744 |
-
)
|
| 745 |
-
|
| 746 |
-
with gr.Tab("Custom Face Mapping (Photo)"):
|
| 747 |
-
gr.Interface(
|
| 748 |
-
fn=swap_faces_custom,
|
| 749 |
-
inputs=[
|
| 750 |
-
gr.Gallery(label="Source Images (order = indices)", columns=3, type="numpy"),
|
| 751 |
-
gr.Image(label="Destination Image", type="numpy"),
|
| 752 |
-
gr.Textbox(label="Mapping (e.g. 2,1,3 means face1←src2, face2←src1 …)", value="1"),
|
| 753 |
-
],
|
| 754 |
-
outputs=[
|
| 755 |
-
gr.Image(label="Result"),
|
| 756 |
-
gr.Textbox(label="Log", lines=8, interactive=False)
|
| 757 |
-
],
|
| 758 |
-
api_name="custom_photo"
|
| 759 |
-
)
|
| 760 |
-
|
| 761 |
-
with gr.Tab("Video Swapping (single face)"):
|
| 762 |
-
gr.Interface(
|
| 763 |
-
fn=swap_video,
|
| 764 |
-
inputs=[
|
| 765 |
-
gr.Image(label="Source Face Image", type="numpy"),
|
| 766 |
-
gr.Number(value=1, label="Source Face Index"),
|
| 767 |
-
gr.Video(label="Target Video"),
|
| 768 |
-
gr.Number(value=1, label="Destination Face Index"),
|
| 769 |
-
gr.Checkbox(label="Delete extracted frames after finish", value=True),
|
| 770 |
-
gr.Checkbox(label="Add original audio", value=True),
|
| 771 |
-
],
|
| 772 |
-
outputs=[
|
| 773 |
-
gr.Video(label="Swapped Video"),
|
| 774 |
-
gr.Textbox(label="Log", lines=10, interactive=False)
|
| 775 |
-
],
|
| 776 |
-
api_name="video_single"
|
| 777 |
-
)
|
| 778 |
-
|
| 779 |
-
with gr.Tab("Video – All Faces"):
|
| 780 |
-
gr.Interface(
|
| 781 |
-
fn=swap_video_all_faces,
|
| 782 |
-
inputs=[
|
| 783 |
-
gr.Image(label="Source Face Image", type="numpy"),
|
| 784 |
-
gr.Video(label="Target Video"),
|
| 785 |
-
gr.Number(value=1, label="How many faces to swap per frame", precision=0),
|
| 786 |
-
gr.Checkbox(label="Delete extracted frames after finish", value=True),
|
| 787 |
-
gr.Checkbox(label="Add original audio", value=True),
|
| 788 |
-
],
|
| 789 |
-
outputs=[
|
| 790 |
-
gr.Video(label="Swapped Video"),
|
| 791 |
-
gr.Textbox(label="Log", lines=10, interactive=False)
|
| 792 |
-
],
|
| 793 |
-
api_name="video_all_faces"
|
| 794 |
-
)
|
| 795 |
-
|
| 796 |
-
with gr.Tab("Video – Custom Mapping"):
|
| 797 |
-
gr.Interface(
|
| 798 |
-
fn=swap_video_custom_mapping,
|
| 799 |
-
inputs=[
|
| 800 |
-
gr.Gallery(label="Source Images (order = indices)", columns=3, type="numpy"),
|
| 801 |
-
gr.Video(label="Target Video"),
|
| 802 |
-
gr.Textbox(label="Mapping (e.g. 2,1,3)", value="1"),
|
| 803 |
-
gr.Checkbox(label="Delete extracted frames after finish", value=True),
|
| 804 |
-
gr.Checkbox(label="Add original audio", value=True),
|
| 805 |
-
],
|
| 806 |
-
outputs=[
|
| 807 |
-
gr.Video(label="Swapped Video"),
|
| 808 |
-
gr.Textbox(label="Log", lines=10, interactive=False)
|
| 809 |
-
],
|
| 810 |
-
api_name="video_custom"
|
| 811 |
-
)
|
| 812 |
-
|
| 813 |
-
with gr.Tab("Single Src → Multi Video"):
|
| 814 |
-
gr.Interface(
|
| 815 |
-
fn=swap_single_src_multi_video,
|
| 816 |
-
inputs=[
|
| 817 |
-
gr.Image(label="Source Face Image", type="numpy"),
|
| 818 |
-
gr.File(label="Target Videos (multi-select)", file_count="multiple", type="filepath"),
|
| 819 |
-
gr.Textbox(label="Dst face indices per video (comma-sep)", value="1"),
|
| 820 |
-
gr.Checkbox(label="Delete extracted frames after each video", value=True),
|
| 821 |
-
gr.Checkbox(label="Add original audio", value=True),
|
| 822 |
-
],
|
| 823 |
-
outputs=[
|
| 824 |
-
gr.Gallery(label="Swapped Videos", type="filepath"),
|
| 825 |
-
gr.Textbox(label="Log", lines=10, interactive=False)
|
| 826 |
-
],
|
| 827 |
-
api_name="single_src_multi_video"
|
| 828 |
)
|
| 829 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
|
| 831 |
if __name__ == "__main__":
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
parser.add_argument("--server-port", type=int, default=7860)
|
| 836 |
-
args = parser.parse_args()
|
| 837 |
-
demo.queue(max_size=4).launch(
|
| 838 |
-
share=args.share,
|
| 839 |
-
server_name=args.server_name,
|
| 840 |
-
server_port=args.server_port,
|
| 841 |
show_error=True,
|
| 842 |
-
ssr_mode=False, # prevents the SvelteKit 405 / Content-Length errors
|
| 843 |
)
|
|
|
|
| 1 |
+
|
| 2 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import time
|
| 4 |
+
import tempfile
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
import gradio as gr
|
| 11 |
|
| 12 |
+
from optimized_swapper import FaceSwapEngine
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
ENGINE = FaceSwapEngine()
|
| 15 |
|
| 16 |
+
def _as_index(v, default=1):
|
| 17 |
try:
|
| 18 |
+
return max(1, int(v))
|
|
|
|
| 19 |
except Exception:
|
| 20 |
+
return default
|
| 21 |
+
|
| 22 |
+
def swap_photo(source, source_idx, target, target_idx, det_size):
|
| 23 |
+
if source is None or target is None:
|
| 24 |
+
raise gr.Error("Upload both source and target images.")
|
| 25 |
+
result = ENGINE.swap_image(
|
| 26 |
+
source, _as_index(source_idx), target, _as_index(target_idx),
|
| 27 |
+
det_size=int(det_size)
|
| 28 |
+
)
|
| 29 |
+
return result
|
| 30 |
+
|
| 31 |
+
def swap_video(source, source_idx, video_path, target_idx, det_size, detection_interval, jpeg_quality, audio):
|
| 32 |
+
if source is None:
|
| 33 |
+
raise gr.Error("Upload a source image.")
|
| 34 |
+
if not video_path:
|
| 35 |
+
raise gr.Error("Upload a target video.")
|
| 36 |
+
|
| 37 |
+
out = ENGINE.swap_video(
|
| 38 |
+
source=source,
|
| 39 |
+
source_idx=_as_index(source_idx),
|
| 40 |
+
video_path=video_path,
|
| 41 |
+
target_idx=_as_index(target_idx),
|
| 42 |
+
det_size=int(det_size),
|
| 43 |
+
detection_interval=max(1, int(detection_interval)),
|
| 44 |
+
jpeg_quality=int(jpeg_quality),
|
| 45 |
+
preserve_audio=bool(audio),
|
| 46 |
+
)
|
| 47 |
+
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
def swap_multi_source_single(source_files, target, target_idx, det_size):
|
| 50 |
+
if not source_files or target is None:
|
| 51 |
+
raise gr.Error("Upload source images and a target image.")
|
| 52 |
results = []
|
| 53 |
+
for item in source_files:
|
| 54 |
+
path = item if isinstance(item, str) else item[0]
|
| 55 |
+
img = cv2.imread(path)
|
| 56 |
+
if img is None:
|
| 57 |
+
continue
|
| 58 |
+
results.append(ENGINE.swap_image(
|
| 59 |
+
img, 1, target, _as_index(target_idx), int(det_size)
|
| 60 |
+
))
|
| 61 |
+
return results
|
| 62 |
+
|
| 63 |
+
def swap_multi_source_multi(source_files, target_files, target_indices, det_size):
|
| 64 |
+
if not source_files or not target_files:
|
| 65 |
+
raise gr.Error("Upload source and target images.")
|
| 66 |
+
indices = [x.strip() for x in str(target_indices).split(",") if x.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
results = []
|
| 68 |
+
targets = []
|
| 69 |
+
for item in target_files:
|
| 70 |
+
path = item if isinstance(item, str) else item[0]
|
| 71 |
+
img = cv2.imread(path)
|
| 72 |
+
if img is not None:
|
| 73 |
+
targets.append(img)
|
| 74 |
+
|
| 75 |
+
for src_item in source_files:
|
| 76 |
+
src_path = src_item if isinstance(src_item, str) else src_item[0]
|
| 77 |
+
src = cv2.imread(src_path)
|
| 78 |
+
if src is None:
|
| 79 |
+
continue
|
| 80 |
+
# Cache the source face once for this source image.
|
| 81 |
+
ENGINE.prepare_source(src, 1, int(det_size))
|
| 82 |
+
for j, dst in enumerate(targets):
|
| 83 |
+
idx = _as_index(indices[j] if j < len(indices) else 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
try:
|
| 85 |
+
results.append(ENGINE.swap_prepared_source(dst, idx, int(det_size)))
|
|
|
|
|
|
|
| 86 |
except Exception as e:
|
| 87 |
+
results.append(f"Error: {e}")
|
| 88 |
+
return results
|
| 89 |
+
|
| 90 |
+
with gr.Blocks(title="Fast Face Swap") as demo:
|
| 91 |
+
gr.Markdown(
|
| 92 |
+
"# Fast Face Swapping Suite\n"
|
| 93 |
+
"CUDA/ONNX Runtime optimized photo and video face swapping. "
|
| 94 |
+
"The source face is detected once and video frames stay in memory."
|
| 95 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
with gr.Tab("Single Photo"):
|
| 98 |
+
with gr.Row():
|
| 99 |
+
with gr.Column():
|
| 100 |
+
src = gr.Image(type="numpy", label="Source Image")
|
| 101 |
+
src_idx = gr.Number(value=1, precision=0, label="Source Face Index")
|
| 102 |
+
with gr.Column():
|
| 103 |
+
dst = gr.Image(type="numpy", label="Target Image")
|
| 104 |
+
dst_idx = gr.Number(value=1, precision=0, label="Target Face Index")
|
| 105 |
+
det = gr.Dropdown([256, 320, 384, 512], value=320, label="Detector Size")
|
| 106 |
+
btn = gr.Button("Swap", variant="primary")
|
| 107 |
+
out = gr.Image(type="numpy", label="Result")
|
| 108 |
+
btn.click(swap_photo, [src, src_idx, dst, dst_idx, det], out)
|
| 109 |
+
|
| 110 |
+
with gr.Tab("Fast Video"):
|
| 111 |
+
vsrc = gr.Image(type="numpy", label="Source Image")
|
| 112 |
+
with gr.Row():
|
| 113 |
+
vid = gr.Video(label="Target Video", type="filepath")
|
| 114 |
+
vdst_idx = gr.Number(value=1, precision=0, label="Target Face Index")
|
| 115 |
+
with gr.Row():
|
| 116 |
+
vdet = gr.Dropdown([256, 320, 384, 512], value=320, label="Detector Size")
|
| 117 |
+
interval = gr.Slider(1, 5, value=1, step=1,
|
| 118 |
+
label="Face detection interval (1 = best tracking accuracy)")
|
| 119 |
+
quality = gr.Slider(75, 98, value=92, step=1, label="JPEG fallback quality")
|
| 120 |
+
audio = gr.Checkbox(value=True, label="Preserve original audio")
|
| 121 |
+
vbtn = gr.Button("Swap Video", variant="primary")
|
| 122 |
+
vout = gr.Video(label="Output Video")
|
| 123 |
+
vbtn.click(
|
| 124 |
+
swap_video,
|
| 125 |
+
[vsrc, gr.Number(value=1, visible=False), vid, vdst_idx,
|
| 126 |
+
vdet, interval, quality, audio],
|
| 127 |
+
vout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
)
|
| 129 |
|
| 130 |
+
with gr.Tab("Multi Source → Single Target"):
|
| 131 |
+
ms = gr.File(file_count="multiple", file_types=["image"], label="Source Images")
|
| 132 |
+
md = gr.Image(type="numpy", label="Target Image")
|
| 133 |
+
mi = gr.Number(value=1, precision=0, label="Target Face Index")
|
| 134 |
+
mb = gr.Button("Process", variant="primary")
|
| 135 |
+
mo = gr.Gallery(label="Results", columns=3)
|
| 136 |
+
mb.click(swap_multi_source_single, [ms, md, mi, det], mo)
|
| 137 |
+
|
| 138 |
+
with gr.Tab("Multi Source → Multi Target"):
|
| 139 |
+
mss = gr.File(file_count="multiple", file_types=["image"], label="Source Images")
|
| 140 |
+
mdd = gr.File(file_count="multiple", file_types=["image"], label="Target Images")
|
| 141 |
+
mids = gr.Textbox(value="1", label="Target face indices, comma-separated")
|
| 142 |
+
mdb = gr.Button("Process", variant="primary")
|
| 143 |
+
mdo = gr.Gallery(label="Results", columns=3)
|
| 144 |
+
mdb.click(swap_multi_source_multi, [mss, mdd, mids, det], mdo)
|
| 145 |
|
| 146 |
if __name__ == "__main__":
|
| 147 |
+
demo.queue(default_concurrency_limit=1, max_size=8).launch(
|
| 148 |
+
server_name="0.0.0.0",
|
| 149 |
+
server_port=int(os.getenv("PORT", "7860")),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
show_error=True,
|
|
|
|
| 151 |
)
|
benchmark.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os, time, cv2
|
| 3 |
+
from optimized_swapper import FaceSwapEngine
|
| 4 |
+
|
| 5 |
+
src = cv2.imread("benchmark_source.jpg")
|
| 6 |
+
dst = cv2.imread("benchmark_target.jpg")
|
| 7 |
+
if src is None or dst is None:
|
| 8 |
+
raise SystemExit("Put benchmark_source.jpg and benchmark_target.jpg in the root.")
|
| 9 |
+
|
| 10 |
+
engine = FaceSwapEngine()
|
| 11 |
+
for _ in range(2):
|
| 12 |
+
engine.swap_image(src, 1, dst, 1, 320)
|
| 13 |
+
|
| 14 |
+
n = 20
|
| 15 |
+
t0 = time.perf_counter()
|
| 16 |
+
for _ in range(n):
|
| 17 |
+
engine.swap_prepared_source(dst, 1, 320, 1)
|
| 18 |
+
dt = time.perf_counter() - t0
|
| 19 |
+
print(f"{n} swaps: {dt:.3f}s")
|
| 20 |
+
print(f"Average: {dt/n*1000:.2f} ms/image")
|
| 21 |
+
print(f"Throughput: {n/dt:.2f} images/sec")
|
download_model.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download the large InsightFace swap model when it is not already present."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from huggingface_hub import hf_hub_download
|
| 4 |
+
|
| 5 |
+
TARGET = Path("inswapper_128.onnx")
|
| 6 |
+
REPO_ID = "dosesnrolls1/FaceSwapAll"
|
| 7 |
+
FILENAME = "inswapper_128.onnx"
|
| 8 |
+
|
| 9 |
+
if TARGET.exists() and TARGET.stat().st_size > 100_000_000:
|
| 10 |
+
print(f"Model already present: {TARGET}")
|
| 11 |
+
else:
|
| 12 |
+
downloaded = hf_hub_download(
|
| 13 |
+
repo_id=REPO_ID,
|
| 14 |
+
filename=FILENAME,
|
| 15 |
+
repo_type="space",
|
| 16 |
+
local_dir=".",
|
| 17 |
+
local_dir_use_symlinks=False,
|
| 18 |
+
)
|
| 19 |
+
print(f"Downloaded: {downloaded}")
|
optimized_swapper.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
import tempfile
|
| 5 |
+
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
import insightface
|
| 11 |
+
from insightface.app import FaceAnalysis
|
| 12 |
+
|
| 13 |
+
MODEL_PATH = os.getenv("INSWAPPER_MODEL", "inswapper_128.onnx")
|
| 14 |
+
GPU = os.getenv("FACE_SWAP_GPU", "1") not in {"0", "false", "False", "cpu"}
|
| 15 |
+
CUDA_DEVICE = int(os.getenv("CUDA_DEVICE_ID", "0"))
|
| 16 |
+
|
| 17 |
+
class FaceSwapEngine:
|
| 18 |
+
"""Performance-focused InsightFace engine.
|
| 19 |
+
|
| 20 |
+
Key optimizations:
|
| 21 |
+
* one persistent FaceAnalysis model
|
| 22 |
+
* one persistent inswapper model
|
| 23 |
+
* source face is cached and reused
|
| 24 |
+
* NumPy/OpenCV frames stay in memory
|
| 25 |
+
* smaller detector size by default
|
| 26 |
+
* target face detection can be throttled for video
|
| 27 |
+
* final video is encoded once by ffmpeg
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self):
|
| 31 |
+
self._source_face = None
|
| 32 |
+
self._source_key = None
|
| 33 |
+
self._source_idx = None
|
| 34 |
+
self._det_size = None
|
| 35 |
+
|
| 36 |
+
providers = []
|
| 37 |
+
if GPU:
|
| 38 |
+
providers.append((
|
| 39 |
+
"CUDAExecutionProvider",
|
| 40 |
+
{
|
| 41 |
+
"device_id": CUDA_DEVICE,
|
| 42 |
+
"arena_extend_strategy": "kSameAsRequested",
|
| 43 |
+
"cudnn_conv_algo_search": "EXHAUSTIVE",
|
| 44 |
+
"do_copy_in_default_stream": True,
|
| 45 |
+
},
|
| 46 |
+
))
|
| 47 |
+
providers.append("CPUExecutionProvider")
|
| 48 |
+
|
| 49 |
+
# Keep detector/model objects alive for the whole Space lifetime.
|
| 50 |
+
self.app = FaceAnalysis(
|
| 51 |
+
name=os.getenv("INSIGHTFACE_MODEL", "buffalo_l"),
|
| 52 |
+
providers=providers,
|
| 53 |
+
)
|
| 54 |
+
self.app.prepare(
|
| 55 |
+
ctx_id=CUDA_DEVICE if GPU else -1,
|
| 56 |
+
det_size=(320, 320),
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
self.swapper = insightface.model_zoo.get_model(
|
| 60 |
+
MODEL_PATH,
|
| 61 |
+
providers=providers,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
self.providers = providers
|
| 65 |
+
self.gpu = GPU
|
| 66 |
+
|
| 67 |
+
@staticmethod
|
| 68 |
+
def _img_key(img):
|
| 69 |
+
# Avoid copying the complete image just to build a cache key.
|
| 70 |
+
return (id(img), img.shape, img.dtype.str)
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def _sort_faces(faces):
|
| 74 |
+
return sorted(faces, key=lambda f: float(f.bbox[0]))
|
| 75 |
+
|
| 76 |
+
def _detect(self, image, det_size=320, max_num=0):
|
| 77 |
+
# InsightFace's FaceAnalysis allows max_num through get().
|
| 78 |
+
if self._det_size != det_size:
|
| 79 |
+
self.app.prepare(
|
| 80 |
+
ctx_id=CUDA_DEVICE if self.gpu else -1,
|
| 81 |
+
det_size=(det_size, det_size),
|
| 82 |
+
)
|
| 83 |
+
self._det_size = det_size
|
| 84 |
+
try:
|
| 85 |
+
return self._sort_faces(self.app.get(image, max_num=max_num))
|
| 86 |
+
except TypeError:
|
| 87 |
+
return self._sort_faces(self.app.get(image))
|
| 88 |
+
|
| 89 |
+
def prepare_source(self, source, source_idx=1, det_size=320):
|
| 90 |
+
faces = self._detect(source, det_size, max_num=0)
|
| 91 |
+
if len(faces) < source_idx:
|
| 92 |
+
raise ValueError(
|
| 93 |
+
f"Source image contains {len(faces)} faces; requested face {source_idx}."
|
| 94 |
+
)
|
| 95 |
+
self._source_face = faces[source_idx - 1]
|
| 96 |
+
self._source_idx = source_idx
|
| 97 |
+
self._source_key = self._img_key(source)
|
| 98 |
+
return self._source_face
|
| 99 |
+
|
| 100 |
+
def swap_prepared_source(self, target, target_idx=1, det_size=320, max_faces=0):
|
| 101 |
+
if self._source_face is None:
|
| 102 |
+
raise RuntimeError("Source face has not been prepared.")
|
| 103 |
+
faces = self._detect(target, det_size, max_num=max_faces)
|
| 104 |
+
if len(faces) < target_idx:
|
| 105 |
+
raise ValueError(
|
| 106 |
+
f"Target image contains {len(faces)} faces; requested face {target_idx}."
|
| 107 |
+
)
|
| 108 |
+
return self.swapper.get(
|
| 109 |
+
target,
|
| 110 |
+
faces[target_idx - 1],
|
| 111 |
+
self._source_face,
|
| 112 |
+
paste_back=True,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def swap_image(self, source, source_idx, target, target_idx, det_size=320):
|
| 116 |
+
self.prepare_source(source, source_idx, det_size)
|
| 117 |
+
# If target index is 1, detector can stop after first face.
|
| 118 |
+
max_faces = target_idx if target_idx > 1 else 1
|
| 119 |
+
return self.swap_prepared_source(target, target_idx, det_size, max_faces)
|
| 120 |
+
|
| 121 |
+
def _detect_video_face(self, frame, target_idx, det_size):
|
| 122 |
+
max_faces = target_idx if target_idx > 1 else 1
|
| 123 |
+
faces = self._detect(frame, det_size, max_num=max_faces)
|
| 124 |
+
if len(faces) < target_idx:
|
| 125 |
+
raise ValueError("Target face not found.")
|
| 126 |
+
return faces[target_idx - 1]
|
| 127 |
+
|
| 128 |
+
def _ffmpeg_encode(self, frames_dir, output_path, fps, audio_source=None):
|
| 129 |
+
pattern = str(Path(frames_dir) / "frame_%08d.jpg")
|
| 130 |
+
cmd = [
|
| 131 |
+
"ffmpeg", "-y",
|
| 132 |
+
"-loglevel", "error",
|
| 133 |
+
"-framerate", f"{fps:.6f}",
|
| 134 |
+
"-i", pattern,
|
| 135 |
+
]
|
| 136 |
+
if audio_source:
|
| 137 |
+
cmd += [
|
| 138 |
+
"-i", audio_source,
|
| 139 |
+
"-map", "0:v:0", "-map", "1:a?",
|
| 140 |
+
"-c:v", "libx264", "-preset", "veryfast",
|
| 141 |
+
"-crf", "18", "-pix_fmt", "yuv420p",
|
| 142 |
+
"-c:a", "aac", "-b:a", "160k",
|
| 143 |
+
"-shortest",
|
| 144 |
+
]
|
| 145 |
+
else:
|
| 146 |
+
cmd += [
|
| 147 |
+
"-c:v", "libx264", "-preset", "veryfast",
|
| 148 |
+
"-crf", "18", "-pix_fmt", "yuv420p",
|
| 149 |
+
]
|
| 150 |
+
cmd.append(output_path)
|
| 151 |
+
subprocess.run(cmd, check=True)
|
| 152 |
+
|
| 153 |
+
def swap_video(
|
| 154 |
+
self, source, source_idx, video_path, target_idx,
|
| 155 |
+
det_size=320, detection_interval=1, jpeg_quality=92,
|
| 156 |
+
preserve_audio=True
|
| 157 |
+
):
|
| 158 |
+
self.prepare_source(source, source_idx, det_size)
|
| 159 |
+
|
| 160 |
+
work = tempfile.mkdtemp(prefix="faceswap_")
|
| 161 |
+
frames_dir = os.path.join(work, "frames")
|
| 162 |
+
os.makedirs(frames_dir, exist_ok=True)
|
| 163 |
+
output = os.path.join(work, "swapped.mp4")
|
| 164 |
+
|
| 165 |
+
cap = cv2.VideoCapture(video_path)
|
| 166 |
+
if not cap.isOpened():
|
| 167 |
+
raise ValueError("Could not open video.")
|
| 168 |
+
|
| 169 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
| 170 |
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
| 171 |
+
|
| 172 |
+
# We encode final frames from JPEGs only. Unlike the old implementation,
|
| 173 |
+
# source/target frames are never written and then read back before swapping.
|
| 174 |
+
idx = 0
|
| 175 |
+
last_face = None
|
| 176 |
+
failed = 0
|
| 177 |
+
t0 = time.perf_counter()
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
while True:
|
| 181 |
+
ok, frame = cap.read()
|
| 182 |
+
if not ok:
|
| 183 |
+
break
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
if last_face is None or idx % detection_interval == 0:
|
| 187 |
+
last_face = self._detect_video_face(
|
| 188 |
+
frame, target_idx, det_size
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
swapped = self.swapper.get(
|
| 192 |
+
frame, last_face, self._source_face, paste_back=True
|
| 193 |
+
)
|
| 194 |
+
except Exception:
|
| 195 |
+
# If a cached face becomes invalid, immediately re-detect.
|
| 196 |
+
try:
|
| 197 |
+
last_face = self._detect_video_face(
|
| 198 |
+
frame, target_idx, det_size
|
| 199 |
+
)
|
| 200 |
+
swapped = self.swapper.get(
|
| 201 |
+
frame, last_face, self._source_face, paste_back=True
|
| 202 |
+
)
|
| 203 |
+
except Exception:
|
| 204 |
+
swapped = frame
|
| 205 |
+
failed += 1
|
| 206 |
+
|
| 207 |
+
path = os.path.join(frames_dir, f"frame_{idx:08d}.jpg")
|
| 208 |
+
cv2.imwrite(
|
| 209 |
+
path,
|
| 210 |
+
swapped,
|
| 211 |
+
[int(cv2.IMWRITE_JPEG_QUALITY), int(jpeg_quality)],
|
| 212 |
+
)
|
| 213 |
+
idx += 1
|
| 214 |
+
|
| 215 |
+
cap.release()
|
| 216 |
+
self._ffmpeg_encode(
|
| 217 |
+
frames_dir,
|
| 218 |
+
output,
|
| 219 |
+
fps,
|
| 220 |
+
audio_source=video_path if preserve_audio else None,
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# Copy result to a stable temp path that Gradio can serve after
|
| 224 |
+
# the worker returns.
|
| 225 |
+
stable = tempfile.NamedTemporaryFile(
|
| 226 |
+
prefix="faceswap_result_", suffix=".mp4", delete=False
|
| 227 |
+
).name
|
| 228 |
+
with open(output, "rb") as srcf, open(stable, "wb") as dstf:
|
| 229 |
+
while True:
|
| 230 |
+
chunk = srcf.read(8 * 1024 * 1024)
|
| 231 |
+
if not chunk:
|
| 232 |
+
break
|
| 233 |
+
dstf.write(chunk)
|
| 234 |
+
|
| 235 |
+
elapsed = time.perf_counter() - t0
|
| 236 |
+
print(
|
| 237 |
+
f"[FaceSwap] processed {idx} frames in {elapsed:.2f}s "
|
| 238 |
+
f"({idx / max(elapsed, 1e-6):.2f} FPS), failures={failed}"
|
| 239 |
+
)
|
| 240 |
+
return stable
|
| 241 |
+
finally:
|
| 242 |
+
import shutil
|
| 243 |
+
shutil.rmtree(work, ignore_errors=True)
|
packages.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
ffmpeg
|
| 2 |
libgl1
|
| 3 |
libglib2.0-0
|
|
|
|
| 1 |
+
|
| 2 |
ffmpeg
|
| 3 |
libgl1
|
| 4 |
libglib2.0-0
|
requirements.txt
CHANGED
|
@@ -1,8 +1,15 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
tqdm
|
| 7 |
-
insightface
|
| 8 |
-
gradio>=4.0.0
|
|
|
|
| 1 |
+
|
| 2 |
+
# Core UI / image processing
|
| 3 |
+
gradio>=5.44,<6
|
| 4 |
+
numpy<2
|
| 5 |
+
opencv-python-headless>=4.10
|
| 6 |
+
pillow>=10
|
| 7 |
+
|
| 8 |
+
# Face analysis / ONNX Runtime
|
| 9 |
+
insightface==0.7.3
|
| 10 |
+
onnx>=1.16,<2
|
| 11 |
+
onnxruntime-gpu>=1.20,<1.24
|
| 12 |
+
|
| 13 |
+
# Utilities
|
| 14 |
+
psutil
|
| 15 |
tqdm
|
|
|
|
|
|