import os # ============================================================ # CPU / RUNTIME ENVIRONMENT # ============================================================ os.environ["CUDA_VISIBLE_DEVICES"] = "" os.environ["ORT_DISABLE_CUDA"] = "1" os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "" # ============================================================ # STANDARD LIBRARY # ============================================================ import sys import asyncio import gc import json import time import tempfile import importlib import threading import shutil from collections import OrderedDict # ============================================================ # THIRD PARTY # ============================================================ import numpy as np import cv2 import gradio as gr import imageio from PIL import Image from huggingface_hub import hf_hub_download # ============================================================ # CONFIG # ============================================================ 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 # ============================================================ # 16 GB RAM / 2 JOB CONFIG # ============================================================ MAX_CONCURRENT_JOBS = 2 MAX_CONCURRENT_INFERENCE = 2 MAX_PREVIEW_JOBS = 2 GRADIO_QUEUE_SIZE = 8 # ============================================================ # GLOBAL LOCKS # ============================================================ JOB_SEMAPHORE = threading.BoundedSemaphore( MAX_CONCURRENT_JOBS ) INFERENCE_SEMAPHORE = threading.BoundedSemaphore( MAX_CONCURRENT_INFERENCE ) MODEL_LOCK = threading.RLock() COMFY_INIT_LOCK = threading.RLock() # ============================================================ # GLOBAL STATE # ============================================================ loaded_models = {} face_cache = OrderedDict() FACE_CACHE_MAX = 64 source_tensor_cache = OrderedDict() SOURCE_TENSOR_CACHE_MAX = 16 # ============================================================ # LOG # ============================================================ def log(*args): print( *args, flush=True ) # ============================================================ # STARTUP INFO # ============================================================ 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) # ============================================================ # DIRECTORIES # ============================================================ for directory in ( MODEL_DIR, INSIGHTFACE_DIR, HYPERSWAP_DIR, FACE_RESTORE_DIR, OUTPUT_DIR ): os.makedirs( directory, exist_ok=True ) # ============================================================ # MODEL DEFINITIONS # ============================================================ 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 ) } # ============================================================ # MODEL DOWNLOAD # ============================================================ 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 ) # -------------------------------------------------------- # MIGRATE OLD BROKEN NESTED PATH # -------------------------------------------------------- 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) ) # -------------------------------------------------------- # DOWNLOAD # -------------------------------------------------------- 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 # ============================================================ # ENSURE MODEL # ============================================================ 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 # ============================================================ # COMFYUI 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() # ============================================================ # FIX UTILS COLLISION # ============================================================ 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() # ============================================================ # COMFY VERSION # ============================================================ 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 ) # ============================================================ # EXTRA MODEL PATHS # ============================================================ 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() # ============================================================ # TORCH # ============================================================ import torch # ============================================================ # COMFY MODEL MANAGEMENT # ============================================================ 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() ) # ============================================================ # COMFY NODES # ============================================================ 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() # ============================================================ # NODE MAPPINGS # ============================================================ from nodes import ( NODE_CLASS_MAPPINGS ) # ============================================================ # VERIFY REACTOR # ============================================================ 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 ) # ============================================================ # LOAD LOADIMAGE # ============================================================ try: loadimage = ( NODE_CLASS_MAPPINGS[ "LoadImage" ]() ) except Exception as e: raise RuntimeError( "Không load được LoadImage:\n" + repr(e) ) # ============================================================ # LOAD REACTOR # ============================================================ try: reactorfaceswap = ( NODE_CLASS_MAPPINGS[ "ReActorFaceSwap" ]() ) except Exception as e: raise RuntimeError( "Không load được ReActorFaceSwap:\n" + repr(e) ) # ============================================================ # REACTOR FACE FUNCTIONS # ============================================================ 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) ) # ============================================================ # FILE HELPERS # ============================================================ 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 # ============================================================ # FACE CACHE # ============================================================ 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 # ============================================================ # GIF FIRST FRAME # ============================================================ 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() # ============================================================ # IMAGE FOR DETECTION # ============================================================ 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 ) # ============================================================ # FACE CROP # ============================================================ 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() # ============================================================ # MAKE FACE REVIEW GALLERY # ============================================================ 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 # ============================================================ # FACE REVIEW FOR MULTIPLE FILES # ============================================================ 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 # ============================================================ # ANNOTATED FACE PREVIEW # ============================================================ 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) # ============================================================ # MULTI FILE PREVIEW # ============================================================ 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: # ================================================= # SOURCE # ================================================= 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) ) # ================================================= # TARGET # ================================================= 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" ) ) # ============================================================ # SOURCE FACE SELECTOR # ============================================================ 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 ) ) # ============================================================ # TARGET FACE SELECTOR # ============================================================ 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 ) ) # ============================================================ # PARSE SOURCE FACE # ============================================================ 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 ) # ============================================================ # PARSE TARGET FACE # ============================================================ 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 ) # ============================================================ # MAPPING FORMAT # ============================================================ 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}" ) # ============================================================ # PARSE MAPPINGS # ============================================================ 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 # ============================================================ # DEFAULT MAPPING # ============================================================ def default_mapping(): return ( "SOURCE 0 FACE 0 " "-> TARGET 0 FACE 0" ) # ============================================================ # ADD MAPPING # ============================================================ 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 ) # ============================================================ # REMOVE LAST # ============================================================ 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 ) # ============================================================ # CLEAR MAPPING # ============================================================ def clear_mapping(): return "" # ============================================================ # VALIDATE MAPPING # ============================================================ 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." ) # ============================================================ # LOAD IMAGE THROUGH COMFY # ============================================================ 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 # ============================================================ # SOURCE CACHE # ============================================================ 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 # ============================================================ # GET VALUE # ============================================================ 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] # ============================================================ # RESULT TO PIL # ============================================================ 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" ) # ============================================================ # PIL -> COMFY IMAGE TENSOR # ============================================================ 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() # ============================================================ # NORMALIZE COMFY IMAGE # ============================================================ 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 ) # ============================================================ # REACTOR SWAP # ============================================================ 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 # ============================================================ # SWAP ONE IMAGE # ============================================================ 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 # ============================================================ # GIF PROCESS # ============================================================ 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 ) # ============================================================ # OUTPUT PATH # ============================================================ 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 ) # ============================================================ # GENERATE # ============================================================ 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 = [] # ==================================================== # GROUP MAPPING BY TARGET # ==================================================== mappings_by_target = {} for mapping in mappings: target_index = mapping[ "target_index" ] mappings_by_target.setdefault( target_index, [] ).append( mapping ) # ==================================================== # PROCESS EACH TARGET # ==================================================== 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 ) # ================================================= # GIF # ================================================= 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() # ================================================= # NORMAL IMAGE # ================================================= 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 # ============================================================ # OUTPUT -> TARGET # ============================================================ 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 # ============================================================ # CLEAR TARGET # ============================================================ 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." ) # ============================================================ # CLEAR SOURCE # ============================================================ 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." ) # ============================================================ # SAVE MAPPING JSON # ============================================================ 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 # ============================================================ # LOAD MAPPING JSON # ============================================================ 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 ) # ============================================================ # AUTO DEFAULT MAPPING # ============================================================ 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 # Kiểm tra source đầu tiên có face 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 = [] # Duyệt toàn bộ target for target_index, target_path in enumerate(target_paths): try: target_faces = detect_reactor_faces(target_path) except Exception: target_faces = [] # Target không có face thì bỏ qua if not target_faces: skipped_targets.append(target_index) continue # Dùng SOURCE 0 FACE 0 cho tất cả face của target 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." ) # Thông báo các target bị bỏ qua if skipped_targets: lines.append("") lines.append( "# BỎ QUA TARGET KHÔNG CÓ FACE: " + ", ".join(map(str, skipped_targets)) ) return "\n".join(lines) # ============================================================ # STATUS # ============================================================ 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}" ) # ============================================================ # OUTPUT PREVIEW VISIBILITY # ============================================================ 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" ) ) # ============================================================ # UI # ============================================================ with gr.Blocks( title=APP_TITLE ) as app: # ======================================================== # SOURCE # ======================================================== 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" ) # ======================================================== # PREVIEW CONTROLS # ======================================================== 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 FACE REVIEW # ======================================================== source_gallery = gr.Gallery( label="Source Face Review — Face Crops", columns=4, rows=2, height="auto", object_fit="contain", preview=True ) # ======================================================== # TARGET FACE REVIEW # ======================================================== target_gallery = gr.Gallery( label="Target Face Review — Face Crops", columns=4, rows=2, height="auto", object_fit="contain", preview=True ) # ======================================================== # SELECTORS # ======================================================== 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 ) # ======================================================== # MAPPING # ======================================================== 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 ) # ======================================================== # MAPPING JSON # ======================================================== 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" ) # ======================================================== # MODELS # ======================================================== 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 # ======================================================== generate_button = gr.Button( "GENERATE MULTI FACE SWAP", variant="primary", size="lg" ) # ======================================================== # STATUS # ======================================================== status = gr.Textbox( label="Status", value="Ready.", interactive=False ) # ======================================================== # OUTPUT # ======================================================== output_files = gr.File( label="Generated Results", file_count="multiple", interactive=False ) # ======================================================== # OUTPUT PREVIEW STATE # # IMPORTANT: # DEFAULT = False # ======================================================== output_preview_visible = gr.State( False ) # ======================================================== # OUTPUT PREVIEW TOGGLE BUTTON # ======================================================== toggle_output_preview_button = gr.Button( "Show Output Preview" ) # ======================================================== # OUTPUT PREVIEW # # IMPORTANT: # DEFAULT HIDDEN # ======================================================== output_gallery = gr.Gallery( label="Output Preview", columns=3, rows=2, height="auto", object_fit="contain", preview=True, visible=False ) # ======================================================== # OUTPUT -> TARGET # ======================================================== use_output_button = gr.Button( "Use Output As Target + Preview" ) # ======================================================== # PREVIEW EVENT # ======================================================== 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 # ======================================================== source_selector.change( fn=source_face_choices_from_source, inputs=[ source_files, source_selector ], outputs=[ source_face_selector ], concurrency_limit=2 ) # ======================================================== # TARGET SELECTOR # ======================================================== target_selector.change( fn=target_face_choices_from_target, inputs=[ target_files, target_selector ], outputs=[ target_face_selector ], concurrency_limit=2 ) # ======================================================== # ADD MAPPING # ======================================================== add_mapping_button.click( fn=add_mapping, inputs=[ mapping_text, mapping_source_selector, mapping_target_selector ], outputs=[ mapping_text ], concurrency_limit=2 ) # ======================================================== # REMOVE # ======================================================== remove_mapping_button.click( fn=remove_last_mapping, inputs=[ mapping_text ], outputs=[ mapping_text ], concurrency_limit=2 ) # ======================================================== # CLEAR # ======================================================== clear_mapping_button.click( fn=clear_mapping, inputs=[], outputs=[ mapping_text ], concurrency_limit=2 ) # ======================================================== # DEFAULT # ======================================================== default_mapping_button.click( fn=default_mapping, inputs=[], outputs=[ mapping_text ], concurrency_limit=2 ) # ======================================================== # AUTO MAPPING # ======================================================== auto_mapping_button.click( fn=auto_mapping, inputs=[ source_files, target_files ], outputs=[ mapping_text ], concurrency_limit=2 ) # ======================================================== # SAVE MAPPING # ======================================================== save_mapping_button.click( fn=save_mapping_json, inputs=[ mapping_text ], outputs=[ mapping_json_upload ], concurrency_limit=2 ) # ======================================================== # LOAD MAPPING # ======================================================== load_mapping_button.click( fn=load_mapping_json, inputs=[ mapping_json_upload ], outputs=[ mapping_text ], concurrency_limit=2 ) # ======================================================== # GENERATE # # Output Preview stays hidden after generation. # Generated files are still populated normally. # ======================================================== 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 # ======================================================== 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 ) # ======================================================== # OUTPUT -> TARGET # ======================================================== 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 # ======================================================== 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 # ======================================================== 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 ) # ============================================================ # GRADIO QUEUE # ============================================================ app.queue( max_size=GRADIO_QUEUE_SIZE, default_concurrency_limit=MAX_CONCURRENT_JOBS ) # ============================================================ # START # ============================================================ 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) # ============================================================ # LAUNCH # ============================================================ if __name__ == "__main__": app.launch( server_name="0.0.0.0", server_port=int( os.environ.get( "PORT", "7860" ) ), share=True, show_error=True )