File size: 9,047 Bytes
3f17887
 
66f36d3
 
3f17887
 
66f36d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f17887
66f36d3
 
3f17887
66f36d3
3f17887
66f36d3
3f17887
 
66f36d3
 
3f17887
 
 
0b000fa
 
 
 
66f36d3
 
3f17887
66f36d3
 
 
 
 
 
3f17887
66f36d3
 
 
 
 
 
3f17887
 
66f36d3
 
3f17887
66f36d3
 
 
 
3f17887
 
66f36d3
3f17887
 
 
 
 
 
66f36d3
 
 
 
 
 
3f17887
 
 
66f36d3
3f17887
66f36d3
3f17887
 
66f36d3
 
 
 
3f17887
66f36d3
 
 
 
3f17887
66f36d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f17887
66f36d3
3f17887
 
 
 
 
66f36d3
 
 
 
 
 
3f17887
d714174
3f17887
66f36d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f17887
66f36d3
 
3f17887
 
66f36d3
 
3f17887
66f36d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d714174
3f17887
 
 
66f36d3
3f17887
66f36d3
 
3f17887
 
 
 
66f36d3
 
 
 
3f17887
 
d714174
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import os

# Allocator config helps with transient spikes from the pixel-space / large
# attention-mask ops in the region-attention editor.
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")

import spaces  # noqa: E402  (must precede torch / CUDA-touching imports)
import time  # noqa: E402
import random  # noqa: E402

import torch  # noqa: E402
import gradio as gr  # noqa: E402
from PIL import Image  # noqa: E402

from replan.pipelines.replan import RePlanPipeline  # noqa: E402

# -----------------------------------------------------------------------------
# Model configuration
# -----------------------------------------------------------------------------
# RePlan couples a reasoning VLM planner (Qwen2.5-VL-7B, fine-tuned by the
# authors) with a diffusion editor. We use FLUX.2 [klein] 4B as the editor:
# it is Apache-2.0 (ungated), compact (~16 GB bf16), distilled (4 steps), and
# is one of the backbones the RePlan authors explicitly support.
VLM_CKPT = "TainU/RePlan-Qwen2.5-VL-7B"
DIFFUSION_MODEL = "black-forest-labs/FLUX.2-klein-4B"
PIPELINE_TYPE = "klein"

# klein-specific defaults from RePlan's run_replan.py
DEFAULT_STEPS = 4
DEFAULT_EXPAND_VALUE = 0.15
DEFAULT_ATTENTION_SWITCH_STEP = 0.05
MAX_SEED = 2**31 - 1

# Load both stages once at module scope, eagerly moved to CUDA. ZeroGPU packs
# the weights to disk at startup and streams them into VRAM on the first
# @spaces.GPU call. enable_flex_attn is left False (see below), so the editor
# uses a dense attention mask via SDPA and never needs torch.compile.
pipeline = RePlanPipeline(
    vlm_ckpt_path=VLM_CKPT,
    diffusion_model_name=DIFFUSION_MODEL,
    pipeline_type=PIPELINE_TYPE,
    output_dir=None,
    device="cuda",
    torch_dtype=torch.bfloat16,
    vlm_prompt_template_path="replan.txt",
    init_vlm=True,
    image_dir=None,
    stage_offload="none",
)


