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