Spaces:
Running on Zero
Running on Zero
Upload 2 files
Browse files- app.py +255 -0
- requirements.txt +10 -0
app.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import math
|
| 4 |
+
import torch
|
| 5 |
+
import numpy as np
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import spaces
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
from huggingface_hub import hf_hub_download, snapshot_download
|
| 11 |
+
from insightface.app import FaceAnalysis
|
| 12 |
+
from diffusers import StableDiffusionXLControlNetPipeline, ControlNetModel
|
| 13 |
+
from diffusers.pipelines.controlnet.multicontrolnet import MultiControlNetModel
|
| 14 |
+
|
| 15 |
+
# ==============================================================================
|
| 16 |
+
# 1. SETUP E INIZIALIZZAZIONE GLOBALE
|
| 17 |
+
# ==============================================================================
|
| 18 |
+
|
| 19 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 20 |
+
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
| 21 |
+
|
| 22 |
+
def download_antelopev2():
|
| 23 |
+
"""Scarica automaticamente i modelli di InsightFace se non sono presenti."""
|
| 24 |
+
model_dir = "./models/antelopev2"
|
| 25 |
+
if not os.path.exists(model_dir) or len(os.listdir(model_dir)) < 5:
|
| 26 |
+
print("Scaricamento dei modelli antelopev2 in corso...")
|
| 27 |
+
os.makedirs(model_dir, exist_ok=True)
|
| 28 |
+
# Scarica da un repository pubblico affidabile
|
| 29 |
+
snapshot_download(repo_id="DIAMONIK7777/antelopev2", local_dir=model_dir)
|
| 30 |
+
print("Scaricamento completato.")
|
| 31 |
+
return "./models"
|
| 32 |
+
|
| 33 |
+
model_root = download_antelopev2()
|
| 34 |
+
|
| 35 |
+
app = FaceAnalysis(name='antelopev2', root=model_root, providers=['CPUExecutionProvider'])
|
| 36 |
+
app.prepare(ctx_id=0, det_size=(640, 640))
|
| 37 |
+
|
| 38 |
+
base_model_path = "stabilityai/stable-diffusion-xl-base-1.0"
|
| 39 |
+
instantid_path = "InstantX/InstantID"
|
| 40 |
+
canny_path = "diffusers/controlnet-canny-sdxl-1.0"
|
| 41 |
+
|
| 42 |
+
print("Caricamento ControlNet InstantID e Canny (per il Logo)...")
|
| 43 |
+
# Usiamo MultiControlNet per unire il riconoscimento facciale (InstantID) e la forma del logo (Canny)
|
| 44 |
+
controlnet_identity = ControlNetModel.from_pretrained(instantid_path, torch_dtype=dtype, use_safetensors=True)
|
| 45 |
+
controlnet_logo = ControlNetModel.from_pretrained(canny_path, torch_dtype=dtype, use_safetensors=True)
|
| 46 |
+
controlnet = MultiControlNetModel([controlnet_identity, controlnet_logo])
|
| 47 |
+
|
| 48 |
+
print("Caricamento Pipeline SDXL...")
|
| 49 |
+
pipe = StableDiffusionXLControlNetPipeline.from_pretrained(
|
| 50 |
+
base_model_path,
|
| 51 |
+
controlnet=controlnet,
|
| 52 |
+
torch_dtype=dtype,
|
| 53 |
+
use_safetensors=True
|
| 54 |
+
)
|
| 55 |
+
pipe.to(device)
|
| 56 |
+
|
| 57 |
+
print("Caricamento Face Adapter InstantID...")
|
| 58 |
+
pipe.load_ip_adapter("InstantX/InstantID", subfolder="models", weight_name="ip-adapter.bin")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ==============================================================================
|
| 62 |
+
# 2. CONFIGURAZIONE STILI (Uso di Modelli Hugging Face Pubblici)
|
| 63 |
+
# ==============================================================================
|
| 64 |
+
|
| 65 |
+
# Invece di file locali, usiamo i repo ID di modelli pubblici gratuiti su Hugging Face
|
| 66 |
+
STYLES = {
|
| 67 |
+
"Stile 1: Bit-Builder (8-Bit/Pixel Art)": {
|
| 68 |
+
"repo_id": "nerijs/pixel-art-xl",
|
| 69 |
+
"weight_name": "pixel-art-xl.safetensors",
|
| 70 |
+
"prompt": "pixelart style, Pixel art portrait of a tech builder, indie hacker typing on a retro mechanical keyboard. He is wearing a jacket with a small green, white, and red square patch on the chest. Cyberpunk desk setup, glowing monitors displaying code. High quality, 8-bit game style.",
|
| 71 |
+
"negative_prompt": "ugly, deformed, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, watermark, text"
|
| 72 |
+
},
|
| 73 |
+
"Stile 2: Il Costruttore Rinascimentale": {
|
| 74 |
+
"repo_id": "davidmoref/sdxl-lora-adapter-renaissance",
|
| 75 |
+
"weight_name": "pytorch_lora_weights.safetensors",
|
| 76 |
+
"prompt": "renaissance oil painting, A masterpiece renaissance oil painting of an Italian genius inventor in the style of Leonardo da Vinci. The man is holding a wooden prototype. He is wearing elegant renaissance dark clothes with a subtle green, white and red enameled pin on the lapel. Architectural blueprints on a rustic wooden table. Chiaroscuro lighting.",
|
| 77 |
+
"negative_prompt": "ugly, deformed, modern, photography, bad anatomy, worst quality, low quality, watermark, text"
|
| 78 |
+
},
|
| 79 |
+
"Stile 3: L'Architetto del Futuro (Design Geometrico)": {
|
| 80 |
+
"repo_id": "goofyai/flat_illustration_sdxl",
|
| 81 |
+
"weight_name": "flat_illustration_sdxl.safetensors",
|
| 82 |
+
"prompt": "flat vector art, Modern flat vector illustration of a tech architect holding a smartphone. He wears a minimalist grey suit jacket featuring a small geometric green, white, and red enamel badge. Clean geometric lines, industrial design background, minimalistic.",
|
| 83 |
+
"negative_prompt": "ugly, deformed, photorealistic, 3d render, bad anatomy, worst quality, low quality, watermark, text"
|
| 84 |
+
},
|
| 85 |
+
"Stile 4: Genio Creativo (Pop-Art)": {
|
| 86 |
+
"repo_id": None, # SDXL di base gestisce molto bene la pop-art senza LoRA
|
| 87 |
+
"weight_name": None,
|
| 88 |
+
"prompt": "vintage pop art, Comic book style pop-art portrait of a creative maker. Vibrant colors. He wears a trendy jacket adorned with an 'Italian Builders' custom patch in green, white and red. Halftone dots, vintage poster aesthetic.",
|
| 89 |
+
"negative_prompt": "ugly, deformed, photorealistic, bad anatomy, worst quality, low quality, watermark, extra text"
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
# ==============================================================================
|
| 94 |
+
# 3. PIPELINE DI GENERAZIONE
|
| 95 |
+
# ==============================================================================
|
| 96 |
+
|
| 97 |
+
def draw_kps(image_pil, kps, color_list=[(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255)]):
|
| 98 |
+
"""Genera keypoints per InstantID."""
|
| 99 |
+
stickwidth = 4
|
| 100 |
+
limbSeq = np.array([[0, 2], [1, 2], [3, 2], [4, 2]])
|
| 101 |
+
kps = np.array(kps)
|
| 102 |
+
w, h = image_pil.size
|
| 103 |
+
out_img = np.zeros([h, w, 3])
|
| 104 |
+
|
| 105 |
+
for i in range(len(limbSeq)):
|
| 106 |
+
index = limbSeq[i]
|
| 107 |
+
color = color_list[index[0]]
|
| 108 |
+
x = kps[index][:, 0]
|
| 109 |
+
y = kps[index][:, 1]
|
| 110 |
+
length = ((x[0] - x[1]) ** 2 + (y[0] - y[1]) ** 2) ** 0.5
|
| 111 |
+
angle = math.degrees(math.atan2(y[0] - y[1], x[0] - x[1]))
|
| 112 |
+
polygon = cv2.ellipse2Poly((int(np.mean(x)), int(np.mean(y))), (int(length / 2), stickwidth), int(angle), 0, 360, 1)
|
| 113 |
+
cv2.fillConvexPoly(out_img, polygon, color)
|
| 114 |
+
out_img = (out_img * 0.6).astype(np.uint8)
|
| 115 |
+
|
| 116 |
+
for idx, kp in enumerate(kps):
|
| 117 |
+
color = color_list[idx]
|
| 118 |
+
x, y = kp
|
| 119 |
+
cv2.circle(out_img, (int(x), int(y)), 10, color, -1)
|
| 120 |
+
|
| 121 |
+
return Image.fromarray(out_img.astype(np.uint8))
|
| 122 |
+
|
| 123 |
+
def process_logo_canny(logo_img):
|
| 124 |
+
"""Estrae i bordi (Canny) dal logo per guidare l'AI sulla forma, ignorando i colori originali."""
|
| 125 |
+
logo_cv = cv2.cvtColor(np.array(logo_img), cv2.COLOR_RGB2GRAY)
|
| 126 |
+
edges = cv2.Canny(logo_cv, 100, 200)
|
| 127 |
+
edges_3c = np.stack([edges]*3, axis=-1)
|
| 128 |
+
|
| 129 |
+
# Crea un'immagine vuota della grandezza finale (es. 1024x1024)
|
| 130 |
+
canvas = np.zeros((1024, 1024, 3), dtype=np.uint8)
|
| 131 |
+
|
| 132 |
+
# Ridimensiona il logo per farlo diventare una spilla/toppa (es. 200x200 pixel)
|
| 133 |
+
logo_resized = cv2.resize(edges_3c, (200, 200))
|
| 134 |
+
|
| 135 |
+
# Posiziona il logo in basso a destra (es. sul petto o nell'angolo)
|
| 136 |
+
y_offset = 700
|
| 137 |
+
x_offset = 200
|
| 138 |
+
canvas[y_offset:y_offset+200, x_offset:x_offset+200] = logo_resized
|
| 139 |
+
|
| 140 |
+
return Image.fromarray(canvas)
|
| 141 |
+
|
| 142 |
+
@spaces.GPU(duration=120)
|
| 143 |
+
def generate_avatar(user_image, logo_image, selected_style):
|
| 144 |
+
if user_image is None:
|
| 145 |
+
raise gr.Error("Per favore, carica un'immagine del volto.")
|
| 146 |
+
if logo_image is None:
|
| 147 |
+
raise gr.Error("Per favore, carica l'immagine del logo.")
|
| 148 |
+
|
| 149 |
+
# 1. Analisi del volto
|
| 150 |
+
cv_image = cv2.cvtColor(np.array(user_image), cv2.COLOR_RGB2BGR)
|
| 151 |
+
faces = app.get(cv_image)
|
| 152 |
+
if len(faces) == 0:
|
| 153 |
+
raise gr.Error("Nessun volto rilevato. Riprova con un'altra foto in cui il volto sia ben visibile.")
|
| 154 |
+
|
| 155 |
+
face = sorted(faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1]
|
| 156 |
+
face_emb = face.normed_embedding
|
| 157 |
+
|
| 158 |
+
# Ridimensioniamo l'immagine utente a 1024x1024 (standard SDXL) prima di estrarre i keypoints
|
| 159 |
+
user_image_resized = user_image.resize((1024, 1024))
|
| 160 |
+
faces_resized = app.get(cv2.cvtColor(np.array(user_image_resized), cv2.COLOR_RGB2BGR))
|
| 161 |
+
if len(faces_resized) > 0:
|
| 162 |
+
face_kps = sorted(faces_resized, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))[-1].kps
|
| 163 |
+
else:
|
| 164 |
+
face_kps = face.kps * (1024 / max(user_image.size)) # Approssimazione se fallisce il resize
|
| 165 |
+
|
| 166 |
+
kps_image = draw_kps(user_image_resized, face_kps)
|
| 167 |
+
|
| 168 |
+
# 2. Elaborazione del Logo
|
| 169 |
+
# Il colore originale viene ignorato, prendiamo solo la sagoma (Canny edges)
|
| 170 |
+
logo_canny_image = process_logo_canny(logo_image)
|
| 171 |
+
|
| 172 |
+
# 3. Gestione LoRA dinamico da Hugging Face
|
| 173 |
+
style_config = STYLES[selected_style]
|
| 174 |
+
repo_id = style_config["repo_id"]
|
| 175 |
+
|
| 176 |
+
if repo_id:
|
| 177 |
+
try:
|
| 178 |
+
print(f"Scaricamento/Caricamento LoRA {repo_id}...")
|
| 179 |
+
pipe.load_lora_weights(repo_id, weight_name=style_config["weight_name"])
|
| 180 |
+
except Exception as e:
|
| 181 |
+
print(f"Errore caricamento LoRA da HF: {e}")
|
| 182 |
+
|
| 183 |
+
# 4. Generazione immagine
|
| 184 |
+
prompt = style_config["prompt"]
|
| 185 |
+
negative_prompt = style_config["negative_prompt"]
|
| 186 |
+
|
| 187 |
+
print("Inizio generazione MultiControlNet...")
|
| 188 |
+
image = pipe(
|
| 189 |
+
prompt=prompt,
|
| 190 |
+
negative_prompt=negative_prompt,
|
| 191 |
+
image_embeds=face_emb,
|
| 192 |
+
# Passiamo le due immagini di controllo: [Volto, Forma del Logo]
|
| 193 |
+
image=[kps_image, logo_canny_image],
|
| 194 |
+
# Bilanciamo l'influenza: 0.8 per il volto, 0.4 per il logo (abbastanza per imprimere la forma ma fuso)
|
| 195 |
+
controlnet_conditioning_scale=[0.8, 0.4],
|
| 196 |
+
ip_adapter_scale=0.8,
|
| 197 |
+
num_inference_steps=30,
|
| 198 |
+
guidance_scale=5.0,
|
| 199 |
+
).images[0]
|
| 200 |
+
|
| 201 |
+
if repo_id:
|
| 202 |
+
try:
|
| 203 |
+
pipe.unload_lora_weights()
|
| 204 |
+
except:
|
| 205 |
+
pass
|
| 206 |
+
|
| 207 |
+
return image
|
| 208 |
+
|
| 209 |
+
# ==============================================================================
|
| 210 |
+
# 4. INTERFACCIA GRADIO (FRONTEND)
|
| 211 |
+
# ==============================================================================
|
| 212 |
+
|
| 213 |
+
custom_css = """
|
| 214 |
+
.container { max-width: 1000px; margin: auto; padding-top: 2rem; }
|
| 215 |
+
.header-title { text-align: center; color: #1e3a8a; font-family: 'Inter', sans-serif; font-weight: 800; font-size: 2.5rem; margin-bottom: 0.5rem; }
|
| 216 |
+
.header-subtitle { text-align: center; color: #4b5563; font-size: 1.1rem; margin-bottom: 2rem; }
|
| 217 |
+
.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; }
|
| 218 |
+
.generate-btn:hover { transform: scale(1.02); }
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
theme = gr.themes.Soft(primary_hue="blue", secondary_hue="emerald").set(
|
| 222 |
+
button_primary_background_fill="*primary_500",
|
| 223 |
+
button_primary_background_fill_hover="*primary_600",
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
with gr.Blocks(css=custom_css, theme=theme) as demo:
|
| 227 |
+
with gr.Column(elem_classes="container"):
|
| 228 |
+
gr.HTML("<h1 class='header-title'>🇮🇹 Italian Builders Avatar Generator 🇮🇹</h1>")
|
| 229 |
+
gr.HTML("<p class='header-subtitle'>Trasforma il tuo selfie. Il tuo brand, la tua identità.</p>")
|
| 230 |
+
|
| 231 |
+
with gr.Row():
|
| 232 |
+
with gr.Column(scale=1):
|
| 233 |
+
input_image = gr.Image(type="pil", label="1. Carica il tuo Selfie")
|
| 234 |
+
logo_image = gr.Image(type="pil", label="2. Carica il Logo (es. PNG a colori)")
|
| 235 |
+
|
| 236 |
+
with gr.Column(scale=1):
|
| 237 |
+
style_selector = gr.Radio(
|
| 238 |
+
choices=list(STYLES.keys()),
|
| 239 |
+
label="3. Scegli il tuo Stile",
|
| 240 |
+
value=list(STYLES.keys())[0]
|
| 241 |
+
)
|
| 242 |
+
gr.Markdown("<br>")
|
| 243 |
+
generate_button = gr.Button("Genera il tuo Avatar 🚀", elem_classes="generate-btn", variant="primary")
|
| 244 |
+
|
| 245 |
+
with gr.Column(scale=1):
|
| 246 |
+
output_image = gr.Image(label="Il tuo Avatar Builder", interactive=False)
|
| 247 |
+
|
| 248 |
+
generate_button.click(
|
| 249 |
+
fn=generate_avatar,
|
| 250 |
+
inputs=[input_image, logo_image, style_selector],
|
| 251 |
+
outputs=output_image
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
spaces
|
| 3 |
+
torch
|
| 4 |
+
diffusers>=0.26.0
|
| 5 |
+
transformers
|
| 6 |
+
accelerate
|
| 7 |
+
insightface
|
| 8 |
+
onnxruntime
|
| 9 |
+
opencv-python
|
| 10 |
+
huggingface-hub
|