Spaces:
Running on Zero
Running on Zero
multimodalart HF Staff
Refit the ZeroGPU duration estimate to measured AOTI timings (was reserving ~2x)
56c0dc9 verified Download app.py from M3st3rJ4k3l/qwen-image-2-1: direct link, hf CLI and curl.
- Browser
- Download file 23 kB
-
https://huggingface.co/spaces/M3st3rJ4k3l/qwen-image-2-1/resolve/main/app.py
- Command line
-
hf download hf://spaces/M3st3rJ4k3l/qwen-image-2-1/app.py
-
curl -L -o app.py https://huggingface.co/spaces/M3st3rJ4k3l/qwen-image-2-1/resolve/main/app.py
23 kB
| """Qwen-Image-2.1 — unified text-to-image generation and image editing on ZeroGPU. | |
| Pipeline for one request: | |
| 1. NCII guard (CPU, sidecar subprocess) — image-input requests have their prompt screened | |
| with ``hfmlsoc/ncii-guard-v02`` before any GPU time is reserved. | |
| 2. Optional prompt enhancement — delegated over the Gradio API to a companion Space | |
| that keeps ``Qwen-Image-2.1-PE-T2I`` and ``Qwen-Image-2.1-PE-I2I`` warm. | |
| 3. Generation — ``QwenImage21Pipeline`` inside ``@spaces.GPU``. | |
| Steps 1 and 2 deliberately run outside the GPU fork so a rejected or enhanced prompt | |
| costs the visitor no ZeroGPU quota. | |
| """ | |
| import os | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import spaces # noqa: E402 — must precede torch / any CUDA-touching import | |
| import json # noqa: E402 | |
| import math # noqa: E402 | |
| import random # noqa: E402 | |
| import tempfile # noqa: E402 | |
| import time # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| import torch # noqa: E402 | |
| from diffusers import QwenImage21Pipeline # noqa: E402 | |
| from PIL import Image # noqa: E402 | |
| import ncii_guard # noqa: E402 | |
| from qwen21_aoti import aoti_load_pipeline # noqa: E402 | |
| MODEL_ID = "Qwen/Qwen-Image-2.1" | |
| AOTI_REPO = os.environ.get("QWEN21_AOTI_REPO", "hugging-apps/qwen-image-2-1-aoti") | |
| GUARD_ID = ncii_guard.GUARD_ID | |
| PE_SPACE_ID = os.environ.get("PE_SPACE_ID", "hugging-apps/qwen-image-2-1-prompt-enhancer") | |
| # The enhancer budgets its ZeroGPU reservation from this cap (~23 tok/s), so the cap is what a | |
| # visitor's quota is charged, not the time actually spent. Measured on the enhancer Space: the | |
| # I2I rewriter runs to the cap whether it is 1024 (≈39s, reservation 56s) or 2048 (≈78s, | |
| # reservation 108s), so doubling it does not stop it being cut off — it only doubles the charge. | |
| # A cut-off answer is salvaged into a usable prompt (minus the ratio hint), and on the rare total | |
| # failure ``prepare`` warns and falls back to the user's own prompt. So keep the cap lean. | |
| PE_MAX_NEW_TOKENS = int(os.environ.get("PE_MAX_NEW_TOKENS", "1024")) | |
| MAX_SEED = 2**31 - 1 | |
| MAX_INPUT_IMAGES = 10 | |
| EXAMPLE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples") | |
| # Shown verbatim when the guard rejects a prompt — deliberately non-explicit. | |
| GUARD_REJECTION_MESSAGE = "prompt invalid based on our classifiers, try again" | |
| # See the model card's threshold sweep: 0.5 is the balanced precision/recall point. | |
| GUARD_THRESHOLD = 0.5 | |
| # The aspect ratios the model card lists, at its 2048-base resolution. | |
| BASE_RESOLUTION = 2048 | |
| ASPECT_RATIOS = { | |
| "1:1": (2048, 2048), | |
| "4:3": (2400, 1792), | |
| "3:4": (1792, 2400), | |
| "3:2": (2528, 1696), | |
| "2:3": (1696, 2528), | |
| "16:9": (2752, 1536), | |
| "9:16": (1536, 2752), | |
| } | |
| RESOLUTION_CHOICES = [("1K — fastest", 1024), ("1.5K", 1536), ("2K — model default", 2048)] | |
| # The step-cost fit below was measured with the AOTI kernels active; falling back to eager | |
| # (QWEN21_AOTI=0, or a failed artifact load) costs roughly a third more per step. | |
| EAGER_STEP_PENALTY = 1.3 | |
| KV_CACHE_BYTES_PER_TOKEN = 32 * 2 * 4096 * 2 | |
| KV_CACHE_BUDGET_GB = 10.0 | |
| def prefix_tokens(n_images: int, resolution: int) -> int: | |
| return n_images * (int(resolution) // 16) ** 2 | |
| def kv_cache_fits(n_images: int, resolution: int) -> bool: | |
| return prefix_tokens(n_images, resolution) * KV_CACHE_BYTES_PER_TOKEN <= KV_CACHE_BUDGET_GB * 1e9 | |
| TRANSPARENCY_INFO = ( | |
| "Describe what to generate, or how to edit the input images (refer to them as " | |
| "<image1>…<image10> in upload order). For a transparent RGBA result, phrase the prompt as: " | |
| "“This is an RGBA image with transparency. … The image has alpha channel and the " | |
| "background is transparent.”" | |
| ) | |
| # -------------------------------------------------------------------------------------- | |
| # Models | |
| # -------------------------------------------------------------------------------------- | |
| print(f"[load] {MODEL_ID} …", flush=True) | |
| pipe = QwenImage21Pipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16) | |
| pipe = pipe.to("cuda") | |
| # The VAE decoder upcasts to fp32 in its upsamplers, so a 2048x2048 decode allocates far | |
| # more than the denoising loop that produced it: it died with an allocator assert | |
| # ("NVML_SUCCESS == r INTERNAL ASSERT FAILED") *after* all 40 steps had completed, and | |
| # left the ZeroGPU worker wedged so every later request reported "No CUDA GPUs are | |
| # available" until the Space was restarted. Tiling the decode keeps peak VAE memory flat. | |
| # The threshold is set above 1536 so the resolutions measured to decode safely in one | |
| # shot still do, and only larger outputs pay the tiling overhead. | |
| pipe.vae.enable_tiling( | |
| tile_sample_min_height=1536, | |
| tile_sample_min_width=1536, | |
| tile_sample_stride_height=1152, | |
| tile_sample_stride_width=1152, | |
| ) | |
| AOTI_LOADED = [] | |
| if os.environ.get("QWEN21_AOTI", "1") != "0": | |
| try: | |
| AOTI_LOADED, _aoti_config = aoti_load_pipeline(pipe, AOTI_REPO, token=os.environ.get("HF_TOKEN")) | |
| print(f"[load] aoti kernels from {AOTI_REPO}: {AOTI_LOADED} (torch {_aoti_config['torch']})", flush=True) | |
| except Exception as exc: # noqa: BLE001 | |
| print(f"[load] aoti unavailable, running eager: {exc!r}", flush=True) | |
| AOTI_BLOCKS = "QwenImage21DecodeBlock" in AOTI_LOADED | |
| print("[load] pipeline ready", flush=True) | |
| print(f"[load] {GUARD_ID} sidecar …", flush=True) | |
| ncii_guard.start() | |
| print("[load] guard ready", flush=True) | |
| def guard_score(text: str) -> float: | |
| """Probability that ``text`` is a request for non-consensual intimate imagery.""" | |
| return ncii_guard.score(text or "") | |
| def check_guard(prompt: str) -> None: | |
| """Raise the non-explicit rejection error when the guard flags ``prompt``.""" | |
| score = guard_score(prompt) | |
| print(f"[guard] score={score:.3f} threshold={GUARD_THRESHOLD}", flush=True) | |
| if score >= GUARD_THRESHOLD: | |
| raise gr.Error(GUARD_REJECTION_MESSAGE) | |
| # -------------------------------------------------------------------------------------- | |
| # Prompt enhancement (delegated to the companion Space) | |
| # -------------------------------------------------------------------------------------- | |
| _pe_client = None | |
| def get_pe_client(): | |
| """Lazily connect to the prompt-enhancement Space (its models are already warm).""" | |
| global _pe_client | |
| if _pe_client is None: | |
| from gradio_client import Client | |
| token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") | |
| # gradio_client 2.x renamed ``hf_token`` to ``token``; accept either client version. | |
| try: | |
| _pe_client = Client( | |
| PE_SPACE_ID, token=token, httpx_kwargs={"timeout": 900}, verbose=False | |
| ) | |
| except TypeError: | |
| _pe_client = Client( | |
| PE_SPACE_ID, hf_token=token, httpx_kwargs={"timeout": 900}, verbose=False | |
| ) | |
| return _pe_client | |
| def enhance_prompt(prompt: str, image_paths: list[str]) -> tuple[str, str]: | |
| """Rewrite ``prompt`` with the official PE models. Returns (prompt, wh_ratio).""" | |
| from gradio_client import handle_file | |
| client = get_pe_client() | |
| started = time.perf_counter() | |
| result = client.predict( | |
| prompt=prompt, | |
| image_paths=[handle_file(p) for p in image_paths], | |
| max_new_tokens=PE_MAX_NEW_TOKENS, | |
| enable_thinking=False, | |
| seed=0, | |
| randomize_seed=True, | |
| api_name="/enhance", | |
| ) | |
| rewritten, wh_ratio = str(result[0]).strip(), str(result[1]).strip() | |
| print(f"[enhance] {time.perf_counter() - started:.1f}s -> ratio={wh_ratio!r}", flush=True) | |
| return (rewritten or prompt), wh_ratio | |
| # -------------------------------------------------------------------------------------- | |
| # Helpers | |
| # -------------------------------------------------------------------------------------- | |
| def normalize_gallery(gallery) -> list[str]: | |
| """Reduce a ``gr.Gallery`` value to a flat list of local file paths.""" | |
| paths: list[str] = [] | |
| for item in gallery or []: | |
| if isinstance(item, (tuple, list)): | |
| item = item[0] | |
| if isinstance(item, dict): | |
| item = item.get("path") or item.get("image") or item.get("name") | |
| if isinstance(item, dict): | |
| item = item.get("path") | |
| if isinstance(item, Image.Image): | |
| tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) | |
| item.save(tmp.name) | |
| item = tmp.name | |
| if isinstance(item, str) and item: | |
| paths.append(item) | |
| return paths | |
| def resolve_size(resolution: int, aspect_ratio: str) -> tuple[int | None, int | None]: | |
| """Map a resolution tier + aspect ratio onto the model card's width/height pairs.""" | |
| if not aspect_ratio or aspect_ratio == "Auto" or aspect_ratio not in ASPECT_RATIOS: | |
| return None, None | |
| base_w, base_h = ASPECT_RATIOS[aspect_ratio] | |
| scale = int(resolution) / BASE_RESOLUTION | |
| round32 = lambda v: max(32, int(round(v * scale / 32)) * 32) # noqa: E731 | |
| return round32(base_w), round32(base_h) | |
| def estimate_duration( | |
| prompt="", | |
| input_images=None, | |
| negative_prompt="", | |
| true_cfg_scale=1.0, | |
| num_inference_steps=40, | |
| seed=0, | |
| resolution=1024, | |
| aspect_ratio="Auto", | |
| *args, | |
| **kwargs, | |
| ): | |
| """Budget GPU seconds from times measured on this Space's own hardware. | |
| Refitted against ``[generate]`` log timings with the AOTI kernels live, all at 40 steps: | |
| 1024px = 16.3s (18.3s worst), 1536px = 40.1s, 2048px = 82.9s, and 1024px with one and two | |
| condition images = 21.7s and 23.1s. Step cost scales as ``latent_tokens ** 1.363``; the | |
| cached prefix of a condition image contributes about a fifth of its tokens to each step on | |
| top of a couple of seconds of one-off prefill. | |
| True CFG doubles the step (it denoises the negative branch too) but the pipeline only turns | |
| it on when a negative prompt is actually set — see ``_diffuse`` — so this mirrors that | |
| condition rather than keying off the scale alone. Doing otherwise doubled the reservation of | |
| every default request, which is charged to the visitor's ZeroGPU quota whether or not it is | |
| spent. 1.25 is the only slack, which lands ~10-30% above the measured call. | |
| """ | |
| try: | |
| steps = int(num_inference_steps) | |
| res = int(resolution) | |
| cfg = float(true_cfg_scale) | |
| except (TypeError, ValueError): | |
| steps, res, cfg = 40, 1024, 1.0 | |
| n_images = len(input_images or []) | |
| tokens = (res / 16.0) ** 2 | |
| prefix = prefix_tokens(n_images, res) | |
| cached = kv_cache_fits(n_images, res) | |
| tokens += 0.2 * prefix if cached else prefix | |
| per_step = 3.4e-6 * tokens**1.363 | |
| if cfg > 1.0 and (negative_prompt or "").strip(): | |
| per_step *= 2.0 | |
| if not (AOTI_BLOCKS and cached): | |
| per_step *= EAGER_STEP_PENALTY | |
| fixed = 4.0 + 2.0 * n_images + 1.5e-6 * res * res | |
| return int(min(340, math.ceil((fixed + steps * per_step) * 1.25))) | |
| # -------------------------------------------------------------------------------------- | |
| # Inference | |
| # -------------------------------------------------------------------------------------- | |
| def _diffuse( | |
| prompt: str, | |
| image_paths: list, | |
| negative_prompt: str, | |
| true_cfg_scale: float, | |
| num_inference_steps: int, | |
| seed: int, | |
| resolution: int, | |
| aspect_ratio: str, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| """Run the diffusion pipeline. Inputs are already validated and screened.""" | |
| condition_images = [Image.open(p) for p in image_paths] or None | |
| width, height = resolve_size(resolution, aspect_ratio) | |
| negative_prompt = (negative_prompt or "").strip() | |
| use_kv_cache = kv_cache_fits(len(image_paths), int(resolution)) | |
| call_kwargs = {"use_kv_cache": use_kv_cache} | |
| if negative_prompt and float(true_cfg_scale) > 1.0: | |
| call_kwargs["negative_prompt"] = negative_prompt | |
| call_kwargs["true_cfg_scale"] = float(true_cfg_scale) | |
| started = time.perf_counter() | |
| image = pipe( | |
| prompt=prompt, | |
| image=condition_images, | |
| height=height, | |
| width=width, | |
| output_resolution=int(resolution), | |
| num_inference_steps=int(num_inference_steps), | |
| generator=torch.Generator(device="cuda").manual_seed(int(seed) % (MAX_SEED + 1)), | |
| **call_kwargs, | |
| ).images[0] | |
| print( | |
| f"[generate] images={len(image_paths)} steps={num_inference_steps} " | |
| f"res={resolution} ratio={aspect_ratio} size={image.size} kv_cache={use_kv_cache} " | |
| f"aoti={AOTI_LOADED} " | |
| f"elapsed={time.perf_counter() - started:.1f}s", | |
| flush=True, | |
| ) | |
| return image | |
| def generate_image( | |
| prompt: str = "", | |
| input_images=None, | |
| negative_prompt: str = "", | |
| true_cfg_scale: float = 1.0, | |
| num_inference_steps: int = 40, | |
| seed: int = 0, | |
| resolution: int = 1024, | |
| aspect_ratio: str = "Auto", | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| """Generate or edit an image with Qwen-Image-2.1. | |
| With no input images this is text-to-image; with one to ten input images it is | |
| image editing, and the prompt may refer to them as <image1>…<image10>. | |
| Args: | |
| prompt: What to generate, or how to edit the inputs. | |
| input_images: Up to 10 condition images. Leave empty for text-to-image. | |
| negative_prompt: What to avoid. Only applied when true_cfg_scale > 1. | |
| true_cfg_scale: Classifier-free guidance scale; 1.0 disables it. | |
| num_inference_steps: Number of flow-matching steps. | |
| seed: Random seed. | |
| resolution: Target side length in pixels (1024, 1536 or 2048). | |
| aspect_ratio: One of the model's supported ratios, or "Auto". | |
| Returns: | |
| The generated image, as RGBA (the model natively supports transparency). | |
| """ | |
| # Validation and screening deliberately happen here, in the main process: a rejected | |
| # prompt must never reserve a GPU slot, and gr.Error raised inside the ZeroGPU worker | |
| # reaches the browser as a bare "RuntimeError" instead of its message. | |
| prompt = (prompt or "").strip() | |
| if not prompt: | |
| raise gr.Error("Please enter a prompt.") | |
| image_paths = normalize_gallery(input_images) | |
| if len(image_paths) > MAX_INPUT_IMAGES: | |
| raise gr.Error(f"Up to {MAX_INPUT_IMAGES} input images are supported.") | |
| # `prepare` already screened the UI path; this keeps the guard in force for callers | |
| # that hit /generate directly. | |
| if image_paths: | |
| check_guard(prompt) | |
| # Gradio only binds a live gr.Progress to the function it calls, which is this one -- | |
| # the copy sitting in _diffuse's signature is an inert import-time default, so without | |
| # forwarding this the UI showed no step counter at all during a 25-120s generation. | |
| return _diffuse( | |
| prompt, | |
| image_paths, | |
| negative_prompt, | |
| true_cfg_scale, | |
| num_inference_steps, | |
| seed, | |
| resolution, | |
| aspect_ratio, | |
| progress, | |
| ) | |
| def prepare(prompt, input_images, enhance, seed, randomize_seed, aspect_ratio): | |
| """Screen the prompt, optionally rewrite it, and resolve the seed — all off-GPU.""" | |
| prompt = (prompt or "").strip() | |
| if not prompt: | |
| raise gr.Error("Please enter a prompt.") | |
| image_paths = normalize_gallery(input_images) | |
| if len(image_paths) > MAX_INPUT_IMAGES: | |
| raise gr.Error(f"Up to {MAX_INPUT_IMAGES} input images are supported.") | |
| # Guard image-input requests before any GPU time is reserved. | |
| if image_paths: | |
| score = guard_score(prompt) | |
| print(f"[guard] score={score:.3f} threshold={GUARD_THRESHOLD}", flush=True) | |
| if score >= GUARD_THRESHOLD: | |
| raise gr.Error(GUARD_REJECTION_MESSAGE) | |
| resolved_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed) | |
| final_prompt, rewritten_display = prompt, "" | |
| if enhance: | |
| try: | |
| final_prompt, wh_ratio = enhance_prompt(prompt, image_paths) | |
| rewritten_display = final_prompt | |
| # Honour the rewriter's ratio only when the user left the choice open and | |
| # there is no input image whose aspect ratio the pipeline should follow. | |
| if wh_ratio and aspect_ratio == "Auto" and not image_paths: | |
| if wh_ratio in ASPECT_RATIOS: | |
| aspect_ratio = wh_ratio | |
| except Exception as exc: # noqa: BLE001 — never let PE take the whole request down | |
| print(f"[enhance] failed: {exc!r}", flush=True) | |
| gr.Warning(f"Prompt enhancement unavailable, using the original prompt ({exc}).") | |
| return final_prompt, rewritten_display, resolved_seed, aspect_ratio | |
| # -------------------------------------------------------------------------------------- | |
| # Examples (Qwen's own demo cases, bundled under examples/) | |
| # -------------------------------------------------------------------------------------- | |
| with open(os.path.join(EXAMPLE_DIR, "cases.json"), encoding="utf-8") as fh: | |
| DEMO_CASES = json.load(fh) | |
| T2I_EXAMPLES = [[c["prompt"]] for c in DEMO_CASES if not c["inputs"]] | |
| T2I_LABELS = [c["title_en"] for c in DEMO_CASES if not c["inputs"]] | |
| EDIT_EXAMPLES = [ | |
| [c["prompt"], [os.path.join(EXAMPLE_DIR, name) for name in c["inputs"]]] | |
| for c in DEMO_CASES | |
| if c["inputs"] | |
| ] | |
| EDIT_LABELS = [c["title_en"] for c in DEMO_CASES if c["inputs"]] | |
| # -------------------------------------------------------------------------------------- | |
| # UI | |
| # -------------------------------------------------------------------------------------- | |
| css = """ | |
| #col-container { margin: 0 auto; max-width: 1100px; } | |
| #logo, #logo > * { padding: 0 !important; margin: 0 !important; min-height: 0 !important; border: none !important; background: transparent !important; } | |
| #logo img { display: block; margin: 0 auto !important; } | |
| /* Citrus leaves some text colours unset in dark mode, which renders dark-on-dark. */ | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| # `theme` / `css` are passed to launch(), not to the Blocks constructor: Gradio 6 moved them | |
| # and warns ("The parameters have been moved from the Blocks constructor to the launch() | |
| # method in Gradio 6.0: theme, css") if they are set here. | |
| with gr.Blocks(title="Qwen-Image-2.1") as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.HTML( | |
| '<img src="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-Image/image2.1/logo.png"' | |
| ' alt="Qwen-Image logo" width="380" style="display:block;margin:0 auto;">', | |
| elem_id="logo", | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| input_images = gr.Gallery( | |
| label="Input images (editing, up to 10)", | |
| type="filepath", | |
| interactive=True, | |
| columns=5, | |
| height=300, | |
| show_label=True, | |
| ) | |
| with gr.Column(): | |
| result = gr.Image( | |
| label="Result", | |
| type="pil", | |
| image_mode="RGBA", | |
| format="png", | |
| height=300, | |
| show_label=True, | |
| ) | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| info=TRANSPARENCY_INFO, | |
| lines=5, | |
| max_lines=24, | |
| placeholder="Describe what you want to generate or edit…", | |
| ) | |
| with gr.Row(): | |
| enhance = gr.Checkbox(label="Enhance prompt", value=True, scale=1) | |
| generate_button = gr.Button("Generate image", variant="primary", scale=2) | |
| rewritten_prompt_output = gr.Textbox( | |
| label="Rewritten prompt", | |
| lines=4, | |
| max_lines=20, | |
| interactive=False, | |
| placeholder="With prompt enhancement on, the rewritten prompt appears here.", | |
| ) | |
| with gr.Accordion("Advanced settings", open=False): | |
| with gr.Row(): | |
| resolution = gr.Dropdown( | |
| label="Resolution", choices=RESOLUTION_CHOICES, value=1024 | |
| ) | |
| aspect_ratio = gr.Dropdown( | |
| label="Aspect ratio", | |
| choices=["Auto"] + list(ASPECT_RATIOS), | |
| value="Auto", | |
| info="Auto follows the input image, or squares up for text-to-image.", | |
| ) | |
| num_inference_steps = gr.Slider( | |
| label="Inference steps", minimum=8, maximum=60, step=1, value=40 | |
| ) | |
| with gr.Row(): | |
| seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0) | |
| randomize_seed = gr.Checkbox(label="Randomize seed", value=True) | |
| negative_prompt = gr.Textbox( | |
| label="Negative prompt", | |
| value="", | |
| lines=2, | |
| info="Only applied when guidance is above 1.0.", | |
| ) | |
| true_cfg_scale = gr.Slider( | |
| label="True CFG scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0 | |
| ) | |
| # A hidden Textbox rather than gr.State: it carries the enhanced prompt from | |
| # `prepare` into `generate_image`, and being a real component it is also what | |
| # makes `prompt` a first-class parameter of the /generate API endpoint. | |
| final_prompt_box = gr.Textbox(visible=False, label="Prompt") | |
| gr.Markdown("### Examples") | |
| gr.Markdown("**Text to image**") | |
| gr.Examples( | |
| examples=T2I_EXAMPLES, | |
| example_labels=T2I_LABELS, | |
| inputs=[prompt], | |
| fn=generate_image, | |
| outputs=[result], | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| label="Text-to-image cases", | |
| ) | |
| gr.Markdown("**Image editing** — the example loads its reference images too") | |
| gr.Examples( | |
| examples=EDIT_EXAMPLES, | |
| example_labels=EDIT_LABELS, | |
| inputs=[prompt, input_images], | |
| fn=generate_image, | |
| outputs=[result], | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| label="Image-editing cases", | |
| ) | |
| generate_button.click( | |
| fn=prepare, | |
| inputs=[prompt, input_images, enhance, seed, randomize_seed, aspect_ratio], | |
| outputs=[final_prompt_box, rewritten_prompt_output, seed, aspect_ratio], | |
| ).then( | |
| fn=generate_image, | |
| inputs=[ | |
| final_prompt_box, | |
| input_images, | |
| negative_prompt, | |
| true_cfg_scale, | |
| num_inference_steps, | |
| seed, | |
| resolution, | |
| aspect_ratio, | |
| ], | |
| outputs=[result], | |
| api_name="generate", | |
| ) | |
| demo.queue(default_concurrency_limit=1, max_size=20) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| mcp_server=True, | |
| theme=gr.themes.Citrus(), | |
| css=css, | |
| allowed_paths=[EXAMPLE_DIR], | |
| ) | |