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

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -882
app.py DELETED
@@ -1,882 +0,0 @@
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()