Gerchegg commited on
Commit
93ffa7c
·
verified ·
1 Parent(s): 9f4ae9c

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +646 -0
app.py ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import random
4
+ import json
5
+ import torch
6
+ import cv2
7
+ from PIL import Image
8
+ import spaces
9
+ import os
10
+ import time
11
+ import logging
12
+
13
+ from diffusers import (
14
+ DiffusionPipeline,
15
+ QwenImageControlNetPipeline,
16
+ QwenImageControlNetModel,
17
+ AutoPipelineForImage2Image
18
+ )
19
+ from huggingface_hub import hf_hub_download
20
+
21
+ # Настройка логирования
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format='%(asctime)s | %(levelname)s | %(message)s',
25
+ datefmt='%Y-%m-%d %H:%M:%S'
26
+ )
27
+ logger = logging.getLogger(__name__)
28
+
29
+ logger.info("=" * 60)
30
+ logger.info("LOADING QWEN-SOLOBAND ADVANCED")
31
+ logger.info("=" * 60)
32
+
33
+ hf_token = os.environ.get("HF_TOKEN")
34
+ device = "cuda" if torch.cuda.is_available() else "cpu"
35
+ dtype = torch.bfloat16
36
+
37
+ # Логируем GPU
38
+ logger.info(f"CUDA available: {torch.cuda.is_available()}")
39
+ if torch.cuda.is_available():
40
+ gpu_count = torch.cuda.device_count()
41
+ logger.info(f"Number of GPUs: {gpu_count}")
42
+ for i in range(gpu_count):
43
+ logger.info(f" GPU {i}: {torch.cuda.get_device_name(i)}")
44
+ logger.info(f" Memory: {torch.cuda.get_device_properties(i).total_memory / 1024**3:.1f} GB")
45
+
46
+ # =================================================================
47
+ # ЗАГРУЗКА МОДЕЛЕЙ
48
+ # =================================================================
49
+
50
+ # 1. Базовая модель для Text-to-Image
51
+ logger.info("\n[1/3] Loading base Text2Image model...")
52
+ model_id = "Gerchegg/Qwen-Soloband-Diffusers"
53
+
54
+ try:
55
+ start_time = time.time()
56
+
57
+ # Определяем device_map
58
+ if gpu_count > 1:
59
+ device_map = "balanced"
60
+ logger.info(f" Device map: balanced ({gpu_count} GPUs)")
61
+ else:
62
+ device_map = None
63
+ logger.info(" Device map: single GPU")
64
+
65
+ # Загружаем базовую модель
66
+ pipe_txt2img = DiffusionPipeline.from_pretrained(
67
+ model_id,
68
+ torch_dtype=dtype,
69
+ device_map=device_map,
70
+ token=hf_token
71
+ )
72
+
73
+ if device_map is None:
74
+ pipe_txt2img.to(device)
75
+
76
+ load_time = time.time() - start_time
77
+ logger.info(f" ✓ Text2Image loaded in {load_time:.1f}s")
78
+
79
+ except Exception as e:
80
+ logger.error(f" ❌ Error loading Text2Image: {e}")
81
+ raise
82
+
83
+ # 2. Image-to-Image модель (используем ту же базу)
84
+ logger.info("\n[2/3] Creating Image2Image pipeline...")
85
+ try:
86
+ pipe_img2img = AutoPipelineForImage2Image.from_pipe(pipe_txt2img)
87
+ logger.info(" ✓ Image2Image pipeline created")
88
+ except Exception as e:
89
+ logger.error(f" ❌ Error creating Image2Image: {e}")
90
+ pipe_img2img = None
91
+
92
+ # 3. ControlNet модель
93
+ logger.info("\n[3/3] Loading ControlNet model...")
94
+ try:
95
+ controlnet_model_id = "InstantX/Qwen-Image-ControlNet-Union"
96
+
97
+ controlnet = QwenImageControlNetModel.from_pretrained(
98
+ controlnet_model_id,
99
+ torch_dtype=dtype,
100
+ token=hf_token
101
+ )
102
+
103
+ # Создаем ControlNet pipeline на базе базовой модели
104
+ pipe_controlnet = QwenImageControlNetPipeline.from_pretrained(
105
+ model_id,
106
+ controlnet=controlnet,
107
+ torch_dtype=dtype,
108
+ token=hf_token
109
+ )
110
+
111
+ if device_map is None:
112
+ pipe_controlnet.to(device)
113
+
114
+ logger.info(" ✓ ControlNet loaded")
115
+
116
+ except Exception as e:
117
+ logger.error(f" ❌ Error loading ControlNet: {e}")
118
+ logger.warning(" ControlNet will be disabled")
119
+ pipe_controlnet = None
120
+
121
+ # Оптимизации памяти
122
+ logger.info("\nApplying memory optimizations...")
123
+ for pipe in [pipe_txt2img, pipe_img2img, pipe_controlnet]:
124
+ if pipe and hasattr(pipe, 'vae'):
125
+ if hasattr(pipe.vae, 'enable_tiling'):
126
+ pipe.vae.enable_tiling()
127
+ if hasattr(pipe.vae, 'enable_slicing'):
128
+ pipe.vae.enable_slicing()
129
+
130
+ logger.info(" ✓ VAE tiling and slicing enabled")
131
+
132
+ logger.info("\n" + "=" * 60)
133
+ logger.info("✓ ALL MODELS LOADED")
134
+ logger.info("=" * 60)
135
+
136
+ # =================================================================
137
+ # PREPROCESSOR FUNCTIONS
138
+ # =================================================================
139
+
140
+ def resize_image(input_image, max_size=1024):
141
+ """Изменяет размер изображения с сохранением пропорций (кратно 8)"""
142
+ w, h = input_image.size
143
+ aspect_ratio = w / h
144
+
145
+ if w > h:
146
+ new_w = max_size
147
+ new_h = int(new_w / aspect_ratio)
148
+ else:
149
+ new_h = max_size
150
+ new_w = int(new_h * aspect_ratio)
151
+
152
+ # Кратно 8
153
+ new_w = new_w - (new_w % 8)
154
+ new_h = new_h - (new_h % 8)
155
+
156
+ if new_w == 0: new_w = 8
157
+ if new_h == 0: new_h = 8
158
+
159
+ return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
160
+
161
+ def extract_canny(input_image, low_threshold=100, high_threshold=200):
162
+ """Canny edge detection"""
163
+ image = np.array(input_image)
164
+ edges = cv2.Canny(image, low_threshold, high_threshold)
165
+ edges = edges[:, :, None]
166
+ edges = np.concatenate([edges, edges, edges], axis=2)
167
+ return Image.fromarray(edges)
168
+
169
+ def extract_depth(input_image):
170
+ """Depth map extraction (простая версия через grayscale)"""
171
+ # Для полноценного depth нужна модель Depth-Anything
172
+ # Упрощенная версия для демонстрации
173
+ gray = input_image.convert('L')
174
+ return gray.convert('RGB')
175
+
176
+ def extract_pose(input_image):
177
+ """Pose detection (заглушка - нужна модель OpenPose)"""
178
+ # Для полноценного pose нужна модель OpenPose
179
+ # Возвращаем Canny как fallback
180
+ return extract_canny(input_image)
181
+
182
+ def get_control_image(input_image, control_type):
183
+ """Применяет препроцессор к изображению"""
184
+ if control_type == "Canny":
185
+ return extract_canny(input_image)
186
+ elif control_type == "Depth":
187
+ return extract_depth(input_image)
188
+ elif control_type == "Pose":
189
+ return extract_pose(input_image)
190
+ else:
191
+ return extract_canny(input_image) # Fallback
192
+
193
+ # =================================================================
194
+ # LORA FUNCTIONS
195
+ # =================================================================
196
+
197
+ # Список доступных LoRA
198
+ AVAILABLE_LORAS = {
199
+ "Realism": {
200
+ "repo": "flymy-ai/qwen-image-realism-lora",
201
+ "trigger": "Super Realism portrait of",
202
+ "weights": "pytorch_lora_weights.safetensors"
203
+ },
204
+ "Anime": {
205
+ "repo": "alfredplpl/qwen-image-modern-anime-lora",
206
+ "trigger": "Japanese modern anime style, ",
207
+ "weights": "pytorch_lora_weights.safetensors"
208
+ },
209
+ "Analog Film": {
210
+ "repo": "janekm/analog_film",
211
+ "trigger": "fifthel",
212
+ "weights": "converted_complete.safetensors"
213
+ }
214
+ }
215
+
216
+ # =================================================================
217
+ # GENERATION FUNCTIONS
218
+ # =================================================================
219
+
220
+ MAX_SEED = np.iinfo(np.int32).max
221
+
222
+ @spaces.GPU(duration=180)
223
+ def generate_text2img(
224
+ prompt,
225
+ negative_prompt=" ",
226
+ width=1664,
227
+ height=928,
228
+ seed=42,
229
+ randomize_seed=False,
230
+ guidance_scale=2.5,
231
+ num_inference_steps=40,
232
+ lora_name="None",
233
+ lora_scale=1.0,
234
+ progress=gr.Progress(track_tqdm=True)
235
+ ):
236
+ """Text-to-Image генерация"""
237
+
238
+ logger.info("\n" + "=" * 60)
239
+ logger.info("TEXT-TO-IMAGE GENERATION")
240
+ logger.info("=" * 60)
241
+
242
+ if randomize_seed:
243
+ seed = random.randint(0, MAX_SEED)
244
+
245
+ logger.info(f" Prompt: {prompt[:100]}...")
246
+ logger.info(f" Size: {width}x{height}")
247
+ logger.info(f" Steps: {num_inference_steps}, CFG: {guidance_scale}")
248
+ logger.info(f" Seed: {seed}")
249
+ logger.info(f" LoRA: {lora_name} (scale: {lora_scale})")
250
+
251
+ try:
252
+ # Загружаем LoRA если выбрана
253
+ if lora_name != "None" and lora_name in AVAILABLE_LORAS:
254
+ lora_info = AVAILABLE_LORAS[lora_name]
255
+ logger.info(f" Loading LoRA: {lora_info['repo']}")
256
+
257
+ pipe_txt2img.load_lora_weights(
258
+ lora_info['repo'],
259
+ weight_name=lora_info.get('weights', 'pytorch_lora_weights.safetensors'),
260
+ token=hf_token
261
+ )
262
+
263
+ # Добавляем trigger word
264
+ if lora_info['trigger']:
265
+ prompt = lora_info['trigger'] + prompt
266
+ logger.info(f" Added trigger: {lora_info['trigger']}")
267
+
268
+ generator = torch.Generator(device=device).manual_seed(seed)
269
+
270
+ image = pipe_txt2img(
271
+ prompt=prompt,
272
+ negative_prompt=negative_prompt,
273
+ width=width,
274
+ height=height,
275
+ num_inference_steps=num_inference_steps,
276
+ true_cfg_scale=guidance_scale,
277
+ generator=generator
278
+ ).images[0]
279
+
280
+ # Выгружаем LoRA после генерации
281
+ if lora_name != "None":
282
+ pipe_txt2img.unload_lora_weights()
283
+
284
+ logger.info(" ✓ Generation completed")
285
+
286
+ return image, seed
287
+
288
+ except Exception as e:
289
+ logger.error(f" ❌ Error: {e}")
290
+ raise
291
+
292
+ @spaces.GPU(duration=180)
293
+ def generate_img2img(
294
+ input_image,
295
+ prompt,
296
+ negative_prompt=" ",
297
+ strength=0.75,
298
+ seed=42,
299
+ randomize_seed=False,
300
+ guidance_scale=2.5,
301
+ num_inference_steps=40,
302
+ lora_name="None",
303
+ lora_scale=1.0,
304
+ progress=gr.Progress(track_tqdm=True)
305
+ ):
306
+ """Image-to-Image генерация"""
307
+
308
+ logger.info("\n" + "=" * 60)
309
+ logger.info("IMAGE-TO-IMAGE GENERATION")
310
+ logger.info("=" * 60)
311
+
312
+ if input_image is None:
313
+ raise gr.Error("Please upload an input image")
314
+
315
+ if randomize_seed:
316
+ seed = random.randint(0, MAX_SEED)
317
+
318
+ # Изменяем размер изображения
319
+ resized = resize_image(input_image, max_size=1024)
320
+
321
+ logger.info(f" Prompt: {prompt[:100]}...")
322
+ logger.info(f" Input size: {input_image.size} → {resized.size}")
323
+ logger.info(f" Strength: {strength}")
324
+ logger.info(f" Steps: {num_inference_steps}, CFG: {guidance_scale}")
325
+ logger.info(f" LoRA: {lora_name}")
326
+
327
+ try:
328
+ if pipe_img2img is None:
329
+ raise gr.Error("Image2Image pipeline not available")
330
+
331
+ # Загружаем LoRA если выбрана
332
+ if lora_name != "None" and lora_name in AVAILABLE_LORAS:
333
+ lora_info = AVAILABLE_LORAS[lora_name]
334
+ pipe_img2img.load_lora_weights(
335
+ lora_info['repo'],
336
+ weight_name=lora_info.get('weights', 'pytorch_lora_weights.safetensors'),
337
+ token=hf_token
338
+ )
339
+ if lora_info['trigger']:
340
+ prompt = lora_info['trigger'] + prompt
341
+
342
+ generator = torch.Generator(device=device).manual_seed(seed)
343
+
344
+ image = pipe_img2img(
345
+ prompt=prompt,
346
+ negative_prompt=negative_prompt,
347
+ image=resized,
348
+ strength=strength,
349
+ num_inference_steps=num_inference_steps,
350
+ true_cfg_scale=guidance_scale,
351
+ generator=generator
352
+ ).images[0]
353
+
354
+ # Выгружаем LoRA
355
+ if lora_name != "None":
356
+ pipe_img2img.unload_lora_weights()
357
+
358
+ logger.info(" ✓ Generation completed")
359
+
360
+ return image, seed
361
+
362
+ except Exception as e:
363
+ logger.error(f" ❌ Error: {e}")
364
+ raise
365
+
366
+ @spaces.GPU(duration=180)
367
+ def generate_controlnet(
368
+ input_image,
369
+ prompt,
370
+ control_type="Canny",
371
+ negative_prompt=" ",
372
+ controlnet_scale=1.0,
373
+ seed=42,
374
+ randomize_seed=False,
375
+ guidance_scale=5.0,
376
+ num_inference_steps=30,
377
+ lora_name="None",
378
+ lora_scale=1.0,
379
+ progress=gr.Progress(track_tqdm=True)
380
+ ):
381
+ """ControlNet генерация"""
382
+
383
+ logger.info("\n" + "=" * 60)
384
+ logger.info("CONTROLNET GENERATION")
385
+ logger.info("=" * 60)
386
+
387
+ if input_image is None:
388
+ raise gr.Error("Please upload an input image")
389
+
390
+ if pipe_controlnet is None:
391
+ raise gr.Error("ControlNet pipeline not available")
392
+
393
+ if randomize_seed:
394
+ seed = random.randint(0, MAX_SEED)
395
+
396
+ # Изменяем размер и применяем препроцессор
397
+ resized = resize_image(input_image, max_size=1024)
398
+ control_image = get_control_image(resized, control_type)
399
+
400
+ logger.info(f" Prompt: {prompt[:100]}...")
401
+ logger.info(f" Control type: {control_type}")
402
+ logger.info(f" Control scale: {controlnet_scale}")
403
+ logger.info(f" Image size: {resized.size}")
404
+ logger.info(f" LoRA: {lora_name}")
405
+
406
+ try:
407
+ # Загружаем LoRA если выбрана
408
+ if lora_name != "None" and lora_name in AVAILABLE_LORAS:
409
+ lora_info = AVAILABLE_LORAS[lora_name]
410
+ pipe_controlnet.load_lora_weights(
411
+ lora_info['repo'],
412
+ weight_name=lora_info.get('weights', 'pytorch_lora_weights.safetensors'),
413
+ token=hf_token
414
+ )
415
+ if lora_info['trigger']:
416
+ prompt = lora_info['trigger'] + prompt
417
+
418
+ generator = torch.Generator(device=device).manual_seed(seed)
419
+
420
+ image = pipe_controlnet(
421
+ prompt=prompt,
422
+ negative_prompt=negative_prompt,
423
+ control_image=control_image,
424
+ controlnet_conditioning_scale=controlnet_scale,
425
+ width=resized.width,
426
+ height=resized.height,
427
+ num_inference_steps=num_inference_steps,
428
+ guidance_scale=guidance_scale,
429
+ generator=generator
430
+ ).images[0]
431
+
432
+ # Выгружаем LoRA
433
+ if lora_name != "None":
434
+ pipe_controlnet.unload_lora_weights()
435
+
436
+ logger.info(" ✓ Generation completed")
437
+
438
+ return image, control_image, seed
439
+
440
+ except Exception as e:
441
+ logger.error(f" ❌ Error: {e}")
442
+ raise
443
+
444
+ # =================================================================
445
+ # GRADIO INTERFACE
446
+ # =================================================================
447
+
448
+ MAX_SEED = np.iinfo(np.int32).max
449
+
450
+ css = """
451
+ #col-container {
452
+ margin: 0 auto;
453
+ max-width: 1400px;
454
+ }
455
+ """
456
+
457
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
458
+ gr.Markdown("""
459
+ # 🎨 Qwen Soloband - Image2Image + ControlNet + LoRA
460
+
461
+ **Продвинутая модель генерации** с поддержкой Image-to-Image, ControlNet и LoRA.
462
+
463
+ ### ✨ Возможности:
464
+ - 🖼️ **Text-to-Image** - Генерация из текста
465
+ - 🔄 **Image-to-Image** - Модификация изображений (denoising strength)
466
+ - 🎮 **ControlNet** - Управление структурой (Canny, Depth, Pose)
467
+ - 🎭 **LoRA** - Стилизация (Realism, Anime, Film)
468
+ - 🔌 **Full API** - Все функции доступны через API
469
+
470
+ **Модель**: [Gerchegg/Qwen-Soloband-Diffusers](https://huggingface.co/Gerchegg/Qwen-Soloband-Diffusers)
471
+ """)
472
+
473
+ with gr.Tabs() as tabs:
474
+
475
+ # TAB 1: Text-to-Image
476
+ with gr.Tab("📝 Text-to-Image"):
477
+ with gr.Row():
478
+ with gr.Column(scale=1):
479
+ t2i_prompt = gr.Text(
480
+ label="Prompt",
481
+ placeholder="SB_AI, a beautiful landscape...",
482
+ lines=3
483
+ )
484
+
485
+ t2i_run = gr.Button("Generate", variant="primary")
486
+
487
+ with gr.Accordion("Advanced Settings", open=False):
488
+ t2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
489
+
490
+ with gr.Row():
491
+ t2i_width = gr.Slider(label="Width", minimum=512, maximum=2048, step=64, value=1664)
492
+ t2i_height = gr.Slider(label="Height", minimum=512, maximum=2048, step=64, value=928)
493
+
494
+ with gr.Row():
495
+ t2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
496
+ t2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
497
+
498
+ with gr.Row():
499
+ t2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
500
+ t2i_random_seed = gr.Checkbox(label="Random", value=True)
501
+
502
+ t2i_lora = gr.Radio(
503
+ label="LoRA Style",
504
+ choices=["None"] + list(AVAILABLE_LORAS.keys()),
505
+ value="None"
506
+ )
507
+ t2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
508
+
509
+ with gr.Column(scale=1):
510
+ t2i_output = gr.Image(label="Generated Image")
511
+ t2i_seed_output = gr.Number(label="Used Seed")
512
+
513
+ # TAB 2: Image-to-Image
514
+ with gr.Tab("🔄 Image-to-Image"):
515
+ with gr.Row():
516
+ with gr.Column(scale=1):
517
+ i2i_input = gr.Image(type="pil", label="Input Image")
518
+ i2i_prompt = gr.Text(
519
+ label="Prompt",
520
+ placeholder="Transform this image into...",
521
+ lines=3
522
+ )
523
+
524
+ i2i_strength = gr.Slider(
525
+ label="Denoising Strength",
526
+ info="0.0 = original image, 1.0 = complete redraw",
527
+ minimum=0.0,
528
+ maximum=1.0,
529
+ step=0.05,
530
+ value=0.75
531
+ )
532
+
533
+ i2i_run = gr.Button("Generate", variant="primary")
534
+
535
+ with gr.Accordion("Advanced Settings", open=False):
536
+ i2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
537
+
538
+ with gr.Row():
539
+ i2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
540
+ i2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
541
+
542
+ with gr.Row():
543
+ i2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
544
+ i2i_random_seed = gr.Checkbox(label="Random", value=True)
545
+
546
+ i2i_lora = gr.Radio(
547
+ label="LoRA Style",
548
+ choices=["None"] + list(AVAILABLE_LORAS.keys()),
549
+ value="None"
550
+ )
551
+ i2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
552
+
553
+ with gr.Column(scale=1):
554
+ i2i_output = gr.Image(label="Generated Image")
555
+ i2i_seed_output = gr.Number(label="Used Seed")
556
+
557
+ # TAB 3: ControlNet
558
+ with gr.Tab("🎮 ControlNet"):
559
+ with gr.Row():
560
+ with gr.Column(scale=1):
561
+ cn_input = gr.Image(type="pil", label="Input Image")
562
+ cn_prompt = gr.Text(
563
+ label="Prompt",
564
+ placeholder="A detailed description...",
565
+ lines=3
566
+ )
567
+
568
+ cn_control_type = gr.Radio(
569
+ label="Control Type (Preprocessor)",
570
+ choices=["Canny", "Depth", "Pose"],
571
+ value="Canny"
572
+ )
573
+
574
+ cn_control_scale = gr.Slider(
575
+ label="Control Strength",
576
+ minimum=0.0,
577
+ maximum=2.0,
578
+ step=0.05,
579
+ value=1.0
580
+ )
581
+
582
+ cn_run = gr.Button("Generate", variant="primary")
583
+
584
+ with gr.Accordion("Advanced Settings", open=False):
585
+ cn_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
586
+
587
+ with gr.Row():
588
+ cn_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=30)
589
+ cn_cfg = gr.Slider(label="CFG", minimum=1.0, maximum=10.0, step=0.1, value=5.0)
590
+
591
+ with gr.Row():
592
+ cn_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
593
+ cn_random_seed = gr.Checkbox(label="Random", value=True)
594
+
595
+ cn_lora = gr.Radio(
596
+ label="LoRA Style",
597
+ choices=["None"] + list(AVAILABLE_LORAS.keys()),
598
+ value="None"
599
+ )
600
+ cn_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
601
+
602
+ with gr.Column(scale=1):
603
+ cn_control_preview = gr.Image(label="Control Image (Preprocessed)")
604
+ cn_output = gr.Image(label="Generated Image")
605
+ cn_seed_output = gr.Number(label="Used Seed")
606
+
607
+ # Event handlers
608
+ t2i_run.click(
609
+ fn=generate_text2img,
610
+ inputs=[
611
+ t2i_prompt, t2i_negative, t2i_width, t2i_height,
612
+ t2i_seed, t2i_random_seed, t2i_cfg, t2i_steps,
613
+ t2i_lora, t2i_lora_scale
614
+ ],
615
+ outputs=[t2i_output, t2i_seed_output],
616
+ api_name="text2img"
617
+ )
618
+
619
+ i2i_run.click(
620
+ fn=generate_img2img,
621
+ inputs=[
622
+ i2i_input, i2i_prompt, i2i_negative, i2i_strength,
623
+ i2i_seed, i2i_random_seed, i2i_cfg, i2i_steps,
624
+ i2i_lora, i2i_lora_scale
625
+ ],
626
+ outputs=[i2i_output, i2i_seed_output],
627
+ api_name="img2img"
628
+ )
629
+
630
+ cn_run.click(
631
+ fn=generate_controlnet,
632
+ inputs=[
633
+ cn_input, cn_prompt, cn_control_type, cn_negative, cn_control_scale,
634
+ cn_seed, cn_random_seed, cn_cfg, cn_steps,
635
+ cn_lora, cn_lora_scale
636
+ ],
637
+ outputs=[cn_output, cn_control_preview, cn_seed_output],
638
+ api_name="controlnet"
639
+ )
640
+
641
+ if __name__ == "__main__":
642
+ demo.launch(
643
+ show_api=True,
644
+ share=False
645
+ )
646
+