| import os |
| import gradio as gr |
| import numpy as np |
| import random |
| import spaces |
| import torch |
| from diffusers import Flux2KleinPipeline |
| from PIL import Image |
|
|
| dtype = torch.bfloat16 |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| MAX_SEED = np.iinfo(np.int32).max |
|
|
| |
| REPO_ID_DISTILLED = "thornmaze/FLUX2-klein-9B" |
|
|
| |
| LORA_REPO_ID = "thornmaze/BFS-Best-Face-Swap" |
| LORA_FILENAME = "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors" |
|
|
| |
| |
|
|
| FACE_SWAP_PROMPT = """head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. Remove the head from Picture 1 completely and replace it with the head from Picture 2. |
| |
| FROM PICTURE 1 (strictly preserve): |
| - Scene: lighting conditions, shadows, highlights, color temperature, environment, background |
| - Head positioning: exact rotation angle, tilt, direction the head is facing |
| - Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion |
| |
| FROM PICTURE 2 (strictly preserve identity): |
| - Facial structure: face shape, bone structure, jawline, chin |
| - All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows |
| - Hair: color, style, texture, hairline |
| - Skin: texture, tone, complexion |
| |
| The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k.""" |
|
|
| print("Loading FLUX.2 Klein 9B Distilled model...") |
| pipe = Flux2KleinPipeline.from_pretrained(REPO_ID_DISTILLED, torch_dtype=dtype) |
| pipe.to(device) |
|
|
| print(f"Loading LoRA from {LORA_REPO_ID}...") |
| pipe.load_lora_weights(LORA_REPO_ID, weight_name=LORA_FILENAME) |
| print("LoRA loaded successfully!") |
|
|
| def update_dimensions_from_image(target_image): |
| """ |
| Update width/height based on target image aspect ratio. |
| |
| Keeps one side at 1024 and scales the other proportionally, |
| with both sides as multiples of 8. |
| |
| Args: |
| target_image: PIL Image of the target/body image. |
| |
| Returns: |
| tuple: A tuple of (width, height) integers, both multiples of 8. |
| """ |
| if target_image is None: |
| return 1024, 1024 |
|
|
| img_width, img_height = target_image.size |
|
|
| aspect_ratio = img_width / img_height |
|
|
| if aspect_ratio >= 1: |
| new_width = 1024 |
| new_height = int(1024 / aspect_ratio) |
| else: |
| new_height = 1024 |
| new_width = int(1024 * aspect_ratio) |
|
|
| |
| new_width = round(new_width / 8) * 8 |
| new_height = round(new_height / 8) * 8 |
|
|
| |
| new_width = max(256, min(1024, new_width)) |
| new_height = max(256, min(1024, new_height)) |
|
|
| return new_width, new_height |
|
|
|
|
| @spaces.GPU(duration=35) |
| def face_swap( |
| reference_face: Image.Image, |
| target_image: Image.Image, |
| seed: int = 42, |
| randomize_seed: bool = False, |
| width: int = 1024, |
| height: int = 1024, |
| num_inference_steps: int = 4, |
| guidance_scale: float = 1.0, |
| progress=gr.Progress(track_tqdm=True) |
| ): |
| """ |
| Perform face swapping using FLUX.2 Klein 9B with LoRA. |
| |
| Args: |
| reference_face: The face image to swap in (Picture 2). |
| target_image: The target body/base image (Picture 1). |
| seed: Random seed for reproducible generation. |
| randomize_seed: Set to True to use a random seed. |
| width: Output image width in pixels (256-1024, must be multiple of 8). |
| height: Output image height in pixels (256-1024, must be multiple of 8). |
| num_inference_steps: Number of denoising steps (default 4 for distilled). |
| guidance_scale: How closely to follow the prompt (default 1.0 for distilled). |
| |
| Returns: |
| tuple: A tuple containing the generated PIL Image and the seed used. |
| """ |
| if reference_face is None or target_image is None: |
| raise gr.Error("Please provide both a reference face and a target image!") |
|
|
| if randomize_seed: |
| seed = random.randint(0, MAX_SEED) |
|
|
| generator = torch.Generator(device=device).manual_seed(seed) |
|
|
| |
| |
| image_list = [target_image, reference_face] |
|
|
| progress(0.2, desc="Swapping face...") |
|
|
| image = pipe( |
| prompt=FACE_SWAP_PROMPT, |
| image=image_list, |
| height=height, |
| width=width, |
| num_inference_steps=num_inference_steps, |
| guidance_scale=guidance_scale, |
| generator=generator, |
| ).images[0] |
|
|
| |
| return (target_image, image), seed |
|
|
|
|
| css = """ |
| #col-container { |
| margin: 0 auto; |
| max-width: 1200px; |
| } |
| .image-container img { |
| object-fit: contain; |
| } |
| """ |
|
|
| with gr.Blocks(css=css) as demo: |
|
|
| with gr.Column(elem_id="col-container"): |
| gr.Markdown("""# Face Swap with FLUX.2 Klein 9B |
| |
| Swap faces using Flux.2 Klein 9B [Alissonerdx/BFS-Best-Face-Swap](https://huggingface.co/Alissonerdx/BFS-Best-Face-Swap) LoRA |
| """) |
|
|
| with gr.Row(): |
| with gr.Column(): |
| with gr.Row(): |
| reference_face = gr.Image( |
| label="Reference Face", |
| type="pil", |
| sources=["upload"], |
| elem_classes="image-container" |
| ) |
| |
| target_image = gr.Image( |
| label="Target Image (Body/Scene)", |
| type="pil", |
| sources=["upload"], |
| elem_classes="image-container" |
| ) |
| run_button = gr.Button("Swap Face", visible=False) |
| with gr.Accordion("Advanced Settings", open=False): |
| seed = gr.Slider( |
| label="Seed", |
| minimum=0, |
| maximum=MAX_SEED, |
| step=1, |
| value=0, |
| ) |
| |
| randomize_seed = gr.Checkbox(label="Randomize seed", value=True) |
| |
| with gr.Row(): |
| width = gr.Slider( |
| label="Width", |
| minimum=256, |
| maximum=1024, |
| step=8, |
| value=1024, |
| ) |
| |
| height = gr.Slider( |
| label="Height", |
| minimum=256, |
| maximum=1024, |
| step=8, |
| value=1024, |
| ) |
| |
| with gr.Row(): |
| num_inference_steps = gr.Slider( |
| label="Inference Steps", |
| minimum=1, |
| maximum=20, |
| step=1, |
| value=4, |
| info="Number of denoising steps (4 is optimal for distilled model)" |
| ) |
| |
| guidance_scale = gr.Slider( |
| label="Guidance Scale", |
| minimum=0.0, |
| maximum=5.0, |
| step=0.1, |
| value=1.0, |
| info="How closely to follow the prompt (1.0 is optimal for distilled model)" |
| ) |
|
|
|
|
| comparison_slider = gr.ImageSlider( |
| label="Before / After", |
| type="pil" |
| ) |
|
|
| |
|
|
| |
| seed_output = gr.Number(label="Seed Used", visible=False) |
|
|
| |
| target_image.upload( |
| fn=update_dimensions_from_image, |
| inputs=[target_image], |
| outputs=[width, height] |
| ) |
|
|
| |
| swap_inputs = [ |
| reference_face, |
| target_image, |
| seed, |
| randomize_seed, |
| width, |
| height, |
| num_inference_steps, |
| guidance_scale |
| ] |
| swap_outputs = [comparison_slider, seed_output] |
|
|
| |
| run_button.click( |
| fn=face_swap, |
| inputs=swap_inputs, |
| outputs=swap_outputs, |
| ) |
|
|
| |
| def auto_swap_wrapper(ref_face, target_img, s, rand_s, w, h, steps, cfg): |
| """Only run face swap if both images are provided""" |
| if ref_face is not None and target_img is not None: |
| result = face_swap(ref_face, target_img, s, rand_s, w, h, steps, cfg) |
| |
| return result[0], result[1], gr.update(visible=True) |
| return None, s, gr.update(visible=False) |
|
|
| |
| reference_face.change( |
| fn=auto_swap_wrapper, |
| inputs=swap_inputs, |
| outputs=[comparison_slider, seed_output, run_button], |
| ) |
|
|
| |
| target_image.change( |
| fn=auto_swap_wrapper, |
| inputs=swap_inputs, |
| outputs=[comparison_slider, seed_output, run_button], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch(share=True, theme=gr.themes.Citrus()) |
|
|