Daankular commited on
Commit
b581095
·
verified ·
1 Parent(s): 8a4c43a

Use ComfyUI native Redcraft T2I

Browse files
Files changed (1) hide show
  1. app.py +228 -422
app.py CHANGED
@@ -1,455 +1,261 @@
1
- import glob
2
  import os
3
  import random
4
  import subprocess
5
  import sys
6
- import spaces
7
- import torch
8
- import gradio as gr
9
- from huggingface_hub import hf_hub_download, login
10
 
11
- if os.environ.get("HF_TOKEN"):
12
- login(token=os.environ["HF_TOKEN"])
 
 
 
13
 
14
- from diffusers import Krea2Pipeline
15
 
16
- DTYPE = torch.bfloat16
17
  MODEL_REPO = "Daankular/redcraft-krea2-fp8"
18
  MODEL_FILE = "redcraftKREA2RedMix_krea2Edition.safetensors"
19
- AOTI_REPO = "multimodalart/krea2-aoti-kernels"
20
- AOTI_FILE = "Krea2TransformerBlock-lora-r64/package.pt2"
 
 
 
21
  MAX_SEED = 2**31 - 1
22
 
23
- checkpoint_path = hf_hub_download(
24
- repo_id=MODEL_REPO,
25
- filename=MODEL_FILE,
26
- token=os.environ.get("HF_TOKEN"),
27
- )
28
- pipe_redcraft = Krea2Pipeline.from_single_file(checkpoint_path, torch_dtype=DTYPE)
29
- pipe_redcraft.to("cuda")
30
-
31
-
32
-
33
- def _load_aoti():
34
- # The compiled Krea2TransformerBlock kernels are public and separate from
35
- # the checkpoint weights. If they cannot be loaded, the Space falls back to
36
- # eager mode.
37
- from spaces.zero.torch.aoti import LazyAOTIModel
38
-
39
- pt2 = hf_hub_download(
40
- repo_id=AOTI_REPO,
41
- filename=AOTI_FILE,
42
- repo_type="dataset",
43
- )
44
- aoti_model = LazyAOTIModel(pt2)
45
- for block in pipe_redcraft.transformer.modules():
46
- if block.__class__.__name__ == "Krea2TransformerBlock":
47
- spaces.aoti_patch(block, aoti_model)
48
-
49
-
50
- try:
51
- _load_aoti()
52
- print("AoTI blocks loaded.")
53
- except Exception as e:
54
- print(f"AoTI load skipped ({e}); running eager.")
55
-
56
- PIPES = {"Redcraft": pipe_redcraft}
57
- DEFAULTS = {
58
- "Redcraft": {"steps": 8, "guidance": 0.0},
59
- }
60
-
61
- # Resolution presets. The model renders up to 2K, but the compiled transformer
62
- # block can exceed this Space's GPU memory above 1024, so 1024 is the default
63
- # and larger sizes are opt-in (see the OOM guard in generate).
64
- RESOLUTIONS = {
65
- "Square · 1024": (1024, 1024),
66
- "Portrait · 1024": (832, 1216),
67
- "Landscape · 1024": (1216, 832),
68
- "Square · 2K": (2048, 2048),
69
- }
70
-
71
- PROMPT_TIPS = """\
72
- Krea 2 is tuned for natural language. Describe the image the way you would describe it to a person.
73
-
74
- - Write in full sentences or rich phrases. Longer, more specific prompts give the best results, but short prompts work too.
75
- - Name the things that matter: subject, setting, lighting, color, framing, medium, and mood.
76
- - To render text in the image, wrap the words in quotes, for example: a storefront window with a neon sign that reads "open late".
77
- - The model can render up to 2K, but very high resolutions may run out of GPU memory on this Space. 1024 is the reliable default.
78
-
79
- This Space uses the Redcraft checkpoint from [Daankular/redcraft-krea2-fp8](https://huggingface.co/Daankular/redcraft-krea2-fp8).
80
- """
81
 
