File size: 8,359 Bytes
c24437c
e26a2a9
 
039eed5
443e0c8
51dd78a
e26a2a9
74b81b3
51dd78a
74b81b3
443e0c8
74b81b3
 
 
51dd78a
c80999d
4d60c66
c80999d
 
 
c24437c
66686bd
 
 
c24437c
c80999d
e26a2a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24dca9f
e26a2a9
 
74b81b3
 
 
 
 
51dd78a
c80999d
74b81b3
 
51dd78a
74b81b3
 
 
 
 
 
 
51dd78a
74b81b3
 
51dd78a
 
e26a2a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24dca9f
 
 
 
66686bd
 
24dca9f
 
 
 
 
 
e26a2a9
24dca9f
 
 
 
 
 
 
 
 
 
 
 
74b81b3
51dd78a
 
74b81b3
 
 
 
 
 
51dd78a
74b81b3
 
 
 
51dd78a
 
74b81b3
 
 
 
 
 
 
51dd78a
 
74b81b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c80999d
74b81b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51dd78a
 
74b81b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51dd78a
 
74b81b3
c80999d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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()