swapface / app.py
malazjanbeih's picture
Update app.py
1593184 verified
# ============================================================================
# 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)