import os import random import gradio as gr import numpy as np import spaces import torch from diffusers import Flux2KleinPipeline from PIL import Image # ────────────────────────────────────────────────────────────────────────────── # Smart Character Swap — FLUX.2 [klein] # # Identity source → the face / character to bring in # Target scene → the photo whose pose, lighting, occlusions and color grade # should be kept while the identity is swapped in # # Occlusion-aware, lighting-matched character / face swap, trained on FLUX.2 [klein]. # ────────────────────────────────────────────────────────────────────────────── MAX_SEED = np.iinfo(np.int32).max dtype = torch.bfloat16 device = "cuda" if torch.cuda.is_available() else "cpu" BASE_MODEL = "black-forest-labs/FLUX.2-klein-9B" LORA_REPO = "nhathoangfoto/Flux.2-Klein-9B-SmartCharacterSwap" LORA_WEIGHTS = "Klein2-9B-SmartCharacterSwap.safetensors" TRIGGER = "jhuangswap" DEFAULT_PROMPT = ( "jhuangswap, masterpiece, high-end photography, realistic portrait, " "matching target lighting, highly detailed skin texture, 8k resolution" ) print("Loading FLUX.2 [klein] 9B...") pipe = Flux2KleinPipeline.from_pretrained(BASE_MODEL, torch_dtype=dtype) pipe.to("cuda") pipe.load_lora_weights(LORA_REPO, weight_name=LORA_WEIGHTS, adapter_name="swap") print("Pipeline ready.") def _fit(img, target=1024, mult=16): """Resize so the longest side ~= target, snapped to a multiple of `mult`.""" img = img.convert("RGB") w, h = img.size scale = target / max(w, h) nw = max(mult, int(round(w * scale / mult)) * mult) nh = max(mult, int(round(h * scale / mult)) * mult) return img.resize((nw, nh), Image.LANCZOS) @spaces.GPU(duration=120) def swap(identity, scene, prompt, lora_scale, steps, guidance, seed, randomize_seed, progress=gr.Progress(track_tqdm=True)): if identity is None: raise gr.Error("Please upload an identity source (the face/character to bring in).") if scene is None: raise gr.Error("Please upload a target scene (the photo to swap the identity into).") pipe.set_adapters(["swap"], adapter_weights=[lora_scale]) scene_f = _fit(scene, target=1024) identity_f = _fit(identity, target=1024) full_prompt = prompt.strip() if prompt and prompt.strip() else DEFAULT_PROMPT if TRIGGER not in full_prompt: full_prompt = f"{TRIGGER}, {full_prompt}" if randomize_seed: seed = random.randint(0, MAX_SEED) generator = torch.Generator(device=device).manual_seed(int(seed)) # Output keeps the target scene's composition; identity is the reference. result = pipe( image=[scene_f, identity_f], prompt=full_prompt, height=scene_f.height, width=scene_f.width, num_inference_steps=int(steps), guidance_scale=guidance, generator=generator, ).images[0] return (scene_f, result), seed css = """ #col { max-width: 1200px; margin: 0 auto; } .header { text-align:center; padding: 8px 0 4px; } .header h1 { font-size: 1.7rem; margin: 0; font-weight: 700; } .header p { color: var(--body-text-color-subdued); margin: 4px 0 0; font-size: 0.95rem; } .header a { color: #6366f1; text-decoration: none; font-weight: 600; } """ with gr.Blocks(css=css) as demo: with gr.Column(elem_id="col"): gr.HTML( """
Swap an identity into a target scene — occlusion-aware, with the scene's own lighting and color grade preserved. · SmartCharacterSwap LoRA by nhathoangfoto