82
- # Drawn from the official Krea 2 prompt guide. These demonstrate the
83
- # detailed, natural-language style the model rewards.
84
- EXAMPLE_PROMPTS = [
85
- ["immense rocket launch exhaust as seen from extremely close up"],
86
- [
87
- "3D rendered matte black designer toy figure, stylized round anthropomorphic shape, "
88
- "backward black baseball cap, oversized gold-rimmed aviator sunglasses, white traditional "
89
- "line-art tattoos of tiger and bird on torso, black studded belt with gold buckle, smooth "
90
- "vinyl texture, studio lighting, solid vibrant blue background, high contrast minimal composition"
91
- ],
92
- [
93
- "A tiny, russet-brown harvest mouse clings to a slender diagonal branch amid vibrant green "
94
- "lobed leaves and small round buds. The mouse has soft textured fur, glossy black eyes, a pink "
95
- "nose, fine whiskers, and delicate pink paws firmly gripping the wood. In this macro photograph, "
96
- "an extremely shallow depth of field sharply focuses on the animal's face. The deep green "
97
- "background dissolves into a smooth, creamy bokeh, illuminated by soft, diffused natural lighting "
98
- "that highlights the intricate details of the fur and foliage."
99
- ],
100
- [
101
- "high-fashion editorial portrait of a young East Asian woman, short choppy platinum blonde bob "
102
- "with heavy bangs, looking over her bare shoulder to the right, lips playfully pursed, wearing a "
103
- "structured black top with an architectural protruding bust detail and thin straps, delicate gold "
104
- "hoop earrings, arm bent with hand resting on hip, warm skin tones, solid striking crimson red "
105
- "background, soft directional studio lighting, cinematic color palette, medium close-up shot"
106
- ],
107
- [
108
- "A minimalist flat-color illustration of a person wading through expansive shallow ocean waves "
109
- "beneath a pale peach sky. The dark-skinned figure, wearing an orange swim cap, light blue top, and "
110
- "bright green shorts, steps carefully through knee-deep water. The ocean is rendered in muted mint "
111
- "green with delicate, thin black linework detailing the continuous ripples and gentle whitecaps. "
112
- "Soft pinkish-peach reflections echo the sky on the water's surface. The high-angle wide perspective "
113
- "emphasizes the vast negative space of the water, utilizing a clean ligne claire drawing aesthetic "
114
- "with a subtle paper texture."
115
- ],
116
- [
117
- "A surreal retro-futuristic space scene features liquid chrome forming an abstract face merging "
118
- "with a glowing planetary horizon. The foreground is dominated by swirling, highly reflective "
119
- "metallic fluid that distorts into a stylized, melting facial profile with deep shadows and bright "
120
- "silver highlights. This undulating chrome form rests against the curved, atmospheric edge of a "
121
- "massive planet bathed in a soft electric blue and purple glow. Set against a deep black starfield, "
122
- "the artwork employs a vintage 1980s airbrush aesthetic with smooth gradients, ethereal lighting, "
123
- "and high-contrast metallic rendering."
124
- ],
125
- [
126
- "Stylized digital painting of a menacing jester figure rendered with bold, expressive brushstrokes "
127
- "and a vibrant, almost psychedelic color palette against a pitch-black background. Dynamic low-angle "
128
- "perspective forces a dramatic, imposing composition as the character leans forward, one leg raised "
129
- "high. The jester wears a classic multi-pointed hat with bells, a ruffled collar, and striped tights "
130
- "in alternating shades of purple, blue, and chartreuse. The figure's face is a smooth, faceless, pale "
131
- "mauve mask with a single glowing white point of light at the center, and it grips a massive ornate "
132
- "sword with a glowing ethereal white blade. Theatrical lighting, dark fantasy concept-art aesthetic."
133
- ],
134
- [
135
- "A close-up portrait of a young East Asian woman with straight black hair, loose strands sweeping "
136
- "across her fair skin, and an intense gaze. She wears a light grey collared shirt with a black tie. "
137
- "A vibrant bouquet of pink and orange lilies with lush green leaves sits in the blurred right "
138
- "foreground. The background is a solid, striking crimson red. Soft, directional studio lighting "
139
- "highlights her facial features, creating a high-contrast composition with a shallow depth of field."
140
- ],
141
- ]
142
-
143
- # Official sample renders for the prompts above, in the same order. Drop the
144
- # matching PNGs into assets/samples/ and the gallery shows them; clicking a
145
- # thumbnail loads its prompt. Filenames follow the Krea 2 prompt guide. If the
146
- # files are absent, the UI falls back to the text example prompts below.
147
- SAMPLE_DIR = os.path.join(os.path.dirname(__file__), "assets", "samples")
148
- SAMPLE_FILES = ["takeoff.png", "3d.png", "mouse.png", "red.png", "beach.png", "future.png", "jester.png", "flowers.png"]
149
- SAMPLE_LABELS = [
150
- "Rocket exhaust",
151
- "Designer toy",
152
- "Harvest mouse",
153
- "Editorial portrait",
154
- "Ligne claire beach",
155
- "Liquid chrome",
156
- "Jester",
157
- "Crimson portrait",
158
- ]
159
-
160
- _gallery = [
161
- (os.path.join(SAMPLE_DIR, fname), label, prompt[0])
162
- for fname, label, prompt in zip(SAMPLE_FILES, SAMPLE_LABELS, EXAMPLE_PROMPTS)
163
- if os.path.exists(os.path.join(SAMPLE_DIR, fname))
164
- ]
165
- GALLERY_ITEMS = [(path, label) for path, label, _ in _gallery]
166
- GALLERY_PROMPTS = [prompt for _, _, prompt in _gallery]
167
-
168
- PLACEHOLDER = (
169
- "Describe your image in natural language. e.g. a russet harvest mouse clinging to a "
170
- 'branch, macro photograph, shallow depth of field, creamy green bokeh, soft natural light. '
171
- 'Wrap words in "quotes" to render them as text.'
172
- )
173
-
174
-
175
- def _duration(prompt, negative_prompt, model, steps, guidance, width, height, seed, randomize, progress=None):
176
- # Scale the GPU reservation by step count and pixel area so larger renders
177
- # are not killed before they finish.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  megapixels = max(1.0, (int(width) * int(height)) / (1024 * 1024))
179
- return int(int(steps) * 2 * megapixels + 25)
180
 
