dosesnrolls1 commited on
Commit
8100963
·
verified ·
1 Parent(s): 7947df5

Upload 8 files

Browse files
Files changed (8) hide show
  1. README.md +27 -49
  2. VideoSwapping.py +8 -77
  3. app.py +130 -822
  4. benchmark.py +21 -0
  5. download_model.py +19 -0
  6. optimized_swapper.py +243 -0
  7. packages.txt +1 -0
  8. requirements.txt +14 -7
README.md CHANGED
@@ -1,57 +1,35 @@
1
- ---
2
- title: FaceSwap GPU
3
- emoji: 🎬
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.29.0
8
- app_file: app.py
9
- pinned: false
10
- license: unknown
11
- short_description: GPU-accelerated all-in-one face swapping (photo + video)
12
- ---
13
-
14
- # FaceSwap GPU – Rented-GPU Ready
15
-
16
- All-in-one face swapping suite optimised for **CUDA GPUs** (RunPod, Vast.ai, Lambda, local RTX, HF GPU Spaces, etc.).
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
- pip install -r requirements.txt
41
- python app.py --share # creates a public link
42
- # or
43
- python app.py --server-port 7860
44
  ```
45
 
46
- First run downloads `buffalo_l` + `inswapper_128.onnx` (~300 MB).
47
 
48
- ## Tips for long videos
 
49
 
50
- - Keep “Delete extracted frames” checked to save disk.
51
- - Processing is frame-by-frame; a 1-minute 30 fps clip 1800 swaps.
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
- ## Face indices
 
 
 
56
 
57
- Faces are sorted left-to-right. Index 1 = leftmost face.
 
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
- def extract_frames(video_path, frames_dir, max_frames=None):
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
- existing = sorted(
13
- [f for f in os.listdir(frames_dir)
14
- if f.startswith("frame_") and f.endswith(".jpg")]
 
 
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
- import gradio as gr
2
  import os
3
- import cv2
4
- import numpy as np
5
- import shutil
6
- import subprocess
7
  import time
8
- from SinglePhoto import FaceSwapper
9
- import argparse
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 safe_rmtree(path):
43
  try:
44
- if os.path.exists(path):
45
- shutil.rmtree(path)
46
  except Exception:
47
- pass
48
-
49
-
50
- def ensure_dirs(*paths):
51
- for p in paths:
52
- os.makedirs(p, exist_ok=True)
53
-
54
-
55
- def _resolve_video_path(video):
56
- """Turn whatever Gradio gives us into a real file path."""
57
- if video is None:
58
- raise ValueError("No video provided")
59
- if isinstance(video, str) and os.path.exists(video):
60
- return video
61
- if hasattr(video, "name") and os.path.exists(getattr(video, "name", "")):
62
- return video.name
63
- if isinstance(video, dict) and "name" in video and os.path.exists(video["name"]):
64
- return video["name"]
65
- if isinstance(video, (list, tuple)) and len(video) > 0:
66
- return _resolve_video_path(video[0])
67
- raise ValueError(f"Could not resolve video path from type {type(video)}: {video}")
68
-
69
-
70
- # ------------------------------------------------------------------
71
- # Photo functions
72
- # ------------------------------------------------------------------
73
- def swap_single_photo(src_img, src_idx, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)):
74
- log = ""
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 swap_multi_src_single_dst(src_imgs, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)):
152
- log = ""
 
153
  results = []
154
- base = "workdir/MultiSrcSingleDst"
155
- src_dir = f"{base}/src"
156
- dst_dir = f"{base}/dst"
157
- out_dir = f"{base}/output"
158
- ensure_dirs(src_dir, dst_dir, out_dir)
159
-
160
- try:
161
- if isinstance(dst_img, tuple):
162
- dst_img = dst_img[0]
163
- dst_path = os.path.join(dst_dir, "data_dst.jpg")
164
- cv2.imwrite(dst_path, cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR))
165
- log += "Saved destination\n"
166
-
167
- total = max(1, len(src_imgs))
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
- base = "workdir/MultiSrcMultiDst"
196
- src_dir = f"{base}/src"
197
- dst_dir = f"{base}/dst"
198
- out_dir = f"{base}/output"
199
- ensure_dirs(src_dir, dst_dir, out_dir)
200
-
201
- try:
202
- if isinstance(dst_indices, str):
203
- idx_list = [int(x.strip()) for x in dst_indices.split(",") if x.strip().isdigit()]
204
- else:
205
- idx_list = [int(x) for x in dst_indices]
206
-
207
- total = max(1, len(src_imgs) * len(dst_imgs))
208
- count = 0
209
- for i, src_img in enumerate(src_imgs):
210
- if isinstance(src_img, tuple):
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
- swapped = swapper.swap_faces(src_paths[src_idx-1], 1, temp_dst, face_idx)
281
- cv2.imwrite(temp_dst, swapped)
282
- log += f"Swapped face {face_idx} ← source {src_idx}\n"
283
  except Exception as e:
284
- log += f"Failed face {face_idx}: {e}\n"
285
-
286
- shutil.copy(temp_dst, out_path)
287
- safe_rmtree(temp_dir)
288
- log += f"Elapsed: {time.time()-start:.2f}s\n"
289
- return out_path, log
290
- except Exception as e:
291
- log += f"FATAL: {e}\n"
292
- return None, log
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.Interface(
687
- fn=swap_single_photo,
688
- inputs=[
689
- gr.Image(label="Source Image", type="numpy"),
690
- gr.Number(value=1, label="Source Face Index (1-based)"),
691
- gr.Image(label="Destination Image", type="numpy"),
692
- gr.Number(value=1, label="Destination Face Index (1-based)"),
693
- ],
694
- outputs=[
695
- gr.Image(label="Result"),
696
- gr.Textbox(label="Log", lines=6, interactive=False)
697
- ],
698
- api_name="single_photo"
699
- )
700
-
701
- with gr.Tab("Single Src → Multi Dst"):
702
- gr.Interface(
703
- fn=swap_single_src_multi_dst,
704
- inputs=[
705
- gr.Image(label="Source Image", type="numpy"),
706
- gr.Gallery(label="Destination Images", columns=3, type="numpy"),
707
- gr.Textbox(label="Dst face indices (comma-sep, e.g. 1,1,2)", value="1"),
708
- ],
709
- outputs=[
710
- gr.Gallery(label="Results"),
711
- gr.Textbox(label="Log", lines=6, interactive=False)
712
- ],
713
- api_name="single_src_multi_dst"
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
- parser = argparse.ArgumentParser()
833
- parser.add_argument("--share", action="store_true", help="Create a public Gradio link")
834
- parser.add_argument("--server-name", default="0.0.0.0")
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
- numpy
2
- opencv-python-headless
3
- onnxruntime-gpu==1.20.2
4
- scikit-learn
5
- scikit-image
 
 
 
 
 
 
 
 
 
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