# roop_custom_nodes.py import os import subprocess import glob from PIL import Image import numpy as np import torch import hmac import hashlib import requests # Helper: save tensor image def save_tensor_as_image(tensor, path): np_img = (tensor[0].cpu().numpy() * 255).astype(np.uint8) if np_img.shape[0] == 3: # If format is CHW np_img = np.transpose(np_img, (1, 2, 0)) # Convert to HWC for PIL img = Image.fromarray(np_img) img.save(path) # Helper: load image as tensor def load_image_as_tensor(path): img = Image.open(path).convert("RGB") np_img = np.array(img).astype(np.float32) / 255.0 return torch.from_numpy(np_img).unsqueeze(0) def get_unique_filename(path): base, ext = os.path.splitext(path) counter = 1 new_path = path while os.path.exists(new_path): new_path = f"{base}_{counter}{ext}" counter += 1 return new_path def send_webhook_image(webhook_url, webhook_secret, output_path, extra_data=None): if not webhook_url: return with open(output_path, 'rb') as f: image_data = f.read() # GitHub-style HMAC signature signature = 'sha256=' + hmac.new( webhook_secret.encode('utf-8'), image_data, hashlib.sha256 ).hexdigest() headers = { 'X-Hub-Signature-256': signature } files = { 'file': ('swapped.png', image_data, 'image/png') } data = extra_data or {} try: resp = requests.post(webhook_url, headers=headers, files=files, data=data) resp.raise_for_status() print(f"[Webhook] Sent successfully to {webhook_url}") except Exception as e: print(f"[Webhook Error] {e}") class RoopFaceSwap: @classmethod def INPUT_TYPES(cls): return { "required": { "source_image": ("IMAGE",), "target_image": ("IMAGE",), "roop_dir": ("STRING", {"default": "/content/roop"}), "output_name": ("STRING", {"default": "roop_output.png"}), "many_faces": ("BOOLEAN", {"default": False}) } } RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("swapped_image",) FUNCTION = "run" CATEGORY = "Roop/Basic" def run(self, source_image, target_image, roop_dir, output_name, many_faces): temp_dir = os.path.join(roop_dir, "temp_io") os.makedirs(temp_dir, exist_ok=True) source_path = os.path.join(temp_dir, "source.png") target_path = os.path.join(temp_dir, "target.png") output_path = get_unique_filename(os.path.join(temp_dir, output_name)) save_tensor_as_image(source_image, source_path) save_tensor_as_image(target_image, target_path) cmd = [ "python", "run.py", "-s", source_path, "-t", target_path, "-o", output_path, "--execution-provider", "cuda", "--frame-processor", "face_swapper" ] if many_faces: cmd.append("--many-faces") subprocess.run(cmd, check=True, cwd=roop_dir) if not os.path.exists(output_path): print(f"[Warning] Roop did not produce output: {output_path}") blank = torch.zeros_like(target_image) return (blank,) return (load_image_as_tensor(output_path),) class RoopFaceSwapWithEnhancer: @classmethod def INPUT_TYPES(cls): return { "required": { "source_image": ("IMAGE",), "target_image": ("IMAGE",), "roop_dir": ("STRING", {"default": "/content/roop"}), "output_name": ("STRING", {"default": "roop_output.png"}), "many_faces": ("BOOLEAN", {"default": False}) } } RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("swapped_image",) FUNCTION = "run" CATEGORY = "Roop/Enhanced" def run(self, source_image, target_image, roop_dir, output_name, many_faces): temp_dir = os.path.join(roop_dir, "temp_io") os.makedirs(temp_dir, exist_ok=True) source_path = os.path.join(temp_dir, "source.png") target_path = os.path.join(temp_dir, "target.png") output_path = get_unique_filename(os.path.join(temp_dir, output_name)) save_tensor_as_image(source_image, source_path) save_tensor_as_image(target_image, target_path) cmd = [ "python", "run.py", "-s", source_path, "-t", target_path, "-o", output_path, "--execution-provider", "cuda", "--frame-processor", "face_swapper", "face_enhancer" ] if many_faces: cmd.append("--many-faces") subprocess.run(cmd, check=True, cwd=roop_dir) if not os.path.exists(output_path): print(f"[Warning] Roop did not produce output: {output_path}") blank = torch.zeros_like(target_image) return (blank,) return (load_image_as_tensor(output_path),) class RoopBatchFaceSwap: @classmethod def INPUT_TYPES(cls): return { "required": { "source_image": ("IMAGE",), "input_dir": ("STRING", {"default": "/input/images"}), "output_dir": ("STRING", {"default": "/output/images"}), "roop_dir": ("STRING", {"default": "/content/roop"}), "use_enhancer": ("BOOLEAN", {"default": False}), "many_faces": ("BOOLEAN", {"default": False}) } } RETURN_TYPES = () RETURN_NAMES = () FUNCTION = "run" CATEGORY = "Roop/Batch" def run(self, source_image, input_dir, output_dir, roop_dir, use_enhancer, many_faces): os.makedirs(output_dir, exist_ok=True) temp_dir = os.path.join(roop_dir, "temp_io") os.makedirs(temp_dir, exist_ok=True) source_path = os.path.join(temp_dir, "source.png") save_tensor_as_image(source_image, source_path) image_paths = glob.glob(os.path.join(input_dir, "*.jpg")) + \ glob.glob(os.path.join(input_dir, "*.png")) for img_path in image_paths: target_name = os.path.basename(img_path) target_path = os.path.join(temp_dir, "target.png") output_path = os.path.join(output_dir, f"out_{target_name}") Image.open(img_path).save(target_path) cmd = [ "python", "run.py", "-s", source_path, "-t", target_path, "-o", output_path, "--execution-provider", "cuda", "--frame-processor", "face_swapper" ] if use_enhancer: cmd[-1] += " face_enhancer" if many_faces: cmd.append("--many-faces") subprocess.run(cmd, check=True, cwd=roop_dir) if not os.path.exists(output_path): print(f"[Skipped] NSFW or error: {img_path} -> No output generated.") continue return () class RoopFaceSwapVideo: @classmethod def INPUT_TYPES(cls): return { "required": { "source_image": ("IMAGE",), "target_video_path": ("STRING", {"default": "/path/to/video.mp4"}), "roop_dir": ("STRING", {"default": "/content/roop"}), "output_name": ("STRING", {"default": "swapped_video.mp4"}), "many_faces": ("BOOLEAN", {"default": False}) } } RETURN_TYPES = () RETURN_NAMES = () FUNCTION = "run" CATEGORY = "Roop/Video" def run(self, source_image, target_video_path, roop_dir, output_name, many_faces): temp_dir = os.path.join(roop_dir, "temp_io") os.makedirs(temp_dir, exist_ok=True) source_path = os.path.join(temp_dir, "source.png") output_path = get_unique_filename(os.path.join(temp_dir, output_name)) save_tensor_as_image(source_image, source_path) cmd = [ "python", "run.py", "-s", source_path, "-t", target_video_path, "-o", output_path, "--keep-fps", "--keep-frames", "--execution-provider", "cuda", "--frame-processor", "face_swapper" ] if many_faces: cmd.append("--many-faces") subprocess.run(cmd, check=True, cwd=roop_dir) if not os.path.exists(output_path): print(f"[Warning] Roop did not produce video output: {output_path}") return () class RoopSendWebhookImage: @classmethod def INPUT_TYPES(cls): return { "required": { "image_tensor": ("IMAGE",), "filename": ("STRING", {"default": "output.png"}), "webhook_url": ("STRING", {"default": ""}), "webhook_secret": ("STRING", {"default": ""}), "enable_webhook": ("BOOLEAN", {"default": True}), "roop_dir": ("STRING", {"default": "/content/roop"}) } } RETURN_TYPES = () RETURN_NAMES = () FUNCTION = "run" CATEGORY = "Roop/Webhook" def run(self, image_tensor, filename, webhook_url, webhook_secret, enable_webhook, roop_dir): if not enable_webhook or not webhook_url: print("[WebhookImage] Disabled or URL not set — skipping.") return () temp_dir = os.path.join(roop_dir, "temp_io") os.makedirs(temp_dir, exist_ok=True) output_path = os.path.join(temp_dir, filename) save_tensor_as_image(image_tensor, output_path) try: with open(output_path, 'rb') as f: file_data = f.read() headers = {} if webhook_secret: signature = 'sha256=' + hmac.new( webhook_secret.encode('utf-8'), file_data, hashlib.sha256 ).hexdigest() headers['X-Hub-Signature-256'] = signature files = { 'file': (filename, file_data, 'image/png') } resp = requests.post(webhook_url, headers=headers, files=files) resp.raise_for_status() print(f"[WebhookImage] Sent image: {filename} → {webhook_url}") except Exception as e: print(f"[WebhookImage Error] {e}") return () class RoopSendWebhookFile: @classmethod def INPUT_TYPES(cls): return { "required": { "file_path": ("STRING",), "filename": ("STRING", {"default": "output.mp4"}), "webhook_url": ("STRING", {"default": ""}), "webhook_secret": ("STRING", {"default": ""}), "enable_webhook": ("BOOLEAN", {"default": True}) } } RETURN_TYPES = () RETURN_NAMES = () FUNCTION = "run" CATEGORY = "Roop/Webhook" def run(self, file_path, filename, webhook_url, webhook_secret, enable_webhook): if not enable_webhook or not webhook_url: print("[WebhookFile] Disabled or URL not set — skipping.") return () if not os.path.exists(file_path): print(f"[WebhookFile] File does not exist: {file_path}") return () try: with open(file_path, 'rb') as f: file_data = f.read() headers = {} if webhook_secret: signature = 'sha256=' + hmac.new( webhook_secret.encode('utf-8'), file_data, hashlib.sha256 ).hexdigest() headers['X-Hub-Signature-256'] = signature content_type = "video/mp4" if filename.endswith(".mp4") else "application/octet-stream" files = { 'file': (filename, file_data, content_type) } resp = requests.post(webhook_url, headers=headers, files=files) resp.raise_for_status() print(f"[WebhookFile] Sent file: {filename} → {webhook_url}") except Exception as e: print(f"[WebhookFile Error] {e}") return () # Register with ComfyUI NODE_CLASS_MAPPINGS = { "RoopFaceSwap": RoopFaceSwap, "RoopFaceSwapWithEnhancer": RoopFaceSwapWithEnhancer, "RoopBatchFaceSwap": RoopBatchFaceSwap, "RoopFaceSwapVideo": RoopFaceSwapVideo, "RoopSendWebhookImage": RoopSendWebhookImage, "RoopSendWebhookFile": RoopSendWebhookFile, } NODE_DISPLAY_NAME_MAPPINGS = { "RoopFaceSwap": "Roop Face Swap (Image)", "RoopFaceSwapWithEnhancer": "Roop Face Swap + Enhancer", "RoopBatchFaceSwap": "Roop Batch Image Folder", "RoopFaceSwapVideo": "Roop Face Swap (Video)", "RoopSendWebhookImage": "Roop Webhook: Image Tensor", "RoopSendWebhookFile": "Roop Webhook: File Path", }