import os # Speed up Hub downloads a lot (parallel chunked transfer instead of a # single stream). Must be set before huggingface_hub is imported/used. os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") import cv2 import gradio as gr import numpy as np import insightface from insightface.app import FaceAnalysis from huggingface_hub import hf_hub_download import subprocess import tempfile # --------------------------------------------------------------------------- # Model loading # --------------------------------------------------------------------------- # inswapper_128.onnx isn't bundled in this repo (it's a large binary model # file). We try to fetch it automatically from a couple of known public # mirrors on the Hugging Face Hub. If that ever stops working (mirrors can # disappear), just upload `inswapper_128.onnx` yourself to the Space's root # folder and the app will pick it up automatically. See README.md. INSWAPPER_MIRRORS = [ ("ezioruan/inswapper_128.onnx", "inswapper_128.onnx"), ("Devrim/inswapper_128", "inswapper_128.onnx"), ("countfloyd/deepfake", "inswapper_128.onnx"), ] LOCAL_MODEL_NAME = "inswapper_128.onnx" def get_swapper_model_path() -> str: if os.path.exists(LOCAL_MODEL_NAME): return LOCAL_MODEL_NAME last_err = None for repo_id, filename in INSWAPPER_MIRRORS: try: print(f"[model-download] trying {repo_id} ...") path = hf_hub_download(repo_id=repo_id, filename=filename) print(f"[model-download] success from {repo_id} -> {path}") return path except Exception as e: # noqa: BLE001 print(f"[model-download] failed from {repo_id}: {e}") last_err = e raise RuntimeError( "Could not auto-download inswapper_128.onnx from any mirror. " "Please upload the file manually to the Space's root directory " f"(same folder as app.py). Last error: {last_err}" ) _face_analyser = None _swapper = None def get_models(): """Lazily load and cache the face-analysis and face-swap models.""" global _face_analyser, _swapper providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] if _face_analyser is None: _face_analyser = FaceAnalysis(name="buffalo_l", providers=providers) _face_analyser.prepare(ctx_id=0, det_size=(640, 640)) if _swapper is None: model_path = get_swapper_model_path() _swapper = insightface.model_zoo.get_model(model_path, providers=providers) return _face_analyser, _swapper # --------------------------------------------------------------------------- # Core face-swap logic # --------------------------------------------------------------------------- def get_source_face(face_analyser, image_bgr): faces = face_analyser.get(image_bgr) if not faces: return None # If several faces are in the source photo, use the largest one. faces = sorted( faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]), reverse=True, ) return faces[0] def swap_face_in_frame(face_analyser, swapper, source_face, frame): faces = face_analyser.get(frame) if not faces: return frame result = frame for face in faces: result = swapper.get(result, face, source_face, paste_back=True) return result def merge_audio(original_video_path, silent_video_path, output_path) -> bool: """Copy the audio track from the original video onto the swapped video.""" try: cmd = [ "ffmpeg", "-y", "-i", silent_video_path, "-i", original_video_path, "-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0?", "-c:a", "aac", "-shortest", output_path, ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0 or not os.path.exists(output_path): print("[ffmpeg] audio merge failed:", result.stderr) return False return True except Exception as e: # noqa: BLE001 print("[ffmpeg] audio merge error:", e) return False def process_video(source_image, target_video_path, progress=gr.Progress()): if source_image is None or target_video_path is None: raise gr.Error("Please upload both a source face image and a target video.") progress(0, desc="Loading models (first run downloads them, ~1-2 min)...") face_analyser, swapper = get_models() source_bgr = cv2.cvtColor(source_image, cv2.COLOR_RGB2BGR) source_face = get_source_face(face_analyser, source_bgr) if source_face is None: raise gr.Error( "No face was detected in the uploaded image. " "Try a clearer, front-facing, well-lit photo." ) cap = cv2.VideoCapture(target_video_path) if not cap.isOpened(): raise gr.Error("Could not open the uploaded video.") fps = cap.get(cv2.CAP_PROP_FPS) or 25 width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0 tmp_dir = tempfile.mkdtemp() silent_path = os.path.join(tmp_dir, "swapped_silent.mp4") final_path = os.path.join(tmp_dir, "swapped_final.mp4") fourcc = cv2.VideoWriter_fourcc(*"mp4v") writer = cv2.VideoWriter(silent_path, fourcc, fps, (width, height)) frame_idx = 0 while True: ret, frame = cap.read() if not ret: break swapped = swap_face_in_frame(face_analyser, swapper, source_face, frame) writer.write(swapped) frame_idx += 1 if total_frames: progress( min(frame_idx / total_frames, 0.95), desc=f"Swapping frame {frame_idx}/{total_frames}", ) else: progress(0.5, desc=f"Swapping frame {frame_idx}") cap.release() writer.release() progress(0.97, desc="Merging original audio track...") ok = merge_audio(target_video_path, silent_path, final_path) result_path = final_path if ok else silent_path progress(1.0, desc="Done!") return result_path # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- with gr.Blocks(title="Face Swap: Image → Video") as demo: gr.Markdown( """ # 🎭 Face Swap — Image onto Video Upload a clear photo of a face and a target video. The face from your photo will be swapped onto every face detected in the video. **Please only use this on content you have the rights and consent to edit.** This tool is for creative and educational use — don't use it to impersonate real people without their permission. """ ) with gr.Row(): with gr.Column(): source_image = gr.Image(label="Source face image", type="numpy") target_video = gr.Video(label="Target video", sources=["upload"]) run_btn = gr.Button("Swap Face", variant="primary") with gr.Column(): output_video = gr.Video(label="Result") run_btn.click( fn=process_video, inputs=[source_image, target_video], outputs=[output_video], ) gr.Markdown( """ --- ### Notes - First run downloads the face-analysis + face-swap models (~300 MB total), which can take a minute or two. - This app needs a **GPU Space** (T4 or better) for reasonable speed. On CPU it will still work but be much slower. - Every frame of the video is processed, so long videos take a while. - Output keeps the original video's audio track when possible. """ ) if __name__ == "__main__": demo.queue(max_size=10).launch( server_name="0.0.0.0", server_port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)), )