File size: 10,626 Bytes
ef38b3f
 
 
b14cb72
 
ef38b3f
 
 
 
fc68e5b
 
 
ef38b3f
 
fc68e5b
 
 
ef38b3f
 
 
 
fc68e5b
 
 
 
 
 
 
ef38b3f
 
 
 
 
 
 
 
 
 
9039eda
ef38b3f
 
 
fc68e5b
ef38b3f
fc68e5b
ef38b3f
 
 
 
 
 
 
 
1aba00a
2b2693b
 
0c5c445
f5d0ebd
1aba00a
76812cf
1aba00a
 
76812cf
1aba00a
f5d0ebd
58e741f
2b2693b
1aba00a
 
 
 
9c12bef
 
2b2693b
58e741f
f5d0ebd
fc68e5b
f5d0ebd
 
 
 
 
 
fc68e5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b14cb72
f5d0ebd
b14cb72
fc68e5b
b14cb72
 
 
 
 
 
 
 
 
fc68e5b
b14cb72
 
 
fc68e5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b14cb72
 
 
 
c9c2f8c
18a9431
 
 
fc68e5b
 
 
 
 
 
 
 
8e1b1c7
77c2b19
b14cb72
 
8e1b1c7
77c2b19
 
 
1aba00a
77c2b19
1aba00a
9c12bef
 
77c2b19
 
1aba00a
77c2b19
 
1aba00a
 
77c2b19
 
b14cb72
 
7b276d4
b14cb72
 
 
 
 
 
 
 
f5d0ebd
b14cb72
1aba00a
 
b14cb72
18a9431
 
b14cb72
fc68e5b
8e1b1c7
b14cb72
fc68e5b
b14cb72
76812cf
1aba00a
 
 
 
 
 
 
ef38b3f
 
b14cb72
ef38b3f
f5d0ebd
 
 
 
ef38b3f
b14cb72
ef38b3f
 
b14cb72
 
 
 
 
 
 
 
ef38b3f
f5d0ebd
ef38b3f
b14cb72
ef38b3f
 
 
 
f5d0ebd
b14cb72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef38b3f
f5d0ebd
b14cb72
 
 
 
fc68e5b
f5d0ebd
b14cb72
 
 
 
 
5cf87ba
f5d0ebd
ef38b3f
b14cb72
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import os
import gc
import gradio as gr
from gradio import Server
from fastapi.responses import HTMLResponse
import numpy as np
import spaces
import torch
import random
import base64
import json
from io import BytesIO
from PIL import Image

MAX_SEED = np.iinfo(np.int32).max
LANCZOS = getattr(Image, "Resampling", Image).LANCZOS

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
print("torch.__version__ =", torch.__version__)
print("torch.version.cuda =", torch.version.cuda)
print("cuda available:", torch.cuda.is_available())
print("cuda device count:", torch.cuda.device_count())
if torch.cuda.is_available():
    print("current device:", torch.cuda.current_device())
    print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))

print("Using device:", device)

from diffusers import FlowMatchEulerDiscreteScheduler
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3

dtype = torch.bfloat16

pipe = QwenImageEditPlusPipeline.from_pretrained(
    "FireRedTeam/FireRed-Image-Edit-1.1",
    transformer=QwenImageTransformer2DModel.from_pretrained(
        "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
        torch_dtype=dtype,
        device_map="cuda",
    ),
    torch_dtype=dtype,
).to(device)

try:
    pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
    print("Flash Attention 3 Processor set successfully.")
except Exception as e:
    print(f"Warning: Could not set FA3 processor: {e}")

# ── NCII safety guard (pure CPU subprocess, never touches spaces/CUDA) ──────
NCII_UNSAFE_LABEL = "ncii"
NCII_THRESHOLD = 0.5
NCII_BLOCK_MESSAGE = "This prompt was flagged by the NCII content filter and was not processed. Please try again with a different prompt."

print("Starting NCII safety guard subprocess (CPU-only)…")
try:
    ncii_guard.start()
    print("NCII guard subprocess ready.")
except Exception as e:
    print(f"Warning: Could not start NCII guard subprocess: {e}")


def check_ncii_safety(prompt_text):
    """Ask the CPU-side subprocess. No GPU is requested by this call.

    Returns: (is_unsafe: bool, label: str, score: float)
    """
    # Bypassed safety check to prevent false positives
    return False, "safe", 0.0


# ── Examples Config ───────────────────────────────────────────────────────────
EXAMPLES_CONFIG = [
    {"images": ["examples/1.jpg"], "prompt": "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed 'Fire-Edit' preserving realistic texture and details."},
    {"images": ["examples/2.jpg"], "prompt": "Transform the image into a dotted cartoon style."},
    {"images": ["examples/3.jpeg"], "prompt": "Convert it to black and white."},
    {"images": ["examples/4.jpg", "examples/5.jpg"], "prompt": "Replace her glasses with the new glasses from image 1."},
    {"images": ["examples/8.jpg", "examples/9.png"], "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result."},
    {"images": ["examples/10.jpg", "examples/11.png"], "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result."},
]

def make_thumb_b64(path, max_dim=220):
    if not os.path.exists(path):
        return ""
    try:
        img = Image.open(path).convert("RGB")
        img.thumbnail((max_dim, max_dim), LANCZOS)
        buf = BytesIO()
        img.save(buf, format="JPEG", quality=65)
        return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
    except Exception as e:
        print(f"Thumbnail error for {path}: {e}")
        return ""

