| import gradio as gr
|
| import numpy as np
|
| import random
|
| import json
|
| import torch
|
| from PIL import Image
|
| import os
|
| import time
|
| import logging
|
|
|
|
|
| try:
|
| import spaces
|
| SPACES_AVAILABLE = True
|
| except ImportError:
|
| SPACES_AVAILABLE = False
|
|
|
| from diffusers import (
|
| DiffusionPipeline,
|
| QwenImageImg2ImgPipeline
|
| )
|
| from huggingface_hub import hf_hub_download
|
|
|
|
|
| from io import StringIO
|
|
|
|
|
| log_buffer = StringIO()
|
|
|
|
|
| logging.basicConfig(
|
| level=logging.INFO,
|
| format='%(asctime)s | %(levelname)s | %(message)s',
|
| datefmt='%Y-%m-%d %H:%M:%S',
|
| handlers=[
|
| logging.StreamHandler(),
|
| logging.StreamHandler(log_buffer)
|
| ]
|
| )
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| if not SPACES_AVAILABLE:
|
| logger.warning("⚠️ spaces module not available - running without ZeroGPU support")
|
|
|
| def get_logs():
|
| """Получить накопленные логи"""
|
| return log_buffer.getvalue()
|
|
|
| def clear_logs():
|
| """Очистить буфер логов"""
|
| global log_buffer
|
| log_buffer = StringIO()
|
|
|
| for handler in logger.handlers:
|
| if isinstance(handler.stream, StringIO):
|
| logger.removeHandler(handler)
|
| new_handler = logging.StreamHandler(log_buffer)
|
| new_handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s', '%Y-%m-%d %H:%M:%S'))
|
| logger.addHandler(new_handler)
|
| return ""
|
|
|
| logger.info("=" * 60)
|
| logger.info("LOADING QWEN-SOLOBAND ADVANCED v2.1")
|
| logger.info("With detailed logging and fixed img2img resize")
|
| logger.info("=" * 60)
|
|
|
| hf_token = os.environ.get("HF_TOKEN")
|
| device = "cuda" if torch.cuda.is_available() else "cpu"
|
| dtype = torch.bfloat16
|
|
|
|
|
| logger.info(f"CUDA available: {torch.cuda.is_available()}")
|
| if torch.cuda.is_available():
|
| gpu_count = torch.cuda.device_count()
|
| logger.info(f"Number of GPUs: {gpu_count}")
|
| for i in range(gpu_count):
|
| logger.info(f" GPU {i}: {torch.cuda.get_device_name(i)}")
|
| logger.info(f" Memory: {torch.cuda.get_device_properties(i).total_memory / 1024**3:.1f} GB")
|
| else:
|
| gpu_count = 0
|
| logger.info("Running on CPU - no GPUs available")
|
|
|
|
|
|
|
|
|
|
|
|
|
| logger.info("\n[1/3] Loading base Text2Image model...")
|
| model_id = os.environ.get("MODEL_ID", "Gerchegg/Qwen-Soloband-Diffusers")
|
| model_revision = os.environ.get("MODEL_REVISION", "main")
|
|
|
| try:
|
| start_time = time.time()
|
|
|
|
|
| if gpu_count > 1:
|
| device_map = "balanced"
|
| logger.info(f" Device map: balanced ({gpu_count} GPUs)")
|
| else:
|
| device_map = None
|
| logger.info(" Device map: single GPU")
|
|
|
|
|
| load_kwargs = {
|
| "torch_dtype": dtype,
|
| "device_map": device_map,
|
| "token": hf_token,
|
| "cache_dir": os.environ.get("HF_HOME", "/workspace/.cache/huggingface")
|
| }
|
| if model_revision:
|
| load_kwargs["revision"] = model_revision
|
|
|
| logger.info(f" Loading model: {model_id}")
|
| logger.info(f" Device map: {device_map}")
|
| logger.info(f" Dtype: {dtype}")
|
|
|
| pipe_txt2img = DiffusionPipeline.from_pretrained(model_id, **load_kwargs)
|
|
|
| if device_map is None:
|
| pipe_txt2img.to(device)
|
|
|
| load_time = time.time() - start_time
|
| logger.info(f" ✓ Text2Image loaded in {load_time:.1f}s")
|
|
|
| except Exception as e:
|
| logger.error(f" ❌ Error loading Text2Image: {e}")
|
| raise
|
|
|
|
|
| logger.info("\n[2/3] Creating Image2Image pipeline...")
|
| try:
|
|
|
|
|
| pipe_img2img = QwenImageImg2ImgPipeline(
|
| vae=pipe_txt2img.vae,
|
| text_encoder=pipe_txt2img.text_encoder,
|
| tokenizer=pipe_txt2img.tokenizer,
|
| transformer=pipe_txt2img.transformer,
|
| scheduler=pipe_txt2img.scheduler
|
| )
|
| logger.info(" ✓ Image2Image pipeline created (reusing components)")
|
| except Exception as e:
|
| logger.error(f" ❌ Error creating Image2Image: {e}")
|
| pipe_img2img = None
|
|
|
|
|
|
|
|
|
| logger.info("\nApplying memory optimizations...")
|
| for pipe in [pipe_txt2img, pipe_img2img]:
|
| if pipe and hasattr(pipe, 'vae'):
|
| if hasattr(pipe.vae, 'enable_tiling'):
|
| pipe.vae.enable_tiling()
|
| if hasattr(pipe.vae, 'enable_slicing'):
|
| pipe.vae.enable_slicing()
|
|
|
| logger.info(" ✓ VAE tiling and slicing enabled")
|
|
|
| logger.info("\n" + "=" * 60)
|
| logger.info("✓ ALL MODELS LOADED")
|
| logger.info("=" * 60)
|
|
|
|
|
|
|
|
|
|
|
| def resize_image(input_image, max_size=2048):
|
| """
|
| Изменяет размер изображения с сохранением пропорций (кратно 16).
|
| Если изображение меньше max_size, оставляет оригинальный размер (с округлением до 16).
|
| """
|
| w, h = input_image.size
|
| logger.info(f"[RESIZE] Входное изображение: {w}×{h}, max_size={max_size}")
|
|
|
|
|
| if w <= max_size and h <= max_size:
|
| new_w = w - (w % 16)
|
| new_h = h - (h % 16)
|
| if new_w == 0: new_w = 16
|
| if new_h == 0: new_h = 16
|
| if new_w != w or new_h != h:
|
| logger.info(f"[RESIZE] Округление до кратного 16: {w}×{h} → {new_w}×{new_h}")
|
| return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
| logger.info(f"[RESIZE] Размер уже кратен 16, изменение не требуется")
|
| return input_image
|
|
|
|
|
| scale = min(max_size / w, max_size / h)
|
| new_w = int(w * scale)
|
| new_h = int(h * scale)
|
| logger.info(f"[RESIZE] Масштабирование: scale={scale:.3f}, промежуточный размер: {new_w}×{new_h}")
|
|
|
|
|
| new_w = new_w - (new_w % 16)
|
| new_h = new_h - (new_h % 16)
|
|
|
|
|
| if new_w < 16: new_w = 16
|
| if new_h < 16: new_h = 16
|
|
|
| logger.info(f"[RESIZE] Финальный размер после округления: {new_w}×{new_h}")
|
| aspect_original = w / h
|
| aspect_new = new_w / new_h
|
| logger.info(f"[RESIZE] Соотношение сторон: {aspect_original:.3f} → {aspect_new:.3f} (разница: {abs(aspect_original - aspect_new):.3f})")
|
|
|
| return input_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
|
|
|
|
| LOCAL_LORA_DIR = "/workspace/loras"
|
|
|
|
|
| HUB_LORAS = {
|
| "Realism": {
|
| "repo": "flymy-ai/qwen-image-realism-lora",
|
| "trigger": "Super Realism portrait of",
|
| "weights": "pytorch_lora_weights.safetensors",
|
| "source": "hub"
|
| },
|
| "Anime": {
|
| "repo": "alfredplpl/qwen-image-modern-anime-lora",
|
| "trigger": "Japanese modern anime style, ",
|
| "weights": "pytorch_lora_weights.safetensors",
|
| "source": "hub"
|
| }
|
|
|
| }
|
|
|
| def scan_local_loras():
|
| """
|
| Сканирует папку /workspace/loras на наличие .safetensors файлов
|
| Возвращает dict с найденными LoRA
|
| """
|
| local_loras = {}
|
|
|
| if not os.path.exists(LOCAL_LORA_DIR):
|
| logger.info(f" Local LoRA directory not found: {LOCAL_LORA_DIR}")
|
| return local_loras
|
|
|
| logger.info(f" Scanning local LoRA directory: {LOCAL_LORA_DIR}")
|
|
|
| try:
|
| for file in os.listdir(LOCAL_LORA_DIR):
|
| if file.endswith('.safetensors'):
|
| lora_name = os.path.splitext(file)[0]
|
| local_path = os.path.join(LOCAL_LORA_DIR, file)
|
|
|
|
|
| local_loras[lora_name] = {
|
| "path": local_path,
|
| "trigger": "",
|
| "weights": file,
|
| "source": "local"
|
| }
|
|
|
| logger.info(f" ✓ Found local LoRA: {lora_name} ({file})")
|
|
|
| except Exception as e:
|
| logger.warning(f" Error scanning local LoRA directory: {e}")
|
|
|
| return local_loras
|
|
|
|
|
| logger.info("\nScanning for LoRA models...")
|
| LOCAL_LORAS = scan_local_loras()
|
|
|
|
|
| AVAILABLE_LORAS = {**HUB_LORAS, **LOCAL_LORAS}
|
|
|
| if LOCAL_LORAS:
|
| logger.info(f" ✓ Found {len(LOCAL_LORAS)} local LoRA(s)")
|
| logger.info(f" Total available LoRAs: {len(AVAILABLE_LORAS)}")
|
|
|
| def load_lora_weights(pipeline, lora_name, lora_scale, hf_token):
|
| """
|
| Загружает LoRA веса в pipeline (ленивая загрузка)
|
| Hub LoRA скачиваются только при использовании
|
| Локальные LoRA загружаются из /workspace/loras/
|
| """
|
| if lora_name == "None" or lora_name not in AVAILABLE_LORAS:
|
| logger.info(f"[LORA] Пропуск загрузки: lora_name='{lora_name}'")
|
| return None
|
|
|
| lora_info = AVAILABLE_LORAS[lora_name]
|
| logger.info(f"[LORA] Начало загрузки: {lora_name}")
|
| logger.info(f"[LORA] Source: {lora_info['source']}")
|
| logger.info(f"[LORA] Scale: {lora_scale}")
|
|
|
| try:
|
| load_start = time.time()
|
|
|
| if lora_info['source'] == 'hub':
|
|
|
| logger.info(f"[LORA] Загрузка из Hub: {lora_info['repo']}")
|
| logger.info(f"[LORA] Weight file: {lora_info.get('weights', 'pytorch_lora_weights.safetensors')}")
|
| logger.info(f"[LORA] (Скачивается если не в кэше...)")
|
|
|
| pipeline.load_lora_weights(
|
| lora_info['repo'],
|
| weight_name=lora_info.get('weights', 'pytorch_lora_weights.safetensors'),
|
| token=hf_token
|
| )
|
|
|
| load_time = time.time() - load_start
|
| logger.info(f"[LORA] ✓ Hub LoRA загружена за {load_time:.2f}s (закэширована)")
|
| else:
|
|
|
| logger.info(f"[LORA] Загрузка локальной LoRA: {lora_info['path']}")
|
| logger.info(f"[LORA] File: {lora_info['weights']}")
|
|
|
| pipeline.load_lora_weights(
|
| lora_info['path'],
|
| adapter_name=lora_name
|
| )
|
|
|
| load_time = time.time() - load_start
|
| logger.info(f"[LORA] ✓ Локальная LoRA загружена за {load_time:.2f}s")
|
|
|
|
|
| if hasattr(pipeline, 'set_adapters'):
|
| logger.info(f"[LORA] Установка adapter scale: {lora_scale}")
|
| pipeline.set_adapters([lora_name], adapter_weights=[lora_scale])
|
|
|
| trigger = lora_info.get('trigger', '')
|
| if trigger:
|
| logger.info(f"[LORA] Trigger word: '{trigger}'")
|
|
|
| return trigger
|
|
|
| except Exception as e:
|
| logger.error(f"[LORA] ❌ Ошибка загрузки {lora_name}: {e}")
|
| import traceback
|
| logger.error(f"[LORA] Traceback:\n{traceback.format_exc()}")
|
| return None
|
|
|
|
|
|
|
|
|
|
|
| MAX_SEED = np.iinfo(np.int32).max
|
|
|
|
|
| def gpu_decorator(duration=180):
|
| def decorator(func):
|
| if SPACES_AVAILABLE:
|
| return spaces.GPU(duration=duration)(func)
|
| return func
|
| return decorator
|
|
|
| @gpu_decorator(duration=180)
|
| def generate_text2img(
|
| prompt,
|
| negative_prompt=" ",
|
| width=1664,
|
| height=928,
|
| seed=42,
|
| randomize_seed=False,
|
| guidance_scale=2.5,
|
| num_inference_steps=40,
|
| lora_name="None",
|
| lora_scale=1.0,
|
| progress=gr.Progress(track_tqdm=True)
|
| ):
|
| """Text-to-Image генерация"""
|
|
|
| generation_start = time.time()
|
|
|
| logger.info("\n" + "=" * 60)
|
| logger.info("TEXT-TO-IMAGE GENERATION")
|
| logger.info("=" * 60)
|
|
|
|
|
| if randomize_seed:
|
| original_seed = seed
|
| seed = random.randint(0, MAX_SEED)
|
| logger.info(f"[SEED] Random seed: {original_seed} → {seed}")
|
| else:
|
| logger.info(f"[SEED] Fixed seed: {seed}")
|
|
|
|
|
| logger.info(f"[PARAMS] Prompt length: {len(prompt)} chars")
|
| logger.info(f"[PARAMS] Prompt: {prompt[:150]}{'...' if len(prompt) > 150 else ''}")
|
| if negative_prompt and negative_prompt != " ":
|
| logger.info(f"[PARAMS] Negative prompt: {negative_prompt[:100]}{'...' if len(negative_prompt) > 100 else ''}")
|
| logger.info(f"[PARAMS] Resolution: {width}×{height} = {width*height:,} pixels")
|
| logger.info(f"[PARAMS] Steps: {num_inference_steps}")
|
| logger.info(f"[PARAMS] CFG Scale: {guidance_scale}")
|
| logger.info(f"[PARAMS] LoRA: {lora_name} (scale: {lora_scale})")
|
|
|
| try:
|
|
|
| trigger_word = None
|
| original_prompt = prompt
|
| if lora_name != "None":
|
| logger.info(f"[T2I STAGE 1/3] Загрузка LoRA...")
|
| lora_start = time.time()
|
| trigger_word = load_lora_weights(pipe_txt2img, lora_name, lora_scale, hf_token)
|
| lora_time = time.time() - lora_start
|
| logger.info(f"[T2I STAGE 1/3] ✓ LoRA загружена за {lora_time:.2f}s")
|
|
|
|
|
| if trigger_word:
|
| prompt = trigger_word + prompt
|
| logger.info(f"[PROMPT] Добавлен trigger: '{trigger_word}'")
|
| logger.info(f"[PROMPT] Финальный промпт: {prompt[:150]}...")
|
| else:
|
| logger.info(f"[T2I STAGE 1/3] LoRA не используется")
|
|
|
|
|
| logger.info(f"[T2I STAGE 2/3] Подготовка генератора...")
|
| generator = torch.Generator(device=device).manual_seed(seed)
|
| logger.info(f"[T2I STAGE 2/3] ✓ Generator готов на устройстве: {device}")
|
|
|
|
|
| logger.info(f"[T2I STAGE 3/3] Начало генерации изображения...")
|
| logger.info(f"[T2I STAGE 3/3] Pipeline: DiffusionPipeline (Text2Img)")
|
| logger.info(f"[T2I STAGE 3/3] Ожидаемое время: ~{num_inference_steps * 0.9:.1f}s")
|
| gen_start = time.time()
|
|
|
| image = pipe_txt2img(
|
| prompt=prompt,
|
| negative_prompt=negative_prompt,
|
| width=width,
|
| height=height,
|
| num_inference_steps=num_inference_steps,
|
| true_cfg_scale=guidance_scale,
|
| generator=generator
|
| ).images[0]
|
|
|
| gen_time = time.time() - gen_start
|
| logger.info(f"[T2I STAGE 3/3] ✓ Изображение сгенерировано за {gen_time:.2f}s")
|
| logger.info(f"[T2I STAGE 3/3] Скорость: {gen_time/num_inference_steps:.3f}s на шаг")
|
|
|
|
|
| if lora_name != "None":
|
| logger.info(f"[CLEANUP] Выгрузка LoRA...")
|
| unload_start = time.time()
|
| pipe_txt2img.unload_lora_weights()
|
| unload_time = time.time() - unload_start
|
| logger.info(f"[CLEANUP] ✓ LoRA выгружена за {unload_time:.2f}s")
|
|
|
|
|
| total_time = time.time() - generation_start
|
| logger.info(f"[РЕЗУЛЬТАТ] Финальное изображение: {image.size[0]}×{image.size[1]}")
|
| logger.info(f"[РЕЗУЛЬТАТ] Использованный seed: {seed}")
|
| logger.info(f"[РЕЗУЛЬТАТ] Общее время генерации: {total_time:.2f}s")
|
| logger.info("=" * 60)
|
| logger.info("✓ TEXT-TO-IMAGE ЗАВЕРШЕНА УСПЕШНО")
|
| logger.info("=" * 60 + "\n")
|
|
|
| return image, seed, get_logs()
|
|
|
| except Exception as e:
|
| logger.error(f"[ERROR] ❌ Ошибка при генерации: {e}")
|
| import traceback
|
| logger.error(f"[ERROR] Traceback:\n{traceback.format_exc()}")
|
| raise gr.Error(f"Ошибка генерации: {e}")
|
|
|
| @gpu_decorator(duration=180)
|
| def generate_img2img(
|
| input_image,
|
| prompt,
|
| negative_prompt=" ",
|
| strength=0.75,
|
| seed=42,
|
| randomize_seed=False,
|
| guidance_scale=2.5,
|
| num_inference_steps=40,
|
| lora_name="None",
|
| lora_scale=1.0,
|
| progress=gr.Progress(track_tqdm=True)
|
| ):
|
| """Image-to-Image генерация"""
|
|
|
| generation_start = time.time()
|
|
|
| logger.info("\n" + "=" * 60)
|
| logger.info("IMAGE-TO-IMAGE GENERATION")
|
| logger.info("=" * 60)
|
|
|
|
|
| if input_image is None:
|
| logger.error("[ERROR] Входное изображение отсутствует!")
|
| raise gr.Error("Please upload an input image")
|
|
|
|
|
| original_width, original_height = input_image.size
|
| logger.info(f"[INPUT] ОРИГИНАЛЬНОЕ изображение: {original_width}×{original_height}")
|
| logger.info(f"[INPUT] Формат: {input_image.format if hasattr(input_image, 'format') else 'N/A'}")
|
| logger.info(f"[INPUT] Режим: {input_image.mode}")
|
| logger.info(f"[INPUT] Соотношение сторон: {original_width/original_height:.3f}")
|
|
|
|
|
| if randomize_seed:
|
| original_seed = seed
|
| seed = random.randint(0, MAX_SEED)
|
| logger.info(f"[SEED] Random seed: {original_seed} → {seed}")
|
| else:
|
| logger.info(f"[SEED] Fixed seed: {seed}")
|
|
|
|
|
| logger.info(f"[I2I STAGE 1/4] Предобработка изображения...")
|
| resize_start = time.time()
|
| resized = resize_image(input_image, max_size=3072)
|
| resize_time = time.time() - resize_start
|
| logger.info(f"[I2I STAGE 1/4] ✓ Изображение подготовлено за {resize_time:.2f}s")
|
|
|
|
|
| logger.info(f"[PARAMS] Prompt length: {len(prompt)} chars")
|
| logger.info(f"[PARAMS] Prompt: {prompt[:150]}{'...' if len(prompt) > 150 else ''}")
|
| if negative_prompt and negative_prompt != " ":
|
| logger.info(f"[PARAMS] Negative prompt: {negative_prompt[:100]}{'...' if len(negative_prompt) > 100 else ''}")
|
| logger.info(f"[PARAMS] Strength: {strength} (0=оригинал, 1=полная перерисовка)")
|
| logger.info(f"[PARAMS] Effective steps: {int(num_inference_steps * strength)} из {num_inference_steps}")
|
| logger.info(f"[PARAMS] CFG Scale: {guidance_scale}")
|
| logger.info(f"[PARAMS] LoRA: {lora_name} (scale: {lora_scale})")
|
|
|
| try:
|
| if pipe_img2img is None:
|
| logger.error("[ERROR] Image2Image pipeline не доступен!")
|
| raise gr.Error("Image2Image pipeline not available")
|
|
|
|
|
| trigger_word = None
|
| original_prompt = prompt
|
| if lora_name != "None":
|
| logger.info(f"[I2I STAGE 2/4] Загрузка LoRA...")
|
| lora_start = time.time()
|
| trigger_word = load_lora_weights(pipe_img2img, lora_name, lora_scale, hf_token)
|
| lora_time = time.time() - lora_start
|
| logger.info(f"[I2I STAGE 2/4] ✓ LoRA загружена за {lora_time:.2f}s")
|
|
|
|
|
| if trigger_word:
|
| prompt = trigger_word + prompt
|
| logger.info(f"[PROMPT] Добавлен trigger: '{trigger_word}'")
|
| logger.info(f"[PROMPT] Финальный промпт: {prompt[:150]}...")
|
| else:
|
| logger.info(f"[I2I STAGE 2/4] LoRA не используется")
|
|
|
|
|
| logger.info(f"[I2I STAGE 3/4] Подготовка генератора...")
|
| generator = torch.Generator(device=device).manual_seed(seed)
|
| logger.info(f"[I2I STAGE 3/4] ✓ Generator готов на устройстве: {device}")
|
|
|
|
|
| img_width, img_height = resized.size
|
| logger.info(f"[I2I STAGE 3/4] Финальное разрешение для генерации: {img_width}×{img_height}")
|
| logger.info(f"[I2I STAGE 3/4] Соотношение сторон: {img_width/img_height:.3f}")
|
|
|
|
|
| logger.info(f"[I2I STAGE 4/4] Начало генерации изображения...")
|
| logger.info(f"[I2I STAGE 4/4] Pipeline: QwenImageImg2ImgPipeline")
|
| effective_steps = int(num_inference_steps * strength)
|
| logger.info(f"[I2I STAGE 4/4] Реальных шагов денойзинга: {effective_steps}")
|
| logger.info(f"[I2I STAGE 4/4] Ожидаемое время: ~{effective_steps * 0.9:.1f}s")
|
| logger.info(f"[DEBUG] 🔍 ПЕРЕДАЕМ В PIPELINE: width={img_width}, height={img_height}")
|
| gen_start = time.time()
|
|
|
| image = pipe_img2img(
|
| prompt=prompt,
|
| negative_prompt=negative_prompt,
|
| image=resized,
|
| width=img_width,
|
| height=img_height,
|
| strength=strength,
|
| num_inference_steps=num_inference_steps,
|
| true_cfg_scale=guidance_scale,
|
| generator=generator
|
| ).images[0]
|
|
|
| gen_time = time.time() - gen_start
|
|
|
|
|
| final_width, final_height = image.size
|
| logger.info(f"[DEBUG] 🔍 ПОЛУЧИЛИ ИЗ PIPELINE: width={final_width}, height={final_height}")
|
|
|
| if final_width != img_width or final_height != img_height:
|
| logger.error(f"[BUG] ⚠️⚠️⚠️ РАЗМЕР ИЗМЕНИЛСЯ! ⚠️⚠️⚠️")
|
| logger.error(f"[BUG] Ожидалось: {img_width}×{img_height}")
|
| logger.error(f"[BUG] Получено: {final_width}×{final_height}")
|
| logger.error(f"[BUG] Соотношение: {final_width/final_height:.3f}")
|
| logger.error(f"[BUG] Это указывает на проблему в pipeline или diffusers!")
|
| else:
|
| logger.info(f"[I2I STAGE 4/4] ✓ Размер результата корректен: {final_width}×{final_height}")
|
|
|
| logger.info(f"[I2I STAGE 4/4] ✓ Изображение сгенерировано за {gen_time:.2f}s")
|
| logger.info(f"[I2I STAGE 4/4] Скорость: {gen_time/effective_steps:.3f}s на шаг")
|
|
|
|
|
| if lora_name != "None":
|
| logger.info(f"[CLEANUP] Выгрузка LoRA...")
|
| unload_start = time.time()
|
| pipe_img2img.unload_lora_weights()
|
| unload_time = time.time() - unload_start
|
| logger.info(f"[CLEANUP] ✓ LoRA выгружена за {unload_time:.2f}s")
|
|
|
|
|
| total_time = time.time() - generation_start
|
| logger.info(f"[РЕЗУЛЬТАТ] Финальное изображение: {image.size[0]}×{image.size[1]}")
|
| logger.info(f"[РЕЗУЛЬТАТ] Использованный seed: {seed}")
|
| logger.info(f"[РЕЗУЛЬТАТ] Общее время генерации: {total_time:.2f}s")
|
| logger.info(f"[РЕЗУЛЬТАТ] Разбивка времени:")
|
| logger.info(f"[РЕЗУЛЬТАТ] - Ресайз: {resize_time:.2f}s ({resize_time/total_time*100:.1f}%)")
|
| logger.info(f"[РЕЗУЛЬТАТ] - Генерация: {gen_time:.2f}s ({gen_time/total_time*100:.1f}%)")
|
| logger.info("=" * 60)
|
| logger.info("✓ IMAGE-TO-IMAGE ЗАВЕРШЕНА УСПЕШНО")
|
| logger.info("=" * 60 + "\n")
|
|
|
| return image, seed, get_logs()
|
|
|
| except Exception as e:
|
| logger.error(f"[ERROR] ❌ Ошибка при генерации: {e}")
|
| import traceback
|
| logger.error(f"[ERROR] Traceback:\n{traceback.format_exc()}")
|
| raise gr.Error(f"Ошибка генерации: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| MAX_SEED = np.iinfo(np.int32).max
|
|
|
| css = """
|
| #col-container {
|
| margin: 0 auto;
|
| max-width: 1400px;
|
| }
|
| """
|
|
|
| with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
|
| lora_choices = ["None"] + list(AVAILABLE_LORAS.keys())
|
|
|
| gr.Markdown(f"""
|
| # 🎨 Qwen Soloband - Image2Image + LoRA v2.2
|
|
|
| **Продвинутая модель генерации** с поддержкой Text-to-Image, Image-to-Image и LoRA стилей.
|
|
|
| ### ✨ Возможности:
|
| - 🖼️ **Text-to-Image** - Генерация из текста, разрешения до 2048×2048
|
| - 🔄 **Image-to-Image** - Модификация изображений с контролем strength (0.0-1.0, до 3072×3072)
|
| - 🎭 **LoRA Support** - {len(AVAILABLE_LORAS)} доступных стилей (Hub + локальные)
|
| - 🔌 **Full API** - Все функции доступны через API
|
| - ⚡ **Optimized** - VAE tiling/slicing, правильный QwenImageImg2ImgPipeline
|
| - 📋 **Detailed Logs** - Подробное логирование всех этапов генерации
|
|
|
| **Модель**: [Gerchegg/Qwen-Soloband-Diffusers](https://huggingface.co/Gerchegg/Qwen-Soloband-Diffusers)
|
|
|
| 💡 **Local LoRAs**: Положите .safetensors файлы в `/workspace/loras/` - они появятся автоматически!
|
| """)
|
|
|
| with gr.Tabs() as tabs:
|
|
|
|
|
| with gr.Tab("📝 Text-to-Image"):
|
| with gr.Row():
|
| with gr.Column(scale=1):
|
| t2i_prompt = gr.Text(
|
| label="Prompt",
|
| placeholder="SB_AI, a beautiful landscape...",
|
| lines=3
|
| )
|
|
|
| t2i_run = gr.Button("Generate", variant="primary")
|
|
|
| with gr.Accordion("Advanced Settings", open=False):
|
| t2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
|
|
|
| with gr.Row():
|
| t2i_width = gr.Slider(label="Width", minimum=512, maximum=2048, step=64, value=1664)
|
| t2i_height = gr.Slider(label="Height", minimum=512, maximum=2048, step=64, value=928)
|
|
|
| with gr.Row():
|
| t2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
|
| t2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
|
|
|
| with gr.Row():
|
| t2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
|
| t2i_random_seed = gr.Checkbox(label="Random", value=True)
|
|
|
| t2i_lora = gr.Radio(
|
| label="LoRA Style",
|
| choices=lora_choices,
|
| value="None",
|
| info=f"Hub: {len(HUB_LORAS)}, Local: {len(LOCAL_LORAS)}"
|
| )
|
| t2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
|
|
|
| with gr.Column(scale=1):
|
| t2i_output = gr.Image(label="Generated Image")
|
| t2i_seed_output = gr.Number(label="Used Seed")
|
|
|
|
|
| with gr.Tab("🔄 Image-to-Image"):
|
| with gr.Row():
|
| with gr.Column(scale=1):
|
| i2i_input = gr.Image(type="pil", label="Input Image")
|
| i2i_prompt = gr.Text(
|
| label="Prompt",
|
| placeholder="Transform this image into...",
|
| lines=3
|
| )
|
|
|
| i2i_strength = gr.Slider(
|
| label="Denoising Strength",
|
| info="0.0 = original image, 1.0 = complete redraw",
|
| minimum=0.0,
|
| maximum=1.0,
|
| step=0.05,
|
| value=0.75
|
| )
|
|
|
| i2i_run = gr.Button("Generate", variant="primary")
|
|
|
| with gr.Accordion("Advanced Settings", open=False):
|
| i2i_negative = gr.Text(label="Negative Prompt", value="blurry, low quality")
|
|
|
| with gr.Row():
|
| i2i_steps = gr.Slider(label="Steps", minimum=1, maximum=50, step=1, value=40)
|
| i2i_cfg = gr.Slider(label="CFG", minimum=0.0, maximum=7.5, step=0.1, value=2.5)
|
|
|
| with gr.Row():
|
| i2i_seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
|
| i2i_random_seed = gr.Checkbox(label="Random", value=True)
|
|
|
| i2i_lora = gr.Radio(
|
| label="LoRA Style",
|
| choices=lora_choices,
|
| value="None",
|
| info=f"Hub: {len(HUB_LORAS)}, Local: {len(LOCAL_LORAS)}"
|
| )
|
| i2i_lora_scale = gr.Slider(label="LoRA Strength", minimum=0.0, maximum=2.0, step=0.1, value=1.0)
|
|
|
| with gr.Column(scale=1):
|
| i2i_output = gr.Image(label="Generated Image")
|
| i2i_seed_output = gr.Number(label="Used Seed")
|
|
|
|
|
| with gr.Tab("📋 Logs"):
|
| gr.Markdown("""
|
| ## 📋 Логи генерации
|
|
|
| Здесь отображаются подробные логи последней генерации:
|
| - 🔍 Параметры запроса (prompt, size, seed, etc.)
|
| - ⚙️ Этапы обработки (resize, LoRA loading, generation)
|
| - ⏱️ Время выполнения каждого этапа
|
| - 📊 Финальная статистика и разбивка времени
|
|
|
| 💡 Логи автоматически обновляются после каждой генерации
|
| """)
|
|
|
| log_output = gr.Textbox(
|
| label="Логи",
|
| lines=25,
|
| max_lines=50,
|
| show_copy_button=True,
|
| interactive=False,
|
| placeholder="Логи появятся после первой генерации..."
|
| )
|
|
|
| with gr.Row():
|
| refresh_logs_btn = gr.Button("🔄 Обновить логи", size="sm")
|
| clear_logs_btn = gr.Button("🗑️ Очистить логи", size="sm", variant="stop")
|
|
|
|
|
| refresh_logs_btn.click(
|
| fn=get_logs,
|
| inputs=None,
|
| outputs=log_output
|
| )
|
|
|
|
|
| clear_logs_btn.click(
|
| fn=clear_logs,
|
| inputs=None,
|
| outputs=log_output
|
| )
|
|
|
|
|
| t2i_run.click(
|
| fn=generate_text2img,
|
| inputs=[
|
| t2i_prompt, t2i_negative, t2i_width, t2i_height,
|
| t2i_seed, t2i_random_seed, t2i_cfg, t2i_steps,
|
| t2i_lora, t2i_lora_scale
|
| ],
|
| outputs=[t2i_output, t2i_seed_output, log_output],
|
| api_name="text2img"
|
| )
|
|
|
| i2i_run.click(
|
| fn=generate_img2img,
|
| inputs=[
|
| i2i_input, i2i_prompt, i2i_negative, i2i_strength,
|
| i2i_seed, i2i_random_seed, i2i_cfg, i2i_steps,
|
| i2i_lora, i2i_lora_scale
|
| ],
|
| outputs=[i2i_output, i2i_seed_output, log_output],
|
| api_name="img2img"
|
| )
|
|
|
| if __name__ == "__main__":
|
| demo.launch(
|
| show_api=True,
|
| share=False,
|
| show_error=True
|
| )
|
|
|
|
|