Spaces:
Runtime error
Runtime error
| """Stable-Makeup: 妆容迁移 · ModelScope 创空间 xGPU | |
| 基于 sky24h/Stable-Makeup-unofficial 成功部署方案。 | |
| 模型从 ModelScope 加载,预训练权重从 HuggingFace Hub 拉取。 | |
| """ | |
| import os | |
| import numpy as np | |
| from PIL import Image | |
| import torch | |
| import gradio as gr | |
| PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) | |
| os.chdir(PROJECT_ROOT) | |
| # ═══ 设备 · 精度自适应 ═══ | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32 | |
| print(f"[Stable-Makeup] 设备: {DEVICE}, 精度: {DTYPE}") | |
| if DEVICE == "cuda": | |
| print(f"[Stable-Makeup] GPU: {torch.cuda.get_device_name(0)}, 显存: {torch.cuda.get_device_properties(0).total_memory/1024**3:.0f}GB") | |
| # ═══ 固定随机种子(与 sky24h 一致) ═══ | |
| torch.manual_seed(1024) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed(1024) | |
| torch.cuda.manual_seed_all(1024) | |
| # ═══════════════════════════════════════════════════════════ | |
| # 模型初始化(模块级加载) | |
| # ═══════════════════════════════════════════════════════════ | |
| def _ms_download(repo_id, cache_dir="./models"): | |
| """从 ModelScope 下载模型""" | |
| from modelscope import snapshot_download | |
| return snapshot_download(repo_id, cache_dir=cache_dir) | |
| def init_pipeline(): | |
| """初始化推理管线——float16 GPU + DDIM""" | |
| from diffusers import UNet2DConditionModel as OriginalUNet2DConditionModel | |
| from diffusers import DDIMScheduler, ControlNetModel | |
| from pipeline_sd15 import StableDiffusionControlNetPipeline | |
| from detail_encoder.encoder_plus import detail_encoder | |
| # ── SD1.5 基础模型 ── | |
| sd15_dir = _ms_download("AI-ModelScope/stable-diffusion-v1-5") | |
| print(f"[Stable-Makeup] SD1.5: {sd15_dir}") | |
| Unet = OriginalUNet2DConditionModel.from_pretrained( | |
| sd15_dir, subfolder="unet", torch_dtype=DTYPE, local_files_only=True | |
| ).to(DEVICE) | |
| # ── 双路 ControlNet ── | |
| id_encoder = ControlNetModel.from_unet(Unet) | |
| pose_encoder = ControlNetModel.from_unet(Unet) | |
| # ── 妆容编码器(CLIP + SSR attention) ── | |
| clip_dir = _ms_download("AI-ModelScope/clip-vit-large-patch14") | |
| print(f"[Stable-Makeup] CLIP: {clip_dir}") | |
| makeup_encoder = detail_encoder(Unet, clip_dir, DEVICE, dtype=DTYPE) | |
| # ── 加载 Stable-Makeup 预训练权重(本地优先 → Google Drive 兜底) ── | |
| print("[Stable-Makeup] 加载预训练权重...") | |
| import gc | |
| REQUIRED = ["pytorch_model.bin", "pytorch_model_1.bin", "pytorch_model_2.bin"] | |
| WEIGHTS_DIR = os.path.join(PROJECT_ROOT, "weights") | |
| GDRIVE_FOLDER = "1397t27GrUyLPnj17qVpKWGwg93EcaFfg" | |
| # 检查缺失文件 → 自动从 Google Drive 下载(HF Spaces 海外服务器可直连) | |
| os.makedirs(WEIGHTS_DIR, exist_ok=True) | |
| missing = [f for f in REQUIRED if not os.path.exists(os.path.join(WEIGHTS_DIR, f))] | |
| if missing: | |
| print(f"[Stable-Makeup] 缺失: {missing},从 Google Drive 下载...") | |
| try: | |
| import gdown | |
| gdown.download_folder(id=GDRIVE_FOLDER, output=WEIGHTS_DIR, quiet=False) | |
| except Exception as e: | |
| print(f"[Stable-Makeup] ⚠️ 自动下载失败: {e}") | |
| print(f" 请手动下载: https://drive.google.com/drive/folders/{GDRIVE_FOLDER}") | |
| print(f" 放到: {WEIGHTS_DIR}/") | |
| raise | |
| def _load_weight(filename): | |
| local_path = os.path.join(WEIGHTS_DIR, filename) | |
| print(f" ✅ 加载: {local_path} ({os.path.getsize(local_path)/1024**3:.1f}GB)") | |
| return torch.load(local_path, map_location="cpu") | |
| id_encoder.load_state_dict(_load_weight("pytorch_model_1.bin"), strict=False) | |
| gc.collect() | |
| pose_encoder.load_state_dict(_load_weight("pytorch_model_2.bin"), strict=False) | |
| gc.collect() | |
| makeup_encoder.load_state_dict(_load_weight("pytorch_model.bin"), strict=False) | |
| gc.collect() | |
| id_encoder.to(device=DEVICE, dtype=DTYPE) | |
| pose_encoder.to(device=DEVICE, dtype=DTYPE) | |
| makeup_encoder.to(device=DEVICE, dtype=DTYPE) | |
| # ── 推理管线 ── | |
| pipe = StableDiffusionControlNetPipeline.from_pretrained( | |
| sd15_dir, | |
| safety_checker=None, | |
| unet=Unet, | |
| controlnet=[id_encoder, pose_encoder], | |
| torch_dtype=DTYPE, | |
| local_files_only=True, | |
| ).to(DEVICE) | |
| # DDIMScheduler(论文原版,与 sky24h 一致) | |
| pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config) | |
| print("[Stable-Makeup] ✅ 模型加载完成 (float16 GPU + DDIM)") | |
| return pipe, makeup_encoder | |
| # ── 启动时加载模型 ── | |
| pipeline, makeup_encoder = init_pipeline() | |
| # ═══════════════════════════════════════════════════════════ | |
| # 推理函数 | |
| # ═══════════════════════════════════════════════════════════ | |
| def get_draw(pil_img, size): | |
| """生成人脸结构控制图(PIL → cv2 BGR → SPIGA → 骨架图)""" | |
| import cv2 | |
| from spiga_draw import spiga_process, spiga_segmentation | |
| cv2_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) | |
| spigas = spiga_process(cv2_img) | |
| if spigas is False: | |
| width, height = pil_img.size | |
| return Image.new("RGB", (width, height), color=(0, 0, 0)) | |
| return spiga_segmentation(spigas, size=size) | |
| def makeup_transfer(id_image, makeup_image, guidance_scale=1.6): | |
| """妆容迁移——与 sky24h 推理逻辑完全一致""" | |
| size = 512 | |
| id_image_resized = id_image.resize((size, size)) | |
| makeup_image_resized = makeup_image.resize((size, size)) | |
| pose_image = get_draw(id_image_resized, size=size) | |
| result = makeup_encoder.generate( | |
| id_image=[id_image_resized, pose_image], | |
| makeup_image=makeup_image_resized, | |
| guidance_scale=guidance_scale, | |
| pipe=pipeline, | |
| ) | |
| return result | |
| # ═══════════════════════════════════════════════════════════ | |
| # Gradio UI | |
| # ═══════════════════════════════════════════════════════════ | |
| with gr.Blocks(title="Stable-Makeup 妆容迁移") as demo: | |
| gr.Markdown(""" | |
| # 💄 Stable-Makeup · 妆容迁移 | |
| 上传素颜照 + 参考妆容图,AI 将妆容迁移到你的照片上。 | |
| 基于 [Stable-Makeup](https://arxiv.org/abs/2403.07764) (arXiv 2403.07764) | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| id_img = gr.Image(label="素颜照", type="pil", height=400) | |
| with gr.Column(): | |
| makeup_img = gr.Image(label="参考妆容", type="pil", height=400) | |
| guidance = gr.Slider( | |
| minimum=1.01, maximum=3.0, value=1.6, step=0.05, | |
| label="妆容浓度 (guidance_scale)", | |
| info="淡妆建议 1.05-1.15,浓妆建议 2.0" | |
| ) | |
| btn = gr.Button("开始试妆", variant="primary") | |
| output = gr.Image(label="试妆结果", type="pil") | |
| btn.click( | |
| fn=makeup_transfer, | |
| inputs=[id_img, makeup_img, guidance], | |
| outputs=output, | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=10).launch() | |