File size: 8,219 Bytes
2837469
0df27ca
2837469
 
 
 
 
e22a186
aa90eb6
6043861
e22a186
2837469
6043861
 
 
e22a186
 
0df27ca
 
6043861
e22a186
 
0df27ca
6043861
 
 
 
0df27ca
 
 
 
6043861
e22a186
 
2837469
0df27ca
8ba05cf
 
6043861
 
0df27ca
6043861
 
 
 
 
 
 
 
 
 
8ba05cf
6043861
 
 
 
 
 
 
0df27ca
2837469
e22a186
6043861
8ba05cf
6043861
8ba05cf
 
6043861
8ba05cf
6043861
 
 
8ba05cf
6043861
 
 
 
8ba05cf
6043861
8ba05cf
 
 
 
 
 
 
6043861
 
8ba05cf
 
6043861
 
8ba05cf
6043861
 
60a5da8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import gc
import json
import random
import threading
import traceback

import gradio as gr
import spaces
import torch
from PIL import Image, ImageDraw
from diffusers import QwenImage21Pipeline

BASE_MODEL = "Qwen/Qwen-Image-2.1"
LORA_REPO = "retronic/Pixelship-2.0"
ADAPTER_NAME = "pixelship"
DTYPE = torch.bfloat16
pipe = None
lock = threading.Lock()


def load_pipeline():
    global pipe
    if pipe is None:
        print("Loading Qwen Image 2.1...", flush=True)
        p = QwenImage21Pipeline.from_pretrained(
            BASE_MODEL, torch_dtype=DTYPE, low_cpu_mem_usage=True
        )
        p.load_lora_weights(LORA_REPO, adapter_name=ADAPTER_NAME)
        p.set_adapters([ADAPTER_NAME], adapter_weights=[0.75])
        p.enable_model_cpu_offload()
        pipe = p
        print("Pixelship pipeline ready", flush=True)
    return pipe


def parse_plan(raw):
    data = json.loads(raw)
    if not isinstance(data, dict):
        raise ValueError("JSON plan must be an object")
    for key in ("prompt", "math", "objects", "background", "style"):
        if key not in data:
            raise ValueError(f"Missing JSON field: {key}")
    if not isinstance(data["objects"], list) or not isinstance(data["background"], dict):
        raise ValueError("objects must be a list and background must be an object")
    bg_objects = data["background"].get("objects", [])
    if not isinstance(bg_objects, list):
        raise ValueError("background.objects must be a list")
    for obj in data["objects"] + bg_objects:
        if not isinstance(obj, dict):
            raise ValueError("Each object must be a JSON object")
        for key in ("description", "x", "y", "width", "height"):
            if key not in obj:
                raise ValueError(f"Missing object field: {key}")
        for key in ("x", "y", "width", "height"):
            v = float(obj[key])
            if not 0 <= v <= 1:
                raise ValueError(f"{key} must be between 0 and 1")
        if float(obj["x"]) + float(obj["width"]) > 1.001 or float(obj["y"]) + float(obj["height"]) > 1.001:
            raise ValueError("Object bounds must fit within the image")
    return data