181
 
182
- @spaces.GPU(duration=_duration, size="xlarge")
183
  def generate(
184
  prompt,
185
  negative_prompt="",
186
- model="Redcraft",
187
- steps=8,
188
- guidance=0,
189
  width=1024,
190
  height=1024,
191
- seed=42,
192
- randomize=False,
193
- progress=gr.Progress(track_tqdm=True),
 
 
 
194
  ):
195
  if not prompt or not prompt.strip():
196
- raise gr.Error("Enter a prompt to generate an image.")
197
- if randomize:
198
  seed = random.randint(0, MAX_SEED)
199
- seed = int(seed)
200
- generator = torch.Generator("cuda").manual_seed(seed)
201
- pipe = PIPES[model]
202
  try:
203
- image = pipe(
204
- prompt=prompt,
205
- negative_prompt=(negative_prompt or None) if guidance > 0 else None,
206
- height=int(height),
207
- width=int(width),
208
- num_inference_steps=int(steps),
209
- guidance_scale=float(guidance),
210
- generator=generator,
211
- ).images[0]
212
- except RuntimeError as exc:
213
- # At high resolution the compiled transformer block can exhaust GPU
214
- # memory, which surfaces as a CUDA allocation / AOTI runtime error.
215
- # Recover the worker and tell the user how to fix it.
216
- torch.cuda.empty_cache()
217
- raise gr.Error(
218
- f"Generation failed at {int(width)}x{int(height)}. This is usually the GPU running "
219
- "out of memory at high resolution. Try 1024x1024 or a smaller size."
220
- ) from exc
221
- return image, seed
222
-
223
-
224
- def on_model_change(model):
225
- d = DEFAULTS[model]
226
- return (
227
- gr.update(value=d["steps"]),
228
- gr.update(value=d["guidance"]),
229
- gr.update(interactive=d["guidance"] > 0),
230
- )
231
-
232
-
233
- def on_resolution_change(label):
234
- w, h = RESOLUTIONS[label]
235
- return gr.update(value=w), gr.update(value=h)
236
-
237
-
238
- # Krea brand identity: neutral grayscale foundation with a single blue action
239
- # accent (krea.ai/press). Dark surfaces, mono utility type, accent reserved for
240
- # the primary action and focus states.
241
- KREA_ACCENT = "#2b5cff"
242
-
243
- theme = gr.themes.Base(
244
- primary_hue=gr.themes.colors.blue,
245
- neutral_hue=gr.themes.colors.neutral,
246
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
247
- font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
248
- ).set(
249
- body_background_fill="#000000",
250
- body_background_fill_dark="#000000",
251
- body_text_color="#f5f5f5",
252
- background_fill_primary="#0d0d0d",
253
- background_fill_secondary="#0d0d0d",
254
- block_background_fill="#0d0d0d",
255
- block_border_color="#262626",
256
- block_border_width="1px",
257
- block_label_background_fill="#0d0d0d",
258
- block_label_text_color="#737373",
259
- block_title_text_color="#d4d4d5",
260
- border_color_primary="#262626",
261
- input_background_fill="#000000",
262
- input_border_color="#262626",
263
- input_border_color_focus=KREA_ACCENT,
264
- button_primary_background_fill=KREA_ACCENT,
265
- button_primary_background_fill_hover="#1f4fff",
266
- button_primary_text_color="#ffffff",
267
- button_primary_border_color=KREA_ACCENT,
268
- button_secondary_background_fill="#171717",
269
- button_secondary_background_fill_hover="#262626",
270
- button_secondary_text_color="#f5f5f5",
271
- button_secondary_border_color="#262626",
272
- slider_color=KREA_ACCENT,
273
- )
274
 
