| 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 |
| } |
|
|