File size: 13,709 Bytes
0b4f874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423284e
 
 
 
0b4f874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d17fd67
0b4f874
 
 
423284e
 
 
 
 
 
 
 
0b4f874
 
 
36a4364
0b4f874
 
 
 
 
 
db98262
 
 
0b4f874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d17fd67
0b4f874
d17fd67
0b4f874
d17fd67
 
0b4f874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423284e
 
 
 
 
0b4f874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6678927
0b4f874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423284e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b4f874
 
 
 
 
 
 
 
36a4364
 
 
 
0b4f874
36a4364
0b4f874
 
 
423284e
0b4f874
423284e
0b4f874
423284e
 
 
0b4f874
36a4364
 
 
 
 
 
0b4f874
 
 
 
 
 
db98262
0b4f874
 
 
36a4364
0b4f874
 
 
36a4364
0b4f874
36a4364
0b4f874
36a4364
 
0b4f874
 
 
36a4364
 
0b4f874
 
4d9e549
 
0b4f874
 
 
 
 
 
 
 
 
 
 
4d9e549
0b4f874
 
36a4364
 
 
db98262
36a4364
 
 
 
 
 
 
0b4f874
36a4364
 
 
 
 
 
0b4f874
 
36a4364
 
 
0b4f874
36a4364
0b4f874
 
 
 
 
 
423284e
 
 
 
 
 
 
36a4364
0b4f874
 
 
 
4d9e549
36a4364
0b4f874
 
 
 
 
 
4d9e549
0b4f874
 
 
 
 
36a4364
0b4f874
36a4364
0b4f874
 
 
 
 
 
 
 
 
 
 
36a4364
 
 
 
0b4f874
36a4364
0b4f874
 
 
 
 
 
 
 
 
 
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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
import gc
import os
import sys


IS_ZERO_GPU = os.environ.get("SPACES_ZERO_GPU") is not None

# `spaces` must be imported before any CUDA-initializing package, so this block stays above the
# torch/diffusers imports below.
if IS_ZERO_GPU:
    import spaces
else:

    class _NoSpaces:
        @staticmethod
        def GPU(*args, **kwargs):
            if len(args) == 1 and callable(args[0]) and not kwargs:
                return args[0]

            def deco(fn):
                return fn

            return deco

    spaces = _NoSpaces()


import gradio as gr  # noqa: E402
import numpy as np  # noqa: E402
import torch  # noqa: E402
from PIL import Image, ImageChops  # noqa: E402

from diffusers.modular_pipelines import (  # noqa: E402
    ComponentsManager,
    ModularPipelineBlocks,
)


BLOCK_REPO = os.environ.get("KREA2_BLOCKS", "OzzyGT/krea2_reference_blocks")

MODELS = {
    "Krea 2 Turbo β€” SDNQ int8 (~18GB)": {
        "repo": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_8bit",
        "distilled": True,
        "steps": 8,
    },
    "Krea 2 Turbo β€” SDNQ int4 (~10GB)": {
        "repo": "OzzyGT/Krea_2_Turbo_sdnq_dynamic_4bit",
        "distilled": True,
        "steps": 8,
    },
}
DEFAULT_MODEL = "Krea 2 Turbo β€” SDNQ int8 (~18GB)"

MODE_LORAS = {
    "off": ("", ""),
    "append": (
        "ostris/krea2_turbo_style_reference",
        "krea2_style_reference.safetensors",
    ),
    "prepend": (
        "conradlocke/krea2-identity-edit",
        "krea2_identity_edit_v1_2.safetensors",
    ),
}

COMPONENTS = ["text_encoder", "tokenizer", "transformer", "vae", "scheduler"]
MAX_SEQUENCE_LENGTH = 512
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# Saturated on purpose: the composite fallback in `_split_editor` diffs strokes against the photo under them.
BRUSH_COLOR = "#ff00ff"

MODE_LABELS = {
    "Vision only": "off",
    "Style reference": "append",
    "Identity edit": "prepend",
}

MASK_MODE_LABELS = {
    "Exclude, blank tokens": "exclude_blank",
    "Exclude, keep tokens": "exclude",
    "De-emphasize only": "deemphasize",
}


# ----------------------------------------------------------------------------- pipeline lifecycle
# One pipeline resident at a time; switching the checkpoint reloads.
_current = {"id": None, "pipe": None, "lora": None, "distilled": False}


def _resolve(choice: str) -> str:
    """Map a dropdown label to its repo id; anything else is used verbatim."""
    preset = MODELS.get(choice)
    return preset["repo"] if preset else choice