275
  CSS = """
276
- .gradio-container { background: #000 !important; }
277
- #page { max-width: 1120px; margin: 0 auto; padding: 4px 8px 32px; }
278
-
279
- #krea-header {
280
- padding: 32px 6px 22px;
281
- border-bottom: 1px solid #1a1a1a;
282
- margin-bottom: 22px;
283
- }
284
- #krea-header .eyebrow {
285
- font-family: 'JetBrains Mono', ui-monospace, monospace;
286
- font-size: 11px;
287
- letter-spacing: 0.24em;
288
- text-transform: uppercase;
289
- color: #737373;
290
- }
291
- #krea-header h1 {
292
- font-size: 42px;
293
- font-weight: 600;
294
- letter-spacing: -0.025em;
295
- line-height: 1.05;
296
- margin: 10px 0 6px;
297
- color: #fff;
298
- }
299
- #krea-header .subtitle {
300
- font-size: 15px;
301
- line-height: 1.5;
302
- color: #a3a3a3;
303
- margin: 0;
304
- max-width: 60ch;
305
- }
306
- #krea-header .meta {
307
- margin-top: 18px;
308
- display: flex;
309
- justify-content: space-between;
310
- align-items: center;
311
- flex-wrap: wrap;
312
- gap: 12px;
313
- }
314
- #krea-header .badges { display: flex; gap: 8px; }
315
- #krea-header .badge {
316
- font-family: 'JetBrains Mono', ui-monospace, monospace;
317
- font-size: 10px;
318
- letter-spacing: 0.12em;
319
- text-transform: uppercase;
320
- color: #d4d4d5;
321
- border: 1px solid #262626;
322
- border-radius: 999px;
323
- padding: 4px 10px;
324
- }
325
- #krea-header .links { display: flex; gap: 16px; }
326
- #krea-header .links a {
327
- font-family: 'JetBrains Mono', ui-monospace, monospace;
328
- font-size: 11px;
329
- letter-spacing: 0.08em;
330
- text-transform: uppercase;
331
- color: #737373;
332
- text-decoration: none;
333
- transition: color 0.15s ease;
334
- }
335
- #krea-header .links a:hover { color: #f5f5f5; }
336
-
337
- #generate-btn { font-weight: 600; letter-spacing: 0.01em; }
338
-
339
- #result-image { min-height: 420px; border-radius: 10px; overflow: hidden; }
340
-
341
- /* Inline code chips legible on the dark theme (e.g. the expansion.txt mention in tips). */
342
- .gradio-container code,
343
- .gradio-container .prose code {
344
- background: #171717 !important;
345
- color: #d4d4d5 !important;
346
- border: 1px solid #262626 !important;
347
- border-radius: 5px !important;
348
- padding: 2px 7px !important;
349
- font-family: 'JetBrains Mono', ui-monospace, monospace !important;
350
- font-size: 0.85em !important;
351
- }
352
-
353
- footer { display: none !important; }
354
- .gradio-container .prose a { color: """ + KREA_ACCENT + """; }
355
  """
