#!/usr/bin/env python3 """Gradio demo for Robust-U1 on Hugging Face Spaces.""" from __future__ import annotations import os import random import subprocess import sys import threading from pathlib import Path from typing import Any, Optional import gradio as gr import numpy as np import torch from huggingface_hub import snapshot_download from PIL import Image REPO_ROOT = Path(__file__).resolve().parent if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) is_spaces = os.getenv("SPACE_ID") is not None spaces_available = False GPU = None if is_spaces: try: from spaces import GPU spaces_available = True except ImportError: pass def gpu_decorator(func): if spaces_available and GPU is not None: return GPU(duration=240)(func) return func project_dir = os.path.dirname(os.path.abspath(__file__)) if not is_spaces: temp_dir = os.path.join(project_dir, ".gradio_temp") os.makedirs(temp_dir, exist_ok=True) os.environ["GRADIO_TEMP_DIR"] = temp_dir MODEL_PATH = os.getenv("MODEL_PATH", "Jiaqi-hkust/Robust-U1") MAX_MEM_PER_GPU = os.environ.get("MAX_MEM_PER_GPU", "80GiB") OFFLOAD_DIR = Path(os.environ.get("ROBUST_U1_OFFLOAD_DIR", "/tmp/robust_u1_offload")) MODEL_ALLOW_PATTERNS = ( "*.json", "*.safetensors", "*.bin", "*.txt", "*.model", "*.py", "*.md", ) DEFAULT_PROMPT = ( "Which direction is the vehicle directly in front of us traveling?\n" "Options:\n" "A. Straight\n" "B. Left\n" "C. Right" ) HIDDEN_PROMPT_SUFFIX = ( "Please restore this corrupted image to its clean version.\n" "Based on what you observe in the restored image, please select the correct answer from the options above." ) APP_CSS = """ :root { --page-max-width: 100vw; --ink-strong: #102a43; --ink-soft: #486581; --surface-primary: rgba(255, 255, 255, 0.92); --surface-secondary: rgba(244, 247, 251, 0.88); --surface-border: rgba(148, 163, 184, 0.24); --surface-shadow: 0 24px 60px rgba(15, 23, 42, 0.08); --accent-start: #0f766e; --accent-end: #0b5ed7; } body { background: radial-gradient(circle at top left, rgba(15, 118, 110, 0.16), transparent 34%), radial-gradient(circle at top right, rgba(14, 116, 144, 0.14), transparent 30%), linear-gradient(180deg, #eef4f7 0%, #f8fbfd 52%, #eef3f8 100%); } body, .gradio-container, input, textarea, button { font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif !important; } .gradio-container { width: 100% !important; max-width: none !important; padding: 24px 12px 40px !important; } .app-shell { gap: 20px; } .hero-card { position: relative; overflow: hidden; padding: 26px 34px 24px; border-radius: 28px; background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(232, 243, 248, 0.96) 100%); border: 1px solid rgba(148, 163, 184, 0.2); box-shadow: var(--surface-shadow); } .hero-card::after { content: ""; position: absolute; inset: auto -80px -110px auto; width: 260px; height: 260px; border-radius: 999px; background: radial-gradient(circle, rgba(11, 94, 215, 0.15), transparent 68%); } .hero-title { margin: 0; font-size: clamp(2.1rem, 4vw, 3.4rem); line-height: 1.05; color: var(--ink-strong); } .hero-subtitle { max-width: 760px; margin: 12px 0 0; font-size: 0.98rem; line-height: 1.6; color: #52606d; } .panel { border: 1px solid var(--surface-border); border-radius: 24px; background: var(--surface-primary); box-shadow: var(--surface-shadow); padding: 20px; backdrop-filter: blur(12px); } .panel-header { margin-bottom: 14px; } .eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: #486581; } .panel-title { margin-top: 4px; font-size: 1.22rem; font-weight: 600; color: var(--ink-strong); } .control-bar { margin-top: 16px; } .toolbar { align-items: center; gap: 12px; } .accordion-shell { border-radius: 24px !important; border: 1px solid var(--surface-border) !important; background: var(--surface-primary) !important; box-shadow: var(--surface-shadow); } .accordion-shell > .label-wrap { padding-top: 4px; padding-bottom: 4px; } #run-button { min-height: 52px; border: none !important; background: linear-gradient(135deg, var(--accent-start) 0%, var(--accent-end) 100%) !important; box-shadow: 0 18px 32px rgba(11, 94, 215, 0.22); } #run-button:hover { filter: brightness(1.03); } #secondary-button { min-height: 52px; border: 1px solid rgba(148, 163, 184, 0.24) !important; background: rgba(248, 250, 252, 0.9) !important; color: var(--ink-strong) !important; } .input-image, .output-image { width: 100% !important; } .input-image img, .output-image img { border-radius: 18px; } .reasoning-box textarea { line-height: 1.55; } .footer-note { margin-top: 6px; font-size: 0.95rem; line-height: 1.65; color: #52606d; } @media (max-width: 900px) { .hero-card { padding: 22px 22px 20px; } .panel { padding: 16px; } .toolbar { flex-direction: column; align-items: stretch; } } """ _runtime_lock = threading.Lock() _runtime_cache: dict[str, Any] = { "inferencer": None, "pil_img2rgb": None, } def _build_header_html() -> str: return """