def draw_map(plan, width, height):
    w, h = int(width), int(height)
    im = Image.new("RGB", (w, h), "#20232a")
    d = ImageDraw.Draw(im)
    for x in range(0, w, max(1, w // 10)):
        d.line((x, 0, x, h), fill="#353a43")
    for y in range(0, h, max(1, h // 10)):
        d.line((0, y, w, y), fill="#353a43")
    entries = [(o, "#4ecdc4") for o in plan["background"].get("objects", [])]
    entries += [(o, "#ffce54") for o in plan["objects"]]
    for i, (obj, color) in enumerate(entries, 1):
        x0 = round(float(obj["x"]) * w)
        y0 = round(float(obj["y"]) * h)
        x1 = round((float(obj["x"]) + float(obj["width"])) * w)
        y1 = round((float(obj["y"]) + float(obj["height"])) * h)
        d.rectangle((x0, y0, x1, y1), outline=color, width=max(1, w // 500))
        d.text((min(w - 1, x0 + 2), max(0, y0 - 13)), f'{i}: {str(obj["description"])[:38]}', fill=color, stroke_width=1, stroke_fill="#101010")
    return im


@spaces.GPU(duration=120)
def generate(prompt, input_image, negative_prompt, width, height, steps, true_cfg,
             lora_strength, seed, random_seed, use_json, plan_text, show_map,
             progress=gr.Progress()):
    if not str(prompt or "").strip() and not (use_json and str(plan_text or "").strip()):
        raise gr.Error("Enter a prompt or supply a JSON plan")
    with lock:
        try:
            plan = None
            actual_prompt = str(prompt or "")
            if use_json:
                if not str(plan_text or "").strip():
                    raise gr.Error("Paste a JSON plan into the JSON box")
                plan = parse_plan(plan_text)
                actual_prompt = json.dumps(plan, ensure_ascii=False)

            progress(0.05, desc="Loading Pixelship 2.0")
            p = load_pipeline()
            seed = random.randint(0, 2**32 - 1) if random_seed else int(seed)
            p.set_adapters([ADAPTER_NAME], adapter_weights=[float(lora_strength)])
            kwargs = dict(
                prompt=actual_prompt,
                num_inference_steps=int(steps),
                true_cfg_scale=float(true_cfg),
                generator=torch.Generator(device="cpu").manual_seed(seed),
            )
            if input_image is not None:
                kwargs["image"] = input_image
            else:
                kwargs.update(width=int(width), height=int(height))
            if float(true_cfg) > 1.0:
                kwargs["negative_prompt"] = negative_prompt or ""

            progress(0.2, desc="Generating image")
            with torch.no_grad():
                result = p(**kwargs)
            image = result.images[0]
            map_image = draw_map(plan, image.width, image.height) if show_map and plan else None
            progress(1, desc="Complete")
            return image, map_image, f"Complete | Seed={seed} | Steps={int(steps)} | LoRA={lora_strength}"
        except torch.cuda.OutOfMemoryError:
            gc.collect()
            if torch.cuda.is_available():
                torch.cuda.empty_cache()
            raise gr.Error("Out of GPU memory. Try a smaller image or fewer steps.")
        except Exception as e:
            traceback.print_exc()
            raise gr.Error(f"Generation failed: {e}")


CSS = """
.gradio-container { max-width: 1600px !important; margin: auto !important; }
#prompt-box textarea { min-height: 220px !important; font-family: monospace !important; }
"""

with gr.Blocks(title="Pixelship 2.0", theme=gr.themes.Soft(), css=CSS) as demo:
    gr.Markdown("# 🎨 Pixelship 2.0\nQwen Image 2.1 + Pixelship LoRA | Manual JSON Styling | Optional spatial map")
    with gr.Row():
        with gr.Column(scale=5):
            prompt = gr.Textbox(label="Prompt", lines=10, max_lines=40, elem_id="prompt-box", placeholder="Describe your image...")
            input_image = gr.Image(label="Image to Edit (optional)", type="pil")
            negative_prompt = gr.Textbox(label="Negative Prompt", lines=2)
            with gr.Accordion("JSON Styling / Spatial Planning", open=False):
                use_json = gr.Checkbox(label="Use manual JSON Styling", value=False)
                plan_text = gr.Code(label="JSON plan (paste your own)", language="json", lines=16)
                show_map = gr.Checkbox(label="Show spatial layout map", value=False)
                gr.Markdown("The map shows planned bounding boxes, not detected objects in the generated image.")
            with gr.Accordion("Generation Settings", open=True):
                with gr.Row():
                    width = gr.Slider(256, 2048, value=1024, step=64, label="Width")
                    height = gr.Slider(256, 2048, value=1024, step=64, label="Height")
                with gr.Row():
                    steps = gr.Slider(1, 100, value=30, step=1, label="Steps")
                    true_cfg = gr.Slider(1.0, 10.0, value=1.0, step=0.1, label="True CFG")
                lora_strength = gr.Slider(0, 2, value=0.75, step=0.05, label="LoRA Strength")
                with gr.Row():
                    seed = gr.Number(label="Seed", value=42, precision=0)
                    random_seed = gr.Checkbox(label="Random Seed", value=True)
            generate_button = gr.Button("Generate", variant="primary", size="lg")
        with gr.Column(scale=5):
            output_image = gr.Image(label="Generated Image", type="pil", format="png", height=600)
            map_output = gr.Image(label="Spatial Layout Map", type="pil", format="png")
            status = gr.Textbox(label="Status", interactive=False)
    generate_button.click(
        fn=generate,
        inputs=[prompt, input_image, negative_prompt, width, height, steps, true_cfg,
                lora_strength, seed, random_seed, use_json, plan_text, show_map],
        outputs=[output_image, map_output, status],
        concurrency_limit=1,
    )

if __name__ == "__main__":
    print("Starting Pixelship 2.0 Gradio server...", flush=True)
    demo.queue(max_size=10, default_concurrency_limit=1)
    demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)