dream2589632147 commited on
Commit
571735f
·
verified ·
1 Parent(s): ff399ff

Upgrade face swap quality with HyperSwap and GFPGAN

Browse files
Files changed (1) hide show
  1. app.py +192 -19
app.py CHANGED
@@ -11,8 +11,8 @@ from pathlib import Path
11
 
12
  import cv2
13
  import gradio as gr
14
- import insightface
15
  import numpy as np
 
16
  import spaces
17
  from huggingface_hub import hf_hub_download
18
  from insightface.app import FaceAnalysis
@@ -25,15 +25,43 @@ MODEL_ROOT = Path(tempfile.gettempdir()) / "insightface"
25
  OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
26
  MODEL_ROOT.mkdir(parents=True, exist_ok=True)
27
 
28
- # The smaller FP16 model cuts startup/download time while preserving the usual
29
- # InSwapper 128 output quality.
30
  SWAPPER_MODEL_PATH = hf_hub_download(
31
- repo_id="ninjawick/webui-faceswap-unlocked",
32
- filename="inswapper_128_fp16.onnx",
 
 
 
 
33
  )
34
 
35
  _MODEL_LOCK = threading.Lock()
36
- _MODEL_CACHE: tuple[FaceAnalysis, object] | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
 
39
  def _cleanup_old_outputs(max_age_seconds: int = 3600) -> None:
