Spaces:
Running on Zero
Running on Zero
| import os | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import spaces # MUST come before any CUDA-touching import | |
| import torch | |
| import gradio as gr | |
| import random | |
| import numpy as np | |
| from diffusers import DiffusionPipeline, AutoencoderTiny, AutoencoderKL | |
| from huggingface_hub import hf_hub_download | |
| dtype = torch.bfloat16 | |
| device = "cuda" | |
| # Tiny VAE for fast preview, good VAE for final output (same pattern as official FLUX.1-dev space) | |
| taef1 = AutoencoderTiny.from_pretrained("madebyollin/taef1", torch_dtype=dtype).to(device) | |
| good_vae = AutoencoderKL.from_pretrained( | |
| "black-forest-labs/FLUX.1-dev", subfolder="vae", torch_dtype=dtype | |
| ).to(device) | |
| # Load base FLUX.1-dev with tiny VAE | |
| BASE_MODEL = "black-forest-labs/FLUX.1-dev" | |
| CMO_LORA = "Bruece/FLUX.1-dev-CMO" | |
| LORA_ALPHA = 128 | |
| LORA_R = 64 | |
| LORA_SCALE = LORA_ALPHA / LORA_R # = 2.0 | |
| pipe = DiffusionPipeline.from_pretrained( | |
| BASE_MODEL, torch_dtype=dtype, vae=taef1 | |
| ).to(device) | |
| # Manually load and merge the CMO LoRA adapter weights. | |
| # Use safetensors.safe_open (numpy backend, no torch) to avoid ZeroGPU's | |
| # torch patching which fails at module scope (no CUDA available yet). | |
| from safetensors import safe_open | |
| _lora_path = hf_hub_download(CMO_LORA, "adapter_model.safetensors", repo_type="model") | |
| # Load all LoRA weights as numpy arrays first, then merge into the transformer | |
| _lora_a_pairs = {} # module_path -> A weight (numpy) | |
| _lora_b_pairs = {} # module_path -> B weight (numpy) | |
| with safe_open(_lora_path, framework="pt", device="cpu") as _f: | |
| for _key in _f.keys(): | |
| if not _key.startswith("base_model.model."): | |
| continue | |
| _rest = _key[len("base_model.model."):] | |
| if _rest.endswith(".lora_A.weight"): | |
| _module_path = _rest[: -len(".lora_A.weight")] | |
| _lora_a_pairs[_module_path] = _f.get_tensor(_key) | |
| elif _rest.endswith(".lora_B.weight"): | |
| _module_path = _rest[: -len(".lora_B.weight")] | |
| _lora_b_pairs[_module_path] = _f.get_tensor(_key) | |
| # Merge LoRA weights into the transformer: w_new = w_orig + scale * (B @ A) | |
| _merge_count = 0 | |
| for _module_path, _a_tensor in _lora_a_pairs.items(): | |
| if _module_path not in _lora_b_pairs: | |
| continue | |
| _b_tensor = _lora_b_pairs[_module_path] | |
| # Navigate to the module in the transformer | |
| _module = pipe.transformer | |
| for _part in _module_path.split("."): | |
| _module = getattr(_module, _part) | |
| # Merge: w_orig + scale * (B @ A) | |
| _delta = (_b_tensor.float() @ _a_tensor.float()) * LORA_SCALE | |
| _module.weight.data.add_(_delta.to(_module.weight.data.dtype)) | |
| _merge_count += 1 | |
| print(f"CMO LoRA: merged {_merge_count} adapter pairs into FLUX.1-dev transformer") | |
| del _lora_a_pairs, _lora_b_pairs | |
| torch.cuda.empty_cache() | |
| MAX_SEED = np.iinfo(np.int32).max | |
| MAX_IMAGE_SIZE = 2048 | |
| def _estimate(prompt, num_inference_steps, *args, **kwargs): | |
| return min(180, 30 + int(num_inference_steps * 4)) | |
| def generate( | |
| prompt: str, | |
| seed: int = 42, | |
| randomize_seed: bool = True, | |
| width: int = 1024, | |
| height: int = 1024, | |
| guidance_scale: float = 4.5, | |
| num_inference_steps: int = 40, | |
| progress: gr.Progress = gr.Progress(track_tqdm=True), | |
| ): | |
| """Generate an image from a text prompt using FLUX.1-dev fine-tuned with CMO. | |
| CMO (Correlation-Weighted Multi-Reward Optimization) improves compositional | |
| text-to-image generation by adaptively weighting conflicting concept rewards | |
| (object existence, attributes, numeracy, size, spatial relations). | |
| Args: | |
| prompt: Text description of the image to generate. | |
| seed: RNG seed for reproducibility. | |
| randomize_seed: If True, pick a random seed each run. | |
| width: Output image width in pixels. | |
| height: Output image height in pixels. | |
| guidance_scale: Classifier-free guidance scale. | |
| num_inference_steps: Number of denoising steps. | |
| """ | |
| if randomize_seed: | |
| seed = random.randint(0, MAX_SEED) | |
| seed = int(seed) | |
| generator = torch.Generator().manual_seed(seed) | |
| image = pipe( | |
| prompt=prompt, | |
| height=height, | |
| width=width, | |
| num_inference_steps=num_inference_steps, | |
| guidance_scale=guidance_scale, | |
| generator=generator, | |
| ).images[0] | |
| return image, seed | |
| CSS = """ | |
| #col-container { max-width: 1100px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| EXAMPLES = [ | |
| ["A red apple is on the left of the yellow banana"], | |
| ["Two Tyrannosaurus rexes engaged in a boxing match"], | |
| ["a photo of a black kite and a green bear"], | |
| ["A brown cow wearing yellow sunglasses in a pastel chalk drawing"], | |
| ["a cat holding a sign that says hello world"], | |
| ["The green plant was on top of the white nightstand"], | |
| ] | |
| with gr.Blocks() as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.Markdown( | |
| """# FLUX.1-dev CMO — Compositional Text-to-Image | |
| FLUX.1-dev fine-tuned with **Correlation-Weighted Multi-Reward Optimization (CMO)** for improved compositional generation. | |
| [[Paper](https://huggingface.co/papers/2603.18528)] [[Code](https://github.com/TheDarkKnight-21th/CMO)] [[Model](https://huggingface.co/Bruece/FLUX.1-dev-CMO)] | |
| """ | |
| ) | |
| with gr.Row(): | |
| prompt = gr.Textbox( | |
| label="Prompt", | |
| show_label=False, | |
| max_lines=1, | |
| placeholder="Enter your prompt (e.g. 'A red apple is on the left of the yellow banana')", | |
| container=False, | |
| scale=4, | |
| ) | |
| run_button = gr.Button("Run", variant="primary", scale=1) | |
| result = gr.Image(label="Result", show_label=False) | |
| with gr.Accordion("Advanced settings", open=False): | |
| seed = gr.Slider( | |
| label="Seed", | |
| minimum=0, | |
| maximum=MAX_SEED, | |
| step=1, | |
| value=42, | |
| ) | |
| randomize_seed = gr.Checkbox(label="Randomize seed", value=True) | |
| with gr.Row(): | |
| width = gr.Slider( | |
| label="Width", | |
| minimum=256, | |
| maximum=MAX_IMAGE_SIZE, | |
| step=32, | |
| value=1024, | |
| ) | |
| height = gr.Slider( | |
| label="Height", | |
| minimum=256, | |
| maximum=MAX_IMAGE_SIZE, | |
| step=32, | |
| value=1024, | |
| ) | |
| with gr.Row(): | |
| guidance_scale = gr.Slider( | |
| label="Guidance Scale", | |
| minimum=1, | |
| maximum=15, | |
| step=0.1, | |
| value=4.5, | |
| ) | |
| num_inference_steps = gr.Slider( | |
| label="Number of inference steps", | |
| minimum=1, | |
| maximum=50, | |
| step=1, | |
| value=40, | |
| ) | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| fn=generate, | |
| inputs=[prompt], | |
| outputs=[result, seed], | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| ) | |
| gr.on( | |
| triggers=[run_button.click, prompt.submit], | |
| fn=generate, | |
| inputs=[ | |
| prompt, | |
| seed, | |
| randomize_seed, | |
| width, | |
| height, | |
| guidance_scale, | |
| num_inference_steps, | |
| ], | |
| outputs=[result, seed], | |
| api_name="generate", | |
| ) | |
| demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS) |