356
 
357
- with gr.Blocks(title="Krea 2") as demo:
358
- with gr.Column(elem_id="page"):
359
- gr.HTML(
360
- """
361
- <header id="krea-header">
362
- <div class="eyebrow">KREA · TEXT-TO-IMAGE</div>
363
- <h1>Krea 2</h1>
364
- <p class="subtitle">Generate images from natural language with the Redcraft Krea 2 checkpoint.</p>
365
- <div class="meta">
366
- <div class="badges">
367
- <span class="badge">Redcraft</span>
368
- <span class="badge">Public AoTI kernels</span>
369
- </div>
370
- <div class="links">
371
- <a href="https://www.krea.ai/blog/krea-2-technical-report" target="_blank" rel="noopener">Technical report ↗</a>
372
- <a href="https://github.com/krea-ai/krea-2" target="_blank" rel="noopener">GitHub ↗</a>
373
- </div>
374
- </div>
375
- </header>
376
- """
377
- )
378
-
379
- with gr.Row(equal_height=False):
380
- with gr.Column(scale=5, elem_classes="panel"):
381
- prompt = gr.Textbox(
382
- label="Prompt",
383
- lines=4,
384
- placeholder=PLACEHOLDER,
385
- show_label=True,
386
- autofocus=True,
387
  )
388
- model = gr.Radio(["Redcraft"], value="Redcraft", label="Model")
389
- run = gr.Button("Generate", variant="primary", elem_id="generate-btn")
390
-
391
- with gr.Accordion("Prompting tips", open=False):
392
- gr.Markdown(PROMPT_TIPS)
393
 
394
- resolution = gr.Radio(
395
- list(RESOLUTIONS.keys()),
396
- value="Square · 1024",
397
- label="Resolution",
398
- )
399
-
400
- with gr.Accordion("Advanced", open=False):
401
- negative_prompt = gr.Textbox(
402
- label="Negative prompt",
403
- lines=1,
404
- interactive=False,
405
- info="Enabled when guidance is above 0.",
406
- )
407
- steps = gr.Slider(1, 50, value=8, step=1, label="Steps")
408
- guidance = gr.Slider(0.0, 10.0, value=0.0, step=0.1, label="Guidance scale")
409
- with gr.Row():
410
- width = gr.Slider(512, 2048, value=1024, step=16, label="Width")
411
- height = gr.Slider(512, 2048, value=1024, step=16, label="Height")
412
- with gr.Row():
413
- seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
414
- randomize = gr.Checkbox(value=True, label="Randomize seed")
415
-
416
- with gr.Column(scale=6, elem_classes="panel"):
417
- output = gr.Image(label="Result", format="png", elem_id="result-image")
418
-
419
- if GALLERY_ITEMS:
420
- gallery = gr.Gallery(
421
- value=GALLERY_ITEMS,
422
- label="Example prompts",
423
- columns=4,
424
- height="auto",
425
- object_fit="cover",
426
- allow_preview=False,
427
- elem_id="examples-gallery",
428
- )
429
-
430
- def use_example(evt: gr.SelectData):
431
- # Clicking a sample loads its prompt into the box, ready to run.
432
- return GALLERY_PROMPTS[evt.index]
433
-
434
- gallery.select(use_example, None, prompt)
435
- else:
436
- # No bundled sample images present; show the prompts as text.
437
- gr.Examples(
438
- fn=generate,
439
- examples=EXAMPLE_PROMPTS,
440
- inputs=[prompt],
441
- outputs=[output, seed],
442
- cache_examples=True,
443
- cache_mode="lazy",
444
- label="Example prompts",
445
- examples_per_page=4,
446
- )
447
-
448
- model.change(on_model_change, model, [steps, guidance, negative_prompt])
449
- resolution.change(on_resolution_change, resolution, [width, height])
450
-
451
- inputs = [prompt, negative_prompt, model, steps, guidance, width, height, seed, randomize]
452
  run.click(generate, inputs, [output, seed])
