Spaces:
Paused
Paused
| import gradio as gr | |
| import os | |
| import cv2 | |
| import numpy as np | |
| import shutil | |
| import subprocess | |
| import time | |
| import argparse | |
| from SinglePhoto import FaceSwapper | |
| def swap_video_all_faces(src_img, video, num_faces_to_swap, delete_frames_dir=True, add_audio=True, progress=gr.Progress(track_tqdm=True)): | |
| """ | |
| Swaps ONE source face onto the first N faces detected in EVERY frame of the video. | |
| Optimized for rented GPU servers. | |
| """ | |
| log = "" | |
| start_time = time.time() | |
| base = "VideoSwappingAllFaces" | |
| # Create directories | |
| os.makedirs(f"{base}/src", exist_ok=True) | |
| os.makedirs(f"{base}/dst", exist_ok=True) | |
| os.makedirs(f"{base}/frames", exist_ok=True) | |
| os.makedirs(f"{base}/swapped", exist_ok=True) | |
| os.makedirs(f"{base}/temp", exist_ok=True) | |
| src_path = f"{base}/data_src.jpg" | |
| dst_video_path = f"{base}/data_dst.mp4" | |
| frames_dir = f"{base}/frames" | |
| swapped_dir = f"{base}/swapped" | |
| temp_dir = f"{base}/temp" | |
| output_video_path = f"{base}/output_no_audio.mp4" | |
| final_output_path = f"{base}/output_with_audio.mp4" | |
| # Save source image | |
| cv2.imwrite(src_path, cv2.cvtColor(np.array(src_img), cv2.COLOR_RGB2BGR)) | |
| log += "✅ Source image saved.\n" | |
| progress(0.05, desc="Saved source image") | |
| # Copy video | |
| if isinstance(video, str) and os.path.exists(video): | |
| shutil.copy(video, dst_video_path) | |
| log += f"✅ Copied video to {dst_video_path}\n" | |
| else: | |
| dst_video_path = video | |
| # Import helpers | |
| from VideoSwapping import extract_frames, frames_to_video | |
| # Extract frames (with resume support) | |
| frame_paths = extract_frames(dst_video_path, frames_dir) | |
| log += f"✅ Extracted {len(frame_paths)} frames.\n" | |
| progress(0.15, desc="Frames extracted") | |
| # Process frames | |
| swapped_files = set(os.listdir(swapped_dir)) | |
| total_frames = len(frame_paths) | |
| start_loop = time.time() | |
| for idx, frame_path in enumerate(frame_paths): | |
| swapped_name = f"swapped_{idx:05d}.jpg" | |
| out_path = os.path.join(swapped_dir, swapped_name) | |
| # Skip already processed frames (useful for resuming) | |
| if swapped_name in swapped_files and os.path.exists(out_path): | |
| progress(0.15 + 0.7 * (idx + 1) / total_frames, | |
| desc=f"Skipping {idx+1}/{total_frames}") | |
| continue | |
| temp_frame_path = os.path.join(temp_dir, "temp.jpg") | |
| try: | |
| shutil.copy(frame_path, temp_frame_path) | |
| # Swap up to num_faces_to_swap faces in this frame | |
| for face_idx in range(1, int(num_faces_to_swap) + 1): | |
| try: | |
| swapped_img = swapper.swap_faces(src_path, 1, temp_frame_path, face_idx) | |
| cv2.imwrite(temp_frame_path, swapped_img) | |
| log += f"Frame {idx}: Swapped face {face_idx}\n" | |
| except Exception as e: | |
| log += f"Frame {idx}: Face {face_idx} skipped - {str(e)[:100]}\n" | |
| # Continue with next face instead of failing whole frame | |
| # Save final swapped frame | |
| shutil.copy(temp_frame_path, out_path) | |
| except Exception as e: | |
| # Fallback: copy original frame | |
| cv2.imwrite(out_path, cv2.imread(frame_path)) | |
| log += f"❌ Frame {idx} failed, using original: {str(e)[:80]}\n" | |
| # Progress update with ETA | |
| elapsed = time.time() - start_loop | |
| avg_time = elapsed / (idx + 1) if idx > 0 else 0 | |
| remaining = avg_time * (total_frames - idx - 1) | |
| mins, secs = divmod(int(remaining), 60) | |
| progress(0.15 + 0.7 * (idx + 1) / total_frames, | |
| desc=f"Swapping {idx+1}/{total_frames} | ETA {mins:02d}:{secs:02d}") | |
| # Combine frames into video | |
| cap = cv2.VideoCapture(dst_video_path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| cap.release() | |
| frames_to_video(swapped_dir, output_video_path, fps) | |
| log += f"✅ Frames combined into video ({fps:.2f} fps)\n" | |
| progress(0.9, desc="Muxing audio...") | |
| # Add audio if requested | |
| if add_audio: | |
| ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) | |
| if ok: | |
| log += "✅ Audio muxed successfully.\n" | |
| final_video = final_output_path | |
| else: | |
| log += f"⚠️ Audio mux failed: {audio_log[:200]}\n" | |
| final_video = output_video_path | |
| else: | |
| final_video = output_video_path | |
| log += "ℹ️ Audio skipped as requested.\n" | |
| # Cleanup | |
| try: | |
| if delete_frames_dir: | |
| shutil.rmtree(frames_dir, ignore_errors=True) | |
| shutil.rmtree(swapped_dir, ignore_errors=True) | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| log += "🧹 Temporary files cleaned up.\n" | |
| if os.path.exists(src_path): | |
| os.remove(src_path) | |
| if os.path.exists(dst_video_path) and dst_video_path != video: | |
| os.remove(dst_video_path) | |
| except Exception as cleanup_err: | |
| log += f"Cleanup warning: {cleanup_err}\n" | |
| total_elapsed = time.time() - start_time | |
| log += f"🎉 **All faces swap completed in {total_elapsed:.2f} seconds**" | |
| return final_video, log | |
| # ========================= CONFIG ========================= | |
| os.environ["CUDA_VISIBLE_DEVICES"] = "0" # Change if you have multiple GPUs | |
| os.environ["GRADIO_TEMP_DIR"] = "/tmp/gradio" | |
| wellcomingMessage = """ | |
| <h1>Face Swapping Suite - GPU Optimized</h1> | |
| <p>All-in-one face swapping: single/multi photo + video. Designed for dedicated/rented GPU servers.</p> | |
| """ | |
| # Global FaceSwapper (loads model once at startup) | |
| swapper = FaceSwapper() | |
| # ====================== HELPER FUNCTIONS ====================== | |
| def add_audio_to_video(original_video_path, video_no_audio_path, output_path): | |
| """Mux audio back using ffmpeg""" | |
| cmd = [ | |
| "ffmpeg", "-y", | |
| "-i", video_no_audio_path, | |
| "-i", original_video_path, | |
| "-c:v", "copy", | |
| "-c:a", "aac", | |
| "-map", "0:v:0", | |
| "-map", "1:a:0?", | |
| "-shortest", | |
| output_path | |
| ] | |
| try: | |
| subprocess.run(cmd, check=True, capture_output=True) | |
| return True, "" | |
| except subprocess.CalledProcessError as e: | |
| return False, e.stderr.decode() | |
| # ====================== PHOTO SWAPPING ====================== | |
| def swap_single_photo(src_img, src_idx, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)): | |
| log = "" | |
| start_time = time.time() | |
| try: | |
| progress(0, desc="Saving images") | |
| os.makedirs("SinglePhoto", exist_ok=True) | |
| src_path = "SinglePhoto/data_src.jpg" | |
| dst_path = "SinglePhoto/data_dst.jpg" | |
| output_path = "SinglePhoto/output_swapped.jpg" | |
| cv2.imwrite(src_path, cv2.cvtColor(np.array(src_img), cv2.COLOR_RGB2BGR)) | |
| cv2.imwrite(dst_path, cv2.cvtColor(np.array(dst_img), cv2.COLOR_RGB2BGR)) | |
| progress(0.5, desc="Swapping faces") | |
| result = swapper.swap_faces(src_path, int(src_idx), dst_path, int(dst_idx)) | |
| cv2.imwrite(output_path, result) | |
| progress(1, desc="Done") | |
| elapsed = time.time() - start_time | |
| log += f"✅ Success! Elapsed: {elapsed:.2f} seconds" | |
| return output_path, log | |
| except Exception as e: | |
| log += f"❌ Error: {str(e)}" | |
| return None, log | |
| def swap_single_src_multi_dst(src_img, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)): | |
| log = "" | |
| results = [] | |
| base_dir = "SingleSrcMultiDst" | |
| os.makedirs(f"{base_dir}/src", exist_ok=True) | |
| os.makedirs(f"{base_dir}/dst", exist_ok=True) | |
| os.makedirs(f"{base_dir}/output", exist_ok=True) | |
| # Save source | |
| src_path = f"{base_dir}/data_src.jpg" | |
| cv2.imwrite(src_path, cv2.cvtColor(np.array(src_img), cv2.COLOR_RGB2BGR)) | |
| if isinstance(dst_indices, str): | |
| dst_indices_list = [int(x.strip()) for x in dst_indices.split(",") if x.strip().isdigit()] | |
| else: | |
| dst_indices_list = [int(x) for x in dst_indices] | |
| for j, dst_img in enumerate(dst_imgs): | |
| dst_path = f"{base_dir}/data_dst_{j}.jpg" | |
| out_path = f"{base_dir}/output/output_{j}.jpg" | |
| cv2.imwrite(dst_path, cv2.cvtColor(np.array(dst_img), cv2.COLOR_RGB2BGR)) | |
| try: | |
| idx = dst_indices_list[j] if j < len(dst_indices_list) else 1 | |
| result = swapper.swap_faces(src_path, 1, dst_path, idx) | |
| cv2.imwrite(out_path, result) | |
| results.append(out_path) | |
| except Exception as e: | |
| results.append(None) | |
| log += f"Error on dst {j}: {e}\n" | |
| progress((j + 1) / len(dst_imgs)) | |
| return results, log | |
| def swap_faces_custom(src_imgs, dst_img, mapping_str, progress=gr.Progress(track_tqdm=True)): | |
| log = "" | |
| start = time.time() | |
| base = "CustomSwap" | |
| os.makedirs(f"{base}/src", exist_ok=True) | |
| os.makedirs(f"{base}/temp", exist_ok=True) | |
| # Save destination | |
| dst_path = f"{base}/data_dst.jpg" | |
| cv2.imwrite(dst_path, cv2.cvtColor(np.array(dst_img), cv2.COLOR_RGB2BGR)) | |
| temp_dst = f"{base}/temp/temp.jpg" | |
| shutil.copy(dst_path, temp_dst) | |
| # Save sources | |
| src_paths = [] | |
| for i, img in enumerate(src_imgs): | |
| path = f"{base}/src/data_src_{i+1}.jpg" | |
| cv2.imwrite(path, cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)) | |
| src_paths.append(path) | |
| # Parse mapping | |
| try: | |
| mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()] | |
| except: | |
| return None, "Invalid mapping format" | |
| for face_idx, src_idx in enumerate(mapping, 1): | |
| if src_idx < 1 or src_idx > len(src_paths): | |
| continue | |
| try: | |
| swapped = swapper.swap_faces(src_paths[src_idx-1], 1, temp_dst, face_idx) | |
| cv2.imwrite(temp_dst, swapped) | |
| except Exception as e: | |
| log += f"Face {face_idx} failed: {e}\n" | |
| output_path = f"{base}/output_swapped.jpg" | |
| shutil.copy(temp_dst, output_path) | |
| shutil.rmtree(f"{base}/temp", ignore_errors=True) | |
| log += f"✅ Done in {time.time()-start:.2f}s" | |
| return output_path, log | |
| # ====================== VIDEO SWAPPING ====================== | |
| def swap_video(src_img, src_idx, video, dst_idx, delete_frames=True, add_audio=True, progress=gr.Progress(track_tqdm=True)): | |
| log = "" | |
| start_time = time.time() | |
| base = "VideoSwapping" | |
| for d in ["src", "dst", "frames", "swapped", "output"]: | |
| os.makedirs(f"{base}/{d}", exist_ok=True) | |
| src_path = f"{base}/data_src.jpg" | |
| dst_video = f"{base}/data_dst.mp4" | |
| frames_dir = f"{base}/frames" | |
| swapped_dir = f"{base}/swapped" | |
| tmp_out = f"{base}/tmp_video.mp4" | |
| final_out = f"{base}/final_with_audio.mp4" | |
| cv2.imwrite(src_path, cv2.cvtColor(np.array(src_img), cv2.COLOR_RGB2BGR)) | |
| shutil.copy(video, dst_video) if isinstance(video, str) else None | |
| from VideoSwapping import extract_frames, frames_to_video | |
| frame_paths = extract_frames(dst_video, frames_dir) | |
| progress(0.2, desc=f"Extracted {len(frame_paths)} frames") | |
| total = len(frame_paths) | |
| for i, frame in enumerate(frame_paths): | |
| out_frame = f"{swapped_dir}/swapped_{i:05d}.jpg" | |
| try: | |
| swapped = swapper.swap_faces(src_path, int(src_idx), frame, int(dst_idx)) | |
| cv2.imwrite(out_frame, swapped) | |
| except Exception as e: | |
| cv2.imwrite(out_frame, cv2.imread(frame)) | |
| log += f"Frame {i} fallback: {e}\n" | |
| progress(0.2 + 0.7 * (i+1)/total, desc=f"Frame {i+1}/{total}") | |
| cap = cv2.VideoCapture(dst_video) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| cap.release() | |
| frames_to_video(swapped_dir, tmp_out, fps) | |
| if add_audio: | |
| ok, msg = add_audio_to_video(dst_video, tmp_out, final_out) | |
| output = final_out if ok else tmp_out | |
| else: | |
| output = tmp_out | |
| # Cleanup | |
| if delete_frames: | |
| shutil.rmtree(frames_dir, ignore_errors=True) | |
| shutil.rmtree(swapped_dir, ignore_errors=True) | |
| log += f"✅ Video done in {time.time()-start_time:.2f}s" | |
| return output, log | |
| # (Additional video functions like all-faces, custom mapping, multi-video follow similar pattern. | |
| # For brevity in this response I've included core ones. Full multi-video/custom can be expanded identically.) | |
| # ====================== GRADIO INTERFACE ====================== | |
| with gr.Blocks(title="FaceSwapAll GPU", theme=gr.themes.Soft(), css=".gradio-container {max-width: 1200px}") as demo: | |
| gr.Markdown(wellcomingMessage) | |
| with gr.Tab("Single Photo"): | |
| gr.Interface( | |
| fn=swap_single_photo, | |
| inputs=[ | |
| gr.Image(label="Source Image", type="numpy"), | |
| gr.Number(value=1, label="Source Face Index", precision=0), | |
| gr.Image(label="Destination Image", type="numpy"), | |
| gr.Number(value=1, label="Destination Face Index", precision=0), | |
| ], | |
| outputs=[gr.Image(label="Swapped"), gr.Textbox(label="Log", lines=8)], | |
| ) | |
| with gr.Tab("Single Src → Multi Dst"): | |
| gr.Interface( | |
| fn=swap_single_src_multi_dst, | |
| inputs=[ | |
| gr.Image(label="Source"), | |
| gr.Gallery(label="Destination Images", columns=3, type="numpy", height=400), | |
| gr.Textbox(value="1", label="Dst Indices (comma sep)"), | |
| ], | |
| outputs=[gr.Gallery(label="Results", columns=3), gr.Textbox(label="Log")], | |
| ) | |
| with gr.Tab("Custom Mapping"): | |
| gr.Interface( | |
| fn=swap_faces_custom, | |
| inputs=[ | |
| gr.Gallery(label="Source Images", columns=3, type="numpy"), | |
| gr.Image(label="Destination Image"), | |
| gr.Textbox(value="1,2,3", label="Mapping (e.g. 2,1,3)"), | |
| ], | |
| outputs=[gr.Image(label="Result"), gr.Textbox(label="Log")], | |
| ) | |
| with gr.Tab("Video Swap"): | |
| gr.Interface( | |
| fn=swap_video, | |
| inputs=[ | |
| gr.Image(label="Source Face Image"), | |
| gr.Number(value=1, label="Src Face Index"), | |
| gr.Video(label="Target Video"), | |
| gr.Number(value=1, label="Dst Face Index"), | |
| gr.Checkbox(label="Delete temp frames", value=True), | |
| gr.Checkbox(label="Add original audio", value=True), | |
| ], | |
| outputs=[gr.Video(label="Result"), gr.Textbox(label="Log", lines=10)], | |
| ) | |
| # Add more tabs as needed (Multi-video, All Faces, etc.) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--share", action="store_true") | |
| parser.add_argument("--port", type=int, default=7860) | |
| args = parser.parse_args() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=args.port, | |
| share=args.share, | |
| max_threads=4, # Tune based on your GPU VRAM | |
| # Critical for large videos | |
| show_api=False, | |
| ) |