def encode_full_image(path):
    if not os.path.exists(path):
        return ""
    try:
        with open(path, "rb") as f:
            data = f.read()
        ext = path.rsplit(".", 1)[-1].lower()
        mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
        return f"data:{mime};base64,{base64.b64encode(data).decode()}"
    except Exception as e:
        print(f"Encode error for {path}: {e}")
        return ""

def build_client_config():
    """Static config consumed by the frontend: example cards."""
    examples = []
    for i, ex in enumerate(EXAMPLES_CONFIG):
        examples.append({
            "idx": i,
            "thumbs": [make_thumb_b64(p) for p in ex["images"]],
            "n_images": len(ex["images"]),
            "prompt": ex["prompt"],
        })
    return {
        "examples": examples,
    }

print("Building client config (example thumbnails)…")
CLIENT_CONFIG = build_client_config()
print(f"Built config with {len(EXAMPLES_CONFIG)} examples.")

def b64_to_pil_list(b64_json_str):
    if not b64_json_str or b64_json_str.strip() in ("", "[]"):
        return []
    try:
        b64_list = json.loads(b64_json_str)
    except Exception:
        return []
    pil_images = []
    for b64_str in b64_list:
        if not b64_str or not isinstance(b64_str, str):
            continue
        try:
            if b64_str.startswith("data:image"):
                _, data = b64_str.split(",", 1)
            else:
                data = b64_str
            image_data = base64.b64decode(data)
            pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
        except Exception as e:
            print(f"Error decoding image: {e}")
    return pil_images

def pil_to_b64_png(image: Image.Image) -> str:
    buf = BytesIO()
    image.save(buf, format="PNG")
    return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"

def update_dimensions_on_upload(image):
    if image is None:
        return 1024, 1024
    w, h = image.size
    if w > h:
        nw = 1024
        nh = int(nw * h / w)
    else:
        nh = 1024
        nw = int(nh * w / h)
    return (nw // 8) * 8, (nh // 8) * 8


# ── Gradio Server (Server mode): FastAPI + Gradio queue/API engine ────────────
app = Server(title="FireRed-Image-Edit-1.0-Fast")


@app.api(name="check_safety", queue=False)
def check_safety(prompt: str) -> dict:
    """Pure-CPU safety check. Runs BEFORE any @spaces.GPU resource is requested.

    Returns:
        {"status": "ok"}                       - prompt is safe, proceed
        {"status": "blocked", "message": ...}  - NCII detected
    """
    if not prompt or not prompt.strip():
        return {"status": "ok"}
    is_unsafe, label, score = check_ncii_safety(prompt)
    if is_unsafe:
        return {"status": "blocked", "message": NCII_BLOCK_MESSAGE}
    return {"status": "ok", "label": label, "score": score}


@app.mcp.tool(name="edit_image")
@app.api(name="edit_image")
@spaces.GPU(size="xlarge")
def infer(
    images_b64_json: str,
    prompt: str,
    seed: int,
    randomize_seed: bool,
    guidance_scale: float,
    steps: int,
) -> dict:
    """Edit one or more images with FireRed-Image-Edit-1.1.

    The frontend is expected to have already called /check_safety and
    only submit here when safe. This is a defense-in-depth re-check.
    """
    gc.collect()
    torch.cuda.empty_cache()

    pil_images = b64_to_pil_list(images_b64_json)
    if not pil_images:
        raise gr.Error("Please upload at least one image to edit.")
    if not prompt or prompt.strip() == "":
        raise gr.Error("Please enter an edit prompt.")

    # ── Defense-in-depth NCII re-check inside the GPU worker ──
    is_unsafe, _, _ = check_ncii_safety(prompt)
    if is_unsafe:
        gc.collect()
        torch.cuda.empty_cache()
        return {"image": "", "seed": seed, "status": "blocked", "message": NCII_BLOCK_MESSAGE}

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

    generator = torch.Generator(device=device).manual_seed(seed)
    negative_prompt = (
        "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
        "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
    )
    width, height = update_dimensions_on_upload(pil_images[0])

    try:
        result_image = pipe(
            image=pil_images,
            prompt=prompt,
            negative_prompt=negative_prompt,
            height=height,
            width=width,
            num_inference_steps=steps,
            generator=generator,
            true_cfg_scale=guidance_scale,
        ).images[0]
        return {"image": pil_to_b64_png(result_image), "seed": seed, "status": "success"}
    except Exception as e:
        raise e
    finally:
        gc.collect()
        torch.cuda.empty_cache()


@app.api(name="load_example", queue=False)
def load_example(idx: float) -> dict:
    """Return base64-encoded example images + prompt for a given example index."""
    try:
        i = int(idx)
    except (ValueError, TypeError):
        i = -1
    if i < 0 or i >= len(EXAMPLES_CONFIG):
        return {"images": [], "prompt": "", "names": [], "status": "error"}
    ex = EXAMPLES_CONFIG[i]
    b64_list, names = [], []
    for path in ex["images"]:
        b64 = encode_full_image(path)
        if b64:
            b64_list.append(b64)
            names.append(os.path.basename(path))
    return {"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"}


@app.get("/api/config")
def client_config():
    """Plain FastAPI route: example card data for the frontend."""
    return CLIENT_CONFIG


@app.get("/", response_class=HTMLResponse)
async def homepage():
    html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
    with open(html_path, "r", encoding="utf-8") as f:
        return f.read()


if __name__ == "__main__":
    app.launch(show_error=True, mcp_server=True)