#!/usr/bin/env python3 import gradio as gr import numpy as np import spaces import torch import random import base64 import io import math from PIL import Image from diffusers import FluxKontextPipeline from diffusers import FluxTransformer2DModel from diffusers.utils import load_image from diffusers import EulerDiscreteScheduler from huggingface_hub import hf_hub_download # Load pipeline with device detection device = "cuda" if torch.cuda.is_available() else "cpu" dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32 print(f"🔧 Using device: {device}") print(f"📊 Using dtype: {dtype}") pipe = FluxKontextPipeline.from_pretrained( "black-forest-labs/FLUX.1-Kontext-dev", torch_dtype=dtype ).to(device) # Keep the default scheduler that works with FluxKontextPipeline print(f"📅 Using default scheduler: {pipe.scheduler.__class__.__name__}") pipe.load_lora_weights( "DoozyWo/Kontext_avatar_LoRA", weight_name="Avataar_LoRA_000003000.safetensors", adapter_name="avatar_lora" ) pipe.set_adapters(["avatar_lora"], adapter_weights=[1.0]) MAX_SEED = np.iinfo(np.int32).max def scale_image_to_megapixels(image, target_megapixels=1.0): """Scale image to target megapixels like ComfyUI ImageScaleToTotalPixels""" original_width, original_height = image.size original_pixels = original_width * original_height target_pixels = target_megapixels * 1000000 if original_pixels <= target_pixels: return image scale_factor = math.sqrt(target_pixels / original_pixels) new_width = int(original_width * scale_factor) new_height = int(original_height * scale_factor) # Ensure dimensions are divisible by 8 for better model performance new_width = (new_width // 8) * 8 new_height = (new_height // 8) * 8 return image.resize((new_width, new_height), Image.Resampling.BICUBIC) @spaces.GPU def transform_to_avatar(input_image, custom_prompt="", guidance_scale=1.0, num_inference_steps=25, seed=42, randomize_seed=False, lora_strength=1.0): """ Transform image to Avatar character using FLUX Kontext + Avatar LoRA Args: input_image: Input PIL Image custom_prompt: Custom prompt for additional details guidance_scale: How closely to follow the prompt num_inference_steps: Quality vs speed seed: Random seed randomize_seed: Whether to randomize seed lora_strength: Strength of Avatar LoRA Returns: tuple: (result_images_list, seed_used, prompt_used) """ if randomize_seed: seed = random.randint(0, MAX_SEED) input_image = input_image.convert("RGB") # Scale image to 1 megapixel like ComfyUI scaled_image = scale_image_to_megapixels(input_image, target_megapixels=1.0) print(f"📏 Image scaled from {input_image.size} to {scaled_image.size}") # Update LoRA strength if needed if lora_strength != 1.0: pipe.set_adapters(["avatar_lora"], adapter_weights=[lora_strength]) # Construct Avatar transformation prompt (matching ComfyUI approach) base_prompt = "Turn this into a photorealistic Na'vi character from Avatar, with blue bioluminescent skin, large eyes, and set in the glowing jungle of Pandora." if custom_prompt.strip(): full_prompt = f"{base_prompt} {custom_prompt.strip()}" else: full_prompt = base_prompt # Use the exact prompt structure from ComfyUI prompt_with_template = full_prompt # No additional template wrapping print(f"Avatar transformation prompt: {prompt_with_template}") # Match ComfyUI parameters: cfg=1, steps=25 # Let the pipeline handle dimensions automatically image = pipe( image=scaled_image, prompt=prompt_with_template, guidance_scale=guidance_scale, # CFG=1 from ComfyUI num_inference_steps=num_inference_steps, generator=torch.Generator(device=pipe.device).manual_seed(seed), ).images[0] return [input_image, image], seed, prompt_with_template # Avatar style suggestions AVATAR_STYLES = { "warrior": "fierce Na'vi warrior with battle scars, tribal war paint, intimidating expression", "mystical": "ethereal Na'vi with glowing tattoos, magical aura, spiritual energy radiating", "forest_guardian": "nature-connected Na'vi, tree bark textures, forest camouflage patterns", "ceremonial": "ornate Na'vi with jewelry, ritual face paint, sacred symbols, regal bearing", "hunter": "stealth Na'vi with camouflage markings, predator instincts, keen alert expression", "shaman": "wise Na'vi spiritual leader, glowing eyes, mystical energy swirling around", "royalty": "regal Na'vi with elaborate headdress, noble posture, ornate decorations", "bioluminescent": "Na'vi with enhanced glowing patterns, bright bio-lights, phosphorescent skin" } def update_prompt_from_style(style_option): """Update the prompt textbox based on style selection""" if style_option == "custom": return "" else: return AVATAR_STYLES.get(style_option, "") css=""" #col-container { margin: 0 auto; max-width: 1020px; } """ with gr.Blocks(css=css) as demo: with gr.Column(elem_id="col-container"): gr.Markdown(f"""# Avatar Transformation Studio 🧞‍♂️ """) gr.Markdown(f"""Transform any portrait into a stunning Na'vi character using FLUX Kontext + Avatar LoRA ✨ """) with gr.Row(): with gr.Column(): input_image = gr.Image(label="Upload portrait for Avatar transformation", type="pil") with gr.Row(): style_dropdown = gr.Dropdown( choices=["custom"] + list(AVATAR_STYLES.keys()), value="warrior", label="Choose Avatar Style", scale=2 ) custom_prompt = gr.Textbox( label="Custom Details", info="Additional details for your Avatar transformation", show_label=True, max_lines=3, placeholder="Select a style above or add custom details like 'glowing tattoos', 'forest setting', etc.", value="fierce Na'vi warrior with battle scars, tribal war paint, intimidating expression", container=True ) run_button = gr.Button("🧞‍♂️ Transform to Avatar", scale=0, variant="primary") with gr.Accordion("Advanced Settings", open=False): seed = gr.Slider( label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42, ) randomize_seed = gr.Checkbox(label="Randomize seed", value=False) guidance_scale = gr.Slider( label="Guidance Scale (CFG)", minimum=1, maximum=10, step=0.1, value=1.0, info="ComfyUI uses CFG=1" ) num_inference_steps = gr.Slider( label="Inference Steps", minimum=10, maximum=50, step=1, value=25, ) lora_strength = gr.Slider( label="Avatar LoRA Strength", minimum=0.5, maximum=1.5, step=0.1, value=1.0, ) with gr.Column(): result = gr.ImageSlider(label="Avatar Transformation Result", show_label=False, interactive=False) final_prompt = gr.Textbox(label="Transformation prompt used", info="The actual prompt used for the Avatar transformation") # Update prompt when dropdown changes style_dropdown.change( fn=update_prompt_from_style, inputs=[style_dropdown], outputs=[custom_prompt] ) gr.Examples( examples=[ ["./examples/portrait1.jpg", "fierce warrior with glowing blue war paint", "warrior", 1.0, 25, 42, False, 1.0], ["./examples/portrait2.jpg", "mystical shaman with ethereal glow and floating spirits", "shaman", 1.0, 25, 42, False, 1.0], ["./examples/portrait3.jpg", "regal princess with ornate jewelry and ceremonial markings", "royalty", 1.0, 25, 42, False, 1.0], ], inputs=[input_image, custom_prompt, style_dropdown, guidance_scale, num_inference_steps, seed, randomize_seed, lora_strength], outputs=[result, seed, final_prompt], fn=transform_to_avatar, cache_examples="lazy" ) gr.on( triggers=[run_button.click, custom_prompt.submit], fn=transform_to_avatar, inputs=[input_image, custom_prompt, guidance_scale, num_inference_steps, seed, randomize_seed, lora_strength], outputs=[result, seed, final_prompt] ) # MCP Server functionality from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types import asyncio app = Server("avatar-transformation") @app.list_tools() async def handle_list_tools() -> types.ListToolsResult: return types.ListToolsResult( tools=[ types.Tool( name="transform_to_avatar", description="Transform portrait to Avatar character using FLUX Kontext + Avatar LoRA", inputSchema={ "type": "object", "properties": { "image_base64": {"type": "string", "description": "Base64 encoded image"}, "custom_prompt": {"type": "string", "default": ""}, "guidance_scale": {"type": "number", "default": 1.0}, "num_inference_steps": {"type": "integer", "default": 25}, "seed": {"type": "integer", "default": 42}, "randomize_seed": {"type": "boolean", "default": False}, "lora_strength": {"type": "number", "default": 1.0} }, "required": ["image_base64"] } ) ] ) @app.call_tool() async def handle_call_tool(request: types.CallToolRequest) -> types.CallToolResult: if request.name == "transform_to_avatar": try: args = request.arguments or {} # Decode base64 image image_data = args.get("image_base64") if not image_data: return types.CallToolResult( content=[types.TextContent(type="text", text="Error: No image provided")], isError=True ) image_bytes = base64.b64decode(image_data) input_image = Image.open(io.BytesIO(image_bytes)) # Transform image result_images, used_seed, used_prompt = transform_to_avatar( input_image=input_image, custom_prompt=args.get("custom_prompt", ""), guidance_scale=args.get("guidance_scale", 1.0), num_inference_steps=args.get("num_inference_steps", 25), seed=args.get("seed", 42), randomize_seed=args.get("randomize_seed", False), lora_strength=args.get("lora_strength", 1.0) ) # Convert result to base64 result_image = result_images[1] # Transformed image buffer = io.BytesIO() result_image.save(buffer, format='PNG') result_base64 = base64.b64encode(buffer.getvalue()).decode() return types.CallToolResult( content=[types.TextContent( type="text", text=f"Avatar transformation complete!\nSeed used: {used_seed}\nPrompt: {used_prompt}\nResult image (base64): {result_base64}" )] ) except Exception as e: return types.CallToolResult( content=[types.TextContent(type="text", text=f"Error: {str(e)}")], isError=True ) return types.CallToolResult( content=[types.TextContent(type="text", text=f"Unknown tool: {request.name}")], isError=True ) async def run_mcp_server(): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, InitializationOptions( server_name="avatar-transformation", server_version="1.0.0", capabilities=app.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) if __name__ == "__main__": # Check if running as MCP server or Gradio demo import sys if "--mcp" in sys.argv: asyncio.run(run_mcp_server()) else: # Launch with MCP server support demo.launch(mcp_server=True)