savvy7007 commited on
Commit
34234e4
·
verified ·
1 Parent(s): ddb740c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -81
app.py CHANGED
@@ -12,6 +12,8 @@ import numpy as np
12
  import cv2
13
  import tempfile
14
  import traceback
 
 
15
 
16
  # -------------------------
17
  # VERY EARLY: initialize session state
@@ -20,7 +22,10 @@ import traceback
20
  for key, default in {
21
  "uploaded_image": None,
22
  "uploaded_video": None,
 
23
  "output_video": None,
 
 
24
  }.items():
25
  if key not in st.session_state:
26
  st.session_state[key] = default
@@ -40,7 +45,11 @@ def _has_cuda():
40
  # Page & Sidebar (controls for speed)
41
  # -----------------------------------
42
  st.set_page_config(page_title="Face Swapper", layout="centered")
43
- st.title("🎭 Savvy Long Video Swapper")
 
 
 
 
44
 
45
  st.sidebar.title("⚙️ Settings")
46
 
@@ -52,20 +61,22 @@ proc_res = st.sidebar.selectbox(
52
  help="Frames are resized before detection/swap. Lower = faster."
53
  )
54
 
55
- # Skip frames to hit a lower effective FPS
56
- fps_cap = st.sidebar.selectbox(
57
- "Target FPS",
58
- ["Original", "24", "15"],
59
- index=0,
60
- help="Lower target FPS drops frames during processing for speed."
61
- )
 
 
62
 
63
- # Keep the original output resolution even if we process smaller
64
- keep_original_res = st.sidebar.checkbox(
65
- "Keep original output resolution",
66
- value=False,
67
- help="If enabled, processed frames are upscaled back to the input size."
68
- )
69
 
70
  # Limit faces per frame (helps speed on crowded scenes)