453
  prompt.submit(generate, inputs, [output, seed])
454
 
455
- demo.launch(theme=theme, css=CSS)
 
 
 
1
+ import json
2
  import os
3
  import random
4
  import subprocess
5
  import sys
6
+ import threading
7
+ import time
8
+ import uuid
9
+ from pathlib import Path
10
 
11
+ import gradio as gr
12
+ import requests
13
+ import spaces
14
+ from huggingface_hub import hf_hub_download
15
+ from PIL import Image
16
 
 
17
 
 
18
  MODEL_REPO = "Daankular/redcraft-krea2-fp8"
19
  MODEL_FILE = "redcraftKREA2RedMix_krea2Edition.safetensors"
20
+ COMFY_REPO = "https://github.com/comfyanonymous/ComfyUI.git"
21
+ COMFY_DIR = Path(os.environ.get("COMFYUI_DIR", "/tmp/ComfyUI"))
22
+ COMFY_HOST = "127.0.0.1"
23
+ COMFY_PORT = int(os.environ.get("COMFYUI_PORT", "8188"))
24
+ COMFY_URL = f"http://{COMFY_HOST}:{COMFY_PORT}"
25
  MAX_SEED = 2**31 - 1
26
 
27
+ _comfy_lock = threading.Lock()
28
+ _comfy_process = None
29
+
30
+
31
+ def _run(cmd, cwd=None):
32
+ print("[setup]", " ".join(map(str, cmd)), flush=True)
33
+ subprocess.check_call(cmd, cwd=str(cwd) if cwd else None)
34
+
35
+
36
+ def _wait_for_comfy(timeout=180):
37
+ deadline = time.time() + timeout
38
+ last_error = None
39
+ while time.time() < deadline:
40
+ try:
41
+ response = requests.get(f"{COMFY_URL}/system_stats", timeout=2)
42
+ if response.ok:
43
+ return
44
+ except Exception as exc:
45
+ last_error = exc
46
+ time.sleep(1)
47
+ raise RuntimeError(f"ComfyUI did not start in time: {last_error}")
48
+
49
+
50
+ def _ensure_comfyui():
51
+ global _comfy_process
52
+
53
+ with _comfy_lock:
54
+ if _comfy_process is not None and _comfy_process.poll() is None:
55
+ return
56
+ try:
57
+ response = requests.get(f"{COMFY_URL}/system_stats", timeout=2)
58
+ if response.ok:
59
+ return
60
+ except Exception:
61
+ pass
62
+
63
+ if not COMFY_DIR.exists():
64
+ _run(["git", "clone", "--depth", "1", COMFY_REPO, str(COMFY_DIR)])
65
+
66
+ marker = COMFY_DIR / ".requirements-installed"
67
+ if not marker.exists():
68
+ _run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=COMFY_DIR)
69
+ marker.write_text("ok", encoding="utf-8")
70
+
71
+ checkpoint_dir = COMFY_DIR / "models" / "checkpoints"
72
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
73
+ hf_hub_download(
74
+ repo_id=MODEL_REPO,
75
+ filename=MODEL_FILE,
76
+ local_dir=str(checkpoint_dir),
77
+ token=os.environ.get("HF_TOKEN"),
78
+ )
 
 
 
 
 
 
79
 