def _load(checkpoint: str, distilled: bool):
    import sdnq  # noqa: F401  registers the SDNQ backend with transformers so quantized checkpoints load

    # `auto_map` names the distilled blockset, so the base one is pulled off the loaded module by name.
    blocks = ModularPipelineBlocks.from_pretrained(BLOCK_REPO, trust_remote_code=True)
    blocks_name = "Krea2TurboReferenceAutoBlocks" if distilled else "Krea2ReferenceAutoBlocks"
    blocks = getattr(sys.modules[type(blocks).__module__], blocks_name)()
    manager = ComponentsManager()
    # The block repo's modular index supplies the per-component subfolders; `load_components` overrides
    # which repo they load from.
    pipe = blocks.init_pipeline(BLOCK_REPO, components_manager=manager)
    pipe.load_components(names=COMPONENTS, pretrained_model_name_or_path=checkpoint, dtype=torch.bfloat16)

    if IS_ZERO_GPU:
        # ZeroGPU hands out a large GPU for the duration of the call, so no offloading is needed.
        pipe.to(DEVICE)
    elif DEVICE == "cuda":
        manager.enable_auto_cpu_offload(device=DEVICE)
    return pipe


def get_pipe(choice: str):
    checkpoint = _resolve(choice)
    preset = MODELS.get(choice)
    distilled = bool(preset["distilled"]) if preset else _current.get("distilled", False)
    if _current["id"] != (checkpoint, distilled):
        _current.update(id=None, pipe=None, lora=None)
        gc.collect()
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
        _current.update(
            id=(checkpoint, distilled),
            distilled=distilled,
            pipe=_load(checkpoint, distilled),
        )
        print(f"loaded {checkpoint}")
    return _current["pipe"]


# On ZeroGPU the platform owns the GPU between calls, so preloading costs nothing. Locally nothing loads
# until the Load button, so this Space does not squat on VRAM while another app is running.
if IS_ZERO_GPU:
    get_pipe(DEFAULT_MODEL)


@spaces.GPU(duration=120)
def load_model(choice):
    get_pipe(choice)
    return f"βœ… Loaded: {_resolve(choice)}"


def unload_model():
    _current.update(id=None, pipe=None, lora=None)
    gc.collect()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
    return "βšͺ No model loaded"


def _sync_lora(pipe, repo: str, weight_name: str):
    """Load, swap or drop the edit LoRA."""
    repo, weight_name = (repo or "").strip(), (weight_name or "").strip()
    key = (repo, weight_name)
    if _current["lora"] == key:
        return
    try:
        pipe.unload_lora_weights()
    except Exception:
        pass
    if repo:
        pipe.load_lora_weights(repo, **({"weight_name": weight_name} if weight_name else {}))
    _current["lora"] = key


# ----------------------------------------------------------------------------- reference
def _split_editor(value):
    """Pull `(reference, mask)` out of an ImageEditor value.

    The painted layer's alpha channel is the mask, in the blocks' polarity: painted is the part of the
    reference to use. A mask of `None` means "no mask", not "use nothing".
    """
    if not value:
        return None, None
    background = value.get("background") if isinstance(value, dict) else None
    if background is None:
        return None, None

    mask = None
    for layer in value.get("layers") or []:
        if layer is None:
            continue
        alpha = Image.fromarray(np.array(layer.convert("RGBA"))[:, :, 3], mode="L")
        if alpha.getextrema()[1] == 0:
            continue
        mask = alpha if mask is None else ImageChops.lighter(mask, alpha)

    # Fallback for an editor build that flattens strokes into the composite instead of keeping a layer:
    # the painted region is then wherever the composite differs from the background.
    composite = value.get("composite")
    if mask is None and composite is not None and composite.size == background.size:
        diff = ImageChops.difference(composite.convert("RGB"), background.convert("RGB")).convert("L")
        if diff.getextrema()[1] > 8:
            mask = diff.point(lambda v: 255 if v > 8 else 0)

    return background.convert("RGB"), mask


class _StepProgress:
    """Stands in for the loop block's tqdm so only the denoise steps reach the Gradio bar."""

    def __init__(self, progress, total):
        self._progress = progress
        self._total = max(1, int(total or 1))
        self._step = 0

    def __enter__(self):
        self._progress(0.0, desc=f"Step 0/{self._total}")
        return self

    def __exit__(self, *exc):
        return False

    def update(self, n=1):
        self._step = min(self._step + int(n), self._total)
        self._progress(self._step / self._total, desc=f"Step {self._step}/{self._total}")


def _report_steps(blocks, progress):
    """Swap `progress_bar` on every denoise loop block for one that drives `progress`.

    Modular pipelines expose no per-step callback, but `progress_bar` is a plain method on
    `LoopSequentialPipelineBlocks`, so an instance attribute shadows it.
    """
    for block in (blocks.sub_blocks or {}).values():
        if hasattr(type(block), "progress_bar"):
            block.progress_bar = lambda iterable=None, total=None, _p=progress: _StepProgress(_p, total)
        _report_steps(block, progress)