# Measured GPU time for a single-region 4-step edit is ~5-6s. Multi-region
# instructions produce longer VLM reasoning (up to 2048 tokens) + more diffusion
# work, so we keep a comfortable-but-lean margin.
@spaces.GPU(duration=60)
def edit_image(
    image: Image.Image,
    instruction: str,
    seed: int = 0,
    randomize_seed: bool = True,
    steps: int = DEFAULT_STEPS,
    guidance_scale: float = 4.0,
    expand_value: float = DEFAULT_EXPAND_VALUE,
    progress=gr.Progress(track_tqdm=True),
):
    """Edit an image following a complex natural-language instruction.

    RePlan first runs a reasoning VLM planner that decomposes the instruction
    into region-specific bounding boxes + local hints and a global prompt, then
    executes the edit with a diffusion backbone using training-free
    region-constrained attention injection.

    Args:
        image: The input image to edit.
        instruction: The natural-language editing instruction.
        seed: RNG seed for reproducibility.
        randomize_seed: If True, pick a fresh random seed each run.
        steps: Number of diffusion denoising steps.
        guidance_scale: Classifier-free guidance scale for the editor.
        expand_value: Ratio by which planned bounding boxes are expanded.

    Returns:
        A tuple of (edited image, VLM plan/reasoning text, the seed used).
    """
    if image is None:
        raise gr.Error("Please provide an input image.")
    if not instruction or not instruction.strip():
        raise gr.Error("Please provide an editing instruction.")

    if randomize_seed:
        seed = random.randint(0, MAX_SEED)
    seed = int(seed)

    if isinstance(image, str):
        image = Image.open(image)
    image = image.convert("RGB")

    t0 = time.perf_counter()
    results = pipeline.edit_single(
        image=image,
        instruction=instruction.strip(),
        expand_value=float(expand_value),
        expand_mode="ratio",
        attention_switch_step=DEFAULT_ATTENTION_SWITCH_STEP,
        bboxes_attend_to_each_other=True,
        symmetric_masking=False,
        enable_flex_attn=False,          # dense-mask SDPA path (no torch.compile)
        flex_attn_use_bitmask=True,
        skip_save=True,
        num_inference_steps=int(steps),
        guidance_scale=float(guidance_scale),
        generator=torch.manual_seed(seed),
    )
    elapsed = time.perf_counter() - t0

    edited = results["edited_image"]
    response = results.get("vlm_response", "") or ""
    global_prompt = results.get("global_prompt", "") or ""
    bbox_data = results.get("bbox_data") or []

    # Build a compact, readable plan summary from the raw VLM response.
    plan_lines = [f"Inference time: {elapsed:.1f}s  |  seed: {seed}", ""]
    if global_prompt.strip():
        plan_lines.append(f"Global edit: {global_prompt.strip()}")
    if bbox_data:
        plan_lines.append("")
        plan_lines.append(f"Planned regions ({len(bbox_data)}):")
        for i, b in enumerate(bbox_data, 1):
            hint = b.get("hint", "")
            box = b.get("bbox_2d", "")
            plan_lines.append(f"  {i}. {hint}  @ bbox {box}")
    plan_lines.append("")
    plan_lines.append("--- Full VLM reasoning ---")
    plan_lines.append(response.strip())
    plan_text = "\n".join(plan_lines)

    return edited, plan_text, seed


# -----------------------------------------------------------------------------
# UI
# -----------------------------------------------------------------------------
CSS = """
#col-container { max-width: 1200px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""

EXAMPLES = [
    ["examples/cup.png", "Replace the glass that has been used and left on the desk with a small potted plant"],
    ["examples/crowd.png", "Find the woman with light blue backpack and change the color of her shoes to red"],
    ["examples/festival.png", "The sun has now set, and someone has decorated the large bush on the right with colorful fairy lights for the evening's festivities."],
    ["examples/sunglasses.png", "Remove the sunglasses from the person sitting on the left."],
]

with gr.Blocks() as demo:
    with gr.Column(elem_id="col-container"):
        gr.Markdown(
            """
            # RePlan — Reasoning-guided Region Planning for Image Editing

            RePlan tackles **complex instruction-based image editing** by first having a
            reasoning VLM planner decompose your instruction into region-specific guidance
            (bounding boxes + local hints), then executing the edit with training-free
            **region-constrained attention injection** on a diffusion editor.

            Planner: [`TainU/RePlan-Qwen2.5-VL-7B`](https://huggingface.co/TainU/RePlan-Qwen2.5-VL-7B) ·
            Editor: [`FLUX.2-klein-4B`](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) ·
            [Paper](https://huggingface.co/papers/2512.16864) ·
            [Code](https://github.com/dvlab-research/RePlan)
            """
        )

        with gr.Row():
            with gr.Column():
                input_image = gr.Image(label="Input image", type="pil", height=380)
                instruction = gr.Textbox(
                    label="Editing instruction",
                    placeholder="e.g. Find the woman with the blue backpack and change her shoes to red",
                    lines=2,
                )
                run_btn = gr.Button("Edit image", variant="primary")
                with gr.Accordion("Advanced settings", open=False):
                    randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
                    seed = gr.Number(label="Seed", value=0, precision=0)
                    steps = gr.Slider(
                        label="Denoising steps", minimum=2, maximum=12, step=1,
                        value=DEFAULT_STEPS,
                    )
                    guidance_scale = gr.Slider(
                        label="Guidance scale", minimum=1.0, maximum=6.0, step=0.5,
                        value=4.0,
                    )
                    expand_value = gr.Slider(
                        label="Region expansion (ratio)", minimum=0.0, maximum=0.5,
                        step=0.05, value=DEFAULT_EXPAND_VALUE,
                    )
            with gr.Column():
                output_image = gr.Image(label="Edited image", height=380)
                plan_out = gr.Textbox(
                    label="Planner reasoning & regions", lines=12,
                )

        gr.Examples(
            examples=EXAMPLES,
            inputs=[input_image, instruction],
            outputs=[output_image, plan_out, seed],
            fn=edit_image,
            cache_examples=True,
            cache_mode="lazy",
        )

    inputs = [input_image, instruction, seed, randomize_seed, steps, guidance_scale, expand_value]
    outputs = [output_image, plan_out, seed]
    run_btn.click(edit_image, inputs=inputs, outputs=outputs, api_name="edit")
    instruction.submit(edit_image, inputs=inputs, outputs=outputs, api_name=False)

if __name__ == "__main__":
    demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)