80
+ cmd = [
81
+ sys.executable,
82
+ "main.py",
83
+ "--listen",
84
+ COMFY_HOST,
85
+ "--port",
86
+ str(COMFY_PORT),
87
+ "--disable-auto-launch",
88
+ ]
89
+ _comfy_process = subprocess.Popen(cmd, cwd=str(COMFY_DIR))
90
+ _wait_for_comfy()
91
+
92
+
93
+ def _build_workflow(prompt, negative_prompt, width, height, steps, cfg, seed, sampler, scheduler):
94
+ workflow = {
95
+ "1": {
96
+ "class_type": "CheckpointLoaderSimple",
97
+ "inputs": {"ckpt_name": MODEL_FILE},
98
+ },
99
+ "2": {
100
+ "class_type": "CLIPTextEncode",
101
+ "inputs": {"text": prompt, "clip": ["1", 1]},
102
+ },
103
+ "4": {
104
+ "class_type": "EmptyLatentImage",
105
+ "inputs": {"width": int(width), "height": int(height), "batch_size": 1},
106
+ },
107
+ "5": {
108
+ "class_type": "KSampler",
109
+ "inputs": {
110
+ "seed": int(seed),
111
+ "steps": int(steps),
112
+ "cfg": float(cfg),
113
+ "sampler_name": sampler,
114
+ "scheduler": scheduler,
115
+ "denoise": 1.0,
116
+ "model": ["1", 0],
117
+ "positive": ["2", 0],
118
+ "negative": ["3", 0],
119
+ "latent_image": ["4", 0],
120
+ },
121
+ },
122
+ "6": {
123
+ "class_type": "VAEDecode",
124
+ "inputs": {"samples": ["5", 0], "vae": ["1", 2]},
125
+ },
126
+ "7": {
127
+ "class_type": "SaveImage",
128
+ "inputs": {"filename_prefix": "redcraft", "images": ["6", 0]},
129
+ },
130
+ }
131
+
132
+ if negative_prompt and negative_prompt.strip():
133
+ workflow["3"] = {
134
+ "class_type": "CLIPTextEncode",
135
+ "inputs": {"text": negative_prompt, "clip": ["1", 1]},
136
+ }
137
+ else:
138
+ workflow["3"] = {
139
+ "class_type": "ConditioningZeroOut",
140
+ "inputs": {"conditioning": ["2", 0]},
141
+ }
142
+ return workflow
143
+
144
+
145
+ def _queue_prompt(workflow):
146
+ payload = {"prompt": workflow, "client_id": str(uuid.uuid4())}
147
+ response = requests.post(f"{COMFY_URL}/prompt", json=payload, timeout=30)
148
+ if not response.ok:
149
+ raise RuntimeError(f"ComfyUI prompt error {response.status_code}: {response.text[:1000]}")
150
+ return response.json()["prompt_id"]
151
+
152
+
153
+ def _wait_for_history(prompt_id, timeout=900):
154
+ deadline = time.time() + timeout
155
+ while time.time() < deadline:
156
+ response = requests.get(f"{COMFY_URL}/history/{prompt_id}", timeout=30)
157
+ response.raise_for_status()
158
+ history = response.json()
159
+ if prompt_id in history:
160
+ item = history[prompt_id]
161
+ status = item.get("status", {})
162
+ if status.get("completed"):
163
+ return item
164
+ messages = status.get("messages") or []
165
+ for message in messages:
166
+ if isinstance(message, list) and message and message[0] == "execution_error":
167
+ raise RuntimeError(json.dumps(message[1], indent=2)[:2000])
168
+ time.sleep(1)
169
+ raise RuntimeError("Timed out waiting for ComfyUI generation.")
170
+
171
+
172
+ def _load_output_image(history_item):
173
+ outputs = history_item.get("outputs", {})
174
+ for output in outputs.values():
175
+ for image in output.get("images", []):
176
+ params = {
177
+ "filename": image["filename"],
178
+ "subfolder": image.get("subfolder", ""),
179
+ "type": image.get("type", "output"),
180
+ }
181
+ response = requests.get(f"{COMFY_URL}/view", params=params, timeout=120)
182
+ response.raise_for_status()
183
+ temp_path = Path("/tmp") / f"{uuid.uuid4().hex}.png"
184
+ temp_path.write_bytes(response.content)
185
+ return Image.open(temp_path).convert("RGB")
186
+ raise RuntimeError("ComfyUI completed without returning an image.")
187
+
188
+
189
+ def _duration(prompt, negative_prompt, width, height, steps, cfg, seed, randomize_seed, sampler, scheduler):
190
  megapixels = max(1.0, (int(width) * int(height)) / (1024 * 1024))