# ----------------------------------------------------------------------------- generate
@spaces.GPU(duration=180)
def generate(
    model_choice,
    prompt,
    mode_label,
    mask_mode_label,
    mask_reference_latents,
    reference,
    subject,
    style,
    grounding_px,
    width,
    height,
    steps,
    seed,
    randomize_seed,
    progress=gr.Progress(),
):
    progress(0.0, desc="Loading model")
    pipe = get_pipe(model_choice)
    # `pipe.blocks` hands back a deepcopy; the pipeline runs `_blocks`, so the shim has to go there.
    _report_steps(pipe._blocks, progress)

    mode = MODE_LABELS[mode_label]
    # The LoRA follows the reference mode: each latent mode only works with the adapter it was trained for.
    _sync_lora(pipe, *MODE_LORAS[mode])

    image, mask = _split_editor(reference)
    if mode != "off" and image is None:
        raise gr.Error(f"`{mode}` needs a reference image; add one or switch to vision-only.")

    if randomize_seed:
        seed = int(torch.randint(0, 2**31 - 1, (1,)).item())
    generator = torch.Generator("cpu").manual_seed(int(seed))

    kwargs = {
        "prompt": prompt or "",
        "height": int(height),
        "width": int(width),
        "num_inference_steps": int(steps),
        "max_sequence_length": MAX_SEQUENCE_LENGTH,
        "generator": generator,
        "output_type": "pil",
    }
    if image is not None:
        kwargs.update(
            reference_images=image,
            reference_mode=mode,
            reference_subject_strength=float(subject),
            reference_style_strength=float(style),
            grounding_px=int(grounding_px),
            mask_reference_latents=bool(mask_reference_latents),
        )
        if mask is not None:
            kwargs["reference_masks"] = mask
            kwargs["reference_mask_mode"] = MASK_MODE_LABELS[mask_mode_label]

    images = pipe(**kwargs).values["images"]
    return images[0], int(seed)


# ----------------------------------------------------------------------------- UI
with gr.Blocks(title="Krea 2 β€” reference conditioning") as demo:
    gr.Markdown(
        "# Krea 2 β€” reference-image conditioning\n"
        "Vision-path references on the stock checkpoint, plus the two clean-latent edit-LoRA modes, via the "
        f"[`{BLOCK_REPO}`](https://huggingface.co/{BLOCK_REPO}) modular blocks."
    )

    with gr.Row():
        with gr.Column(scale=1):
            prompt = gr.Textbox(label="Prompt", lines=3, placeholder="a photograph of ...")

            with gr.Row():
                mode = gr.Dropdown(
                    choices=list(MODE_LABELS),
                    value="Vision only",
                    label="Reference mode",
                )
                mask_mode = gr.Dropdown(
                    choices=list(MASK_MODE_LABELS),
                    value="Exclude, blank tokens",
                    label="Mask behaviour",
                )

            reference = gr.ImageEditor(
                label="Reference β€” paint the part to use",
                type="pil",
                layers=False,
                brush=gr.Brush(colors=[BRUSH_COLOR], default_color=BRUSH_COLOR, color_mode="fixed"),
                height=360,
            )

            with gr.Row():
                subject = gr.Slider(0.0, 1.5, value=1.0, step=0.05, label="Subject")
                style = gr.Slider(0.0, 1.5, value=1.0, step=0.05, label="Style")

            mask_reference_latents = gr.Checkbox(value=False, label="Mask the reference latents")

            with gr.Accordion("Advanced", open=False):
                with gr.Row():
                    width = gr.Slider(512, 2048, value=1024, step=32, label="Width")
                    height = gr.Slider(512, 2048, value=1024, step=32, label="Height")
                with gr.Row():
                    steps = gr.Slider(
                        1,
                        60,
                        value=MODELS[DEFAULT_MODEL]["steps"],
                        step=1,
                        label="Steps",
                    )
                    grounding_px = gr.Slider(0, 1536, value=768, step=64, label="Grounding px (prepend)")
                with gr.Row():
                    seed = gr.Number(value=0, precision=0, label="Seed")
                    randomize_seed = gr.Checkbox(value=True, label="Randomize")

        with gr.Column(scale=1):
            model_choice = gr.Dropdown(choices=list(MODELS), value=DEFAULT_MODEL, label="Model")
            with gr.Row(visible=not IS_ZERO_GPU):
                load_btn = gr.Button("Load model")
                unload_btn = gr.Button("Unload")
            status = gr.Markdown("🟒 Ready (ZeroGPU)" if IS_ZERO_GPU else "βšͺ No model loaded")

            run = gr.Button("Generate", variant="primary")
            gallery = gr.Image(label="Result", type="pil", format="png", interactive=False)
            used_seed = gr.Number(label="Seed used", precision=0, interactive=False)

    def on_model_change(choice):
        """A preset carries its own schedule; a custom path leaves whatever is set alone."""
        preset = MODELS.get(choice)
        return gr.update() if preset is None else gr.update(value=preset["steps"])

    model_choice.change(on_model_change, inputs=[model_choice], outputs=[steps])

    load_btn.click(load_model, inputs=[model_choice], outputs=[status])
    unload_btn.click(unload_model, outputs=[status])
    run.click(
        generate,
        inputs=[
            model_choice,
            prompt,
            mode,
            mask_mode,
            mask_reference_latents,
            reference,
            subject,
            style,
            grounding_px,
            width,
            height,
            steps,
            seed,
            randomize_seed,
        ],
        outputs=[gallery, used_seed],
    )


if __name__ == "__main__":
    demo.queue().launch()