# -*- coding: utf-8 -*- """ Fluent Emoji Generator Hugging Face Space: klibaner/fluent-emoji-generator Gradio 6.x + ZeroGPU + SDXL LoRA """ from __future__ import annotations import random import tempfile import time import uuid from pathlib import Path import gradio as gr import spaces import torch from diffusers import DiffusionPipeline from PIL import Image from rembg import new_session, remove # --------------------------------------------------------------------------- # Model configuration # --------------------------------------------------------------------------- BASE_MODEL_ID = "stabilityai/stable-diffusion-xl-base-1.0" LORA_MODEL_ID = "klibaner/fluent-emoji-style-sdxl" LORA_WEIGHT_NAME = "microsoft-fluent-emoji-sdxl.safetensors" LORA_ADAPTER_NAME = "fluent_emoji" TRIGGER_PHRASE = "msemj style" # Hard limits enforced server-side. The UI widgets below (sliders, max_length) # only constrain well-behaved browser clients -- anyone calling the exposed # `generate` API endpoint directly can send arbitrary values, so every one of # these bounds is re-checked inside generate() itself. MAX_PROMPT_LENGTH = 700 MAX_NEGATIVE_PROMPT_LENGTH = 500 MIN_STEPS, MAX_STEPS = 15, 50 MIN_GUIDANCE, MAX_GUIDANCE = 1.0, 12.0 MIN_LORA_WEIGHT, MAX_LORA_WEIGHT = 0.0, 1.5 MIN_SEED, MAX_SEED = 0, 2_147_483_647 DEFAULT_PROMPT = ( "msemj style, a friendly blue robot holding a laptop, " "3d emoji icon, smooth rounded shapes, soft studio lighting, " "isolated on white background" ) DEFAULT_NEGATIVE_PROMPT = ( "text, letters, watermark, logo, blurry, low quality, distorted, " "deformed, cropped, duplicate objects, busy background" ) EXAMPLES = [ [ "msemj style, a friendly blue robot holding a laptop, " "3d emoji icon, smooth rounded shapes, isolated on white background" ], [ "msemj style, a golden trophy with stars, " "3d emoji icon, isolated on white background" ], [ "msemj style, a small online store with a striped awning, " "3d emoji icon, isolated on white background" ], [ "msemj style, a red rocket launching from a cloud, " "3d emoji icon, isolated on white background" ], [ "msemj style, a purple artificial intelligence brain with small circuits, " "3d emoji icon, smooth rounded shapes, isolated on white background" ], [ "msemj style, a cheerful orange cat wearing headphones, " "3d emoji icon, smooth rounded shapes, isolated on white background" ], ] # --------------------------------------------------------------------------- # Internationalization # --------------------------------------------------------------------------- i18n = gr.I18n( en={ "hero": ( "# ✨ Fluent Emoji Generator\n" "Create colorful, rounded 3D emoji-style icons with an SDXL LoRA.\n\n" "**Tip:** English prompts usually produce the most reliable results." ), "prompt": "Prompt", "prompt_placeholder": "Describe one central object…", "negative_prompt": "Negative prompt", "settings": "Generation settings", "lora_weight": "LoRA weight", "steps": "Steps", "guidance": "Guidance scale", "seed": "Seed", "seed_info": "Use -1 for a random seed.", "generate": "Generate", "clear": "Clear", "examples": "### Prompt examples", "generated_image": "Generated PNG", "transparent_image": "Transparent PNG", "seed_used": "Seed used", "effective_prompt": "Prompt used", "download_original": "Download PNG", "download_transparent": "Download transparent PNG", "about": ( "The transparent version removes a white or nearly white background. " "It works best when the prompt includes `isolated on white background`." ), "disclaimer": ( "Independent community project. Not affiliated with or endorsed by Microsoft." ), }, ) # --------------------------------------------------------------------------- # Load the pipeline at module level. # ZeroGPU optimizes CUDA placement performed during Space startup. # --------------------------------------------------------------------------- torch.backends.cuda.matmul.allow_tf32 = True pipe = DiffusionPipeline.from_pretrained( BASE_MODEL_ID, torch_dtype=torch.float16, variant="fp16", use_safetensors=True, ) pipe.load_lora_weights( LORA_MODEL_ID, weight_name=LORA_WEIGHT_NAME, adapter_name=LORA_ADAPTER_NAME, ) pipe.set_adapters(LORA_ADAPTER_NAME, adapter_weights=0.9) pipe.set_progress_bar_config(disable=True) pipe.enable_vae_tiling() pipe.to("cuda") # --------------------------------------------------------------------------- # Background removal # --------------------------------------------------------------------------- # Keep rembg on CPU so ZeroGPU remains dedicated to SDXL inference. REMBG_MODEL = "isnet-general-use" rembg_session = new_session(REMBG_MODEL) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def normalize_prompt(prompt: str) -> str: """Validate the prompt and add the trigger phrase when it is missing.""" cleaned = " ".join((prompt or "").strip().split()) if not cleaned: raise gr.Error("Enter a prompt.") if len(cleaned) > MAX_PROMPT_LENGTH: raise gr.Error( f"The prompt is too long (maximum {MAX_PROMPT_LENGTH} characters)." ) if TRIGGER_PHRASE.lower() not in cleaned.lower(): cleaned = f"{TRIGGER_PHRASE}, {cleaned}" return cleaned def remove_background(image: Image.Image) -> Image.Image: """Remove the image background with rembg and return a transparent RGBA image.""" result = remove( image, session=rembg_session, ) return result.convert("RGBA") MAX_OUTPUT_FILE_AGE_SECONDS = 3600 # 1 hour def _cleanup_old_outputs(output_dir: Path) -> None: """Delete previously generated files older than MAX_OUTPUT_FILE_AGE_SECONDS. Generated PNGs are never referenced again once served to the client, but were previously left on disk forever, which would eventually fill up the machine's temp storage under sustained use. This keeps only recent files. """ cutoff = time.time() - MAX_OUTPUT_FILE_AGE_SECONDS for existing_file in output_dir.glob("fluent_emoji_*.png"): try: if existing_file.stat().st_mtime < cutoff: existing_file.unlink(missing_ok=True) except OSError: # Best-effort cleanup; never let this break image generation. continue def save_outputs( image: Image.Image, transparent_image: Image.Image, seed: int, ) -> tuple[str, str]: """Save both PNG files and return their temporary paths.""" output_dir = Path(tempfile.gettempdir()) / "fluent_emoji_generator" output_dir.mkdir(parents=True, exist_ok=True) _cleanup_old_outputs(output_dir) unique_id = uuid.uuid4().hex[:10] original_path = output_dir / f"fluent_emoji_{seed}_{unique_id}.png" transparent_path = ( output_dir / f"fluent_emoji_{seed}_{unique_id}_transparent.png" ) image.save(original_path, format="PNG") transparent_image.save(transparent_path, format="PNG") return str(original_path), str(transparent_path) # --------------------------------------------------------------------------- # Inference # --------------------------------------------------------------------------- @spaces.GPU(duration=120) @torch.inference_mode() def generate( prompt: str, negative_prompt: str, lora_weight: float, steps: int, guidance_scale: float, seed: int | float, ): effective_prompt = normalize_prompt(prompt) negative_prompt = (negative_prompt or "").strip() if len(negative_prompt) > MAX_NEGATIVE_PROMPT_LENGTH: raise gr.Error( f"The negative prompt is too long (maximum " f"{MAX_NEGATIVE_PROMPT_LENGTH} characters)." ) # Every numeric input below is re-validated here because the `generate` # API endpoint can be called directly, bypassing the slider limits in # the UI. Anything malformed raises gr.Error instead of crashing or # silently running with an out-of-range / expensive value. try: seed = int(seed) except (TypeError, ValueError): raise gr.Error("Seed must be an integer (use -1 for random).") if seed < 0: actual_seed = random.SystemRandom().randint(MIN_SEED, MAX_SEED) elif MIN_SEED <= seed <= MAX_SEED: actual_seed = seed else: raise gr.Error(f"Seed must be between {MIN_SEED} and {MAX_SEED}, or -1.") try: steps = int(steps) except (TypeError, ValueError): raise gr.Error("Steps must be an integer.") if not (MIN_STEPS <= steps <= MAX_STEPS): raise gr.Error(f"Steps must be between {MIN_STEPS} and {MAX_STEPS}.") try: lora_weight = float(lora_weight) except (TypeError, ValueError): raise gr.Error("LoRA weight must be a number.") if not (MIN_LORA_WEIGHT <= lora_weight <= MAX_LORA_WEIGHT): raise gr.Error( f"LoRA weight must be between {MIN_LORA_WEIGHT} and {MAX_LORA_WEIGHT}." ) try: guidance_scale = float(guidance_scale) except (TypeError, ValueError): raise gr.Error("Guidance scale must be a number.") if not (MIN_GUIDANCE <= guidance_scale <= MAX_GUIDANCE): raise gr.Error( f"Guidance scale must be between {MIN_GUIDANCE} and {MAX_GUIDANCE}." ) pipe.set_adapters( LORA_ADAPTER_NAME, adapter_weights=lora_weight, ) generator = torch.Generator(device="cuda").manual_seed(actual_seed) result = pipe( prompt=effective_prompt, negative_prompt=negative_prompt or None, num_inference_steps=steps, guidance_scale=guidance_scale, width=1024, height=1024, generator=generator, num_images_per_prompt=1, ) image = result.images[0].convert("RGB") transparent_image = remove_background(image) original_path, transparent_path = save_outputs( image=image, transparent_image=transparent_image, seed=actual_seed, ) return ( image, transparent_image, actual_seed, effective_prompt, original_path, transparent_path, ) def reset_form(): return ( DEFAULT_PROMPT, DEFAULT_NEGATIVE_PROMPT, 0.9, 30, 7.0, -1, None, None, None, "", None, None, ) # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- CSS = """ .gradio-container { max-width: 1180px !important; margin: 0 auto !important; padding-top: 24px !important; } .hero { text-align: center; margin-bottom: 12px; } .hero h1 { font-size: clamp(2rem, 5vw, 3.3rem); margin-bottom: 0.45rem; } .generate-btn { min-height: 48px; font-weight: 700; } .output-image img { object-fit: contain !important; } .small-note { opacity: 0.78; font-size: 0.92rem; } footer { opacity: 0.8; } """ theme = gr.themes.Soft( primary_hue="purple", secondary_hue="violet", neutral_hue="slate", radius_size="lg", ) with gr.Blocks( theme=theme, css=CSS, title="Fluent Emoji Generator", ) as demo: gr.Markdown(i18n("hero"), elem_classes=["hero"]) with gr.Row(equal_height=False): with gr.Column(scale=5, min_width=320): prompt = gr.Textbox( value=DEFAULT_PROMPT, label=i18n("prompt"), placeholder=i18n("prompt_placeholder"), lines=4, max_lines=8, max_length=700, autofocus=True, buttons=["copy"], ) negative_prompt = gr.Textbox( value=DEFAULT_NEGATIVE_PROMPT, label=i18n("negative_prompt"), lines=2, max_lines=5, max_length=500, ) with gr.Accordion(i18n("settings"), open=True): lora_weight = gr.Slider( minimum=0.0, maximum=1.5, value=0.9, step=0.05, label=i18n("lora_weight"), ) steps = gr.Slider( minimum=15, maximum=50, value=30, step=1, label=i18n("steps"), ) guidance_scale = gr.Slider( minimum=1.0, maximum=12.0, value=7.0, step=0.5, label=i18n("guidance"), ) seed = gr.Number( value=-1, precision=0, label=i18n("seed"), info=i18n("seed_info"), ) with gr.Row(): generate_button = gr.Button( i18n("generate"), variant="primary", elem_classes=["generate-btn"], scale=3, ) clear_button = gr.Button( i18n("clear"), variant="secondary", scale=1, ) gr.Markdown(i18n("examples")) gr.Examples( examples=EXAMPLES, inputs=[prompt], cache_examples=False, examples_per_page=6, ) with gr.Column(scale=6, min_width=360): with gr.Tabs(): with gr.Tab(i18n("generated_image")): generated_image = gr.Image( label=i18n("generated_image"), type="pil", format="png", interactive=False, height=540, elem_classes=["output-image"], ) with gr.Tab(i18n("transparent_image")): transparent_image = gr.Image( label=i18n("transparent_image"), type="pil", format="png", interactive=False, height=540, elem_classes=["output-image"], ) with gr.Row(): seed_used = gr.Number( label=i18n("seed_used"), precision=0, interactive=False, ) effective_prompt = gr.Textbox( label=i18n("effective_prompt"), lines=2, interactive=False, buttons=["copy"], ) with gr.Row(): original_file = gr.File( label=i18n("download_original"), interactive=False, ) transparent_file = gr.File( label=i18n("download_transparent"), interactive=False, ) gr.Markdown(i18n("about"), elem_classes=["small-note"]) gr.Markdown("---") gr.Markdown( i18n("disclaimer"), elem_classes=["small-note"], ) inference_inputs = [ prompt, negative_prompt, lora_weight, steps, guidance_scale, seed, ] inference_outputs = [ generated_image, transparent_image, seed_used, effective_prompt, original_file, transparent_file, ] generate_button.click( fn=generate, inputs=inference_inputs, outputs=inference_outputs, api_name="generate", concurrency_limit=1, concurrency_id="sdxl_gpu", ) prompt.submit( fn=generate, inputs=inference_inputs, outputs=inference_outputs, concurrency_limit=1, concurrency_id="sdxl_gpu", ) clear_button.click( fn=reset_form, inputs=None, outputs=[ prompt, negative_prompt, lora_weight, steps, guidance_scale, seed, generated_image, transparent_image, seed_used, effective_prompt, original_file, transparent_file, ], queue=False, ) if __name__ == "__main__": demo.queue(max_size=20).launch(i18n=i18n)