@@ -74,6 +102,7 @@ def _estimate_gpu_seconds(
74
  target_face_index: int,
75
  clip_length: int,
76
  output_resolution: str,
 
77
  keep_audio: bool,
78
  add_watermark: bool,
79
  consent: bool,
@@ -86,10 +115,11 @@ def _estimate_gpu_seconds(
86
  seconds = min(_video_metadata(target_video)[3] or seconds, seconds)
87
  except Exception:
88
  pass
89
- return int(max(90, min(300, 45 + seconds * 5.5)))
 
90
 
91
 
92
- def _load_models() -> tuple[FaceAnalysis, object]:
93
  """Create CUDA ONNX sessions only after ZeroGPU has allocated a GPU."""
94
  global _MODEL_CACHE
95
  with _MODEL_LOCK:
@@ -104,14 +134,131 @@ def _load_models() -> tuple[FaceAnalysis, object]:
104
  providers=providers,
105
  )
106
  analyser.prepare(ctx_id=0, det_size=(640, 640))
107
- swapper = insightface.model_zoo.get_model(
108
- SWAPPER_MODEL_PATH,
109
- providers=providers,
 
110
  )
111
- _MODEL_CACHE = (analyser, swapper)
 
 
 
112
  return _MODEL_CACHE
113
 
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  def _resize_to_limit(frame: np.ndarray, resolution: str) -> np.ndarray:
116
  limits = {"720p (fast)": 1280, "1080p": 1920, "Original": None}
117
  limit = limits.get(resolution, 1280)
@@ -217,6 +364,7 @@ def swap_video_face(
217
  target_face_index: int,
218
  clip_length: int,
219
  output_resolution: str,
 
220
  keep_audio: bool,
221
  add_watermark: bool,
222
  consent: bool,
@@ -235,7 +383,7 @@ def swap_video_face(
235
  requested_seconds = min(requested_seconds, duration)
236
  frame_limit = max(1, int(round(requested_seconds * fps)))
237
 
238
- analyser, swapper = _load_models()
239
  source_frame = cv2.imread(source_image)
240
  if source_frame is None:
241
  raise gr.Error("The source image format is unsupported.")
@@ -244,6 +392,7 @@ def swap_video_face(
244
  if not source_faces:
245
  raise gr.Error("No face was detected in the source image. Use a clear, front-facing portrait.")
246
  source_face = _largest_face(source_faces)
 
247
 
248
  job_dir = OUTPUT_ROOT / uuid.uuid4().hex
249
  job_dir.mkdir(parents=True, exist_ok=False)
@@ -253,6 +402,8 @@ def swap_video_face(
253
  cap = cv2.VideoCapture(target_video)
254
  writer: cv2.VideoWriter | None = None
255
  tracked_embedding: np.ndarray | None = None
 
 
256
  swapped_frames = 0
257
  processed_frames = 0
258
  face_index = max(1, int(target_face_index))
@@ -288,8 +439,23 @@ def swap_video_face(
288
  target_face = _select_tracked_face(faces, tracked_embedding)
289
 
290
  if target_face is not None:
291
- frame = swapper.get(frame, target_face, source_face, paste_back=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  swapped_frames += 1
 
 
293
 
294
  if add_watermark:
295
  frame = _add_ai_watermark(frame)
@@ -329,7 +495,7 @@ def swap_video_face(
329
 
330
  status = (
331
  f"Completed {processed_seconds:.1f}s at {fps:.2f} FPS — "
332
- f"face replaced in {swapped_frames}/{processed_frames} frames."
333
  )
334
  return str(final_path), status
335
 
@@ -346,8 +512,8 @@ with gr.Blocks(title=APP_TITLE, theme=gr.themes.Soft(), css=CSS) as demo:
346
  """
347
  <div class="hero">
348
  <h1>🎭 Dream Video Face Swap</h1>
349
- <p>ZeroGPU face replacement with identity tracking and original-audio preservation.</p>
350
- <p dir="rtl">تبديل الوجه في الفيديو باستخدام ZeroGPU مع تتبع الشخص والاحتفاظ بالصوت الأصلي.</p>
351
  </div>
352
  """
353
  )
@@ -383,9 +549,15 @@ with gr.Blocks(title=APP_TITLE, theme=gr.themes.Soft(), css=CSS) as demo:
383
  )
384
  output_resolution = gr.Radio(
385
  choices=["720p (fast)", "1080p", "Original"],
386
- value="720p (fast)",
387
  label="Output resolution",
388
  )
 
 
 
 
 
 
389
  keep_audio = gr.Checkbox(value=True, label="Keep original audio")
390
  add_watermark = gr.Checkbox(value=True, label="Add ‘AI FACE SWAP’ watermark")
391
  consent = gr.Checkbox(
@@ -401,7 +573,7 @@ with gr.Blocks(title=APP_TITLE, theme=gr.themes.Soft(), css=CSS) as demo:
401
  """
402
  <div class="notice">
403
 
404
- **Tips / نصائح:** Use a sharp, front-facing source portrait. For multiple people, select the target by its left-to-right position in the first clear frame. Start with 720p and a short clip for the fastest ZeroGPU queue.
405
 
406
  استخدم صورة أمامية واضحة للوجه. إذا ظهر أكثر من شخص، اختر رقم الشخص حسب ترتيبه من اليسار إلى اليمين في أول لقطة واضحة.
407
 
@@ -419,6 +591,7 @@ Uploads are processed temporarily and old results are automatically removed. Do
419
  target_face_index,
420
  clip_length,
421
  output_resolution,
 
422
  keep_audio,
423
  add_watermark,
424
  consent,
 
11
 
12
  import cv2
13
  import gradio as gr
 
14
  import numpy as np
15
+ import onnxruntime as ort
16
  import spaces
17
  from huggingface_hub import hf_hub_download
18
  from insightface.app import FaceAnalysis
 
25
  OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
26
  MODEL_ROOT.mkdir(parents=True, exist_ok=True)
27
 
28
+ # HyperSwap works at 256x256 instead of the previous 128x128 InSwapper crop.
29
+ # Both models are downloaded from FaceFusion's official Hugging Face repos.
30
  SWAPPER_MODEL_PATH = hf_hub_download(
31
+ repo_id="facefusion/models-3.3.0",
32
+ filename="hyperswap_1a_256.onnx",
33
+ )
34
+ ENHANCER_MODEL_PATH = hf_hub_download(
35
+ repo_id="facefusion/models-3.0.0",
36
+ filename="gfpgan_1.4.onnx",
37
  )
38
 
39
  _MODEL_LOCK = threading.Lock()
40
+ _MODEL_CACHE: tuple[FaceAnalysis, ort.InferenceSession, ort.InferenceSession] | None = None
41
+
42
+ QUALITY_BALANCED = "Balanced — HyperSwap 256"
43
+ QUALITY_BEST = "Best quality — HyperSwap 256 + GFPGAN 512"
44
+
45
+ ARCFACE_128_TEMPLATE = np.array(
46
+ [
47
+ [0.36167656, 0.40387734],
48
+ [0.63696719, 0.40235469],
49
+ [0.50019687, 0.56044219],
50
+ [0.38710391, 0.72160547],
51
+ [0.61507734, 0.72034453],
52
+ ],
53
+ dtype=np.float32,
54
+ )
55
+ FFHQ_512_TEMPLATE = np.array(
56
+ [
57
+ [0.37691676, 0.46864664],
58
+ [0.62285697, 0.46912813],
59
+ [0.50123859, 0.61331904],
60
+ [0.39308822, 0.72541100],
61
+ [0.61150205, 0.72490465],
62
+ ],
63
+ dtype=np.float32,
64
+ )
65
 
66
 
67
  def _cleanup_old_outputs(max_age_seconds: int = 3600) -> None:
 
102
  target_face_index: int,
103
  clip_length: int,
104
  output_resolution: str,
105
+ quality_mode: str,
106
  keep_audio: bool,
107
  add_watermark: bool,
108
  consent: bool,
 
115
  seconds = min(_video_metadata(target_video)[3] or seconds, seconds)
116
  except Exception:
117
  pass
118
+ seconds_per_second = 8.0 if quality_mode == QUALITY_BEST else 5.5
119
+ return int(max(120, min(300, 60 + seconds * seconds_per_second)))
120
 
121
 
122
+ def _load_models() -> tuple[FaceAnalysis, ort.InferenceSession, ort.InferenceSession]:
123
  """Create CUDA ONNX sessions only after ZeroGPU has allocated a GPU."""
124
  global _MODEL_CACHE
125
  with _MODEL_LOCK:
 
134
  providers=providers,
135
  )
136
  analyser.prepare(ctx_id=0, det_size=(640, 640))
137
+ session_options = ort.SessionOptions()
138
+ session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
139
+ swapper = ort.InferenceSession(
140
+ SWAPPER_MODEL_PATH, sess_options=session_options, providers=providers
141
  )
142
+ enhancer = ort.InferenceSession(
143
+ ENHANCER_MODEL_PATH, sess_options=session_options, providers=providers
144
+ )
145
+ _MODEL_CACHE = (analyser, swapper, enhancer)
146
  return _MODEL_CACHE
147
 
148
 
149
+ def _input_dtype(session: ort.InferenceSession, input_name: str) -> np.dtype:
150
+ input_type = next(item.type for item in session.get_inputs() if item.name == input_name)
151
+ if "float16" in input_type:
152
+ return np.dtype(np.float16)
153
+ if "double" in input_type:
154
+ return np.dtype(np.float64)
155
+ return np.dtype(np.float32)
156
+
157
+
158
+ def _warp_face(
159
+ frame: np.ndarray,
160
+ landmarks: np.ndarray,
161
+ template: np.ndarray,
162
+ size: tuple[int, int],
163
+ ) -> tuple[np.ndarray, np.ndarray]:
164
+ destination = template * np.array(size, dtype=np.float32)
165
+ matrix = cv2.estimateAffinePartial2D(
166
+ np.asarray(landmarks, dtype=np.float32),
167
+ destination,
168
+ method=cv2.RANSAC,
169
+ ransacReprojThreshold=100,
170
+ )[0]
171
+ if matrix is None:
172
+ raise RuntimeError("Could not align the target face.")
173
+ crop = cv2.warpAffine(
174
+ frame,
175
+ matrix,
176
+ size,
177
+ flags=cv2.INTER_LINEAR,
178
+ borderMode=cv2.BORDER_REPLICATE,
179
+ )
180
+ return crop, matrix
181
+
182
+
183
+ def _soft_face_mask(size: tuple[int, int], blur: float = 0.12) -> np.ndarray:
184
+ width, height = size
185
+ mask = np.zeros((height, width), dtype=np.float32)
186
+ center = (width // 2, int(height * 0.53))
187
+ axes = (int(width * 0.43), int(height * 0.48))
188
+ cv2.ellipse(mask, center, axes, 0, 0, 360, 1.0, -1, cv2.LINE_AA)
189
+ sigma = max(1.0, width * blur * 0.35)
190
+ return cv2.GaussianBlur(mask, (0, 0), sigma).clip(0, 1)
191
+
192
+
193
+ def _paste_face(
194
+ frame: np.ndarray,
195
+ crop: np.ndarray,
196
+ mask: np.ndarray,
197
+ affine_matrix: np.ndarray,
198
+ ) -> np.ndarray:
199
+ height, width = frame.shape[:2]
200
+ inverse_matrix = cv2.invertAffineTransform(affine_matrix)
201
+ pasted = cv2.warpAffine(
202
+ crop,
203
+ inverse_matrix,
204
+ (width, height),
205
+ flags=cv2.INTER_LINEAR,
206
+ borderMode=cv2.BORDER_REPLICATE,
207
+ )
208
+ pasted_mask = cv2.warpAffine(
209
+ mask,
210
+ inverse_matrix,
211
+ (width, height),
212
+ flags=cv2.INTER_LINEAR,
213
+ ).clip(0, 1)[..., None]
214
+ return (frame * (1 - pasted_mask) + pasted * pasted_mask).astype(np.uint8)
215
+
216
+
217
+ def _swap_face_hq(
218
+ frame: np.ndarray,
219
+ landmarks: np.ndarray,
220
+ source_embedding: np.ndarray,
221
+ swapper: ort.InferenceSession,
222
+ ) -> np.ndarray:
223
+ size = (256, 256)
224
+ crop, matrix = _warp_face(frame, landmarks, ARCFACE_128_TEMPLATE, size)
225
+ target = crop[:, :, ::-1].astype(np.float32) / 255.0
226
+ target = ((target - 0.5) / 0.5).transpose(2, 0, 1)[None]
227
+
228
+ inputs: dict[str, np.ndarray] = {}
229
+ for item in swapper.get_inputs():
230
+ if item.name == "source":
231
+ inputs[item.name] = source_embedding.astype(_input_dtype(swapper, item.name))
232
+ elif item.name == "target":
233
+ inputs[item.name] = target.astype(_input_dtype(swapper, item.name))
234
+ output = swapper.run(None, inputs)[0][0].transpose(1, 2, 0)
235
+ output = ((output * 0.5 + 0.5).clip(0, 1)[:, :, ::-1] * 255).astype(np.uint8)
236
+ return _paste_face(frame, output, _soft_face_mask(size), matrix)
237
+
238
+
239
+ def _enhance_face(
240
+ frame: np.ndarray,
241
+ landmarks: np.ndarray,
242
+ enhancer: ort.InferenceSession,
243
+ blend: float = 0.75,
244
+ ) -> np.ndarray:
245
+ size = (512, 512)
246
+ crop, matrix = _warp_face(frame, landmarks, FFHQ_512_TEMPLATE, size)
247
+ prepared = crop[:, :, ::-1].astype(np.float32) / 255.0
248
+ prepared = ((prepared - 0.5) / 0.5).transpose(2, 0, 1)[None]
249
+
250
+ inputs: dict[str, np.ndarray] = {}
251
+ for item in enhancer.get_inputs():
252
+ if item.name == "input":
253
+ inputs[item.name] = prepared.astype(_input_dtype(enhancer, item.name))
254
+ elif item.name == "weight":
255
+ inputs[item.name] = np.array([0.5], dtype=_input_dtype(enhancer, item.name))
256
+ output = enhancer.run(None, inputs)[0][0].clip(-1, 1)
257
+ output = (((output + 1) * 0.5).transpose(1, 2, 0)[:, :, ::-1] * 255).astype(np.uint8)
258
+ enhanced = _paste_face(frame, output, _soft_face_mask(size, blur=0.10), matrix)
259
+ return cv2.addWeighted(frame, 1.0 - blend, enhanced, blend, 0)
260
+
261
+
262
  def _resize_to_limit(frame: np.ndarray, resolution: str) -> np.ndarray:
263
  limits = {"720p (fast)": 1280, "1080p": 1920, "Original": None}
264
  limit = limits.get(resolution, 1280)
 
364
  target_face_index: int,
365
  clip_length: int,
366
  output_resolution: str,
367
+ quality_mode: str,
368
  keep_audio: bool,
369
  add_watermark: bool,
370
  consent: bool,
 
383
  requested_seconds = min(requested_seconds, duration)
384
  frame_limit = max(1, int(round(requested_seconds * fps)))
385
 
386
+ analyser, swapper, enhancer = _load_models()
387
  source_frame = cv2.imread(source_image)
388
  if source_frame is None:
389
  raise gr.Error("The source image format is unsupported.")
 
392
  if not source_faces:
393
  raise gr.Error("No face was detected in the source image. Use a clear, front-facing portrait.")
394
  source_face = _largest_face(source_faces)
395
+ source_embedding = _embedding(source_face).reshape(1, -1)
396
 
397
  job_dir = OUTPUT_ROOT / uuid.uuid4().hex
398
  job_dir.mkdir(parents=True, exist_ok=False)
 
402
  cap = cv2.VideoCapture(target_video)
403
  writer: cv2.VideoWriter | None = None
404
  tracked_embedding: np.ndarray | None = None
405
+ smoothed_landmarks: np.ndarray | None = None
406
+ missed_face_frames = 0
407
  swapped_frames = 0
408
  processed_frames = 0
409
  face_index = max(1, int(target_face_index))
 
439
  target_face = _select_tracked_face(faces, tracked_embedding)
440
 
441
  if target_face is not None:
442
+ current_landmarks = np.asarray(target_face.kps, dtype=np.float32)
443
+ if smoothed_landmarks is None or missed_face_frames > 2:
444
+ smoothed_landmarks = current_landmarks
445
+ else:
446
+ smoothed_landmarks = 0.72 * current_landmarks + 0.28 * smoothed_landmarks
447
+ missed_face_frames = 0
448
+ frame = _swap_face_hq(
449
+ frame,
450
+ smoothed_landmarks,
451
+ source_embedding,
452
+ swapper,
453
+ )
454
+ if quality_mode == QUALITY_BEST:
455
+ frame = _enhance_face(frame, smoothed_landmarks, enhancer)
456
  swapped_frames += 1
457
+ else:
458
+ missed_face_frames += 1
459
 
460
  if add_watermark:
461
  frame = _add_ai_watermark(frame)
 
495
 
496
  status = (
497
  f"Completed {processed_seconds:.1f}s at {fps:.2f} FPS — "
498
+ f"face replaced in {swapped_frames}/{processed_frames} frames using {quality_mode}."
499
  )
500
  return str(final_path), status
501
 
 
512
  """
513
  <div class="hero">
514
  <h1>🎭 Dream Video Face Swap</h1>
515
+ <p>High-quality ZeroGPU face replacement with HyperSwap, enhancement, identity tracking, and audio preservation.</p>
516
+ <p dir="rtl">تبديل وجه عالي الجودة عبر ZeroGPU باستخدام HyperSwap وتحسين تفاصيل الوجه مع تتبع الشخص والاحتفاظ بالصوت.</p>
517
  </div>
518
  """
519
  )
 
549
  )
550
  output_resolution = gr.Radio(
551
  choices=["720p (fast)", "1080p", "Original"],
552
+ value="1080p",
553
  label="Output resolution",
554
  )
555
+ quality_mode = gr.Radio(
556
+ choices=[QUALITY_BEST, QUALITY_BALANCED],
557
+ value=QUALITY_BEST,
558
+ label="Quality / الجودة",
559
+ info="Best quality restores facial detail at 512×512; Balanced is faster.",
560
+ )
561
  keep_audio = gr.Checkbox(value=True, label="Keep original audio")
562
  add_watermark = gr.Checkbox(value=True, label="Add ‘AI FACE SWAP’ watermark")
563
  consent = gr.Checkbox(
 
573
  """
574
  <div class="notice">
575
 
576
+ **Tips / نصائح:** Use a sharp, front-facing source portrait. For multiple people, select the target by its left-to-right position in the first clear frame. “Best quality” uses HyperSwap 256 plus GFPGAN 512; start with a 5–10 second clip.
577
 
578
  استخدم صورة أمامية واضحة للوجه. إذا ظهر أكثر من شخص، اختر رقم الشخص حسب ترتيبه من اليسار إلى اليمين في أول لقطة واضحة.
579
 
 
591
  target_face_index,
592
  clip_length,
593
  output_resolution,
594
+ quality_mode,
595
  keep_audio,
596
  add_watermark,
597
  consent,