|
|
import os |
|
|
import gc |
|
|
from typing import Tuple, Optional, Dict, Any |
|
|
|
|
|
from PIL import Image |
|
|
import torch |
|
|
|
|
|
try: |
|
|
import spaces |
|
|
HAS_SPACES = True |
|
|
except ImportError: |
|
|
HAS_SPACES = False |
|
|
|
|
|
|
|
|
|
|
|
IDENTITY_PRESERVE = "same person, same face, same ethnicity, same age" |
|
|
IDENTITY_NEGATIVE = "different person, altered face, changed ethnicity, age change, distorted features" |
|
|
|
|
|
|
|
|
FACE_RESTORE_PRESERVE = "(same person:1.4), (preserve face:1.3), (same ethnicity:1.2), same pose, same lighting" |
|
|
FACE_RESTORE_NEGATIVE = "(different person:1.4), (deformed face:1.3), wrong ethnicity, age change, western features" |
|
|
|
|
|
|
|
|
|
|
|
IP_ADAPTER_REPO = "h94/IP-Adapter" |
|
|
IP_ADAPTER_SUBFOLDER = "sdxl_models" |
|
|
IP_ADAPTER_WEIGHT = "ip-adapter_sdxl.bin" |
|
|
IP_ADAPTER_SCALE_DEFAULT = 0.5 |
|
|
|
|
|
|
|
|
FACE_RESTORE_STYLE_SETTINGS = { |
|
|
"3d_cartoon": {"max_strength": 0.45, "lora_scale_mult": 0.7, "ip_scale": 0.4}, |
|
|
"anime": {"max_strength": 0.45, "lora_scale_mult": 0.7, "ip_scale": 0.4}, |
|
|
"illustrated_fantasy": {"max_strength": 0.42, "lora_scale_mult": 0.65, "ip_scale": 0.45}, |
|
|
"watercolor": {"max_strength": 0.40, "lora_scale_mult": 0.6, "ip_scale": 0.5}, |
|
|
"oil_painting": {"max_strength": 0.35, "lora_scale_mult": 0.5, "ip_scale": 0.6}, |
|
|
"pixel_art": {"max_strength": 0.50, "lora_scale_mult": 0.8, "ip_scale": 0.3}, |
|
|
} |
|
|
|
|
|
|
|
|
STYLE_CONFIGS = { |
|
|
"3d_cartoon": { |
|
|
"name": "3D Cartoon", |
|
|
"emoji": "🎬", |
|
|
"lora_repo": "imagepipeline/Samaritan-3d-Cartoon-SDXL", |
|
|
"lora_weight": "Samaritan 3d Cartoon.safetensors", |
|
|
"prompt": "3D cartoon style, smooth rounded features, soft ambient lighting, CGI quality, vibrant colors, cel-shaded, studio render", |
|
|
"negative_prompt": "ugly, deformed, noisy, blurry, low quality, flat, sketch", |
|
|
"lora_scale": 0.75, |
|
|
"recommended_strength": 0.55, |
|
|
}, |
|
|
"anime": { |
|
|
"name": "Anime Illustration", |
|
|
"emoji": "🌸", |
|
|
"lora_repo": None, |
|
|
"lora_weight": None, |
|
|
"prompt": "anime illustration, soft lighting, rich colors, delicate linework, smooth gradients, expressive eyes, cel shading, masterpiece", |
|
|
"negative_prompt": "ugly, deformed, bad anatomy, bad hands, blurry, low quality", |
|
|
"lora_scale": 0.0, |
|
|
"recommended_strength": 0.50, |
|
|
}, |
|
|
"illustrated_fantasy": { |
|
|
"name": "Illustrated Fantasy", |
|
|
"emoji": "🍃", |
|
|
"lora_repo": "ntc-ai/SDXL-LoRA-slider.Studio-Ghibli-style", |
|
|
"lora_weight": "Studio Ghibli style.safetensors", |
|
|
"prompt": "Ghibli style illustration, hand-painted look, soft watercolor textures, dreamy atmosphere, pastel colors, golden hour lighting, storybook quality", |
|
|
"negative_prompt": "ugly, dark, horror, scary, blurry, low quality, modern", |
|
|
"lora_scale": 1.0, |
|
|
"recommended_strength": 0.50, |
|
|
}, |
|
|
"watercolor": { |
|
|
"name": "Watercolor Art", |
|
|
"emoji": "🌊", |
|
|
"lora_repo": "ostris/watercolor_style_lora_sdxl", |
|
|
"lora_weight": "watercolor_style_lora.safetensors", |
|
|
"prompt": "watercolor painting, wet-on-wet technique, soft color bleeds, paper texture, transparent washes, feathered edges, hand-painted", |
|
|
"negative_prompt": "sharp edges, solid flat colors, harsh lines, vector art, airbrushed", |
|
|
"lora_scale": 1.0, |
|
|
"recommended_strength": 0.50, |
|
|
}, |
|
|
"oil_painting": { |
|
|
"name": "Classic Oil Paint", |
|
|
"emoji": "🖼️", |
|
|
"lora_repo": "EldritchAdam/ClassipeintXL", |
|
|
"lora_weight": "ClassipeintXL.safetensors", |
|
|
"prompt": "oil painting style, impasto technique, palette knife strokes, visible canvas texture, rich saturated pigments, masterful lighting, museum quality", |
|
|
"negative_prompt": "flat, smooth, cartoon, anime, blurry, low quality, modern, airbrushed", |
|
|
"lora_scale": 0.9, |
|
|
"recommended_strength": 0.50, |
|
|
}, |
|
|
"pixel_art": { |
|
|
"name": "Pixel Art", |
|
|
"emoji": "👾", |
|
|
"lora_repo": "nerijs/pixel-art-xl", |
|
|
"lora_weight": "pixel-art-xl.safetensors", |
|
|
"prompt": "pixel art style, crisp blocky pixels, limited color palette, 16-bit aesthetic, retro game vibes, dithering effects, sprite art", |
|
|
"negative_prompt": "smooth, blurry, anti-aliased, soft gradient, painterly", |
|
|
"lora_scale": 0.9, |
|
|
"recommended_strength": 0.60, |
|
|
}, |
|
|
} |
|
|
|
|
|
|
|
|
STYLE_BLENDS = { |
|
|
"cartoon_anime": { |
|
|
"name": "3D Anime Fusion", |
|
|
"emoji": "🎭", |
|
|
"description": "70% 3D Cartoon + 30% Anime linework", |
|
|
"primary_style": "3d_cartoon", |
|
|
"secondary_style": "anime", |
|
|
"primary_weight": 0.7, |
|
|
"secondary_weight": 0.3, |
|
|
"prompt": "3D cartoon with anime linework, smooth features, soft lighting, CGI quality, vibrant colors, cel-shaded", |
|
|
"negative_prompt": "ugly, deformed, noisy, blurry, low quality", |
|
|
"strength": 0.52, |
|
|
}, |
|
|
"fantasy_watercolor": { |
|
|
"name": "Dreamy Watercolor", |
|
|
"emoji": "🌈", |
|
|
"description": "60% Illustrated Fantasy + 40% Watercolor", |
|
|
"primary_style": "illustrated_fantasy", |
|
|
"secondary_style": "watercolor", |
|
|
"primary_weight": 0.6, |
|
|
"secondary_weight": 0.4, |
|
|
"prompt": "Ghibli style with watercolor washes, soft color bleeds, storybook atmosphere, paper texture, warm golden lighting", |
|
|
"negative_prompt": "dark, horror, harsh lines, solid colors", |
|
|
"strength": 0.50, |
|
|
}, |
|
|
"anime_fantasy": { |
|
|
"name": "Anime Storybook", |
|
|
"emoji": "📖", |
|
|
"description": "50% Anime + 50% Illustrated Fantasy", |
|
|
"primary_style": "anime", |
|
|
"secondary_style": "illustrated_fantasy", |
|
|
"primary_weight": 0.5, |
|
|
"secondary_weight": 0.5, |
|
|
"prompt": "Ghibli anime illustration, hand-painted storybook, soft lighting, pastel colors, expressive eyes, warm glow", |
|
|
"negative_prompt": "ugly, deformed, bad anatomy, dark, horror, blurry", |
|
|
"strength": 0.48, |
|
|
}, |
|
|
"oil_classical": { |
|
|
"name": "Renaissance Portrait", |
|
|
"emoji": "👑", |
|
|
"description": "Classical oil painting style", |
|
|
"primary_style": "oil_painting", |
|
|
"secondary_style": "oil_painting", |
|
|
"primary_weight": 1.0, |
|
|
"secondary_weight": 0.0, |
|
|
"prompt": "classical oil portrait, impasto technique, palette knife strokes, chiaroscuro lighting, canvas texture, museum quality", |
|
|
"negative_prompt": "flat, cartoon, anime, modern, minimalist, overexposed", |
|
|
"strength": 0.50, |
|
|
}, |
|
|
"pixel_retro": { |
|
|
"name": "Retro Game Art", |
|
|
"emoji": "🕹️", |
|
|
"description": "Pixel art with enhanced retro feel", |
|
|
"primary_style": "pixel_art", |
|
|
"secondary_style": "pixel_art", |
|
|
"primary_weight": 1.0, |
|
|
"secondary_weight": 0.0, |
|
|
"prompt": "retro pixel art, crisp blocky pixels, limited palette, arcade aesthetic, dithering, 16-bit charm, sprite art", |
|
|
"negative_prompt": "smooth, blurry, anti-aliased, modern, gradient", |
|
|
"strength": 0.58, |
|
|
}, |
|
|
} |
|
|
|
|
|
|
|
|
class StyleTransferEngine: |
|
|
""" |
|
|
Multi-style image transformation engine using SDXL + LoRAs. |
|
|
Supports: 3D Cartoon, Anime, Watercolor, Oil Painting, Pixel Art styles. |
|
|
With IP-Adapter support for identity preservation. |
|
|
""" |
|
|
|
|
|
BASE_MODEL = "stabilityai/stable-diffusion-xl-base-1.0" |
|
|
|
|
|
def __init__(self): |
|
|
self.device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
self.pipe = None |
|
|
self.current_lora = None |
|
|
self.is_loaded = False |
|
|
self.ip_adapter_loaded = False |
|
|
|
|
|
def load_model(self) -> None: |
|
|
"""Load SDXL base pipeline.""" |
|
|
if self.is_loaded: |
|
|
return |
|
|
|
|
|
print("→ Loading SDXL base model...") |
|
|
|
|
|
from diffusers import AutoPipelineForImage2Image |
|
|
|
|
|
actual_device = "cuda" if torch.cuda.is_available() else self.device |
|
|
|
|
|
self.pipe = AutoPipelineForImage2Image.from_pretrained( |
|
|
self.BASE_MODEL, |
|
|
torch_dtype=torch.float16 if actual_device == "cuda" else torch.float32, |
|
|
variant="fp16" if actual_device == "cuda" else None, |
|
|
use_safetensors=True, |
|
|
) |
|
|
|
|
|
self.pipe.to(actual_device) |
|
|
|
|
|
|
|
|
if actual_device == "cuda": |
|
|
try: |
|
|
self.pipe.enable_xformers_memory_efficient_attention() |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
self.is_loaded = True |
|
|
self.device = actual_device |
|
|
print(f"✓ SDXL base loaded ({actual_device})") |
|
|
|
|
|
def _load_lora(self, style_key: str) -> None: |
|
|
"""Load LoRA for the specified style.""" |
|
|
config = STYLE_CONFIGS.get(style_key) |
|
|
if not config: |
|
|
return |
|
|
|
|
|
lora_repo = config.get("lora_repo") |
|
|
|
|
|
|
|
|
if lora_repo is None: |
|
|
if self.current_lora is not None: |
|
|
print("→ Unloading previous LoRA...") |
|
|
self.pipe.unload_lora_weights() |
|
|
self.current_lora = None |
|
|
return |
|
|
|
|
|
if self.current_lora == lora_repo: |
|
|
return |
|
|
|
|
|
|
|
|
if self.current_lora is not None: |
|
|
print(f"→ Unloading previous LoRA: {self.current_lora}") |
|
|
self.pipe.unload_lora_weights() |
|
|
|
|
|
|
|
|
print(f"→ Loading LoRA: {config['name']}...") |
|
|
try: |
|
|
lora_weight = config.get("lora_weight") |
|
|
if lora_weight: |
|
|
self.pipe.load_lora_weights(lora_repo, weight_name=lora_weight) |
|
|
else: |
|
|
self.pipe.load_lora_weights(lora_repo) |
|
|
|
|
|
self.current_lora = lora_repo |
|
|
print(f"✓ LoRA loaded: {config['name']}") |
|
|
except Exception as e: |
|
|
print(f"⚠ LoRA loading failed: {e}, continuing without LoRA") |
|
|
self.current_lora = None |
|
|
|
|
|
def _load_ip_adapter(self) -> bool: |
|
|
"""Load IP-Adapter for identity preservation.""" |
|
|
if self.ip_adapter_loaded: |
|
|
return True |
|
|
|
|
|
if self.pipe is None: |
|
|
return False |
|
|
|
|
|
print("→ Loading IP-Adapter for face preservation...") |
|
|
try: |
|
|
self.pipe.load_ip_adapter( |
|
|
IP_ADAPTER_REPO, |
|
|
subfolder=IP_ADAPTER_SUBFOLDER, |
|
|
weight_name=IP_ADAPTER_WEIGHT |
|
|
) |
|
|
self.ip_adapter_loaded = True |
|
|
print("✓ IP-Adapter loaded") |
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"⚠ IP-Adapter loading failed: {e}") |
|
|
self.ip_adapter_loaded = False |
|
|
return False |
|
|
|
|
|
def _unload_ip_adapter(self) -> None: |
|
|
"""Unload IP-Adapter to free memory.""" |
|
|
if not self.ip_adapter_loaded or self.pipe is None: |
|
|
return |
|
|
|
|
|
try: |
|
|
self.pipe.unload_ip_adapter() |
|
|
self.ip_adapter_loaded = False |
|
|
print("✓ IP-Adapter unloaded") |
|
|
except Exception as e: |
|
|
print(f"⚠ IP-Adapter unload failed: {e}") |
|
|
|
|
|
def unload_model(self) -> None: |
|
|
"""Unload model and free memory.""" |
|
|
if not self.is_loaded: |
|
|
return |
|
|
|
|
|
|
|
|
if self.ip_adapter_loaded: |
|
|
self._unload_ip_adapter() |
|
|
|
|
|
if self.pipe is not None: |
|
|
del self.pipe |
|
|
self.pipe = None |
|
|
|
|
|
self.current_lora = None |
|
|
self.ip_adapter_loaded = False |
|
|
|
|
|
gc.collect() |
|
|
if torch.cuda.is_available(): |
|
|
torch.cuda.empty_cache() |
|
|
|
|
|
self.is_loaded = False |
|
|
print("✓ Model unloaded") |
|
|
|
|
|
def _preprocess_image(self, image: Image.Image) -> Image.Image: |
|
|
"""Preprocess image for SDXL - resize to appropriate dimensions.""" |
|
|
if image.mode != 'RGB': |
|
|
image = image.convert('RGB') |
|
|
|
|
|
|
|
|
max_size = 1024 |
|
|
width, height = image.size |
|
|
|
|
|
if width > height: |
|
|
new_width = max_size |
|
|
new_height = int(height * (max_size / width)) |
|
|
else: |
|
|
new_height = max_size |
|
|
new_width = int(width * (max_size / height)) |
|
|
|
|
|
|
|
|
new_width = (new_width // 8) * 8 |
|
|
new_height = (new_height // 8) * 8 |
|
|
|
|
|
|
|
|
new_width = max(new_width, 512) |
|
|
new_height = max(new_height, 512) |
|
|
|
|
|
image = image.resize((new_width, new_height), Image.LANCZOS) |
|
|
return image |
|
|
|
|
|
def generate_styled_image( |
|
|
self, |
|
|
image: Image.Image, |
|
|
style_key: str = "3d_cartoon", |
|
|
strength: float = 0.65, |
|
|
guidance_scale: float = 7.5, |
|
|
num_inference_steps: int = 30, |
|
|
custom_prompt: str = "", |
|
|
seed: int = -1, |
|
|
face_restore: bool = False |
|
|
) -> Tuple[Image.Image, int]: |
|
|
""" |
|
|
Convert image to the specified style. |
|
|
|
|
|
Args: |
|
|
image: Input PIL Image |
|
|
style_key: One of: 3d_cartoon, anime, illustrated_fantasy, watercolor, oil_painting, pixel_art |
|
|
strength: How much to transform (0.0-1.0) |
|
|
guidance_scale: How closely to follow the prompt |
|
|
num_inference_steps: Number of denoising steps |
|
|
custom_prompt: Additional prompt text |
|
|
seed: Random seed (-1 for random) |
|
|
face_restore: Enable enhanced face preservation mode |
|
|
|
|
|
Returns: |
|
|
Tuple of (Stylized PIL Image, seed used) |
|
|
""" |
|
|
if not self.is_loaded: |
|
|
self.load_model() |
|
|
|
|
|
|
|
|
config = STYLE_CONFIGS.get(style_key, STYLE_CONFIGS["3d_cartoon"]) |
|
|
|
|
|
|
|
|
self._load_lora(style_key) |
|
|
|
|
|
|
|
|
print("→ Preprocessing image...") |
|
|
processed_image = self._preprocess_image(image) |
|
|
|
|
|
|
|
|
face_settings = FACE_RESTORE_STYLE_SETTINGS.get(style_key, { |
|
|
"max_strength": 0.45, "lora_scale_mult": 0.7, "ip_scale": 0.5 |
|
|
}) |
|
|
|
|
|
|
|
|
base_prompt = config["prompt"] |
|
|
ip_adapter_image = None |
|
|
ip_scale = 0.0 |
|
|
|
|
|
if face_restore: |
|
|
|
|
|
preserve_prompt = FACE_RESTORE_PRESERVE |
|
|
negative_base = FACE_RESTORE_NEGATIVE |
|
|
|
|
|
|
|
|
max_str = face_settings["max_strength"] |
|
|
strength = min(strength, max_str) |
|
|
print(f"→ Face Restore enabled: strength capped at {strength} (style: {style_key})") |
|
|
|
|
|
|
|
|
if self._load_ip_adapter(): |
|
|
ip_adapter_image = processed_image |
|
|
ip_scale = face_settings["ip_scale"] |
|
|
print(f"→ IP-Adapter scale: {ip_scale}") |
|
|
else: |
|
|
preserve_prompt = IDENTITY_PRESERVE |
|
|
negative_base = IDENTITY_NEGATIVE |
|
|
|
|
|
if self.ip_adapter_loaded: |
|
|
self._unload_ip_adapter() |
|
|
|
|
|
if custom_prompt: |
|
|
prompt = f"{preserve_prompt}, {base_prompt}, {custom_prompt}" |
|
|
else: |
|
|
prompt = f"{preserve_prompt}, {base_prompt}" |
|
|
|
|
|
|
|
|
negative_prompt = f"{negative_base}, {config['negative_prompt']}" |
|
|
|
|
|
|
|
|
lora_scale = config.get("lora_scale", 1.0) |
|
|
if face_restore: |
|
|
lora_scale = lora_scale * face_settings["lora_scale_mult"] |
|
|
|
|
|
|
|
|
if seed == -1: |
|
|
seed = torch.randint(0, 2147483647, (1,)).item() |
|
|
generator = torch.Generator(device=self.device).manual_seed(seed) |
|
|
|
|
|
|
|
|
print(f"→ Generating {config['name']} style (strength: {strength}, steps: {num_inference_steps}, seed: {seed})...") |
|
|
|
|
|
|
|
|
gen_kwargs = { |
|
|
"prompt": prompt, |
|
|
"negative_prompt": negative_prompt, |
|
|
"image": processed_image, |
|
|
"strength": strength, |
|
|
"guidance_scale": guidance_scale, |
|
|
"num_inference_steps": num_inference_steps, |
|
|
"generator": generator, |
|
|
} |
|
|
|
|
|
|
|
|
if self.current_lora is not None: |
|
|
gen_kwargs["cross_attention_kwargs"] = {"scale": lora_scale} |
|
|
|
|
|
|
|
|
if ip_adapter_image is not None and self.ip_adapter_loaded: |
|
|
self.pipe.set_ip_adapter_scale(ip_scale) |
|
|
gen_kwargs["ip_adapter_image"] = ip_adapter_image |
|
|
|
|
|
result = self.pipe(**gen_kwargs).images[0] |
|
|
|
|
|
print(f"✓ {config['name']} style generated (seed: {seed})") |
|
|
|
|
|
|
|
|
gc.collect() |
|
|
if torch.cuda.is_available(): |
|
|
torch.cuda.empty_cache() |
|
|
|
|
|
return result, seed |
|
|
|
|
|
def generate_blended_style( |
|
|
self, |
|
|
image: Image.Image, |
|
|
blend_key: str, |
|
|
custom_prompt: str = "", |
|
|
seed: int = -1, |
|
|
face_restore: bool = False |
|
|
) -> Tuple[Image.Image, int]: |
|
|
""" |
|
|
Generate image using a style blend preset. |
|
|
|
|
|
Args: |
|
|
image: Input PIL Image |
|
|
blend_key: Key from STYLE_BLENDS |
|
|
custom_prompt: Additional prompt text |
|
|
seed: Random seed (-1 for random) |
|
|
face_restore: Enable enhanced face preservation mode |
|
|
|
|
|
Returns: |
|
|
Tuple of (Stylized PIL Image, seed used) |
|
|
""" |
|
|
if not self.is_loaded: |
|
|
self.load_model() |
|
|
|
|
|
blend_config = STYLE_BLENDS.get(blend_key) |
|
|
if not blend_config: |
|
|
return self.generate_styled_image(image, "3d_cartoon", seed=seed, face_restore=face_restore) |
|
|
|
|
|
|
|
|
primary_style = blend_config["primary_style"] |
|
|
self._load_lora(primary_style) |
|
|
|
|
|
|
|
|
print("→ Preprocessing image...") |
|
|
processed_image = self._preprocess_image(image) |
|
|
|
|
|
|
|
|
face_settings = FACE_RESTORE_STYLE_SETTINGS.get(primary_style, { |
|
|
"max_strength": 0.45, "lora_scale_mult": 0.7, "ip_scale": 0.5 |
|
|
}) |
|
|
|
|
|
|
|
|
base_prompt = blend_config["prompt"] |
|
|
ip_adapter_image = None |
|
|
ip_scale = 0.0 |
|
|
|
|
|
if face_restore: |
|
|
preserve_prompt = FACE_RESTORE_PRESERVE |
|
|
negative_base = FACE_RESTORE_NEGATIVE |
|
|
|
|
|
|
|
|
max_str = face_settings["max_strength"] |
|
|
strength = min(blend_config["strength"], max_str) |
|
|
print(f"→ Face Restore enabled: strength capped at {strength} (blend: {blend_key})") |
|
|
|
|
|
|
|
|
if self._load_ip_adapter(): |
|
|
ip_adapter_image = processed_image |
|
|
ip_scale = face_settings["ip_scale"] |
|
|
print(f"→ IP-Adapter scale: {ip_scale}") |
|
|
else: |
|
|
preserve_prompt = IDENTITY_PRESERVE |
|
|
negative_base = IDENTITY_NEGATIVE |
|
|
strength = blend_config["strength"] |
|
|
|
|
|
if self.ip_adapter_loaded: |
|
|
self._unload_ip_adapter() |
|
|
|
|
|
if custom_prompt: |
|
|
prompt = f"{preserve_prompt}, {base_prompt}, {custom_prompt}" |
|
|
else: |
|
|
prompt = f"{preserve_prompt}, {base_prompt}" |
|
|
|
|
|
|
|
|
negative_prompt = f"{negative_base}, {blend_config['negative_prompt']}" |
|
|
|
|
|
|
|
|
primary_config = STYLE_CONFIGS.get(primary_style, {}) |
|
|
lora_scale = primary_config.get("lora_scale", 1.0) * blend_config["primary_weight"] |
|
|
if face_restore: |
|
|
lora_scale = lora_scale * face_settings["lora_scale_mult"] |
|
|
|
|
|
|
|
|
if seed == -1: |
|
|
seed = torch.randint(0, 2147483647, (1,)).item() |
|
|
generator = torch.Generator(device=self.device).manual_seed(seed) |
|
|
|
|
|
|
|
|
print(f"→ Generating {blend_config['name']} blend (seed: {seed})...") |
|
|
|
|
|
gen_kwargs = { |
|
|
"prompt": prompt, |
|
|
"negative_prompt": negative_prompt, |
|
|
"image": processed_image, |
|
|
"strength": strength, |
|
|
"guidance_scale": 7.5, |
|
|
"num_inference_steps": 30, |
|
|
"generator": generator, |
|
|
} |
|
|
|
|
|
if self.current_lora is not None: |
|
|
gen_kwargs["cross_attention_kwargs"] = {"scale": lora_scale} |
|
|
|
|
|
|
|
|
if ip_adapter_image is not None and self.ip_adapter_loaded: |
|
|
self.pipe.set_ip_adapter_scale(ip_scale) |
|
|
gen_kwargs["ip_adapter_image"] = ip_adapter_image |
|
|
|
|
|
result = self.pipe(**gen_kwargs).images[0] |
|
|
|
|
|
print(f"✓ {blend_config['name']} blend generated (seed: {seed})") |
|
|
|
|
|
|
|
|
gc.collect() |
|
|
if torch.cuda.is_available(): |
|
|
torch.cuda.empty_cache() |
|
|
|
|
|
return result, seed |
|
|
|
|
|
def generate_all_outputs( |
|
|
self, |
|
|
image: Image.Image, |
|
|
style_key: str = "3d_cartoon", |
|
|
strength: float = 0.65, |
|
|
guidance_scale: float = 7.5, |
|
|
num_inference_steps: int = 30, |
|
|
custom_prompt: str = "", |
|
|
seed: int = -1, |
|
|
is_blend: bool = False, |
|
|
face_restore: bool = False |
|
|
) -> dict: |
|
|
""" |
|
|
Generate styled image output. |
|
|
|
|
|
Returns dict with success status, stylized image, and seed used. |
|
|
""" |
|
|
result = { |
|
|
"success": False, |
|
|
"stylized_image": None, |
|
|
"preview_image": None, |
|
|
"style_name": "", |
|
|
"seed_used": 0, |
|
|
"error": None |
|
|
} |
|
|
|
|
|
try: |
|
|
if is_blend: |
|
|
|
|
|
blend_config = STYLE_BLENDS.get(style_key, {}) |
|
|
result["style_name"] = blend_config.get("name", "Unknown Blend") |
|
|
|
|
|
stylized, seed_used = self.generate_blended_style( |
|
|
image=image, |
|
|
blend_key=style_key, |
|
|
custom_prompt=custom_prompt, |
|
|
seed=seed, |
|
|
face_restore=face_restore |
|
|
) |
|
|
else: |
|
|
|
|
|
config = STYLE_CONFIGS.get(style_key, STYLE_CONFIGS["3d_cartoon"]) |
|
|
result["style_name"] = config["name"] |
|
|
|
|
|
stylized, seed_used = self.generate_styled_image( |
|
|
image=image, |
|
|
style_key=style_key, |
|
|
strength=strength, |
|
|
guidance_scale=guidance_scale, |
|
|
num_inference_steps=num_inference_steps, |
|
|
custom_prompt=custom_prompt, |
|
|
seed=seed, |
|
|
face_restore=face_restore |
|
|
) |
|
|
|
|
|
result["stylized_image"] = stylized |
|
|
result["preview_image"] = stylized |
|
|
result["seed_used"] = seed_used |
|
|
result["success"] = True |
|
|
print(f"✓ {result['style_name']} conversion completed (seed: {seed_used})") |
|
|
|
|
|
except Exception as e: |
|
|
result["error"] = str(e) |
|
|
print(f"✗ Style conversion failed: {e}") |
|
|
|
|
|
return result |
|
|
|
|
|
@staticmethod |
|
|
def get_available_styles() -> Dict[str, Dict[str, Any]]: |
|
|
"""Return available style configurations.""" |
|
|
return { |
|
|
key: { |
|
|
"name": config["name"], |
|
|
"emoji": config["emoji"], |
|
|
} |
|
|
for key, config in STYLE_CONFIGS.items() |
|
|
} |
|
|
|
|
|
@staticmethod |
|
|
def get_style_choices() -> list: |
|
|
"""Return style choices for UI dropdown.""" |
|
|
return [ |
|
|
f"{config['emoji']} {config['name']}" |
|
|
for config in STYLE_CONFIGS.values() |
|
|
] |
|
|
|
|
|
@staticmethod |
|
|
def get_style_key_from_choice(choice: str) -> str: |
|
|
"""Convert UI choice back to style key.""" |
|
|
for key, config in STYLE_CONFIGS.items(): |
|
|
if config["name"] in choice: |
|
|
return key |
|
|
return "3d_cartoon" |
|
|
|
|
|
@staticmethod |
|
|
def get_blend_choices() -> list: |
|
|
"""Return blend preset choices for UI dropdown.""" |
|
|
return [ |
|
|
f"{config['emoji']} {config['name']} - {config['description']}" |
|
|
for config in STYLE_BLENDS.values() |
|
|
] |
|
|
|
|
|
@staticmethod |
|
|
def get_blend_key_from_choice(choice: str) -> str: |
|
|
"""Convert UI blend choice back to blend key.""" |
|
|
for key, config in STYLE_BLENDS.items(): |
|
|
if config["name"] in choice: |
|
|
return key |
|
|
return "cartoon_anime" |
|
|
|
|
|
@staticmethod |
|
|
def get_all_choices() -> dict: |
|
|
"""Return both style and blend choices for UI.""" |
|
|
styles = [ |
|
|
f"{config['emoji']} {config['name']}" |
|
|
for config in STYLE_CONFIGS.values() |
|
|
] |
|
|
blends = [ |
|
|
f"{config['emoji']} {config['name']}" |
|
|
for config in STYLE_BLENDS.values() |
|
|
] |
|
|
return { |
|
|
"styles": styles, |
|
|
"blends": blends, |
|
|
"all": styles + ["─── Style Blends ───"] + blends |
|
|
} |
|
|
|