Fataj commited on
Commit
f321dd2
·
verified ·
1 Parent(s): d52db3e

Upload 4 files

Browse files
Files changed (4) hide show
  1. .gitkeep +0 -0
  2. INSTALL.txt +13 -0
  3. README.md +8 -18
  4. app.py +882 -0
.gitkeep ADDED
File without changes
INSTALL.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ TtC fixed package
2
+ =================
3
+
4
+ Replace your current app.py with this app.py.
5
+ Add your .safetensors LoRAs under ./loras/.
6
+ Keep your existing requirements.txt, README.md, examples/, and other Space files.
7
+
8
+ Added:
9
+ - automatic ./loras scan
10
+ - multiselect LoRA dropdown in Generate tab
11
+ - shared LoRA strength control
12
+ - sync to /tmp/ComfyUI/models/loras
13
+ - chained LoraLoaderModelOnly nodes before KSampler
README.md CHANGED
@@ -1,21 +1,11 @@
1
- ---
2
- title: Redcraft Krea 2
3
- emoji: 🎨
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.15.1
8
- app_file: app.py
9
- short_description: Redcraft Krea2 ComfyUI text-to-image
10
- python_version: '3.12'
11
- startup_duration_timeout: 1h
12
- ---
13
 
14
- Private Gradio text-to-image app for the Redcraft Krea2 checkpoint from `Daankular/redcraft-krea2-fp8`.
15
 
16
- The app uses ComfyUI-native `CheckpointLoaderSimple` loading instead of Diffusers single-file conversion.
 
 
 
17
 
