import os import re import gc import traceback import base64 import io import gradio as gr import numpy as np import spaces import torch import random from PIL import Image from typing import Iterable, Optional, Tuple from transformers import ( AutoImageProcessor, AutoModelForDepthEstimation, ) from huggingface_hub import hf_hub_download from huggingface_hub import InferenceClient from safetensors.torch import load_file as safetensors_load_file from gradio.themes import Soft from gradio.themes.utils import colors, fonts, sizes # ============================================================ # Theme # ============================================================ colors.orange_red = colors.Color( name="orange_red", c50="#FFF0E5", c100="#FFE0CC", c200="#FFC299", c300="#FFA366", c400="#FF8533", c500="#FF4500", c600="#E63E00", c700="#CC3700", c800="#B33000", c900="#992900", c950="#802200", ) class OrangeRedTheme(Soft): def __init__( self, *, primary_hue: colors.Color | str = colors.gray, secondary_hue: colors.Color | str = colors.orange_red, neutral_hue: colors.Color | str = colors.slate, text_size: sizes.Size | str = sizes.text_lg, font: fonts.Font | str | Iterable[fonts.Font | str] = ( fonts.GoogleFont("Outfit"), "Arial", "sans-serif", ), font_mono: fonts.Font | str | Iterable[fonts.Font | str] = ( fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace", ), ): super().__init__( primary_hue=primary_hue, secondary_hue=secondary_hue, neutral_hue=neutral_hue, text_size=text_size, font=font, font_mono=font_mono, ) super().set( background_fill_primary="*primary_50", background_fill_primary_dark="*primary_900", body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)", body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)", button_primary_text_color="white", button_primary_text_color_hover="white", button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)", button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)", button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)", button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)", button_secondary_text_color="black", button_secondary_text_color_hover="white", button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)", button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)", button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)", button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)", slider_color="*secondary_500", slider_color_dark="*secondary_600", block_title_text_weight="600", block_border_width="3px", block_shadow="*shadow_drop_lg", button_primary_shadow="*shadow_drop_lg", button_large_padding="11px", color_accent_soft="*primary_100", block_label_background_fill="*primary_200", ) orange_red_theme = OrangeRedTheme() # ============================================================ # Device # ============================================================ device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES")) print("torch.__version__ =", torch.__version__) print("torch.version.cuda =", torch.version.cuda) print("cuda available:", torch.cuda.is_available()) print("cuda device count:", torch.cuda.device_count()) if torch.cuda.is_available(): print("current device:", torch.cuda.current_device()) print("device name:", torch.cuda.get_device_name(torch.cuda.current_device())) print("Using device:", device) # ============================================================ # AIO version (Space variable) # ============================================================ AIO_REPO_ID = "Pr0f3ssi0n4ln00b/Phr00t-Qwen-Rapid-AIO" DEFAULT_AIO_VERSION = "v19" _VER_RE = re.compile(r"^v\d+$") _DIGITS_RE = re.compile(r"^\d+$") def _normalize_version(raw: str) -> Optional[str]: if raw is None: return None s = str(raw).strip() if not s: return None if _VER_RE.fullmatch(s): return s if _DIGITS_RE.fullmatch(s): return f"v{s}" return None _AIO_ENV_RAW = os.environ.get("AIO_VERSION", "") _AIO_ENV_NORM = _normalize_version(_AIO_ENV_RAW) AIO_VERSION = _AIO_ENV_NORM or DEFAULT_AIO_VERSION AIO_VERSION_SOURCE = "env" if _AIO_ENV_NORM else "default(v19)" print(f"AIO_VERSION (env raw) = {_AIO_ENV_RAW!r}") print(f"AIO_VERSION (normalized) = {_AIO_ENV_NORM!r}") print(f"Using AIO_VERSION = {AIO_VERSION} ({AIO_VERSION_SOURCE})") # ============================================================ # Pipeline # ============================================================ from diffusers import FlowMatchEulerDiscreteScheduler # noqa: F401 from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 dtype = torch.bfloat16 def _load_pipe_with_version(version: str) -> QwenImageEditPlusPipeline: sub = f"{version}/transformer" print(f"📦 Loading AIO transformer: {AIO_REPO_ID} / {sub}") p = QwenImageEditPlusPipeline.from_pretrained( "Qwen/Qwen-Image-Edit-2511", transformer=QwenImageTransformer2DModel.from_pretrained( AIO_REPO_ID, subfolder=sub, torch_dtype=dtype, device_map="cuda", ), torch_dtype=dtype, ).to(device) return p try: pipe = _load_pipe_with_version(AIO_VERSION) except Exception: print("❌ Failed to load requested AIO_VERSION. Falling back to v19.") print(traceback.format_exc()) AIO_VERSION = DEFAULT_AIO_VERSION AIO_VERSION_SOURCE = "fallback_to_v19" pipe = _load_pipe_with_version(AIO_VERSION) try: pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) print("Flash Attention 3 Processor set successfully.") except Exception as e: print(f"Warning: Could not set FA3 processor: {e}") MAX_SEED = np.iinfo(np.int32).max # ============================================================ # Derived conditioning (Depth Anything) ONLY — ViTPose removed # ============================================================ DEPTH_MODEL_ID = "depth-anything/Depth-Anything-V2-Small-hf" _DEPTH_CACHE = {} def _derived_device(use_gpu: bool) -> torch.device: return torch.device("cuda" if (use_gpu and torch.cuda.is_available()) else "cpu") def _load_depth_models(dev: torch.device): key = str(dev) if key in _DEPTH_CACHE: return _DEPTH_CACHE[key] proc = AutoImageProcessor.from_pretrained(DEPTH_MODEL_ID) model = AutoModelForDepthEstimation.from_pretrained(DEPTH_MODEL_ID).to(dev) model.eval() _DEPTH_CACHE[key] = (proc, model) return _DEPTH_CACHE[key] def make_depth_map(img: Image.Image, *, use_gpu: bool) -> Image.Image: img = img.convert("RGB") dev = _derived_device(use_gpu) proc, model = _load_depth_models(dev) inputs = proc(images=img, return_tensors="pt") inputs = {k: v.to(dev) for k, v in inputs.items()} with torch.no_grad(): out = model(**inputs) pred = out.predicted_depth pred = torch.nn.functional.interpolate( pred.unsqueeze(1), size=(img.height, img.width), mode="bicubic", align_corners=False, ).squeeze(1)[0] arr = pred.detach().float().cpu().numpy() arr = arr - float(arr.min()) denom = float(arr.max()) + 1e-8 arr = arr / denom depth8 = (arr * 255.0).clip(0, 255).astype(np.uint8) return Image.fromarray(depth8, mode="L").convert("RGB") def _to_pil_rgb(item): if item is None: return None if isinstance(item, (tuple, list)) and len(item) >= 1: item = item[0] if isinstance(item, Image.Image): return item.convert("RGB") if isinstance(item, np.ndarray): return Image.fromarray(item).convert("RGB") return None def _append_to_gallery(existing, new_img: Image.Image): items = [] if existing: for it in existing: pil = _to_pil_rgb(it) if pil is not None: items.append(pil) items.append(new_img) return items # ============================================================ # LoRA adapters + presets # ============================================================ NONE_LORA = "None" ADAPTER_SPECS = { "Consistance": { "type": "single", "repo": "Pr0f3ssi0n4ln00b/QIE_2511_Consistency_Lora", "weights": "qe2511_consis_alpha_patched.safetensors", "adapter_name": "Consistency", "strength": 0.6, }, "Semirealistic-photo-detailer": { "type": "single", "repo": "rzgar/Qwen-Image-Edit-semi-realistic-detailer", "weights": "Qwen-Image-Edit-Anime-Semi-Realistic-Detailer-v1.safetensors", "adapter_name": "semirealistic", "strength": 1.0, }, "AnyPose": { "type": "package", "requires_two_images": True, "image2_label": "Upload Pose Reference (Image 2)", "parts": [ { "repo": "lilylilith/AnyPose", "weights": "2511-AnyPose-base-000006250.safetensors", "adapter_name": "anypose-base", "strength": 0.7, }, { "repo": "lilylilith/AnyPose", "weights": "2511-AnyPose-helper-00006000.safetensors", "adapter_name": "anypose-helper", "strength": 0.7, }, ], }, "Any2Real_2601": { "type": "single", "repo": "lrzjason/Anything2Real_2601", "weights": "anything2real_2601_A_final_patched.safetensors", "adapter_name": "photoreal", "strength": 1.0, }, "Hyperrealistic-Portrait": { "type": "single", "repo": "prithivMLmods/Qwen-Image-Edit-2511-Hyper-Realistic-Portrait", "weights": "HRP_20.safetensors", "adapter_name": "HRPortrait", "strength": 1.0, }, "Ultrarealistic-Portrait": { "type": "single", "repo": "prithivMLmods/Qwen-Image-Edit-2511-Ultra-Realistic-Portrait", "weights": "URP_20.safetensors", "adapter_name": "URPortrait", "strength": 1.0, }, "BFS-Best-FaceSwap": { "type": "single", "requires_two_images": True, "image2_label": "Upload Head/Face Donor (Image 2)", "repo": "Alissonerdx/BFS-Best-Face-Swap", "weights": "bfs_head_v5_2511_original.safetensors", "adapter_name": "BFS-Best-Faceswap", "strength": 1.0, "needs_alpha_fix": True, }, "BFS-Best-FaceSwap-merge": { "type": "single", "requires_two_images": True, "image2_label": "Upload Head/Face Donor (Image 2)", "repo": "Alissonerdx/BFS-Best-Face-Swap", "weights": "bfs_head_v5_2511_merged_version_rank_32_fp32.safetensors", "adapter_name": "BFS-Best-Faceswap-merge", "strength": 1.1, "needs_alpha_fix": True, }, "F2P": { "type": "single", "repo": "DiffSynth-Studio/Qwen-Image-Edit-F2P", "weights": "edit_0928_lora_step40000.safetensors", "adapter_name": "F2P", "strength": 1.0, }, "Multiple-Angles": { "type": "single", "repo": "dx8152/Qwen-Edit-2509-Multiple-angles", "weights": "镜头转换.safetensors", "adapter_name": "multiple-angles", "strength": 1.0, }, "Light-Restoration": { "type": "single", "repo": "dx8152/Qwen-Image-Edit-2509-Light_restoration", "weights": "移除光影.safetensors", "adapter_name": "light-restoration", "strength": 1.0, }, "Relight": { "type": "single", "repo": "dx8152/Qwen-Image-Edit-2509-Relight", "weights": "Qwen-Edit-Relight.safetensors", "adapter_name": "relight", "strength": 1.0, }, "Multi-Angle-Lighting": { "type": "single", "repo": "dx8152/Qwen-Edit-2509-Multi-Angle-Lighting", "weights": "多角度灯光-251116.safetensors", "adapter_name": "multi-angle-lighting", "strength": 1.0, }, "Edit-Skin": { "type": "single", "repo": "tlennon-ie/qwen-edit-skin", "weights": "qwen-edit-skin_1.1_000002750.safetensors", "adapter_name": "edit-skin", "strength": 1.0, }, "Next-Scene": { "type": "single", "repo": "lovis93/next-scene-qwen-image-lora-2509", "weights": "next-scene_lora-v2-3000.safetensors", "adapter_name": "next-scene", "strength": 1.0, }, "Flat-Log": { "type": "single", "repo": "tlennon-ie/QwenEdit2509-FlatLogColor", "weights": "QwenEdit2509-FlatLogColor.safetensors", "adapter_name": "flat-log", "strength": 1.0, }, "Upscale-Image": { "type": "single", "repo": "vafipas663/Qwen-Edit-2509-Upscale-LoRA", "weights": "qwen-edit-enhance_64-v3_000001000.safetensors", "adapter_name": "upscale-image", "strength": 1.0, }, "Upscale2K": { "type": "single", "repo": "valiantcat/Qwen-Image-Edit-2509-Upscale2K", "weights": "qwen_image_edit_2509_upscale.safetensors", "adapter_name": "upscale-2k", "strength": 1.0, "target_long_edge": 2048, }, } LORA_PRESET_PROMPTS = { "Any2Real_2601": "change the picture 1 to realistic photograph", "Semirealistic-photo-detailer": "transform the image to semi-realistic image", "AnyPose": "Make the person in image 1 do the exact same pose of the person in image 2. Changing the style and background of the image of the person in image 1 is undesirable, so don't do it. The new pose should be pixel accurate to the pose we are trying to copy. The position of the arms and head and legs should be the same as the pose we are trying to copy. Change the field of view and angle to match exactly image 2. Head tilt and eye gaze pose should match the person in image 2.", "Hyperrealistic-Portrait": "Transform the image into an ultra-realistic photorealistic portrait with strict identity preservation, facing straight to the camera. Enhance pore-level skin textures, realistic moisture effects, and natural wet hair clumping against the skin. Apply cool-toned soft-box lighting with subtle highlights and shadows, maintain realistic green-hazel eye catchlights without synthetic gloss, and preserve soft natural lip texture. Use shallow depth of field with a clean background, an 85mm macro photographic look, and raw photo grading without retouching to maintain realism and original details.", "Ultrarealistic-Portrait": "Transform the image into an ultra-realistic glamour portrait while strictly preserving the subject’s identity. Apply a close-up composition with a slight head tilt and a hand near the face, enhance cinematic directional lighting with dramatic fashion-style highlights, and refine makeup details including glowing skin, glossy lips, luminous highlighter, and defined eyes. Increase skin realism with detailed epidermal textures such as micropores, microhairs, subtle oil sheen, natural highlights, soft wrinkles, and subsurface scattering. Maintain a luxury fashion-magazine look in a 9:16 aspect ratio, preserving realism, facial structure, and original details without over-smoothing or retouching.", "Upscale2K": "Upscale this picture to 4K resolution.", "BFS-Best-FaceSwap": "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, strictly preserving the hair, eye color, and nose structure of Picture 2. copy the eye direction, head rotation, and micro-expressions from Picture 1. high quality, sharp details, 4k", "BFS-Best-FaceSwap-merge": "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, strictly preserving the hair, eye color, and nose structure of Picture 2. copy the eye direction, head rotation, and micro-expressions from Picture 1. high quality, sharp details, 4k", } LOADED_ADAPTERS = set() # ============================================================ # Helpers: resolution # ============================================================ def _round_to_multiple(x: int, m: int) -> int: return max(m, (int(x) // m) * m) def compute_canvas_dimensions_from_area( image: Image.Image, target_area: int, multiple_of: int = 64, ) -> Tuple[int, int]: w0, h0 = image.size if w0 <= 0 or h0 <= 0: return 512, 512 aspect = w0 / h0 w = int((target_area * aspect) ** 0.5) h = int(w / aspect) if aspect != 0 else int((target_area) ** 0.5) w = _round_to_multiple(w, multiple_of) h = _round_to_multiple(h, multiple_of) w = max(multiple_of, w) h = max(multiple_of, h) return w, h def get_target_area_for_lora(image: Image.Image, lora_adapter: str, target_megapixels: float) -> int: spec = ADAPTER_SPECS.get(lora_adapter, {}) long_edge = spec.get("target_long_edge", None) if long_edge: w0, h0 = image.size if w0 <= 0 or h0 <= 0: return int(1.0 * 1024 * 1024) scale = float(long_edge) / float(max(w0, h0)) w = int(w0 * scale) h = int(h0 * scale) return max(64 * 64, w * h) mp = float(target_megapixels) return max(64 * 64, int(mp * 1_000_000)) # ============================================================ # Helpers: LoRA loading + alpha fix # ============================================================ def _download_from_hf(repo_id: str, filename: str) -> str: return hf_hub_download(repo_id=repo_id, filename=filename) def _maybe_apply_alpha_fix(state_dict: dict) -> dict: if "img_in.alpha" not in state_dict: for k in list(state_dict.keys()): if k.endswith("img_in.weight") or k.endswith("img_in.bias"): t = state_dict[k] if hasattr(t, "new_zeros"): state_dict["img_in.alpha"] = t.new_zeros(()) break return state_dict def _load_single_lora(spec: dict): local_path = _download_from_hf(spec["repo"], spec["weights"]) sd = safetensors_load_file(local_path) if spec.get("needs_alpha_fix", False): sd = _maybe_apply_alpha_fix(sd) pipe.load_lora_weights(sd, adapter_name=spec["adapter_name"]) LOADED_ADAPTERS.add(spec["adapter_name"]) def _ensure_loaded_and_get_active_adapters(lora_adapter: str): spec = ADAPTER_SPECS.get(lora_adapter, None) if spec is None: return [], [] if spec["type"] == "single": if spec["adapter_name"] not in LOADED_ADAPTERS: _load_single_lora(spec) return [spec["adapter_name"]], [spec.get("strength", 1.0)] adapter_names = [] weights = [] for part in spec["parts"]: if part["adapter_name"] not in LOADED_ADAPTERS: _load_single_lora(part) adapter_names.append(part["adapter_name"]) weights.append(part.get("strength", 1.0)) return adapter_names, weights def lora_requires_two_images(lora_adapter: str) -> bool: spec = ADAPTER_SPECS.get(lora_adapter, {}) return bool(spec.get("requires_two_images", False)) def get_image2_label_for_lora(lora_adapter: str) -> str: spec = ADAPTER_SPECS.get(lora_adapter, {}) return spec.get("image2_label", "Upload Reference (Image 2)") def build_labeled_images(img1: Image.Image, img2: Optional[Image.Image], extras: list[Image.Image]): labeled = {"image_1": img1} if img2 is not None: labeled["image_2"] = img2 for ex in extras: labeled[f"image_{len(labeled) + 1}"] = ex return labeled # ============================================================ # UI: lora change handler # ============================================================ def on_lora_change_ui(lora_adapter, current_prompt, current_extras_condition_only): preset = LORA_PRESET_PROMPTS.get(lora_adapter, None) prompt_update = gr.update(value=preset) if preset else gr.update(value=current_prompt) needs_two = lora_requires_two_images(lora_adapter) img2_update = gr.update(visible=needs_two, label=get_image2_label_for_lora(lora_adapter)) extras_update = gr.update(value=True) if needs_two else gr.update(value=current_extras_condition_only) return prompt_update, img2_update, extras_update # ============================================================ # Output routing + derived conditioning # ============================================================ def set_output_as_image1(last): if last is None: raise gr.Error("No output available yet.") return gr.update(value=last) def set_output_as_image2(last): if last is None: raise gr.Error("No output available yet.") return gr.update(value=last) def set_output_as_extra(last, existing_extra): if last is None: raise gr.Error("No output available yet.") return _append_to_gallery(existing_extra, last) @spaces.GPU def add_derived_ref(img1, existing_extra, derived_type, derived_use_gpu): if img1 is None: raise gr.Error("Please upload Image 1 first.") if derived_type == "None": return gr.update(value=existing_extra), gr.update(visible=False, value=None) base = img1.convert("RGB") if derived_type == "Depth (Depth Anything V2 Small)": derived = make_depth_map(base, use_gpu=bool(derived_use_gpu)) else: raise gr.Error(f"Unknown derived type: {derived_type}") new_gallery = _append_to_gallery(existing_extra, derived) return gr.update(value=new_gallery), gr.update(visible=True, value=derived) # ============================================================ # Prompt Helper (outsourced VLM calls, UI stays clean) # ============================================================ # Configuration via env vars (no UI clutter) HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() or os.environ.get("HUGGINGFACEHUB_API_TOKEN", "").strip() HF_PROVIDER = os.environ.get("HF_PROVIDER", "nebius").strip() HF_VLM_MODEL = os.environ.get("HF_VLM_MODEL", "Qwen/Qwen2.5-VL-7B-Instruct").strip() _client_cache = {} def _get_client() -> InferenceClient: key = (HF_PROVIDER, bool(HF_TOKEN)) if key in _client_cache: return _client_cache[key] if not HF_TOKEN: raise gr.Error("Captioning is not configured (missing HF_TOKEN).") client = InferenceClient(provider=HF_PROVIDER, api_key=HF_TOKEN) _client_cache[key] = client return client def _encode_image_data_url(img: Image.Image, max_side: int = 1536, fmt: str = "PNG") -> str: """ Converts PIL to data URL (base64). Downscales to keep payload reasonable. """ img = img.convert("RGB") w, h = img.size scale = min(1.0, float(max_side) / float(max(w, h))) if max(w, h) > 0 else 1.0 if scale < 1.0: img = img.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format=fmt) b64 = base64.b64encode(buf.getvalue()).decode("utf-8") mime = "image/png" if fmt.upper() == "PNG" else "image/jpeg" return f"data:{mime};base64,{b64}" def _chat_with_image( system_prompt: str, user_text: str, image: Image.Image, *, max_tokens: int, temperature: float, ) -> str: client = _get_client() data_url = _encode_image_data_url(image) messages = [ {"role": "system", "content": system_prompt}, { "role": "user", "content": [ {"type": "text", "text": user_text}, {"type": "image_url", "image_url": {"url": data_url}}, ], }, ] # Hugging Face chat.completions interface resp = client.chat.completions.create( model=HF_VLM_MODEL, messages=messages, max_tokens=int(max_tokens), temperature=float(temperature), ) return (resp.choices[0].message.content or "").strip() def _chat_text_only( system_prompt: str, user_text: str, *, max_tokens: int, temperature: float, ) -> str: client = _get_client() messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": [{"type": "text", "text": user_text}]}, ] resp = client.chat.completions.create( model=HF_VLM_MODEL, messages=messages, max_tokens=int(max_tokens), temperature=float(temperature), ) return (resp.choices[0].message.content or "").strip() def _has_header(text: str, header: str) -> bool: return header in (text or "") def _enforce_once_retry_image(system_prompt: str, user_text: str, image: Image.Image, header: str, max_tokens: int, temperature: float) -> str: out = _chat_with_image(system_prompt, user_text, image, max_tokens=max_tokens, temperature=temperature) if _has_header(out, header): return out # one strict retry retry_user = ( user_text + "\n\nIMPORTANT: You did not follow the required output format. " + f"Return EXACTLY the block starting with {header} and fill each line. No extra text." ) out2 = _chat_with_image(system_prompt, retry_user, image, max_tokens=max_tokens, temperature=temperature) return out2 def _enforce_once_retry_text(system_prompt: str, user_text: str, header: str, max_tokens: int, temperature: float) -> str: out = _chat_text_only(system_prompt, user_text, max_tokens=max_tokens, temperature=temperature) if _has_header(out, header): return out retry_user = ( user_text + "\n\nIMPORTANT: You did not follow the required output format. " + f"Return EXACTLY the sections starting with {header}. No extra text." ) return _chat_text_only(system_prompt, retry_user, max_tokens=max_tokens, temperature=temperature) # --------- BASE (Pic1) extraction prompt (no identity) ---------- BFS_BASE_SYSTEM = """You are extracting non-identity facial and contextual signals from Picture 1 (BASE) for a head/face swap. CRITICAL: DO NOT describe identity/likeness traits. That means: - No age, ethnicity/race/nationality guesses, attractiveness judgments, “looks like X” - No skin tone, facial structure descriptions, “round face”, “strong jaw”, etc. - No hair color/style as identity markers (only mention hair if it occludes the face, e.g. “hair covering left eye”) Focus ONLY on: - Head pose (yaw/pitch/roll, tilt, chin/jaw position) - Gaze and eyelids (direction, openness) - Micro-expressions / muscle cues (brow knit/raise, squint, lip tension, mouth corners, cheek tension, jaw set) - Mouth details (open/closed, teeth, tongue if visible) - Mood inference (max 2 labels) with visible evidence cues - Occlusions and interactions (hands, objects, glasses, shadows) relevant to face recreation - Visibility notes (unclear/occluded/shadowed) Output format (return exactly this block, nothing else): [BASE_SIGNALS_PIC1] Head pose: Gaze & eyelids: Expression (muscle cues): Mouth details: Mood (max 2 labels): Evidence for mood (visible cues only): Occlusions & interactions: Visibility notes (unclear/occluded/shadowed areas): """ BFS_BASE_USER = """Analyze the single provided image as Picture 1 (BASE). Fill every line with either an observation or the word "unclear". Keep it concise.""" # --------- DONOR (Pic2) extraction prompt (identity only) ---------- BFS_DONOR_SYSTEM = """You are extracting inherent identity/likeness traits from Picture 2 (DONOR) for a head/face swap. CRITICAL: DO NOT describe expression, mood, gaze direction, head pose/rotation, body pose, or actions. Focus ONLY on visible physical traits: - Face shape & proportions (jawline, cheekbones, chin shape) - Skin tone/undertone + texture (freckles/moles only if visible) - Eyes (color, shape), brows (shape/thickness) - Nose structure (bridge, tip, nostrils) - Lips/mouth shape (fullness, cupid’s bow) - Chin/jaw details - Hair (color, style, hairline) - Distinctive traits (scars/moles/freckles if visible) - Visibility notes (unclear/occluded/shadowed) Output format (return exactly this block, nothing else): [DONOR_TRAITS_PIC2] Face shape & proportions: Skin tone & texture: Eyes & brows: Nose structure: Lips & mouth shape: Chin/jaw details: Hair (color, style, hairline): Distinctive traits (scars/moles/freckles if visible): Visibility notes (unclear/occluded/shadowed areas): """ BFS_DONOR_USER = """Analyze the single provided image as Picture 2 (DONOR). Fill every line with either an observation or the word "unclear". Keep it concise.""" # --------- Text-only prompt builder ---------- BFS_BUILDER_SYSTEM = """You are a prompt editor for BFS-BestFaceSwap. Input you may receive: - A core prompt (already includes head_swap instructions) - BASE_SIGNALS_PIC1 text (pose/expression/mood/occlusions; non-identity) - Optional DONOR_TRAITS_PIC2 text (identity-only traits) Your job: - Produce a compact addendum that improves expressiveness transfer and reduces ambiguity. - Do NOT add any identity traits from the base signals. - Do NOT add any pose/expression/mood from donor traits. - Prefer concrete, visible cues over vague adjectives. - Keep it short (ideally 6–14 lines total). - If donor traits are missing or mostly "unclear", omit donor section entirely. Output EXACTLY two sections (donor section may be omitted if not provided/usable): [ADDENDUM_BASE] (bullets or short lines; use the best cues from BASE_SIGNALS) [ADDENDUM_DONOR] (optional; only if donor traits contain useful visible info; no pose/expression) """ def scrub_placeholder(text: str, enabled: bool) -> str: # Placeholder for future strict scrubber pass (no-op). return text @spaces.GPU def caption_base_pic1( img1, max_new_tokens: int, temperature: float, strict_scrubber: bool, show_debug: bool, ): if img1 is None: raise gr.Error("Please upload Image 1 (base) first.") raw = _enforce_once_retry_image( BFS_BASE_SYSTEM, BFS_BASE_USER, img1, header="[BASE_SIGNALS_PIC1]", max_tokens=int(max_new_tokens), temperature=float(temperature), ) out = scrub_placeholder(raw, enabled=bool(strict_scrubber)) debug = raw if bool(show_debug) else "" return out, debug @spaces.GPU def caption_donor_pic2( img2, max_new_tokens: int, temperature: float, strict_scrubber: bool, show_debug: bool, ): if img2 is None: raise gr.Error("Please upload Image 2 (donor) first.") raw = _enforce_once_retry_image( BFS_DONOR_SYSTEM, BFS_DONOR_USER, img2, header="[DONOR_TRAITS_PIC2]", max_tokens=int(max_new_tokens), temperature=float(temperature), ) out = scrub_placeholder(raw, enabled=bool(strict_scrubber)) debug = raw if bool(show_debug) else "" return out, debug def _compose_final_prompt(core_prompt: str, addendum_text: str, mode: str) -> str: core = (core_prompt or "").strip() addendum = (addendum_text or "").strip() if not addendum: return core if (mode or "").lower().startswith("inject"): injected = core if "{BFS_ADDENDUM}" in injected: injected = injected.replace("{BFS_ADDENDUM}", addendum + "\n") return injected.strip() return (core + "\n\n" + addendum).strip() @spaces.GPU def build_bfs_addendum_and_final_prompt( core_prompt: str, base_caption: str, donor_caption: str, integration_mode: str, max_new_tokens: int, temperature: float, show_debug: bool, ): base = (base_caption or "").strip() donor = (donor_caption or "").strip() core = (core_prompt or "").strip() if not base: raise gr.Error("Generate BASE signals (Pic1) first (or paste them) before building an addendum.") user_text = ( "CORE PROMPT:\n" f"{core}\n\n" "BASE_SIGNALS_PIC1:\n" f"{base}\n\n" "DONOR_TRAITS_PIC2:\n" f"{donor if donor else '(none)'}\n\n" "Produce the addendum now." ) raw = _enforce_once_retry_text( BFS_BUILDER_SYSTEM, user_text, header="[ADDENDUM_BASE]", max_tokens=int(max_new_tokens), temperature=float(temperature), ) final_prompt = _compose_final_prompt(core, raw, integration_mode) debug = raw if bool(show_debug) else "" return raw, final_prompt, debug # ============================================================ # Inference # ============================================================ @spaces.GPU def infer( input_image_1, input_image_2, input_images_extra, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, target_megapixels, extras_condition_only, pad_to_canvas, progress=gr.Progress(track_tqdm=True), ): gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() if input_image_1 is None: raise gr.Error("Please upload Image 1.") if lora_adapter == NONE_LORA: try: pipe.set_adapters([], adapter_weights=[]) except Exception: if LOADED_ADAPTERS: pipe.set_adapters(list(LOADED_ADAPTERS), adapter_weights=[0.0] * len(LOADED_ADAPTERS)) else: adapter_names, adapter_weights = _ensure_loaded_and_get_active_adapters(lora_adapter) pipe.set_adapters(adapter_names, adapter_weights=adapter_weights) if randomize_seed: seed = random.randint(0, MAX_SEED) generator = torch.Generator(device=device).manual_seed(seed) negative_prompt = ( "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, " "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry" ) img1 = input_image_1.convert("RGB") img2 = input_image_2.convert("RGB") if input_image_2 is not None else None extra_imgs: list[Image.Image] = [] if input_images_extra: for item in input_images_extra: pil = _to_pil_rgb(item) if pil is not None: extra_imgs.append(pil) if lora_requires_two_images(lora_adapter) and img2 is None: raise gr.Error("This LoRA needs two images. Please upload Image 2 as well.") labeled = build_labeled_images(img1, img2, extra_imgs) pipe_images = list(labeled.values()) if len(pipe_images) == 1: pipe_images = pipe_images[0] target_area = get_target_area_for_lora(img1, lora_adapter, float(target_megapixels)) width, height = compute_canvas_dimensions_from_area( img1, target_area=target_area, multiple_of=int(pipe.vae_scale_factor * 2), ) vae_image_indices = None if extras_condition_only: if isinstance(pipe_images, list) and len(pipe_images) > 2: vae_image_indices = [0, 1] if len(pipe_images) >= 2 else [0] try: result = pipe( image=pipe_images, prompt=prompt, negative_prompt=negative_prompt, height=height, width=width, num_inference_steps=steps, generator=generator, true_cfg_scale=guidance_scale, vae_image_indices=vae_image_indices, pad_to_canvas=bool(pad_to_canvas), ).images[0] return result, seed, result finally: gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() @spaces.GPU def infer_example(input_image, prompt, lora_adapter): if input_image is None: return None, 0, None input_pil = input_image.convert("RGB") guidance_scale = 1.0 steps = 4 result, seed, last = infer( input_pil, None, None, prompt, lora_adapter, 0, True, guidance_scale, steps, 1.0, True, True, ) return result, seed, last # ============================================================ # UI # ============================================================ css = """ #col-container { margin: 0 auto; max-width: 960px; } #main-title h1 { font-size: 2.1em !important; } """ aio_status_line = ( f"**AIO transformer version:** `{AIO_VERSION}` " f"({AIO_VERSION_SOURCE}; env `AIO_VERSION`={_AIO_ENV_RAW!r})" ) with gr.Blocks() as demo: with gr.Column(elem_id="col-container"): gr.Markdown("# **Qwen-Image-Edit-2511-LoRAs-Fast**", elem_id="main-title") gr.Markdown( "Perform diverse image edits using specialized " "[LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters for the " "[Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) model." ) gr.Markdown(aio_status_line) with gr.Row(equal_height=True): with gr.Column(): input_image_1 = gr.Image(label="Upload Image 1 (Base / Target)", type="pil", height=290) input_image_2 = gr.Image(label="Upload Reference (Image 2)", type="pil", height=290, visible=False) input_images_extra = gr.Gallery( label="Upload Additional Images (auto-indexed after Image 1/2)", type="pil", height=290, columns=4, rows=2, interactive=True, ) prompt = gr.Text( label="Edit Prompt", show_label=True, placeholder="e.g., transform into photo..", ) with gr.Accordion("BFS Prompt Helper", open=False): with gr.Row(): helper_max_tokens = gr.Slider(label="Max new tokens", minimum=64, maximum=1024, step=16, value=384) helper_temperature = gr.Slider(label="Temperature (0 = deterministic)", minimum=0.0, maximum=1.2, step=0.05, value=0.2) with gr.Row(): strict_scrubber = gr.Checkbox(label="Strict scrubber (placeholder, no-op)", value=False) show_debug = gr.Checkbox(label="Show debug outputs", value=False) with gr.Row(): btn_cap_base = gr.Button("Generate BASE signals (Pic1)", variant="secondary") btn_cap_donor = gr.Button("Generate DONOR traits (Pic2) (optional)", variant="secondary") with gr.Row(): caption_pic1 = gr.Textbox(label="BASE signals (from Image 1)", lines=12, value="") caption_pic2 = gr.Textbox(label="DONOR traits (from Image 2) (optional)", lines=12, value="") with gr.Row(): debug_base = gr.Textbox(label="Debug: raw BASE output", lines=8, visible=False) debug_donor = gr.Textbox(label="Debug: raw DONOR output", lines=8, visible=False) integration_mode = gr.Radio( label="How to apply addendum to the core prompt", choices=["Concatenate", "Inject (placeholder {BFS_ADDENDUM})"], value="Concatenate", ) with gr.Row(): btn_build_addendum = gr.Button("Build addendum + final prompt", variant="primary") btn_apply_final = gr.Button("Apply final prompt → Edit Prompt", variant="secondary") bfs_addendum = gr.Textbox(label="Built addendum (editable)", lines=10, value="") bfs_final_prompt = gr.Textbox(label="Final prompt preview (editable)", lines=10, value="") debug_builder = gr.Textbox(label="Debug: raw builder output", lines=8, visible=False) run_button = gr.Button("Edit Image", variant="primary") with gr.Column(): output_image = gr.Image(label="Output Image", interactive=False, format="png", height=353) last_output = gr.State(value=None) with gr.Row(): btn_out_to_img1 = gr.Button("⬅️ Output → Image 1", variant="secondary") btn_out_to_img2 = gr.Button("⬅️ Output → Image 2", variant="secondary") btn_out_to_extra = gr.Button("➕ Output → Extra Ref", variant="secondary") derived_preview = gr.Image( label="Derived Conditioning Preview", interactive=False, format="png", height=200, visible=False, ) with gr.Row(): lora_choices = [NONE_LORA] + list(ADAPTER_SPECS.keys()) lora_adapter = gr.Dropdown( label="Choose Editing Style", choices=lora_choices, value=NONE_LORA, ) with gr.Accordion("Advanced Settings", open=False, visible=True): with gr.Accordion("Derived Conditioning (Depth)", open=False): derived_type = gr.Dropdown( label="Derived Type (from Image 1)", choices=["None", "Depth (Depth Anything V2 Small)"], value="None", ) derived_use_gpu = gr.Checkbox(label="Use GPU for derived model", value=False) add_derived_btn = gr.Button("➕ Add derived ref to Extras (conditioning-only recommended)") seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0) randomize_seed = gr.Checkbox(label="Randomize Seed", value=True) guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0) steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4) target_megapixels = gr.Slider( label="Target Megapixels (canvas)", minimum=0.5, maximum=6.0, step=0.1, value=1.0, ) extras_condition_only = gr.Checkbox( label="Extra references are conditioning-only (exclude from VAE)", value=True, ) pad_to_canvas = gr.Checkbox( label="Pad images to canvas aspect (avoid warping)", value=True, ) # LoRA selection: preset prompt + toggle Image 2 lora_adapter.change( fn=on_lora_change_ui, inputs=[lora_adapter, prompt, extras_condition_only], outputs=[prompt, input_image_2, extras_condition_only], ) # Debug visibility toggles show_debug.change( fn=lambda x: ( gr.update(visible=bool(x)), gr.update(visible=bool(x)), gr.update(visible=bool(x)), ), inputs=[show_debug], outputs=[debug_base, debug_donor, debug_builder], ) # Caption buttons (single-image) btn_cap_base.click( fn=caption_base_pic1, inputs=[input_image_1, helper_max_tokens, helper_temperature, strict_scrubber, show_debug], outputs=[caption_pic1, debug_base], ) btn_cap_donor.click( fn=caption_donor_pic2, inputs=[input_image_2, helper_max_tokens, helper_temperature, strict_scrubber, show_debug], outputs=[caption_pic2, debug_donor], ) # Builder (text-only) btn_build_addendum.click( fn=build_bfs_addendum_and_final_prompt, inputs=[ prompt, caption_pic1, caption_pic2, integration_mode, helper_max_tokens, helper_temperature, show_debug, ], outputs=[bfs_addendum, bfs_final_prompt, debug_builder], ) # Apply final prompt to the Edit Prompt box btn_apply_final.click( fn=lambda x: gr.update(value=x), inputs=[bfs_final_prompt], outputs=[prompt], ) gr.Examples( examples=[ ["examples/5.jpg", "Remove shadows and relight the image using soft lighting.", "Light-Restoration"], ["examples/4.jpg", "Use a subtle golden-hour filter with smooth light diffusion.", "Relight"], ["examples/2.jpeg", "Rotate the camera 45 degrees to the left.", "Multiple-Angles"], ["examples/11.jpg", "Upscale this picture to 4K resolution.", "Upscale2K"], ], inputs=[input_image_1, prompt, lora_adapter], outputs=[output_image, seed, last_output], fn=infer_example, cache_examples=False, label="Examples", ) run_button.click( fn=infer, inputs=[ input_image_1, input_image_2, input_images_extra, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, target_megapixels, extras_condition_only, pad_to_canvas, ], outputs=[output_image, seed, last_output], ) # Output routing btn_out_to_img1.click(fn=set_output_as_image1, inputs=[last_output], outputs=[input_image_1]) btn_out_to_img2.click(fn=set_output_as_image2, inputs=[last_output], outputs=[input_image_2]) btn_out_to_extra.click(fn=set_output_as_extra, inputs=[last_output, input_images_extra], outputs=[input_images_extra]) # Derived conditioning: append depth map add_derived_btn.click( fn=add_derived_ref, inputs=[input_image_1, input_images_extra, derived_type, derived_use_gpu], outputs=[input_images_extra, derived_preview], ) if __name__ == "__main__": demo.queue(max_size=30).launch( css=css, theme=orange_red_theme, mcp_server=True, ssr_mode=False, show_error=True, )