| import os |
|
|
| |
| |
| |
|
|
| os.environ["CUDA_VISIBLE_DEVICES"] = "" |
| os.environ["ORT_DISABLE_CUDA"] = "1" |
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "" |
|
|
| |
| |
| |
|
|
| import sys |
| import asyncio |
| import gc |
| import json |
| import time |
| import tempfile |
| import importlib |
| import threading |
| import shutil |
|
|
| from collections import OrderedDict |
|
|
| |
| |
| |
|
|
| import numpy as np |
| import cv2 |
| import gradio as gr |
| import imageio |
|
|
| from PIL import Image |
| from huggingface_hub import hf_hub_download |
|
|
| |
| |
| |
|
|
| APP_TITLE = "CPU Multi Face Swap Pro" |
|
|
| BASE_DIR = os.path.abspath( |
| os.path.dirname(__file__) |
| ) |
|
|
| MODEL_DIR = os.path.join( |
| BASE_DIR, |
| "models" |
| ) |
|
|
| INSIGHTFACE_DIR = os.path.join( |
| MODEL_DIR, |
| "insightface" |
| ) |
|
|
| HYPERSWAP_DIR = os.path.join( |
| MODEL_DIR, |
| "hyperswap" |
| ) |
|
|
| FACE_RESTORE_DIR = os.path.join( |
| MODEL_DIR, |
| "facerestore_models" |
| ) |
|
|
| OUTPUT_DIR = os.path.join( |
| BASE_DIR, |
| "output" |
| ) |
|
|
| COMFYUI_ROOT = BASE_DIR |
|
|
| REACTOR_PATH = os.path.join( |
| COMFYUI_ROOT, |
| "custom_nodes", |
| "comfyui-reactor-node" |
| ) |
|
|
| if not os.path.isdir(REACTOR_PATH): |
|
|
| REACTOR_PATH = os.path.join( |
| BASE_DIR, |
| "custom_nodes", |
| "comfyui-reactor-node" |
| ) |
|
|
| DEFAULT_SWAP_MODEL = ( |
| "inswapper_128.onnx" |
| ) |
|
|
| DEFAULT_RESTORE_MODEL = ( |
| "GPEN-BFR-512.onnx" |
| ) |
|
|
| DEFAULT_RESTORE_STRENGTH = 0.70 |
|
|
| |
| |
| |
|
|
| MAX_CONCURRENT_JOBS = 2 |
| MAX_CONCURRENT_INFERENCE = 2 |
| MAX_PREVIEW_JOBS = 2 |
| GRADIO_QUEUE_SIZE = 8 |
|
|
| |
| |
| |
|
|
| JOB_SEMAPHORE = threading.BoundedSemaphore( |
| MAX_CONCURRENT_JOBS |
| ) |
|
|
| INFERENCE_SEMAPHORE = threading.BoundedSemaphore( |
| MAX_CONCURRENT_INFERENCE |
| ) |
|
|
| MODEL_LOCK = threading.RLock() |
| COMFY_INIT_LOCK = threading.RLock() |
|
|
| |
| |
| |
|
|
| loaded_models = {} |
|
|
| face_cache = OrderedDict() |
| FACE_CACHE_MAX = 64 |
|
|
| source_tensor_cache = OrderedDict() |
| SOURCE_TENSOR_CACHE_MAX = 16 |
|
|
| |
| |
| |
|
|
| def log(*args): |
|
|
| print( |
| *args, |
| flush=True |
| ) |
|
|
| |
| |
| |
|
|
| log("=" * 70) |
|
|
| log( |
| "[APP] BASE DIR:", |
| BASE_DIR |
| ) |
|
|
| log( |
| "[APP] MODEL DIR:", |
| MODEL_DIR |
| ) |
|
|
| log( |
| "[APP] OUTPUT DIR:", |
| OUTPUT_DIR |
| ) |
|
|
| log( |
| "[APP] MAX JOBS:", |
| MAX_CONCURRENT_JOBS |
| ) |
|
|
| log( |
| "[APP] MAX INFERENCE:", |
| MAX_CONCURRENT_INFERENCE |
| ) |
|
|
| log("=" * 70) |
|
|
| |
| |
| |
|
|
| for directory in ( |
| MODEL_DIR, |
| INSIGHTFACE_DIR, |
| HYPERSWAP_DIR, |
| FACE_RESTORE_DIR, |
| OUTPUT_DIR |
| ): |
|
|
| os.makedirs( |
| directory, |
| exist_ok=True |
| ) |
|
|
| |
| |
| |
|
|
| MODEL_REPOS = { |
|
|
| "inswapper_128.onnx": ( |
| "ezioruan/inswapper_128.onnx", |
| "inswapper_128.onnx", |
| INSIGHTFACE_DIR |
| ), |
|
|
| "hyperswap_1a_256.onnx": ( |
| "facefusion/models-3.3.0", |
| "hyperswap_1a_256.onnx", |
| HYPERSWAP_DIR |
| ), |
|
|
| "hyperswap_1b_256.onnx": ( |
| "facefusion/models-3.3.0", |
| "hyperswap_1b_256.onnx", |
| HYPERSWAP_DIR |
| ), |
|
|
| "hyperswap_1c_256.onnx": ( |
| "facefusion/models-3.3.0", |
| "hyperswap_1c_256.onnx", |
| HYPERSWAP_DIR |
| ), |
|
|
| "GPEN-BFR-512.onnx": ( |
| "martintomov/comfy", |
| "facerestore_models/GPEN-BFR-512.onnx", |
| FACE_RESTORE_DIR |
| ) |
| } |
|
|
| |
| |
| |
|
|
| def download_model(model_name): |
|
|
| if model_name not in MODEL_REPOS: |
|
|
| raise RuntimeError( |
| f"Unknown model: {model_name}" |
| ) |
|
|
| repo, filename, local_dir = ( |
| MODEL_REPOS[ |
| model_name |
| ] |
| ) |
|
|
| basename = os.path.basename( |
| filename |
| ) |
|
|
| local_path = os.path.join( |
| local_dir, |
| basename |
| ) |
|
|
| if os.path.isfile( |
| local_path |
| ): |
|
|
| log( |
| "[MODEL] Already exists:", |
| local_path |
| ) |
|
|
| return local_path |
|
|
| os.makedirs( |
| local_dir, |
| exist_ok=True |
| ) |
|
|
| |
| |
| |
|
|
| nested_path = os.path.join( |
| local_dir, |
| os.path.dirname(filename), |
| basename |
| ) |
|
|
| if ( |
| os.path.isfile(nested_path) |
| and not os.path.isfile(local_path) |
| ): |
|
|
| log( |
| "[MODEL] Migrating old nested model:", |
| nested_path |
| ) |
|
|
| try: |
|
|
| shutil.copy2( |
| nested_path, |
| local_path |
| ) |
|
|
| if os.path.isfile( |
| local_path |
| ): |
|
|
| log( |
| "[MODEL] Migrated:", |
| local_path |
| ) |
|
|
| return local_path |
|
|
| except Exception as e: |
|
|
| log( |
| "[MODEL] Migration warning:", |
| repr(e) |
| ) |
|
|
| |
| |
| |
|
|
| log( |
| "[MODEL] Downloading:", |
| model_name |
| ) |
|
|
| download_dir = tempfile.mkdtemp( |
| prefix="model_download_" |
| ) |
|
|
| try: |
|
|
| downloaded_path = hf_hub_download( |
| repo_id=repo, |
| filename=filename, |
| local_dir=download_dir |
| ) |
|
|
| if not os.path.isfile( |
| downloaded_path |
| ): |
|
|
| raise RuntimeError( |
| "Model download failed:\n" |
| + str(downloaded_path) |
| ) |
|
|
| shutil.copy2( |
| downloaded_path, |
| local_path |
| ) |
|
|
| finally: |
|
|
| try: |
|
|
| shutil.rmtree( |
| download_dir, |
| ignore_errors=True |
| ) |
|
|
| except Exception: |
| pass |
|
|
| if not os.path.isfile( |
| local_path |
| ): |
|
|
| raise RuntimeError( |
| "Model was downloaded but " |
| "could not be placed at:\n" |
| + local_path |
| ) |
|
|
| log( |
| "[MODEL] Ready:", |
| local_path |
| ) |
|
|
| return local_path |
|
|
| |
| |
| |
|
|
| def ensure_model(model_name): |
|
|
| if not model_name: |
| return None |
|
|
| if model_name == "none": |
| return None |
|
|
| with MODEL_LOCK: |
|
|
| cached = loaded_models.get( |
| model_name |
| ) |
|
|
| if ( |
| cached |
| and os.path.isfile(cached) |
| ): |
|
|
| return cached |
|
|
| path = download_model( |
| model_name |
| ) |
|
|
| loaded_models[ |
| model_name |
| ] = path |
|
|
| return path |
|
|
| |
| |
| |
|
|
| def setup_comfyui_path(): |
|
|
| if COMFYUI_ROOT in sys.path: |
|
|
| try: |
|
|
| sys.path.remove( |
| COMFYUI_ROOT |
| ) |
|
|
| except ValueError: |
| pass |
|
|
| sys.path.insert( |
| 0, |
| COMFYUI_ROOT |
| ) |
|
|
| log( |
| "[COMFYUI] Root:", |
| COMFYUI_ROOT |
| ) |
|
|
| log( |
| "[COMFYUI] sys.path[0]:", |
| sys.path[0] |
| ) |
|
|
| setup_comfyui_path() |
|
|
| |
| |
| |
|
|
| def fix_comfy_utils_namespace(): |
|
|
| utils_dir = os.path.join( |
| COMFYUI_ROOT, |
| "utils" |
| ) |
|
|
| if not os.path.isdir( |
| utils_dir |
| ): |
|
|
| return |
|
|
| existing = sys.modules.get( |
| "utils" |
| ) |
|
|
| if existing is not None: |
|
|
| existing_path = getattr( |
| existing, |
| "__path__", |
| None |
| ) |
|
|
| if existing_path is None: |
|
|
| try: |
|
|
| del sys.modules[ |
| "utils" |
| ] |
|
|
| except KeyError: |
| pass |
|
|
| if COMFYUI_ROOT in sys.path: |
|
|
| try: |
|
|
| sys.path.remove( |
| COMFYUI_ROOT |
| ) |
|
|
| except ValueError: |
| pass |
|
|
| sys.path.insert( |
| 0, |
| COMFYUI_ROOT |
| ) |
|
|
| importlib.invalidate_caches() |
|
|
| try: |
|
|
| import utils |
|
|
| log( |
| "[COMFYUI] utils:", |
| getattr( |
| utils, |
| "__file__", |
| None |
| ) |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[COMFYUI] utils warning:", |
| repr(e) |
| ) |
|
|
| fix_comfy_utils_namespace() |
|
|
| |
| |
| |
|
|
| try: |
|
|
| import comfyui_version |
|
|
| comfy_version = getattr( |
| comfyui_version, |
| "__version__", |
| None |
| ) |
|
|
| if comfy_version is None: |
|
|
| comfy_version = getattr( |
| comfyui_version, |
| "VERSION", |
| "unknown" |
| ) |
|
|
| except Exception: |
|
|
| comfy_version = "unknown" |
|
|
| log( |
| "[COMFYUI] Version:", |
| comfy_version |
| ) |
|
|
| |
| |
| |
|
|
| def add_extra_model_paths(): |
|
|
| config_path = os.path.join( |
| COMFYUI_ROOT, |
| "extra_model_paths.yaml" |
| ) |
|
|
| if not os.path.isfile( |
| config_path |
| ): |
|
|
| return |
|
|
| try: |
|
|
| from main import ( |
| load_extra_path_config |
| ) |
|
|
| load_extra_path_config( |
| config_path |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[COMFYUI] Extra paths warning:", |
| repr(e) |
| ) |
|
|
| add_extra_model_paths() |
|
|
| |
| |
| |
|
|
| import torch |
|
|
| |
| |
| |
|
|
| import comfy.model_management |
|
|
| from comfy.model_management import ( |
| CPUState |
| ) |
|
|
| try: |
|
|
| comfy.model_management.cpu_state = ( |
| CPUState.CPU |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[DEVICE] CPU state warning:", |
| repr(e) |
| ) |
|
|
| log( |
| "[DEVICE] CPU forced" |
| ) |
|
|
| log( |
| "[DEVICE] CUDA available:", |
| torch.cuda.is_available() |
| ) |
|
|
| |
| |
| |
|
|
| def import_custom_nodes(): |
|
|
| with COMFY_INIT_LOCK: |
|
|
| log( |
| "[COMFYUI] Initializing nodes..." |
| ) |
|
|
| fix_comfy_utils_namespace() |
|
|
| import execution |
|
|
| from nodes import ( |
| init_extra_nodes |
| ) |
|
|
| import server |
|
|
| loop = asyncio.new_event_loop() |
|
|
| try: |
|
|
| asyncio.set_event_loop( |
| loop |
| ) |
|
|
| server_instance = ( |
| server.PromptServer( |
| loop |
| ) |
| ) |
|
|
| if not hasattr( |
| server_instance, |
| "prompt_queue" |
| ): |
|
|
| try: |
|
|
| execution.PromptQueue( |
| server_instance |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[COMFYUI] PromptQueue warning:", |
| repr(e) |
| ) |
|
|
| result = init_extra_nodes() |
|
|
| if asyncio.iscoroutine( |
| result |
| ): |
|
|
| loop.run_until_complete( |
| result |
| ) |
|
|
| finally: |
|
|
| try: |
|
|
| asyncio.set_event_loop( |
| None |
| ) |
|
|
| except Exception: |
| pass |
|
|
| try: |
|
|
| loop.close() |
|
|
| except Exception: |
| pass |
|
|
| log( |
| "[COMFYUI] Nodes initialized." |
| ) |
|
|
| import_custom_nodes() |
|
|
| |
| |
| |
|
|
| from nodes import ( |
| NODE_CLASS_MAPPINGS |
| ) |
|
|
| |
| |
| |
|
|
| if not os.path.isdir( |
| REACTOR_PATH |
| ): |
|
|
| raise RuntimeError( |
| "Không tìm thấy ComfyUI-ReActor:\n" |
| + REACTOR_PATH |
| ) |
|
|
| if REACTOR_PATH not in sys.path: |
|
|
| sys.path.insert( |
| 0, |
| REACTOR_PATH |
| ) |
|
|
| log( |
| "[REACTOR] Path:", |
| REACTOR_PATH |
| ) |
|
|
| |
| |
| |
|
|
| try: |
|
|
| loadimage = ( |
| NODE_CLASS_MAPPINGS[ |
| "LoadImage" |
| ]() |
| ) |
|
|
| except Exception as e: |
|
|
| raise RuntimeError( |
| "Không load được LoadImage:\n" |
| + repr(e) |
| ) |
|
|
| |
| |
| |
|
|
| try: |
|
|
| reactorfaceswap = ( |
| NODE_CLASS_MAPPINGS[ |
| "ReActorFaceSwap" |
| ]() |
| ) |
|
|
| except Exception as e: |
|
|
| raise RuntimeError( |
| "Không load được ReActorFaceSwap:\n" |
| + repr(e) |
| ) |
|
|
| |
| |
| |
|
|
| try: |
|
|
| from scripts.reactor_swapper import ( |
| analyze_faces, |
| sort_by_order |
| ) |
|
|
| except Exception as e: |
|
|
| raise RuntimeError( |
| "Không import được reactor_swapper:\n" |
| + repr(e) |
| ) |
|
|
| |
| |
| |
|
|
| def get_file_path(file): |
|
|
| if file is None: |
| return None |
|
|
| if isinstance( |
| file, |
| str |
| ): |
|
|
| return file |
|
|
| if hasattr( |
| file, |
| "path" |
| ): |
|
|
| return file.path |
|
|
| if hasattr( |
| file, |
| "name" |
| ): |
|
|
| return file.name |
|
|
| return str(file) |
|
|
|
|
| def get_file_paths(files): |
|
|
| if not files: |
| return [] |
|
|
| if isinstance( |
| files, |
| (str, bytes) |
| ): |
|
|
| files = [ |
| files |
| ] |
|
|
| result = [] |
|
|
| for item in files: |
|
|
| path = get_file_path( |
| item |
| ) |
|
|
| if ( |
| path |
| and os.path.isfile( |
| path |
| ) |
| ): |
|
|
| result.append( |
| path |
| ) |
|
|
| return result |
|
|
| |
| |
| |
|
|
| def clear_face_cache(): |
|
|
| face_cache.clear() |
|
|
| gc.collect() |
|
|
|
|
| def detect_reactor_faces( |
| image_path |
| ): |
|
|
| if not image_path: |
| return [] |
|
|
| if not os.path.isfile( |
| image_path |
| ): |
|
|
| return [] |
|
|
| try: |
|
|
| mtime = os.path.getmtime( |
| image_path |
| ) |
|
|
| except Exception: |
|
|
| return [] |
|
|
| key = ( |
| image_path, |
| mtime |
| ) |
|
|
| if key in face_cache: |
|
|
| faces = face_cache.pop( |
| key |
| ) |
|
|
| face_cache[key] = faces |
|
|
| return faces |
|
|
| img = cv2.imread( |
| image_path |
| ) |
|
|
| if img is None: |
| return [] |
|
|
| faces = analyze_faces( |
| img, |
| det_size=( |
| 640, |
| 640 |
| ) |
| ) |
|
|
| if faces is None: |
| faces = [] |
|
|
| faces = sort_by_order( |
| faces, |
| "large-small" |
| ) |
|
|
| face_cache[key] = faces |
|
|
| while len(face_cache) > FACE_CACHE_MAX: |
|
|
| face_cache.popitem( |
| last=False |
| ) |
|
|
| return faces |
|
|
| |
| |
| |
|
|
| def get_gif_first_frame( |
| path |
| ): |
|
|
| reader = imageio.get_reader( |
| path |
| ) |
|
|
| try: |
|
|
| frame = reader.get_data( |
| 0 |
| ) |
|
|
| return np.asarray( |
| frame |
| ).copy() |
|
|
| finally: |
|
|
| reader.close() |
|
|
| |
| |
| |
|
|
| def get_detection_image( |
| path |
| ): |
|
|
| if path.lower().endswith( |
| ".gif" |
| ): |
|
|
| return get_gif_first_frame( |
| path |
| ) |
|
|
| img = cv2.imread( |
| path |
| ) |
|
|
| if img is None: |
| return None |
|
|
| return cv2.cvtColor( |
| img, |
| cv2.COLOR_BGR2RGB |
| ) |
|
|
| |
| |
| |
|
|
| def crop_face_from_image( |
| rgb, |
| face, |
| padding=0.35 |
| ): |
|
|
| if rgb is None: |
| return None |
|
|
| h, w = rgb.shape[:2] |
|
|
| x1, y1, x2, y2 = ( |
| face.bbox |
| ) |
|
|
| x1 = int( |
| round(x1) |
| ) |
|
|
| y1 = int( |
| round(y1) |
| ) |
|
|
| x2 = int( |
| round(x2) |
| ) |
|
|
| y2 = int( |
| round(y2) |
| ) |
|
|
| face_w = max( |
| 1, |
| x2 - x1 |
| ) |
|
|
| face_h = max( |
| 1, |
| y2 - y1 |
| ) |
|
|
| pad_x = int( |
| face_w * padding |
| ) |
|
|
| pad_y = int( |
| face_h * padding |
| ) |
|
|
| x1 = max( |
| 0, |
| x1 - pad_x |
| ) |
|
|
| y1 = max( |
| 0, |
| y1 - pad_y |
| ) |
|
|
| x2 = min( |
| w, |
| x2 + pad_x |
| ) |
|
|
| y2 = min( |
| h, |
| y2 + pad_y |
| ) |
|
|
| if x2 <= x1 or y2 <= y1: |
| return None |
|
|
| crop = rgb[ |
| y1:y2, |
| x1:x2 |
| ] |
|
|
| if crop.size == 0: |
| return None |
|
|
| return np.asarray( |
| crop |
| ).copy() |
|
|
| |
| |
| |
|
|
| def make_face_review_gallery( |
| path, |
| prefix |
| ): |
|
|
| if not path: |
| return [] |
|
|
| rgb = get_detection_image( |
| path |
| ) |
|
|
| if rgb is None: |
| return [] |
|
|
| faces = detect_reactor_faces( |
| path |
| ) |
|
|
| gallery = [] |
|
|
| for face_index, face in enumerate( |
| faces |
| ): |
|
|
| crop = crop_face_from_image( |
| rgb, |
| face, |
| padding=0.35 |
| ) |
|
|
| if crop is None: |
| continue |
|
|
| filename = os.path.basename( |
| path |
| ) |
|
|
| caption = ( |
| f"{prefix} FACE {face_index} | " |
| f"{filename}" |
| ) |
|
|
| gallery.append( |
| ( |
| crop, |
| caption |
| ) |
| ) |
|
|
| return gallery |
|
|
| |
| |
| |
|
|
| def make_multi_face_review( |
| paths, |
| prefix |
| ): |
|
|
| gallery = [] |
|
|
| for index, path in enumerate( |
| paths |
| ): |
|
|
| try: |
|
|
| items = make_face_review_gallery( |
| path, |
| f"{prefix} {index}" |
| ) |
|
|
| gallery.extend( |
| items |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[FACE REVIEW ERROR]", |
| path, |
| repr(e) |
| ) |
|
|
| return gallery |
|
|
| |
| |
| |
|
|
| def make_annotated_preview( |
| path, |
| prefix |
| ): |
|
|
| if not path: |
| return None, 0 |
|
|
| rgb = get_detection_image( |
| path |
| ) |
|
|
| if rgb is None: |
| return None, 0 |
|
|
| bgr = cv2.cvtColor( |
| rgb, |
| cv2.COLOR_RGB2BGR |
| ) |
|
|
| h, w = bgr.shape[:2] |
|
|
| faces = detect_reactor_faces( |
| path |
| ) |
|
|
| for face_index, face in enumerate( |
| faces |
| ): |
|
|
| x1, y1, x2, y2 = ( |
| face.bbox |
| ) |
|
|
| x1 = max( |
| 0, |
| min( |
| w - 1, |
| int(x1) |
| ) |
| ) |
|
|
| y1 = max( |
| 0, |
| min( |
| h - 1, |
| int(y1) |
| ) |
| ) |
|
|
| x2 = max( |
| x1 + 1, |
| min( |
| w - 1, |
| int(x2) |
| ) |
| ) |
|
|
| y2 = max( |
| y1 + 1, |
| min( |
| h - 1, |
| int(y2) |
| ) |
| ) |
|
|
| cv2.rectangle( |
| bgr, |
| (x1, y1), |
| (x2, y2), |
| (0, 255, 0), |
| max( |
| 2, |
| int( |
| min(w, h) / 350 |
| ) |
| ) |
| ) |
|
|
| label = ( |
| f"{prefix} FACE {face_index}" |
| ) |
|
|
| font_scale = max( |
| 0.55, |
| min( |
| 1.15, |
| min(w, h) / 800 |
| ) |
| ) |
|
|
| thickness = max( |
| 1, |
| int( |
| font_scale * 2 |
| ) |
| ) |
|
|
| ( |
| tw, |
| th |
| ), baseline = cv2.getTextSize( |
| label, |
| cv2.FONT_HERSHEY_SIMPLEX, |
| font_scale, |
| thickness |
| ) |
|
|
| label_y1 = max( |
| 0, |
| y1 - th - baseline - 8 |
| ) |
|
|
| label_y2 = ( |
| y1 |
| if y1 > th + baseline + 8 |
| else y1 + th + baseline + 12 |
| ) |
|
|
| cv2.rectangle( |
| bgr, |
| ( |
| x1, |
| label_y1 |
| ), |
| ( |
| min( |
| w - 1, |
| x1 + tw + 12 |
| ), |
| min( |
| h - 1, |
| label_y2 |
| ) |
| ), |
| (0, 255, 0), |
| -1 |
| ) |
|
|
| text_y = ( |
| label_y2 - 6 |
| if y1 > th + baseline + 8 |
| else label_y1 + th + 6 |
| ) |
|
|
| cv2.putText( |
| bgr, |
| label, |
| ( |
| x1 + 6, |
| text_y |
| ), |
| cv2.FONT_HERSHEY_SIMPLEX, |
| font_scale, |
| (0, 0, 0), |
| thickness, |
| cv2.LINE_AA |
| ) |
|
|
| preview = cv2.cvtColor( |
| bgr, |
| cv2.COLOR_BGR2RGB |
| ) |
|
|
| return preview, len(faces) |
|
|
| |
| |
| |
|
|
| def preview_source_target( |
| source_files, |
| target_files |
| ): |
|
|
| source_paths = get_file_paths( |
| source_files |
| ) |
|
|
| target_paths = get_file_paths( |
| target_files |
| ) |
|
|
| source_gallery = [] |
| target_gallery = [] |
|
|
| source_choices = [] |
| target_choices = [] |
|
|
| source_face_choices = [] |
| target_face_choices = [] |
|
|
| try: |
|
|
| with JOB_SEMAPHORE: |
|
|
| |
| |
| |
|
|
| for source_index, path in enumerate( |
| source_paths |
| ): |
|
|
| try: |
|
|
| faces = detect_reactor_faces( |
| path |
| ) |
|
|
| count = len( |
| faces |
| ) |
|
|
| crops = make_face_review_gallery( |
| path, |
| f"SOURCE {source_index}" |
| ) |
|
|
| source_gallery.extend( |
| crops |
| ) |
|
|
| source_choices.append( |
| f"SOURCE {source_index}: " |
| f"{os.path.basename(path)}" |
| ) |
|
|
| for face_index in range( |
| count |
| ): |
|
|
| source_face_choices.append( |
| f"SOURCE {source_index} " |
| f"FACE {face_index}" |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[PREVIEW SOURCE ERROR]", |
| path, |
| repr(e) |
| ) |
|
|
| |
| |
| |
|
|
| for target_index, path in enumerate( |
| target_paths |
| ): |
|
|
| try: |
|
|
| faces = detect_reactor_faces( |
| path |
| ) |
|
|
| count = len( |
| faces |
| ) |
|
|
| crops = make_face_review_gallery( |
| path, |
| f"TARGET {target_index}" |
| ) |
|
|
| target_gallery.extend( |
| crops |
| ) |
|
|
| target_choices.append( |
| f"TARGET {target_index}: " |
| f"{os.path.basename(path)}" |
| ) |
|
|
| for face_index in range( |
| count |
| ): |
|
|
| target_face_choices.append( |
| f"TARGET {target_index} " |
| f"FACE {face_index}" |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[PREVIEW TARGET ERROR]", |
| path, |
| repr(e) |
| ) |
|
|
| except Exception as e: |
|
|
| log( |
| "[PREVIEW ERROR]", |
| repr(e) |
| ) |
|
|
| source_value = ( |
| source_choices[0] |
| if source_choices |
| else None |
| ) |
|
|
| target_value = ( |
| target_choices[0] |
| if target_choices |
| else None |
| ) |
|
|
| source_face_value = ( |
| source_face_choices[0] |
| if source_face_choices |
| else None |
| ) |
|
|
| target_face_value = ( |
| target_face_choices[0] |
| if target_face_choices |
| else None |
| ) |
|
|
| return ( |
|
|
| source_gallery, |
|
|
| target_gallery, |
|
|
| gr.update( |
| choices=source_choices, |
| value=source_value |
| ), |
|
|
| gr.update( |
| choices=target_choices, |
| value=target_value |
| ), |
|
|
| gr.update( |
| choices=source_face_choices, |
| value=source_face_value |
| ), |
|
|
| gr.update( |
| choices=target_face_choices, |
| value=target_face_value |
| ), |
|
|
| ( |
| f"Preview completed: " |
| f"{len(source_paths)} source / " |
| f"{len(target_paths)} target" |
| ) |
| ) |
|
|
| |
| |
| |
|
|
| def source_face_choices_from_source( |
| source_files, |
| source_selector |
| ): |
|
|
| paths = get_file_paths( |
| source_files |
| ) |
|
|
| if not paths: |
|
|
| return gr.update( |
| choices=[], |
| value=None |
| ) |
|
|
| index = 0 |
|
|
| if source_selector: |
|
|
| try: |
|
|
| index = int( |
| source_selector.split( |
| ":", |
| 1 |
| )[0].replace( |
| "SOURCE", |
| "" |
| ).strip() |
| ) |
|
|
| except Exception: |
|
|
| index = 0 |
|
|
| if index < 0 or index >= len(paths): |
| index = 0 |
|
|
| faces = detect_reactor_faces( |
| paths[index] |
| ) |
|
|
| choices = [ |
|
|
| f"SOURCE {index} FACE {i}" |
|
|
| for i in range( |
| len(faces) |
| ) |
| ] |
|
|
| return gr.update( |
| choices=choices, |
| value=( |
| choices[0] |
| if choices |
| else None |
| ) |
| ) |
|
|
| |
| |
| |
|
|
| def target_face_choices_from_target( |
| target_files, |
| target_selector |
| ): |
|
|
| paths = get_file_paths( |
| target_files |
| ) |
|
|
| if not paths: |
|
|
| return gr.update( |
| choices=[], |
| value=None |
| ) |
|
|
| index = 0 |
|
|
| if target_selector: |
|
|
| try: |
|
|
| index = int( |
| target_selector.split( |
| ":", |
| 1 |
| )[0].replace( |
| "TARGET", |
| "" |
| ).strip() |
| ) |
|
|
| except Exception: |
|
|
| index = 0 |
|
|
| if index < 0 or index >= len(paths): |
| index = 0 |
|
|
| faces = detect_reactor_faces( |
| paths[index] |
| ) |
|
|
| choices = [ |
|
|
| f"TARGET {index} FACE {i}" |
|
|
| for i in range( |
| len(faces) |
| ) |
| ] |
|
|
| return gr.update( |
| choices=choices, |
| value=( |
| choices[0] |
| if choices |
| else None |
| ) |
| ) |
|
|
| |
| |
| |
|
|
| def parse_source_face( |
| value |
| ): |
|
|
| if not value: |
|
|
| return ( |
| -1, |
| -1 |
| ) |
|
|
| text = str( |
| value |
| ).upper() |
|
|
| try: |
|
|
| source_index = int( |
| text.split( |
| "SOURCE", |
| 1 |
| )[1].split( |
| "FACE", |
| 1 |
| )[0].strip() |
| ) |
|
|
| face_index = int( |
| text.split( |
| "FACE", |
| 1 |
| )[1].strip() |
| ) |
|
|
| return ( |
| source_index, |
| face_index |
| ) |
|
|
| except Exception: |
|
|
| return ( |
| -1, |
| -1 |
| ) |
|
|
| |
| |
| |
|
|
| def parse_target_face( |
| value |
| ): |
|
|
| if not value: |
|
|
| return ( |
| -1, |
| -1 |
| ) |
|
|
| text = str( |
| value |
| ).upper() |
|
|
| try: |
|
|
| target_index = int( |
| text.split( |
| "TARGET", |
| 1 |
| )[1].split( |
| "FACE", |
| 1 |
| )[0].strip() |
| ) |
|
|
| face_index = int( |
| text.split( |
| "FACE", |
| 1 |
| )[1].strip() |
| ) |
|
|
| return ( |
| target_index, |
| face_index |
| ) |
|
|
| except Exception: |
|
|
| return ( |
| -1, |
| -1 |
| ) |
|
|
| |
| |
| |
|
|
| def normalize_mapping_line( |
| source_face, |
| target_face |
| ): |
|
|
| source_index, source_face_index = ( |
| parse_source_face( |
| source_face |
| ) |
| ) |
|
|
| target_index, target_face_index = ( |
| parse_target_face( |
| target_face |
| ) |
| ) |
|
|
| if ( |
| source_index < 0 |
| or source_face_index < 0 |
| or target_index < 0 |
| or target_face_index < 0 |
| ): |
|
|
| return None |
|
|
| return ( |
| f"SOURCE {source_index} " |
| f"FACE {source_face_index} " |
| f"-> TARGET {target_index} " |
| f"FACE {target_face_index}" |
| ) |
|
|
| |
| |
| |
|
|
| def parse_mappings( |
| mapping_text |
| ): |
|
|
| mappings = [] |
|
|
| if not mapping_text: |
| return mappings |
|
|
| for raw in str( |
| mapping_text |
| ).splitlines(): |
|
|
| line = raw.strip() |
|
|
| if not line: |
| continue |
|
|
| upper = line.upper() |
|
|
| if ( |
| "SOURCE" not in upper |
| or "TARGET" not in upper |
| or "FACE" not in upper |
| ): |
|
|
| continue |
|
|
| try: |
|
|
| left, right = ( |
| upper.split( |
| "->", |
| 1 |
| ) |
| ) |
|
|
| s_index = int( |
| left.split( |
| "SOURCE", |
| 1 |
| )[1].split( |
| "FACE", |
| 1 |
| )[0].strip() |
| ) |
|
|
| s_face = int( |
| left.split( |
| "FACE", |
| 1 |
| )[1].strip() |
| ) |
|
|
| t_index = int( |
| right.split( |
| "TARGET", |
| 1 |
| )[1].split( |
| "FACE", |
| 1 |
| )[0].strip() |
| ) |
|
|
| t_face = int( |
| right.split( |
| "FACE", |
| 1 |
| )[1].strip() |
| ) |
|
|
| mappings.append( |
| { |
| "source_index": s_index, |
| "source_face": s_face, |
| "target_index": t_index, |
| "target_face": t_face |
| } |
| ) |
|
|
| except Exception: |
|
|
| continue |
|
|
| return mappings |
|
|
| |
| |
| |
|
|
| def default_mapping(): |
|
|
| return ( |
| "SOURCE 0 FACE 0 " |
| "-> TARGET 0 FACE 0" |
| ) |
|
|
| |
| |
| |
|
|
| def add_mapping( |
| mapping_text, |
| source_face, |
| target_face |
| ): |
|
|
| line = normalize_mapping_line( |
| source_face, |
| target_face |
| ) |
|
|
| if not line: |
|
|
| raise gr.Error( |
| "Mapping không hợp lệ." |
| ) |
|
|
| existing = [] |
|
|
| if mapping_text: |
|
|
| existing = [ |
|
|
| x.strip() |
|
|
| for x in str( |
| mapping_text |
| ).splitlines() |
|
|
| if x.strip() |
| ] |
|
|
| if line not in existing: |
|
|
| existing.append( |
| line |
| ) |
|
|
| return "\n".join( |
| existing |
| ) |
|
|
| |
| |
| |
|
|
| def remove_last_mapping( |
| mapping_text |
| ): |
|
|
| lines = [ |
|
|
| x.strip() |
|
|
| for x in str( |
| mapping_text or "" |
| ).splitlines() |
|
|
| if x.strip() |
| ] |
|
|
| if lines: |
| lines.pop() |
|
|
| return "\n".join( |
| lines |
| ) |
|
|
| |
| |
| |
|
|
| def clear_mapping(): |
|
|
| return "" |
|
|
| |
| |
| |
|
|
| def validate_mappings( |
| mappings, |
| source_paths, |
| target_paths |
| ): |
|
|
| if not mappings: |
|
|
| raise gr.Error( |
| "Chưa có Face Mapping." |
| ) |
|
|
| for mapping in mappings: |
|
|
| si = mapping[ |
| "source_index" |
| ] |
|
|
| sf = mapping[ |
| "source_face" |
| ] |
|
|
| ti = mapping[ |
| "target_index" |
| ] |
|
|
| tf = mapping[ |
| "target_face" |
| ] |
|
|
| if ( |
| si < 0 |
| or si >= len(source_paths) |
| ): |
|
|
| raise gr.Error( |
| f"Source index không tồn tại: {si}" |
| ) |
|
|
| if ( |
| ti < 0 |
| or ti >= len(target_paths) |
| ): |
|
|
| raise gr.Error( |
| f"Target index không tồn tại: {ti}" |
| ) |
|
|
| source_faces = ( |
| detect_reactor_faces( |
| source_paths[si] |
| ) |
| ) |
|
|
| target_faces = ( |
| detect_reactor_faces( |
| target_paths[ti] |
| ) |
| ) |
|
|
| if ( |
| sf < 0 |
| or sf >= len(source_faces) |
| ): |
|
|
| raise gr.Error( |
| f"SOURCE {si} FACE {sf} " |
| "không tồn tại." |
| ) |
|
|
| if ( |
| tf < 0 |
| or tf >= len(target_faces) |
| ): |
|
|
| raise gr.Error( |
| f"TARGET {ti} FACE {tf} " |
| "không tồn tại." |
| ) |
|
|
| |
| |
| |
|
|
| def comfy_load_image( |
| path |
| ): |
|
|
| result = loadimage.load_image( |
| image=path |
| ) |
|
|
| if result is None: |
|
|
| raise RuntimeError( |
| "ComfyUI LoadImage returned None." |
| ) |
|
|
| if isinstance( |
| result, |
| dict |
| ): |
|
|
| values = list( |
| result.values() |
| ) |
|
|
| if not values: |
|
|
| raise RuntimeError( |
| "LoadImage returned empty dict." |
| ) |
|
|
| image = values[0] |
|
|
| else: |
|
|
| try: |
|
|
| image = result[0] |
|
|
| except Exception: |
|
|
| image = result |
|
|
| if image is None: |
|
|
| raise RuntimeError( |
| "Loaded image is None." |
| ) |
|
|
| return image |
|
|
| |
| |
| |
|
|
| def load_source_cached( |
| path |
| ): |
|
|
| try: |
|
|
| mtime = os.path.getmtime( |
| path |
| ) |
|
|
| except Exception: |
|
|
| mtime = 0 |
|
|
| key = ( |
| path, |
| mtime |
| ) |
|
|
| if key in source_tensor_cache: |
|
|
| value = ( |
| source_tensor_cache.pop( |
| key |
| ) |
| ) |
|
|
| source_tensor_cache[ |
| key |
| ] = value |
|
|
| return value |
|
|
| image = comfy_load_image( |
| path |
| ) |
|
|
| source_tensor_cache[ |
| key |
| ] = image |
|
|
| while ( |
| len(source_tensor_cache) |
| > SOURCE_TENSOR_CACHE_MAX |
| ): |
|
|
| source_tensor_cache.popitem( |
| last=False |
| ) |
|
|
| return image |
|
|
| |
| |
| |
|
|
| def get_value_at_index( |
| obj, |
| index |
| ): |
|
|
| if obj is None: |
|
|
| raise RuntimeError( |
| "ComfyUI node returned None." |
| ) |
|
|
| if isinstance( |
| obj, |
| dict |
| ): |
|
|
| values = list( |
| obj.values() |
| ) |
|
|
| if index >= len(values): |
|
|
| raise RuntimeError( |
| "ComfyUI output index out of range." |
| ) |
|
|
| return values[index] |
|
|
| return obj[index] |
|
|
| |
| |
| |
|
|
| def result_to_pil( |
| result |
| ): |
|
|
| if result is None: |
|
|
| raise RuntimeError( |
| "ReActor output is None." |
| ) |
|
|
| try: |
|
|
| value = get_value_at_index( |
| result, |
| 0 |
| ) |
|
|
| except Exception: |
|
|
| value = result |
|
|
| if value is None: |
|
|
| raise RuntimeError( |
| "ReActor image output is None." |
| ) |
|
|
| if isinstance( |
| value, |
| (list, tuple) |
| ): |
|
|
| if not value: |
|
|
| raise RuntimeError( |
| "ReActor returned empty image." |
| ) |
|
|
| value = value[0] |
|
|
| if hasattr( |
| value, |
| "detach" |
| ): |
|
|
| value = ( |
| value |
| .detach() |
| .cpu() |
| .float() |
| .numpy() |
| ) |
|
|
| value = np.asarray( |
| value |
| ) |
|
|
| if value.ndim == 4: |
|
|
| value = value[0] |
|
|
| if value.ndim != 3: |
|
|
| raise RuntimeError( |
| "Invalid ReActor output shape: " |
| + str(value.shape) |
| ) |
|
|
| if value.shape[-1] != 3: |
|
|
| if value.shape[0] == 3: |
|
|
| value = np.transpose( |
| value, |
| (1, 2, 0) |
| ) |
|
|
| else: |
|
|
| raise RuntimeError( |
| "Invalid image channels: " |
| + str(value.shape) |
| ) |
|
|
| if value.max() <= 1.0: |
|
|
| value = value * 255.0 |
|
|
| value = np.clip( |
| value, |
| 0, |
| 255 |
| ).astype( |
| np.uint8 |
| ) |
|
|
| return Image.fromarray( |
| value |
| ).convert( |
| "RGB" |
| ) |
|
|
| |
| |
| |
|
|
| def pil_to_comfy_image( |
| image |
| ): |
|
|
| if image is None: |
|
|
| raise RuntimeError( |
| "Cannot convert None image to ComfyUI IMAGE." |
| ) |
|
|
| if isinstance( |
| image, |
| Image.Image |
| ): |
|
|
| pil = image.convert( |
| "RGB" |
| ) |
|
|
| array = np.asarray( |
| pil, |
| dtype=np.float32 |
| ) |
|
|
| else: |
|
|
| array = np.asarray( |
| image |
| ) |
|
|
| if array.ndim == 2: |
|
|
| array = np.stack( |
| [ |
| array, |
| array, |
| array |
| ], |
| axis=-1 |
| ) |
|
|
| if array.ndim == 3: |
|
|
| if array.shape[-1] == 4: |
|
|
| array = ( |
| array[ |
| :, :, :3 |
| ] |
| ) |
|
|
| elif array.ndim == 4: |
|
|
| if array.shape[0] == 1: |
|
|
| array = array[0] |
|
|
| else: |
|
|
| raise RuntimeError( |
| "Invalid image shape for " |
| "ComfyUI conversion: " |
| + str(array.shape) |
| ) |
|
|
| array = array.astype( |
| np.float32, |
| copy=False |
| ) |
|
|
| if array.max() > 1.0: |
|
|
| array /= 255.0 |
|
|
| array = np.clip( |
| array, |
| 0.0, |
| 1.0 |
| ) |
|
|
| tensor = torch.from_numpy( |
| array |
| ) |
|
|
| if tensor.ndim == 3: |
|
|
| tensor = tensor.unsqueeze( |
| 0 |
| ) |
|
|
| if tensor.ndim != 4: |
|
|
| raise RuntimeError( |
| "Invalid converted ComfyUI " |
| "IMAGE tensor shape: " |
| + str(tuple(tensor.shape)) |
| ) |
|
|
| if tensor.shape[-1] != 3: |
|
|
| raise RuntimeError( |
| "Invalid converted image channels: " |
| + str(tuple(tensor.shape)) |
| ) |
|
|
| return tensor.contiguous() |
|
|
| |
| |
| |
|
|
| def normalize_target_image( |
| image |
| ): |
|
|
| if image is None: |
| return None |
|
|
| if torch.is_tensor( |
| image |
| ): |
|
|
| tensor = image |
|
|
| if tensor.ndim == 3: |
|
|
| tensor = tensor.unsqueeze( |
| 0 |
| ) |
|
|
| if tensor.ndim != 4: |
|
|
| raise RuntimeError( |
| "Invalid ComfyUI IMAGE " |
| "tensor shape: " |
| + str(tuple(tensor.shape)) |
| ) |
|
|
| if tensor.shape[-1] != 3: |
|
|
| raise RuntimeError( |
| "Invalid ComfyUI IMAGE " |
| "channels: " |
| + str(tuple(tensor.shape)) |
| ) |
|
|
| if tensor.dtype != torch.float32: |
|
|
| tensor = tensor.float() |
|
|
| if tensor.numel() > 0: |
|
|
| max_value = float( |
| tensor.detach() |
| .max() |
| .cpu() |
| ) |
|
|
| if max_value > 1.0: |
|
|
| tensor = ( |
| tensor / 255.0 |
| ) |
|
|
| return tensor.clamp( |
| 0.0, |
| 1.0 |
| ).contiguous() |
|
|
| return pil_to_comfy_image( |
| image |
| ) |
|
|
| |
| |
| |
|
|
| def reactor_swap( |
| source_image, |
| target_image, |
| source_face_index, |
| target_face_index, |
| swap_model, |
| restore_model, |
| restore_strength |
| ): |
|
|
| source_image = normalize_target_image( |
| source_image |
| ) |
|
|
| target_image = normalize_target_image( |
| target_image |
| ) |
|
|
| if source_image is None: |
|
|
| raise RuntimeError( |
| "ReActor source_image is None." |
| ) |
|
|
| if target_image is None: |
|
|
| raise RuntimeError( |
| "ReActor target_image is None." |
| ) |
|
|
| swap_model_path = ensure_model( |
| swap_model |
| ) |
|
|
| if not swap_model_path: |
|
|
| raise RuntimeError( |
| "Swap model unavailable." |
| ) |
|
|
| restore_name = ( |
| restore_model |
| if restore_model |
| else "none" |
| ) |
|
|
| if restore_name != "none": |
|
|
| restore_path = ensure_model( |
| restore_name |
| ) |
|
|
| log( |
| "[RESTORE] Model path:", |
| restore_path |
| ) |
|
|
| if not restore_path: |
|
|
| log( |
| "[RESTORE] Model unavailable. " |
| "Disabling restore." |
| ) |
|
|
| restore_name = "none" |
|
|
| kwargs = { |
|
|
| "enabled": True, |
|
|
| "swap_model": swap_model, |
|
|
| "facedetection": ( |
| "retinaface_resnet50" |
| ), |
|
|
| "face_restore_model": ( |
| restore_name |
| ), |
|
|
| "face_restore_visibility": ( |
| float( |
| restore_strength |
| ) |
| ), |
|
|
| "codeformer_weight": 0.5, |
|
|
| "detect_gender_input": "no", |
|
|
| "detect_gender_source": "no", |
|
|
| "input_faces_index": str( |
| target_face_index |
| ), |
|
|
| "source_faces_index": str( |
| source_face_index |
| ), |
|
|
| "console_log_level": 1, |
|
|
| "input_image": target_image, |
|
|
| "source_image": source_image |
| } |
|
|
| try: |
|
|
| result = reactorfaceswap.execute( |
| **kwargs |
| ) |
|
|
| return result_to_pil( |
| result |
| ) |
|
|
| except TypeError as e: |
|
|
| message = str(e) |
|
|
| if ( |
| restore_name != "none" |
| and ( |
| "NoneType" in message |
| or "Unable to load" in message |
| or "No such file" in message |
| or "not found" in message.lower() |
| ) |
| ): |
|
|
| log( |
| "[RESTORE FALLBACK]", |
| message |
| ) |
|
|
| kwargs[ |
| "face_restore_model" |
| ] = "none" |
|
|
| kwargs[ |
| "face_restore_visibility" |
| ] = 0.0 |
|
|
| result = reactorfaceswap.execute( |
| **kwargs |
| ) |
|
|
| return result_to_pil( |
| result |
| ) |
|
|
| raise |
|
|
| except Exception as e: |
|
|
| message = str(e) |
|
|
| if ( |
| restore_name != "none" |
| and ( |
| "NoneType" in message |
| or "Unable to load" in message |
| or "No such file" in message |
| or "not found" in message.lower() |
| ) |
| ): |
|
|
| log( |
| "[RESTORE FALLBACK]", |
| message |
| ) |
|
|
| kwargs[ |
| "face_restore_model" |
| ] = "none" |
|
|
| kwargs[ |
| "face_restore_visibility" |
| ] = 0.0 |
|
|
| result = reactorfaceswap.execute( |
| **kwargs |
| ) |
|
|
| return result_to_pil( |
| result |
| ) |
|
|
| raise |
|
|
| |
| |
| |
|
|
| def swap_one_image( |
| source_path, |
| target_path, |
| source_face_index, |
| target_face_index, |
| swap_model, |
| restore_model, |
| restore_strength, |
| target_image_override=None |
| ): |
|
|
| source_tensor = load_source_cached( |
| source_path |
| ) |
|
|
| source_tensor = normalize_target_image( |
| source_tensor |
| ) |
|
|
| if target_image_override is None: |
|
|
| target_tensor = comfy_load_image( |
| target_path |
| ) |
|
|
| else: |
|
|
| target_tensor = normalize_target_image( |
| target_image_override |
| ) |
|
|
| if source_tensor is None: |
|
|
| raise RuntimeError( |
| "Source tensor is None." |
| ) |
|
|
| if target_tensor is None: |
|
|
| raise RuntimeError( |
| "Target tensor is None." |
| ) |
|
|
| log( |
| "[SWAP] Source type:", |
| type(source_tensor), |
| "shape:", |
| getattr( |
| source_tensor, |
| "shape", |
| None |
| ) |
| ) |
|
|
| log( |
| "[SWAP] Target type:", |
| type(target_tensor), |
| "shape:", |
| getattr( |
| target_tensor, |
| "shape", |
| None |
| ) |
| ) |
|
|
| with INFERENCE_SEMAPHORE: |
|
|
| result_image = reactor_swap( |
|
|
| source_tensor, |
|
|
| target_tensor, |
|
|
| source_face_index, |
|
|
| target_face_index, |
|
|
| swap_model, |
|
|
| restore_model, |
|
|
| restore_strength |
| ) |
|
|
| if result_image is None: |
|
|
| raise RuntimeError( |
| "Swap returned None." |
| ) |
|
|
| return result_image |
|
|
| |
| |
| |
|
|
| def process_gif_mapping( |
| source_path, |
| target_path, |
| source_face_index, |
| target_face_index, |
| swap_model, |
| restore_model, |
| restore_strength |
| ): |
|
|
| reader = imageio.get_reader( |
| target_path |
| ) |
|
|
| frames = [] |
| durations = [] |
|
|
| try: |
|
|
| try: |
|
|
| meta = reader.get_meta_data() |
|
|
| except Exception: |
|
|
| meta = {} |
|
|
| duration = meta.get( |
| "duration", |
| 100 |
| ) |
|
|
| fps = meta.get( |
| "fps", |
| None |
| ) |
|
|
| if fps and not duration: |
|
|
| duration = ( |
| 1000.0 / |
| float(fps) |
| ) |
|
|
| for frame_index, frame in enumerate( |
| reader |
| ): |
|
|
| frame_pil = ( |
| Image.fromarray( |
| frame |
| ).convert( |
| "RGB" |
| ) |
| ) |
|
|
| with tempfile.NamedTemporaryFile( |
| suffix=".png", |
| delete=False |
| ) as tmp: |
|
|
| temp_path = tmp.name |
|
|
| try: |
|
|
| frame_pil.save( |
| temp_path |
| ) |
|
|
| result = swap_one_image( |
|
|
| source_path, |
|
|
| temp_path, |
|
|
| source_face_index, |
|
|
| target_face_index, |
|
|
| swap_model, |
|
|
| restore_model, |
|
|
| restore_strength |
| ) |
|
|
| frames.append( |
| result |
| ) |
|
|
| durations.append( |
| duration |
| ) |
|
|
| finally: |
|
|
| try: |
|
|
| os.remove( |
| temp_path |
| ) |
|
|
| except Exception: |
| pass |
|
|
| if ( |
| frame_index % 5 |
| == 0 |
| ): |
|
|
| gc.collect() |
|
|
| finally: |
|
|
| reader.close() |
|
|
| if not frames: |
|
|
| raise gr.Error( |
| "GIF không có frame." |
| ) |
|
|
| return ( |
| frames, |
| durations |
| ) |
|
|
| |
| |
| |
|
|
| def safe_name( |
| value |
| ): |
|
|
| result = [] |
|
|
| for char in str( |
| value |
| ): |
|
|
| if ( |
| char.isalnum() |
| or char in ( |
| "-", |
| "_", |
| "." |
| ) |
| ): |
|
|
| result.append( |
| char |
| ) |
|
|
| else: |
|
|
| result.append( |
| "_" |
| ) |
|
|
| return "".join( |
| result |
| ) |
|
|
|
|
| def make_output_path( |
| source_path, |
| target_path, |
| batch_index |
| ): |
|
|
| source_name = safe_name( |
| os.path.splitext( |
| os.path.basename( |
| source_path |
| ) |
| )[0] |
| ) |
|
|
| target_name = safe_name( |
| os.path.splitext( |
| os.path.basename( |
| target_path |
| ) |
| )[0] |
| ) |
|
|
| filename = ( |
| f"{source_name}" |
| f"_TO_" |
| f"{target_name}" |
| f"_{batch_index:04d}.webp" |
| ) |
|
|
| return os.path.join( |
| OUTPUT_DIR, |
| filename |
| ) |
|
|
| |
| |
| |
|
|
| def generate_image( |
| source_files, |
| target_files, |
| mapping_text, |
| swap_model, |
| restore_model, |
| restore_strength |
| ): |
|
|
| source_paths = get_file_paths( |
| source_files |
| ) |
|
|
| target_paths = get_file_paths( |
| target_files |
| ) |
|
|
| if not source_paths: |
|
|
| raise gr.Error( |
| "Chưa upload Source." |
| ) |
|
|
| if not target_paths: |
|
|
| raise gr.Error( |
| "Chưa upload Target." |
| ) |
|
|
| mappings = parse_mappings( |
| mapping_text |
| ) |
|
|
| if not mappings: |
|
|
| mappings = [ |
| { |
| "source_index": 0, |
| "source_face": 0, |
| "target_index": 0, |
| "target_face": 0 |
| } |
| ] |
|
|
| if not swap_model: |
|
|
| swap_model = ( |
| DEFAULT_SWAP_MODEL |
| ) |
|
|
| if not restore_model: |
|
|
| restore_model = ( |
| DEFAULT_RESTORE_MODEL |
| ) |
|
|
| restore_strength = float( |
| restore_strength |
| if restore_strength is not None |
| else DEFAULT_RESTORE_STRENGTH |
| ) |
|
|
| log("=" * 70) |
|
|
| log( |
| "[GENERATE] START" |
| ) |
|
|
| log( |
| "[GENERATE] Sources:", |
| len(source_paths) |
| ) |
|
|
| log( |
| "[GENERATE] Targets:", |
| len(target_paths) |
| ) |
|
|
| log( |
| "[GENERATE] Mappings:", |
| len(mappings) |
| ) |
|
|
| log( |
| "[GENERATE] Swap:", |
| swap_model |
| ) |
|
|
| log( |
| "[GENERATE] Restore:", |
| restore_model |
| ) |
|
|
| log( |
| "[GENERATE] Strength:", |
| restore_strength |
| ) |
|
|
| log("=" * 70) |
|
|
| with JOB_SEMAPHORE: |
|
|
| validate_mappings( |
| mappings, |
| source_paths, |
| target_paths |
| ) |
|
|
| ensure_model( |
| swap_model |
| ) |
|
|
| if ( |
| restore_model |
| and restore_model != "none" |
| ): |
|
|
| ensure_model( |
| restore_model |
| ) |
|
|
| output_paths = [] |
|
|
| |
| |
| |
|
|
| mappings_by_target = {} |
|
|
| for mapping in mappings: |
|
|
| target_index = mapping[ |
| "target_index" |
| ] |
|
|
| mappings_by_target.setdefault( |
| target_index, |
| [] |
| ).append( |
| mapping |
| ) |
|
|
| |
| |
| |
|
|
| for target_index, target_path in enumerate( |
| target_paths |
| ): |
|
|
| target_mappings = ( |
| mappings_by_target.get( |
| target_index, |
| [] |
| ) |
| ) |
|
|
| if not target_mappings: |
| continue |
|
|
| log( |
| "[TARGET]", |
| target_index, |
| target_path |
| ) |
|
|
| |
| |
| |
|
|
| if target_path.lower().endswith( |
| ".gif" |
| ): |
|
|
| reader = imageio.get_reader( |
| target_path |
| ) |
|
|
| frames = [] |
| durations = [] |
|
|
| try: |
|
|
| try: |
|
|
| meta = reader.get_meta_data() |
|
|
| except Exception: |
|
|
| meta = {} |
|
|
| duration = meta.get( |
| "duration", |
| 100 |
| ) |
|
|
| for frame_index, frame in enumerate( |
| reader |
| ): |
|
|
| current = ( |
| Image.fromarray( |
| frame |
| ).convert( |
| "RGB" |
| ) |
| ) |
|
|
| for mapping in target_mappings: |
|
|
| source_index = mapping[ |
| "source_index" |
| ] |
|
|
| source_face = mapping[ |
| "source_face" |
| ] |
|
|
| target_face = mapping[ |
| "target_face" |
| ] |
|
|
| source_path = ( |
| source_paths[ |
| source_index |
| ] |
| ) |
|
|
| with tempfile.NamedTemporaryFile( |
| suffix=".png", |
| delete=False |
| ) as tmp: |
|
|
| temp_path = tmp.name |
|
|
| try: |
|
|
| current.save( |
| temp_path |
| ) |
|
|
| current = ( |
| swap_one_image( |
|
|
| source_path, |
|
|
| temp_path, |
|
|
| source_face, |
|
|
| target_face, |
|
|
| swap_model, |
|
|
| restore_model, |
|
|
| restore_strength, |
|
|
| target_image_override=( |
| current |
| ) |
| ) |
| ) |
|
|
| finally: |
|
|
| try: |
|
|
| os.remove( |
| temp_path |
| ) |
|
|
| except Exception: |
| pass |
|
|
| frames.append( |
| current |
| ) |
|
|
| durations.append( |
| duration |
| ) |
|
|
| if ( |
| frame_index % 3 |
| == 0 |
| ): |
|
|
| gc.collect() |
|
|
| finally: |
|
|
| reader.close() |
|
|
| if not frames: |
|
|
| raise gr.Error( |
| "GIF không có frame." |
| ) |
|
|
| output_path = ( |
| make_output_path( |
|
|
| source_paths[ |
| target_mappings[0][ |
| "source_index" |
| ] |
| ], |
|
|
| target_path, |
|
|
| target_index |
| ) |
| ) |
|
|
| frames[0].save( |
|
|
| output_path, |
|
|
| save_all=True, |
|
|
| append_images=frames[1:], |
|
|
| duration=durations, |
|
|
| loop=0, |
|
|
| format="WEBP", |
|
|
| quality=90, |
|
|
| method=6 |
| ) |
|
|
| output_paths.append( |
| output_path |
| ) |
|
|
| del frames |
| del durations |
|
|
| gc.collect() |
|
|
| |
| |
| |
|
|
| else: |
|
|
| current_path = target_path |
|
|
| current_image = None |
|
|
| first_source_path = None |
|
|
| for mapping_index, mapping in enumerate( |
| target_mappings |
| ): |
|
|
| source_index = mapping[ |
| "source_index" |
| ] |
|
|
| source_face = mapping[ |
| "source_face" |
| ] |
|
|
| target_face = mapping[ |
| "target_face" |
| ] |
|
|
| source_path = ( |
| source_paths[ |
| source_index |
| ] |
| ) |
|
|
| if first_source_path is None: |
|
|
| first_source_path = ( |
| source_path |
| ) |
|
|
| log( |
| "[MAPPING]", |
| ( |
| f"SOURCE {source_index} " |
| f"FACE {source_face} " |
| f"-> TARGET {target_index} " |
| f"FACE {target_face}" |
| ) |
| ) |
|
|
| current_image = ( |
| swap_one_image( |
|
|
| source_path, |
|
|
| current_path, |
|
|
| source_face, |
|
|
| target_face, |
|
|
| swap_model, |
|
|
| restore_model, |
|
|
| restore_strength, |
|
|
| target_image_override=( |
| current_image |
| ) |
| ) |
| ) |
|
|
| if current_image is None: |
|
|
| raise RuntimeError( |
| "Mapping output is None." |
| ) |
|
|
| current_path = None |
|
|
| if current_image is None: |
|
|
| raise RuntimeError( |
| "No output generated." |
| ) |
|
|
| output_path = ( |
| make_output_path( |
|
|
| first_source_path, |
|
|
| target_path, |
|
|
| target_index |
| ) |
| ) |
|
|
| current_image.save( |
|
|
| output_path, |
|
|
| format="WEBP", |
|
|
| quality=90, |
|
|
| method=6 |
| ) |
|
|
| output_paths.append( |
| output_path |
| ) |
|
|
| del current_image |
|
|
| gc.collect() |
|
|
| gc.collect() |
|
|
| log( |
| "[GENERATE] OUTPUT:", |
| output_paths |
| ) |
|
|
| return output_paths |
|
|
| |
| |
| |
|
|
| def use_output_as_target( |
| output_files |
| ): |
|
|
| paths = get_file_paths( |
| output_files |
| ) |
|
|
| if not paths: |
|
|
| raise gr.Error( |
| "Chưa có output." |
| ) |
|
|
| valid = [ |
|
|
| p |
|
|
| for p in paths |
|
|
| if os.path.isfile(p) |
| ] |
|
|
| if not valid: |
|
|
| raise gr.Error( |
| "Không tìm thấy output." |
| ) |
|
|
| return valid |
|
|
| |
| |
| |
|
|
| def clear_target(): |
|
|
| clear_face_cache() |
|
|
| return ( |
|
|
| [], |
|
|
| [], |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| "Target cleared." |
| ) |
|
|
| |
| |
| |
|
|
| def clear_source(): |
|
|
| clear_face_cache() |
|
|
| source_tensor_cache.clear() |
|
|
| return ( |
|
|
| [], |
|
|
| [], |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| gr.update( |
| choices=[], |
| value=None |
| ), |
|
|
| "Source cleared." |
| ) |
|
|
| |
| |
| |
|
|
| def save_mapping_json( |
| mapping_text |
| ): |
|
|
| mappings = parse_mappings( |
| mapping_text |
| ) |
|
|
| if not mappings: |
|
|
| raise gr.Error( |
| "Mapping trống." |
| ) |
|
|
| fd, path = tempfile.mkstemp( |
| suffix=".json", |
| prefix="face_mapping_" |
| ) |
|
|
| os.close(fd) |
|
|
| with open( |
| path, |
| "w", |
| encoding="utf-8" |
| ) as f: |
|
|
| json.dump( |
|
|
| mappings, |
|
|
| f, |
|
|
| ensure_ascii=False, |
|
|
| indent=2 |
| ) |
|
|
| return path |
|
|
| |
| |
| |
|
|
| def load_mapping_json( |
| mapping_file |
| ): |
|
|
| path = get_file_path( |
| mapping_file |
| ) |
|
|
| if not path: |
|
|
| raise gr.Error( |
| "Chưa chọn mapping JSON." |
| ) |
|
|
| with open( |
| path, |
| "r", |
| encoding="utf-8" |
| ) as f: |
|
|
| data = json.load(f) |
|
|
| lines = [] |
|
|
| for item in data: |
|
|
| try: |
|
|
| lines.append( |
|
|
| f"SOURCE " |
| f"{int(item['source_index'])} " |
| f"FACE " |
| f"{int(item['source_face'])} " |
| f"-> TARGET " |
| f"{int(item['target_index'])} " |
| f"FACE " |
| f"{int(item['target_face'])}" |
|
|
| ) |
|
|
| except Exception: |
|
|
| continue |
|
|
| return "\n".join( |
| lines |
| ) |
|
|
| |
| |
| |
|
|
| def auto_mapping(source_files, target_files): |
| source_paths = get_file_paths(source_files) |
| target_paths = get_file_paths(target_files) |
|
|
| if not source_paths: |
| raise gr.Error("Chưa có source.") |
|
|
| if not target_paths: |
| raise gr.Error("Chưa có target.") |
|
|
| source_index = 0 |
| source_face_index = 0 |
|
|
| |
| source_faces = detect_reactor_faces(source_paths[source_index]) |
|
|
| if not source_faces: |
| raise gr.Error("Source đầu tiên không có face.") |
|
|
| if source_face_index >= len(source_faces): |
| raise gr.Error( |
| f"Source đầu tiên chỉ có {len(source_faces)} face." |
| ) |
|
|
| lines = [] |
| skipped_targets = [] |
|
|
| |
| for target_index, target_path in enumerate(target_paths): |
| try: |
| target_faces = detect_reactor_faces(target_path) |
| except Exception: |
| target_faces = [] |
|
|
| |
| if not target_faces: |
| skipped_targets.append(target_index) |
| continue |
|
|
| |
| for target_face_index in range(len(target_faces)): |
| lines.append( |
| f"SOURCE {source_index} " |
| f"FACE {source_face_index} " |
| f"-> TARGET {target_index} " |
| f"FACE {target_face_index}" |
| ) |
|
|
| if not lines: |
| raise gr.Error( |
| "Không có target nào phát hiện được face." |
| ) |
|
|
| |
| if skipped_targets: |
| lines.append("") |
| lines.append( |
| "# BỎ QUA TARGET KHÔNG CÓ FACE: " |
| + ", ".join(map(str, skipped_targets)) |
| ) |
|
|
| return "\n".join(lines) |
|
|
| |
| |
| |
|
|
| def status_text( |
| source_files, |
| target_files, |
| mapping_text |
| ): |
|
|
| sources = len( |
| get_file_paths( |
| source_files |
| ) |
| ) |
|
|
| targets = len( |
| get_file_paths( |
| target_files |
| ) |
| ) |
|
|
| mappings = len( |
| parse_mappings( |
| mapping_text |
| ) |
| ) |
|
|
| return ( |
|
|
| f"Sources: {sources} | " |
|
|
| f"Targets: {targets} | " |
|
|
| f"Mappings: {mappings} | " |
|
|
| f"Workers: {MAX_CONCURRENT_JOBS}" |
| ) |
|
|
| |
| |
| |
|
|
| def toggle_output_preview( |
| is_visible |
| ): |
|
|
| new_visible = not bool( |
| is_visible |
| ) |
|
|
| return ( |
|
|
| gr.update( |
| visible=new_visible |
| ), |
|
|
| new_visible, |
|
|
| ( |
| "Hide Output Preview" |
| if new_visible |
| else "Show Output Preview" |
| ) |
| ) |
|
|
| |
| |
| |
|
|
| with gr.Blocks( |
| title=APP_TITLE |
| ) as app: |
|
|
| |
| |
| |
|
|
| with gr.Row(): |
|
|
| source_files = gr.File( |
|
|
| label="Source Images", |
|
|
| file_count="multiple", |
|
|
| file_types=[ |
| ".jpg", |
| ".jpeg", |
| ".png", |
| ".webp" |
| ], |
|
|
| type="filepath" |
| ) |
|
|
| target_files = gr.File( |
|
|
| label="Target Images / GIF", |
|
|
| file_count="multiple", |
|
|
| file_types=[ |
| ".jpg", |
| ".jpeg", |
| ".png", |
| ".webp", |
| ".gif" |
| ], |
|
|
| type="filepath" |
| ) |
|
|
| |
| |
| |
|
|
| with gr.Row(): |
|
|
| preview_button = gr.Button( |
| "Preview All Faces", |
| variant="primary" |
| ) |
|
|
| clear_source_button = gr.Button( |
| "Clear Source" |
| ) |
|
|
| clear_target_button = gr.Button( |
| "Clear Target" |
| ) |
|
|
| |
| |
| |
|
|
| source_gallery = gr.Gallery( |
|
|
| label="Source Face Review — Face Crops", |
|
|
| columns=4, |
|
|
| rows=2, |
|
|
| height="auto", |
|
|
| object_fit="contain", |
|
|
| preview=True |
| ) |
|
|
| |
| |
| |
|
|
| target_gallery = gr.Gallery( |
|
|
| label="Target Face Review — Face Crops", |
|
|
| columns=4, |
|
|
| rows=2, |
|
|
| height="auto", |
|
|
| object_fit="contain", |
|
|
| preview=True |
| ) |
|
|
| |
| |
| |
|
|
| with gr.Row(): |
|
|
| source_selector = gr.Dropdown( |
|
|
| label="Source Image", |
|
|
| choices=[], |
|
|
| value=None, |
|
|
| interactive=True |
| ) |
|
|
| target_selector = gr.Dropdown( |
|
|
| label="Target Image", |
|
|
| choices=[], |
|
|
| value=None, |
|
|
| interactive=True |
| ) |
|
|
| with gr.Row(): |
|
|
| source_face_selector = gr.Dropdown( |
|
|
| label="Source Face", |
|
|
| choices=[], |
|
|
| value=None, |
|
|
| interactive=True |
| ) |
|
|
| target_face_selector = gr.Dropdown( |
|
|
| label="Target Face", |
|
|
| choices=[], |
|
|
| value=None, |
|
|
| interactive=True |
| ) |
|
|
| mapping_source_selector = gr.Dropdown( |
|
|
| label="Mapping Source Face", |
|
|
| choices=[], |
|
|
| value=None, |
|
|
| interactive=True |
| ) |
|
|
| mapping_target_selector = gr.Dropdown( |
|
|
| label="Mapping Target Face", |
|
|
| choices=[], |
|
|
| value=None, |
|
|
| interactive=True |
| ) |
|
|
| |
| |
| |
|
|
| with gr.Row(): |
|
|
| add_mapping_button = gr.Button( |
| "Add Mapping", |
| variant="primary" |
| ) |
|
|
| remove_mapping_button = gr.Button( |
| "Remove Last" |
| ) |
|
|
| clear_mapping_button = gr.Button( |
| "Clear Mapping" |
| ) |
|
|
| default_mapping_button = gr.Button( |
| "Default Mapping" |
| ) |
|
|
| auto_mapping_button = gr.Button( |
| "Auto Mapping" |
| ) |
|
|
| mapping_text = gr.Textbox( |
|
|
| label="Face Mapping", |
|
|
| value=default_mapping(), |
|
|
| lines=8, |
|
|
| max_lines=30, |
|
|
| interactive=True |
| ) |
|
|
| |
| |
| |
|
|
| with gr.Row(): |
|
|
| mapping_json_upload = gr.File( |
|
|
| label="Load Mapping JSON", |
|
|
| file_count="single", |
|
|
| file_types=[ |
| ".json" |
| ], |
|
|
| type="filepath" |
| ) |
|
|
| save_mapping_button = gr.Button( |
| "Save Mapping JSON" |
| ) |
|
|
| load_mapping_button = gr.Button( |
| "Load Mapping JSON" |
| ) |
|
|
| |
| |
| |
|
|
| with gr.Row(): |
|
|
| swap_model = gr.Dropdown( |
|
|
| label="Swap Model", |
|
|
| choices=[ |
|
|
| "inswapper_128.onnx", |
|
|
| "hyperswap_1a_256.onnx", |
|
|
| "hyperswap_1b_256.onnx", |
|
|
| "hyperswap_1c_256.onnx" |
| ], |
|
|
| value=DEFAULT_SWAP_MODEL, |
|
|
| interactive=True |
| ) |
|
|
| restore_model = gr.Dropdown( |
|
|
| label="Face Restore", |
|
|
| choices=[ |
|
|
| "none", |
|
|
| "GPEN-BFR-512.onnx" |
| ], |
|
|
| value=DEFAULT_RESTORE_MODEL, |
|
|
| interactive=True |
| ) |
|
|
| restore_strength = gr.Slider( |
|
|
| label="Restore Strength", |
|
|
| minimum=0.0, |
|
|
| maximum=1.0, |
|
|
| value=DEFAULT_RESTORE_STRENGTH, |
|
|
| step=0.05, |
|
|
| interactive=True |
| ) |
|
|
| |
| |
| |
|
|
| generate_button = gr.Button( |
|
|
| "GENERATE MULTI FACE SWAP", |
|
|
| variant="primary", |
|
|
| size="lg" |
| ) |
|
|
| |
| |
| |
|
|
| status = gr.Textbox( |
|
|
| label="Status", |
|
|
| value="Ready.", |
|
|
| interactive=False |
| ) |
|
|
| |
| |
| |
|
|
| output_files = gr.File( |
|
|
| label="Generated Results", |
|
|
| file_count="multiple", |
|
|
| interactive=False |
| ) |
|
|
| |
| |
| |
| |
| |
| |
|
|
| output_preview_visible = gr.State( |
| False |
| ) |
|
|
| |
| |
| |
|
|
| toggle_output_preview_button = gr.Button( |
| "Show Output Preview" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
|
|
| output_gallery = gr.Gallery( |
|
|
| label="Output Preview", |
|
|
| columns=3, |
|
|
| rows=2, |
|
|
| height="auto", |
|
|
| object_fit="contain", |
|
|
| preview=True, |
|
|
| visible=False |
| ) |
|
|
| |
| |
| |
|
|
| use_output_button = gr.Button( |
|
|
| "Use Output As Target + Preview" |
| ) |
|
|
| |
| |
| |
|
|
| preview_button.click( |
|
|
| fn=preview_source_target, |
|
|
| inputs=[ |
|
|
| source_files, |
|
|
| target_files |
| ], |
|
|
| outputs=[ |
|
|
| source_gallery, |
|
|
| target_gallery, |
|
|
| source_selector, |
|
|
| target_selector, |
|
|
| mapping_source_selector, |
|
|
| mapping_target_selector, |
|
|
| status |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| source_selector.change( |
|
|
| fn=source_face_choices_from_source, |
|
|
| inputs=[ |
|
|
| source_files, |
|
|
| source_selector |
| ], |
|
|
| outputs=[ |
|
|
| source_face_selector |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| target_selector.change( |
|
|
| fn=target_face_choices_from_target, |
|
|
| inputs=[ |
|
|
| target_files, |
|
|
| target_selector |
| ], |
|
|
| outputs=[ |
|
|
| target_face_selector |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| add_mapping_button.click( |
|
|
| fn=add_mapping, |
|
|
| inputs=[ |
|
|
| mapping_text, |
|
|
| mapping_source_selector, |
|
|
| mapping_target_selector |
| ], |
|
|
| outputs=[ |
|
|
| mapping_text |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| remove_mapping_button.click( |
|
|
| fn=remove_last_mapping, |
|
|
| inputs=[ |
|
|
| mapping_text |
| ], |
|
|
| outputs=[ |
|
|
| mapping_text |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| clear_mapping_button.click( |
|
|
| fn=clear_mapping, |
|
|
| inputs=[], |
|
|
| outputs=[ |
|
|
| mapping_text |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| default_mapping_button.click( |
|
|
| fn=default_mapping, |
|
|
| inputs=[], |
|
|
| outputs=[ |
|
|
| mapping_text |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| auto_mapping_button.click( |
|
|
| fn=auto_mapping, |
|
|
| inputs=[ |
|
|
| source_files, |
|
|
| target_files |
| ], |
|
|
| outputs=[ |
|
|
| mapping_text |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| save_mapping_button.click( |
|
|
| fn=save_mapping_json, |
|
|
| inputs=[ |
|
|
| mapping_text |
| ], |
|
|
| outputs=[ |
|
|
| mapping_json_upload |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| load_mapping_button.click( |
|
|
| fn=load_mapping_json, |
|
|
| inputs=[ |
|
|
| mapping_json_upload |
| ], |
|
|
| outputs=[ |
|
|
| mapping_text |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
| |
| |
| |
|
|
| generate_button.click( |
|
|
| fn=generate_image, |
|
|
| inputs=[ |
|
|
| source_files, |
|
|
| target_files, |
|
|
| mapping_text, |
|
|
| swap_model, |
|
|
| restore_model, |
|
|
| restore_strength |
| ], |
|
|
| outputs=[ |
|
|
| output_files |
| ], |
|
|
| concurrency_limit=2 |
|
|
| ).then( |
|
|
| fn=lambda files: files, |
|
|
| inputs=[ |
|
|
| output_files |
| ], |
|
|
| outputs=[ |
|
|
| output_gallery |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| toggle_output_preview_button.click( |
|
|
| fn=toggle_output_preview, |
|
|
| inputs=[ |
|
|
| output_preview_visible |
| ], |
|
|
| outputs=[ |
|
|
| output_gallery, |
|
|
| output_preview_visible, |
|
|
| toggle_output_preview_button |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| use_output_button.click( |
|
|
| fn=use_output_as_target, |
|
|
| inputs=[ |
|
|
| output_files |
| ], |
|
|
| outputs=[ |
|
|
| target_files |
| ], |
|
|
| concurrency_limit=2 |
|
|
| ).then( |
|
|
| fn=preview_source_target, |
|
|
| inputs=[ |
|
|
| source_files, |
|
|
| target_files |
| ], |
|
|
| outputs=[ |
|
|
| source_gallery, |
|
|
| target_gallery, |
|
|
| source_selector, |
|
|
| target_selector, |
|
|
| mapping_source_selector, |
|
|
| mapping_target_selector, |
|
|
| status |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| clear_source_button.click( |
|
|
| fn=clear_source, |
|
|
| inputs=[], |
|
|
| outputs=[ |
|
|
| source_gallery, |
|
|
| target_gallery, |
|
|
| source_selector, |
|
|
| target_selector, |
|
|
| mapping_source_selector, |
|
|
| mapping_target_selector, |
|
|
| status |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| clear_target_button.click( |
|
|
| fn=clear_target, |
|
|
| inputs=[], |
|
|
| outputs=[ |
|
|
| source_gallery, |
|
|
| target_gallery, |
|
|
| source_selector, |
|
|
| target_selector, |
|
|
| mapping_source_selector, |
|
|
| mapping_target_selector, |
|
|
| status |
| ], |
|
|
| concurrency_limit=2 |
| ) |
|
|
| |
| |
| |
|
|
| app.queue( |
|
|
| max_size=GRADIO_QUEUE_SIZE, |
|
|
| default_concurrency_limit=MAX_CONCURRENT_JOBS |
| ) |
|
|
| |
| |
| |
|
|
| log("=" * 70) |
|
|
| log( |
| "[APP] Starting:", |
| APP_TITLE |
| ) |
|
|
| log( |
| "[APP] Device: CPU" |
| ) |
|
|
| log( |
| "[APP] Swap:", |
| DEFAULT_SWAP_MODEL |
| ) |
|
|
| log( |
| "[APP] Restore:", |
| DEFAULT_RESTORE_MODEL |
| ) |
|
|
| log( |
| "[APP] RAM profile: 16 GB" |
| ) |
|
|
| log( |
| "[APP] Concurrent jobs:", |
| MAX_CONCURRENT_JOBS |
| ) |
|
|
| log( |
| "[APP] Concurrent inference:", |
| MAX_CONCURRENT_INFERENCE |
| ) |
|
|
| log( |
| "[APP] Multi Source Face: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Multi Target Face: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Face Mapping: ENABLED" |
| ) |
|
|
| log( |
| "[APP] GIF: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Face Crop Review: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Source Face Crop: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Target Face Crop: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Preview Batch Mode: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Multi-Mapping Tensor Adapter: ENABLED" |
| ) |
|
|
| log( |
| "[APP] GPEN Path Fix: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Output Preview Toggle: ENABLED" |
| ) |
|
|
| log( |
| "[APP] Output Preview Default: HIDDEN" |
| ) |
|
|
| log("=" * 70) |
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
|
|
| app.launch( |
|
|
| server_name="0.0.0.0", |
|
|
| server_port=int( |
| os.environ.get( |
| "PORT", |
| "7860" |
| ) |
| ), |
|
|
| share=True, |
|
|
| show_error=True |
| ) |