from __future__ import annotations import os import sys from dataclasses import dataclass from pathlib import Path from typing import Any import gradio as gr import spaces import torch from huggingface_hub import hf_hub_download from PIL import Image, ImageOps from torch.amp import autocast ROOT = Path(__file__).resolve().parent ART_ROOT = ROOT / "ART" if (ROOT / "ART" / "pipeline_flux_kontext_art.py").exists() else ROOT if str(ART_ROOT) not in sys.path: sys.path.insert(0, str(ART_ROOT)) from pipeline_flux_kontext_art import FluxKontextPipeline_Art BASE_MODEL_ID = os.getenv("ART_BASE_MODEL_ID", "black-forest-labs/FLUX.1-Kontext-dev") CHECKPOINT_REPO_ID = os.getenv("ART_CHECKPOINT_REPO_ID", "csbowei/ART") CHECKPOINT_PREFIX = os.getenv("ART_CHECKPOINT_PREFIX", "").strip("/") PROMPT = "Apply makeup transfer" SEED = 42 PANEL_MIN_WIDTH = 260 PANEL_MAX_WIDTH = 340 RESULT_PANEL_MAX_WIDTH = 350 EXAMPLE_COLUMNS = 4 EXAMPLE_GAP = 8 EXAMPLE_ROW_GAP = 8 LOCAL_PROXY_BYPASS = ("localhost", "127.0.0.1", "0.0.0.0", "::1") HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN") @dataclass(frozen=True) class CheckpointConfig: filename: str guidance_scale: float = 3.5 steps: int = 28 gpu_duration: int = 120 CHECKPOINTS = { 512: CheckpointConfig("art_transfer_lora_512.safetensors", gpu_duration=120), 1024: CheckpointConfig("art_transfer_lora_1024.safetensors", guidance_scale=3.5, steps=28, gpu_duration=100), } DISABLED_CHECKPOINTS = { 2048: CheckpointConfig("art_transfer_lora_2048.safetensors", guidance_scale=3.5, steps=28, gpu_duration=300), } ALL_RESOLUTIONS = sorted([*CHECKPOINTS, *DISABLED_CHECKPOINTS]) SOURCE_EXAMPLES = sorted((ART_ROOT / "examples" / "source").glob("*")) REFERENCE_EXAMPLES = sorted((ART_ROOT / "examples" / "ref").glob("*")) def _ensure_local_proxy_bypass() -> None: for key in ("NO_PROXY", "no_proxy"): existing = os.environ.get(key, "") entries = [entry.strip() for entry in existing.split(",") if entry.strip()] merged = entries + [host for host in LOCAL_PROXY_BYPASS if host not in entries] os.environ[key] = ",".join(merged) def checkpoint_filename(filename: str) -> str: if CHECKPOINT_PREFIX: return f"{CHECKPOINT_PREFIX}/{filename}" return filename def load_pipeline() -> FluxKontextPipeline_Art: pipeline = FluxKontextPipeline_Art.from_pretrained( BASE_MODEL_ID, torch_dtype=torch.bfloat16, token=HF_TOKEN, ) for resolution, config in CHECKPOINTS.items(): checkpoint_path = hf_hub_download( repo_id=CHECKPOINT_REPO_ID, filename=checkpoint_filename(config.filename), token=HF_TOKEN, ) pipeline.load_lora_weights( checkpoint_path, adapter_name=f"art_transfer_{resolution}", ) pipeline.set_adapters(f"art_transfer_{min(CHECKPOINTS)}") pipeline.to("cuda", dtype=torch.bfloat16) pipeline.set_progress_bar_config(disable=True) return pipeline pipe = load_pipeline() def prepare_image(image: Image.Image, resolution: int) -> Image.Image: image = ImageOps.exif_transpose(image).convert("RGB") return image.resize((resolution, resolution), Image.Resampling.LANCZOS) def gpu_duration( source_image: Image.Image | None, reference_image: Image.Image | None, resolution: int | str, ) -> int: del source_image, reference_image try: resolution = int(resolution) except (TypeError, ValueError): return 120 config = CHECKPOINTS.get(resolution) or DISABLED_CHECKPOINTS.get(resolution) return config.gpu_duration if config else 120 @spaces.GPU(duration=gpu_duration) def transfer_makeup( source_image: Image.Image | None, reference_image: Image.Image | None, resolution: int | str, ) -> tuple[Image.Image, Image.Image]: if source_image is None: raise gr.Error("Please upload or select a source image.") if reference_image is None: raise gr.Error("Please upload or select a reference image.") resolution = int(resolution) if resolution in DISABLED_CHECKPOINTS: raise gr.Error(f"Resolution {resolution} is not available yet.") if resolution not in CHECKPOINTS: raise gr.Error(f"Resolution {resolution} is not available yet.") config = CHECKPOINTS[resolution] pipe.set_adapters(f"art_transfer_{resolution}") source = prepare_image(source_image, resolution) reference = prepare_image(reference_image, resolution) generator = torch.Generator(device="cuda").manual_seed(SEED) with torch.inference_mode(), autocast(device_type="cuda", dtype=torch.bfloat16): result = pipe( image=source, cond=reference, prompt=PROMPT, guidance_scale=config.guidance_scale, height=resolution, width=resolution, num_inference_steps=config.steps, generator=generator, ).images[0] return source, result def open_example(paths: list[Path], evt: gr.SelectData) -> str: index = evt.index if isinstance(index, (tuple, list)): index = index[0] * EXAMPLE_COLUMNS + index[1] return str(paths[int(index)]) def open_source_example(evt: gr.SelectData) -> str: return open_example(SOURCE_EXAMPLES, evt) def open_reference_example(evt: gr.SelectData) -> str: return open_example(REFERENCE_EXAMPLES, evt) def example_columns(paths: list[Path]) -> int: return max(1, min(EXAMPLE_COLUMNS, len(paths))) CSS = f""" .gradio-container {{ max-width: 1220px !important; margin: 0 auto !important; padding: 28px 28px 32px !important; }} .hero {{ max-width: 1164px; margin: 0 auto 30px; padding-bottom: 0; border-bottom: 0; text-align: center; }} .hero h1 {{ max-width: none; margin: 0 0 14px; font-size: clamp(28px, 3.1vw, 40px); line-height: 1.18; letter-spacing: 0; position: relative; isolation: isolate; }} .hero h1::after {{ content: ""; position: absolute; left: 50%; bottom: 24px; width: min(680px, 72vw); height: 28px; transform: translateX(-50%); border-radius: 999px; background: linear-gradient(to right, rgb(198, 255, 221), rgb(251, 215, 134), rgb(247, 121, 125)); opacity: 0; filter: blur(22px); z-index: -1; }} .title-lead {{ display: block; font-size: 1em; font-weight: 820; color: transparent !important; background-image: linear-gradient(90deg, #ffffff 0%, #ffd9c4 45%, #ff7a18 100%) !important; -webkit-background-clip: text !important; background-clip: text !important; -webkit-text-fill-color: transparent !important; }} .title-tail {{ display: block; margin-top: 4px; font-size: 0.64em; font-weight: 680; color: transparent !important; background-image: linear-gradient(90deg, #f8fafc 0%, #f0d7e7 55%, #ffb36a 100%) !important; -webkit-background-clip: text !important; background-clip: text !important; -webkit-text-fill-color: transparent !important; }} .hero-note {{ margin: 12px 0 0; color: var(--body-text-color-subdued); font-size: 13px; line-height: 1.45; }} @media (prefers-color-scheme: light) {{ .hero h1::after {{ opacity: 0.28; }} .title-lead {{ color: transparent !important; background-image: linear-gradient( to right, color-mix(in srgb, rgb(198, 255, 221) 52%, var(--body-text-color)), color-mix(in srgb, rgb(251, 215, 134) 66%, var(--body-text-color)), color-mix(in srgb, rgb(247, 121, 125) 82%, var(--body-text-color)) ) !important; -webkit-background-clip: text !important; background-clip: text !important; -webkit-text-fill-color: transparent !important; text-shadow: none !important; }} .title-tail {{ color: transparent !important; background-image: linear-gradient( to right, color-mix(in srgb, rgb(198, 255, 221) 58%, var(--body-text-color)), color-mix(in srgb, rgb(251, 215, 134) 62%, var(--body-text-color)), color-mix(in srgb, rgb(247, 121, 125) 72%, var(--body-text-color)) ) !important; -webkit-background-clip: text !important; background-clip: text !important; -webkit-text-fill-color: transparent !important; text-shadow: none !important; }} }} .badge-row {{ display: inline-flex; justify-content: center; align-items: center; gap: 7px; flex-wrap: wrap; margin-bottom: 0; }} .badge-row a {{ display: inline-flex !important; align-items: center; gap: 7px; min-height: 30px; padding: 0 11px; border: 1px solid var(--border-color-primary); border-radius: 999px; background: color-mix(in srgb, var(--block-background-fill) 78%, transparent); color: var(--body-text-color) !important; font-size: 12px; font-weight: 650; text-decoration: none !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); }} .badge-row a:hover {{ border-color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 10%, var(--block-background-fill)); }} .badge-icon {{ display: inline-grid; place-items: center; width: 17px; height: 17px; color: var(--color-accent); }} .badge-icon svg {{ width: 15px; height: 15px; display: block; stroke: currentColor; }} .badge-text {{ color: var(--body-text-color); font-weight: 720; }} .main-row {{ justify-content: center !important; gap: 18px !important; align-items: flex-start !important; }} .panel-col {{ flex: 1 1 {PANEL_MIN_WIDTH}px !important; max-width: {PANEL_MAX_WIDTH}px !important; min-width: {PANEL_MIN_WIDTH}px !important; gap: 8px !important; }} .result-panel {{ flex: 1.24 1 {PANEL_MAX_WIDTH}px !important; max-width: {RESULT_PANEL_MAX_WIDTH}px !important; }} .panel-title {{ display: flex; align-items: center; gap: 10px; margin: 0 0 8px; color: var(--body-text-color); font-weight: 720; font-size: 14px; line-height: 1; }} .panel-title::after {{ content: ""; flex: 1; height: 1px; background: var(--border-color-primary); opacity: 0.8; }} .image-box {{ width: 100% !important; aspect-ratio: 1 / 1 !important; margin-top: 0 !important; border-radius: 8px !important; overflow: visible !important; }} .image-box .image-container:not(:fullscreen) .image-frame img {{ width: 100% !important; height: 100% !important; object-fit: cover !important; border-radius: 8px !important; }} .result-stage {{ position: relative !important; display: block !important; width: 100% !important; aspect-ratio: 1 / 1 !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; gap: 0 !important; border: 1px solid var(--border-color-primary) !important; border-radius: 8px !important; background: transparent !important; box-shadow: none !important; box-sizing: border-box !important; overflow: hidden !important; }} .result-stage .result-stage {{ border: 0 !important; }} #result-comparison {{ position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important; margin: 0 !important; box-sizing: border-box !important; border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; overflow: hidden !important; z-index: 1; }} #result-comparison .wrap, #result-comparison .image-container, #result-comparison .slider-wrap {{ width: 100% !important; height: 100% !important; margin: 0 !important; padding: 0 !important; border: 0 !important; box-shadow: none !important; background: transparent !important; }} #result-comparison img {{ object-fit: cover !important; }} .example-gallery {{ --example-thumb-radius: 8px; width: 100% !important; height: auto !important; min-height: 0 !important; margin-top: 12px !important; border-radius: 8px !important; overflow: visible !important; }} .example-gallery > div, .example-gallery .fixed-height, .example-gallery .grid-wrap, .example-gallery .grid-container {{ height: auto !important; min-height: 0 !important; overflow: visible !important; max-height: none !important; }} .example-gallery .grid-container {{ column-gap: {EXAMPLE_GAP}px !important; row-gap: {EXAMPLE_ROW_GAP}px !important; grid-template-rows: none !important; grid-auto-rows: max-content !important; align-content: start !important; }} .example-gallery .grid-container, .example-gallery .grid {{ grid-template-columns: repeat({EXAMPLE_COLUMNS}, minmax(0, 1fr)) !important; }} .example-gallery .thumbnail-item, .example-gallery .thumbnail-lg, .example-gallery button {{ aspect-ratio: 1 / 1 !important; height: auto !important; min-height: 0 !important; padding: 0 !important; border-radius: var(--example-thumb-radius) !important; overflow: hidden !important; background: transparent !important; box-sizing: border-box !important; line-height: 0 !important; }} .example-gallery img {{ width: 100% !important; height: 100% !important; object-fit: cover !important; display: block !important; border-radius: calc(var(--example-thumb-radius) - 2px) !important; }} .example-gallery .thumbnail-item *, .example-gallery .thumbnail-lg *, .example-gallery button * {{ border-radius: calc(var(--example-thumb-radius) - 2px) !important; overflow: hidden !important; }} .example-gallery .thumbnail-item, .example-gallery .thumbnail-lg {{ position: relative; z-index: 0; transform-origin: var(--hover-origin-x, 50%) var(--hover-origin-y, 50%); transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease; }} .example-gallery .grid-container {{ isolation: isolate; }} .example-gallery .grid-container > :nth-child(4n + 1), .example-gallery .grid-container > :nth-child(4n + 2) {{ --hover-origin-x: 0%; }} .example-gallery .grid-container > :nth-child(4n + 3), .example-gallery .grid-container > :nth-child(4n) {{ --hover-origin-x: 100%; }} .example-gallery .grid-container > :nth-child(-n + 4) {{ --hover-origin-y: 0%; }} .example-gallery .grid-container > :nth-child(n + 9) {{ --hover-origin-y: 100%; }} .example-gallery .grid-container > :hover {{ position: relative; z-index: 3; }} .example-gallery .thumbnail-item:hover, .example-gallery .thumbnail-lg:hover {{ z-index: 3; transform: scale(1.4); border-color: var(--color-accent) !important; box-shadow: 0 14px 28px rgba(0, 0, 0, 0.5) !important; }} #generate-button {{ min-height: 52px; width: 100% !important; margin-top: 10px !important; border-radius: 8px !important; font-weight: 750 !important; }} .resolution-card {{ margin-top: 12px !important; gap: 7px !important; padding: 9px 10px 10px !important; border: 1px solid var(--border-color-primary) !important; border-radius: 8px !important; background: transparent !important; box-shadow: none !important; min-height: 0 !important; }} .resolution-card > div, .resolution-card .form {{ padding: 0 !important; border: 0 !important; background: transparent !important; box-shadow: none !important; min-height: 0 !important; }} .resolution-card .panel-title {{ margin-bottom: 2px !important; }} .resolution-radio {{ width: 100% !important; margin-top: -2px !important; padding: 0 !important; border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; flex-wrap: nowrap !important; }} .resolution-radio .wrap {{ gap: 7px !important; }} .resolution-radio > div, .resolution-radio fieldset, .resolution-radio .form, .resolution-radio .wrap, .resolution-radio .block {{ padding: 0 !important; border: 0 !important; background: transparent !important; box-shadow: none !important; }} .resolution-radio label {{ flex: 1 !important; text-align: center !important; min-height: 38px !important; padding-top: 0 !important; padding-bottom: 0 !important; }} .resolution-radio label:has(input[value="2048"]), .resolution-radio label:nth-of-type(3) {{ opacity: 0.62 !important; cursor: not-allowed !important; pointer-events: none !important; }} """ with gr.Blocks(title="ART Makeup Transfer") as demo: gr.HTML( """
Transfer reference makeup while preserving the source identity.