I2V-VIP / face_swapper.py
dayona's picture
enforce
cdefade
Raw
History Blame Contribute Delete
10.7 kB
import os
import cv2
import numpy as np
from PIL import Image
from huggingface_hub import hf_hub_download
from tqdm import tqdm
_app = None
_swapper = None
def get_swapper_models():
global _app, _swapper
if _app is None or _swapper is None:
try:
import insightface
from insightface.app import FaceAnalysis
print("Initializing InsightFace CPU model (det_size=320)...")
_app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider'])
_app.prepare(ctx_id=-1, det_size=(320, 320))
# Download inswapper_128.onnx from HF Hub
hf_token = os.environ.get("HF_TOKEN") or True
try:
model_path = hf_hub_download(
repo_id="ezioruan/inswapper_128.onnx",
filename="inswapper_128.onnx",
token=hf_token
)
except Exception as dl_err:
print(f"Primary repo download notice: {dl_err}. Trying fallback...")
model_path = hf_hub_download(
repo_id="Gourieff/ReActor",
filename="models/inswapper_128.onnx",
repo_type="dataset",
token=hf_token
)
_swapper = insightface.model_zoo.get_model(model_path, providers=['CPUExecutionProvider'])
print("✅ InsightFace CPU Swapper loaded successfully!")
except Exception as e:
print(f"⚠️ Face Swapper load warning: {e}")
_app = None
_swapper = None
return _app, _swapper
def swap_face_in_frames(
source_pil_image: Image.Image,
frames_np: list,
ref_face_image: Image.Image = None,
target_gender: str = "Any / All Faces",
swap_last_n: int = 4,
progress=None
) -> list:
"""
Swaps face from ref_face_image (or source_pil_image) into video frames using InsightFace CPU.
Supports swap_last_n frames (0 = All Frames). If swap_last_n > total_frames, falls back to 2.
Runs 100% on CPU (0 GPU quota used).
"""
app_model, swapper_model = get_swapper_models()
if app_model is None or swapper_model is None:
print("⚠️ Face Swapper model unavailable. Returning original frames.")
return frames_np
try:
source_img = ref_face_image if ref_face_image is not None else source_pil_image
if source_img is None:
return frames_np
source_bgr = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
source_faces = app_model.get(source_bgr)
if not source_faces:
print("⚠️ No face detected in source/reference image. Skipping face swap.")
return frames_np
source_faces.sort(key=lambda x: (x.bbox[2]-x.bbox[0]) * (x.bbox[3]-x.bbox[1]), reverse=True)
source_face = source_faces[0]
total_all = len(frames_np)
swap_last_n = int(swap_last_n)
# Fallback calculation
if swap_last_n == 0:
n_swap = total_all
elif swap_last_n > total_all:
print(f"Notice: swap_last_n ({swap_last_n}) exceeds total frames ({total_all}). Fallback to 2 frames.")
n_swap = min(2, total_all)
else:
n_swap = swap_last_n
if n_swap < total_all:
unchanged_prefix = list(frames_np[:-n_swap])
target_frames = list(frames_np[-n_swap:])
else:
unchanged_prefix = []
target_frames = list(frames_np)
swapped_sub = []
total_sub = len(target_frames)
print(f"👤 Processing CPU Face Swap on {total_sub} frames (Last N={n_swap}, Gender filter: {target_gender})...")
for idx, frame in enumerate(tqdm(target_frames, desc="👤 CPU Face Swap")):
if progress is not None:
try:
progress((idx + 1) / total_sub, desc=f"👤 Swapping Face on Frame {idx+1}/{total_sub} (CPU)...")
except Exception:
pass
if isinstance(frame, Image.Image):
frame_uint8 = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)
elif isinstance(frame, np.ndarray):
frame_uint8 = (frame * 255).astype(np.uint8) if frame.dtype != np.uint8 else frame.copy()
frame_uint8 = cv2.cvtColor(frame_uint8, cv2.COLOR_RGB2BGR)
else:
frame_uint8 = np.array(frame, dtype=np.uint8)
frame_uint8 = cv2.cvtColor(frame_uint8, cv2.COLOR_RGB2BGR)
target_bgr = frame_uint8
target_faces = app_model.get(target_bgr)
if target_faces:
res_bgr = target_bgr.copy()
for target_face in target_faces:
gender_val = getattr(target_face, 'gender', None)
sex_val = getattr(target_face, 'sex', None)
if target_gender == "Female Faces Only":
is_female = (gender_val == 0) or (sex_val == 'F')
if not is_female:
continue
elif target_gender == "Male Faces Only":
is_male = (gender_val == 1) or (sex_val == 'M')
if not is_male:
continue
res_bgr = swapper_model.get(res_bgr, target_face, source_face, paste_back=True)
res_rgb = cv2.cvtColor(res_bgr, cv2.COLOR_BGR2RGB)
if isinstance(frame, np.ndarray) and frame.dtype != np.uint8:
swapped_sub.append(res_rgb.astype(np.float32) / 255.0)
elif isinstance(frame, Image.Image):
swapped_sub.append(Image.fromarray(res_rgb))
else:
swapped_sub.append(res_rgb)
else:
swapped_sub.append(frame)
final_result = unchanged_prefix + swapped_sub
print(f"✅ CPU Face Swap complete ({len(swapped_sub)} frames swapped)!")
return final_result
except Exception as e:
print(f"⚠️ Face Swapper execution error: {e}")
return frames_np
def map_gender_param(target_gender: str) -> str:
if not target_gender:
return "all"
tg = str(target_gender).lower()
if "female" in tg or "wanita" in tg or "perempuan" in tg:
return "female"
elif "male" in tg or "pria" in tg or "laki" in tg:
return "male"
return "all"
def call_sulphur_faceswap_api(source_img: Image.Image, target_img: Image.Image, target_gender: str = "all", enhance_with_gfpgan: bool = True, server_url: str = None) -> Image.Image:
"""
Calls Sulphur AI API (/api/v1/faceswap) to perform InsightFace Face Swap + GFPGAN Face Restoration.
"""
import io
import requests
import config
target_url = server_url or config.SULPHUR_API_URL or os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
if not target_url or not str(target_url).strip():
return None
clean_url = str(target_url).strip().rstrip("/")
endpoint = f"{clean_url}/api/v1/faceswap"
try:
source_bytes = io.BytesIO()
source_img.convert("RGB").save(source_bytes, format="JPEG", quality=95)
source_bytes.seek(0)
target_bytes = io.BytesIO()
target_img.convert("RGB").save(target_bytes, format="JPEG", quality=95)
target_bytes.seek(0)
files = {
"source_image": ("source.jpg", source_bytes, "image/jpeg"),
"target_image": ("target.jpg", target_bytes, "image/jpeg")
}
data = {
"enhance_with_gfpgan": "true" if enhance_with_gfpgan else "false",
"target_gender": map_gender_param(target_gender)
}
print(f"🌐 Calling Sulphur AI Face Swap API at {endpoint} (Gender: {map_gender_param(target_gender)})...")
res = requests.post(endpoint, files=files, data=data, timeout=15)
if res.status_code == 200 and res.content:
result_img = Image.open(io.BytesIO(res.content)).convert("RGB")
print("✅ Sulphur AI Face Swap + GFPGAN API succeeded!")
return result_img
else:
print(f"⚠️ Sulphur AI Face Swap API returned status {res.status_code}")
except Exception as e:
print(f"⚠️ Sulphur AI Face Swap API notice: {e}")
return None
def call_sulphur_enhance_face_api(image: Image.Image, server_url: str = None) -> Image.Image:
"""
Calls Sulphur AI API (/api/v1/enhance-face) to sharpen & restore face details via GFPGAN v1.4.
"""
import io
import requests
import config
target_url = server_url or config.SULPHUR_API_URL or os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
if not target_url or not str(target_url).strip():
return None
clean_url = str(target_url).strip().rstrip("/")
endpoint = f"{clean_url}/api/v1/enhance-face"
try:
img_bytes = io.BytesIO()
image.convert("RGB").save(img_bytes, format="JPEG", quality=95)
img_bytes.seek(0)
files = {"image": ("face.jpg", img_bytes, "image/jpeg")}
print(f"🌐 Calling Sulphur AI GFPGAN Face Enhance API at {endpoint}...")
res = requests.post(endpoint, files=files, timeout=15)
if res.status_code == 200 and res.content:
result_img = Image.open(io.BytesIO(res.content)).convert("RGB")
print("✅ Sulphur AI GFPGAN Face Enhance succeeded!")
return result_img
else:
print(f"⚠️ Sulphur AI Face Enhance API status {res.status_code}")
except Exception as e:
print(f"⚠️ Sulphur AI Face Enhance API notice: {e}")
return None
def swap_face_in_single_image(
target_pil_image: Image.Image,
ref_face_image: Image.Image = None,
target_gender: str = "Any / All Faces",
enhance_with_gfpgan: bool = True
) -> Image.Image:
"""
Swaps face on a single PIL image strictly using local CPU InsightFace (0 GPU quota).
Returns swapped PIL Image.
"""
if target_pil_image is None:
return None
# Execute strictly on local CPU InsightFace
swapped_frames = swap_face_in_frames(
source_pil_image=target_pil_image,
frames_np=[target_pil_image],
ref_face_image=ref_face_image,
target_gender=target_gender,
swap_last_n=0
)
res_frame = swapped_frames[0]
if isinstance(res_frame, Image.Image):
return res_frame
elif isinstance(res_frame, np.ndarray):
if res_frame.dtype != np.uint8:
res_frame = (res_frame * 255).astype(np.uint8)
return Image.fromarray(res_frame)
return target_pil_image