| import os, io, math, time, base64 |
| from typing import Tuple |
| import gradio as gr |
| from PIL import Image, ImageFilter, ImageOps |
| import numpy as np |
| import torch |
|
|
| |
| MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen-Image-Edit") |
| HF_TOKEN = os.getenv("HF_TOKEN", "").strip() or None |
|
|
| |
| |
| |
| from diffusers import AutoPipelineForInpainting, StableDiffusionInpaintPipeline |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| dtype = torch.float16 if device.type == "cuda" else torch.float32 |
|
|
| def _load_pipeline(): |
| auth = {"token": HF_TOKEN} if HF_TOKEN else {} |
| try: |
| pipe = AutoPipelineForInpainting.from_pretrained( |
| MODEL_ID, torch_dtype=dtype, **auth |
| ) |
| except Exception: |
| |
| pipe = StableDiffusionInpaintPipeline.from_pretrained( |
| MODEL_ID, torch_dtype=dtype, **auth |
| ) |
| pipe = pipe.to(device) |
| if hasattr(pipe, "enable_attention_slicing"): |
| pipe.enable_attention_slicing() |
| return pipe |
|
|
| PIPE = None |
| LOAD_ERR = None |
| try: |
| PIPE = _load_pipeline() |
| except Exception as e: |
| LOAD_ERR = str(e) |
|
|
| |
| def _pad_canvas(img: Image.Image, left: int, right: int, top: int, bottom: int, |
| fill=(127,127,127)) -> Tuple[Image.Image, Tuple[int,int,int,int]]: |
| """Создаёт расширенный холст и возвращает (canvas, bbox вставки исходника).""" |
| w, h = img.size |
| canvas = Image.new("RGB", (w + left + right, h + top + bottom), fill) |
| canvas.paste(img, (left, top)) |
| return canvas, (left, top, left + w, top + h) |
|
|
| def _feather_mask(size: Tuple[int,int], bbox: Tuple[int,int,int,int], feather_px: int = 32) -> Image.Image: |
| """Маска: 255 = дорисовать, 0 = оставить исходник. По краю делаем плавный градиент.""" |
| W, H = size |
| L, T, R, B = bbox |
| mask = Image.new("L", (W, H), 255) |
| base = Image.new("L", (W, H), 0) |
| base.paste(255, (L, T, R, B)) |
| |
| inv = ImageOps.invert(base) |
| if feather_px > 0: |
| inv = inv.filter(ImageFilter.GaussianBlur(radius=feather_px)) |
| return inv |
|
|
| def expand_with_model(image: Image.Image, left: int, right: int, top: int, bottom: int, |
| prompt: str, neg: str, steps: int, guidance: float, seed: int | None): |
| if LOAD_ERR: |
| raise gr.Error(f"Не удалось загрузить модель '{MODEL_ID}'.\n{LOAD_ERR}") |
|
|
| for name, val in [("left",left),("right",right),("top",top),("bottom",bottom)]: |
| if val is None or val < 0 or val > 2048: |
| raise gr.Error(f"{name} должен быть в диапазоне 0..2048") |
|
|
| |
| canvas, bbox = _pad_canvas(image.convert("RGB"), left, right, top, bottom) |
| mask = _feather_mask(canvas.size, bbox, feather_px=48) |
|
|
| |
| clean_prompt = (prompt or "").strip() |
| if not clean_prompt: |
| clean_prompt = ( |
| "Seamlessly continue the existing background so it looks like a natural, " |
| "wider version of the same image. Match colors, textures, lighting and perspective. " |
| "No frames, no collage, no new subjects." |
| ) |
| negative_prompt = (neg or "frames, borders, phone mockup, collage tiles, UI elements, captions, watermark").strip() |
|
|
| generator = None |
| if isinstance(seed, int) and seed >= 0: |
| generator = torch.Generator(device=device).manual_seed(seed) |
|
|
| |
| out = PIPE( |
| prompt=clean_prompt, |
| negative_prompt=negative_prompt, |
| image=canvas, |
| mask_image=mask, |
| num_inference_steps=int(steps), |
| guidance_scale=float(guidance), |
| generator=generator |
| ).images[0] |
|
|
| return out, clean_prompt, negative_prompt |
|
|
| |
| with gr.Blocks(title="Qwen Image — Seamless Expand") as demo: |
| gr.Markdown("## Qwen Image — Seamless Background Expand\n" |
| "Загрузи картинку, задай сколько пикселей дорисовать, при желании уточни промпт.") |
|
|
| if LOAD_ERR: |
| gr.Markdown( |
| f"**⚠️ Модель не загрузилась.** Проверь `MODEL_ID` в Settings → Variables. Текущая: `{MODEL_ID}` " |
| f"\nСообщение: `{LOAD_ERR}`" |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| img_in = gr.Image(type="pil", label="Картинка") |
| prompt = gr.Textbox(label="Prompt (EN)", placeholder="Describe seamless continuation…") |
| neg = gr.Textbox(label="Negative prompt", value="frames, borders, phone mockup, collage tiles, UI elements, captions, watermark") |
| with gr.Row(): |
| left = gr.Number(value=256, label="Left (px)", precision=0) |
| right = gr.Number(value=256, label="Right (px)", precision=0) |
| with gr.Row(): |
| top = gr.Number(value=256, label="Top (px)", precision=0) |
| bottom = gr.Number(value=256, label="Bottom (px)", precision=0) |
| with gr.Row(): |
| steps = gr.Slider(10, 60, value=30, step=1, label="Steps") |
| guidance = gr.Slider(0.5, 12.0, value=5.5, step=0.1, label="Guidance scale") |
| seed = gr.Number(value=-1, label="Seed (-1 = random)", precision=0) |
| btn = gr.Button("Expand", variant="primary") |
| with gr.Column(scale=1): |
| img_out = gr.Image(label="Результат") |
| used_prompt = gr.Textbox(label="Использованный prompt", interactive=False) |
| used_neg = gr.Textbox(label="Использованный negative", interactive=False) |
|
|
| btn.click( |
| expand_with_model, |
| inputs=[img_in, left, right, top, bottom, prompt, neg, steps, guidance, seed], |
| outputs=[img_out, used_prompt, used_neg] |
| ) |
|
|
| demo.launch() |
|
|