Gerchegg commited on
Commit
c0fe752
·
verified ·
1 Parent(s): 9efb67d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +814 -814
app.py CHANGED
@@ -1,814 +1,814 @@
1
- import gradio as gr
2
- import numpy as np
3
- import random
4
- import json
5
- import torch
6
- from PIL import Image
7
- import os
8
- import time
9
- import logging
10
-
11
- # Опциональный импорт spaces для работы в Runpod
12
- try:
13
- import spaces
14
- SPACES_AVAILABLE = True
15
- except ImportError:
16
- SPACES_AVAILABLE = False
17
-
18
- from diffusers import (
19
- DiffusionPipeline,
20
- QwenImageImg2ImgPipeline
21
- )
22
- from huggingface_hub import hf_hub_download
23
-
24
- # Настройка логирования
25
- from io import StringIO
26
-
27
- # Создаем буфер для логов
28
- log_buffer = StringIO()
29
-
30
- # Настраиваем логирование с выводом в консоль и буфер
31
- logging.basicConfig(
32
- level=logging.INFO,
33
- format='%(asctime)s | %(levelname)s | %(message)s',
34
- datefmt='%Y-%m-%d %H:%M:%S',
35
- handlers=[
36
- logging.StreamHandler(), # Вывод в консоль
37
- logging.StreamHandler(log_buffer) # Вывод в буфер для UI
38
- ]
39
- )
40
- logger = logging.getLogger(__name__)
41
-
42
- # Логируем если spaces не доступен
43
- if not SPACES_AVAILABLE:
44
- logger.warning("⚠️ spaces module not available - running without ZeroGPU support")
45
-
46
- def get_logs():
47
- """Получить накопленные логи"""
48
- return log_buffer.getvalue()
49
-
50
- def clear_logs():
51
- """Очистить буфер логов"""
52
- global log_buffer
53
- log_buffer = StringIO()
54
- # Обновляем handlers
55
- for handler in logger.handlers:
56
- if isinstance(handler.stream, StringIO):
57
- logger.removeHandler(handler)
58
- new_handler = logging.StreamHandler(log_buffer)
59
- new_handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s', '%Y-%m-%d %H:%M:%S'))
60
- logger.addHandler(new_handler)
61
- return ""
62
-
63
- logger.info("=" * 60)
64
- logger.info("LOADING QWEN-SOLOBAND ADVANCED v2.1")
65
- logger.info("With detailed logging and fixed img2img resize")
66
- logger.info("=" * 60)
67
-
68
- hf_token = os.environ.get("HF_TOKEN")
69
- device = "cuda" if torch.cuda.is_available() else "cpu"
70
- dtype = torch.bfloat16
71
-
72
- # Логируем GPU
73
- logger.info(f"CUDA available: {torch.cuda.is_available()}")
74
- if torch.cuda.is_available():
75
- gpu_count = torch.cuda.device_count()
76
- logger.info(f"Number of GPUs: {gpu_count}")
77
- for i in range(gpu_count):
78
- logger.info(f" GPU {i}: {torch.cuda.get_device_name(i)}")
79
- logger.info(f" Memory: {torch.cuda.get_device_properties(i).total_memory / 1024**3:.1f} GB")
80
- else:
81
- gpu_count = 0
82
- logger.info("Running on CPU - no GPUs available")
83
-
84
- # =================================================================
85
- # ЗАГРУЗКА МОДЕЛЕЙ
86
- # =================================================================
87
-
88
- # 1. Базовая модель для Text-to-Image
89
- logger.info("\n[1/3] Loading base Text2Image model...")
90
- model_id = os.environ.get("MODEL_ID", "Gerchegg/Qwen-Soloband-Diffusers")
91
- model_revision = os.environ.get("MODEL_REVISION", "main")
92
-
93
- try:
94
- start_time = time.time()
95
-
96
- # Определяем device_map
97
- if gpu_count > 1:
98
- device_map = "balanced"
99
- logger.info(f" Device map: balanced ({gpu_count} GPUs)")
100
- else:
101
- device_map = None
102
- logger.info(" Device map: single GPU")
103
-
104
- # Загружаем базовую модель
105
- load_kwargs = {
106
- "torch_dtype": dtype,
107
- "device_map": device_map,
108
- "token": hf_token,
109
- "cache_dir": os.environ.get("HF_HOME", "/workspace/.cache/huggingface")
110
- }
111
- if model_revision:
112
- load_kwargs["revision"] = model_revision
113
-
114
- logger.info(f" Loading model: {model_id}")
115
- logger.info(f" Device map: {device_map}")
116
- logger.info(f" Dtype: {dtype}")
117
-
118
- pipe_txt2img = DiffusionPipeline.from_pretrained(model_id, **load_kwargs)
119
-
120
- if device_map is None:
121
- pipe_txt2img.to(device)
122
-
123
- load_time = time.time() - start_time
124
- logger.info(f" ✓ Text2Image loaded in {load_time:.1f}s")
125
-
126
- except Exception as e:
127
- logger.error(f" ❌ Error loading Text2Image: {e}")
128
- raise
129
-
130
- # 2. Image-to-Image модель (используем те же компоненты)
131
- logger.info("\n[2/3] Creating Image2Image pipeline...")
132
- try:
133
- # Создаем QwenImageImg2ImgPipeline переиспользуя компоненты Text2Image pipeline
134
- # Это правильный способ для Qwen-Image архитектуры
135
- pipe_img2img = QwenImageImg2ImgPipeline(
136
- vae=pipe_txt2img.vae,
137
- text_encoder=pipe_txt2img.text_encoder,
138
- tokenizer=pipe_txt2img.tokenizer,
139
- transformer=pipe_txt2img.transformer,
140
- scheduler=pipe_txt2img.scheduler
141
- )
142
- logger.info(" ✓ Image2Image pipeline created (reusing components)")
143
- except Exception as e:
144
- logger.error(f" ❌ Error creating Image2Image: {e}")
145
- pipe_img2img = None
146
-
147
- # ControlNet не используется - убран для упрощения
148
-
149
- # Оптимизации памяти
150
- logger.info("\nApplying memory optimizations...")
151
- for pipe in [pipe_txt2img, pipe_img2img]:
152
- if pipe and hasattr(pipe, 'vae'):
153
- if hasattr(pipe.vae, 'enable_tiling'):
154
- pipe.vae.enable_tiling()
155
- if hasattr(pipe.vae, 'enable_slicing'):
156
- pipe.vae.enable_slicing()
157
-
158
- logger.info(" ✓ VAE tiling and slicing enabled")
159
-
160
- logger.info("\n" + "=" * 60)
161
- logger.info("✓ ALL MODELS LOADED")
162
- logger.info("=" * 60)
163
-
164
- # =================================================================
165
- # HELPER FUNCTIONS
166
- # =================================================================
167
-
168
- def resize_image(input_image, max_size=2048):
169
- """
170
- Изменяет размер изображения с сохранением пропорций (кратно 16).
171
- Если изображение меньше max_size, оставляет оригинальный размер (с округлением до 16).
172
- """
173
- w, h = input_image.size
174
- logger.info(f"[RESIZE] Входное изображение: {w}×{h}, max_size={max_size}")
175
-
176
- # Если изображение уже меньше max_size, просто округляем до 16
177
- if w <= max_size and h <= max_size:
178
- new_w = w - (w % 16)
179
- new_h = h - (h % 16)
180
- if new_w == 0: new_w = 16
181
- if new_h == 0: new_h = 16
182
- if new_w != w or new_h != h:
183
- logger.info(f"[RESIZE] Округление до кратного 16: {w}×{h} → {new_w}×{new_h}")
184
- return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
185
- logger.info(f"[RESIZE] Размер уже кратен 16, изменение не требуется")
186
- return input_image
187
-
188
- # Определяем масштабирование с сохранением пропорций
189
- scale = min(max_size / w, max_size / h)
190
- new_w = int(w * scale)
191
- new_h = int(h * scale)
192
- logger.info(f"[RESIZE] Масштабирование: scale={scale:.3f}, промежуточный размер: {new_w}×{new_h}")
193
-
194
- # Округляем до ближайшего кратного 16
195
- new_w = new_w - (new_w % 16)
196
- new_h = new_h - (new_h % 16)
197
-
198
- # Минимальные размеры
199
- if new_w < 16: new_w = 16
200
- if new_h < 16: new_h = 16
201
-
202
- logger.info(f"[RESIZE] Финальный размер после округления: {new_w}×{new_h}")
203
- aspect_original = w / h
204
- aspect_new = new_w / new_h
205
- logger.info(f"[RESIZE] Соотношение сторон: {aspect_original:.3f} → {aspect_new:.3f} (разница: {abs(aspect_original - aspect_new):.3f})")
206
-
207
- return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
208
-
209
- # =================================================================
210
- # LORA FUNCTIONS
211
- # =================================================================
212
-
213
- # Папка для локальных LoRA
214
- LOCAL_LORA_DIR = "/workspace/loras"
215
-
216
- # Базовые LoRA из HuggingFace Hub (загружаются по требованию)
217
- HUB_LORAS = {
218
- "Realism": {
219
- "repo": "flymy-ai/qwen-image-realism-lora",
220
- "trigger": "Super Realism portrait of",
221
- "weights": "pytorch_lora_weights.safetensors",
222
- "source": "hub"
223
- },
224
- "Anime": {
225
- "repo": "alfredplpl/qwen-image-modern-anime-lora",
226
- "trigger": "Japanese modern anime style, ",
227
- "weights": "pytorch_lora_weights.safetensors",
228
- "source": "hub"
229
- }
230
- # Другие LoRA положите в /workspace/loras/ как .safetensors файлы
231
- }
232
-
233
- def scan_local_loras():
234
- """
235
- Сканирует папку /workspace/loras на наличие .safetensors файлов
236
- Возвращает dict с найденными LoRA
237
- """
238
- local_loras = {}
239
-
240
- if not os.path.exists(LOCAL_LORA_DIR):
241
- logger.info(f" Local LoRA directory not found: {LOCAL_LORA_DIR}")
242
- return local_loras
243
-
244
- logger.info(f" Scanning local LoRA directory: {LOCAL_LORA_DIR}")
245
-
246
- try:
247
- for file in os.listdir(LOCAL_LORA_DIR):
248
- if file.endswith('.safetensors'):
249
- lora_name = os.path.splitext(file)[0] # Имя без расширения
250
- local_path = os.path.join(LOCAL_LORA_DIR, file)
251
-
252
- # Добавляем в список
253
- local_loras[lora_name] = {
254
- "path": local_path,
255
- "trigger": "", # Без trigger word для локальных
256
- "weights": file,
257
- "source": "local"
258
- }
259
-
260
- logger.info(f" ✓ Found local LoRA: {lora_name} ({file})")
261
-
262
- except Exception as e:
263
- logger.warning(f" Error scanning local LoRA directory: {e}")
264
-
265
- return local_loras
266
-
267
- # Сканируем локальные LoRA
268
- logger.info("\nScanning for LoRA models...")
269
- LOCAL_LORAS = scan_local_loras()
270
-
271
- # Объединяем Hub и локальные LoRA
272
- AVAILABLE_LORAS = {**HUB_LORAS, **LOCAL_LORAS}
273
-
274
- if LOCAL_LORAS:
275
- logger.info(f" ✓ Found {len(LOCAL_LORAS)} local LoRA(s)")
276
- logger.info(f" Total available LoRAs: {len(AVAILABLE_LORAS)}")
277
-
278
- def load_lora_weights(pipeline, lora_name, lora_scale, hf_token):
279
- """
280
- Загружает LoRA веса в pipeline (ленивая загрузка)
281
- Hub LoRA скачиваются только при использовании
282
- Локальные LoRA загружаются из /workspace/loras/
283
- """
284
- if lora_name == "None" or lora_name not in AVAILABLE_LORAS:
285
- logger.info(f"[LORA] Пропуск загрузки: lora_name='{lora_name}'")
286
- return None
287
-
288
- lora_info = AVAILABLE_LORAS[lora_name]
289
- logger.info(f"[LORA] Начало загрузки: {lora_name}")
290
- logger.info(f"[LORA] Source: {lora_info['source']}")
291
- logger.info(f"[LORA] Scale: {lora_scale}")
292
-
293
- try:
294
- load_start = time.time()
295
-
296
- if lora_info['source'] == 'hub':
297
- # Ленивая загрузка с HuggingFace Hub (скачивается при первом использовании)
298
- logger.info(f"[LORA] Загрузка из Hub: {lora_info['repo']}")
299
- logger.info(f"[LORA] Weight file: {lora_info.get('weights', 'pytorch_lora_weights.safetensors')}")
300
- logger.info(f"[LORA] (Скачивается если не в кэше...)")
301
-
302
- pipeline.load_lora_weights(
303
- lora_info['repo'],
304
- weight_name=lora_info.get('weights', 'pytorch_lora_weights.safetensors'),
305
- token=hf_token
306
- )
307
-
308
- load_time = time.time() - load_start
309
- logger.info(f"[LORA] ✓ Hub LoRA загружена за {load_time:.2f}s (закэширована)")
310
- else:
311
- # Загрузка локального файла из /workspace/loras/
312
- logger.info(f"[LORA] Загрузка локальной LoRA: {lora_info['path']}")
313
- logger.info(f"[LORA] File: {lora_info['weights']}")
314
-
315
- pipeline.load_lora_weights(
316
- lora_info['path'],
317
- adapter_name=lora_name
318
- )
319
-
320
- load_time = time.time() - load_start
321
- logger.info(f"[LORA] ✓ Локальная LoRA загружена за {load_time:.2f}s")
322
-
323
- # Устанавливаем scale
324
- if hasattr(pipeline, 'set_adapters'):
325
- logger.info(f"[LORA] Установка adapter scale: {lora_scale}")
326
- pipeline.set_adapters([lora_name], adapter_weights=[lora_scale])
327
-
328
- trigger = lora_info.get('trigger', '')
329
- if trigger:
330
- logger.info(f"[LORA] Trigger word: '{trigger}'")
331
-
332
- return trigger
333
-
334
- except Exception as e:
335
- logger.error(f"[LORA] ❌ Ошибка загрузки {lora_name}: {e}")
336
- import traceback
337
- logger.error(f"[LORA] Traceback:\n{traceback.format_exc()}")
338
- return None
339
-
340
- # =================================================================
341
- # GENERATION FUNCTIONS
342
- # =================================================================
343
-
344
- MAX_SEED = np.iinfo(np.int32).max
345
-
346
- # Декоратор для spaces если доступен
347
- def gpu_decorator(duration=180):
348
- def decorator(func):
349
- if SPACES_AVAILABLE:
350
- return spaces.GPU(duration=duration)(func)
351
- return func
352
- return decorator
353
-
354
- @gpu_decorator(duration=180)
355
- def generate_text2img(
356
- prompt,
357
- negative_prompt=" ",
358
- width=1664,
359
- height=928,
360
- seed=42,
361
- randomize_seed=False,
362
- guidance_scale=2.5,
363
- num_inference_steps=40,
364
- lora_name="None",
365
- lora_scale=1.0,
366
- progress=gr.Progress(track_tqdm=True)
367
- ):
368
- """Text-to-Image генерация"""
369
-
370
- generation_start = time.time()
371
-
372
- logger.info("\n" + "=" * 60)
373
- logger.info("TEXT-TO-IMAGE GENERATION")
374
- logger.info("=" * 60)
375
-
376
- # Генерация seed
377
- if randomize_seed:
378
- original_seed = seed
379
- seed = random.randint(0, MAX_SEED)
380
- logger.info(f"[SEED] Random seed: {original_seed} → {seed}")
381
- else:
382
- logger.info(f"[SEED] Fixed seed: {seed}")
383
-
384
- # Логирование параметров
385
- logger.info(f"[PARAMS] Prompt length: {len(prompt)} chars")
386
- logger.info(f"[PARAMS] Prompt: {prompt[:150]}{'...' if len(prompt) > 150 else ''}")
387
- if negative_prompt and negative_prompt != " ":
388
- logger.info(f"[PARAMS] Negative prompt: {negative_prompt[:100]}{'...' if len(negative_prompt) > 100 else ''}")
389
- logger.info(f"[PARAMS] Resolution: {width}×{height} = {width*height:,} pixels")
390
- logger.info(f"[PARAMS] Steps: {num_inference_steps}")
391
- logger.info(f"[PARAMS] CFG Scale: {guidance_scale}")
392
- logger.info(f"[PARAMS] LoRA: {lora_name} (scale: {lora_scale})")
393
-
394
- try:
395
- # Загружаем LoRA если выбрана
396
- trigger_word = None
397
- original_prompt = prompt
398
- if lora_name != "None":
399
- logger.info(f"[T2I STAGE 1/3] Загрузка LoRA...")
400
- lora_start = time.time()
401
- trigger_word = load_lora_weights(pipe_txt2img, lora_name, lora_scale, hf_token)
402
- lora_time = time.time() - lora_start
403
- logger.info(f"[T2I STAGE 1/3] ✓ LoRA загружена за {lora_time:.2f}s")
404
-
405
- # Добавляем trigger word если есть
406
- if trigger_word:
407
- prompt = trigger_word + prompt
408
- logger.info(f"[PROMPT] Добавлен trigger: '{trigger_word}'")
409
- logger.info(f"[PROMPT] Финальный промпт: {prompt[:150]}...")
410
- else:
411
- logger.info(f"[T2I STAGE 1/3] LoRA не используется")
412
-
413
- # Создание генератора
414
- logger.info(f"[T2I STAGE 2/3] Подготовка генератора...")
415
- generator = torch.Generator(device=device).manual_seed(seed)
416
- logger.info(f"[T2I STAGE 2/3] ✓ Generator готов на устройстве: {device}")
417
-
418
- # Генерация изображения
419
- logger.info(f"[T2I STAGE 3/3] Начало генерации изображения...")
420
- logger.info(f"[T2I STAGE 3/3] Pipeline: DiffusionPipeline (Text2Img)")
421
- logger.info(f"[T2I STAGE 3/3] Ожидаемое время: ~{num_inference_steps * 0.9:.1f}s")
422
- gen_start = time.time()
423
-
424
- image = pipe_txt2img(
425
- prompt=prompt,
426
- negative_prompt=negative_prompt,
427
- width=width,
428
- height=height,
429
- num_inference_steps=num_inference_steps,
430
- true_cfg_scale=guidance_scale,
431
- generator=generator
432
- ).images[0]
433
-
434
- gen_time = time.time() - gen_start
435
- logger.info(f"[T2I STAGE 3/3] ✓ Изображение сгенерировано за {gen_time:.2f}s")
436
- logger.info(f"[T2I STAGE 3/3] Скорость: {gen_time/num_inference_steps:.3f}s на шаг")
437
-
438
- # Выгружаем LoRA после генерации
439
- if lora_name != "None":
440
- logger.info(f"[CLEANUP] Выгрузка LoRA...")
441
- unload_start = time.time()
442
- pipe_txt2img.unload_lora_weights()
443
- unload_time = time.time() - unload_start
444
- logger.info(f"[CLEANUP] ✓ LoRA выгружена за {unload_time:.2f}s")
445
-
446
- # Итоговая статистика
447
- total_time = time.time() - generation_start
448
- logger.info(f"[РЕЗУЛЬТАТ] Финальное изображение: {image.size[0]}×{image.size[1]}")
449
- logger.info(f"[РЕЗУЛЬТАТ] Использованный seed: {seed}")
450
- logger.info(f"[РЕЗУЛЬТАТ] Общее время генерации: {total_time:.2f}s")
451
- logger.info("=" * 60)
452
- logger.info("✓ TEXT-TO-IMAGE ЗАВЕРШЕНА УСПЕШНО")
453
- logger.info("=" * 60 + "\n")
454
-
455
- return image, seed, get_logs()
456
-
457
- except Exception as e:
458
- logger.error(f"[ERROR] ❌ Ошибка при генерации: {e}")
459
- import traceback
460
- logger.error(f"[ERROR] Traceback:\n{traceback.format_exc()}")
461
- raise gr.Error(f"Ошибка генерации: {e}")
462
-
463
- @gpu_decorator(duration=180)
464
- def generate_img2img(
465
- input_image,
466
- prompt,
467
- negative_prompt=" ",
468
- strength=0.75,
469
- seed=42,
470
- randomize_seed=False,
471
- guidance_scale=2.5,
472
- num_inference_steps=40,
473
- lora_name="None",
474
- lora_scale=1.0,
475
- progress=gr.Progress(track_tqdm=True)
476
- ):
477
- """Image-to-Image генерация"""
478
-
479
- generation_start = time.time()
480
-
481
- logger.info("\n" + "=" * 60)
482
- logger.info("IMAGE-TO-IMAGE GENERATION")
483
- logger.info("=" * 60)
484
-
485
- # Проверка входного изображения
486
- if input_image is None:
487
- logger.error("[ERROR] Входное изображение отсутствует!")
488
- raise gr.Error("Please upload an input image")
489
-
490
- # КРИТИЧНО: Сохраняем оригинальные размеры ДО любых преобразований
491
- original_width, original_height = input_image.size
492
- logger.info(f"[INPUT] ОРИГИНАЛЬНОЕ изображение: {original_width}×{original_height}")
493
- logger.info(f"[INPUT] Формат: {input_image.format if hasattr(input_image, 'format') else 'N/A'}")
494
- logger.info(f"[INPUT] Режим: {input_image.mode}")
495
- logger.info(f"[INPUT] Соотношение сторон: {original_width/original_height:.3f}")
496
-
497
- # Генерация seed
498
- if randomize_seed:
499
- original_seed = seed
500
- seed = random.randint(0, MAX_SEED)
501
- logger.info(f"[SEED] Random seed: {original_seed} → {seed}")
502
- else:
503
- logger.info(f"[SEED] Fixed seed: {seed}")
504
-
505
- # Изменяем размер изображения (max 3072 для поддержки больших изображений)
506
- logger.info(f"[I2I STAGE 1/4] Предобработка изображения...")
507
- resize_start = time.time()
508
- resized = resize_image(input_image, max_size=3072)
509
- resize_time = time.time() - resize_start
510
- logger.info(f"[I2I STAGE 1/4] ✓ Изображение подготовлено за {resize_time:.2f}s")
511
-
512
- # Логирование параметров
513
- logger.info(f"[PARAMS] Prompt length: {len(prompt)} chars")
514
- logger.info(f"[PARAMS] Prompt: {prompt[:150]}{'...' if len(prompt) > 150 else ''}")
515
- if negative_prompt and negative_prompt != " ":
516
- logger.info(f"[PARAMS] Negative prompt: {negative_prompt[:100]}{'...' if len(negative_prompt) > 100 else ''}")
517
- logger.info(f"[PARAMS] Strength: {strength} (0=оригинал, 1=полная перерисовка)")
518
- logger.info(f"[PARAMS] Effective steps: {int(num_inference_steps * strength)} из {num_inference_steps}")
519
- logger.info(f"[PARAMS] CFG Scale: {guidance_scale}")
520
- logger.info(f"[PARAMS] LoRA: {lora_name} (scale: {lora_scale})")
521
-
522
- try:
523
- if pipe_img2img is None:
524
- logger.error("[ERROR] Image2Image pipeline не доступен!")
525
- raise gr.Error("Image2Image pipeline not available")
526
-
527
- # Загружаем LoRA если выбрана
528
- trigger_word = None
529
- original_prompt = prompt
530
- if lora_name != "None":
531
- logger.info(f"[I2I STAGE 2/4] Загрузка LoRA...")
532
- lora_start = time.time()
533
- trigger_word = load_lora_weights(pipe_img2img, lora_name, lora_scale, hf_token)
534
- lora_time = time.time() - lora_start
535
- logger.info(f"[I2I STAGE 2/4] ✓ LoRA загружена за {lora_time:.2f}s")
536
-
537
- # Добавляем trigger word если есть
538
- if trigger_word:
539
- prompt = trigger_word + prompt
540
- logger.info(f"[PROMPT] Добавлен trigger: '{trigger_word}'")
541
- logger.info(f"[PROMPT] Финальный промпт: {prompt[:150]}...")
542
- else:
543
- logger.info(f"[I2I STAGE 2/4] LoRA не используется")
544
-
545
- # Создание генератора
546
- logger.info(f"[I2I STAGE 3/4] Подготовка генератора...")
547
- generator = torch.Generator(device=device).manual_seed(seed)
548
- logger.info(f"[I2I STAGE 3/4] ✓ Generator готов на устройстве: {device}")
549
-
550
- # КРИТИЧНО: Передаем width и height явно, иначе пайплайн использует дефолтные 1024x1024!
551
- img_width, img_height = resized.size
552
- logger.info(f"[I2I STAGE 3/4] Финальное разрешение для генерации: {img_width}×{img_height}")
553
- logger.info(f"[I2I STAGE 3/4] Соотношение сторон: {img_width/img_height:.3f}")
554
-
555
- # Генерация изображения
556
- logger.info(f"[I2I STAGE 4/4] Начало генерации изображения...")
557
- logger.info(f"[I2I STAGE 4/4] Pipeline: QwenImageImg2ImgPipeline")
558
- effective_steps = int(num_inference_steps * strength)
559
- logger.info(f"[I2I STAGE 4/4] Реальных шагов денойзинга: {effective_steps}")
560
- logger.info(f"[I2I STAGE 4/4] Ожидаемое время: ~{effective_steps * 0.9:.1f}s")
561
- logger.info(f"[DEBUG] 🔍 ПЕРЕДАЕМ В PIPELINE: width={img_width}, height={img_height}")
562
- gen_start = time.time()
563
-
564
- image = pipe_img2img(
565
- prompt=prompt,
566
- negative_prompt=negative_prompt,
567
- image=resized,
568
- width=img_width,
569
- height=img_height,
570
- strength=strength,
571
- num_inference_steps=num_inference_steps,
572
- true_cfg_scale=guidance_scale,
573
- generator=generator
574
- ).images[0]
575
-
576
- gen_time = time.time() - gen_start
577
-
578
- # КРИТИЧНО: Проверяем финальный размер результата
579
- final_width, final_height = image.size
580
- logger.info(f"[DEBUG] 🔍 ПОЛУЧИЛИ ИЗ PIPELINE: width={final_width}, height={final_height}")
581
-
582
- if final_width != img_width or final_height != img_height:
583
- logger.error(f"[BUG] ⚠️⚠️⚠️ РАЗМЕР ИЗМЕНИЛСЯ! ⚠️⚠️⚠️")
584
- logger.error(f"[BUG] Ожидалось: {img_width}×{img_height}")
585
- logger.error(f"[BUG] Получено: {final_width}×{final_height}")
586
- logger.error(f"[BUG] Соотношение: {final_width/final_height:.3f}")
587
- logger.error(f"[BUG] Это указывает на проблему в pipeline или diffusers!")
588
- else:
589
- logger.info(f"[I2I STAGE 4/4] ✓ Размер результата корректен: {final_width}×{final_height}")
590
-
591
- logger.info(f"[I2I STAGE 4/4] ✓ Изображение сгенерировано за {gen_time:.2f}s")
592
- logger.info(f"[I2I STAGE 4/4] Скорость: {gen_time/effective_steps:.3f}s на шаг")
593
-
594
- # Выгружаем LoRA
595
- if lora_name != "None":
596
- logger.info(f"[CLEANUP] Выгрузка LoRA...")
597
- unload_start = time.time()
598
- pipe_img2img.unload_lora_weights()
599
- unload_time = time.time() - unload_start
600
- logger.info(f"[CLEANUP] ✓ LoRA выгружена за {unload_time:.2f}s")
601
-
602
- # Итоговая статистика
603
- total_time = time.time() - generation_start
604
- logger.info(f"[РЕЗУЛЬТАТ] Финальное изображение: {image.size[0]}×{image.size[1]}")
605
- logger.info(f"[РЕЗУЛЬТАТ] Использованный seed: {seed}")
606
- logger.info(f"[РЕЗУЛЬТАТ] Общее время генерации: {total_time:.2f}s")
607
- logger.info(f"[РЕЗУЛЬТАТ] Разбивка времени:")
608
- logger.info(f"[РЕЗУЛЬТАТ] - Ресайз: {resize_time:.2f}s ({resize_time/total_time*100:.1f}%)")
609
- logger.info(f"[РЕЗУЛЬТАТ] - Генерация: {gen_time:.2f}s ({gen_time/total_time*100:.1f}%)")
610
- logger.info("=" * 60)
611
- logger.info("✓ IMAGE-TO-IMAGE ЗАВЕРШЕНА УСПЕШНО")
612
- logger.info("=" * 60 + "\n")
613
-
614
- return image, seed, get_logs()
615
-
616
- except Exception as e:
617
- logger.error(f"[ERROR] ❌ Ошибка при генерации: {e}")
618
- import traceback
619
- logger.error(f"[ERROR] Traceback:\n{traceback.format_exc()}")
620
- raise gr.Error(f"Ошибка генерации: {e}")
621
-
622
- # ControlNet функция убрана - не используется
623
-
624
- # =================================================================
625
- # GRADIO INTERFACE
626
- # =================================================================
627
-
628
- MAX_SEED = np.iinfo(np.int32).max
629
-
630
- css = """
631
- #col-container {
632
- margin: 0 auto;
633
- max-width: 1400px;
634
- }
635
- """
636
-
637
- with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
638
- lora_choices = ["None"] + list(AVAILABLE_LORAS.keys())
639
-
640
- gr.Markdown(f"""
641
- # 🎨 Qwen Soloband - Image2Image + LoRA v2.2
642
-
643
- **Продвинутая модель генерации** с поддержкой Text-to-Image, Image-to-Image и LoRA стилей.
644
-
645
- ### ✨ Возможности:
646
- - 🖼️ **Text-to-Image** - Генерация из текста, разрешения до 2048×2048
647
- - 🔄 **Image-to-Image** - Модификация изображений с контролем strength (0.0-1.0, до 3072×3072)
648
- - 🎭 **LoRA Support** - {len(AVAILABLE_LORAS)} доступных стилей (Hub + локальные)
649
- - 🔌 **Full API** - Все функции доступны через API
650
- - ⚡ **Optimized** - VAE tiling/slicing, правильный QwenImageImg2ImgPipeline
651
- - 📋 **Detailed Logs** - Подробное логирование всех этапов генерации
652
-
653
- **Модель**: [Gerchegg/Qwen-Soloband-Diffusers](https://huggingface.co/Gerchegg/Qwen-Soloband-Diffusers)
654
-
655
- 💡 **Local LoRAs**: Положите .safetensors файлы в `/workspace/loras/` - они появятся автоматически!
656
- """)
657
-
658
- with gr.Tabs() as tabs:
659
-
660
- # TAB 1: Text-to-Image
661
- with gr.Tab("📝 Text-to-Image"):
662
- with gr.Row():
663
- with gr.Column(scale=1):
664
- t2i_prompt = gr.Text(
665
- label="Prompt",
666
- placeholder="SB_AI, a beautiful landscape...",
667
- lines=3
668
- )
669
-
670
- t2i_run = gr.Button("Generate", variant="primary")
671
-
672
- with gr.Accordion("Advanced Settings", open=False):
673
- t2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
674
-
675
- with gr.Row():
676
- t2i_width = gr.Slider(label="Width", minimum=512, maximum=2048, step=64, value=1664)
677
- t2i_height = gr.Slider(label="Height", minimum=512, maximum=2048, step=64, value=928)
678
-
679
- with gr.Row():
680
- t2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
681
- t2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
682
-
683
- with gr.Row():
684
- t2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
685
- t2i_random_seed = gr.Checkbox(label="Random", value=True)
686
-
687
- t2i_lora = gr.Radio(
688
- label="LoRA Style",
689
- choices=lora_choices,
690
- value="None",
691
- info=f"Hub: {len(HUB_LORAS)}, Local: {len(LOCAL_LORAS)}"
692
- )
693
- t2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
694
-
695
- with gr.Column(scale=1):
696
- t2i_output = gr.Image(label="Generated Image")
697
- t2i_seed_output = gr.Number(label="Used Seed")
698
-
699
- # TAB 2: Image-to-Image
700
- with gr.Tab("🔄 Image-to-Image"):
701
- with gr.Row():
702
- with gr.Column(scale=1):
703
- i2i_input = gr.Image(type="pil", label="Input Image")
704
- i2i_prompt = gr.Text(
705
- label="Prompt",
706
- placeholder="Transform this image into...",
707
- lines=3
708
- )
709
-
710
- i2i_strength = gr.Slider(
711
- label="Denoising Strength",
712
- info="0.0 = original image, 1.0 = complete redraw",
713
- minimum=0.0,
714
- maximum=1.0,
715
- step=0.05,
716
- value=0.75
717
- )
718
-
719
- i2i_run = gr.Button("Generate", variant="primary")
720
-
721
- with gr.Accordion("Advanced Settings", open=False):
722
- i2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
723
-
724
- with gr.Row():
725
- i2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
726
- i2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
727
-
728
- with gr.Row():
729
- i2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
730
- i2i_random_seed = gr.Checkbox(label="Random", value=True)
731
-
732
- i2i_lora = gr.Radio(
733
- label="LoRA Style",
734
- choices=lora_choices,
735
- value="None",
736
- info=f"Hub: {len(HUB_LORAS)}, Local: {len(LOCAL_LORAS)}"
737
- )
738
- i2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
739
-
740
- with gr.Column(scale=1):
741
- i2i_output = gr.Image(label="Generated Image")
742
- i2i_seed_output = gr.Number(label="Used Seed")
743
-
744
- # TAB 3: Logs
745
- with gr.Tab("📋 Logs"):
746
- gr.Markdown("""
747
- ## 📋 Логи генерации
748
-
749
- Здесь отображаются подробные логи последней генерации:
750
- - 🔍 Параметры запроса (prompt, size, seed, etc.)
751
- - ⚙️ Этапы обработки (resize, LoRA loading, generation)
752
- - ⏱️ Время выполнения каждого этапа
753
- - 📊 Финальная статистика и разбивка времени
754
-
755
- 💡 Логи автоматически обновляются после каждой генерации
756
- """)
757
-
758
- log_output = gr.Textbox(
759
- label="Логи",
760
- lines=25,
761
- max_lines=50,
762
- show_copy_button=True,
763
- interactive=False,
764
- placeholder="Логи появятся после первой генерации..."
765
- )
766
-
767
- with gr.Row():
768
- refresh_logs_btn = gr.Button("🔄 Обновить логи", size="sm")
769
- clear_logs_btn = gr.Button("🗑️ Очистить логи", size="sm", variant="stop")
770
-
771
- # Кнопка обновления логов
772
- refresh_logs_btn.click(
773
- fn=get_logs,
774
- inputs=None,
775
- outputs=log_output
776
- )
777
-
778
- # Кнопка очистки логов
779
- clear_logs_btn.click(
780
- fn=clear_logs,
781
- inputs=None,
782
- outputs=log_output
783
- )
784
-
785
- # Event handlers
786
- t2i_run.click(
787
- fn=generate_text2img,
788
- inputs=[
789
- t2i_prompt, t2i_negative, t2i_width, t2i_height,
790
- t2i_seed, t2i_random_seed, t2i_cfg, t2i_steps,
791
- t2i_lora, t2i_lora_scale
792
- ],
793
- outputs=[t2i_output, t2i_seed_output, log_output],
794
- api_name="text2img"
795
- )
796
-
797
- i2i_run.click(
798
- fn=generate_img2img,
799
- inputs=[
800
- i2i_input, i2i_prompt, i2i_negative, i2i_strength,
801
- i2i_seed, i2i_random_seed, i2i_cfg, i2i_steps,
802
- i2i_lora, i2i_lora_scale
803
- ],
804
- outputs=[i2i_output, i2i_seed_output, log_output],
805
- api_name="img2img"
806
- )
807
-
808
- if __name__ == "__main__":
809
- demo.launch(
810
- show_api=True,
811
- share=False,
812
- show_error=True # Показывать подробные ошибки в API
813
- )
814
-
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import random
4
+ import json
5
+ import torch
6
+ from PIL import Image
7
+ import os
8
+ import time
9
+ import logging
10
+
11
+ # Опциональный импорт spaces для работы в Runpod
12
+ try:
13
+ import spaces
14
+ SPACES_AVAILABLE = True
15
+ except ImportError:
16
+ SPACES_AVAILABLE = False
17
+
18
+ from diffusers import (
19
+ DiffusionPipeline,
20
+ QwenImageImg2ImgPipeline
21
+ )
22
+ from huggingface_hub import hf_hub_download
23
+
24
+ # Настройка логирования
25
+ from io import StringIO
26
+
27
+ # Создаем буфер для логов
28
+ log_buffer = StringIO()
29
+
30
+ # Настраиваем логирование с выводом в консоль и буфер
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format='%(asctime)s | %(levelname)s | %(message)s',
34
+ datefmt='%Y-%m-%d %H:%M:%S',
35
+ handlers=[
36
+ logging.StreamHandler(), # Вывод в консоль
37
+ logging.StreamHandler(log_buffer) # Вывод в буфер для UI
38
+ ]
39
+ )
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # Логируем если spaces не доступен
43
+ if not SPACES_AVAILABLE:
44
+ logger.warning("⚠️ spaces module not available - running without ZeroGPU support")
45
+
46
+ def get_logs():
47
+ """Получить накопленные логи"""
48
+ return log_buffer.getvalue()
49
+
50
+ def clear_logs():
51
+ """Очистить буфер логов"""
52
+ global log_buffer
53
+ log_buffer = StringIO()
54
+ # Обновляем handlers
55
+ for handler in logger.handlers:
56
+ if isinstance(handler.stream, StringIO):
57
+ logger.removeHandler(handler)
58
+ new_handler = logging.StreamHandler(log_buffer)
59
+ new_handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s', '%Y-%m-%d %H:%M:%S'))
60
+ logger.addHandler(new_handler)
61
+ return ""
62
+
63
+ logger.info("=" * 60)
64
+ logger.info("LOADING QWEN-SOLOBAND ADVANCED v2.1")
65
+ logger.info("With detailed logging and fixed img2img resize")
66
+ logger.info("=" * 60)
67
+
68
+ hf_token = os.environ.get("HF_TOKEN")
69
+ device = "cuda" if torch.cuda.is_available() else "cpu"
70
+ dtype = torch.bfloat16
71
+
72
+ # Логируем GPU
73
+ logger.info(f"CUDA available: {torch.cuda.is_available()}")
74
+ if torch.cuda.is_available():
75
+ gpu_count = torch.cuda.device_count()
76
+ logger.info(f"Number of GPUs: {gpu_count}")
77
+ for i in range(gpu_count):
78
+ logger.info(f" GPU {i}: {torch.cuda.get_device_name(i)}")
79
+ logger.info(f" Memory: {torch.cuda.get_device_properties(i).total_memory / 1024**3:.1f} GB")
80
+ else:
81
+ gpu_count = 0
82
+ logger.info("Running on CPU - no GPUs available")
83
+
84
+ # =================================================================
85
+ # ЗАГРУЗКА МОДЕЛЕЙ
86
+ # =================================================================
87
+
88
+ # 1. Базовая модель для Text-to-Image
89
+ logger.info("\n[1/3] Loading base Text2Image model...")
90
+ model_id = os.environ.get("MODEL_ID", "Gerchegg/Qwen-Soloband-Diffusers")
91
+ model_revision = os.environ.get("MODEL_REVISION", "main")
92
+
93
+ try:
94
+ start_time = time.time()
95
+
96
+ # Определяем device_map
97
+ if gpu_count > 1:
98
+ device_map = "balanced"
99
+ logger.info(f" Device map: balanced ({gpu_count} GPUs)")
100
+ else:
101
+ device_map = None
102
+ logger.info(" Device map: single GPU")
103
+
104
+ # Загружаем базовую модель
105
+ load_kwargs = {
106
+ "torch_dtype": dtype,
107
+ "device_map": device_map,
108
+ "token": hf_token,
109
+ "cache_dir": os.environ.get("HF_HOME", "/workspace/.cache/huggingface")
110
+ }
111
+ if model_revision:
112
+ load_kwargs["revision"] = model_revision
113
+
114
+ logger.info(f" Loading model: {model_id}")
115
+ logger.info(f" Device map: {device_map}")
116
+ logger.info(f" Dtype: {dtype}")
117
+
118
+ pipe_txt2img = DiffusionPipeline.from_pretrained(model_id, **load_kwargs)
119
+
120
+ if device_map is None:
121
+ pipe_txt2img.to(device)
122
+
123
+ load_time = time.time() - start_time
124
+ logger.info(f" ✓ Text2Image loaded in {load_time:.1f}s")
125
+
126
+ except Exception as e:
127
+ logger.error(f" ❌ Error loading Text2Image: {e}")
128
+ raise
129
+
130
+ # 2. Image-to-Image модель (используем те же компоненты)
131
+ logger.info("\n[2/3] Creating Image2Image pipeline...")
132
+ try:
133
+ # Создаем QwenImageImg2ImgPipeline переиспользуя компоненты Text2Image pipeline
134
+ # Это правильный способ для Qwen-Image архитектуры
135
+ pipe_img2img = QwenImageImg2ImgPipeline(
136
+ vae=pipe_txt2img.vae,
137
+ text_encoder=pipe_txt2img.text_encoder,
138
+ tokenizer=pipe_txt2img.tokenizer,
139
+ transformer=pipe_txt2img.transformer,
140
+ scheduler=pipe_txt2img.scheduler
141
+ )
142
+ logger.info(" ✓ Image2Image pipeline created (reusing components)")
143
+ except Exception as e:
144
+ logger.error(f" ❌ Error creating Image2Image: {e}")
145
+ pipe_img2img = None
146
+
147
+ # ControlNet не используется - убран для упрощения
148
+
149
+ # Оптимизации памяти
150
+ logger.info("\nApplying memory optimizations...")
151
+ for pipe in [pipe_txt2img, pipe_img2img]:
152
+ if pipe and hasattr(pipe, 'vae'):
153
+ if hasattr(pipe.vae, 'enable_tiling'):
154
+ pipe.vae.enable_tiling()
155
+ if hasattr(pipe.vae, 'enable_slicing'):
156
+ pipe.vae.enable_slicing()
157
+
158
+ logger.info(" ✓ VAE tiling and slicing enabled")
159
+
160
+ logger.info("\n" + "=" * 60)
161
+ logger.info("✓ ALL MODELS LOADED")
162
+ logger.info("=" * 60)
163
+
164
+ # =================================================================
165
+ # HELPER FUNCTIONS
166
+ # =================================================================
167
+
168
+ def resize_image(input_image, max_size=2048):
169
+ """
170
+ Изменяет размер изображения с сохранением пропорций (кратно 16).
171
+ Если изображение меньше max_size, оставляет оригинальный размер (с округлением до 16).
172
+ """
173
+ w, h = input_image.size
174
+ logger.info(f"[RESIZE] Входное изображение: {w}×{h}, max_size={max_size}")
175
+
176
+ # Если изображение уже меньше max_size, просто округляем до 16
177
+ if w <= max_size and h <= max_size:
178
+ new_w = w - (w % 16)
179
+ new_h = h - (h % 16)
180
+ if new_w == 0: new_w = 16
181
+ if new_h == 0: new_h = 16
182
+ if new_w != w or new_h != h:
183
+ logger.info(f"[RESIZE] Округление до кратного 16: {w}×{h} → {new_w}×{new_h}")
184
+ return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
185
+ logger.info(f"[RESIZE] Размер уже кратен 16, изменение не требуется")
186
+ return input_image
187
+
188
+ # Определяем масштабирование с сохранением пропорций
189
+ scale = min(max_size / w, max_size / h)
190
+ new_w = int(w * scale)
191
+ new_h = int(h * scale)
192
+ logger.info(f"[RESIZE] Масштабирование: scale={scale:.3f}, промежуточный размер: {new_w}×{new_h}")
193
+
194
+ # Округляем до ближайшего кратного 16
195
+ new_w = new_w - (new_w % 16)
196
+ new_h = new_h - (new_h % 16)
197
+
198
+ # Минимальные размеры
199
+ if new_w < 16: new_w = 16
200
+ if new_h < 16: new_h = 16
201
+
202
+ logger.info(f"[RESIZE] Финальный размер после округления: {new_w}×{new_h}")
203
+ aspect_original = w / h
204
+ aspect_new = new_w / new_h
205
+ logger.info(f"[RESIZE] Соотношение сторон: {aspect_original:.3f} → {aspect_new:.3f} (разница: {abs(aspect_original - aspect_new):.3f})")
206
+
207
+ return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
208
+
209
+ # =================================================================
210
+ # LORA FUNCTIONS
211
+ # =================================================================
212
+
213
+ # Папка для локальных LoRA
214
+ LOCAL_LORA_DIR = "/workspace/loras"
215
+
216
+ # Базовые LoRA из HuggingFace Hub (загружаются по требованию)
217
+ HUB_LORAS = {
218
+ "Realism": {
219
+ "repo": "flymy-ai/qwen-image-realism-lora",
220
+ "trigger": "Super Realism portrait of",
221
+ "weights": "pytorch_lora_weights.safetensors",
222
+ "source": "hub"
223
+ },
224
+ "Anime": {
225
+ "repo": "alfredplpl/qwen-image-modern-anime-lora",
226
+ "trigger": "Japanese modern anime style, ",
227
+ "weights": "pytorch_lora_weights.safetensors",
228
+ "source": "hub"
229
+ }
230
+ # Другие LoRA положите в /workspace/loras/ как .safetensors файлы
231
+ }
232
+
233
+ def scan_local_loras():
234
+ """
235
+ Сканирует папку /workspace/loras на наличие .safetensors файлов
236
+ Возвращает dict с найденными LoRA
237
+ """
238
+ local_loras = {}
239
+
240
+ if not os.path.exists(LOCAL_LORA_DIR):
241
+ logger.info(f" Local LoRA directory not found: {LOCAL_LORA_DIR}")
242
+ return local_loras
243
+
244
+ logger.info(f" Scanning local LoRA directory: {LOCAL_LORA_DIR}")
245
+
246
+ try:
247
+ for file in os.listdir(LOCAL_LORA_DIR):
248
+ if file.endswith('.safetensors'):
249
+ lora_name = os.path.splitext(file)[0] # Имя без расширения
250
+ local_path = os.path.join(LOCAL_LORA_DIR, file)
251
+
252
+ # Добавляем в список
253
+ local_loras[lora_name] = {
254
+ "path": local_path,
255
+ "trigger": "", # Без trigger word для локальных
256
+ "weights": file,
257
+ "source": "local"
258
+ }
259
+
260
+ logger.info(f" ✓ Found local LoRA: {lora_name} ({file})")
261
+
262
+ except Exception as e:
263
+ logger.warning(f" Error scanning local LoRA directory: {e}")
264
+
265
+ return local_loras
266
+
267
+ # Сканируем локальные LoRA
268
+ logger.info("\nScanning for LoRA models...")
269
+ LOCAL_LORAS = scan_local_loras()
270
+
271
+ # Объединяем Hub и локальные LoRA
272
+ AVAILABLE_LORAS = {**HUB_LORAS, **LOCAL_LORAS}
273
+
274
+ if LOCAL_LORAS:
275
+ logger.info(f" ✓ Found {len(LOCAL_LORAS)} local LoRA(s)")
276
+ logger.info(f" Total available LoRAs: {len(AVAILABLE_LORAS)}")
277
+
278
+ def load_lora_weights(pipeline, lora_name, lora_scale, hf_token):
279
+ """
280
+ Загружает LoRA веса в pipeline (ленивая загрузка)
281
+ Hub LoRA скачиваются только при использовании
282
+ Локальные LoRA загружаются из /workspace/loras/
283
+ """
284
+ if lora_name == "None" or lora_name not in AVAILABLE_LORAS:
285
+ logger.info(f"[LORA] Пропуск загрузки: lora_name='{lora_name}'")
286
+ return None
287
+
288
+ lora_info = AVAILABLE_LORAS[lora_name]
289
+ logger.info(f"[LORA] Начало загрузки: {lora_name}")
290
+ logger.info(f"[LORA] Source: {lora_info['source']}")
291
+ logger.info(f"[LORA] Scale: {lora_scale}")
292
+
293
+ try:
294
+ load_start = time.time()
295
+
296
+ if lora_info['source'] == 'hub':
297
+ # Ленивая загрузка с HuggingFace Hub (скачивается при первом использовании)
298
+ logger.info(f"[LORA] Загрузка из Hub: {lora_info['repo']}")
299
+ logger.info(f"[LORA] Weight file: {lora_info.get('weights', 'pytorch_lora_weights.safetensors')}")
300
+ logger.info(f"[LORA] (Скачивается если не в кэше...)")
301
+
302
+ pipeline.load_lora_weights(
303
+ lora_info['repo'],
304
+ weight_name=lora_info.get('weights', 'pytorch_lora_weights.safetensors'),
305
+ token=hf_token
306
+ )
307
+
308
+ load_time = time.time() - load_start
309
+ logger.info(f"[LORA] ✓ Hub LoRA загружена за {load_time:.2f}s (закэширована)")
310
+ else:
311
+ # Загрузка локального файла из /workspace/loras/
312
+ logger.info(f"[LORA] Загрузка локальной LoRA: {lora_info['path']}")
313
+ logger.info(f"[LORA] File: {lora_info['weights']}")
314
+
315
+ pipeline.load_lora_weights(
316
+ lora_info['path'],
317
+ adapter_name=lora_name
318
+ )
319
+
320
+ load_time = time.time() - load_start
321
+ logger.info(f"[LORA] ✓ Локальная LoRA загружена за {load_time:.2f}s")
322
+
323
+ # Устанавливаем scale
324
+ if hasattr(pipeline, 'set_adapters'):
325
+ logger.info(f"[LORA] Установка adapter scale: {lora_scale}")
326
+ pipeline.set_adapters([lora_name], adapter_weights=[lora_scale])
327
+
328
+ trigger = lora_info.get('trigger', '')
329
+ if trigger:
330
+ logger.info(f"[LORA] Trigger word: '{trigger}'")
331
+
332
+ return trigger
333
+
334
+ except Exception as e:
335
+ logger.error(f"[LORA] ❌ Ошибка загрузки {lora_name}: {e}")
336
+ import traceback
337
+ logger.error(f"[LORA] Traceback:\n{traceback.format_exc()}")
338
+ return None
339
+
340
+ # =================================================================
341
+ # GENERATION FUNCTIONS
342
+ # =================================================================
343
+
344
+ MAX_SEED = np.iinfo(np.int32).max
345
+
346
+ # Декоратор для spaces если доступен
347
+ def gpu_decorator(duration=180):
348
+ def decorator(func):
349
+ if SPACES_AVAILABLE:
350
+ return spaces.GPU(duration=duration)(func)
351
+ return func
352
+ return decorator
353
+
354
+ @gpu_decorator(duration=180)
355
+ def generate_text2img(
356
+ prompt,
357
+ negative_prompt=" ",
358
+ width=1664,
359
+ height=928,
360
+ seed=42,
361
+ randomize_seed=False,
362
+ guidance_scale=2.5,
363
+ num_inference_steps=40,
364
+ lora_name="None",
365
+ lora_scale=1.0,
366
+ progress=gr.Progress(track_tqdm=True)
367
+ ):
368
+ """Text-to-Image генерация"""
369
+
370
+ generation_start = time.time()
371
+
372
+ logger.info("\n" + "=" * 60)
373
+ logger.info("TEXT-TO-IMAGE GENERATION")
374
+ logger.info("=" * 60)
375
+
376
+ # Генерация seed
377
+ if randomize_seed:
378
+ original_seed = seed
379
+ seed = random.randint(0, MAX_SEED)
380
+ logger.info(f"[SEED] Random seed: {original_seed} → {seed}")
381
+ else:
382
+ logger.info(f"[SEED] Fixed seed: {seed}")
383
+
384
+ # Логирование параметров
385
+ logger.info(f"[PARAMS] Prompt length: {len(prompt)} chars")
386
+ logger.info(f"[PARAMS] Prompt: {prompt[:150]}{'...' if len(prompt) > 150 else ''}")
387
+ if negative_prompt and negative_prompt != " ":
388
+ logger.info(f"[PARAMS] Negative prompt: {negative_prompt[:100]}{'...' if len(negative_prompt) > 100 else ''}")
389
+ logger.info(f"[PARAMS] Resolution: {width}×{height} = {width*height:,} pixels")
390
+ logger.info(f"[PARAMS] Steps: {num_inference_steps}")
391
+ logger.info(f"[PARAMS] CFG Scale: {guidance_scale}")
392
+ logger.info(f"[PARAMS] LoRA: {lora_name} (scale: {lora_scale})")
393
+
394
+ try:
395
+ # Загружаем LoRA если выбрана
396
+ trigger_word = None
397
+ original_prompt = prompt
398
+ if lora_name != "None":
399
+ logger.info(f"[T2I STAGE 1/3] Загрузка LoRA...")
400
+ lora_start = time.time()
401
+ trigger_word = load_lora_weights(pipe_txt2img, lora_name, lora_scale, hf_token)
402
+ lora_time = time.time() - lora_start
403
+ logger.info(f"[T2I STAGE 1/3] ✓ LoRA загружена за {lora_time:.2f}s")
404
+
405
+ # Добавляем trigger word если есть
406
+ if trigger_word:
407
+ prompt = trigger_word + prompt
408
+ logger.info(f"[PROMPT] Добавлен trigger: '{trigger_word}'")
409
+ logger.info(f"[PROMPT] Финальный промпт: {prompt[:150]}...")
410
+ else:
411
+ logger.info(f"[T2I STAGE 1/3] LoRA не используется")
412
+
413
+ # Создание генератора
414
+ logger.info(f"[T2I STAGE 2/3] Подготовка генератора...")
415
+ generator = torch.Generator(device=device).manual_seed(seed)
416
+ logger.info(f"[T2I STAGE 2/3] ✓ Generator готов на устройстве: {device}")
417
+
418
+ # Генерация изображения
419
+ logger.info(f"[T2I STAGE 3/3] Начало генерации изображения...")
420
+ logger.info(f"[T2I STAGE 3/3] Pipeline: DiffusionPipeline (Text2Img)")
421
+ logger.info(f"[T2I STAGE 3/3] Ожидаемое время: ~{num_inference_steps * 0.9:.1f}s")
422
+ gen_start = time.time()
423
+
424
+ image = pipe_txt2img(
425
+ prompt=prompt,
426
+ negative_prompt=negative_prompt,
427
+ width=width,
428
+ height=height,
429
+ num_inference_steps=num_inference_steps,
430
+ true_cfg_scale=guidance_scale,
431
+ generator=generator
432
+ ).images[0]
433
+
434
+ gen_time = time.time() - gen_start
435
+ logger.info(f"[T2I STAGE 3/3] ✓ Изображение сгенерировано за {gen_time:.2f}s")
436
+ logger.info(f"[T2I STAGE 3/3] Скорость: {gen_time/num_inference_steps:.3f}s на шаг")
437
+
438
+ # Выгружаем LoRA после генерации
439
+ if lora_name != "None":
440
+ logger.info(f"[CLEANUP] Выгрузка LoRA...")
441
+ unload_start = time.time()
442
+ pipe_txt2img.unload_lora_weights()
443
+ unload_time = time.time() - unload_start
444
+ logger.info(f"[CLEANUP] ✓ LoRA выгружена за {unload_time:.2f}s")
445
+
446
+ # Итоговая статистика
447
+ total_time = time.time() - generation_start
448
+ logger.info(f"[РЕЗУЛЬТАТ] Финальное изображение: {image.size[0]}×{image.size[1]}")
449
+ logger.info(f"[РЕЗУЛЬТАТ] Использованный seed: {seed}")
450
+ logger.info(f"[РЕЗУЛЬТАТ] Общее время генерации: {total_time:.2f}s")
451
+ logger.info("=" * 60)
452
+ logger.info("✓ TEXT-TO-IMAGE ЗАВЕРШЕНА УСПЕШНО")
453
+ logger.info("=" * 60 + "\n")
454
+
455
+ return image, seed, get_logs()
456
+
457
+ except Exception as e:
458
+ logger.error(f"[ERROR] ❌ Ошибка при генерации: {e}")
459
+ import traceback
460
+ logger.error(f"[ERROR] Traceback:\n{traceback.format_exc()}")
461
+ raise gr.Error(f"Ошибка генерации: {e}")
462
+
463
+ @gpu_decorator(duration=180)
464
+ def generate_img2img(
465
+ input_image,
466
+ prompt,
467
+ negative_prompt=" ",
468
+ strength=0.75,
469
+ seed=42,
470
+ randomize_seed=False,
471
+ guidance_scale=2.5,
472
+ num_inference_steps=40,
473
+ lora_name="None",
474
+ lora_scale=1.0,
475
+ progress=gr.Progress(track_tqdm=True)
476
+ ):
477
+ """Image-to-Image ��енерация"""
478
+
479
+ generation_start = time.time()
480
+
481
+ logger.info("\n" + "=" * 60)
482
+ logger.info("IMAGE-TO-IMAGE GENERATION")
483
+ logger.info("=" * 60)
484
+
485
+ # Проверка входного изображения
486
+ if input_image is None:
487
+ logger.error("[ERROR] Входное изображение отсутствует!")
488
+ raise gr.Error("Please upload an input image")
489
+
490
+ # КРИТИЧНО: Сохраняем оригинальные размеры ДО любых преобразований
491
+ original_width, original_height = input_image.size
492
+ logger.info(f"[INPUT] ОРИГИНАЛЬНОЕ изображение: {original_width}×{original_height}")
493
+ logger.info(f"[INPUT] Формат: {input_image.format if hasattr(input_image, 'format') else 'N/A'}")
494
+ logger.info(f"[INPUT] Режим: {input_image.mode}")
495
+ logger.info(f"[INPUT] Соотношение сторон: {original_width/original_height:.3f}")
496
+
497
+ # Генерация seed
498
+ if randomize_seed:
499
+ original_seed = seed
500
+ seed = random.randint(0, MAX_SEED)
501
+ logger.info(f"[SEED] Random seed: {original_seed} → {seed}")
502
+ else:
503
+ logger.info(f"[SEED] Fixed seed: {seed}")
504
+
505
+ # Изменяем размер изображения (max 3072 для поддержки больших изображений)
506
+ logger.info(f"[I2I STAGE 1/4] Предобработка изображения...")
507
+ resize_start = time.time()
508
+ resized = resize_image(input_image, max_size=3072)
509
+ resize_time = time.time() - resize_start
510
+ logger.info(f"[I2I STAGE 1/4] ✓ Изображение подготовлено за {resize_time:.2f}s")
511
+
512
+ # Логирование параметров
513
+ logger.info(f"[PARAMS] Prompt length: {len(prompt)} chars")
514
+ logger.info(f"[PARAMS] Prompt: {prompt[:150]}{'...' if len(prompt) > 150 else ''}")
515
+ if negative_prompt and negative_prompt != " ":
516
+ logger.info(f"[PARAMS] Negative prompt: {negative_prompt[:100]}{'...' if len(negative_prompt) > 100 else ''}")
517
+ logger.info(f"[PARAMS] Strength: {strength} (0=оригинал, 1=полная перерисовка)")
518
+ logger.info(f"[PARAMS] Effective steps: {int(num_inference_steps * strength)} из {num_inference_steps}")
519
+ logger.info(f"[PARAMS] CFG Scale: {guidance_scale}")
520
+ logger.info(f"[PARAMS] LoRA: {lora_name} (scale: {lora_scale})")
521
+
522
+ try:
523
+ if pipe_img2img is None:
524
+ logger.error("[ERROR] Image2Image pipeline не доступен!")
525
+ raise gr.Error("Image2Image pipeline not available")
526
+
527
+ # Загружаем LoRA если выбрана
528
+ trigger_word = None
529
+ original_prompt = prompt
530
+ if lora_name != "None":
531
+ logger.info(f"[I2I STAGE 2/4] Загрузка LoRA...")
532
+ lora_start = time.time()
533
+ trigger_word = load_lora_weights(pipe_img2img, lora_name, lora_scale, hf_token)
534
+ lora_time = time.time() - lora_start
535
+ logger.info(f"[I2I STAGE 2/4] ✓ LoRA загружена за {lora_time:.2f}s")
536
+
537
+ # Добавляем trigger word если есть
538
+ if trigger_word:
539
+ prompt = trigger_word + prompt
540
+ logger.info(f"[PROMPT] Добавлен trigger: '{trigger_word}'")
541
+ logger.info(f"[PROMPT] Финальный промпт: {prompt[:150]}...")
542
+ else:
543
+ logger.info(f"[I2I STAGE 2/4] LoRA не используется")
544
+
545
+ # Создание генератора
546
+ logger.info(f"[I2I STAGE 3/4] Подготовка генератора...")
547
+ generator = torch.Generator(device=device).manual_seed(seed)
548
+ logger.info(f"[I2I STAGE 3/4] ✓ Generator готов на устройстве: {device}")
549
+
550
+ # КРИТИЧНО: Передаем width и height явно, иначе пайплайн использует дефолтные 1024x1024!
551
+ img_width, img_height = resized.size
552
+ logger.info(f"[I2I STAGE 3/4] Финальное разрешение для генерации: {img_width}×{img_height}")
553
+ logger.info(f"[I2I STAGE 3/4] Соотношение сторон: {img_width/img_height:.3f}")
554
+
555
+ # Генерация изображения
556
+ logger.info(f"[I2I STAGE 4/4] Начало генерации изображения...")
557
+ logger.info(f"[I2I STAGE 4/4] Pipeline: QwenImageImg2ImgPipeline")
558
+ effective_steps = int(num_inference_steps * strength)
559
+ logger.info(f"[I2I STAGE 4/4] Реальных шагов денойзинга: {effective_steps}")
560
+ logger.info(f"[I2I STAGE 4/4] Ожидаемое время: ~{effective_steps * 0.9:.1f}s")
561
+ logger.info(f"[DEBUG] 🔍 ПЕРЕДАЕМ В PIPELINE: width={img_width}, height={img_height}")
562
+ gen_start = time.time()
563
+
564
+ image = pipe_img2img(
565
+ prompt=prompt,
566
+ negative_prompt=negative_prompt,
567
+ image=resized,
568
+ width=img_width,
569
+ height=img_height,
570
+ strength=strength,
571
+ num_inference_steps=num_inference_steps,
572
+ true_cfg_scale=guidance_scale,
573
+ generator=generator
574
+ ).images[0]
575
+
576
+ gen_time = time.time() - gen_start
577
+
578
+ # КРИТИЧНО: Проверяем финальный размер результата
579
+ final_width, final_height = image.size
580
+ logger.info(f"[DEBUG] 🔍 ПОЛУЧИЛИ ИЗ PIPELINE: width={final_width}, height={final_height}")
581
+
582
+ if final_width != img_width or final_height != img_height:
583
+ logger.error(f"[BUG] ⚠️⚠️⚠️ РАЗМЕР ИЗМЕНИЛСЯ! ⚠️⚠️⚠️")
584
+ logger.error(f"[BUG] Ожидалось: {img_width}×{img_height}")
585
+ logger.error(f"[BUG] Получено: {final_width}×{final_height}")
586
+ logger.error(f"[BUG] Соотношение: {final_width/final_height:.3f}")
587
+ logger.error(f"[BUG] Это указывает на проблему в pipeline или diffusers!")
588
+ else:
589
+ logger.info(f"[I2I STAGE 4/4] ✓ Размер результата корректен: {final_width}×{final_height}")
590
+
591
+ logger.info(f"[I2I STAGE 4/4] ✓ Изображение сгенерировано за {gen_time:.2f}s")
592
+ logger.info(f"[I2I STAGE 4/4] Скорость: {gen_time/effective_steps:.3f}s на шаг")
593
+
594
+ # Выгружаем LoRA
595
+ if lora_name != "None":
596
+ logger.info(f"[CLEANUP] Выгрузка LoRA...")
597
+ unload_start = time.time()
598
+ pipe_img2img.unload_lora_weights()
599
+ unload_time = time.time() - unload_start
600
+ logger.info(f"[CLEANUP] ✓ LoRA выгружена за {unload_time:.2f}s")
601
+
602
+ # Итоговая статистика
603
+ total_time = time.time() - generation_start
604
+ logger.info(f"[РЕЗУЛЬТАТ] Финальное изображение: {image.size[0]}×{image.size[1]}")
605
+ logger.info(f"[РЕЗУЛЬТАТ] Использованный seed: {seed}")
606
+ logger.info(f"[РЕЗУЛЬТАТ] Общее время генерации: {total_time:.2f}s")
607
+ logger.info(f"[РЕЗУЛЬТАТ] Разбивка времени:")
608
+ logger.info(f"[РЕЗУЛЬТАТ] - Ресайз: {resize_time:.2f}s ({resize_time/total_time*100:.1f}%)")
609
+ logger.info(f"[РЕЗУЛЬТАТ] - Генерация: {gen_time:.2f}s ({gen_time/total_time*100:.1f}%)")
610
+ logger.info("=" * 60)
611
+ logger.info("✓ IMAGE-TO-IMAGE ЗАВЕРШЕНА УСПЕШНО")
612
+ logger.info("=" * 60 + "\n")
613
+
614
+ return image, seed, get_logs()
615
+
616
+ except Exception as e:
617
+ logger.error(f"[ERROR] ❌ Ошибка при генерации: {e}")
618
+ import traceback
619
+ logger.error(f"[ERROR] Traceback:\n{traceback.format_exc()}")
620
+ raise gr.Error(f"Ошибка генерации: {e}")
621
+
622
+ # ControlNet функция убрана - не используется
623
+
624
+ # =================================================================
625
+ # GRADIO INTERFACE
626
+ # =================================================================
627
+
628
+ MAX_SEED = np.iinfo(np.int32).max
629
+
630
+ css = """
631
+ #col-container {
632
+ margin: 0 auto;
633
+ max-width: 1400px;
634
+ }
635
+ """
636
+
637
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
638
+ lora_choices = ["None"] + list(AVAILABLE_LORAS.keys())
639
+
640
+ gr.Markdown(f"""
641
+ # 🎨 Qwen Soloband - Image2Image + LoRA v2.2
642
+
643
+ **Продвинутая модель генерации** с поддержкой Text-to-Image, Image-to-Image и LoRA стилей.
644
+
645
+ ### ✨ Возможности:
646
+ - 🖼️ **Text-to-Image** - Генерация из текста, разрешения до 2048×2048
647
+ - 🔄 **Image-to-Image** - Модификация изображений с контролем strength (0.0-1.0, до 3072×3072)
648
+ - 🎭 **LoRA Support** - {len(AVAILABLE_LORAS)} доступных стилей (Hub + локальные)
649
+ - 🔌 **Full API** - Все функции доступны через API
650
+ - ⚡ **Optimized** - VAE tiling/slicing, правильный QwenImageImg2ImgPipeline
651
+ - 📋 **Detailed Logs** - Подробное логирование всех этапов генерации
652
+
653
+ **Модель**: [Gerchegg/Qwen-Soloband-Diffusers](https://huggingface.co/Gerchegg/Qwen-Soloband-Diffusers)
654
+
655
+ 💡 **Local LoRAs**: Положите .safetensors файлы в `/workspace/loras/` - они появятся автоматически!
656
+ """)
657
+
658
+ with gr.Tabs() as tabs:
659
+
660
+ # TAB 1: Text-to-Image
661
+ with gr.Tab("📝 Text-to-Image"):
662
+ with gr.Row():
663
+ with gr.Column(scale=1):
664
+ t2i_prompt = gr.Text(
665
+ label="Prompt",
666
+ placeholder="SB_AI, a beautiful landscape...",
667
+ lines=3
668
+ )
669
+
670
+ t2i_run = gr.Button("Generate", variant="primary")
671
+
672
+ with gr.Accordion("Advanced Settings", open=False):
673
+ t2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
674
+
675
+ with gr.Row():
676
+ t2i_width = gr.Slider(label="Width", minimum=512, maximum=2048, step=64, value=1664)
677
+ t2i_height = gr.Slider(label="Height", minimum=512, maximum=2048, step=64, value=928)
678
+
679
+ with gr.Row():
680
+ t2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
681
+ t2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
682
+
683
+ with gr.Row():
684
+ t2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
685
+ t2i_random_seed = gr.Checkbox(label="Random", value=True)
686
+
687
+ t2i_lora = gr.Radio(
688
+ label="LoRA Style",
689
+ choices=lora_choices,
690
+ value="None",
691
+ info=f"Hub: {len(HUB_LORAS)}, Local: {len(LOCAL_LORAS)}"
692
+ )
693
+ t2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
694
+
695
+ with gr.Column(scale=1):
696
+ t2i_output = gr.Image(label="Generated Image")
697
+ t2i_seed_output = gr.Number(label="Used Seed")
698
+
699
+ # TAB 2: Image-to-Image
700
+ with gr.Tab("🔄 Image-to-Image"):
701
+ with gr.Row():
702
+ with gr.Column(scale=1):
703
+ i2i_input = gr.Image(type="pil", label="Input Image")
704
+ i2i_prompt = gr.Text(
705
+ label="Prompt",
706
+ placeholder="Transform this image into...",
707
+ lines=3
708
+ )
709
+
710
+ i2i_strength = gr.Slider(
711
+ label="Denoising Strength",
712
+ info="0.0 = original image, 1.0 = complete redraw",
713
+ minimum=0.0,
714
+ maximum=1.0,
715
+ step=0.05,
716
+ value=0.75
717
+ )
718
+
719
+ i2i_run = gr.Button("Generate", variant="primary")
720
+
721
+ with gr.Accordion("Advanced Settings", open=False):
722
+ i2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
723
+
724
+ with gr.Row():
725
+ i2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
726
+ i2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
727
+
728
+ with gr.Row():
729
+ i2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
730
+ i2i_random_seed = gr.Checkbox(label="Random", value=True)
731
+
732
+ i2i_lora = gr.Radio(
733
+ label="LoRA Style",
734
+ choices=lora_choices,
735
+ value="None",
736
+ info=f"Hub: {len(HUB_LORAS)}, Local: {len(LOCAL_LORAS)}"
737
+ )
738
+ i2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
739
+
740
+ with gr.Column(scale=1):
741
+ i2i_output = gr.Image(label="Generated Image")
742
+ i2i_seed_output = gr.Number(label="Used Seed")
743
+
744
+ # TAB 3: Logs
745
+ with gr.Tab("📋 Logs"):
746
+ gr.Markdown("""
747
+ ## 📋 Логи генерации
748
+
749
+ Здесь отображаются подробные логи последней генерации:
750
+ - 🔍 Параметры запроса (prompt, size, seed, etc.)
751
+ - ⚙️ Этапы обработки (resize, LoRA loading, generation)
752
+ - ⏱️ Время выполнения каждого этапа
753
+ - 📊 Финальная статистика и разбивка времени
754
+
755
+ 💡 Логи автоматически обновляются после каждой генерации
756
+ """)
757
+
758
+ log_output = gr.Textbox(
759
+ label="Логи",
760
+ lines=25,
761
+ max_lines=50,
762
+ show_copy_button=True,
763
+ interactive=False,
764
+ placeholder="Логи появятся после первой генерации..."
765
+ )
766
+
767
+ with gr.Row():
768
+ refresh_logs_btn = gr.Button("🔄 Обновить логи", size="sm")
769
+ clear_logs_btn = gr.Button("🗑️ Очистить логи", size="sm", variant="stop")
770
+
771
+ # Кнопка обновления логов
772
+ refresh_logs_btn.click(
773
+ fn=get_logs,
774
+ inputs=None,
775
+ outputs=log_output
776
+ )
777
+
778
+ # Кнопка очистки логов
779
+ clear_logs_btn.click(
780
+ fn=clear_logs,
781
+ inputs=None,
782
+ outputs=log_output
783
+ )
784
+
785
+ # Event handlers
786
+ t2i_run.click(
787
+ fn=generate_text2img,
788
+ inputs=[
789
+ t2i_prompt, t2i_negative, t2i_width, t2i_height,
790
+ t2i_seed, t2i_random_seed, t2i_cfg, t2i_steps,
791
+ t2i_lora, t2i_lora_scale
792
+ ],
793
+ outputs=[t2i_output, t2i_seed_output, log_output],
794
+ api_name="text2img"
795
+ )
796
+
797
+ i2i_run.click(
798
+ fn=generate_img2img,
799
+ inputs=[
800
+ i2i_input, i2i_prompt, i2i_negative, i2i_strength,
801
+ i2i_seed, i2i_random_seed, i2i_cfg, i2i_steps,
802
+ i2i_lora, i2i_lora_scale
803
+ ],
804
+ outputs=[i2i_output, i2i_seed_output, log_output],
805
+ api_name="img2img"
806
+ )
807
+
808
+ if __name__ == "__main__":
809
+ demo.launch(
810
+ show_api=True,
811
+ share=False,
812
+ show_error=True # Показывать подробные ошибки в API
813
+ )
814
+