multimodalart HF Staff commited on
Commit
6a4beb4
·
verified ·
1 Parent(s): 981fef6

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/man_beach.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/woman.jpg filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,49 @@
1
  ---
2
- title: Krea2 Identity Edit
3
- emoji: 📚
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Krea 2 Identity Edit
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.20.0
 
8
  app_file: app.py
9
  pinned: false
10
+ hardware: zero-a10g
11
+ python_version: "3.10"
12
+ startup_duration_timeout: 45m
13
+ short_description: Identity-preserving instruction image editing on Krea 2
14
+ models:
15
+ - krea/Krea-2-Turbo
16
+ - conradlocke/krea2-identity-edit
17
  ---
18
 
19
+ # 🧬 Krea 2 Identity Edit
20
+
21
+ Instruction-based, **identity-preserving** image editing built on
22
+ [Krea 2 Turbo](https://huggingface.co/krea/Krea-2-Turbo) with the community LoRA
23
+ [`conradlocke/krea2-identity-edit`](https://huggingface.co/conradlocke/krea2-identity-edit).
24
+
25
+ Give it an image and a plain-language instruction; it edits while preserving what you
26
+ didn't ask to change — including the person's likeness. Person re-staging, local edits
27
+ (recolor / add / remove / replace), replace-with-reference, and full-image restyles.
28
+
29
+ ## How it works
30
+
31
+ Krea 2 is a text-to-image MMDiT; this LoRA turns it into an instruction editor via a
32
+ **dual-conditioning** recipe that stock `Krea2Pipeline` does not provide. This Space
33
+ reproduces the two custom pieces from the reference
34
+ [ComfyUI-Krea2Edit](https://github.com/lbouaraba/comfyui-krea2edit) node pack in plain
35
+ diffusers / PyTorch:
36
+
37
+ - **Grounded encode** — the instruction is encoded *together with the source image*
38
+ through the Qwen3-VL text encoder (image tokens inserted via the grounded chat
39
+ template), tapping the 12 selected decoder layers.
40
+ - **Source patch** — the VAE-encoded source latent is prepended to the transformer
41
+ sequence as clean tokens (RoPE frame index: source = 1, target = 0), so appearance is
42
+ carried in-context and only the target tokens are denoised.
43
+
44
+ Default recipe: Turbo, 8 steps, guidance off (Krea convention). `grounding_px` trades
45
+ edit adherence (lower) against identity/likeness (higher); 768 is balanced, 1024+ for
46
+ people. For removals / large deletions, raise steps and guidance.
47
+
48
+ *An unofficial community fine-tune of Krea 2 Raw — not affiliated with or endorsed by
49
+ Krea.ai, Inc. Weights are distributed under the Krea 2 Community License.*
app.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Krea 2 Identity Edit — instruction-based, identity-preserving image editing.
2
+
3
+ This Space runs the community LoRA `conradlocke/krea2-identity-edit` on top of
4
+ `krea/Krea-2-Turbo` (the distilled 8-step checkpoint). The LoRA is trained with a
5
+ *dual conditioning* recipe that stock text-to-image `Krea2Pipeline` does not
6
+ provide, so this app reproduces the two custom pieces from the reference
7
+ ComfyUI-Krea2Edit node pack (https://github.com/lbouaraba/comfyui-krea2edit) in
8
+ plain diffusers / PyTorch:
9
+
10
+ 1. Krea2EditGroundedEncode — the instruction is encoded *together with the
11
+ source image* through the Qwen3-VL text encoder (vision tokens inserted via
12
+ the image-grounded chat template), and the 12 selected decoder layers are
13
+ tapped, exactly like the text-only Krea 2 path but with the image grounding
14
+ the semantics ("the man on the left", "the sign in the back").
15
+
16
+ 2. Krea2EditModelPatch — the VAE-encoded SOURCE latent is prepended to the
17
+ transformer sequence as a block of *clean* tokens, distinguished from the
18
+ noisy target purely by the 3-axis RoPE frame index (source frame = 1,
19
+ target frame = 0, h/w aligned). The sequence becomes
20
+ [text | source(frame=1) | target(frame=0)] and only the target tokens are
21
+ kept as the velocity prediction — mirroring ai-toolkit's
22
+ `predict_velocity_edit`.
23
+
24
+ Everything runs on ZeroGPU: modules go on CUDA at module scope, inference is
25
+ wrapped in @spaces.GPU, no torch.compile, no CPU offload.
26
+ """
27
+
28
+ import os
29
+ import random
30
+
31
+ import numpy as np
32
+ import spaces
33
+ import torch
34
+ import gradio as gr
35
+ from PIL import Image
36
+
37
+ from diffusers import Krea2Pipeline
38
+ from diffusers.pipelines.krea2.pipeline_krea2 import retrieve_timesteps
39
+ from transformers import AutoProcessor
40
+
41
+ # --------------------------------------------------------------------------------------
42
+ # Constants
43
+ # --------------------------------------------------------------------------------------
44
+ BASE_MODEL = "krea/Krea-2-Turbo"
45
+ LORA_REPO = "conradlocke/krea2-identity-edit"
46
+ LORA_WEIGHT = "krea2_identity_edit_v1.safetensors"
47
+ DTYPE = torch.bfloat16
48
+ MAX_SEED = np.iinfo(np.int32).max
49
+
50
+ # Turbo recipe for "most edits" (add / recolor / restyle / re-stage), per the LoRA card:
51
+ # Turbo, 8 steps, CFG 1.0 (== guidance disabled in the Krea convention).
52
+ DEFAULT_STEPS = 8
53
+ DEFAULT_GUIDANCE = 0.0 # Krea convention: 0.0 disables guidance
54
+ DEFAULT_GROUNDING_PX = 768 # trained dial 512-1536; 768 balanced, 1024+ for people
55
+ DEFAULT_LORA_SCALE = 1.0
56
+ MAX_MEGAPIXELS = 2.0 # card: generate at <= 2MP
57
+
58
+ # The image-grounded instruction template from ComfyUI-Krea2Edit. The system
59
+ # prefix is byte-identical to the diffusers Krea 2 text template; the difference
60
+ # is the <|vision_start|><|image_pad|><|vision_end|> block inserted before the
61
+ # instruction so the VLM grounds the edit on the source image.
62
+ GROUNDED_TEMPLATE = (
63
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, "
64
+ "texture, quantity, text, spatial relationships of the objects and background:"
65
+ "<|im_end|>\n<|im_start|>user\n<|vision_start|><|image_pad|><|vision_end|>"
66
+ "{}<|im_end|>\n<|im_start|>assistant\n"
67
+ )
68
+
69
+ # --------------------------------------------------------------------------------------
70
+ # Load the pipeline (module scope, eager CUDA placement — required for ZeroGPU)
71
+ # --------------------------------------------------------------------------------------
72
+ pipe = Krea2Pipeline.from_pretrained(BASE_MODEL, torch_dtype=DTYPE)
73
+
74
+ # Load the identity-edit LoRA onto the transformer (Krea 2 LoRAs load through the
75
+ # transformer's adapter API, per the base-model reference and the LoRA cards).
76
+ pipe.transformer.load_lora_adapter(LORA_REPO, weight_name=LORA_WEIGHT)
77
+ pipe.transformer.set_adapters("default", weights=DEFAULT_LORA_SCALE)
78
+
79
+ pipe.to("cuda")
80
+
81
+ # A Qwen3-VL processor for the grounded (image + text) encode. The Krea 2 repo
82
+ # ships only a text tokenizer; the vision-side preprocessing (image mean/std,
83
+ # 16px patch, spatial-merge=2) comes from the Qwen3-VL processor. We reuse the
84
+ # Krea 2 tokenizer's special tokens by aligning the template above with the
85
+ # processor's <|image_pad|> expansion.
86
+ try:
87
+ processor = AutoProcessor.from_pretrained("Qwen/Qwen3-VL-4B-Instruct")
88
+ except Exception as e: # pragma: no cover - surfaced in logs if it happens
89
+ print(f"[warn] could not load Qwen3-VL processor, falling back: {e}")
90
+ processor = None
91
+
92
+ # VAE latent normalization stats (the transformer works in normalized latent
93
+ # space; randn target latents already live there, so the source must be
94
+ # normalized the same way: (z - mean) * std).
95
+ _LATENTS_MEAN = (
96
+ torch.tensor(pipe.vae.config.latents_mean).view(1, pipe.vae.config.z_dim, 1, 1, 1)
97
+ )
98
+ _LATENTS_STD = (
99
+ torch.tensor(pipe.vae.config.latents_std).view(1, pipe.vae.config.z_dim, 1, 1, 1)
100
+ )
101
+
102
+
103
+ # --------------------------------------------------------------------------------------
104
+ # Grounded instruction encoding (semantic path)
105
+ # --------------------------------------------------------------------------------------
106
+ def _grounded_encode(instruction: str, source: Image.Image, grounding_px: int):
107
+ """Encode the instruction grounded on the source image through Qwen3-VL.
108
+
109
+ Returns (prompt_embeds, prompt_embeds_mask) shaped like the diffusers
110
+ Krea 2 text conditioning: (1, seq, num_text_layers, text_hidden_dim) and
111
+ (1, seq).
112
+ """
113
+ device = pipe._execution_device
114
+ select_layers = pipe.text_encoder_select_layers
115
+ prefix_idx = pipe.prompt_template_encode_start_idx # 34: drop the system prefix
116
+
117
+ # Cap the longest side fed to the VLM (the LoRA trained with 384-768px jitter).
118
+ img = source.convert("RGB")
119
+ if grounding_px and max(img.size) > grounding_px:
120
+ s = grounding_px / max(img.size)
121
+ img = img.resize(
122
+ (max(16, round(img.size[0] * s)), max(16, round(img.size[1] * s))),
123
+ Image.LANCZOS,
124
+ )
125
+
126
+ text = GROUNDED_TEMPLATE.format(instruction or "")
127
+
128
+ inputs = processor(
129
+ text=[text],
130
+ images=[img],
131
+ padding=True,
132
+ return_tensors="pt",
133
+ ).to(device)
134
+
135
+ outputs = pipe.text_encoder(
136
+ input_ids=inputs["input_ids"],
137
+ attention_mask=inputs.get("attention_mask"),
138
+ pixel_values=inputs.get("pixel_values"),
139
+ image_grid_thw=inputs.get("image_grid_thw"),
140
+ output_hidden_states=True,
141
+ )
142
+ hidden_states = torch.stack(
143
+ [outputs.hidden_states[i] for i in select_layers], dim=2
144
+ ) # (1, seq, num_layers, dim)
145
+
146
+ attention_mask = inputs.get("attention_mask")
147
+ if attention_mask is None:
148
+ attention_mask = torch.ones(
149
+ hidden_states.shape[:2], device=device, dtype=torch.bool
150
+ )
151
+ else:
152
+ attention_mask = attention_mask.bool()
153
+
154
+ # Drop the system-prefix tokens (identical prefix to the text-only path).
155
+ hidden_states = hidden_states[:, prefix_idx:]
156
+ attention_mask = attention_mask[:, prefix_idx:]
157
+ return hidden_states.to(DTYPE), attention_mask
158
+
159
+
160
+ def _text_only_encode(instruction: str):
161
+ """Fallback text-only encode if the processor is unavailable."""
162
+ return pipe.encode_prompt(prompt=instruction, device=pipe._execution_device)
163
+
164
+
165
+ # --------------------------------------------------------------------------------------
166
+ # Source-preservation forward (appearance path)
167
+ # --------------------------------------------------------------------------------------
168
+ def _encode_source_latent(source: Image.Image, height: int, width: int):
169
+ """VAE-encode the source image to a packed, normalized latent block matching
170
+ the target grid, ready to prepend to the transformer sequence."""
171
+ device = pipe._execution_device
172
+ # Preprocess to the target resolution (training pairs are same-size; the card
173
+ # says match output AR to the source, which we enforce upstream).
174
+ px = pipe.image_processor.preprocess(source.convert("RGB"), height=height, width=width)
175
+ px = px.unsqueeze(2).to(device=device, dtype=pipe.vae.dtype) # (B,C,1,H,W)
176
+
177
+ latent = pipe.vae.encode(px).latent_dist.mode() # (B, z, 1, lh, lw), unnormalized
178
+ mean = _LATENTS_MEAN.to(latent.device, latent.dtype)
179
+ std = _LATENTS_STD.to(latent.device, latent.dtype)
180
+ latent = (latent - mean) * std # normalized latent space
181
+ latent = latent[:, :, 0] # (B, z, lh, lw)
182
+
183
+ b, c, lh, lw = latent.shape
184
+ packed = pipe._pack_latents(latent, b, c, lh, lw) # (B, lh*lw/p^2, c*p*p)
185
+ return packed.to(DTYPE)
186
+
187
+
188
+ def _edit_position_ids(text_seq_len, grid_h, grid_w, n_src, device):
189
+ """Build (text + n_src*grid + grid, 3) rotary coords:
190
+ text @ (0,0,0); each source block @ frame=(i+1) with (h,w); target @ frame=0.
191
+ """
192
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
193
+
194
+ def _img_ids(frame):
195
+ ids = torch.zeros(grid_h, grid_w, 3, device=device)
196
+ ids[..., 0] = frame
197
+ ids[..., 1] = torch.arange(grid_h, device=device)[:, None]
198
+ ids[..., 2] = torch.arange(grid_w, device=device)[None, :]
199
+ return ids.reshape(grid_h * grid_w, 3)
200
+
201
+ blocks = [text_ids]
202
+ blocks += [_img_ids(i + 1) for i in range(n_src)] # sources frame=1..N
203
+ blocks += [_img_ids(0)] # target frame=0
204
+ return torch.cat(blocks, dim=0)
205
+
206
+
207
+ def _edit_transformer_forward(latents, src_packed, prompt_embeds, prompt_mask,
208
+ timestep, position_ids):
209
+ """Run the Krea 2 transformer with the source latent block prepended, keeping
210
+ only the target tokens out. Reproduces ComfyUI-Krea2Edit's krea2_edit_forward
211
+ against the diffusers Krea2Transformer2DModel (img_in == m.first,
212
+ transformer_blocks == m.blocks, text_fusion/txt_in == m.txtfusion/m.txtmlp,
213
+ rotary_emb == m.pe_embedder, final_layer == m.last)."""
214
+ m = pipe.transformer
215
+ combined_img = torch.cat([src_packed, latents], dim=1) # [source | target] packed
216
+
217
+ temb = m.time_embed(timestep, dtype=latents.dtype)
218
+ temb_mod = m.time_mod_proj(torch.nn.functional.gelu(temb, approximate="tanh"))
219
+
220
+ # Text fusion + projection (attention mask over text only; all image tokens valid).
221
+ text_attn_mask = prompt_mask[:, None, None, :] if prompt_mask is not None else None
222
+ enc = m.text_fusion(prompt_embeds, attention_mask=text_attn_mask)
223
+ enc = m.txt_in(enc)
224
+
225
+ img = m.img_in(combined_img)
226
+ hidden = torch.cat([enc, img], dim=1) # [text | source | target]
227
+
228
+ image_rotary_emb = m.rotary_emb(position_ids)
229
+
230
+ # Attention mask: text uses its key-padding mask, all image tokens (src+tgt) valid.
231
+ attention_mask = None
232
+ if prompt_mask is not None:
233
+ img_ones = prompt_mask.new_ones((combined_img.shape[0], combined_img.shape[1]))
234
+ attention_mask = torch.cat([prompt_mask, img_ones], dim=1)[:, None, None, :]
235
+
236
+ for block in m.transformer_blocks:
237
+ hidden = block(hidden, temb_mod, image_rotary_emb, attention_mask)
238
+
239
+ text_seq_len = enc.shape[1]
240
+ tgt_len = latents.shape[1]
241
+ hidden = hidden[:, text_seq_len:] # drop text -> [source | target]
242
+ hidden = hidden[:, -tgt_len:] # keep target tokens only
243
+ return m.final_layer(hidden, temb)
244
+
245
+
246
+ # --------------------------------------------------------------------------------------
247
+ # Sizing helpers
248
+ # --------------------------------------------------------------------------------------
249
+ def _target_size(source: Image.Image):
250
+ """Match the output AR to the source (card requirement) and cap at ~2MP,
251
+ snapping each side to a multiple of vae_scale_factor * patch_size."""
252
+ multiple = pipe.vae_scale_factor * pipe.patch_size # 8 * 2 = 16
253
+ w, h = source.size
254
+ mp = (w * h) / 1e6
255
+ if mp > MAX_MEGAPIXELS:
256
+ s = (MAX_MEGAPIXELS / mp) ** 0.5
257
+ w, h = round(w * s), round(h * s)
258
+ w = max(multiple, (w // multiple) * multiple)
259
+ h = max(multiple, (h // multiple) * multiple)
260
+ return h, w
261
+
262
+
263
+ # --------------------------------------------------------------------------------------
264
+ # Inference
265
+ # --------------------------------------------------------------------------------------
266
+ @spaces.GPU(duration=90)
267
+ def edit(
268
+ source_image,
269
+ instruction,
270
+ grounding_px=DEFAULT_GROUNDING_PX,
271
+ lora_scale=DEFAULT_LORA_SCALE,
272
+ steps=DEFAULT_STEPS,
273
+ guidance_scale=DEFAULT_GUIDANCE,
274
+ seed=0,
275
+ randomize_seed=True,
276
+ progress=gr.Progress(track_tqdm=True),
277
+ ):
278
+ if source_image is None:
279
+ raise gr.Error("Please upload a source image to edit.")
280
+ if not instruction or not instruction.strip():
281
+ raise gr.Error("Please describe the edit you want (e.g. 'put this person at a night market').")
282
+
283
+ if randomize_seed:
284
+ seed = random.randint(0, MAX_SEED)
285
+ seed = int(seed)
286
+
287
+ device = pipe._execution_device
288
+ source = source_image if isinstance(source_image, Image.Image) else Image.fromarray(source_image)
289
+ source = source.convert("RGB")
290
+
291
+ pipe.transformer.set_adapters("default", weights=float(lora_scale))
292
+
293
+ height, width = _target_size(source)
294
+
295
+ # --- Semantic path: grounded instruction encode -----------------------------------
296
+ if processor is not None:
297
+ prompt_embeds, prompt_mask = _grounded_encode(instruction.strip(), source, int(grounding_px))
298
+ else:
299
+ prompt_embeds, prompt_mask = _text_only_encode(instruction.strip())
300
+
301
+ do_cfg = float(guidance_scale) > 0
302
+ if do_cfg:
303
+ # At CFG > 1 the card says ground the negative too: empty prompt, same image.
304
+ if processor is not None:
305
+ neg_embeds, neg_mask = _grounded_encode("", source, int(grounding_px))
306
+ else:
307
+ neg_embeds, neg_mask = _text_only_encode("")
308
+
309
+ # --- Appearance path: encode + pack the source latent -----------------------------
310
+ src_packed = _encode_source_latent(source, height, width)
311
+
312
+ # --- Prepare noisy target latents -------------------------------------------------
313
+ num_channels_latents = pipe.transformer.config.in_channels // (pipe.patch_size ** 2)
314
+ generator = torch.Generator(device=device).manual_seed(seed)
315
+ latents = pipe.prepare_latents(
316
+ 1, num_channels_latents, height, width, DTYPE, device, generator, None
317
+ )
318
+
319
+ grid_h = height // (pipe.vae_scale_factor * pipe.patch_size)
320
+ grid_w = width // (pipe.vae_scale_factor * pipe.patch_size)
321
+ position_ids = _edit_position_ids(prompt_embeds.shape[1], grid_h, grid_w, 1, device)
322
+
323
+ # --- Timesteps (distilled schedule: fixed mu = 1.15) ------------------------------
324
+ sigmas = np.linspace(1.0, 1 / int(steps), int(steps))
325
+ timesteps, num_steps = retrieve_timesteps(
326
+ pipe.scheduler, int(steps), device, sigmas=sigmas, mu=1.15
327
+ )
328
+
329
+ # --- Denoising loop ---------------------------------------------------------------
330
+ pipe.scheduler.set_begin_index(0)
331
+ for t in progress.tqdm(timesteps, desc="Editing"):
332
+ timestep = (t / pipe.scheduler.config.num_train_timesteps).expand(latents.shape[0]).to(latents.dtype)
333
+
334
+ noise_pred = _edit_transformer_forward(
335
+ latents, src_packed, prompt_embeds, prompt_mask, timestep, position_ids
336
+ )
337
+ if do_cfg:
338
+ neg_position_ids = _edit_position_ids(neg_embeds.shape[1], grid_h, grid_w, 1, device)
339
+ neg_pred = _edit_transformer_forward(
340
+ latents, src_packed, neg_embeds, neg_mask, timestep, neg_position_ids
341
+ )
342
+ noise_pred = noise_pred + float(guidance_scale) * (noise_pred - neg_pred)
343
+
344
+ latents = pipe.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
345
+
346
+ # --- Decode -----------------------------------------------------------------------
347
+ latents = pipe._unpack_latents(latents, height, width).to(pipe.vae.dtype)
348
+ mean = _LATENTS_MEAN.to(latents.device, latents.dtype)
349
+ std_recip = (1.0 / _LATENTS_STD).to(latents.device, latents.dtype)
350
+ latents = latents * std_recip + mean
351
+ image = pipe.vae.decode(latents, return_dict=False)[0][:, :, 0]
352
+ image = pipe.image_processor.postprocess(image, output_type="pil")[0]
353
+
354
+ return image, seed
355
+
356
+
357
+ # --------------------------------------------------------------------------------------
358
+ # UI
359
+ # --------------------------------------------------------------------------------------
360
+ CSS = """
361
+ #col-container { max-width: 1100px; margin: 0 auto; }
362
+ """
363
+
364
+ DESCRIPTION = """
365
+ # 🧬 Krea 2 Identity Edit
366
+
367
+ Instruction-based, **identity-preserving** image editing on
368
+ [Krea 2 Turbo](https://huggingface.co/krea/Krea-2-Turbo) with the community LoRA
369
+ [`conradlocke/krea2-identity-edit`](https://huggingface.co/conradlocke/krea2-identity-edit).
370
+
371
+ Upload a photo of a person (or any scene), type a plain-language instruction, and the
372
+ model edits it while preserving what you didn't ask to change — **including the face**.
373
+ Try *"put this person at a busy night market"*, *"change the jacket to red leather"*, or
374
+ *"make it a vintage film photo"*.
375
+
376
+ This demo reproduces the dual-conditioning recipe (in-context VAE source tokens +
377
+ image-grounded Qwen3-VL encoding) from the reference
378
+ [ComfyUI-Krea2Edit](https://github.com/lbouaraba/comfyui-krea2edit) node pack.
379
+ """
380
+
381
+ with gr.Blocks(css=CSS, title="Krea 2 Identity Edit") as demo:
382
+ with gr.Column(elem_id="col-container"):
383
+ gr.Markdown(DESCRIPTION)
384
+
385
+ with gr.Row(equal_height=True):
386
+ with gr.Column():
387
+ source_image = gr.Image(label="Source image", type="pil", height=420)
388
+ instruction = gr.Textbox(
389
+ label="Edit instruction",
390
+ placeholder="e.g. create a photo of this person at a night market",
391
+ lines=2,
392
+ )
393
+ run_button = gr.Button("Edit", variant="primary", size="lg")
394
+
395
+ with gr.Accordion("Advanced settings", open=False):
396
+ grounding_px = gr.Slider(
397
+ label="Grounding resolution (px)",
398
+ minimum=512, maximum=1536, step=64, value=DEFAULT_GROUNDING_PX,
399
+ info="Lower = stronger edit adherence; higher = stronger identity/likeness. Try 1024+ for people.",
400
+ )
401
+ lora_scale = gr.Slider(
402
+ label="LoRA strength",
403
+ minimum=0.0, maximum=1.5, step=0.05, value=DEFAULT_LORA_SCALE,
404
+ )
405
+ steps = gr.Slider(
406
+ label="Steps",
407
+ minimum=4, maximum=28, step=1, value=DEFAULT_STEPS,
408
+ info="Turbo default is 8. For removals/large deletions try more steps + guidance > 0.",
409
+ )
410
+ guidance_scale = gr.Slider(
411
+ label="Guidance (0 = off, Turbo default)",
412
+ minimum=0.0, maximum=5.0, step=0.5, value=DEFAULT_GUIDANCE,
413
+ info="Krea convention: 0 disables guidance. Raise (e.g. 3) for removals/large edits.",
414
+ )
415
+ with gr.Row():
416
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
417
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
418
+
419
+ with gr.Column():
420
+ result = gr.Image(label="Edited image", height=420)
421
+ used_seed = gr.Number(label="Seed used", interactive=False)
422
+
423
+ gr.Examples(
424
+ examples=[
425
+ ["examples/woman.jpg", "create a photo of this person at a busy night market at night"],
426
+ ["examples/businessman_suit.jpg", "change the suit jacket to a red leather jacket"],
427
+ ["examples/man_beach.jpg", "make it a vintage film photo with warm golden-hour light"],
428
+ ],
429
+ inputs=[source_image, instruction],
430
+ outputs=[result, used_seed],
431
+ fn=edit,
432
+ cache_examples=True,
433
+ cache_mode="lazy",
434
+ )
435
+
436
+ gr.on(
437
+ triggers=[run_button.click, instruction.submit],
438
+ fn=edit,
439
+ inputs=[source_image, instruction, grounding_px, lora_scale, steps, guidance_scale, seed, randomize_seed],
440
+ outputs=[result, used_seed],
441
+ )
442
+
443
+ if __name__ == "__main__":
444
+ demo.launch(theme=gr.themes.Citrus(), css=CSS)
examples/businessman_suit.jpg ADDED
examples/man_beach.jpg ADDED

Git LFS Details

  • SHA256: c06f6906ef2e94c56221cff67268e2a8243b2f0ff9b1626081bbbea729a6e15c
  • Pointer size: 131 Bytes
  • Size of remote file: 333 kB
examples/woman.jpg ADDED

Git LFS Details

  • SHA256: bd50dea7cae6d4e8dae99a3478aa28b237c7b46c41a081899f04fba9673d445b
  • Pointer size: 131 Bytes
  • Size of remote file: 128 kB
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/huggingface/diffusers
2
+ transformers
3
+ accelerate
4
+ peft
5
+ safetensors
6
+ sentencepiece
7
+ torchvision
8
+ Pillow
9
+ numpy