| """ |
| 文件路径: FaceSwap/app.py |
| 创建时间: 未知 |
| 上次修改时间: 未知 |
| 开发者: aidaox |
| |
| 项目说明: |
| 这是一个基于Gradio的人脸交换(Face Swapping)Web应用程序。 |
| 项目使用insightface库进行人脸检测和交换,支持多种使用场景: |
| 1. 单张照片的人脸交换 |
| 2. 一个源人脸对应多个目标照片 |
| 3. 多个源人脸对应一个目标照片 |
| 4. 多个源人脸对应多个目标照片 |
| 5. 自定义人脸映射(指定哪个源人脸替换目标照片中的哪个人脸) |
| 6. 视频中的人脸交换(支持单个人脸、所有人脸、自定义映射) |
| 7. 一个源人脸对应多个视频 |
| |
| 技术栈: |
| - Gradio: 用于构建Web界面 |
| - OpenCV (cv2): 图像和视频处理 |
| - NumPy: 数值计算和数组操作 |
| - insightface: 人脸检测和交换的核心库 |
| - FFmpeg: 视频音频处理(通过subprocess调用) |
| """ |
|
|
| |
| import gradio as gr |
| import os |
| import cv2 |
| import numpy as np |
| import shutil |
| import subprocess |
| import time |
| from SinglePhoto import FaceSwapper |
| import argparse |
|
|
| |
| |
| wellcomingMessage = """ |
| <div style="text-align: center; padding: 20px 24px; background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); border-radius: 14px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(79,70,229,0.12); border: none;"> |
| <h1 style="color: #fff; margin: 0; font-size: 2.25rem; font-weight: 600; letter-spacing: -0.02em;">Face Swapping Free</h1> |
| <p style="color: rgba(255,255,255,0.9); font-size: 1rem; margin: 8px 0 0 0;">Photos, videos, batch — one tool</p> |
| </div> |
| """ |
|
|
| |
| |
| swapper = FaceSwapper() |
|
|
| |
|
|
| def swap_single_photo(src_img, src_idx, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)): |
| """ |
| 功能: 单张照片的人脸交换 |
| 这是最基础的功能,将源照片中的一个人脸替换到目标照片中的一个人脸上 |
| |
| 参数说明: |
| src_img: numpy数组,源图像(包含要提取的人脸) |
| src_idx: 整数,源图像中要使用的人脸索引(从1开始,1表示第一张脸) |
| dst_img: numpy数组,目标图像(要被替换人脸的图像) |
| dst_idx: 整数,目标图像中要被替换的人脸索引(从1开始) |
| progress: Gradio进度条对象,用于显示处理进度 |
| |
| 返回值: |
| output_path: 字符串,交换后图像的保存路径 |
| log: 字符串,处理过程的日志信息 |
| |
| 工作流程: |
| 1. 保存源图像和目标图像到临时文件 |
| 2. 调用FaceSwapper进行人脸交换 |
| 3. 保存结果图像 |
| 4. 清理临时文件 |
| 5. 返回结果路径和日志 |
| """ |
| log = "" |
| start_time = time.time() |
| try: |
| |
| progress(0, desc="Preparing files") |
| src_path = "SinglePhoto/data_src.jpg" |
| dst_path = "SinglePhoto/data_dst.jpg" |
| output_path = "SinglePhoto/output_swapped.jpg" |
| |
| os.makedirs(os.path.dirname(src_path), exist_ok=True) |
| os.makedirs(os.path.dirname(dst_path), exist_ok=True) |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) |
| |
| |
| |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| cv2.imwrite(dst_path, dst_img_bgr) |
| log += f"Saved source to {src_path}, destination to {dst_path}\n" |
| |
| |
| 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) |
| log += f"Swapped and saved result to {output_path}\n" |
| |
| |
| progress(0.8, desc="Cleaning up") |
| try: |
| if os.path.exists(src_path): |
| os.remove(src_path) |
| if os.path.exists(dst_path): |
| os.remove(dst_path) |
| log += "Cleaned up temp files.\n" |
| except Exception as cleanup_error: |
| log += f"Cleanup error: {cleanup_error}\n" |
| |
| |
| progress(1, desc="Done") |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return output_path, log |
| except Exception as e: |
| |
| log = f"[FAILED] Processing failed. See details below.\n" + log |
| log += f"Error: {e}\n" |
| progress(1, desc="Error") |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return None, log |
|
|
| def swap_single_src_multi_dst(src_img, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)): |
| """ |
| 功能: 一个源人脸对应多个目标照片 |
| 将同一个源照片中的人脸,分别替换到多个目标照片中 |
| |
| 参数说明: |
| src_img: numpy数组,源图像(包含要提取的人脸) |
| dst_imgs: 图像列表,多个目标图像(Gradio Gallery组件返回的列表) |
| dst_indices: 字符串或列表,每个目标图像中要替换的人脸索引 |
| 例如 "1,1,2" 表示第一个目标用索引1,第二个用索引1,第三个用索引2 |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| results: 列表,所有交换后图像的路径列表 |
| log: 字符串,处理过程的日志信息 |
| |
| 注意: 这个函数在代码中出现了两次(第59行和第205行),可能是重复定义,建议删除其中一个 |
| """ |
| log = "" |
| results = [] |
| |
| src_dir = "SingleSrcMultiDst/src" |
| dst_dir = "SingleSrcMultiDst/dst" |
| output_dir = "SingleSrcMultiDst/output" |
| |
| os.makedirs(src_dir, exist_ok=True) |
| os.makedirs(dst_dir, exist_ok=True) |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| |
| if isinstance(src_img, tuple): |
| src_img = src_img[0] |
| |
| src_path = os.path.join(src_dir, "data_src.jpg") |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| log += f"Saved source image to {src_path}\n" |
| progress(0.05, desc="Saved source image") |
|
|
| |
| if isinstance(dst_indices, str): |
| |
| dst_indices_list = [int(idx.strip()) for idx in dst_indices.split(",") if idx.strip().isdigit()] |
| else: |
| |
| dst_indices_list = [int(idx) for idx in dst_indices] |
|
|
| |
| for j, dst_img in enumerate(dst_imgs): |
| if isinstance(dst_img, tuple): |
| dst_img = dst_img[0] |
| |
| dst_path = os.path.join(dst_dir, f"data_dst_{j}.jpg") |
| output_path = os.path.join(output_dir, f"output_swapped_{j}.jpg") |
| |
| dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(dst_path, dst_img_bgr) |
| log += f"Saved destination image {j} to {dst_path}\n" |
| try: |
| |
| dst_idx = dst_indices_list[j] if j < len(dst_indices_list) else 1 |
| |
| result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx)) |
| cv2.imwrite(output_path, result) |
| results.append(output_path) |
| log += f"Swapped and saved result to {output_path}\n" |
| except Exception as e: |
| |
| results.append(f"Error: {e}") |
| log += f"Error swapping with destination {j}: {e}\n" |
| |
| progress((j + 1) / len(dst_imgs), desc=f"Swapping destination {j+1}/{len(dst_imgs)}") |
| progress(1, desc="Done") |
| return results, log |
|
|
| def swap_multi_src_single_dst(src_imgs, dst_img, dst_idx, progress=gr.Progress(track_tqdm=True)): |
| """ |
| 功能: 多个源人脸对应一个目标照片 |
| 将多个源照片中的人脸,分别替换到同一个目标照片中的同一个人脸上 |
| 每次交换都是独立的,生成多个结果图像 |
| |
| 参数说明: |
| src_imgs: 图像列表,多个源图像(Gradio Gallery组件返回的列表) |
| dst_img: numpy数组,目标图像(要被替换人脸的图像) |
| dst_idx: 整数,目标图像中要被替换的人脸索引(从1开始) |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| results: 列表,所有交换后图像的路径列表 |
| log: 字符串,处理过程的日志信息 |
| |
| 使用场景: 例如,想看看不同人的脸放在同一个目标照片上的效果 |
| """ |
| log = "" |
| results = [] |
| |
| src_dir = "MultiSrcSingleDst/src" |
| dst_dir = "MultiSrcSingleDst/dst" |
| output_dir = "MultiSrcSingleDst/output" |
| |
| os.makedirs(src_dir, exist_ok=True) |
| os.makedirs(dst_dir, exist_ok=True) |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| |
| if isinstance(dst_img, tuple): |
| dst_img = dst_img[0] |
| dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) |
| dst_path = os.path.join(dst_dir, "data_dst.jpg") |
| cv2.imwrite(dst_path, dst_img_bgr) |
| log += f"Saved destination image to {dst_path}\n" |
| progress(0.05, desc="Saved destination image") |
|
|
| |
| for i, src_img in enumerate(src_imgs): |
| if isinstance(src_img, tuple): |
| src_img = src_img[0] |
| |
| src_path = os.path.join(src_dir, f"data_src_{i}.jpg") |
| output_path = os.path.join(output_dir, f"output_swapped_{i}.jpg") |
| |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| log += f"Saved source image {i} to {src_path}\n" |
| try: |
| |
| result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx)) |
| cv2.imwrite(output_path, result) |
| results.append(output_path) |
| log += f"Swapped and saved result to {output_path}\n" |
| except Exception as e: |
| |
| results.append(f"Error: {e}") |
| log += f"Error swapping source {i}: {e}\n" |
| |
| progress((i + 1) / len(src_imgs), desc=f"Swapping source {i+1}/{len(src_imgs)}") |
| progress(1, desc="Done") |
| return results, log |
|
|
| def swap_multi_src_multi_dst(src_imgs, dst_imgs, dst_indices, progress=gr.Progress(track_tqdm=True)): |
| """ |
| 功能: 多个源人脸对应多个目标照片(笛卡尔积) |
| 将每个源图像中的人脸,分别替换到每个目标图像中 |
| 如果有3个源图像和2个目标图像,将生成3×2=6个结果图像 |
| |
| 参数说明: |
| src_imgs: 图像列表,多个源图像 |
| dst_imgs: 图像列表,多个目标图像 |
| dst_indices: 字符串或列表,每个目标图像中要替换的人脸索引 |
| 例如 "1,2" 表示第一个目标用索引1,第二个用索引2 |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| results: 列表,所有交换后图像的路径列表 |
| log: 字符串,处理过程的日志信息 |
| |
| 使用场景: 批量处理,例如有多个人的照片,想分别替换到多个场景中 |
| """ |
| log = "" |
| results = [] |
| |
| src_dir = "MultiSrcMultiDst/src" |
| dst_dir = "MultiSrcMultiDst/dst" |
| output_dir = "MultiSrcMultiDst/output" |
| |
| os.makedirs(src_dir, exist_ok=True) |
| os.makedirs(dst_dir, exist_ok=True) |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| |
| if isinstance(dst_indices, str): |
| |
| dst_indices_list = [int(idx.strip()) for idx in dst_indices.split(",") if idx.strip().isdigit()] |
| else: |
| |
| dst_indices_list = [int(idx) for idx in dst_indices] |
|
|
| |
| total = max(1, len(src_imgs) * len(dst_imgs)) |
| count = 0 |
|
|
| |
| for i, src_img in enumerate(src_imgs): |
| if isinstance(src_img, tuple): |
| src_img = src_img[0] |
| |
| if src_img is None: |
| results.append(f"Error: Source image at index {i} is None") |
| log += f"Source image at index {i} is None\n" |
| continue |
| |
| |
| src_path = os.path.join(src_dir, f"data_src_{i}.jpg") |
| if isinstance(src_img, np.ndarray): |
| |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| log += f"Saved source image {i} to {src_path}\n" |
| elif isinstance(src_img, str) and os.path.exists(src_img): |
| |
| shutil.copy(src_img, src_path) |
| log += f"Copied source image {i} from {src_img} to {src_path}\n" |
| else: |
| |
| results.append(f"Error: Invalid source image at index {i}") |
| log += f"Invalid source image at index {i}\n" |
| continue |
| |
| |
| for j, dst_img in enumerate(dst_imgs): |
| if isinstance(dst_img, tuple): |
| dst_img = dst_img[0] |
| |
| if dst_img is None: |
| results.append(f"Error: Destination image at index {j} is None") |
| log += f"Destination image at index {j} is None\n" |
| continue |
| |
| |
| dst_path = os.path.join(dst_dir, f"data_dst_{j}.jpg") |
| output_path = os.path.join(output_dir, f"output_swapped_{i}_{j}.jpg") |
| |
| |
| if isinstance(dst_img, np.ndarray): |
| dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(dst_path, dst_img_bgr) |
| log += f"Saved destination image {j} to {dst_path}\n" |
| elif isinstance(dst_img, str) and os.path.exists(dst_img): |
| shutil.copy(dst_img, dst_path) |
| log += f"Copied destination image {j} from {dst_img} to {dst_path}\n" |
| else: |
| results.append(f"Error: Invalid destination image at index {j}") |
| log += f"Invalid destination image at index {j}\n" |
| continue |
| |
| |
| try: |
| |
| dst_idx = dst_indices_list[j] if j < len(dst_indices_list) else 1 |
| |
| result = swapper.swap_faces(src_path, 1, dst_path, int(dst_idx)) |
| cv2.imwrite(output_path, result) |
| results.append(output_path) |
| log += f"Swapped src {i} with dst {j} and saved to {output_path}\n" |
| except Exception as e: |
| |
| results.append(f"Error: {e}") |
| log += f"Error swapping src {i} with dst {j}: {e}\n" |
| |
| |
| count += 1 |
| progress(count / total, desc=f"Swapping ({count}/{total})") |
| progress(1, desc="Done") |
| return results, log |
|
|
| def swap_faces_custom(src_imgs, dst_img, mapping_str, progress=gr.Progress(track_tqdm=True)): |
| """ |
| 功能: 自定义人脸映射(一张目标照片,多个源人脸,自定义替换规则) |
| 这是最灵活的功能,可以指定目标照片中的每个人脸分别用哪个源人脸替换 |
| |
| 参数说明: |
| src_imgs: 图像列表,多个源图像(Gradio Gallery组件返回的列表) |
| dst_img: numpy数组,目标图像(要被替换人脸的图像) |
| mapping_str: 字符串,逗号分隔的映射关系 |
| 例如 "2,1,3" 表示: |
| - 目标照片中的第1个人脸用源图像2中的人脸替换 |
| - 目标照片中的第2个人脸用源图像1中的人脸替换 |
| - 目标照片中的第3个人脸用源图像3中的人脸替换 |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| output_path: 字符串,交换后图像的保存路径 |
| log: 字符串,处理过程的日志信息 |
| |
| 使用场景: 例如目标照片中有3个人,想分别用3个不同源照片中的人脸替换 |
| """ |
| log = "" |
| start_time = time.time() |
| |
| dst_path = "CustomSwap/data_dst.jpg" |
| output_path = "CustomSwap/output_swapped.jpg" |
| src_dir = "CustomSwap/src" |
| temp_dir = "CustomSwap/temp" |
| |
| os.makedirs(src_dir, exist_ok=True) |
| os.makedirs(temp_dir, exist_ok=True) |
| os.makedirs(os.path.dirname(dst_path), exist_ok=True) |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) |
|
|
| |
| dst_img_bgr = cv2.cvtColor(dst_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(dst_path, dst_img_bgr) |
| log += f"Saved destination image to {dst_path}\n" |
|
|
| |
| src_paths = [] |
| for i, src_img in enumerate(src_imgs): |
| src_path = os.path.join(src_dir, f"data_src_{i+1}.jpg") |
| if isinstance(src_img, tuple): |
| src_img = src_img[0] |
| |
| if src_img is None: |
| log += f"Source image {i+1} is None, skipping.\n" |
| continue |
| |
| if isinstance(src_img, np.ndarray): |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| src_paths.append(src_path) |
| log += f"Saved source image {i+1} to {src_path}\n" |
| elif isinstance(src_img, str) and os.path.exists(src_img): |
| shutil.copy(src_img, src_path) |
| src_paths.append(src_path) |
| log += f"Copied source image {i+1} from {src_img} to {src_path}\n" |
| else: |
| log += f"Source image {i+1} is not a valid image, skipping.\n" |
|
|
| |
| try: |
| |
| mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()] |
| except Exception as e: |
| log += f"Error parsing mapping: {e}\n" |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return None, log |
|
|
| |
| |
| |
| temp_dst_path = os.path.join(temp_dir, "temp_dst.jpg") |
| shutil.copy(dst_path, temp_dst_path) |
|
|
| |
| |
| for face_idx, src_idx in enumerate(mapping, start=1): |
| |
| |
| |
| |
| if src_idx < 1 or src_idx > len(src_paths): |
| log += f"Invalid source index {src_idx} for face {face_idx}, skipping.\n" |
| continue |
| try: |
| |
| |
| |
| |
| |
| swapped_img = swapper.swap_faces(src_paths[src_idx-1], 1, temp_dst_path, face_idx) |
| cv2.imwrite(temp_dst_path, swapped_img) |
| log += f"Swapped face {face_idx} in destination with source {src_idx}\n" |
| except Exception as e: |
| log += f"Failed to swap face {face_idx} with source {src_idx}: {e}\n" |
|
|
| |
| shutil.copy(temp_dst_path, output_path) |
| log += f"Saved swapped image to {output_path}\n" |
| |
| if os.path.exists(temp_dst_path): |
| os.remove(temp_dst_path) |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return output_path, log |
|
|
| |
|
|
| def add_audio_to_video(original_video_path, video_no_audio_path, output_path): |
| """ |
| 功能: 将原始视频的音频添加到没有音频的视频中 |
| 视频处理过程中,我们只处理视频帧(图像),生成的视频没有音频 |
| 这个函数使用FFmpeg将原始视频的音频合并到新视频中 |
| |
| 参数说明: |
| original_video_path: 字符串,原始视频路径(包含音频) |
| video_no_audio_path: 字符串,没有音频的视频路径(处理后的视频) |
| output_path: 字符串,最终输出视频路径(包含音频) |
| |
| 返回值: |
| (True, "") 或 (False, error_message): 元组,第一个元素表示是否成功,第二个是错误信息 |
| |
| 技术说明: 使用FFmpeg的-map选项选择视频流和音频流进行合并 |
| """ |
| |
| 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, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| return True, "" |
| except subprocess.CalledProcessError as e: |
| |
| return False, e.stderr.decode() |
|
|
| def swap_video(src_img, src_idx, video, dst_idx, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress()): |
| """ |
| 功能: 视频中单个人脸交换 |
| 将源照片中的人脸替换到视频中的指定人脸上 |
| |
| 参数说明: |
| src_img: numpy数组,源图像(包含要提取的人脸) |
| src_idx: 整数,源图像中要使用的人脸索引(从1开始) |
| video: 字符串或视频对象,目标视频路径 |
| dst_idx: 整数,目标视频中要被替换的人脸索引(从1开始) |
| delete_frames_dir: 布尔值,处理完成后是否删除提取的帧目录(节省空间) |
| add_audio: 布尔值,是否将原始视频的音频添加到输出视频中 |
| copy_to_drive: 布尔值,是否复制到Google Drive(用于Colab环境,本地环境可忽略) |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| final_output_path: 字符串,最终输出视频路径 |
| log: 字符串,处理过程的日志信息 |
| |
| 工作流程: |
| 1. 提取视频的所有帧 |
| 2. 对每一帧进行人脸交换 |
| 3. 将交换后的帧重新组合成视频 |
| 4. 可选:添加原始视频的音频 |
| 5. 可选:清理临时文件 |
| """ |
| log = "" |
| start_time = time.time() |
| |
| src_path = "VideoSwapping/data_src.jpg" |
| dst_video_path = "VideoSwapping/data_dst.mp4" |
| frames_dir = "VideoSwapping/video_frames" |
| swapped_dir = "VideoSwapping/swapped_frames" |
| output_video_path = "VideoSwapping/output_tmp_output_video.mp4" |
| final_output_path = "VideoSwapping/output_with_audio.mp4" |
|
|
| |
| os.makedirs(os.path.dirname(src_path), exist_ok=True) |
| os.makedirs(os.path.dirname(dst_video_path), exist_ok=True) |
| os.makedirs(frames_dir, exist_ok=True) |
| os.makedirs(swapped_dir, exist_ok=True) |
|
|
| |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| log += f"Saved source image to {src_path}\n" |
| progress(0.05, desc="Saved source image") |
|
|
| |
| 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 |
|
|
| |
| from VideoSwapping import extract_frames, frames_to_video |
|
|
| |
| frame_paths = extract_frames(dst_video_path, frames_dir) |
| log += f"Extracted {len(frame_paths)} frames to {frames_dir}\n" |
| progress(0.15, desc="Extracted frames") |
|
|
| |
| swapped_files = set(os.listdir(swapped_dir)) |
| total_frames = len(frame_paths) |
| start_loop_time = 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) |
| |
| |
| if swapped_name in swapped_files and os.path.exists(out_path): |
| log += f"Frame {idx}: already swapped, skipping.\n" |
| |
| elapsed = time.time() - start_loop_time |
| avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 |
| remaining = avg_time * (total_frames - (idx + 1)) |
| mins, secs = divmod(int(remaining), 60) |
| progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") |
| continue |
| |
| try: |
| try: |
| |
| swapped = swapper.swap_faces(src_path, int(src_idx), frame_path, int(dst_idx)) |
| except ValueError as ve: |
| |
| if int(dst_idx) != 1 and "Target image contains" in str(ve): |
| swapped = swapper.swap_faces(src_path, int(src_idx), frame_path, 1) |
| log += f"Frame {idx}: dst_idx {dst_idx} not found, used 1 instead.\n" |
| else: |
| raise ve |
| cv2.imwrite(out_path, swapped) |
| log += f"Swapped frame {idx} and saved to {out_path}\n" |
| except Exception as e: |
| |
| cv2.imwrite(out_path, cv2.imread(frame_path)) |
| log += f"Failed to swap frame {idx}: {e}\n" |
| |
| |
| elapsed = time.time() - start_loop_time |
| avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 |
| remaining = avg_time * (total_frames - (idx + 1)) |
| mins, secs = divmod(int(remaining), 60) |
| progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") |
| |
| |
| 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"Combined swapped frames into video {output_video_path}\n" |
| progress(0.8, desc="Muxing audio") |
|
|
| |
| if copy_to_drive: |
| drive_path = "/content/drive/MyDrive/" + os.path.basename(output_video_path) |
| try: |
| shutil.copy(output_video_path, drive_path) |
| log += f"Copied swapped video without audio to Google Drive: {drive_path}\n" |
| except Exception as e: |
| log += f"Failed to copy to Google Drive: {e}\n" |
|
|
| |
| if add_audio: |
| ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) |
| if ok: |
| log += f"Added audio to {final_output_path}\n" |
| else: |
| log += f"Audio muxing failed: {audio_log}\n" |
| final_output_path = output_video_path |
| else: |
| final_output_path = output_video_path |
| log += "Audio was not added as per user request.\n" |
|
|
| |
| try: |
| if os.path.exists(src_path): |
| os.remove(src_path) |
| if os.path.exists(dst_video_path): |
| os.remove(dst_video_path) |
| if delete_frames_dir and os.path.exists(frames_dir): |
| shutil.rmtree(frames_dir) |
| log += "Deleted video_frames directory.\n" |
| elif not delete_frames_dir: |
| log += "Kept video_frames directory as requested.\n" |
| if os.path.exists(swapped_dir): |
| shutil.rmtree(swapped_dir) |
| log += "Cleaned up temp files and folders.\n" |
| except Exception as cleanup_error: |
| log += f"Cleanup error: {cleanup_error}\n" |
| |
| progress(1, desc="Done") |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return final_output_path, log |
|
|
| def swap_video_all_faces(src_img, video, num_faces_to_swap, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress()): |
| """ |
| 功能: 视频中所有人脸交换 |
| 将源照片中的人脸替换到视频中的前N个人脸上(N由num_faces_to_swap指定) |
| |
| 参数说明: |
| src_img: numpy数组,源图像(包含要提取的人脸) |
| video: 字符串或视频对象,目标视频路径 |
| num_faces_to_swap: 整数,要交换的人脸数量(从第1个到第N个) |
| delete_frames_dir: 布尔值,处理完成后是否删除提取的帧目录 |
| add_audio: 布尔值,是否将原始视频的音频添加到输出视频中 |
| copy_to_drive: 布尔值,是否复制到Google Drive |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| final_output_path: 字符串,最终输出视频路径 |
| log: 字符串,处理过程的日志信息 |
| |
| 使用场景: 例如视频中有多个人,想用同一个源人脸替换所有人 |
| """ |
| log = "" |
| start_time = time.time() |
| src_path = "VideoSwappingAllFaces/data_src.jpg" |
| dst_video_path = "VideoSwappingAllFaces/data_dst.mp4" |
| frames_dir = "VideoSwappingAllFaces/video_frames" |
| swapped_dir = "VideoSwappingAllFaces/swapped_frames" |
| output_video_path = "VideoSwappingAllFaces/output_tmp_output_video.mp4" |
| final_output_path = "VideoSwappingAllFaces/output_with_audio.mp4" |
|
|
| os.makedirs(os.path.dirname(src_path), exist_ok=True) |
| os.makedirs(os.path.dirname(dst_video_path), exist_ok=True) |
| os.makedirs(frames_dir, exist_ok=True) |
| os.makedirs(swapped_dir, exist_ok=True) |
|
|
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| log += f"Saved source image to {src_path}\n" |
| progress(0.05, desc="Saved source image") |
|
|
| 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 |
|
|
| from VideoSwapping import extract_frames, frames_to_video |
|
|
| frame_paths = extract_frames(dst_video_path, frames_dir) |
| log += f"Extracted {len(frame_paths)} frames to {frames_dir}\n" |
| progress(0.15, desc="Extracted frames") |
|
|
| swapped_files = set(os.listdir(swapped_dir)) |
| temp_dir = os.path.join(swapped_dir, "temp_swap") |
| os.makedirs(temp_dir, exist_ok=True) |
| total_frames = len(frame_paths) |
| start_loop_time = 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) |
| temp_frame_path = os.path.join(temp_dir, "temp.jpg") |
| if swapped_name in swapped_files and os.path.exists(out_path): |
| log += f"Frame {idx}: already swapped, skipping.\n" |
| elapsed = time.time() - start_loop_time |
| avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 |
| remaining = avg_time * (total_frames - (idx + 1)) |
| mins, secs = divmod(int(remaining), 60) |
| progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") |
| continue |
| try: |
| |
| shutil.copy(frame_path, temp_frame_path) |
| |
| 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) |
| except Exception as e: |
| log += f"Failed to swap face {face_idx} in frame {idx}: {e}\n" |
| |
| shutil.copy(temp_frame_path, out_path) |
| log += f"Swapped all faces in frame {idx} and saved to {out_path}\n" |
| |
| if os.path.exists(temp_frame_path): |
| os.remove(temp_frame_path) |
| except Exception as e: |
| cv2.imwrite(out_path, cv2.imread(frame_path)) |
| log += f"Failed to swap frame {idx}: {e}\n" |
| elapsed = time.time() - start_loop_time |
| avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 |
| remaining = avg_time * (total_frames - (idx + 1)) |
| mins, secs = divmod(int(remaining), 60) |
| progress(0.15 + 0.6 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") |
| if os.path.exists(temp_dir): |
| shutil.rmtree(temp_dir) |
|
|
| 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"Combined swapped frames into video {output_video_path}\n" |
| progress(0.8, desc="Muxing audio") |
|
|
| |
| if copy_to_drive: |
| drive_path = "/content/drive/MyDrive/" + os.path.basename(output_video_path) |
| try: |
| shutil.copy(output_video_path, drive_path) |
| log += f"Copied swapped video without audio to Google Drive: {drive_path}\n" |
| except Exception as e: |
| log += f"Failed to copy to Google Drive: {e}\n" |
|
|
| if add_audio: |
| ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) |
| if ok: |
| log += f"Added audio to {final_output_path}\n" |
| else: |
| log += f"Audio muxing failed: {audio_log}\n" |
| final_output_path = output_video_path |
| else: |
| final_output_path = output_video_path |
| log += "Audio was not added as per user request.\n" |
|
|
| try: |
| if os.path.exists(src_path): |
| os.remove(src_path) |
| if os.path.exists(dst_video_path): |
| os.remove(dst_video_path) |
| if delete_frames_dir and os.path.exists(frames_dir): |
| shutil.rmtree(frames_dir) |
| log += "Deleted video_frames directory.\n" |
| elif not delete_frames_dir: |
| log += "Kept video_frames directory as requested.\n" |
| if os.path.exists(swapped_dir): |
| shutil.rmtree(swapped_dir) |
| log += "Cleaned up temp files and folders.\n" |
| except Exception as cleanup_error: |
| log += f"Cleanup error: {cleanup_error}\n" |
| progress(1, desc="Done") |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return final_output_path, log |
|
|
| def swap_video_custom_mapping(src_imgs, video, mapping_str, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress()): |
| log = "" |
| start_time = time.time() |
| src_dir = "CustomVideoSwap/src" |
| temp_dir = "CustomVideoSwap/temp" |
| frames_dir = "CustomVideoSwap/frames" |
| swapped_dir = "CustomVideoSwap/swapped_frames" |
| output_video_path = "CustomVideoSwap/output_tmp_output_video.mp4" |
| final_output_path = "CustomVideoSwap/output_with_audio.mp4" |
| dst_video_path = "CustomVideoSwap/data_dst.mp4" |
|
|
| os.makedirs(src_dir, exist_ok=True) |
| os.makedirs(temp_dir, exist_ok=True) |
| os.makedirs(frames_dir, exist_ok=True) |
| os.makedirs(swapped_dir, exist_ok=True) |
|
|
| |
| src_paths = [] |
| for i, src_img in enumerate(src_imgs): |
| src_path = os.path.join(src_dir, f"data_src_{i+1}.jpg") |
| if isinstance(src_img, tuple): |
| src_img = src_img[0] |
| if src_img is None: |
| log += f"Source image {i+1} is None, skipping.\n" |
| continue |
| if isinstance(src_img, np.ndarray): |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| src_paths.append(src_path) |
| log += f"Saved source image {i+1} to {src_path}\n" |
| elif isinstance(src_img, str) and os.path.exists(src_img): |
| shutil.copy(src_img, src_path) |
| src_paths.append(src_path) |
| log += f"Copied source image {i+1} from {src_img} to {src_path}\n" |
| else: |
| log += f"Source image {i+1} is not a valid image, skipping.\n" |
|
|
| |
| try: |
| mapping = [int(x.strip()) for x in mapping_str.split(",") if x.strip().isdigit()] |
| except Exception as e: |
| log += f"Error parsing mapping: {e}\n" |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return None, log |
|
|
| |
| 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 |
|
|
| from VideoSwapping import extract_frames, frames_to_video |
|
|
| frame_paths = extract_frames(dst_video_path, frames_dir) |
| log += f"Extracted {len(frame_paths)} frames to {frames_dir}\n" |
| progress(0.1, desc="Extracted frames") |
|
|
| swapped_files = set(os.listdir(swapped_dir)) |
| temp_frame_path = os.path.join(temp_dir, "temp.jpg") |
| total_frames = len(frame_paths) |
| start_loop_time = 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) |
| if swapped_name in swapped_files and os.path.exists(out_path): |
| log += f"Frame {idx}: already swapped, skipping.\n" |
| elapsed = time.time() - start_loop_time |
| avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 |
| remaining = avg_time * (total_frames - (idx + 1)) |
| mins, secs = divmod(int(remaining), 60) |
| progress(0.1 + 0.7 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") |
| continue |
| try: |
| |
| shutil.copy(frame_path, temp_frame_path) |
| |
| |
| for face_idx, src_idx in enumerate(mapping, start=1): |
| |
| |
| |
| |
| if src_idx < 1 or src_idx > len(src_paths): |
| log += f"Invalid source index {src_idx} for face {face_idx} in frame {idx}, skipping.\n" |
| continue |
| try: |
| |
| |
| |
| |
| |
| swapped_img = swapper.swap_faces(src_paths[src_idx-1], 1, temp_frame_path, face_idx) |
| cv2.imwrite(temp_frame_path, swapped_img) |
| log += f"Frame {idx}: Swapped face {face_idx} with source {src_idx}\n" |
| except Exception as e: |
| log += f"Frame {idx}: Failed to swap face {face_idx} with source {src_idx}: {e}\n" |
| |
| shutil.copy(temp_frame_path, out_path) |
| log += f"Swapped all faces in frame {idx} and saved to {out_path}\n" |
| |
| if os.path.exists(temp_frame_path): |
| os.remove(temp_frame_path) |
| except Exception as e: |
| cv2.imwrite(out_path, cv2.imread(frame_path)) |
| log += f"Failed to swap frame {idx}: {e}\n" |
| elapsed = time.time() - start_loop_time |
| avg_time = elapsed / (idx + 1) if idx + 1 > 0 else 0 |
| remaining = avg_time * (total_frames - (idx + 1)) |
| mins, secs = divmod(int(remaining), 60) |
| progress(0.1 + 0.7 * (idx + 1) / total_frames, desc=f"Swapping {idx+1}/{total_frames} | {mins:02d}:{secs:02d} left") |
| if os.path.exists(temp_dir): |
| shutil.rmtree(temp_dir) |
|
|
| 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"Combined swapped frames into video {output_video_path}\n" |
| progress(0.9, desc="Muxing audio") |
|
|
| if add_audio: |
| ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, final_output_path) |
| if ok: |
| log += f"Added audio to {final_output_path}\n" |
| else: |
| log += f"Audio muxing failed: {audio_log}\n" |
| final_output_path = output_video_path |
| else: |
| final_output_path = output_video_path |
| log += "Audio was not added as per user request.\n" |
|
|
| try: |
| if os.path.exists(dst_video_path): |
| os.remove(dst_video_path) |
| if delete_frames_dir and os.path.exists(frames_dir): |
| shutil.rmtree(frames_dir) |
| log += "Deleted video_frames directory.\n" |
| elif not delete_frames_dir: |
| log += "Kept video_frames directory as requested.\n" |
| if os.path.exists(swapped_dir): |
| shutil.rmtree(swapped_dir) |
| log += "Cleaned up temp files and folders.\n" |
| except Exception as cleanup_error: |
| log += f"Cleanup error: {cleanup_error}\n" |
| progress(1, desc="Done") |
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return final_output_path, log |
|
|
| def swap_single_src_multi_video(src_img, dst_videos, dst_indices, delete_frames_dir=True, add_audio=True, copy_to_drive=False, progress=gr.Progress(track_tqdm=True)): |
| """ |
| 功能: 一个源人脸对应多个视频 |
| 将同一个源照片中的人脸,分别替换到多个视频中的指定人脸上 |
| 每个视频依次处理,生成多个输出视频 |
| |
| 参数说明: |
| src_img: numpy数组,源图像(包含要提取的人脸) |
| dst_videos: 视频列表,多个目标视频(Gradio File组件返回的列表) |
| dst_indices: 字符串或列表,每个视频中要替换的人脸索引 |
| 例如 "1,2,1" 表示第一个视频用索引1,第二个用索引2,第三个用索引1 |
| delete_frames_dir: 布尔值,处理完成后是否删除提取的帧目录 |
| add_audio: 布尔值,是否将原始视频的音频添加到输出视频中 |
| copy_to_drive: 布尔值,是否复制到Google Drive |
| progress: Gradio进度条对象 |
| |
| 返回值: |
| results: 列表,所有输出视频的路径列表 |
| log: 字符串,处理过程的日志信息 |
| |
| 使用场景: 例如有一个人的照片,想替换到多个不同的视频中 |
| """ |
| log = "" |
| results = [] |
| start_time = time.time() |
| base_dir = "SingleSrcMultiVideo" |
| dst_dir = os.path.join(base_dir, "dst") |
| frames_dir = os.path.join(base_dir, "video_frames") |
| swapped_dir = os.path.join(base_dir, "swap_frames") |
| output_dir = os.path.join(base_dir, "output") |
| os.makedirs(dst_dir, exist_ok=True) |
| os.makedirs(frames_dir, exist_ok=True) |
| os.makedirs(swapped_dir, exist_ok=True) |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| |
| if isinstance(dst_indices, str): |
| dst_indices_list = [int(idx.strip()) for idx in dst_indices.split(",") if idx.strip().isdigit()] |
| else: |
| dst_indices_list = [int(idx) for idx in dst_indices] |
|
|
| |
| src_path = os.path.join(base_dir, "data_src.jpg") |
| src_img_bgr = cv2.cvtColor(src_img, cv2.COLOR_RGB2BGR) |
| cv2.imwrite(src_path, src_img_bgr) |
| log += f"Saved source image to {src_path}\n" |
|
|
| from VideoSwapping import extract_frames, frames_to_video |
|
|
| for i, video in enumerate(dst_videos): |
| dst_idx = dst_indices_list[i] if i < len(dst_indices_list) else 1 |
| video_name = f"video_{i}.mp4" |
| dst_video_path = os.path.join(dst_dir, video_name) |
| output_video_path = os.path.join(output_dir, f"output_{i}.mp4") |
| output_video_with_audio = os.path.join(output_dir, f"output_with_audio_{i}.mp4") |
|
|
| |
| if isinstance(video, str) and os.path.exists(video): |
| shutil.copy(video, dst_video_path) |
| log += f"Copied video {video} to {dst_video_path}\n" |
| else: |
| dst_video_path = video |
|
|
| |
| frame_paths = extract_frames(dst_video_path, frames_dir) |
| log += f"Extracted {len(frame_paths)} frames from {dst_video_path} to {frames_dir}\n" |
| progress(i / len(dst_videos), desc=f"Processing video {i+1}/{len(dst_videos)}") |
|
|
| swapped_files = set(os.listdir(swapped_dir)) |
| total_frames = len(frame_paths) |
| temp_frame_path = os.path.join(swapped_dir, "temp.jpg") |
| start_loop_time = 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) |
| if swapped_name in swapped_files and os.path.exists(out_path): |
| continue |
| try: |
| shutil.copy(frame_path, temp_frame_path) |
| try: |
| swapped_img = swapper.swap_faces(src_path, 1, temp_frame_path, int(dst_idx)) |
| cv2.imwrite(temp_frame_path, swapped_img) |
| except Exception as e: |
| log += f"Failed to swap face in frame {idx} of video {i}: {e}\n" |
| shutil.copy(temp_frame_path, out_path) |
| if os.path.exists(temp_frame_path): |
| os.remove(temp_frame_path) |
| except Exception as e: |
| cv2.imwrite(out_path, cv2.imread(frame_path)) |
| log += f"Failed to swap frame {idx} of video {i}: {e}\n" |
|
|
| |
| 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"Combined swapped frames into video {output_video_path}\n" |
|
|
| |
| if copy_to_drive: |
| drive_path = "/content/drive/MyDrive/" + os.path.basename(output_video_path) |
| try: |
| shutil.copy(output_video_path, drive_path) |
| log += f"Copied swapped video without audio to Google Drive: {drive_path}\n" |
| except Exception as e: |
| log += f"Failed to copy to Google Drive: {e}\n" |
|
|
| |
| if add_audio: |
| ok, audio_log = add_audio_to_video(dst_video_path, output_video_path, output_video_with_audio) |
| if ok: |
| log += f"Added audio to {output_video_with_audio}\n" |
| results.append(output_video_with_audio) |
| else: |
| log += f"Audio muxing failed for video {i}: {audio_log}\n" |
| results.append(output_video_path) |
| else: |
| results.append(output_video_path) |
| log += f"Audio was not added for video {i} as per user request.\n" |
|
|
| |
| if delete_frames_dir and os.path.exists(frames_dir): |
| shutil.rmtree(frames_dir) |
| os.makedirs(frames_dir, exist_ok=True) |
| log += f"Deleted video_frames directory after video {i}.\n" |
| elif not delete_frames_dir: |
| log += f"Kept video_frames directory after video {i} as requested.\n" |
| if os.path.exists(swapped_dir): |
| shutil.rmtree(swapped_dir) |
| os.makedirs(swapped_dir, exist_ok=True) |
| log += f"Cleared swap_frames directory after video {i}.\n" |
|
|
| elapsed = time.time() - start_time |
| log += f"Elapsed time: {elapsed:.2f} seconds\n" |
| return results, log |
|
|
|
|
|
|
| |
| custom_css = """ |
| footer {display: none !important;} |
| .gradio-container footer {display: none !important;} |
| #footer {display: none !important;} |
| .gradio-footer {display: none !important;} |
| footer.absolute {display: none !important;} |
| [data-testid="footer"] {display: none !important;} |
| .gradio-container { margin: 0 auto; padding: 1.25rem 1.5rem; box-sizing: border-box; background: #fafaf9; min-height: 100vh; } |
| .gradio-container, .gradio-container > div { width: 100%; max-width: 100%; box-sizing: border-box; } |
| .gr-block { border-radius: 14px; box-sizing: border-box; } |
| .input-panel, .output-panel { background: #ffffff; border-radius: 14px; padding: 1.25rem; border: 1px solid #e7e5e4; box-shadow: 0 1px 2px rgba(0,0,0,0.04); } |
| .gr-row { gap: 1.5rem !important; } |
| button.primary { border-radius: 10px; font-weight: 600; box-shadow: 0 1px 2px rgba(79,70,229,0.15); } |
| button.primary:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(79,70,229,0.2); } |
| .small-text { font-size: 0.875rem; color: #57534e; } |
| label, .label { color: #44403c !important; } |
| button[aria-selected="true"], .tabs button.selected { border-bottom: 2px solid #4f46e5; font-weight: 600; } |
| @media (max-width: 768px) { |
| .input-panel, .output-panel { padding: 1rem; } |
| button.primary { min-height: 44px; } |
| } |
| @media (min-width: 1220px) { |
| .gradio-container { width: 1200px !important; max-width: 1200px !important; min-width: 1200px !important; } |
| .gradio-container > div { width: 100% !important; min-width: 100% !important; } |
| } |
| """ |
|
|
|
|
|
|
|
|
| _clarity_script = '<script type="text/javascript">(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,"clarity","script","umy0o631ds");</script>' |
| with gr.Blocks(title="Face Swapping Free", css=custom_css, theme=gr.themes.Soft(primary_hue="indigo")) as demo: |
| gr.HTML(_clarity_script) |
| gr.Markdown(wellcomingMessage) |
| |
| |
| with gr.Tab("📷 Single Photo Swapping", id="single_photo"): |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| - **Source Image**: Image containing the face to extract |
| - **Source Face Index**: Which face to use if multiple (from 1) |
| - **Destination Image**: Image where the face will be replaced |
| - **Destination Face Index**: Which face in destination to replace (from 1) |
| - Click **Start Swapping** to run |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| gr.Markdown("*Upload images with clear, front-facing faces for best results.*", elem_classes=["small-text"]) |
| src_img = gr.Image(label="Source Image", type="numpy", height=280) |
| src_idx = gr.Number(value=1, label="Source Face Index", minimum=1, precision=0, |
| info="If the image contains multiple faces, specify which one to use (starting from 1)") |
| dst_img = gr.Image(label="Destination Image", type="numpy", height=300) |
| dst_idx = gr.Number(value=1, label="Destination Face Index", minimum=1, precision=0, |
| info="Specify which face in the destination image to replace") |
| gr.Markdown("*Upload both images first, then click Start. Face index starts at 1 (use 1 for single face).*", elem_classes=["small-text"]) |
| submit_btn = gr.Button("🚀 Start Swapping", variant="primary", size="lg") |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| gr.Markdown("*Result will appear here after processing. You can download directly from the preview.*", elem_classes=["small-text"]) |
| output_img = gr.Image(label="Swapped Image", height=360) |
| log_output = gr.Textbox(label="Log Output", lines=6, interactive=False, |
| placeholder="Processing logs will appear here...") |
| submit_btn.click( |
| fn=swap_single_photo, |
| inputs=[src_img, src_idx, dst_img, dst_idx], |
| outputs=[output_img, log_output] |
| ) |
| |
| |
| with gr.Tab("🖼️ Single Source Multi-Destination", id="single_src_multi_dst"): |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Replace the same source face into multiple destination images. Perfect for batch processing. |
| - **Source Image**: Image containing the face to extract |
| - **Destination Images**: Upload multiple images (Gallery supports multiple selection) |
| - **Destination Face Indices**: Comma-separated, e.g. "1,1,2" = index 1 for 1st & 2nd image, index 2 for 3rd |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_img_multi = gr.Image(label="Source Image", type="numpy", height=250) |
| dst_imgs = gr.Gallery(label="Destination Images", type="numpy", columns=2, |
| height=300, show_label=True) |
| dst_indices_multi = gr.Textbox( |
| label="Destination Face Indices", |
| placeholder="e.g., 1,1,2", |
| info="Comma-separated values, corresponding to each destination image's face index" |
| ) |
| submit_multi_btn = gr.Button("🚀 Start Batch Processing", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_gallery = gr.Gallery(label="Swapped Images", columns=2, height=400) |
| log_multi = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_multi_btn.click( |
| fn=swap_single_src_multi_dst, |
| inputs=[src_img_multi, dst_imgs, dst_indices_multi], |
| outputs=[output_gallery, log_multi] |
| ) |
| |
| |
| with gr.Tab("👥 Multi-Source Single Destination", id="multi_src_single_dst"): |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Replace different faces from multiple sources into the same destination image. |
| - **Source Images**: Upload multiple images with different faces |
| - **Destination Image**: One destination image |
| - **Destination Face Index**: Which face in destination to replace (from 1) |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_imgs_multi = gr.Gallery(label="Source Images", type="numpy", columns=2, height=250) |
| dst_img_single = gr.Image(label="Destination Image", type="numpy", height=250) |
| dst_idx_single = gr.Number(value=1, label="Destination Face Index", |
| minimum=1, precision=0) |
| submit_multi_src_btn = gr.Button("🚀 Start Processing", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_gallery_multi = gr.Gallery(label="Swapped Images", columns=2, height=400) |
| log_multi_src = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_multi_src_btn.click( |
| fn=swap_multi_src_single_dst, |
| inputs=[src_imgs_multi, dst_img_single, dst_idx_single], |
| outputs=[output_gallery_multi, log_multi_src] |
| ) |
| |
| |
| with gr.Tab("🔄 Multi-Source Multi-Destination", id="multi_src_multi_dst"): |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Batch: each source face × each destination image (Cartesian product). E.g. 3 sources × 2 destinations = 6 results. |
| - **Source Images** / **Destination Images**: Upload multiple |
| - **Destination Face Indices**: Comma-separated, one index per destination image |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_imgs_cartesian = gr.Gallery(label="Source Images", type="numpy", columns=2, height=200) |
| dst_imgs_cartesian = gr.Gallery(label="Destination Images", type="numpy", columns=2, height=200) |
| dst_indices_cartesian = gr.Textbox( |
| label="Destination Face Indices", |
| placeholder="e.g., 1,1,2", |
| info="Comma-separated values, corresponding to each destination image" |
| ) |
| submit_cartesian_btn = gr.Button("🚀 Start Batch Processing", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_gallery_cartesian = gr.Gallery(label="Swapped Images", columns=3, height=400) |
| log_cartesian = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_cartesian_btn.click( |
| fn=swap_multi_src_multi_dst, |
| inputs=[src_imgs_cartesian, dst_imgs_cartesian, dst_indices_cartesian], |
| outputs=[output_gallery_cartesian, log_cartesian] |
| ) |
| |
| |
| with gr.Tab("🎯 Custom Face Mapping", id="custom_mapping"): |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Specify which source face replaces each face in the destination. E.g. mapping "2,1,3": 1st dest face → source 2, 2nd → source 1, 3rd → source 3. |
| - **Source Images** / **Destination Image**: Upload; destination can have multiple faces |
| - **Mapping**: Comma-separated, one number per face in destination (numbers = source image index from 1) |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_imgs_custom = gr.Gallery(label="Source Images", type="numpy", columns=2, height=250) |
| dst_img_custom = gr.Image(label="Destination Image", type="numpy", height=250) |
| mapping_str = gr.Textbox( |
| label="Mapping", |
| placeholder="e.g., 2,1,3", |
| info="Comma-separated values, numbers correspond to source image indices (starting from 1)" |
| ) |
| submit_custom_btn = gr.Button("🚀 Start Processing", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_img_custom = gr.Image(label="Swapped Image", height=400) |
| log_custom = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_custom_btn.click( |
| fn=swap_faces_custom, |
| inputs=[src_imgs_custom, dst_img_custom, mapping_str], |
| outputs=[output_img_custom, log_custom] |
| ) |
| |
| with gr.Tab("🎬 Video Face Swapping", id="video_swapping"): |
| gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Replace the source face into a specific face in the target video. |
| - **Source Image** / **Target Video**: Upload; **Face Indices**: which face in source and in video (from 1) |
| - **Processing Options**: Keep audio, delete temp frames, etc. |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_img_video = gr.Image(label="Source Image", type="numpy", height=200) |
| src_idx_video = gr.Number(value=1, label="Source Face Index", minimum=1, precision=0) |
| video_input = gr.Video(label="Target Video", height=200) |
| dst_idx_video = gr.Number(value=1, label="Destination Face Index", minimum=1, precision=0) |
| |
| gr.Markdown("### ⚙️ Processing Options") |
| delete_frames = gr.Checkbox(label="Delete temporary frame files after processing", value=True) |
| add_audio = gr.Checkbox(label="Keep original video audio", value=True) |
| copy_to_drive = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) |
| |
| submit_video_btn = gr.Button("🚀 Start Processing Video", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_video = gr.Video(label="Swapped Video", height=400) |
| log_video = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_video_btn.click( |
| fn=swap_video, |
| inputs=[src_img_video, src_idx_video, video_input, dst_idx_video, delete_frames, add_audio, copy_to_drive], |
| outputs=[output_video, log_video] |
| ) |
| |
| |
| with gr.Tab("👥 Video All Faces Swapping", id="video_all_faces"): |
| gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Replace the same source face into the first N faces in the video. |
| - **Source Image** / **Target Video**: Upload |
| - **Number of Faces**: How many faces to replace (from 1st face) |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_img_all = gr.Image(label="Source Image", type="numpy", height=200) |
| video_all = gr.Video(label="Target Video", height=200) |
| num_faces = gr.Number(value=1, label="Number of Faces to Swap", |
| minimum=1, precision=0, info="Starting from the 1st face, replace the first N faces") |
| |
| gr.Markdown("### ⚙️ Processing Options") |
| delete_frames_all = gr.Checkbox(label="Delete temporary frame files after processing", value=True) |
| add_audio_all = gr.Checkbox(label="Keep original video audio", value=True) |
| copy_to_drive_all = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) |
| |
| submit_all_btn = gr.Button("🚀 Start Processing Video", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_video_all = gr.Video(label="Swapped Video", height=400) |
| log_all = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_all_btn.click( |
| fn=swap_video_all_faces, |
| inputs=[src_img_all, video_all, num_faces, delete_frames_all, add_audio_all, copy_to_drive_all], |
| outputs=[output_video_all, log_all] |
| ) |
| |
| |
| with gr.Tab("🎯 Video Custom Face Mapping", id="video_custom_mapping"): |
| gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Specify which source face replaces each face in the video. E.g. mapping "2,1,3": 1st video face → source 2, 2nd → source 1, 3rd → source 3. |
| - **Source Images** / **Target Video**: Upload |
| - **Mapping**: Comma-separated, one number per face in video (numbers = source image index from 1) |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_imgs_video_custom = gr.Gallery(label="Source Images", type="numpy", columns=2, height=200) |
| video_custom = gr.Video(label="Target Video", height=200) |
| mapping_video = gr.Textbox( |
| label="Mapping", |
| placeholder="e.g., 2,1,3", |
| info="Comma-separated values, corresponding to source image indices for each face in the video" |
| ) |
| |
| gr.Markdown("### ⚙️ Processing Options") |
| delete_frames_custom = gr.Checkbox(label="Delete temporary frame files after processing", value=True) |
| add_audio_custom = gr.Checkbox(label="Keep original video audio", value=True) |
| copy_to_drive_custom = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) |
| |
| submit_video_custom_btn = gr.Button("🚀 Start Processing Video", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_video_custom = gr.Video(label="Swapped Video", height=400) |
| log_video_custom = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_video_custom_btn.click( |
| fn=swap_video_custom_mapping, |
| inputs=[src_imgs_video_custom, video_custom, mapping_video, delete_frames_custom, add_audio_custom, copy_to_drive_custom], |
| outputs=[output_video_custom, log_video_custom] |
| ) |
| |
| |
| with gr.Tab("📹 Single Source Multi-Video", id="single_src_multi_video"): |
| gr.Markdown("*⏱ Video processing: ~1–2 min per minute of video. Please wait in queue and do not close the page.*", elem_classes=["small-text"]) |
| with gr.Accordion("💡 Usage Instructions", open=False): |
| gr.Markdown(""" |
| Batch: replace the same source face into multiple videos. |
| - **Source Image** / **Target Videos**: Upload (multiple videos supported) |
| - **Destination Face Indices**: Comma-separated, one face index per video |
| """) |
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["input-panel"]): |
| gr.Markdown("### 📥 Input") |
| src_img_multi_video = gr.Image(label="Source Image", type="numpy", height=200) |
| videos_multi = gr.File( |
| label="Target Videos", |
| file_count="multiple", |
| type="filepath", |
| file_types=["video"] |
| ) |
| gr.Markdown("*💡 You can upload multiple video files*", elem_classes="small-text") |
| dst_indices_multi_video = gr.Textbox( |
| label="Destination Face Indices", |
| placeholder="e.g., 1,2,1", |
| info="Comma-separated values, corresponding to each video's face index" |
| ) |
| |
| gr.Markdown("### ⚙️ Processing Options") |
| delete_frames_multi_video = gr.Checkbox(label="Delete temporary frame files after processing", value=True) |
| add_audio_multi_video = gr.Checkbox(label="Keep original video audio", value=True) |
| copy_to_drive_multi_video = gr.Checkbox(label="Copy to Google Drive (Colab environment only)", value=False) |
| |
| submit_multi_video_btn = gr.Button("🚀 Start Batch Processing", variant="primary", size="lg") |
| |
| with gr.Column(scale=1, elem_classes=["output-panel"]): |
| gr.Markdown("### 📤 Output") |
| output_videos_gallery = gr.Gallery(label="Swapped Videos", type="filepath", columns=2, height=400) |
| log_multi_video = gr.Textbox(label="Log Output", lines=10, interactive=False) |
| |
| submit_multi_video_btn.click( |
| fn=swap_single_src_multi_video, |
| inputs=[src_img_multi_video, videos_multi, dst_indices_multi_video, delete_frames_multi_video, add_audio_multi_video, copy_to_drive_multi_video], |
| outputs=[output_videos_gallery, log_multi_video] |
| ) |
| |
| gr.Markdown(""" |
| <div style="text-align: center; padding: 16px; margin-top: 16px; border-top: 1px solid #e7e5e4; color: #57534e; font-size: 0.875rem;"> |
| <p style="margin: 0;">💡 Processing time depends on size and complexity; video may take a while.</p> |
| <p style="margin: 6px 0 0 0;">Face Swapping Free · InsightFace & Gradio</p> |
| </div> |
| """) |
| |
| |
| demo.queue(max_size=10) |
| |
|
|
| |
| if __name__ == "__main__": |
| |
| parser = argparse.ArgumentParser(description="Face Swapping Web Application") |
| |
| parser.add_argument("--share", action="store_true", help="Launch Gradio with share=True (creates public link)") |
| args = parser.parse_args() |
| |
| |
| |
| |
| |
| |
| |
| |
| demo.launch( |
| share=args.share, |
| |
| |
| server_port=7860, |
| show_error=True |
| ) |
|
|