import spaces import gradio as gr import torch import cv2 import numpy as np import os from PIL import Image, ImageDraw, ImageFilter from diffusers import StableDiffusionXLInpaintPipeline from insightface.app import FaceAnalysis import insightface from huggingface_hub import hf_hub_download, snapshot_download # ============================================================================== # 1. SETUP DELL'AMBIENTE E DOWNLOAD MODELLI # ============================================================================== # Download sicuro e robusto di antelopev2 da Hugging Face per evitare errori di unzip print("Download modelli AntelopeV2 da Hugging Face...") try: snapshot_download("DIAMONIK7777/antelopev2", local_dir="./models/antelopev2") except Exception as e: print(f"Errore download AntelopeV2: {e}") if not os.path.exists("./models/inswapper_128.onnx"): print("Download Inswapper...") hf_hub_download(repo_id="ezioruan/inswapper_128.onnx", filename="inswapper_128.onnx", local_dir="./models") # Inizializza l'analizzatore di volti (InsightFace) print("Inizializzazione FaceAnalysis...") face_app = FaceAnalysis(name='antelopev2', root='./', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) face_app.prepare(ctx_id=0, det_size=(640, 640)) # Inizializza Inswapper (per fissare l'identità alla fine) print("Inizializzazione Inswapper...") swapper = insightface.model_zoo.get_model('./models/inswapper_128.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) # Caricamento Pipeline Inpainting SDXL print("Caricamento pipeline SDXL Inpaint...") device = "cuda" if torch.cuda.is_available() else "cpu" dtype = torch.float16 if torch.cuda.is_available() else "float32" pipe = StableDiffusionXLInpaintPipeline.from_pretrained( "diffusers/stable-diffusion-xl-1.0-inpainting-0.1", torch_dtype=dtype, variant="fp16" if torch.cuda.is_available() else None, use_safetensors=True ) # Aggiunta IP-Adapter per l'analisi semantica (Capelli, Occhiali, Barba) print("Caricamento IP-Adapter per SDXL...") pipe.load_ip_adapter("h94/IP-Adapter", subfolder="sdxl_models", weight_name="ip-adapter_sdxl.bin") pipe.set_ip_adapter_scale(0.7) # Bilanciamento tra fedeltà al selfie e stile del prompt pipe.watermark = None # ============================================================================== # 2. CONFIGURAZIONE STILI E TEMPLATE # ============================================================================== STYLES = { "Stile 1: Bit-Builder (8-Bit/Pixel Art)": { "template": "esempi/8bit.png", "repo_id": "nerijs/pixel-art-xl", "weight_name": "pixel-art-xl.safetensors", "prompt": "pixelart style, 8-bit game style face and hair, perfect pixel art shading, 8-bit retro aesthetic, matching lighting", "negative_prompt": "ugly, deformed, lowres, realistic, photographic, 3d render, photo, photorealistic" }, "Stile 2: Il Costruttore Rinascimentale": { "template": "esempi/rinascimentale.png", "repo_id": "davidmoref/sdxl-lora-adapter-renaissance", "weight_name": "pytorch_lora_weights.safetensors", "prompt": "renaissance oil painting, oil painting face and hair, elegant brush strokes, chiaroscuro lighting, classic masterpiece, perfectly integrated head", "negative_prompt": "ugly, deformed, modern, photography, bad anatomy, worst quality, low quality, pixel art, cartoon" }, "Stile 3: Genio Creativo (Pop-Art)": { "template": "esempi/popart.png", "repo_id": None, "weight_name": None, "prompt": "vintage pop art, Comic book style pop-art face and hair, vibrant colors, halftone dots, graphic illustration, bold lines", "negative_prompt": "ugly, deformed, photorealistic, realistic photography, oil painting, 3d render" } } # ============================================================================== # 3. FUNZIONI CORE DELL'APPLICAZIONE # ============================================================================== def create_head_mask(image_pil, face_bbox): """Crea una maschera che copre l'intera testa (inclusi capelli e barba) del template.""" x1, y1, x2, y2 = face_bbox w = x2 - x1 h = y2 - y1 cx, cy = x1 + w/2, y1 + h/2 mask = Image.new("L", image_pil.size, 0) draw = ImageDraw.Draw(mask) # Area ovale molto abbondante per coprire capelli in alto e barba in basso mask_x1 = max(0, cx - w*1.1) mask_x2 = min(image_pil.width, cx + w*1.1) mask_y1 = max(0, cy - h*1.4) # Su per i capelli/cappelli mask_y2 = min(image_pil.height, cy + h*0.8) # Giù per la barba draw.ellipse((mask_x1, mask_y1, mask_x2, mask_y2), fill=255) mask = mask.filter(ImageFilter.GaussianBlur(15)) return mask @spaces.GPU(duration=60) def generate_avatar(user_image, selected_style): if user_image is None: raise gr.Error("Per favore, carica un'immagine del volto.") # 1. Caricamento e analisi del Template style_config = STYLES[selected_style] template_path = style_config["template"] if not os.path.exists(template_path): raise gr.Error(f"Errore: il file di template {template_path} non esiste.") template_pil = Image.open(template_path).convert("RGB") cv_template_image = cv2.cvtColor(np.array(template_pil), cv2.COLOR_RGB2BGR) template_faces = face_app.get(cv_template_image) if len(template_faces) == 0: raise gr.Error("Impossibile trovare un volto nel template di base.") template_face = sorted(template_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1] # 2. Analisi del volto utente cv_user_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR) user_faces = face_app.get(cv_user_image) if len(user_faces) == 0: raise gr.Error("Nessun volto rilevato nella tua foto. Riprova con un selfie più chiaro.") user_face = sorted(user_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1] # 3. Creazione maschera per rigenerazione della testa print("Creazione maschera per svuotare la testa del template...") mask_pil = create_head_mask(template_pil, template_face.bbox) # Ritaglio del selfie (per dare all'IP-Adapter solo la testa e ignorare lo sfondo della stanza) x1, y1, x2, y2 = user_face.bbox w = x2 - x1 h = y2 - y1 cx, cy = x1 + w/2, y1 + h/2 crop_size = max(w, h) * 2.5 ip_x1 = max(0, int(cx - crop_size/2)) ip_y1 = max(0, int(cy - crop_size/1.5)) ip_x2 = min(user_image.width, int(cx + crop_size/2)) ip_y2 = min(user_image.height, int(cy + crop_size/2)) ip_image = user_image.crop((ip_x1, ip_y1, ip_x2, ip_y2)) # 4. Rigenerazione Semantica (SDXL + IP-Adapter) repo_id = style_config["repo_id"] if repo_id: try: print(f"Caricamento LoRA {repo_id}...") pipe.load_lora_weights(repo_id, weight_name=style_config["weight_name"]) except Exception as e: print(f"Errore caricamento LoRA: {e}") pipe.to(device) print("Inizio Generazione Semantica della Testa (SDXL + IP-Adapter)...") # Usa una strength quasi al 100% per ignorare i pixel della testa originale sotto la maschera generated_image = pipe( prompt=style_config["prompt"], negative_prompt=style_config["negative_prompt"], image=template_pil, mask_image=mask_pil, ip_adapter_image=ip_image, strength=0.99, guidance_scale=8.0, num_inference_steps=35, ).images[0] if repo_id: try: pipe.unload_lora_weights() except: pass # 5. Iniezione dell'Identità pura (FaceSwap) print("Iniezione dell'identità tramite Inswapper...") cv_generated = cv2.cvtColor(np.array(generated_image), cv2.COLOR_RGB2BGR) generated_faces = face_app.get(cv_generated) if len(generated_faces) > 0: # Trova la faccia appena generata dall'IA gen_face = sorted(generated_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1] # Incolla ESATTAMENTE gli occhi, naso e bocca originali dell'utente sopra la testa generata cv_final = swapper.get(cv_generated, gen_face, user_face, paste_back=True) final_image = Image.fromarray(cv2.cvtColor(cv_final, cv2.COLOR_BGR2RGB)) else: print("Attenzione: SDXL non ha generato un volto rilevabile. Restituisco l'immagine senza Inswapper.") final_image = generated_image return final_image # ============================================================================== # 4. INTERFACCIA GRADIO (FRONTEND) # ============================================================================== custom_css = """ .container { max-width: 1000px; margin: auto; padding-top: 2rem; } .header-title { text-align: center; color: #1e3a8a; font-family: 'Inter', sans-serif; font-weight: 800; font-size: 2.5rem; margin-bottom: 0.5rem; } .header-subtitle { text-align: center; color: #4b5563; font-size: 1.1rem; margin-bottom: 2rem; } .generate-btn { background: linear-gradient(90deg, #10b981 0%, #3b82f6 100%) !important; color: white !important; font-weight: bold !important; font-size: 1.2rem !important; border: none !important; transition: transform 0.2s ease; } .generate-btn:hover { transform: scale(1.02); } """ theme = gr.themes.Soft(primary_hue="blue", secondary_hue="emerald").set( button_primary_background_fill="*primary_500", button_primary_background_fill_hover="*primary_600", ) with gr.Blocks(css=custom_css, theme=theme) as demo: with gr.Column(elem_classes="container"): gr.HTML("

🇮🇹 Italian Builders Avatar Generator 🇮🇹

") gr.HTML("

Trasforma il tuo selfie e unisciti alla community.

") with gr.Row(): with gr.Column(scale=1): input_image = gr.Image(type="pil", label="1. Carica il tuo Selfie (Ben illuminato)") with gr.Column(scale=1): style_selector = gr.Radio( choices=list(STYLES.keys()), label="2. Scegli il tuo Stile", value=list(STYLES.keys())[0] ) gr.Markdown("
") generate_button = gr.Button("Genera il tuo Avatar 🚀", elem_classes="generate-btn", variant="primary") with gr.Column(scale=1): output_image = gr.Image(label="Il tuo Avatar", interactive=False) generate_button.click( fn=generate_avatar, inputs=[input_image, style_selector], outputs=output_image ) if __name__ == "__main__": demo.launch()