71
  max_faces = st.sidebar.slider(
@@ -124,7 +135,8 @@ with st.spinner("Loading models…"):
124
  app, swapper, providers, ctx_id = load_models()
125
  except Exception as e:
126
  st.error("❌ Model loading failed. See logs for details.")
127
- raise
 
128
 
129
  st.caption(
130
  f"Device: {'GPU (CUDA)' if ctx_id == 0 else 'CPU'} • ORT Providers: {', '.join(providers)}"
@@ -168,9 +180,90 @@ def _safe_imdecode(file_bytes):
168
  img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
169
  return img
170
 
 
 
 
 
 
 
 
 
 
171
  # -------------------------------------
172
- # Core: face swap over an input video
173
  # -------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  def swap_faces_in_video(
175
  image_bgr: np.ndarray,
176
  video_path: str,
@@ -191,13 +284,7 @@ def swap_faces_in_video(
191
  st.error("❌ No face detected in the source image.")
192
  return None
193
 
194
- # Use the largest detected face if there are multiple
195
- source_face = max(
196
- source_faces,
197
- key=lambda f: (f.bbox[2]-f.bbox[0]) * (f.bbox[1]-f.bbox[3]) # absolute area doesn't depend on sign but keep positive
198
- if hasattr(f, "bbox") else 0
199
- )
200
- # (safer area) re-compute properly
201
  source_face = max(
202
  source_faces,
203
  key=lambda f: max(1, int((f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])))
@@ -286,14 +373,10 @@ def swap_faces_in_video(
286
  result_frame = proc_frame.copy()
287
  for tface in target_faces:
288
  try:
289
- # Some insightface builds want base=proc_frame, some allow in-place
290
- result_frame = swapper.get(
291
- proc_frame, tface, source_face, paste_back=True
292
- )
293
- except Exception:
294
- result_frame = swapper.get(
295
- result_frame, tface, source_face, paste_back=True
296
- )
297
 
298
  # Upscale back to original if requested
299
  if keep_original_res and (proc_w, proc_h) != (orig_w, orig_h):
@@ -320,6 +403,9 @@ def swap_faces_in_video(
320
  # Fallback progress for unknown frame counts
321
  progress.progress(min(1.0, (processed_frames % 300) / 300.0))
322
 
 
 
 
323
  finally:
324
  cap.release()
325
  out.release()
@@ -329,28 +415,36 @@ def swap_faces_in_video(
329
  # -------------------------
330
  # UI: Uploads & Preview
331
  # -------------------------
332
- st.write("Upload a **source face image** and a **target video**, preview them, tweak speed options, then start swapping.")
333
 
334
  image_file = st.file_uploader("Upload Source Image", type=["jpg", "jpeg", "png"])
335
- video_file = st.file_uploader("Upload Target Video", type=["mp4", "mov", "mkv", "avi"])
336
 
337
- # Previews (Streamlit handles these safely)
 
 
 
 
 
338
  if image_file:
339
  st.subheader("📷 Source Image Preview")
340
  st.image(image_file, caption="Source Image", use_column_width=True)
341
 
342
- if video_file:
343
- st.subheader("🎬 Target Video Preview")
344
- st.video(video_file)
 
 
 
 
345
 
346
  # -------------------------
347
  # Run button
348
  # -------------------------
349
  if st.button("🚀 Start Face Swap"):
350
- if not image_file or not video_file:
351
- st.error("⚠️ Please upload both a source image and a target video.")
352
  else:
353
- # Read uploads safely (do not consume file pointer used by preview)
354
  try:
355
  image_bytes = image_file.getvalue()
356
  source_image = _safe_imdecode(image_bytes)
@@ -361,51 +455,89 @@ if st.button("🚀 Start Face Swap"):
361
  st.error(f"❌ Failed to read the source image bytes: {e}")
362
  st.stop()
363
 
364
- try:
365
- # Persist temp video for OpenCV
366
- video_bytes = video_file.getvalue()
367
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_video:
368
- tmp_video.write(video_bytes)
369
- tmp_video_path = tmp_video.name
370
- except Exception as e:
371
- st.error(f"❌ Failed to save the uploaded video to a temp file: {e}")
372
- st.stop()
 
 
373
 
374
- with st.spinner("Processing video… This can take a while ⏳"):
375
- progress_bar = st.progress(0)
376
- output_video_path = swap_faces_in_video(
377
- source_image,
378
- tmp_video_path,
379
- proc_res=proc_res,
380
- fps_cap=fps_cap,
381
- keep_original_res=keep_original_res,
382
- max_faces=max_faces,
383
- progress=progress_bar
384
- )
385
-
386
- if output_video_path:
387
- st.success("✅ Face swapping completed!")
388
-
389
- st.subheader("📺 Output Video Preview")
390
- st.video(output_video_path)
391
-
392
- # Download button
 
 
 
 
 
 
 
 
 
 
 
393
  try:
394
- with open(output_video_path, "rb") as f:
395
- st.download_button(
396
- label="⬇️ Download Processed Video",
397
- data=f,
398
- file_name="output_swapped_video.mp4",
399
- mime="video/mp4"
400
- )
 
 
 
 
 
401
  except Exception as e:
402
- st.warning(f"⚠️ Could not open the output file for download: {e}")
 
403
 
404
- # Cleanup temp input video; keep output so it can be downloaded
405
- try:
406
- os.remove(tmp_video_path)
407
- except Exception:
408
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  # -------------
411
  # Diagnostics
 
12
  import cv2
13
  import tempfile
14
  import traceback
15
+ from PIL import Image
16
+ import io
17
 
18
  # -------------------------
19
  # VERY EARLY: initialize session state
 
22
  for key, default in {
23
  "uploaded_image": None,
24
  "uploaded_video": None,
25
+ "uploaded_target_image": None,
26
  "output_video": None,
27
+ "output_image": None,
28
+ "mode": "video", # 'video' or 'image'
29
  }.items():
30
  if key not in st.session_state:
31
  st.session_state[key] = default
 
45
  # Page & Sidebar (controls for speed)
46
  # -----------------------------------
47
  st.set_page_config(page_title="Face Swapper", layout="centered")
48
+ st.title("🎭 Savvy Face Swapper")
49
+
50
+ # Mode selection
51
+ mode = st.radio("Select Mode:", ["Video", "Image"], horizontal=True)
52
+ st.session_state.mode = mode.lower()
53
 
54
  st.sidebar.title("⚙️ Settings")
55
 
 
61
  help="Frames are resized before detection/swap. Lower = faster."
62
  )
63
 
64
+ # For video mode only
65
+ if st.session_state.mode == "video":
66
+ # Skip frames to hit a lower effective FPS
67
+ fps_cap = st.sidebar.selectbox(
68
+ "Target FPS",
69
+ ["Original", "24", "15"],
70
+ index=0,
71
+ help="Lower target FPS drops frames during processing for speed."
72
+ )
73
 
74
+ # Keep the original output resolution even if we process smaller
75
+ keep_original_res = st.sidebar.checkbox(
76
+ "Keep original output resolution",
77
+ value=False,
78
+ help="If enabled, processed frames are upscaled back to the input size."
79
+ )
80
 
81
  # Limit faces per frame (helps speed on crowded scenes)
82
  max_faces = st.sidebar.slider(
 
135
  app, swapper, providers, ctx_id = load_models()
136
  except Exception as e:
137
  st.error("❌ Model loading failed. See logs for details.")
138
+ st.error(str(e))
139
+ st.stop()
140
 
141
  st.caption(
142
  f"Device: {'GPU (CUDA)' if ctx_id == 0 else 'CPU'} • ORT Providers: {', '.join(providers)}"
 
180
  img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
181
  return img
182
 
183
+ def _cv2_to_pil(image):
184
+ """Convert OpenCV BGR image to PIL RGB image"""
185
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
186
+ return Image.fromarray(image_rgb)
187
+
188
+ def _pil_to_cv2(image):
189
+ """Convert PIL RGB image to OpenCV BGR image"""
190
+ return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
191
+
192
  # -------------------------------------
193
+ # Core: face swap functions
194
  # -------------------------------------
195
+ def swap_faces_in_image(
196
+ source_image_bgr: np.ndarray,
197
+ target_image_bgr: np.ndarray,
198
+ proc_res: str,
199
+ max_faces: int
200
+ ):
201
+ # Validate source image
202
+ try:
203
+ source_faces = app.get(source_image_bgr)
204
+ except Exception as e:
205
+ st.error(f"❌ FaceAnalysis failed on source image: {e}")
206
+ return None
207
+
208
+ if not source_faces:
209
+ st.error("❌ No face detected in the source image.")
210
+ return None
211
+
212
+ # Use the largest detected face
213
+ source_face = max(
214
+ source_faces,
215
+ key=lambda f: max(1, int((f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])))
216
+ )
217
+
218
+ # Get processing size
219
+ orig_h, orig_w = target_image_bgr.shape[:2]
220
+ proc_w, proc_h = _get_proc_size_choice(orig_w, orig_h, proc_res)
221
+
222
+ # Resize target image for processing
223
+ if (proc_w, proc_h) != (orig_w, orig_h):
224
+ target_image_proc = cv2.resize(target_image_bgr, (proc_w, proc_h), interpolation=cv2.INTER_AREA)
225
+ else:
226
+ target_image_proc = target_image_bgr.copy()
227
+
228
+ try:
229
+ # Detect faces on target image
230
+ try:
231
+ target_faces = app.get(target_image_proc)
232
+ except Exception as det_e:
233
+ st.error(f"[ERROR] Detection failed on target image: {det_e}")
234
+ target_faces = []
235
+
236
+ if not target_faces:
237
+ st.warning("⚠️ No faces detected in the target image.")
238
+ return _cv2_to_pil(target_image_bgr)
239
+
240
+ # Optionally limit faces to largest N
241
+ target_faces = sorted(
242
+ target_faces,
243
+ key=lambda f: (f.bbox[2]-f.bbox[0])*(f.bbox[3]-f.bbox[1]),
244
+ reverse=True
245
+ )[:max_faces]
246
+
247
+ # Swap faces
248
+ result_image = target_image_proc.copy()
249
+ for tface in target_faces:
250
+ try:
251
+ result_image = swapper.get(result_image, tface, source_face, paste_back=True)
252
+ except Exception as swap_e:
253
+ st.error(f"Face swap error: {swap_e}")
254
+ continue
255
+
256
+ # Resize back to original if needed
257
+ if (proc_w, proc_h) != (orig_w, orig_h):
258
+ result_image = cv2.resize(result_image, (orig_w, orig_h), interpolation=cv2.INTER_CUBIC)
259
+
260
+ return _cv2_to_pil(result_image)
261
+
262
+ except Exception as e:
263
+ st.error(f"❌ Error processing image: {e}")
264
+ traceback.print_exc()
265
+ return _cv2_to_pil(target_image_bgr)
266
+
267
  def swap_faces_in_video(
268
  image_bgr: np.ndarray,
269
  video_path: str,
 
284
  st.error("❌ No face detected in the source image.")
285
  return None
286
 
287
+ # Use the largest detected face
 
 
 
 
 
 
288
  source_face = max(
289
  source_faces,
290
  key=lambda f: max(1, int((f.bbox[2]-f.bbox[0]) * (f.bbox[3]-f.bbox[1])))
 
373
  result_frame = proc_frame.copy()
374
  for tface in target_faces:
375
  try:
376
+ result_frame = swapper.get(result_frame, tface, source_face, paste_back=True)
377
+ except Exception as swap_e:
378
+ print(f"[WARN] Face swap failed on frame {read_idx}: {swap_e}")
379
+ continue
 
 
 
 
380
 
381
  # Upscale back to original if requested
382
  if keep_original_res and (proc_w, proc_h) != (orig_w, orig_h):
 
403
  # Fallback progress for unknown frame counts
404
  progress.progress(min(1.0, (processed_frames % 300) / 300.0))
405
 
406
+ except Exception as e:
407
+ st.error(f"❌ Error during video processing: {e}")
408
+ traceback.print_exc()
409
  finally:
410
  cap.release()
411
  out.release()
 
415
  # -------------------------
416
  # UI: Uploads & Preview
417
  # -------------------------
418
+ st.write("Upload a **source face image** and a **target**, preview them, tweak options, then start swapping.")
419
 
420
  image_file = st.file_uploader("Upload Source Image", type=["jpg", "jpeg", "png"])
 
421
 
422
+ if st.session_state.mode == "video":
423
+ target_file = st.file_uploader("Upload Target Video", type=["mp4", "mov", "mkv", "avi"])
424
+ else:
425
+ target_file = st.file_uploader("Upload Target Image", type=["jpg", "jpeg", "png"])
426
+
427
+ # Previews
428
  if image_file:
429
  st.subheader("📷 Source Image Preview")
430
  st.image(image_file, caption="Source Image", use_column_width=True)
431
 
432
+ if target_file:
433
+ if st.session_state.mode == "video":
434
+ st.subheader("🎬 Target Video Preview")
435
+ st.video(target_file)
436
+ else:
437
+ st.subheader("🖼️ Target Image Preview")
438
+ st.image(target_file, caption="Target Image", use_column_width=True)
439
 
440
  # -------------------------
441
  # Run button
442
  # -------------------------
443
  if st.button("🚀 Start Face Swap"):
444
+ if not image_file or not target_file:
445
+ st.error("⚠️ Please upload both a source image and a target.")
446
  else:
447
+ # Read source image
448
  try:
449
  image_bytes = image_file.getvalue()
450
  source_image = _safe_imdecode(image_bytes)
 
455
  st.error(f"❌ Failed to read the source image bytes: {e}")
456
  st.stop()
457
 
458
+ if st.session_state.mode == "video":
459
+ # Process video
460
+ try:
461
+ # Persist temp video for OpenCV
462
+ video_bytes = target_file.getvalue()
463
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_video:
464
+ tmp_video.write(video_bytes)
465
+ tmp_video_path = tmp_video.name
466
+ except Exception as e:
467
+ st.error(f"❌ Failed to save the uploaded video to a temp file: {e}")
468
+ st.stop()
469
 
470
+ with st.spinner("Processing video… This can take a while ⏳"):
471
+ progress_bar = st.progress(0)
472
+ output_path = swap_faces_in_video(
473
+ source_image,
474
+ tmp_video_path,
475
+ proc_res=proc_res,
476
+ fps_cap=fps_cap,
477
+ keep_original_res=keep_original_res,
478
+ max_faces=max_faces,
479
+ progress=progress_bar
480
+ )
481
+
482
+ if output_path:
483
+ st.success("✅ Face swapping completed!")
484
+ st.subheader("📺 Output Video Preview")
485
+ st.video(output_path)
486
+
487
+ # Download button
488
+ try:
489
+ with open(output_path, "rb") as f:
490
+ st.download_button(
491
+ label="⬇️ Download Processed Video",
492
+ data=f,
493
+ file_name="output_swapped_video.mp4",
494
+ mime="video/mp4"
495
+ )
496
+ except Exception as e:
497
+ st.warning(f"⚠️ Could not open the output file for download: {e}")
498
+
499
+ # Cleanup temp input video
500
  try:
501
+ os.remove(tmp_video_path)
502
+ except Exception:
503
+ pass
504
+
505
+ else:
506
+ # Process image
507
+ try:
508
+ target_bytes = target_file.getvalue()
509
+ target_image = _safe_imdecode(target_bytes)
510
+ if target_image is None:
511
+ st.error("❌ Failed to decode target image. Please use a valid JPG/PNG.")
512
+ st.stop()
513
  except Exception as e:
514
+ st.error(f" Failed to read the target image bytes: {e}")
515
+ st.stop()
516
 
517
+ with st.spinner("Processing image…"):
518
+ result_image = swap_faces_in_image(
519
+ source_image,
520
+ target_image,
521
+ proc_res=proc_res,
522
+ max_faces=max_faces
523
+ )
524
+
525
+ if result_image:
526
+ st.success("✅ Face swapping completed!")
527
+ st.subheader("🖼️ Output Image Preview")
528
+ st.image(result_image, caption="Result Image", use_column_width=True)
529
+
530
+ # Download button
531
+ buf = io.BytesIO()
532
+ result_image.save(buf, format="JPEG")
533
+ byte_im = buf.getvalue()
534
+
535
+ st.download_button(
536
+ label="⬇️ Download Processed Image",
537
+ data=byte_im,
538
+ file_name="output_swapped_image.jpg",
539
+ mime="image/jpeg"
540
+ )
541
 
542
  # -------------
543
  # Diagnostics