import base64 import io import json import os import random import subprocess import sys import threading import time import uuid from pathlib import Path import gradio as gr import requests import spaces from huggingface_hub import hf_hub_download from PIL import Image import piexif MODEL_REPO = "beznogim666/test2" MODEL_FILE = "model.safetensors" COMFY_KREA_REPO = "Comfy-Org/Krea-2" TEXT_ENCODER_FILE = "qwen3vl_4b_bf16.safetensors" VAE_FILE = "qwen_image_vae.safetensors" COMFY_REPO = "https://github.com/comfyanonymous/ComfyUI.git" COMFY_DIR = Path(os.environ.get("COMFYUI_DIR", "/tmp/ComfyUI")) COMFY_HOST = "127.0.0.1" COMFY_PORT = int(os.environ.get("COMFYUI_PORT", "8188")) COMFY_URL = f"http://{COMFY_HOST}:{COMFY_PORT}" MAX_SEED = 2**31 - 1 LORA_REPO = "beznogim666/test2" LORA_FILE = "lora1.safetensors" LORA_NONE = "none" DURATION_MIN = 10 DURATION_MAX = 300 DURATION_DEFAULT = 90 _comfy_lock = threading.Lock() _comfy_process = None def _run(cmd, cwd=None): print("[setup]", " ".join(map(str, cmd)), flush=True) subprocess.check_call(cmd, cwd=str(cwd) if cwd else None) def _wait_for_comfy(timeout=180): deadline = time.time() + timeout last_error = None while time.time() < deadline: try: response = requests.get(f"{COMFY_URL}/system_stats", timeout=2) if response.ok: return except Exception as exc: last_error = exc time.sleep(1) raise RuntimeError(f"ComfyUI did not start in time: {last_error}") def _validate_comfyui(): response = requests.get(f"{COMFY_URL}/object_info", timeout=30) response.raise_for_status() object_info = response.json() required_nodes = [ "UNETLoader", "CLIPLoader", "VAELoader", "CLIPTextEncode", "KSampler", "VAEDecode", "SaveImage", "LoraLoaderModelOnly", ] missing = [node for node in required_nodes if node not in object_info] if missing: raise RuntimeError(f"ComfyUI is missing required nodes: {', '.join(missing)}") unet_info = object_info["UNETLoader"]["input"]["required"]["unet_name"][0] clip_info = object_info["CLIPLoader"]["input"]["required"]["clip_name"][0] vae_info = object_info["VAELoader"]["input"]["required"]["vae_name"][0] if MODEL_FILE not in unet_info: raise RuntimeError(f"Redcraft diffusion model is not visible to ComfyUI. First models: {', '.join(unet_info[:10])}") if TEXT_ENCODER_FILE not in clip_info: raise RuntimeError(f"Krea2 text encoder is not visible to ComfyUI. First encoders: {', '.join(clip_info[:10])}") if VAE_FILE not in vae_info: raise RuntimeError(f"Krea2 VAE is not visible to ComfyUI. First VAEs: {', '.join(vae_info[:10])}") lora_info = object_info["LoraLoaderModelOnly"]["input"]["required"]["lora_name"][0] if LORA_FILE not in lora_info: raise RuntimeError(f"LoRA is not visible to ComfyUI. First LoRAs: {', '.join(lora_info[:10])}") def _ensure_comfyui(): if not COMFY_DIR.exists(): _run(["git", "clone", "--depth", "1", COMFY_REPO, str(COMFY_DIR)]) marker = COMFY_DIR / ".requirements-installed" if not marker.exists(): _run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=COMFY_DIR) marker.write_text("ok", encoding="utf-8") diffusion_dir = COMFY_DIR / "models" / "diffusion_models" text_encoder_dir = COMFY_DIR / "models" / "text_encoders" vae_dir = COMFY_DIR / "models" / "vae" lora_dir = COMFY_DIR / "models" / "loras" diffusion_dir.mkdir(parents=True, exist_ok=True) text_encoder_dir.mkdir(parents=True, exist_ok=True) vae_dir.mkdir(parents=True, exist_ok=True) lora_dir.mkdir(parents=True, exist_ok=True) hf_hub_download( repo_id=MODEL_REPO, filename=MODEL_FILE, local_dir=str(diffusion_dir), token=os.environ.get("HF_TOKEN"), ) hf_hub_download( repo_id=LORA_REPO, filename=LORA_FILE, local_dir=str(lora_dir), token=os.environ.get("HF_TOKEN"), ) hf_hub_download( repo_id=COMFY_KREA_REPO, filename=f"text_encoders/{TEXT_ENCODER_FILE}", local_dir=str(COMFY_DIR / "models"), token=os.environ.get("HF_TOKEN"), ) hf_hub_download( repo_id=COMFY_KREA_REPO, filename=f"vae/{VAE_FILE}", local_dir=str(COMFY_DIR / "models"), token=os.environ.get("HF_TOKEN"), ) def _start_comfyui(): global _comfy_process with _comfy_lock: if _comfy_process is not None and _comfy_process.poll() is None: try: response = requests.get(f"{COMFY_URL}/system_stats", timeout=2) if response.ok: return except Exception: pass cmd = [ sys.executable, "main.py", "--listen", COMFY_HOST, "--port", str(COMFY_PORT), "--disable-auto-launch", "--use-sage-attention", ] _comfy_process = subprocess.Popen(cmd, cwd=str(COMFY_DIR)) _wait_for_comfy() _validate_comfyui() def _list_loras(): try: response = requests.get(f"{COMFY_URL}/object_info/LoraLoaderModelOnly", timeout=5) if response.ok: info = response.json() names = info["LoraLoaderModelOnly"]["input"]["required"]["lora_name"][0] return [LORA_NONE] + list(names) except Exception: pass lora_dir = COMFY_DIR / "models" / "loras" if lora_dir.exists(): names = sorted(p.name for p in lora_dir.glob("*.safetensors")) return [LORA_NONE] + names return [LORA_NONE] def _refresh_comfy_models(): """ Форсит обновление внутреннего кэша путей ComfyUI, чтобы свежескачанные модели/лоры сразу стали доступны API-лоадерам. """ try: response = requests.post(f"{COMFY_URL}/extra_model_paths", json={}, timeout=5) if response.ok: print("[setup] ComfyUI model cache refreshed successfully.", flush=True) except Exception as e: print(f"[error] Failed to refresh ComfyUI model cache: {e}", flush=True) def _cleanup_old_files(directory, keep_filenames): """ Удаляет все файлы в указанной директории, кроме базовых дефолтных и того кастомного файла, который мы используем прямо сейчас. """ try: directory = Path(directory) if not directory.exists(): return keep_set = {f.lower() for f in keep_filenames if f} for file_path in directory.glob("*"): if file_path.is_file(): # Не трогаем скрытые/системные файлы (.gitattributes и т.д.) if file_path.name.startswith("."): continue if file_path.name.lower() not in keep_set: try: file_path.unlink() print(f"[ZDR] Cleaned up old file from disk: {file_path}", flush=True) except Exception as e: print(f"[ZDR] Failed to delete old file {file_path}: {e}", flush=True) except Exception as e: print(f"[error] Error during disk cleanup: {e}", flush=True) def _download_dynamic_model(repo, filename): if not repo or not filename: # Если кастомное поле пустое — чистим старые скачанные модели, оставляя только дефолт _cleanup_old_files(COMFY_DIR / "models" / "diffusion_models", [MODEL_FILE]) return MODEL_FILE repo = repo.strip() filename = filename.strip() if not repo or not filename: _cleanup_old_files(COMFY_DIR / "models" / "diffusion_models", [MODEL_FILE]) return MODEL_FILE diffusion_dir = COMFY_DIR / "models" / "diffusion_models" diffusion_dir.mkdir(parents=True, exist_ok=True) try: print(f"[setup] Downloading dynamic Checkpoint: {repo}/{filename}", flush=True) local_path = hf_hub_download( repo_id=repo, filename=filename, local_dir=str(diffusion_dir), token=os.environ.get("HF_TOKEN"), ) downloaded_name = Path(local_path).name # Оставляем на диске только базовую модель и ту, что только что скачали _cleanup_old_files(diffusion_dir, [MODEL_FILE, downloaded_name]) _refresh_comfy_models() return str(Path(local_path).relative_to(diffusion_dir)) except Exception as e: print(f"[error] Failed to download dynamic Checkpoint from {repo}/{filename}: {e}", flush=True) raise RuntimeError(f"Failed to download dynamic Checkpoint: {e}") def _download_dynamic_lora(repo, filename): if not repo or not filename: # Чистим старые динамические лоры, оставляя только дефолт _cleanup_old_files(COMFY_DIR / "models" / "loras", [LORA_FILE]) return None repo = repo.strip() filename = filename.strip() if not repo or not filename: _cleanup_old_files(COMFY_DIR / "models" / "loras", [LORA_FILE]) return None lora_dir = COMFY_DIR / "models" / "loras" lora_dir.mkdir(parents=True, exist_ok=True) try: print(f"[setup] Downloading dynamic LoRA: {repo}/{filename}", flush=True) local_path = hf_hub_download( repo_id=repo, filename=filename, local_dir=str(lora_dir), token=os.environ.get("HF_TOKEN"), ) downloaded_name = Path(local_path).name # Оставляем только базовую лору и новую скачанную _cleanup_old_files(lora_dir, [LORA_FILE, downloaded_name]) _refresh_comfy_models() return str(Path(local_path).relative_to(lora_dir)) except Exception as e: print(f"[error] Failed to download dynamic LoRA from {repo}/{filename}: {e}", flush=True) raise RuntimeError(f"Failed to download dynamic LoRA: {e}") def _add_loras_to_workflow(workflow, model_node_ref, lora_name, lora_strength, lora2_name=None, lora2_strength=0.0): current_model = model_node_ref if lora_name and lora_name != LORA_NONE and float(lora_strength) != 0.0: workflow["20"] = { "class_type": "LoraLoaderModelOnly", "inputs": { "model": current_model, "lora_name": lora_name, "strength_model": float(lora_strength), }, } current_model = ["20", 0] if lora2_name and lora2_name != LORA_NONE and float(lora2_strength) != 0.0: workflow["21"] = { "class_type": "LoraLoaderModelOnly", "inputs": { "model": current_model, "lora_name": lora2_name, "strength_model": float(lora2_strength), }, } current_model = ["21", 0] return current_model def _build_workflow( prompt, negative_prompt, width, height, steps, cfg, seed, sampler, scheduler, lora_name=LORA_NONE, lora_strength=1.0, lora2_name=None, lora2_strength=0.0, unet_name=MODEL_FILE, ): workflow = { "1": { "class_type": "UNETLoader", "inputs": {"unet_name": unet_name, "weight_dtype": "default"}, }, "8": { "class_type": "CLIPLoader", "inputs": {"clip_name": TEXT_ENCODER_FILE, "type": "krea2", "device": "default"}, }, "9": { "class_type": "VAELoader", "inputs": {"vae_name": VAE_FILE}, }, "2": { "class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["8", 0]}, }, "4": { "class_type": "EmptyLatentImage", "inputs": {"width": int(width), "height": int(height), "batch_size": 1}, }, "5": { "class_type": "KSampler", "inputs": { "seed": int(seed), "steps": int(steps), "cfg": float(cfg), "sampler_name": sampler, "scheduler": scheduler, "denoise": 1.0, "model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["4", 0], }, }, "6": { "class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["9", 0]}, }, "7": { "class_type": "SaveImage", "inputs": {"filename_prefix": "redcraft", "images": ["6", 0]}, }, } if negative_prompt and negative_prompt.strip(): workflow["3"] = { "class_type": "CLIPTextEncode", "inputs": {"text": negative_prompt, "clip": ["8", 0]}, } else: workflow["3"] = { "class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["2", 0]}, } model_ref = _add_loras_to_workflow(workflow, ["1", 0], lora_name, lora_strength, lora2_name, lora2_strength) workflow["5"]["inputs"]["model"] = model_ref return workflow def _upload_image_to_comfy(image): if image is None: raise ValueError("Upload an image to edit.") image = image.convert("RGB") filename = f"redcraft-input-{uuid.uuid4().hex}.png" temp_path = Path("/tmp") / filename image.save(temp_path) try: with temp_path.open("rb") as handle: response = requests.post( f"{COMFY_URL}/upload/image", files={"image": (filename, handle, "image/png")}, data={"overwrite": "true"}, timeout=120, ) response.raise_for_status() finally: if temp_path.exists(): temp_path.unlink() print(f"[ZDR] Deleted temp upload file from /tmp: {temp_path}", flush=True) data = response.json() return data.get("name", filename) def _resize_for_edit(image, width, height): width, height = int(width), int(height) if width <= 0 or height <= 0: return image.convert("RGB") return image.convert("RGB").resize((width, height), Image.LANCZOS) def _build_edit_workflow( input_filename, prompt, negative_prompt, steps, cfg, seed, sampler, scheduler, denoise, lora_name=LORA_NONE, lora_strength=1.0, lora2_name=None, lora2_strength=0.0, unet_name=MODEL_FILE, ): workflow = { "1": { "class_type": "UNETLoader", "inputs": {"unet_name": unet_name, "weight_dtype": "default"}, }, "8": { "class_type": "CLIPLoader", "inputs": {"clip_name": TEXT_ENCODER_FILE, "type": "krea2", "device": "default"}, }, "9": { "class_type": "VAELoader", "inputs": {"vae_name": VAE_FILE}, }, "10": { "class_type": "LoadImage", "inputs": {"image": input_filename}, }, "11": { "class_type": "VAEEncode", "inputs": {"pixels": ["10", 0], "vae": ["9", 0]}, }, "2": { "class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["8", 0]}, }, "5": { "class_type": "KSampler", "inputs": { "seed": int(seed), "steps": int(steps), "cfg": float(cfg), "sampler_name": sampler, "scheduler": scheduler, "denoise": float(denoise), "model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["11", 0], }, }, "6": { "class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["9", 0]}, }, "7": { "class_type": "SaveImage", "inputs": {"filename_prefix": "redcraft-edit", "images": ["6", 0]}, }, } if negative_prompt and negative_prompt.strip(): workflow["3"] = { "class_type": "CLIPTextEncode", "inputs": {"text": negative_prompt, "clip": ["8", 0]}, } else: workflow["3"] = { "class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["2", 0]}, } model_ref = _add_loras_to_workflow(workflow, ["1", 0], lora_name, lora_strength, lora2_name, lora2_strength) workflow["5"]["inputs"]["model"] = model_ref return workflow def _queue_prompt(workflow): payload = {"prompt": workflow, "client_id": str(uuid.uuid4())} response = requests.post(f"{COMFY_URL}/prompt", json=payload, timeout=30) if not response.ok: raise RuntimeError(f"ComfyUI prompt error {response.status_code}: {response.text[:1000]}") return response.json()["prompt_id"] def _wait_for_history(prompt_id, timeout=900): deadline = time.time() + timeout while time.time() < deadline: response = requests.get(f"{COMFY_URL}/history/{prompt_id}", timeout=30) response.raise_for_status() history = response.json() if prompt_id in history: item = history[prompt_id] status = item.get("status", {}) if status.get("completed"): return item messages = status.get("messages") or [] for message in messages: if isinstance(message, list) and message and message[0] == "execution_error": raise RuntimeError(json.dumps(message[1], indent=2)[:2000]) time.sleep(1) raise RuntimeError("Timed out waiting for ComfyUI generation.") def _load_output_image(history_item): outputs = history_item.get("outputs", {}) for output in outputs.values(): for image in output.get("images", []): filename = image["filename"] subfolder = image.get("subfolder", "") image_type = image.get("type", "output") if image_type == "output": file_path = COMFY_DIR / "output" / subfolder / filename else: file_path = COMFY_DIR / "temp" / subfolder / filename if not file_path.exists(): raise RuntimeError(f"Generated file not found on disk: {file_path}") file_bytes = file_path.read_bytes() img = Image.open(io.BytesIO(file_bytes)) prompt_data = img.info.get("prompt", "") workflow_data = img.info.get("workflow", "") prompt_bytes = prompt_data.encode('utf-8') if isinstance(prompt_data, str) else prompt_data workflow_bytes = workflow_data.encode('utf-8') if isinstance(workflow_data, str) else workflow_data exif_dict = { "0th": { piexif.ImageIFD.Make: prompt_bytes, piexif.ImageIFD.ImageDescription: workflow_bytes } } exif_bytes = piexif.dump(exif_dict) buffer = io.BytesIO() img.convert("RGB").save(buffer, "WEBP", exif=exif_bytes, quality=90) webp_bytes = buffer.getvalue() base64_str = base64.b64encode(webp_bytes).decode("utf-8") data_url = f"data:image/webp;base64,{base64_str}" try: file_path.unlink() print(f"[ZDR] Generation file successfully deleted from Space disk: {file_path}", flush=True) except Exception as e: print(f"[ZDR] Failed to delete generation file {file_path}: {e}", flush=True) return data_url raise RuntimeError("ComfyUI completed without returning an image.") def _clamp_duration(value): try: value = int(value) except (TypeError, ValueError): value = DURATION_DEFAULT return max(DURATION_MIN, min(DURATION_MAX, value)) def _duration( prompt, negative_prompt, width, height, steps, cfg, seed, randomize_seed, sampler, scheduler, lora_name, lora_strength, lora2_repo, lora2_file, lora2_strength, custom_model_repo, custom_model_file, gpu_duration, ): return _clamp_duration(gpu_duration) def _edit_duration( input_image, prompt, negative_prompt, width, height, steps, cfg, denoise, seed, randomize_seed, sampler, scheduler, lora_name, lora_strength, edit_lora2_repo, edit_lora2_file, edit_lora2_strength, edit_custom_model_repo, edit_custom_model_file, gpu_duration, ): return _clamp_duration(gpu_duration) @spaces.GPU(duration=_duration) def generate( prompt, negative_prompt="", width=1024, height=1024, steps=10, cfg=1.0, seed=0, randomize_seed=True, sampler="er_sde", scheduler="simple", lora_name=LORA_NONE, lora_strength=1.0, lora2_repo="", lora2_file="", lora2_strength=0.0, custom_model_repo="", custom_model_file="", gpu_duration=DURATION_DEFAULT, ): if not prompt or not prompt.strip(): raise gr.Error("Enter a prompt.") if randomize_seed: seed = random.randint(0, MAX_SEED) try: _start_comfyui() unet_name = _download_dynamic_model(custom_model_repo, custom_model_file) lora2_name = _download_dynamic_lora(lora2_repo, lora2_file) workflow = _build_workflow( prompt, negative_prompt, width, height, steps, cfg, seed, sampler, scheduler, lora_name, lora_strength, lora2_name, lora2_strength, unet_name=unet_name, ) prompt_id = _queue_prompt(workflow) history_item = _wait_for_history(prompt_id) data_url = _load_output_image(history_item) html_img = f'' return html_img, seed except Exception as exc: raise gr.Error(str(exc)) from exc @spaces.GPU(duration=_edit_duration) def edit_image( input_image, prompt, negative_prompt="", width=1024, height=1024, steps=12, cfg=1.2, denoise=0.35, seed=0, randomize_seed=True, sampler="er_sde", scheduler="simple", lora_name=LORA_NONE, lora_strength=1.0, edit_lora2_repo="", edit_lora2_file="", edit_lora2_strength=0.0, edit_custom_model_repo="", edit_custom_model_file="", gpu_duration=DURATION_DEFAULT, ): if input_image is None: raise gr.Error("Upload an image to edit.") if not prompt or not prompt.strip(): raise gr.Error("Enter an edit prompt.") if randomize_seed: seed = random.randint(0, MAX_SEED) try: _start_comfyui() unet_name = _download_dynamic_model(edit_custom_model_repo, edit_custom_model_file) lora2_name = _download_dynamic_lora(edit_lora2_repo, edit_lora2_file) resized = _resize_for_edit(input_image, width, height) input_filename = _upload_image_to_comfy(resized) workflow = _build_edit_workflow( input_filename, prompt, negative_prompt, steps, cfg, seed, sampler, scheduler, denoise, lora_name, lora_strength, lora2_name, lora2_strength, unet_name=unet_name, ) prompt_id = _queue_prompt(workflow) history_item = _wait_for_history(prompt_id) data_url = _load_output_image(history_item) try: input_file_path = COMFY_DIR / "input" / input_filename if input_file_path.exists(): input_file_path.unlink() print(f"[ZDR] Input file successfully deleted from Space disk: {input_file_path}", flush=True) except Exception as e: print(f"[ZDR] Failed to delete input file: {e}", flush=True) html_img = f'' return html_img, seed except Exception as exc: raise gr.Error(str(exc)) from exc def refresh_loras(): choices = _list_loras() return gr.update(choices=choices, value=LORA_NONE) CSS = """ .gradio-container { max-width: 1120px !important; margin: 0 auto !important; } #result-image { min-height: 520px; } /* Temporary hotfix for Chromium 144+ subpixel layout shifting in HF iframes */ .gradio-container { display: inline-table !important; width: 0px !important; height: 0px !important; overflow: hidden !important; opacity: 0 !important; pointer-events: none !important; } """ DURATION_INFO = ( "How many seconds of ZeroGPU quota to request for this call. Krea2-Turbo " "usually finishes in ~10-20s once ComfyUI is warm - keep this low to save " "quota and get better queue priority. Bump it up only if a call times out " "(e.g. the very first generation after the Space restarts, while ComfyUI " "and the model are still loading)." ) LORA_INFO = ( "Drop .safetensors files into ComfyUI/models/loras in the repo, then hit " "Refresh. 'none' skips LoRA entirely." ) with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo: gr.Markdown("# Redcraft Krea2") gr.Markdown("ComfyUI-native Redcraft Krea2 generation and image editing.") with gr.Tabs(): with gr.Tab("Generate"): with gr.Row(): with gr.Column(scale=5): prompt = gr.Textbox(label="Prompt", lines=5, placeholder="Describe the image to generate.") negative_prompt = gr.Textbox(label="Negative prompt", lines=2, value="") with gr.Row(): width = gr.Slider(512, 3072, value=1024, step=64, label="Width") height = gr.Slider(512, 3072, value=1024, step=64, label="Height") with gr.Row(): steps = gr.Slider(1, 30, value=10, step=1, label="Steps") cfg = gr.Slider(0.0, 8.0, value=1.0, step=0.1, label="CFG") with gr.Row(): seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed") randomize_seed = gr.Checkbox(value=True, label="Randomize seed") with gr.Accordion("Custom Checkpoint (HuggingFace Dynamic)", open=False): custom_model_repo = gr.Textbox(label="HF Repo ID", placeholder="e.g., beznogim666/test2", value="") custom_model_file = gr.Textbox(label="Filename", placeholder="e.g., test_rc3.safetensors", value="") with gr.Row(): lora_name = gr.Dropdown( choices=[LORA_NONE, LORA_FILE], value=LORA_FILE, label="LoRA 1", info=LORA_INFO, allow_custom_value=True, scale=4, ) lora_refresh = gr.Button("Refresh", scale=1) lora_strength = gr.Slider(0.0, 10.0, value=0.0, step=0.05, label="LoRA 1 strength") with gr.Accordion("LoRA 2 (HuggingFace Dynamic)", open=False): lora2_repo = gr.Textbox(label="HF Repo ID", placeholder="e.g., beznogim666/test2", value="") lora2_file = gr.Textbox(label="Filename", placeholder="e.g., lora2.safetensors", value="") lora2_strength = gr.Slider(0.0, 10.0, value=0.0, step=0.05, label="LoRA 2 strength") gpu_duration = gr.Slider( DURATION_MIN, DURATION_MAX, value=DURATION_DEFAULT, step=5, label="ZeroGPU duration (s)", info=DURATION_INFO, ) with gr.Accordion("Sampler", open=False): sampler = gr.Dropdown( ["er_sde", "euler", "euler_ancestral", "dpmpp_2m", "dpmpp_sde"], value="er_sde", label="Sampler", ) scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler") run = gr.Button("Generate", variant="primary") with gr.Column(scale=6): output = gr.HTML(label="Result", elem_id="result-image") inputs = [ prompt, negative_prompt, width, height, steps, cfg, seed, randomize_seed, sampler, scheduler, lora_name, lora_strength, lora2_repo, lora2_file, lora2_strength, custom_model_repo, custom_model_file, gpu_duration, ] run.click(generate, inputs, [output, seed]) prompt.submit(generate, inputs, [output, seed]) lora_refresh.click(refresh_loras, None, lora_name) with gr.Tab("Edit Image"): with gr.Row(): with gr.Column(scale=5): edit_input = gr.Image(type="pil", label="Input image") edit_prompt = gr.Textbox(label="Edit prompt", lines=5, placeholder="Describe the edit while preserving identity.") edit_negative_prompt = gr.Textbox(label="Negative prompt", lines=2, value="") with gr.Row(): edit_width = gr.Slider(512, 3072, value=1024, step=64, label="Width") edit_height = gr.Slider(512, 3072, value=1024, step=64, label="Height") with gr.Row(): edit_steps = gr.Slider(1, 30, value=12, step=1, label="Steps") edit_cfg = gr.Slider(0.0, 8.0, value=1.2, step=0.1, label="CFG") edit_denoise = gr.Slider( 0.05, 0.8, value=0.35, step=0.05, label="Edit strength", info="Lower values preserve identity and composition more strongly.", ) with gr.Row(): edit_seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed") edit_randomize_seed = gr.Checkbox(value=True, label="Randomize seed") with gr.Accordion("Custom Checkpoint (HuggingFace Dynamic)", open=False): edit_custom_model_repo = gr.Textbox(label="HF Repo ID", placeholder="e.g., beznogim666/test2", value="") edit_custom_model_file = gr.Textbox(label="Filename", placeholder="e.g., test_rc3.safetensors", value="") with gr.Row(): edit_lora_name = gr.Dropdown( choices=[LORA_NONE, LORA_FILE], value=LORA_FILE, label="LoRA 1", info=LORA_INFO, allow_custom_value=True, scale=4, ) edit_lora_refresh = gr.Button("Refresh", scale=1) edit_lora_strength = gr.Slider(-2.0, 2.0, value=1.0, step=0.05, label="LoRA 1 strength") with gr.Accordion("LoRA 2 (HuggingFace Dynamic)", open=False): edit_lora2_repo = gr.Textbox(label="HF Repo ID", placeholder="e.g., beznogim666/test2", value="") edit_lora2_file = gr.Textbox(label="Filename", placeholder="e.g., lora2.safetensors", value="") edit_lora2_strength = gr.Slider(-2.0, 2.0, value=0.0, step=0.05, label="LoRA 2 strength") edit_gpu_duration = gr.Slider( DURATION_MIN, DURATION_MAX, value=DURATION_DEFAULT, step=5, label="ZeroGPU duration (s)", info=DURATION_INFO, ) with gr.Accordion("Sampler", open=False): edit_sampler = gr.Dropdown( ["er_sde", "euler", "euler_ancestral", "dpmpp_2m", "dpmpp_sde"], value="er_sde", label="Sampler", ) edit_scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler") edit_run = gr.Button("Edit Image", variant="primary") with gr.Column(scale=6): edit_output = gr.HTML(label="Edited image", elem_id="result-image") edit_inputs = [ edit_input, edit_prompt, edit_negative_prompt, edit_width, edit_height, edit_steps, edit_cfg, edit_denoise, edit_seed, edit_randomize_seed, edit_sampler, edit_scheduler, edit_lora_name, edit_lora_strength, edit_lora2_repo, edit_lora2_file, edit_lora2_strength, edit_custom_model_repo, edit_custom_model_file, edit_gpu_duration, ] edit_run.click(edit_image, edit_inputs, [edit_output, edit_seed]) edit_prompt.submit(edit_image, edit_inputs, [edit_output, edit_seed]) edit_lora_refresh.click(refresh_loras, None, edit_lora_name) _ensure_comfyui() if __name__ == "__main__": demo.queue().launch()