Robust-U1

Robust-U1: Can MLLMs Self-Recover Corrupted Visual Content for Robust Understanding?

""" def _ensure_flash_attn_available() -> None: try: import flash_attn # noqa: F401 except ModuleNotFoundError: env = os.environ.copy() env.setdefault("FLASH_ATTENTION_SKIP_CUDA_BUILD", "TRUE") subprocess.run( [sys.executable, "-m", "pip", "install", "flash-attn", "--no-build-isolation"], check=True, env=env, ) def _resolve_checkpoint(model_path: Path) -> Path: for name in ("model_bf16.safetensors", "model.safetensors", "pytorch_model.bin"): candidate = model_path / name if candidate.is_file(): return candidate raise FileNotFoundError(f"No checkpoint file found under {model_path}") def _resolve_model_path() -> Path: candidate = Path(MODEL_PATH).expanduser() if candidate.exists(): return candidate.resolve() token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN") return Path( snapshot_download( repo_id=MODEL_PATH, repo_type="model", token=token, allow_patterns=list(MODEL_ALLOW_PATTERNS), ) ) def set_seed(seed: int) -> None: if seed <= 0: return random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False def _build_device_map(model: Any) -> tuple[dict[str, Any], str]: from accelerate import infer_auto_device_map gpu_count = torch.cuda.device_count() if gpu_count < 1: raise RuntimeError("Robust-U1 inference requires a GPU-enabled Hugging Face Space.") device_map = infer_auto_device_map( model, max_memory={i: MAX_MEM_PER_GPU for i in range(gpu_count)}, no_split_module_classes=["Bagel", "Qwen2MoTDecoderLayer"], ) same_device_modules = [ "language_model.model.embed_tokens", "time_embedder", "latent_pos_embed", "vae2llm", "llm2vae", "connector", "vit_pos_embed", ] if gpu_count == 1: first_device = device_map.get(same_device_modules[0], "cuda:0") for module_name in same_device_modules: device_map[module_name] = first_device if module_name in device_map else "cuda:0" else: first_device = device_map.get(same_device_modules[0], "cuda:0") for module_name in same_device_modules: if module_name in device_map: device_map[module_name] = first_device return device_map, same_device_modules[0] def _clean_reasoning_trace(text: str) -> str: for token in ("<|im_start|>", "<|im_end|>", "<|endoftext|>"): text = text.replace(token, "") end_tag = "" if end_tag in text: text = text.split(end_tag, 1)[0] + end_tag return text def ensure_runtime() -> tuple[Any, Any]: inferencer = _runtime_cache["inferencer"] pil_img2rgb = _runtime_cache["pil_img2rgb"] if inferencer is not None and pil_img2rgb is not None: return inferencer, pil_img2rgb with _runtime_lock: inferencer = _runtime_cache["inferencer"] pil_img2rgb = _runtime_cache["pil_img2rgb"] if inferencer is not None and pil_img2rgb is not None: return inferencer, pil_img2rgb _ensure_flash_attn_available() from accelerate import init_empty_weights, load_checkpoint_and_dispatch from data.data_utils import add_special_tokens, pil_img2rgb from data.transforms import ImageTransform from inferencer import InterleaveInferencer from modeling.autoencoder import load_ae from modeling.bagel import ( Bagel, BagelConfig, Qwen2Config, Qwen2ForCausalLM, SiglipVisionConfig, SiglipVisionModel, ) from modeling.qwen2 import Qwen2Tokenizer model_path = _resolve_model_path() checkpoint_path = _resolve_checkpoint(model_path) llm_config = Qwen2Config.from_json_file(os.path.join(model_path, "llm_config.json")) llm_config.qk_norm = True llm_config.tie_word_embeddings = False llm_config.layer_module = "Qwen2MoTDecoderLayer" vit_config = SiglipVisionConfig.from_json_file(os.path.join(model_path, "vit_config.json")) vit_config.rope = False vit_config.num_hidden_layers -= 1 vae_model, vae_config = load_ae(local_path=os.path.join(model_path, "ae.safetensors")) config = BagelConfig( visual_gen=True, visual_und=True, llm_config=llm_config, vit_config=vit_config, vae_config=vae_config, vit_max_num_patch_per_side=70, connector_act="gelu_pytorch_tanh", latent_patch_size=2, max_latent_size=64, ) with init_empty_weights(): language_model = Qwen2ForCausalLM(llm_config) vit_model = SiglipVisionModel(vit_config) model = Bagel(language_model, vit_model, config) model.vit_model.vision_model.embeddings.convert_conv2d_to_linear(vit_config, meta=True) tokenizer = Qwen2Tokenizer.from_pretrained(str(model_path)) tokenizer, new_token_ids, _ = add_special_tokens(tokenizer) vae_transform = ImageTransform(1024, 512, 16) vit_transform = ImageTransform(980, 224, 14) device_map, vae_anchor_module = _build_device_map(model) OFFLOAD_DIR.mkdir(parents=True, exist_ok=True) model = load_checkpoint_and_dispatch( model, checkpoint=str(checkpoint_path), device_map=device_map, offload_buffers=True, offload_folder=str(OFFLOAD_DIR), dtype=torch.bfloat16, force_hooks=True, ).eval() vae_device = device_map.get(vae_anchor_module, "cuda:0") vae_model = vae_model.to(device=vae_device, dtype=torch.bfloat16).eval() inferencer = InterleaveInferencer( model=model, vae_model=vae_model, tokenizer=tokenizer, vae_transform=vae_transform, vit_transform=vit_transform, new_token_ids=new_token_ids, ) _runtime_cache["inferencer"] = inferencer _runtime_cache["pil_img2rgb"] = pil_img2rgb return inferencer, pil_img2rgb @gpu_decorator def edit_image( image: Optional[Image.Image], prompt: str, show_thinking: bool = False, cfg_text_scale: float = 4.0, cfg_img_scale: float = 2.0, cfg_interval: float = 0.0, timestep_shift: float = 3.0, num_timesteps: int = 50, cfg_renorm_min: float = 1.0, cfg_renorm_type: str = "text_channel", max_think_token_n: int = 1024, do_sample: bool = False, text_temperature: float = 0.3, seed: int = 0, ): if image is None: yield None, "Upload an image to begin." return try: inferencer, pil_img2rgb = ensure_runtime() except Exception as exc: yield None, f"Unable to initialize Robust-U1 on this Space: {exc}" return if isinstance(image, np.ndarray): image = Image.fromarray(image) set_seed(seed) image = pil_img2rgb(image) inference_hyper = dict( max_think_token_n=max_think_token_n if show_thinking else 1024, do_sample=do_sample if show_thinking else False, text_temperature=text_temperature if show_thinking else 0.3, cfg_text_scale=cfg_text_scale, cfg_img_scale=cfg_img_scale, cfg_interval=[cfg_interval, 1.0], timestep_shift=timestep_shift, num_timesteps=num_timesteps, cfg_renorm_min=cfg_renorm_min, cfg_renorm_type=cfg_renorm_type, ) result_text = "" last_image = None model_prompt = f"{prompt.rstrip()}\n\n{HIDDEN_PROMPT_SUFFIX}" if prompt.strip() else HIDDEN_PROMPT_SUFFIX for chunk in inferencer( image=image, text=model_prompt, think=show_thinking, understanding_output=False, **inference_hyper, ): if isinstance(chunk, str): chunk = ( chunk.replace("<|im_start|>", "") .replace("<|im_end|>", "") .replace("<|endoftext|>", "") ) result_text += chunk else: last_image = chunk yield last_image, _clean_reasoning_trace(result_text) def update_edit_thinking_visibility(show: bool): return gr.update(visible=show), gr.update(visible=show) def load_example_image(image_path: Path) -> Optional[Image.Image]: if not image_path.is_file(): return None return Image.open(image_path).convert("RGB") def build_demo() -> gr.Blocks: default_in = REPO_ROOT / "test_images" / "Dis_image.jpg" default_pil = load_example_image(default_in) def reset_workspace(): return ( default_pil, DEFAULT_PROMPT, None, gr.update(value="", visible=True), True, 4.0, 2.0, 0.0, 3.0, 50, 1.0, "text_channel", 1024, False, 0.3, 0, gr.update(visible=False), ) theme = gr.themes.Soft( primary_hue=gr.themes.colors.cyan, secondary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.slate, ) with gr.Blocks( theme=theme, css=APP_CSS, title="Robust-U1", elem_classes="app-shell", fill_width=True, ) as demo: gr.HTML(_build_header_html()) with gr.Row(equal_height=False): with gr.Column(scale=1, elem_classes="panel"): gr.HTML( """
Input
Source Image and Edit Instruction
""" ) edit_image_input = gr.Image( label="Input image", value=default_pil, type="pil", sources=["upload", "clipboard"], elem_classes="input-image", ) edit_prompt = gr.Textbox( label="Prompt", value=DEFAULT_PROMPT, placeholder="Enter an edit instruction or question.", lines=4, ) with gr.Row(elem_classes="control-bar toolbar"): edit_show_thinking = gr.Checkbox(label="Show reasoning trace", value=True) edit_btn = gr.Button("Run Inference", variant="primary", elem_id="run-button") reset_btn = gr.Button("Reset Workspace", elem_id="secondary-button") with gr.Column(scale=1, elem_classes="panel"): gr.HTML( """
Output
Generated Result and Trace
""" ) edit_image_output = gr.Image( label="Edited output", type="pil", interactive=False, show_download_button=True, elem_classes="output-image", ) edit_thinking_output = gr.Textbox( label="Reasoning trace", visible=True, lines=8, show_copy_button=True, elem_classes="reasoning-box", ) with gr.Accordion("Advanced Inference Controls", open=False, elem_classes="accordion-shell"): with gr.Row(): edit_seed = gr.Slider( minimum=0, maximum=1_000_000, value=0, step=1, label="Seed", info="Use 0 for a non-deterministic run, or a positive integer for reproducibility.", ) edit_cfg_text_scale = gr.Slider( minimum=1.0, maximum=8.0, value=4.0, step=0.1, label="CFG text scale", ) with gr.Row(): edit_cfg_img_scale = gr.Slider( minimum=1.0, maximum=4.0, value=2.0, step=0.1, label="CFG image scale", ) edit_cfg_interval = gr.Slider( minimum=0.0, maximum=1.0, value=0.0, step=0.1, label="CFG activation start", ) with gr.Row(): edit_cfg_renorm_type = gr.Dropdown( choices=["global", "local", "text_channel"], value="text_channel", label="CFG renormalization type", ) edit_cfg_renorm_min = gr.Slider( minimum=0.0, maximum=1.0, value=1.0, step=0.1, label="CFG renormalization floor", ) with gr.Row(): edit_num_timesteps = gr.Slider( minimum=10, maximum=100, value=50, step=5, label="Diffusion timesteps", ) edit_timestep_shift = gr.Slider( minimum=1.0, maximum=10.0, value=3.0, step=0.5, label="Timestep shift", ) edit_thinking_params = gr.Group(visible=True) with edit_thinking_params: with gr.Row(): edit_do_sample = gr.Checkbox(label="Sample reasoning tokens", value=False) edit_max_think_token_n = gr.Slider( minimum=64, maximum=4006, value=1024, step=64, label="Max reasoning tokens", ) edit_text_temperature = gr.Slider( minimum=0.1, maximum=1.0, value=0.3, step=0.1, label="Reasoning temperature", ) edit_show_thinking.change( fn=update_edit_thinking_visibility, inputs=[edit_show_thinking], outputs=[edit_thinking_output, edit_thinking_params], ) reset_btn.click( fn=reset_workspace, inputs=None, outputs=[ edit_image_input, edit_prompt, edit_image_output, edit_thinking_output, edit_show_thinking, edit_cfg_text_scale, edit_cfg_img_scale, edit_cfg_interval, edit_timestep_shift, edit_num_timesteps, edit_cfg_renorm_min, edit_cfg_renorm_type, edit_max_think_token_n, edit_do_sample, edit_text_temperature, edit_seed, edit_thinking_params, ], ) gr.on( triggers=[edit_btn.click, edit_prompt.submit], fn=edit_image, inputs=[ edit_image_input, edit_prompt, edit_show_thinking, edit_cfg_text_scale, edit_cfg_img_scale, edit_cfg_interval, edit_timestep_shift, edit_num_timesteps, edit_cfg_renorm_min, edit_cfg_renorm_type, edit_max_think_token_n, edit_do_sample, edit_text_temperature, edit_seed, ], outputs=[edit_image_output, edit_thinking_output], ) gr.HTML( f""" """ ) return demo demo = build_demo() if __name__ == "__main__": demo.queue(max_size=4).launch()