18
- Three tabs: **Generate** (text-to-image), **Edit Image** (denoise-based img2img), and
19
- **Identity Edit** instruction-based, identity-preserving editing powered by the community
20
- LoRA [`conradlocke/krea2-identity-edit`](https://huggingface.co/conradlocke/krea2-identity-edit)
21
- via the [ComfyUI-Krea2Edit](https://github.com/lbouaraba/comfyui-krea2edit) node pack.
 
1
+ # LoRA folder
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ Upload Krea-2-compatible .safetensors LoRA files into this folder.
4
 
5
+ Examples:
6
+ - Krea2_sbgrid.safetensors
7
+ - Akira Toriyama Krea.safetensors
8
+ - sxz-chuckartt-krea2-v1_epoch_10.safetensors
9
 
10
+ The patched app.py scans this folder and shows the files in the Generate tab LoRAs dropdown.
11
+ After adding or replacing LoRA files in the Hugging Face Space repository, commit the changes and let the Space restart/rebuild.
 
 
app.py ADDED
@@ -0,0 +1,882 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import random
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import threading
8
+ import time
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ import gradio as gr
13
+ import requests
14
+ import spaces
15
+ from huggingface_hub import hf_hub_download
16
+ from PIL import Image
17
+
18
+
19
+ MODEL_REPO = "Daankular/redcraft-krea2-fp8"
20
+ MODEL_FILE = "redcraftKREA2RedMix_krea2Edition.safetensors"
21
+ COMFY_KREA_REPO = "Comfy-Org/Krea-2"
22
+ TEXT_ENCODER_FILE = "qwen3vl_4b_bf16.safetensors"
23
+ VAE_FILE = "qwen_image_vae.safetensors"
24
+ IDENTITY_LORA_REPO = "conradlocke/krea2-identity-edit"
25
+ IDENTITY_LORA_FILE = "krea2_identity_edit_v1_2.safetensors"
26
+ KREA2EDIT_NODE_REPO = "https://github.com/lbouaraba/comfyui-krea2edit"
27
+ KREA2EDIT_NODE_DIRNAME = "comfyui-krea2edit"
28
+ COMFY_REPO = "https://github.com/comfyanonymous/ComfyUI.git"
29
+ COMFY_DIR = Path(os.environ.get("COMFYUI_DIR", "/tmp/ComfyUI"))
30
+ COMFY_HOST = "127.0.0.1"
31
+ COMFY_PORT = int(os.environ.get("COMFYUI_PORT", "8188"))
32
+ COMFY_URL = f"http://{COMFY_HOST}:{COMFY_PORT}"
33
+ MAX_SEED = 2**31 - 1
34
+ SPACE_LORA_DIR = Path(__file__).resolve().parent / "loras"
35
+
36
+ _comfy_lock = threading.Lock()
37
+ _comfy_process = None
38
+
39
+
40
+ def _run(cmd, cwd=None):
41
+ print("[setup]", " ".join(map(str, cmd)), flush=True)
42
+ subprocess.check_call(cmd, cwd=str(cwd) if cwd else None)
43
+
44
+
45
+ def _scan_space_loras():
46
+ """Return LoRA filenames uploaded to the Space repo under ./loras."""
47
+ SPACE_LORA_DIR.mkdir(parents=True, exist_ok=True)
48
+ return sorted(path.name for path in SPACE_LORA_DIR.glob("*.safetensors") if path.is_file())
49
+
50
+
51
+ def _sync_space_loras_to_comfy(lora_dir: Path):
52
+ """Copy repo LoRAs into ComfyUI's runtime LoRA folder."""
53
+ SPACE_LORA_DIR.mkdir(parents=True, exist_ok=True)
54
+ lora_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ for src in SPACE_LORA_DIR.glob("*.safetensors"):
57
+ if not src.is_file():
58
+ continue
59
+ dst = lora_dir / src.name
60
+ if (not dst.exists()) or src.stat().st_size != dst.stat().st_size or src.stat().st_mtime > dst.stat().st_mtime:
61
+ print(f"[lora] syncing {src.name}", flush=True)
62
+ shutil.copy2(src, dst)
63
+
64
+
65
+ def _refresh_lora_choices():
66
+ """Refresh the Gradio LoRA dropdown from files in ./loras."""
67
+ return gr.update(choices=_scan_space_loras(), value=[])
68
+
69
+
70
+ def _wait_for_comfy(timeout=180):
71
+ deadline = time.time() + timeout
72
+ last_error = None
73
+ while time.time() < deadline:
74
+ try:
75
+ response = requests.get(f"{COMFY_URL}/system_stats", timeout=2)
76
+ if response.ok:
77
+ return
78
+ except Exception as exc:
79
+ last_error = exc
80
+ time.sleep(1)
81
+ raise RuntimeError(f"ComfyUI did not start in time: {last_error}")
82
+
83
+
84
+ def _validate_comfyui():
85
+ response = requests.get(f"{COMFY_URL}/object_info", timeout=30)
86
+ response.raise_for_status()
87
+ object_info = response.json()
88
+ required_nodes = ["UNETLoader", "CLIPLoader", "VAELoader", "CLIPTextEncode", "KSampler", "VAEDecode", "SaveImage"]
89
+ missing = [node for node in required_nodes if node not in object_info]
90
+ if missing:
91
+ raise RuntimeError(f"ComfyUI is missing required nodes: {', '.join(missing)}")
92
+
93
+ unet_info = object_info["UNETLoader"]["input"]["required"]["unet_name"][0]
94
+ clip_info = object_info["CLIPLoader"]["input"]["required"]["clip_name"][0]
95
+ vae_info = object_info["VAELoader"]["input"]["required"]["vae_name"][0]
96
+ if MODEL_FILE not in unet_info:
97
+ raise RuntimeError(f"Redcraft diffusion model is not visible to ComfyUI. First models: {', '.join(unet_info[:10])}")
98
+ if TEXT_ENCODER_FILE not in clip_info:
99
+ raise RuntimeError(f"Krea2 text encoder is not visible to ComfyUI. First encoders: {', '.join(clip_info[:10])}")
100
+ if VAE_FILE not in vae_info:
101
+ raise RuntimeError(f"Krea2 VAE is not visible to ComfyUI. First VAEs: {', '.join(vae_info[:10])}")
102
+
103
+ identity_edit_nodes = ["LoraLoaderModelOnly", "Krea2EditModelPatch", "Krea2EditGroundedEncode", "EmptySD3LatentImage"]
104
+ missing_identity_nodes = [node for node in identity_edit_nodes if node not in object_info]
105
+ if missing_identity_nodes:
106
+ raise RuntimeError(
107
+ f"ComfyUI-Krea2Edit nodes are missing: {', '.join(missing_identity_nodes)}. "
108
+ f"Check the {KREA2EDIT_NODE_REPO} custom node install."
109
+ )
110
+ lora_info = object_info["LoraLoaderModelOnly"]["input"]["required"]["lora_name"][0]
111
+ if IDENTITY_LORA_FILE not in lora_info:
112
+ raise RuntimeError(f"Identity-edit LoRA is not visible to ComfyUI. First loras: {', '.join(lora_info[:10])}")
113
+
114
+
115
+ def _ensure_comfyui():
116
+ global _comfy_process
117
+
118
+ with _comfy_lock:
119
+ if _comfy_process is not None and _comfy_process.poll() is None:
120
+ return
121
+ try:
122
+ response = requests.get(f"{COMFY_URL}/system_stats", timeout=2)
123
+ if response.ok:
124
+ return
125
+ except Exception:
126
+ pass
127
+
128
+ if not COMFY_DIR.exists():
129
+ _run(["git", "clone", "--depth", "1", COMFY_REPO, str(COMFY_DIR)])
130
+
131
+ marker = COMFY_DIR / ".requirements-installed"
132
+ if not marker.exists():
133
+ _run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=COMFY_DIR)
134
+ marker.write_text("ok", encoding="utf-8")
135
+
136
+ krea2edit_dir = COMFY_DIR / "custom_nodes" / KREA2EDIT_NODE_DIRNAME
137
+ if not krea2edit_dir.exists():
138
+ _run(["git", "clone", "--depth", "1", KREA2EDIT_NODE_REPO, str(krea2edit_dir)])
139
+
140
+ diffusion_dir = COMFY_DIR / "models" / "diffusion_models"
141
+ text_encoder_dir = COMFY_DIR / "models" / "text_encoders"
142
+ vae_dir = COMFY_DIR / "models" / "vae"
143
+ lora_dir = COMFY_DIR / "models" / "loras"
144
+ diffusion_dir.mkdir(parents=True, exist_ok=True)
145
+ text_encoder_dir.mkdir(parents=True, exist_ok=True)
146
+ vae_dir.mkdir(parents=True, exist_ok=True)
147
+ lora_dir.mkdir(parents=True, exist_ok=True)
148
+
149
+ # Make every .safetensors uploaded to ./loras available to ComfyUI.
150
+ _sync_space_loras_to_comfy(lora_dir)
151
+
152
+ hf_hub_download(
153
+ repo_id=MODEL_REPO,
154
+ filename=MODEL_FILE,
155
+ local_dir=str(diffusion_dir),
156
+ token=os.environ.get("HF_TOKEN"),
157
+ )
158
+ hf_hub_download(
159
+ repo_id=COMFY_KREA_REPO,
160
+ filename=f"text_encoders/{TEXT_ENCODER_FILE}",
161
+ local_dir=str(COMFY_DIR / "models"),
162
+ token=os.environ.get("HF_TOKEN"),
163
+ )
164
+ hf_hub_download(
165
+ repo_id=COMFY_KREA_REPO,
166
+ filename=f"vae/{VAE_FILE}",
167
+ local_dir=str(COMFY_DIR / "models"),
168
+ token=os.environ.get("HF_TOKEN"),
169
+ )
170
+ hf_hub_download(
171
+ repo_id=IDENTITY_LORA_REPO,
172
+ filename=IDENTITY_LORA_FILE,
173
+ local_dir=str(lora_dir),
174
+ token=os.environ.get("HF_TOKEN"),
175
+ )
176
+
177
+
178
+ def _start_comfyui():
179
+ global _comfy_process
180
+
181
+ with _comfy_lock:
182
+ if _comfy_process is not None and _comfy_process.poll() is None:
183
+ return
184
+ try:
185
+ response = requests.get(f"{COMFY_URL}/system_stats", timeout=2)
186
+ if response.ok:
187
+ _validate_comfyui()
188
+ return
189
+ except Exception:
190
+ pass
191
+
192
+ cmd = [
193
+ sys.executable,
194
+ "main.py",
195
+ "--listen",
196
+ COMFY_HOST,
197
+ "--port",
198
+ str(COMFY_PORT),
199
+ "--disable-auto-launch",
200
+ ]
201
+ _comfy_process = subprocess.Popen(cmd, cwd=str(COMFY_DIR))
202
+ _wait_for_comfy()
203
+ _validate_comfyui()
204
+
205
+
206
+ def _build_workflow(
207
+ prompt,
208
+ negative_prompt,
209
+ width,
210
+ height,
211
+ steps,
212
+ cfg,
213
+ seed,
214
+ sampler,
215
+ scheduler,
216
+ selected_loras=None,
217
+ lora_strength=0.8,
218
+ ):
219
+ if selected_loras is None:
220
+ selected_loras = []
221
+ elif isinstance(selected_loras, str):
222
+ selected_loras = [selected_loras]
223
+
224
+ available = set(_scan_space_loras())
225
+ selected_loras = [name for name in selected_loras if name in available]
226
+
227
+ workflow = {
228
+ "1": {
229
+ "class_type": "UNETLoader",
230
+ "inputs": {"unet_name": MODEL_FILE, "weight_dtype": "default"},
231
+ },
232
+ "8": {
233
+ "class_type": "CLIPLoader",
234
+ "inputs": {"clip_name": TEXT_ENCODER_FILE, "type": "krea2", "device": "default"},
235
+ },
236
+ "9": {
237
+ "class_type": "VAELoader",
238
+ "inputs": {"vae_name": VAE_FILE},
239
+ },
240
+ "2": {
241
+ "class_type": "CLIPTextEncode",
242
+ "inputs": {"text": prompt, "clip": ["8", 0]},
243
+ },
244
+ "4": {
245
+ "class_type": "EmptyLatentImage",
246
+ "inputs": {"width": int(width), "height": int(height), "batch_size": 1},
247
+ },
248
+ "5": {
249
+ "class_type": "KSampler",
250
+ "inputs": {
251
+ "seed": int(seed),
252
+ "steps": int(steps),
253
+ "cfg": float(cfg),
254
+ "sampler_name": sampler,
255
+ "scheduler": scheduler,
256
+ "denoise": 1.0,
257
+ "model": ["1", 0],
258
+ "positive": ["2", 0],
259
+ "negative": ["3", 0],
260
+ "latent_image": ["4", 0],
261
+ },
262
+ },
263
+ "6": {
264
+ "class_type": "VAEDecode",
265
+ "inputs": {"samples": ["5", 0], "vae": ["9", 0]},
266
+ },
267
+ "7": {
268
+ "class_type": "SaveImage",
269
+ "inputs": {"filename_prefix": "redcraft", "images": ["6", 0]},
270
+ },
271
+ }
272
+
273
+ # Chain selected LoRAs model-only, matching the existing Krea2 identity workflow.
274
+ model_node = ["1", 0]
275
+ next_node_id = 20
276
+ for lora_name in selected_loras:
277
+ node_id = str(next_node_id)
278
+ workflow[node_id] = {
279
+ "class_type": "LoraLoaderModelOnly",
280
+ "inputs": {
281
+ "lora_name": lora_name,
282
+ "strength_model": float(lora_strength),
283
+ "model": model_node,
284
+ },
285
+ }
286
+ model_node = [node_id, 0]
287
+ next_node_id += 1
288
+
289
+ workflow["5"]["inputs"]["model"] = model_node
290
+
291
+ if negative_prompt and negative_prompt.strip():
292
+ workflow["3"] = {
293
+ "class_type": "CLIPTextEncode",
294
+ "inputs": {"text": negative_prompt, "clip": ["8", 0]},
295
+ }
296
+ else:
297
+ workflow["3"] = {
298
+ "class_type": "ConditioningZeroOut",
299
+ "inputs": {"conditioning": ["2", 0]},
300
+ }
301
+ return workflow
302
+
303
+
304
+ def _upload_image_to_comfy(image):
305
+ if image is None:
306
+ raise ValueError("Upload an image to edit.")
307
+
308
+ image = image.convert("RGB")
309
+ filename = f"redcraft-input-{uuid.uuid4().hex}.png"
310
+ temp_path = Path("/tmp") / filename
311
+ image.save(temp_path)
312
+ with temp_path.open("rb") as handle:
313
+ response = requests.post(
314
+ f"{COMFY_URL}/upload/image",
315
+ files={"image": (filename, handle, "image/png")},
316
+ data={"overwrite": "true"},
317
+ timeout=120,
318
+ )
319
+ response.raise_for_status()
320
+ data = response.json()
321
+ return data.get("name", filename)
322
+
323
+
324
+ def _resize_for_edit(image, width, height):
325
+ width, height = int(width), int(height)
326
+ if width <= 0 or height <= 0:
327
+ return image.convert("RGB")
328
+ return image.convert("RGB").resize((width, height), Image.LANCZOS)
329
+
330
+
331
+ def _target_size_from_source(image, max_megapixels=1.0):
332
+ multiple = 16
333
+ width, height = image.size
334
+ megapixels = (width * height) / 1_000_000
335
+ if megapixels > max_megapixels:
336
+ scale = (max_megapixels / megapixels) ** 0.5
337
+ width, height = round(width * scale), round(height * scale)
338
+ width = max(multiple, (width // multiple) * multiple)
339
+ height = max(multiple, (height // multiple) * multiple)
340
+ return width, height
341
+
342
+
343
+ def _build_edit_workflow(input_filename, prompt, negative_prompt, steps, cfg, seed, sampler, scheduler, denoise):
344
+ workflow = {
345
+ "1": {
346
+ "class_type": "UNETLoader",
347
+ "inputs": {"unet_name": MODEL_FILE, "weight_dtype": "default"},
348
+ },
349
+ "8": {
350
+ "class_type": "CLIPLoader",
351
+ "inputs": {"clip_name": TEXT_ENCODER_FILE, "type": "krea2", "device": "default"},
352
+ },
353
+ "9": {
354
+ "class_type": "VAELoader",
355
+ "inputs": {"vae_name": VAE_FILE},
356
+ },
357
+ "10": {
358
+ "class_type": "LoadImage",
359
+ "inputs": {"image": input_filename},
360
+ },
361
+ "11": {
362
+ "class_type": "VAEEncode",
363
+ "inputs": {"pixels": ["10", 0], "vae": ["9", 0]},
364
+ },
365
+ "2": {
366
+ "class_type": "CLIPTextEncode",
367
+ "inputs": {"text": prompt, "clip": ["8", 0]},
368
+ },
369
+ "5": {
370
+ "class_type": "KSampler",
371
+ "inputs": {
372
+ "seed": int(seed),
373
+ "steps": int(steps),
374
+ "cfg": float(cfg),
375
+ "sampler_name": sampler,
376
+ "scheduler": scheduler,
377
+ "denoise": float(denoise),
378
+ "model": ["1", 0],
379
+ "positive": ["2", 0],
380
+ "negative": ["3", 0],
381
+ "latent_image": ["11", 0],
382
+ },
383
+ },
384
+ "6": {
385
+ "class_type": "VAEDecode",
386
+ "inputs": {"samples": ["5", 0], "vae": ["9", 0]},
387
+ },
388
+ "7": {
389
+ "class_type": "SaveImage",
390
+ "inputs": {"filename_prefix": "redcraft-edit", "images": ["6", 0]},
391
+ },
392
+ }
393
+
394
+ if negative_prompt and negative_prompt.strip():
395
+ workflow["3"] = {
396
+ "class_type": "CLIPTextEncode",
397
+ "inputs": {"text": negative_prompt, "clip": ["8", 0]},
398
+ }
399
+ else:
400
+ workflow["3"] = {
401
+ "class_type": "ConditioningZeroOut",
402
+ "inputs": {"conditioning": ["2", 0]},
403
+ }
404
+ return workflow
405
+
406
+
407
+ def _build_identity_edit_workflow(input_filename, prompt, width, height, steps, cfg, seed, ref_boost, grounding_px, sampler, scheduler):
408
+ return {
409
+ "1": {
410
+ "class_type": "UNETLoader",
411
+ "inputs": {"unet_name": MODEL_FILE, "weight_dtype": "default"},
412
+ },
413
+ "8": {
414
+ "class_type": "CLIPLoader",
415
+ "inputs": {"clip_name": TEXT_ENCODER_FILE, "type": "krea2", "device": "default"},
416
+ },
417
+ "9": {
418
+ "class_type": "VAELoader",
419
+ "inputs": {"vae_name": VAE_FILE},
420
+ },
421
+ "20": {
422
+ "class_type": "LoraLoaderModelOnly",
423
+ "inputs": {"lora_name": IDENTITY_LORA_FILE, "strength_model": 1.0, "model": ["1", 0]},
424
+ },
425
+ "10": {
426
+ "class_type": "LoadImage",
427
+ "inputs": {"image": input_filename},
428
+ },
429
+ "11": {
430
+ "class_type": "VAEEncode",
431
+ "inputs": {"pixels": ["10", 0], "vae": ["9", 0]},
432
+ },
433
+ "21": {
434
+ "class_type": "Krea2EditModelPatch",
435
+ "inputs": {
436
+ "model": ["20", 0],
437
+ "source_latent": ["11", 0],
438
+ "ref_boost": float(ref_boost),
439
+ "fit_mode": "fit",
440
+ "vae": ["9", 0],
441
+ "source_image": ["10", 0],
442
+ },
443
+ },
444
+ "2": {
445
+ "class_type": "Krea2EditGroundedEncode",
446
+ "inputs": {"clip": ["8", 0], "prompt": prompt, "image": ["10", 0], "grounding_px": int(grounding_px)},
447
+ },
448
+ "3": {
449
+ "class_type": "Krea2EditGroundedEncode",
450
+ "inputs": {"clip": ["8", 0], "prompt": "", "image": ["10", 0], "grounding_px": int(grounding_px)},
451
+ },
452
+ "4": {
453
+ "class_type": "EmptySD3LatentImage",
454
+ "inputs": {"width": int(width), "height": int(height), "batch_size": 1},
455
+ },
456
+ "5": {
457
+ "class_type": "KSampler",
458
+ "inputs": {
459
+ "seed": int(seed),
460
+ "steps": int(steps),
461
+ "cfg": float(cfg),
462
+ "sampler_name": sampler,
463
+ "scheduler": scheduler,
464
+ "denoise": 1.0,
465
+ "model": ["21", 0],
466
+ "positive": ["2", 0],
467
+ "negative": ["3", 0],
468
+ "latent_image": ["4", 0],
469
+ },
470
+ },
471
+ "6": {
472
+ "class_type": "VAEDecode",
473
+ "inputs": {"samples": ["5", 0], "vae": ["9", 0]},
474
+ },
475
+ "7": {
476
+ "class_type": "SaveImage",
477
+ "inputs": {"filename_prefix": "identity-edit", "images": ["6", 0]},
478
+ },
479
+ }
480
+
481
+
482
+ def _queue_prompt(workflow):
483
+ payload = {"prompt": workflow, "client_id": str(uuid.uuid4())}
484
+ response = requests.post(f"{COMFY_URL}/prompt", json=payload, timeout=30)
485
+ if not response.ok:
486
+ raise RuntimeError(f"ComfyUI prompt error {response.status_code}: {response.text[:1000]}")
487
+ return response.json()["prompt_id"]
488
+
489
+
490
+ def _wait_for_history(prompt_id, timeout=900):
491
+ deadline = time.time() + timeout
492
+ while time.time() < deadline:
493
+ response = requests.get(f"{COMFY_URL}/history/{prompt_id}", timeout=30)
494
+ response.raise_for_status()
495
+ history = response.json()
496
+ if prompt_id in history:
497
+ item = history[prompt_id]
498
+ status = item.get("status", {})
499
+ if status.get("completed"):
500
+ return item
501
+ messages = status.get("messages") or []
502
+ for message in messages:
503
+ if isinstance(message, list) and message and message[0] == "execution_error":
504
+ raise RuntimeError(json.dumps(message[1], indent=2)[:2000])
505
+ time.sleep(1)
506
+ raise RuntimeError("Timed out waiting for ComfyUI generation.")
507
+
508
+
509
+ def _load_output_image(history_item):
510
+ outputs = history_item.get("outputs", {})
511
+ for output in outputs.values():
512
+ for image in output.get("images", []):
513
+ params = {
514
+ "filename": image["filename"],
515
+ "subfolder": image.get("subfolder", ""),
516
+ "type": image.get("type", "output"),
517
+ }
518
+ response = requests.get(f"{COMFY_URL}/view", params=params, timeout=120)
519
+ response.raise_for_status()
520
+ temp_path = Path("/tmp") / f"{uuid.uuid4().hex}.png"
521
+ temp_path.write_bytes(response.content)
522
+ return Image.open(temp_path).convert("RGB")
523
+ raise RuntimeError("ComfyUI completed without returning an image.")
524
+
525
+
526
+ def _duration(prompt, negative_prompt, width, height, steps, cfg, seed, randomize_seed, sampler, scheduler, selected_loras=None, lora_strength=0.8):
527
+ megapixels = max(1.0, (int(width) * int(height)) / (1024 * 1024))
528
+ return int(900 + int(steps) * 8 * megapixels)
529
+
530
+
531
+ def _edit_duration(
532
+ input_image,
533
+ prompt,
534
+ negative_prompt,
535
+ width,
536
+ height,
537
+ steps,
538
+ cfg,
539
+ denoise,
540
+ seed,
541
+ randomize_seed,
542
+ sampler,
543
+ scheduler,
544
+ ):
545
+ megapixels = max(1.0, (int(width) * int(height)) / (1024 * 1024))
546
+ return int(900 + int(steps) * 9 * megapixels)
547
+
548
+
549
+ def _identity_edit_duration(
550
+ input_image,
551
+ prompt,
552
+ ref_boost,
553
+ grounding_px,
554
+ max_megapixels,
555
+ steps,
556
+ cfg,
557
+ seed,
558
+ randomize_seed,
559
+ sampler,
560
+ scheduler,
561
+ ):
562
+ return int(900 + int(steps) * 10 * max(1.0, float(max_megapixels)))
563
+
564
+
565
+ @spaces.GPU(duration=_duration)
566
+ def generate(
567
+ prompt,
568
+ negative_prompt="",
569
+ width=1024,
570
+ height=1024,
571
+ steps=10,
572
+ cfg=1.0,
573
+ seed=0,
574
+ randomize_seed=True,
575
+ sampler="er_sde",
576
+ scheduler="simple",
577
+ selected_loras=None,
578
+ lora_strength=0.8,
579
+ ):
580
+ if not prompt or not prompt.strip():
581
+ raise gr.Error("Enter a prompt.")
582
+ if randomize_seed:
583
+ seed = random.randint(0, MAX_SEED)
584
+
585
+ try:
586
+ _start_comfyui()
587
+ workflow = _build_workflow(prompt, negative_prompt, width, height, steps, cfg, seed, sampler, scheduler, selected_loras, lora_strength)
588
+ prompt_id = _queue_prompt(workflow)
589
+ history_item = _wait_for_history(prompt_id)
590
+ return _load_output_image(history_item), seed
591
+ except Exception as exc:
592
+ raise gr.Error(str(exc)) from exc
593
+
594
+
595
+ @spaces.GPU(duration=_edit_duration)
596
+ def edit_image(
597
+ input_image,
598
+ prompt,
599
+ negative_prompt="",
600
+ width=1024,
601
+ height=1024,
602
+ steps=12,
603
+ cfg=1.2,
604
+ denoise=0.35,
605
+ seed=0,
606
+ randomize_seed=True,
607
+ sampler="er_sde",
608
+ scheduler="simple",
609
+ ):
610
+ if input_image is None:
611
+ raise gr.Error("Upload an image to edit.")
612
+ if not prompt or not prompt.strip():
613
+ raise gr.Error("Enter an edit prompt.")
614
+ if randomize_seed:
615
+ seed = random.randint(0, MAX_SEED)
616
+
617
+ try:
618
+ _start_comfyui()
619
+ resized = _resize_for_edit(input_image, width, height)
620
+ input_filename = _upload_image_to_comfy(resized)
621
+ workflow = _build_edit_workflow(
622
+ input_filename,
623
+ prompt,
624
+ negative_prompt,
625
+ steps,
626
+ cfg,
627
+ seed,
628
+ sampler,
629
+ scheduler,
630
+ denoise,
631
+ )
632
+ prompt_id = _queue_prompt(workflow)
633
+ history_item = _wait_for_history(prompt_id)
634
+ return _load_output_image(history_item), seed
635
+ except Exception as exc:
636
+ raise gr.Error(str(exc)) from exc
637
+
638
+
639
+ @spaces.GPU(duration=_identity_edit_duration)
640
+ def identity_edit(
641
+ input_image,
642
+ prompt,
643
+ ref_boost=4.0,
644
+ grounding_px=768,
645
+ max_megapixels=1.0,
646
+ steps=10,
647
+ cfg=1.0,
648
+ seed=0,
649
+ randomize_seed=True,
650
+ sampler="euler",
651
+ scheduler="simple",
652
+ ):
653
+ if input_image is None:
654
+ raise gr.Error("Upload an image to edit.")
655
+ if not prompt or not prompt.strip():
656
+ raise gr.Error("Enter an edit instruction.")
657
+ if randomize_seed:
658
+ seed = random.randint(0, MAX_SEED)
659
+
660
+ try:
661
+ _start_comfyui()
662
+ source = input_image.convert("RGB")
663
+ width, height = _target_size_from_source(source, max_megapixels)
664
+ input_filename = _upload_image_to_comfy(source)
665
+ workflow = _build_identity_edit_workflow(
666
+ input_filename,
667
+ prompt,
668
+ width,
669
+ height,
670
+ steps,
671
+ cfg,
672
+ seed,
673
+ ref_boost,
674
+ grounding_px,
675
+ sampler,
676
+ scheduler,
677
+ )
678
+ prompt_id = _queue_prompt(workflow)
679
+ history_item = _wait_for_history(prompt_id)
680
+ return _load_output_image(history_item), seed
681
+ except Exception as exc:
682
+ raise gr.Error(str(exc)) from exc
683
+
684
+
685
+ CSS = """
686
+ .gradio-container { max-width: 1120px !important; margin: 0 auto !important; }
687
+ #result-image { min-height: 520px; }
688
+ """
689
+
690
+ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
691
+ gr.Markdown("# Redcraft Krea2")
692
+ gr.Markdown("ComfyUI-native Redcraft Krea2 generation and image editing.")
693
+
694
+ with gr.Tabs():
695
+ with gr.Tab("Generate"):
696
+ with gr.Row():
697
+ with gr.Column(scale=5):
698
+ prompt = gr.Textbox(label="Prompt", lines=5, placeholder="Describe the image to generate.")
699
+ negative_prompt = gr.Textbox(label="Negative prompt", lines=2, value="")
700
+ with gr.Row():
701
+ lora_selector = gr.Dropdown(
702
+ choices=_scan_space_loras(),
703
+ value=[],
704
+ multiselect=True,
705
+ label="LoRAs",
706
+ info="Select one or more .safetensors files uploaded to the loras/ folder.",
707
+ )
708
+ refresh_loras = gr.Button("Refresh LoRAs", scale=0)
709
+ lora_strength = gr.Slider(
710
+ 0.0,
711
+ 1.5,
712
+ value=0.8,
713
+ step=0.05,
714
+ label="LoRA strength",
715
+ info="Shared model strength for all selected LoRAs.",
716
+ )
717
+ refresh_loras.click(_refresh_lora_choices, outputs=lora_selector)
718
+ with gr.Row():
719
+ width = gr.Slider(512, 1536, value=1024, step=64, label="Width")
720
+ height = gr.Slider(512, 1536, value=1024, step=64, label="Height")
721
+ with gr.Row():
722
+ steps = gr.Slider(1, 30, value=10, step=1, label="Steps")
723
+ cfg = gr.Slider(0.0, 8.0, value=1.0, step=0.1, label="CFG")
724
+ with gr.Row():
725
+ seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
726
+ randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
727
+ with gr.Accordion("Sampler", open=False):
728
+ sampler = gr.Dropdown(
729
+ ["er_sde", "euler", "euler_ancestral", "dpmpp_2m", "dpmpp_sde"],
730
+ value="er_sde",
731
+ label="Sampler",
732
+ )
733
+ scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
734
+ run = gr.Button("Generate", variant="primary")
735
+ with gr.Column(scale=6):
736
+ output = gr.Image(label="Result", format="png", elem_id="result-image")
737
+
738
+ inputs = [prompt, negative_prompt, width, height, steps, cfg, seed, randomize_seed, sampler, scheduler, lora_selector, lora_strength]
739
+ run.click(generate, inputs, [output, seed])
740
+ prompt.submit(generate, inputs, [output, seed])
741
+
742
+ with gr.Tab("Edit Image"):
743
+ with gr.Row():
744
+ with gr.Column(scale=5):
745
+ edit_input = gr.Image(type="pil", label="Input image")
746
+ edit_prompt = gr.Textbox(label="Edit prompt", lines=5, placeholder="Describe the edit while preserving identity.")
747
+ edit_negative_prompt = gr.Textbox(label="Negative prompt", lines=2, value="")
748
+ with gr.Row():
749
+ edit_width = gr.Slider(512, 1536, value=1024, step=64, label="Width")
750
+ edit_height = gr.Slider(512, 1536, value=1024, step=64, label="Height")
751
+ with gr.Row():
752
+ edit_steps = gr.Slider(1, 30, value=12, step=1, label="Steps")
753
+ edit_cfg = gr.Slider(0.0, 8.0, value=1.2, step=0.1, label="CFG")
754
+ edit_denoise = gr.Slider(
755
+ 0.05,
756
+ 0.8,
757
+ value=0.35,
758
+ step=0.05,
759
+ label="Edit strength",
760
+ info="Lower values preserve identity and composition more strongly.",
761
+ )
762
+ with gr.Row():
763
+ edit_seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
764
+ edit_randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
765
+ with gr.Accordion("Sampler", open=False):
766
+ edit_sampler = gr.Dropdown(
767
+ ["er_sde", "euler", "euler_ancestral", "dpmpp_2m", "dpmpp_sde"],
768
+ value="er_sde",
769
+ label="Sampler",
770
+ )
771
+ edit_scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
772
+ edit_run = gr.Button("Edit Image", variant="primary")
773
+ with gr.Column(scale=6):
774
+ edit_output = gr.Image(label="Edited image", format="png", elem_id="result-image")
775
+
776
+ edit_inputs = [
777
+ edit_input,
778
+ edit_prompt,
779
+ edit_negative_prompt,
780
+ edit_width,
781
+ edit_height,
782
+ edit_steps,
783
+ edit_cfg,
784
+ edit_denoise,
785
+ edit_seed,
786
+ edit_randomize_seed,
787
+ edit_sampler,
788
+ edit_scheduler,
789
+ ]
790
+ edit_run.click(edit_image, edit_inputs, [edit_output, edit_seed])
791
+ edit_prompt.submit(edit_image, edit_inputs, [edit_output, edit_seed])
792
+
793
+ with gr.Tab("Identity Edit"):
794
+ gr.Markdown(
795
+ "Instruction-based, identity-preserving editing using the community LoRA "
796
+ "[`conradlocke/krea2-identity-edit`](https://huggingface.co/conradlocke/krea2-identity-edit) "
797
+ "on the Redcraft Krea2 checkpoint, via the "
798
+ "[ComfyUI-Krea2Edit](https://github.com/lbouaraba/comfyui-krea2edit) node pack. "
799
+ "Give it an image and a plain-language instruction; it edits while preserving what you "
800
+ "didn't ask to change, including the person."
801
+ )
802
+ with gr.Row():
803
+ with gr.Column(scale=5):
804
+ id_input = gr.Image(type="pil", label="Source image")
805
+ id_prompt = gr.Textbox(
806
+ label="Edit instruction",
807
+ lines=3,
808
+ placeholder="e.g. create a photo of this person at a night market",
809
+ )
810
+ id_ref_boost = gr.Slider(
811
+ 0.0,
812
+ 10.0,
813
+ value=4.0,
814
+ step=0.5,
815
+ label="Likeness (ref_boost)",
816
+ info="How hard the edit holds the reference. 1 = off, 4 = strong likeness (recommended), 8+ over-copies.",
817
+ )
818
+ with gr.Accordion("Advanced settings", open=False):
819
+ id_grounding_px = gr.Slider(
820
+ 384,
821
+ 1536,
822
+ value=768,
823
+ step=64,
824
+ label="Grounding resolution (px)",
825
+ info="Lower = stronger edit adherence. Higher = stronger identity likeness. Trained range 384-768; 1024+ often still works.",
826
+ )
827
+ id_max_mp = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Output size (megapixels)")
828
+ with gr.Row():
829
+ id_steps = gr.Slider(4, 28, value=10, step=1, label="Steps")
830
+ id_cfg = gr.Slider(
831
+ 0.0,
832
+ 5.0,
833
+ value=1.0,
834
+ step=0.5,
835
+ label="CFG",
836
+ info="Turbo convention: 1.0 = guidance off. Raise (e.g. 3) with more steps for removals/large edits.",
837
+ )
838
+ with gr.Row():
839
+ id_seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
840
+ id_randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
841
+ with gr.Row():
842
+ id_sampler = gr.Dropdown(
843
+ ["euler", "er_sde", "euler_ancestral", "dpmpp_2m", "dpmpp_sde"],
844
+ value="euler",
845
+ label="Sampler",
846
+ )
847
+ id_scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
848
+ id_run = gr.Button("Edit Identity", variant="primary")
849
+ with gr.Column(scale=6):
850
+ id_output = gr.Image(label="Edited image", format="png", elem_id="result-image")
851
+
852
+ id_inputs = [
853
+ id_input,
854
+ id_prompt,
855
+ id_ref_boost,
856
+ id_grounding_px,
857
+ id_max_mp,
858
+ id_steps,
859
+ id_cfg,
860
+ id_seed,
861
+ id_randomize_seed,
862
+ id_sampler,
863
+ id_scheduler,
864
+ ]
865
+ id_run.click(identity_edit, id_inputs, [id_output, id_seed])
866
+ id_prompt.submit(identity_edit, id_inputs, [id_output, id_seed])
867
+
868
+ gr.Examples(
869
+ examples=[
870
+ ["examples/woman.jpg", "create a photo of this person at a busy night market at night"],
871
+ ["examples/businessman_suit.jpg", "change the suit jacket to a red leather jacket"],
872
+ ["examples/man_beach.jpg", "make it a vintage film photo with warm golden-hour light"],
873
+ ],
874
+ inputs=[id_input, id_prompt],
875
+ label="Examples",
876
+ )
877
+
878
+
879
+ _ensure_comfyui()
880
+
881
+ if __name__ == "__main__":
882
+ demo.queue().launch()