Spaces:
Paused
Paused
| # ============================================================================ | |
| # ULTIMATE FACE SWAP - 100% QUALITY + HEAD SWAP (WITH HAIR!) | |
| # Fixed for Hugging Face Spaces deployment | |
| # ============================================================================ | |
| print("="*80) | |
| print("ULTIMATE FACE SWAP - 100% QUALITY + HEAD SWAP MODE!") | |
| print("="*80) | |
| import subprocess, sys | |
| print("\n[1/7] Installing packages...") | |
| subprocess.check_call([ | |
| sys.executable, "-m", "pip", "install", "-q", | |
| "gradio==3.50.2", "insightface==0.7.3", "onnxruntime", | |
| "opencv-python-headless", "moviepy==1.0.3", "numpy", "scipy", "tqdm", | |
| "gfpgan", "basicsr", "facexlib", "torch", "torchvision" | |
| ]) | |
| print("β Installed") | |
| print("\n[2/7] Importing libraries...") | |
| import gradio as gr | |
| import cv2 | |
| import numpy as np | |
| import os | |
| import tempfile | |
| from insightface.app import FaceAnalysis | |
| from insightface.model_zoo import get_model | |
| # Import moviepy with fallback for different versions | |
| try: | |
| from moviepy.editor import VideoFileClip, ImageSequenceClip | |
| except ImportError: | |
| from moviepy import VideoFileClip, ImageSequenceClip | |
| from tqdm import tqdm | |
| print("β Imported") | |
| # ============================================================================ | |
| # SECTION 1: FACE DETECTION (CPU MODE) | |
| # ============================================================================ | |
| print("\n[3/7] Loading face detector...") | |
| face_app = FaceAnalysis(name="buffalo_l", providers=['CPUExecutionProvider']) | |
| face_app.prepare(ctx_id=-1, det_size=(640, 640)) # ctx_id=-1 for CPU | |
| print("β Face detector loaded (CPU mode)") | |
| # ============================================================================ | |
| # SECTION 2: INSWAPPER MODEL (CPU MODE) | |
| # ============================================================================ | |
| print("\n[4/7] Loading INSwapper...") | |
| swapper = None | |
| SWAPPER_LOADED = False | |
| try: | |
| model_path = 'inswapper_128.onnx' | |
| if not os.path.exists(model_path) or os.path.getsize(model_path) < 100_000_000: | |
| print(" Downloading from HuggingFace...") | |
| import urllib.request | |
| url = "https://huggingface.co/CountFloyd/deepfake/resolve/main/inswapper_128.onnx" | |
| urllib.request.urlretrieve(url, model_path) | |
| print(f" β Downloaded ({os.path.getsize(model_path) // 1_000_000}MB)") | |
| swapper = get_model(model_path, download=False, download_zip=False, providers=['CPUExecutionProvider']) | |
| SWAPPER_LOADED = True | |
| print("β INSwapper loaded (CPU mode)") | |
| except Exception as e: | |
| print(f"β INSwapper failed: {e}") | |
| # ============================================================================ | |
| # SECTION 3: CODEFORMER (SIMPLIFIED FOR CPU) | |
| # ============================================================================ | |
| print("\n[5/7] Loading CodeFormer...") | |
| codeformer_net = None | |
| CODEFORMER_LOADED = False | |
| try: | |
| from basicsr.archs.rrdbnet_arch import RRDBNet | |
| from basicsr.utils.download_util import load_file_from_url | |
| from basicsr.utils import imwrite, img2tensor, tensor2img | |
| from facexlib.utils.face_restoration_helper import FaceRestoreHelper | |
| import torch | |
| # Download CodeFormer model | |
| model_url = 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth' | |
| model_path = 'codeformer.pth' | |
| if not os.path.exists(model_path): | |
| print(" Downloading CodeFormer...") | |
| import urllib.request | |
| urllib.request.urlretrieve(model_url, model_path) | |
| print(" β Downloaded") | |
| # Load CodeFormer network | |
| from basicsr.archs import build_network | |
| codeformer_net = build_network({ | |
| 'type': 'CodeFormer', | |
| 'dim_embd': 512, | |
| 'n_head': 8, | |
| 'n_layers': 9, | |
| 'connect_list': ['32', '64', '128', '256'] | |
| }) | |
| checkpoint = torch.load(model_path, map_location='cpu') | |
| codeformer_net.load_state_dict(checkpoint['params_ema']) | |
| codeformer_net.eval() | |
| # Always use CPU for Spaces | |
| device = 'cpu' | |
| codeformer_net = codeformer_net.to(device) | |
| # Face helper for detection and alignment | |
| face_helper = FaceRestoreHelper( | |
| upscale_factor=1, | |
| face_size=512, | |
| crop_ratio=(1, 1), | |
| det_model='retinaface_resnet50', | |
| save_ext='png', | |
| use_parse=True, | |
| device=device | |
| ) | |
| CODEFORMER_LOADED = True | |
| print("β CodeFormer loaded (CPU mode)") | |
| except Exception as e: | |
| print(f"β CodeFormer failed: {e}") | |
| print(" Will use basic enhancement only") | |
| # ============================================================================ | |
| # SECTION 4: GFPGAN (BACKUP/COMPLEMENTARY) | |
| # ============================================================================ | |
| print("\n[6/7] Loading GFPGAN...") | |
| gfpgan_restorer = None | |
| GFPGAN_LOADED = False | |
| try: | |
| from gfpgan import GFPGANer | |
| model_file = 'GFPGANv1.4.pth' | |
| if not os.path.exists(model_file): | |
| print(" Downloading GFPGAN...") | |
| import urllib.request | |
| urllib.request.urlretrieve( | |
| "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.4.pth", | |
| model_file | |
| ) | |
| gfpgan_restorer = GFPGANer( | |
| model_path=model_file, | |
| upscale=2, | |
| arch='clean', | |
| channel_multiplier=2, | |
| bg_upsampler=None, | |
| device='cpu' # Force CPU | |
| ) | |
| GFPGAN_LOADED = True | |
| print("β GFPGAN loaded (CPU mode)") | |
| except Exception as e: | |
| print(f"β GFPGAN unavailable: {e}") | |
| # ============================================================================ | |
| # HELPER FUNCTIONS | |
| # ============================================================================ | |
| def resize_preview(image, max_width=400, max_height=300): | |
| """Resize to 1/4 size""" | |
| if image is None: | |
| return None | |
| h, w = image.shape[:2] | |
| scale = min(max_width / w, max_height / h, 1.0) | |
| if scale < 1.0: | |
| new_w = int(w * scale) | |
| new_h = int(h * scale) | |
| return cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) | |
| return image | |
| def detect_faces_with_preview(image): | |
| """Detect faces with small preview""" | |
| if image is None: | |
| return None, [] | |
| if len(image.shape) == 2: | |
| image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) | |
| elif image.shape[2] == 4: | |
| image = cv2.cvtColor(image, cv2.COLOR_RGBA2BGR) | |
| faces = face_app.get(image) | |
| if not faces: | |
| preview_small = resize_preview(image) | |
| return cv2.cvtColor(preview_small, cv2.COLOR_BGR2RGB), [] | |
| preview = image.copy() | |
| for i, face in enumerate(faces): | |
| x1, y1, x2, y2 = face.bbox.astype(int) | |
| cv2.rectangle(preview, (x1, y1), (x2, y2), (0, 255, 0), 2) | |
| cv2.putText( | |
| preview, f"Face {i+1}", (x1, y1 - 10), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2 | |
| ) | |
| preview_small = resize_preview(preview) | |
| return cv2.cvtColor(preview_small, cv2.COLOR_BGR2RGB), faces | |
| # ============================================================================ | |
| # CODEFORMER RESTORATION FUNCTION | |
| # ============================================================================ | |
| def restore_with_codeformer(face_img, fidelity_weight=0.2): | |
| """Apply CodeFormer restoration""" | |
| import torch | |
| from torchvision.transforms import functional as F | |
| device = 'cpu' | |
| # Prepare image | |
| face_img = cv2.resize(face_img, (512, 512), interpolation=cv2.INTER_LINEAR) | |
| face_img = face_img.astype(np.float32) / 255.0 | |
| face_img = torch.from_numpy(face_img).permute(2, 0, 1).unsqueeze(0).to(device) | |
| # Normalize | |
| face_img = F.normalize(face_img, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) | |
| # Run CodeFormer | |
| with torch.no_grad(): | |
| output = codeformer_net(face_img, w=fidelity_weight)[0] | |
| # Convert back | |
| output = output.squeeze(0).permute(1, 2, 0).cpu().numpy() | |
| output = np.clip((output + 1) / 2 * 255, 0, 255).astype(np.uint8) | |
| return output | |
| # ============================================================================ | |
| # ENHANCED FACE SWAP | |
| # ============================================================================ | |
| def swap_face_in_frame(frame, source_face, target_face_idx=None, include_hair=False): | |
| """Enhanced face swap with restoration""" | |
| if not SWAPPER_LOADED: | |
| return frame | |
| target_faces = face_app.get(frame) | |
| if len(target_faces) == 0: | |
| return frame | |
| # Swap face(s) | |
| if target_face_idx is not None: | |
| if target_face_idx >= len(target_faces): | |
| return frame | |
| result = swapper.get(frame, target_faces[target_face_idx], source_face, paste_back=True) | |
| else: | |
| result = frame.copy() | |
| for target_face in target_faces: | |
| result = swapper.get(result, target_face, source_face, paste_back=True) | |
| # Apply restoration | |
| if CODEFORMER_LOADED and codeformer_net: | |
| try: | |
| swapped_faces = face_app.get(result) | |
| for face in swapped_faces: | |
| x1, y1, x2, y2 = face.bbox.astype(int) | |
| h, w = result.shape[:2] | |
| if include_hair: | |
| pad = int(max(x2-x1, y2-y1) * 0.6) | |
| else: | |
| pad = int(max(x2-x1, y2-y1) * 0.3) | |
| x1 = max(0, x1 - pad) | |
| y1 = max(0, y1 - pad) | |
| x2 = min(w, x2 + pad) | |
| y2 = min(h, y2 + pad) | |
| face_region = result[y1:y2, x1:x2].copy() | |
| original_size = (x2-x1, y2-y1) | |
| restored_face = restore_with_codeformer(face_region, fidelity_weight=0.2) | |
| restored_face = cv2.resize(restored_face, original_size, interpolation=cv2.INTER_LANCZOS4) | |
| if GFPGAN_LOADED and gfpgan_restorer: | |
| try: | |
| _, _, restored_face = gfpgan_restorer.enhance( | |
| restored_face, | |
| has_aligned=False, | |
| paste_back=True, | |
| weight=0.5 | |
| ) | |
| except: | |
| pass | |
| result[y1:y2, x1:x2] = restored_face | |
| print(" β CodeFormer applied") | |
| except Exception as e: | |
| print(f" β CodeFormer error: {e}") | |
| elif GFPGAN_LOADED and gfpgan_restorer: | |
| try: | |
| swapped_faces = face_app.get(result) | |
| for face in swapped_faces: | |
| x1, y1, x2, y2 = face.bbox.astype(int) | |
| h, w = result.shape[:2] | |
| pad = int(max(x2-x1, y2-y1) * (0.6 if include_hair else 0.3)) | |
| x1 = max(0, x1 - pad) | |
| y1 = max(0, y1 - pad) | |
| x2 = min(w, x2 + pad) | |
| y2 = min(h, y2 + pad) | |
| face_region = result[y1:y2, x1:x2].copy() | |
| _, _, restored_face = gfpgan_restorer.enhance( | |
| face_region, | |
| has_aligned=False, | |
| paste_back=True, | |
| weight=0.9 | |
| ) | |
| result[y1:y2, x1:x2] = restored_face | |
| print(" β GFPGAN applied") | |
| except Exception as e: | |
| print(f" β GFPGAN error: {e}") | |
| return result | |
| # ============================================================================ | |
| # VIDEO PROCESSING | |
| # ============================================================================ | |
| def process_video(video_path, source_face, target_face_index, include_hair, progress_fn): | |
| """Process video with face swap""" | |
| if not SWAPPER_LOADED: | |
| raise ValueError("INSwapper not loaded!") | |
| clip = VideoFileClip(video_path) | |
| fps = clip.fps | |
| total_frames = int(clip.duration * fps) | |
| print(f"\nProcessing: {total_frames} frames @ {fps}fps") | |
| if include_hair: | |
| print("HEAD SWAP MODE: Swapping face + hair + ears!") | |
| else: | |
| print("FACE SWAP MODE: Swapping face only") | |
| processed_frames = [] | |
| for i, frame in enumerate(clip.iter_frames()): | |
| frame_bgr = frame[:, :, ::-1] | |
| swapped = swap_face_in_frame( | |
| frame_bgr, | |
| source_face, | |
| target_face_index, | |
| include_hair | |
| ) | |
| swapped_rgb = swapped[:, :, ::-1] | |
| processed_frames.append(swapped_rgb) | |
| if i % 3 == 0: | |
| progress_fn((i + 1) / total_frames, desc=f"Frame {i+1}/{total_frames}") | |
| output_clip = ImageSequenceClip(processed_frames, fps=fps) | |
| if clip.audio is not None: | |
| output_clip = output_clip.set_audio(clip.audio) | |
| output_path = tempfile.mktemp(suffix='.mp4') | |
| output_clip.write_videofile( | |
| output_path, | |
| codec='libx264', | |
| audio_codec='aac', | |
| temp_audiofile=tempfile.mktemp(suffix='.m4a'), | |
| remove_temp=True | |
| ) | |
| clip.close() | |
| return output_path | |
| # ============================================================================ | |
| # GRADIO HANDLERS | |
| # ============================================================================ | |
| state = { | |
| 'source_faces': [], | |
| 'target_faces': [], | |
| 'video_path': None | |
| } | |
| def handle_source_image(image): | |
| if image is None: | |
| return None, "Upload source image", gr.Dropdown(choices=[]) | |
| try: | |
| preview, faces = detect_faces_with_preview(image) | |
| state['source_faces'] = faces | |
| if not faces: | |
| return preview, "β No faces detected", gr.Dropdown(choices=[]) | |
| message = f"β Found {len(faces)} face(s)" | |
| choices = [f"Face {i+1}" for i in range(len(faces))] | |
| return preview, message, gr.Dropdown( | |
| choices=choices, | |
| value=choices[0], | |
| interactive=True | |
| ) | |
| except Exception as e: | |
| return None, f"β Error: {e}", gr.Dropdown(choices=[]) | |
| def handle_target_video(video): | |
| if video is None: | |
| return None, "Upload target video", gr.Dropdown(choices=[]) | |
| try: | |
| state['video_path'] = video | |
| clip = VideoFileClip(video) | |
| frame = clip.get_frame(0) | |
| frame_bgr = frame[:, :, ::-1] | |
| clip.close() | |
| preview, faces = detect_faces_with_preview(frame_bgr) | |
| state['target_faces'] = faces | |
| if not faces: | |
| return preview, "β No faces in video", gr.Dropdown(choices=[]) | |
| message = f"β Found {len(faces)} person(s)" | |
| choices = [f"Person {i+1}" for i in range(len(faces))] | |
| return preview, message, gr.Dropdown( | |
| choices=choices, | |
| value=choices[0], | |
| interactive=True | |
| ) | |
| except Exception as e: | |
| return None, f"β Error: {e}", gr.Dropdown(choices=[]) | |
| def handle_generate(source_choice, target_choice, include_hair, progress=gr.Progress()): | |
| if not SWAPPER_LOADED: | |
| return None, "β INSwapper not loaded!" | |
| if not state['source_faces']: | |
| return None, "β Upload source image first" | |
| if not state['target_faces'] or not state['video_path']: | |
| return None, "β Upload target video first" | |
| try: | |
| source_idx = int(source_choice.split()[1]) - 1 | |
| target_idx = int(target_choice.split()[1]) - 1 | |
| source_face = state['source_faces'][source_idx] | |
| progress(0, desc="Starting...") | |
| result = process_video( | |
| state['video_path'], | |
| source_face, | |
| target_idx, | |
| include_hair, | |
| progress | |
| ) | |
| progress(1.0, desc="Complete!") | |
| status = "β DONE!\n\n" | |
| status += "Applied:\n" | |
| status += "β INSwapper face swap\n" | |
| if include_hair: | |
| status += "β HEAD SWAP (face + hair + ears)\n" | |
| else: | |
| status += "β FACE SWAP (face only)\n" | |
| if CODEFORMER_LOADED: | |
| status += "β CodeFormer restoration\n" | |
| elif GFPGAN_LOADED: | |
| status += "β GFPGAN restoration\n" | |
| return result, status | |
| except Exception as e: | |
| import traceback | |
| return None, f"β Error:\n{e}\n\n{traceback.format_exc()}" | |
| # ============================================================================ | |
| # GRADIO UI | |
| # ============================================================================ | |
| print("\n[7/7] Building interface...") | |
| with gr.Blocks(theme=gr.themes.Soft(), title="Ultimate Face Swap") as demo: | |
| gr.Markdown("# π₯ ULTIMATE FACE SWAP + HEAD SWAP!") | |
| gr.Markdown("### Professional face swapping with enhancement") | |
| if SWAPPER_LOADED: | |
| gr.Markdown("β **INSwapper Loaded**") | |
| else: | |
| gr.Markdown("β **INSwapper Failed**") | |
| if CODEFORMER_LOADED: | |
| gr.Markdown("β **CodeFormer Active**") | |
| elif GFPGAN_LOADED: | |
| gr.Markdown("β **GFPGAN Active**") | |
| else: | |
| gr.Markdown("β οΈ **No restoration available**") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### πΈ Source Image") | |
| source_image = gr.Image(type="numpy", label="Upload Source Face") | |
| source_preview = gr.Image(label="Detected", height=300) | |
| source_status = gr.Textbox(label="Status", lines=2) | |
| source_dropdown = gr.Dropdown(label="Select Face") | |
| with gr.Column(): | |
| gr.Markdown("### π¬ Target Video") | |
| target_video = gr.Video(label="Upload Target Video") | |
| target_preview = gr.Image(label="Detected", height=300) | |
| target_status = gr.Textbox(label="Status", lines=2) | |
| target_dropdown = gr.Dropdown(label="Select Person") | |
| gr.Markdown("### π Generate Video") | |
| head_swap_checkbox = gr.Checkbox( | |
| value=False, | |
| label="π₯ HEAD SWAP MODE (includes hair, ears, neck!)" | |
| ) | |
| generate_button = gr.Button( | |
| "π Generate Video!", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| generation_status = gr.Textbox(label="Status", lines=6) | |
| result_video = gr.Video(label="Result") | |
| # Events | |
| source_image.change( | |
| handle_source_image, | |
| inputs=[source_image], | |
| outputs=[source_preview, source_status, source_dropdown] | |
| ) | |
| target_video.change( | |
| handle_target_video, | |
| inputs=[target_video], | |
| outputs=[target_preview, target_status, target_dropdown] | |
| ) | |
| generate_button.click( | |
| handle_generate, | |
| inputs=[source_dropdown, target_dropdown, head_swap_checkbox], | |
| outputs=[result_video, generation_status] | |
| ) | |
| print("β Interface built") | |
| print("\n" + "="*80) | |
| print("LAUNCHING!") | |
| print("="*80) | |
| demo.queue() | |
| demo.launch() # Removed share=True for Spaces | |
| print("\nβ Running!") | |
| print("="*80) |