# FLUX.2 Klein 9B — reference-image editing with runtime LoRA. # # This is the model that answers both open requests at once. Qwen-Image-Edit # holds identity but renders glossy; Z-Image looks photographic but its editing # pipeline needs siglip weights Tongyi has not released; neither supports LoRA # on our stack. Klein is tagged image-to-image, takes one or more reference # images, and Flux2KleinPipeline carries load_lora_weights — so one checkpoint # covers generation, editing and per-character LoRA. # # Started on the 4B because it is Apache-2.0 and ungated. Its output was not # convincing enough, so this runs the 9B — see the note at MODEL below. # # `import spaces` first and stays first — it patches torch.cuda.*, and once CUDA # is initialised in the main process it is too late to intercept. import spaces import base64 import random import traceback from io import BytesIO import gradio as gr import numpy as np import torch from diffusers import Flux2KleinPipeline # 9B, not the 4B this started on — the 4B's output was not convincing enough. # It is gated behind Black Forest Labs' flux-non-commercial-license, so the # account running this Space must accept the terms on the model page first, and # the Space needs an HF_TOKEN secret to authenticate the download. Without both, # the build dies on a 403 fetching model_index.json. MODEL = "black-forest-labs/FLUX.2-klein-9B" dtype = torch.bfloat16 pipe = Flux2KleinPipeline.from_pretrained(MODEL, torch_dtype=dtype) # Eagerly at module scope: ZeroGPU intercepts this, packs the weights to disk # and streams them into VRAM on the first decorated call. Loading lazily inside # the decorator makes every caller pay the cost instead of nobody. pipe = pipe.to("cuda") MAX_SEED = np.iinfo(np.int32).max STEPS = 4 # Klein is distilled; the reference Space defaults to 4 GUIDANCE = 2.5 # Adapters are cached by name and never re-downloaded. Loading a LoRA per # request would re-fetch tens of megabytes every call, and the Space process # survives between calls, so the cache is free. Adapters also STACK if you do # not manage them — set_adapters replaces the active set, and disable_lora # turns them all off, which is what an unadorned request needs. LOADED = set() def _apply_loras(specs): """specs is a list of {repo, weight_name, scale}. Empty disables all.""" if not specs: pipe.disable_lora() return [] names, scales = [], [] for i, spec in enumerate(specs): repo = spec.get("repo") if not repo: continue weight_name = spec.get("weight_name") or None name = spec.get("adapter_name") or f"a{abs(hash((repo, weight_name))) % 10**8}" if name not in LOADED: pipe.load_lora_weights(repo, weight_name=weight_name, adapter_name=name) LOADED.add(name) names.append(name) scales.append(float(spec.get("scale", 1.0))) if not names: pipe.disable_lora() return [] pipe.set_adapters(names, adapter_weights=scales) return names def _duration(*a, **k): # ZeroGPU bills the duration REQUESTED but ABORTS anything that overruns, # so one flat number either wastes quota on small images or kills big ones. w = int(k.get("width") or 1024) h = int(k.get("height") or 1024) return min(120, max(30, round((w * h) / 40000))) @spaces.GPU(duration=_duration) def infer(image_1, image_2, prompt, seed=0, randomize_seed=True, guidance_scale=GUIDANCE, num_inference_steps=STEPS, width=1024, height=1024, loras=None, progress=gr.Progress(track_tqdm=True)): """Edit one or two reference images according to a prompt.""" if randomize_seed: seed = random.randint(0, MAX_SEED) _apply_loras(loras or []) images = [im for im in (image_1, image_2) if im is not None] if not images: raise gr.Error("At least one reference image is required.") out = pipe( image=images if len(images) > 1 else images[0], prompt=prompt, width=int(width), height=int(height), num_inference_steps=int(num_inference_steps), guidance_scale=float(guidance_scale), generator=torch.Generator("cuda").manual_seed(int(seed)), ).images[0] return out, seed with gr.Blocks(delete_cache=(3600, 86400)) as demo: with gr.Column(): # No public interface. This is a backend for one application, and a # landing page only attracts traffic that spends the ZeroGPU quota. The # controls below still exist, hidden, because hiding them removes the # interface without removing the event registration that publishes the # API — delete the components and the endpoint goes with them. gr.HTML( """