|
import gradio as gr |
|
import os |
|
from PIL import Image, ImageEnhance |
|
import requests |
|
import io |
|
import gc |
|
import json |
|
from typing import Tuple, Optional, Dict, Any |
|
import logging |
|
from dotenv import load_dotenv |
|
|
|
|
|
logging.basicConfig( |
|
level=logging.DEBUG, |
|
format='%(asctime)s - %(levelname)s - %(message)s' |
|
) |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
ART_STYLES = { |
|
|
|
"Ultra Réaliste": { |
|
"prompt_prefix": """ultra realistic photograph, stunning photorealistic quality, unreal engine 5 quality, |
|
octane render, ray tracing, volumetric lighting, subsurface scattering, 8k UHD, cinema quality, |
|
masterpiece, perfect composition, award winning photography, professional lighting""", |
|
"negative_prompt": """artificial, digital art, illustration, painting, drawing, artistic, cartoon, |
|
anime, unreal, fake, low quality, blurry, soft, deformed, noisy, unclear, imperfect, amateur""", |
|
"quality_multiplier": 1.2 |
|
}, |
|
"Photoréaliste": { |
|
"prompt_prefix": """hyperrealistic photograph, extremely detailed, studio quality, professional photography, |
|
8k uhd, perfect lighting, high-end camera, professional grade lens, expert composition""", |
|
"negative_prompt": """artistic, painterly, abstract, cartoon, illustration, low quality, |
|
amateur, imperfect, blurry, noise""", |
|
"quality_multiplier": 1.1 |
|
}, |
|
|
|
|
|
"Expressionniste": { |
|
"prompt_prefix": """expressive painting style, intense emotional art, bold brushstrokes, vibrant colors, |
|
van gogh inspired, artistic masterpiece, deep emotional impact, dynamic composition""", |
|
"negative_prompt": """realistic, subtle, photographic, clean lines, digital art, flat, unemotional, |
|
generic, weak composition""", |
|
"quality_multiplier": 1.0 |
|
}, |
|
"Impressionniste": { |
|
"prompt_prefix": """impressionist painting style, soft light, visible brushstrokes, outdoor scene, |
|
monet inspired, artistic mastery, natural lighting, atmospheric effect""", |
|
"negative_prompt": """sharp details, high contrast, digital, modern, artificial, harsh, |
|
unrealistic colors""", |
|
"quality_multiplier": 1.0 |
|
}, |
|
|
|
|
|
"Art Moderne": { |
|
"prompt_prefix": """modern art style poster, professional design, contemporary aesthetic, |
|
trending on artstation, perfect composition, high-end production""", |
|
"negative_prompt": """traditional, cluttered, busy design, vintage, amateur, low quality, |
|
unprofessional""", |
|
"quality_multiplier": 1.1 |
|
}, |
|
"Minimaliste": { |
|
"prompt_prefix": """minimalist design poster, clean composition, elegant simplicity, |
|
perfect balance, sophisticated style, high-end design""", |
|
"negative_prompt": """complex, detailed, ornate, busy, cluttered, chaotic, unbalanced, |
|
amateur""", |
|
"quality_multiplier": 1.0 |
|
}, |
|
|
|
|
|
"Cyberpunk": { |
|
"prompt_prefix": """cyberpunk style, neon lights, futuristic design, high-tech aesthetic, |
|
detailed machinery, holographic elements, cinematic lighting""", |
|
"negative_prompt": """vintage, natural, rustic, traditional, simple, flat, dull, |
|
low-tech""", |
|
"quality_multiplier": 1.1 |
|
}, |
|
"Art Déco": { |
|
"prompt_prefix": """art deco style, geometric patterns, luxury design, 1920s aesthetic, |
|
golden age glamour, sophisticated composition""", |
|
"negative_prompt": """modern, minimalist, casual, contemporary, simple, rustic, |
|
unrefined""", |
|
"quality_multiplier": 1.0 |
|
} |
|
} |
|
|
|
|
|
COMPOSITION_PARAMS = { |
|
"Layouts": { |
|
"Centré": { |
|
"description": "centered composition, balanced layout, harmonious arrangement", |
|
"weight": 1.2 |
|
}, |
|
"Asymétrique": { |
|
"description": "dynamic asymmetrical composition, creative balance, artistic flow", |
|
"weight": 1.1 |
|
}, |
|
"Grille": { |
|
"description": "grid-based layout, structured composition, organized design, perfect alignment", |
|
"weight": 1.0 |
|
}, |
|
"Diagonal": { |
|
"description": "diagonal dynamic composition, energetic flow, dramatic arrangement", |
|
"weight": 1.1 |
|
}, |
|
"Minimaliste": { |
|
"description": "minimal composition, lots of whitespace, elegant spacing, perfect balance", |
|
"weight": 1.0 |
|
} |
|
}, |
|
"Ambiances": { |
|
"Dramatique": { |
|
"description": "dramatic lighting, high contrast, intense mood, cinematic atmosphere", |
|
"weight": 1.2 |
|
}, |
|
"Doux": { |
|
"description": "soft lighting, gentle atmosphere, subtle mood, delicate ambiance", |
|
"weight": 1.0 |
|
}, |
|
"Vibrant": { |
|
"description": "vibrant colors, energetic mood, dynamic atmosphere, bold presence", |
|
"weight": 1.1 |
|
}, |
|
"Mystérieux": { |
|
"description": "mysterious atmosphere, moody lighting, enigmatic feel, intriguing shadows", |
|
"weight": 1.1 |
|
}, |
|
"Serein": { |
|
"description": "peaceful atmosphere, calm mood, tranquil setting, harmonious lighting", |
|
"weight": 1.0 |
|
} |
|
} |
|
} |
|
|
|
class ImageGenerator: |
|
def __init__(self): |
|
self.API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0" |
|
token = os.getenv('HUGGINGFACE_TOKEN') |
|
if not token: |
|
logger.error("HUGGINGFACE_TOKEN non trouvé!") |
|
self.headers = {"Authorization": f"Bearer {token}"} |
|
logger.info("ImageGenerator initialisé") |
|
|
|
def _optimize_prompt(self, params: Dict[str, Any]) -> Tuple[str, str]: |
|
"""Optimisation avancée des prompts avec gestion contextuelle""" |
|
style_info = ART_STYLES.get(params["style"], ART_STYLES["Art Moderne"]) |
|
layout_info = COMPOSITION_PARAMS["Layouts"].get(params["layout"]) |
|
ambiance_info = COMPOSITION_PARAMS["Ambiances"].get(params["ambiance"]) |
|
|
|
|
|
base_prompt = f"{params['subject']}" |
|
if params.get('title'): |
|
base_prompt += f", with text '{params['title']}'" |
|
|
|
|
|
composition_elements = [ |
|
style_info["prompt_prefix"], |
|
layout_info["description"], |
|
ambiance_info["description"] |
|
] |
|
|
|
|
|
quality_multiplier = ( |
|
style_info.get("quality_multiplier", 1.0) * |
|
layout_info.get("weight", 1.0) * |
|
ambiance_info.get("weight", 1.0) |
|
) |
|
|
|
|
|
enhanced_prompt = f"{base_prompt}, {', '.join(composition_elements)}" |
|
|
|
|
|
negative_prompt = f"{style_info['negative_prompt']}, low quality, bad anatomy, worst quality, low resolution" |
|
|
|
return enhanced_prompt, negative_prompt, quality_multiplier |
|
|
|
def _enhance_image(self, image: Image.Image, params: Dict[str, Any]) -> Image.Image: |
|
"""Post-traitement avancé des images""" |
|
try: |
|
|
|
sharpness_factor = 1.2 if params["style"] in ["Ultra Réaliste", "Photoréaliste"] else 1.1 |
|
enhancer = ImageEnhance.Sharpness(image) |
|
image = enhancer.enhance(sharpness_factor) |
|
|
|
|
|
contrast_factor = 1.2 if params["ambiance"] == "Dramatique" else 1.1 |
|
enhancer = ImageEnhance.Contrast(image) |
|
image = enhancer.enhance(contrast_factor) |
|
|
|
return image |
|
except Exception as e: |
|
logger.warning(f"Erreur lors de l'amélioration de l'image: {str(e)}") |
|
return image |
|
|
|
def generate(self, params: Dict[str, Any]) -> Tuple[Optional[Image.Image], str]: |
|
"""Génération d'image avec optimisations avancées""" |
|
try: |
|
logger.info(f"Début de génération avec paramètres: {json.dumps(params, indent=2)}") |
|
|
|
if 'Bearer None' in self.headers['Authorization']: |
|
return None, "⚠️ Erreur: Token Hugging Face non configuré" |
|
|
|
|
|
enhanced_prompt, negative_prompt, quality_multiplier = self._optimize_prompt(params) |
|
|
|
|
|
generation_params = { |
|
"num_inference_steps": min(int(50 * quality_multiplier), 60), |
|
"guidance_scale": min(8.5 * (params["creativity"]/10), 12.0), |
|
"width": 1024 if params.get("quality", 35) > 40 else 768, |
|
"height": 1024 if params["orientation"] == "Portrait" else 768 |
|
} |
|
|
|
payload = { |
|
"inputs": enhanced_prompt, |
|
"parameters": { |
|
**generation_params, |
|
"negative_prompt": negative_prompt |
|
} |
|
} |
|
|
|
logger.debug(f"Payload final: {json.dumps(payload, indent=2)}") |
|
|
|
|
|
response = requests.post( |
|
self.API_URL, |
|
headers=self.headers, |
|
json=payload, |
|
timeout=45 |
|
) |
|
|
|
if response.status_code == 200: |
|
image = Image.open(io.BytesIO(response.content)) |
|
|
|
image = self._enhance_image(image, params) |
|
return image, "✨ Création réussie!" |
|
else: |
|
error_msg = f"⚠️ Erreur API {response.status_code}: {response.text}" |
|
logger.error(error_msg) |
|
return None, error_msg |
|
|
|
except Exception as e: |
|
error_msg = f"⚠️ Erreur: {str(e)}" |
|
logger.exception("Erreur pendant la génération:") |
|
return None, error_msg |
|
finally: |
|
gc.collect() |
|
|
|
def create_interface(): |
|
"""Création de l'interface utilisateur enrichie""" |
|
logger.info("Création de l'interface Gradio") |
|
|
|
|
|
css = """ |
|
.container { max-width: 1200px; margin: auto; } |
|
.welcome { |
|
text-align: center; |
|
margin: 20px 0; |
|
padding: 20px; |
|
background: linear-gradient(135deg, #1e293b, #334155); |
|
border-radius: 10px; |
|
color: white; |
|
} |
|
.controls-group { |
|
background: #2d3748; |
|
padding: 15px; |
|
border-radius: 5px; |
|
margin: 10px 0; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
.advanced-controls { |
|
background: #374151; |
|
padding: 12px; |
|
border-radius: 5px; |
|
margin: 8px 0; |
|
} |
|
.info-tooltip { |
|
color: #94a3b8; |
|
font-size: 0.9em; |
|
margin-top: 4px; |
|
} |
|
""" |
|
|
|
generator = ImageGenerator() |
|
|
|
with gr.Blocks(css=css) as app: |
|
|
|
gr.HTML(""" |
|
<div class="welcome"> |
|
<h1>🎨 Equity Artisan 3.0</h1> |
|
<p>Assistant de création d'affiches professionnelles</p> |
|
<p style="font-size: 0.9em; opacity: 0.8;">Powered by Stable Diffusion XL</p> |
|
</div> |
|
""") |
|
|
|
with gr.Column(elem_classes="container"): |
|
|
|
gr.Markdown(""" |
|
### 🎯 Guide d'utilisation |
|
1. Choisissez le format et l'orientation de votre affiche |
|
2. Sélectionnez un style artistique et une composition |
|
3. Décrivez votre vision dans "Description" |
|
4. Ajustez les paramètres fins selon vos besoins |
|
5. Cliquez sur "Générer" ! |
|
|
|
*💡 Pro Tip: Pour de meilleurs résultats, soyez précis dans votre description et expérimentez avec différents styles.* |
|
""") |
|
|
|
|
|
with gr.Group(elem_classes="controls-group"): |
|
gr.Markdown("### 📐 Format et Orientation") |
|
with gr.Row(): |
|
format_size = gr.Dropdown( |
|
choices=["A4", "A3", "A2", "A1", "A0"], |
|
value="A4", |
|
label="Format", |
|
info="Choisissez la taille de votre affiche" |
|
) |
|
orientation = gr.Radio( |
|
choices=["Portrait", "Paysage"], |
|
value="Portrait", |
|
label="Orientation" |
|
) |
|
|
|
|
|
with gr.Group(elem_classes="controls-group"): |
|
gr.Markdown("### 🎨 Style et Composition") |
|
with gr.Row(): |
|
style = gr.Dropdown( |
|
choices=list(ART_STYLES.keys()), |
|
value="Art Moderne", |
|
label="Style artistique" |
|
) |
|
layout = gr.Dropdown( |
|
choices=list(COMPOSITION_PARAMS["Layouts"].keys()), |
|
value="Centré", |
|
label="Composition" |
|
) |
|
|
|
with gr.Row(): |
|
ambiance = gr.Dropdown( |
|
choices=list(COMPOSITION_PARAMS["Ambiances"].keys()), |
|
value="Dramatique", |
|
label="Ambiance" |
|
) |
|
palette = gr.Dropdown( |
|
choices=["Monochrome", "Contrasté", "Pastel", "Terre", "Néon"], |
|
value="Contrasté", |
|
label="Palette" |
|
) |
|
|
|
|
|
with gr.Group(elem_classes="controls-group"): |
|
gr.Markdown("### 📝 Contenu") |
|
subject = gr.Textbox( |
|
label="Description", |
|
placeholder="Ex: Une affiche moderne pour un festival de musique, avec des instruments colorés flottant dans l'espace", |
|
lines=3 |
|
) |
|
title = gr.Textbox( |
|
label="Titre (optionnel)", |
|
placeholder="Le titre qui apparaîtra sur l'affiche..." |
|
) |
|
|
|
|
|
with gr.Group(elem_classes="advanced-controls"): |
|
gr.Markdown("### 🎯 Paramètres Av# Contrôles avancés |
|
with gr.Group(elem_classes="advanced-controls"): |
|
gr.Markdown(" |
|
with gr.Row(): |
|
detail_level = gr.Slider( |
|
minimum=1, |
|
maximum=10, |
|
value=7, |
|
step=1, |
|
label="Niveau de Détail", |
|
info="Plus la valeur est élevée, plus l'image sera détaillée" |
|
) |
|
contrast = gr.Slider( |
|
minimum=1, |
|
maximum=10, |
|
value=5, |
|
step=1, |
|
label="Contraste", |
|
info="Influence l'intensité des couleurs et la différence entre les zones claires et sombres" |
|
) |
|
saturation = gr.Slider( |
|
minimum=1, |
|
maximum=10, |
|
value=5, |
|
step=1, |
|
label="Saturation", |
|
info="Contrôle la vivacité des couleurs" |
|
) |
|
|
|
|
|
with gr.Group(elem_classes="controls-group"): |
|
gr.Markdown("### ⚙️ Paramètres de Génération") |
|
with gr.Row(): |
|
quality = gr.Slider( |
|
minimum=30, |
|
maximum=50, |
|
value=35, |
|
label="Qualité", |
|
info="Influence la qualité finale de l'image. Une valeur plus élevée prend plus de temps" |
|
) |
|
creativity = gr.Slider( |
|
minimum=5, |
|
maximum=15, |
|
value=7.5, |
|
label="Créativité", |
|
info="Plus la valeur est élevée, plus l'IA prendra de libertés créatives" |
|
) |
|
|
|
|
|
with gr.Row(): |
|
generate_btn = gr.Button("✨ Générer", variant="primary") |
|
clear_btn = gr.Button("🗑️ Effacer", variant="secondary") |
|
|
|
|
|
with gr.Group(elem_classes="controls-group"): |
|
gr.Markdown("### 🖼️ Résultat") |
|
image_output = gr.Image(label="Aperçu", height=512) |
|
status = gr.Textbox(label="Statut", interactive=False) |
|
|
|
|
|
with gr.Group(elem_classes="controls-group"): |
|
gr.Markdown("### 📋 Historique des Générations") |
|
history = gr.Gallery(label="Générations précédentes", show_label=True, columns=4, height=200) |
|
|
|
|
|
def generate(*args): |
|
logger.info("Démarrage d'une nouvelle génération") |
|
params = { |
|
"format_size": args[0], |
|
"orientation": args[1], |
|
"style": args[2], |
|
"layout": args[3], |
|
"ambiance": args[4], |
|
"palette": args[5], |
|
"subject": args[6], |
|
"title": args[7], |
|
"detail_level": args[8], |
|
"contrast": args[9], |
|
"saturation": args[10], |
|
"quality": args[11], |
|
"creativity": args[12] |
|
} |
|
result = generator.generate(params) |
|
logger.info(f"Génération terminée avec statut: {result[1]}") |
|
return result |
|
|
|
|
|
generate_btn.click( |
|
generate, |
|
inputs=[ |
|
format_size, |
|
orientation, |
|
style, |
|
layout, |
|
ambiance, |
|
palette, |
|
subject, |
|
title, |
|
detail_level, |
|
contrast, |
|
saturation, |
|
quality, |
|
creativity |
|
], |
|
outputs=[image_output, status] |
|
) |
|
|
|
clear_btn.click( |
|
lambda: (None, "🗑️ Image effacée"), |
|
outputs=[image_output, status] |
|
) |
|
|
|
logger.info("Interface créée avec succès") |
|
return app |
|
|
|
if __name__ == "__main__": |
|
app = create_interface() |
|
logger.info("Démarrage de l'application") |
|
app.launch() |