face-head-swap / app.py
tuan2308's picture
Update app.py
e26a2a9 verified
import os
import subprocess
import sys
import spaces
import torch
from optimization import optimize_pipeline_
from diffusers import QwenImageEditPlusPipeline
from PIL import Image
import gradio as gr
HF_BASE_MODEL = "Qwen/Qwen-Image-Edit-2509"
BFS_LORA = "Alissonerdx/BFS-Best-Face-Swap"
BFS_LORA_WEIGHT = "bfs_head_v3_qwen_image_edit_2509.safetensors" # Head V3 (recommended)
# --------- PIPELINE (ZERO GPU) ---------
# Lưu device để dùng lại trong quá trình suy luận.
EXEC_DEVICE = "cpu"
# Cho phép ép dùng CPU nếu GPU yếu hoặc hay abort (ZeroGPU không ổn định).
FORCE_CPU = bool(int(os.getenv("FORCE_CPU", "1")))
# Chỉ bật GPU khi thực sự muốn (mặc định = 0 để tránh abort vì OOM trên ZeroGPU).
PREFER_GPU = bool(int(os.getenv("PREFER_GPU", "0")))
def ensure_torchvision():
"""
Qwen2VLProcessor yêu cầu torchvision. Thử import, nếu thiếu sẽ cài đặt phiên bản khớp torch.
"""
try:
import torchvision # noqa: F401
return
except ImportError:
torch_version = torch.__version__.split("+")[0]
try:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", f"torchvision=={torch_version}"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
import torchvision # noqa: F401
except Exception as exc: # pragma: no cover - chỉ chạy trên hạ tầng Spaces
raise ImportError(
"Torchvision is required for Qwen2VLProcessor. "
"Please add a matching torchvision to requirements (e.g. pip install torchvision==torch_version)."
) from exc
def _build_pipeline(device: str, dtype: torch.dtype):
ensure_torchvision()
pipe = QwenImageEditPlusPipeline.from_pretrained(
HF_BASE_MODEL,
torch_dtype=dtype,
)
pipe.to(device)
# giảm chiếm dụng VRAM/RAM
pipe.enable_attention_slicing()
pipe.enable_vae_slicing()
# load LoRA BFS Head V3
pipe.load_lora_weights(
BFS_LORA,
weight_name=BFS_LORA_WEIGHT,
adapter_name="bfs_head_v3",
)
pipe.set_adapters(["bfs_head_v3"], adapter_weights=[1.0])
pipe.set_progress_bar_config(disable=True)
return pipe
def maybe_optimize_pipeline(pipe):
"""
Áp dụng AOTI tối ưu hóa trên GPU (nếu đang chạy CUDA).
Dùng input dummy nhỏ để tránh tốn VRAM, fallback im lặng nếu lỗi.
"""
if EXEC_DEVICE != "cuda":
return pipe
try:
dummy = Image.new("RGB", (256, 256))
generator = torch.Generator(device="cuda").manual_seed(0)
optimize_pipeline_(
pipe,
image=[dummy, dummy],
prompt="warmup",
negative_prompt=" ",
num_inference_steps=1,
true_cfg_scale=1.0,
guidance_scale=1.0,
num_images_per_prompt=1,
generator=generator,
width=256,
height=256,
)
except Exception:
# Nếu tối ưu thất bại (thường do bộ nhớ), giữ pipeline gốc để tiếp tục chạy.
pass
return pipe
@spaces.GPU # bắt buộc cho ZeroGPU
def load_pipeline():
global EXEC_DEVICE
# Mặc định chạy CPU để tránh GPU abort. Bật GPU bằng PREFER_GPU=1 và FORCE_CPU=0.
prefer_cuda = torch.cuda.is_available() and PREFER_GPU and not FORCE_CPU
device = "cuda" if prefer_cuda else "cpu"
dtype = torch.float16 if device == "cuda" else torch.float32
try:
pipe = _build_pipeline(device, dtype)
EXEC_DEVICE = device
pipe = maybe_optimize_pipeline(pipe)
return pipe
except Exception as exc:
# GPU worker thường abort vì OOM. Fallback CPU để không crash app.
if device == "cuda":
device = "cpu"
dtype = torch.float32
pipe = _build_pipeline(device, dtype)
EXEC_DEVICE = device
return pipe
raise exc
pipe = load_pipeline()
# --------- UTILITIES ---------
def resize_to_max(img: Image.Image, max_side: int = 896) -> Image.Image:
w, h = img.size
max_dim = max(w, h)
if max_dim <= max_side:
return img # không upscale
scale = max_side / max_dim
new_w = int(w * scale)
new_h = int(h * scale)
return img.resize((new_w, new_h), Image.Resampling.LANCZOS)
DEFAULT_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. ensure the head and body have correct "
"anatomical proportions, and blend the skin tones, shadows, and lighting naturally "
"so the final result appears as one coherent, realistic person."
)
# --------- INFERENCE FUNCTION ---------
def run_bfs(
body_image, # Picture 1 (body)
face_image, # Picture 2 (face)
prompt_text,
steps,
true_cfg_scale,
guidance_scale,
seed,
):
if body_image is None or face_image is None:
return None, "⚠️ Cần upload đủ 2 ảnh: Picture 1 (body) và Picture 2 (face)."
# BFS Head V3: Image 1 = body, Image 2 = face
body_image = resize_to_max(body_image)
face_image = resize_to_max(face_image)
if not str(prompt_text).strip():
prompt = DEFAULT_PROMPT
else:
prompt = prompt_text.strip()
generator = torch.Generator(device=EXEC_DEVICE).manual_seed(int(seed))
inputs = {
"image": [body_image, face_image], # rất quan trọng: [body, face]
"prompt": prompt,
"negative_prompt": " ",
"num_inference_steps": int(steps),
"true_cfg_scale": float(true_cfg_scale),
"guidance_scale": float(guidance_scale),
"num_images_per_prompt": 1,
"generator": generator,
"width": body_image.width,
"height": body_image.height,
}
with torch.inference_mode():
out = pipe(**inputs)
return out.images[0], ""
# --------- GRADIO UI ---------
with gr.Blocks(title="BFS - Best Face Swap (Qwen Image Edit 2509, CPU)") as demo:
gr.Markdown(
"""
# 🧠 BFS - Best Face Swap (Qwen Image Edit 2509, CPU)
**BFS Head V3** – Picture 1 = **Body**, Picture 2 = **Face**.
Model chạy trên **CPU (zero GPU)** nên sẽ hơi chậm, ưu tiên ảnh vừa phải (≤ 896px cạnh dài).
> Vui lòng không dùng cho người thật / người nổi tiếng ngoài đời.
"""
)
with gr.Row():
with gr.Column():
body_image = gr.Image(
label="Picture 1 - BODY (ảnh gốc, giữ background)",
type="pil",
)
face_image = gr.Image(
label="Picture 2 - FACE (ảnh mặt muốn ghép)",
type="pil",
)
prompt_box = gr.Textbox(
label="Prompt (để trống dùng prompt BFS Head V3 mặc định)",
value="",
lines=4,
)
steps = gr.Slider(
label="Steps",
minimum=8,
maximum=40,
value=24,
step=1,
)
true_cfg_scale = gr.Slider(
label="True CFG Scale",
minimum=0.0,
maximum=10.0,
value=4.0,
step=0.1,
)
guidance_scale = gr.Slider(
label="Guidance Scale",
minimum=0.0,
maximum=8.0,
value=1.0,
step=0.1,
)
seed = gr.Number(
label="Seed",
value=0,
precision=0,
)
run_button = gr.Button("🚀 Run Face / Head Swap", variant="primary")
with gr.Column():
output_image = gr.Image(
label="Kết quả",
type="pil",
)
info = gr.Markdown("")
run_button.click(
fn=run_bfs,
inputs=[body_image, face_image, prompt_box, steps, true_cfg_scale, guidance_scale, seed],
outputs=[output_image, info],
)
if __name__ == "__main__":
demo.launch()