191
+ return int(900 + int(steps) * 8 * megapixels)
192
 
193
 
194
+ @spaces.GPU(duration=_duration)
195
  def generate(
196
  prompt,
197
  negative_prompt="",
 
 
 
198
  width=1024,
199
  height=1024,
200
+ steps=10,
201
+ cfg=1.0,
202
+ seed=0,
203
+ randomize_seed=True,
204
+ sampler="er_sde",
205
+ scheduler="simple",
206
  ):
207
  if not prompt or not prompt.strip():
208
+ raise gr.Error("Enter a prompt.")
209
+ if randomize_seed:
210
  seed = random.randint(0, MAX_SEED)
211
+
 
 
212
  try:
213
+ _ensure_comfyui()
214
+ workflow = _build_workflow(prompt, negative_prompt, width, height, steps, cfg, seed, sampler, scheduler)
215
+ prompt_id = _queue_prompt(workflow)
216
+ history_item = _wait_for_history(prompt_id)
217
+ return _load_output_image(history_item), seed
218
+ except Exception as exc:
219
+ raise gr.Error(str(exc)) from exc
220
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  CSS = """
223
+ .gradio-container { max-width: 1120px !important; margin: 0 auto !important; }
224
+ #result-image { min-height: 520px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  """
226
 
227
+ with gr.Blocks(title="Redcraft Krea2", css=CSS) as demo:
228
+ gr.Markdown("# Redcraft Krea2")
229
+ gr.Markdown("Text-to-image generation through ComfyUI-native checkpoint loading.")
230
+
231
+ with gr.Row():
232
+ with gr.Column(scale=5):
233
+ prompt = gr.Textbox(label="Prompt", lines=5, placeholder="Describe the image to generate.")
234
+ negative_prompt = gr.Textbox(label="Negative prompt", lines=2, value="")
235
+ with gr.Row():
236
+ width = gr.Slider(512, 1536, value=1024, step=64, label="Width")
237
+ height = gr.Slider(512, 1536, value=1024, step=64, label="Height")
238
+ with gr.Row():
239
+ steps = gr.Slider(1, 30, value=10, step=1, label="Steps")
240
+ cfg = gr.Slider(0.0, 8.0, value=1.0, step=0.1, label="CFG")
241
+ with gr.Row():
242
+ seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
243
+ randomize_seed = gr.Checkbox(value=True, label="Randomize seed")
244
+ with gr.Accordion("Sampler", open=False):
245
+ sampler = gr.Dropdown(
246
+ ["er_sde", "euler", "euler_ancestral", "dpmpp_2m", "dpmpp_sde"],
247
+ value="er_sde",
248
+ label="Sampler",
 
 
 
 
 
 
 
 
249
  )
250
+ scheduler = gr.Dropdown(["simple", "normal", "karras", "exponential"], value="simple", label="Scheduler")
251
+ run = gr.Button("Generate", variant="primary")
252
+ with gr.Column(scale=6):
253
+ output = gr.Image(label="Result", format="png", elem_id="result-image")
 
254
 
255
+ inputs = [prompt, negative_prompt, width, height, steps, cfg, seed, randomize_seed, sampler, scheduler]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  run.click(generate, inputs, [output, seed])
257
  prompt.submit(generate, inputs, [output, seed])
258
 
259
+
260
+ if __name__ == "__main__":
261
+ demo.queue().launch()