Update app.py
Browse files
app.py
CHANGED
@@ -1,436 +1,121 @@
|
|
1 |
import gradio as gr
|
2 |
import os
|
3 |
-
from PIL import Image
|
4 |
import requests
|
5 |
import io
|
6 |
-
import
|
|
|
|
|
|
|
7 |
|
8 |
-
#
|
9 |
-
|
10 |
-
|
11 |
-
"Art Moderne": {
|
12 |
-
"prompt_prefix": "modern art style poster, professional design",
|
13 |
-
"text_style": "modern clean typography, geometric style",
|
14 |
-
"guidance": 9.0, # Réduit pour plus de stabilité
|
15 |
-
"steps": 55,
|
16 |
-
"negative_prompt": "traditional, photorealistic, cluttered, busy design"
|
17 |
-
},
|
18 |
-
"Neo Vintage": {
|
19 |
-
"prompt_prefix": "vintage style advertising poster, retro design",
|
20 |
-
"text_style": "retro typography, vintage lettering",
|
21 |
-
"guidance": 8.5,
|
22 |
-
"steps": 50,
|
23 |
-
"negative_prompt": "modern, digital, contemporary style"
|
24 |
-
},
|
25 |
-
"Pop Art": {
|
26 |
-
"prompt_prefix": "pop art style poster, bold design",
|
27 |
-
"text_style": "bold typography, comic book style text",
|
28 |
-
"guidance": 8.0,
|
29 |
-
"steps": 45,
|
30 |
-
"negative_prompt": "subtle, realistic, traditional art"
|
31 |
-
},
|
32 |
-
"Minimaliste": {
|
33 |
-
"prompt_prefix": "minimalist design poster, clean composition",
|
34 |
-
"text_style": "clean minimal typography, simple text layout",
|
35 |
-
"guidance": 7.5,
|
36 |
-
"steps": 40,
|
37 |
-
"negative_prompt": "complex, detailed, ornate, busy"
|
38 |
-
}
|
39 |
-
}
|
40 |
-
ART_STYLES = {
|
41 |
-
# Styles Photoréalistes
|
42 |
-
"Photo HDR": {
|
43 |
-
"prompt_prefix": "ultra realistic photograph, professional HDR photography, extremely detailed, 8k uhd",
|
44 |
-
"text_style": "photographic text overlay",
|
45 |
-
"guidance": 9.0,
|
46 |
-
"steps": 60,
|
47 |
-
"negative_prompt": "illustration, painting, drawing, cartoon, blurry, low quality, artistic"
|
48 |
-
},
|
49 |
-
"Portrait Studio": {
|
50 |
-
"prompt_prefix": "professional studio photography, high end photoshoot, perfect lighting, sharp focus",
|
51 |
-
"text_style": "elegant text overlay, magazine style typography",
|
52 |
-
"guidance": 8.5,
|
53 |
-
"steps": 55,
|
54 |
-
"negative_prompt": "illustration, drawing, cartoon, painting, low quality"
|
55 |
-
},
|
56 |
-
"Nature Pro": {
|
57 |
-
"prompt_prefix": "professional nature photography, national geographic style, perfect natural lighting",
|
58 |
-
"text_style": "outdoor photography text style",
|
59 |
-
"guidance": 8.0,
|
60 |
-
"steps": 50,
|
61 |
-
"negative_prompt": "illustration, artificial, cartoon, painting"
|
62 |
-
},
|
63 |
-
"Urban Photo": {
|
64 |
-
"prompt_prefix": "professional urban photography, architectural photo, perfect city shot",
|
65 |
-
"text_style": "modern photographic typography",
|
66 |
-
"guidance": 8.5,
|
67 |
-
"steps": 55,
|
68 |
-
"negative_prompt": "illustration, drawing, cartoon, unrealistic"
|
69 |
-
},
|
70 |
-
|
71 |
-
# Styles Artistiques existants
|
72 |
-
"Art Moderne": {
|
73 |
-
"prompt_prefix": "modern art style poster, professional design",
|
74 |
-
"text_style": "modern clean typography, geometric style",
|
75 |
-
"guidance": 9.0,
|
76 |
-
"steps": 55,
|
77 |
-
"negative_prompt": "photorealistic, traditional, cluttered"
|
78 |
-
},
|
79 |
-
"Neo Vintage": {
|
80 |
-
"prompt_prefix": "vintage style advertising poster, retro design",
|
81 |
-
"text_style": "retro typography, vintage lettering",
|
82 |
-
"guidance": 8.5,
|
83 |
-
"steps": 50,
|
84 |
-
"negative_prompt": "modern, photographic, contemporary"
|
85 |
-
},
|
86 |
-
# ... autres styles existants ...
|
87 |
-
}
|
88 |
|
89 |
-
#
|
90 |
-
|
91 |
-
"HDR": {
|
92 |
-
"exposure_strength": 1.2,
|
93 |
-
"contrast": 1.1,
|
94 |
-
"saturation": 1.05
|
95 |
-
},
|
96 |
-
"Portrait": {
|
97 |
-
"skin_softening": 0.8,
|
98 |
-
"bokeh_strength": 0.7,
|
99 |
-
"lighting": "studio"
|
100 |
-
},
|
101 |
-
"Nature": {
|
102 |
-
"sharpness": 1.2,
|
103 |
-
"vibrance": 1.1,
|
104 |
-
"detail_enhancement": 1.15
|
105 |
-
}
|
106 |
-
}
|
107 |
|
108 |
-
|
109 |
-
"""
|
110 |
-
base_prompt = f"highly detailed photograph of {subject}, {ART_STYLES[style]['prompt_prefix']}"
|
111 |
-
|
112 |
-
photo_enhancements = {
|
113 |
-
"HDR": ", ultra high resolution photograph, perfect exposure, dramatic lighting, professional color grading",
|
114 |
-
"Portrait": ", professional portrait photography, perfect skin texture, studio lighting setup, shallow depth of field",
|
115 |
-
"Nature": ", sharp nature photography, perfect natural lighting, high detail macro shot, professional outdoor photography",
|
116 |
-
"Urban": ", professional architectural photography, perfect perspective, golden hour lighting, urban landscape"
|
117 |
-
}
|
118 |
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
if is_photo_style:
|
134 |
-
prompt = enhance_photo_prompt(subject, style)
|
135 |
-
guidance_scale = 8.0 # Optimisé pour le photoréalisme
|
136 |
-
steps = 55
|
137 |
-
else:
|
138 |
-
prompt = f"{ART_STYLES[style]['prompt_prefix']}, {subject}"
|
139 |
-
guidance_scale = ART_STYLES[style]['guidance']
|
140 |
-
steps = ART_STYLES[style]['steps']
|
141 |
-
|
142 |
-
if text_effect != "Standard":
|
143 |
-
prompt += f", {TEXT_EFFECTS[text_effect]['prompt_suffix']}"
|
144 |
|
145 |
-
|
146 |
-
prompt
|
147 |
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
"
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
|
|
|
|
156 |
}
|
157 |
-
}
|
158 |
-
|
159 |
-
print(f"Prompt: {prompt}") # Debug
|
160 |
-
print(f"Paramètres: {payload}") # Debug
|
161 |
-
|
162 |
-
response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
|
163 |
-
|
164 |
-
if response.status_code == 200:
|
165 |
-
image = Image.open(io.BytesIO(response.content))
|
166 |
|
167 |
-
|
168 |
-
if is_photo_style:
|
169 |
-
image = enhance_photo(image, style)
|
170 |
-
|
171 |
-
return image, "✨ Photo créée avec succès!"
|
172 |
-
else:
|
173 |
-
return None, f"⚠️ Erreur {response.status_code}: Essayez de modifier les paramètres"
|
174 |
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
enhancer = ImageEnhance.Contrast(image)
|
183 |
-
image = enhancer.enhance(1.1)
|
184 |
-
|
185 |
-
enhancer = ImageEnhance.Color(image)
|
186 |
-
image = enhancer.enhance(1.05)
|
187 |
-
|
188 |
-
enhancer = ImageEnhance.Sharpness(image)
|
189 |
-
image = enhancer.enhance(1.15)
|
190 |
-
|
191 |
-
return image
|
192 |
-
except:
|
193 |
-
return image
|
194 |
-
# Effets de texte stabilisés
|
195 |
-
TEXT_EFFECTS = {
|
196 |
-
"Standard": {
|
197 |
-
"prompt_suffix": "with clear readable text",
|
198 |
-
"text_params": {"weight": 1.0}
|
199 |
-
},
|
200 |
-
"Graffiti": {
|
201 |
-
"prompt_suffix": "with urban graffiti style text",
|
202 |
-
"text_params": {"weight": 0.8}
|
203 |
-
},
|
204 |
-
"Néon": {
|
205 |
-
"prompt_suffix": "with glowing neon text",
|
206 |
-
"text_params": {"weight": 0.9}
|
207 |
-
},
|
208 |
-
"3D": {
|
209 |
-
"prompt_suffix": "with 3D style text",
|
210 |
-
"text_params": {"weight": 0.85}
|
211 |
-
}
|
212 |
-
}
|
213 |
-
|
214 |
-
# Collections optimisées
|
215 |
-
THEME_COLLECTIONS = {
|
216 |
-
"Tech": {
|
217 |
-
"prompts": ["technology inspired", "digital aesthetic"],
|
218 |
-
"styles": ["modern tech elements", "futuristic design"],
|
219 |
-
"negative": "organic, rustic, natural"
|
220 |
-
},
|
221 |
-
"Nature": {
|
222 |
-
"prompts": ["natural elements", "organic design"],
|
223 |
-
"styles": ["flowing shapes", "natural textures"],
|
224 |
-
"negative": "artificial, geometric"
|
225 |
-
},
|
226 |
-
"Urbain": {
|
227 |
-
"prompts": ["urban style", "city aesthetic"],
|
228 |
-
"styles": ["street art influence", "urban textures"],
|
229 |
-
"negative": "rural, natural"
|
230 |
-
}
|
231 |
-
}
|
232 |
-
|
233 |
-
def generate_image(format_size, orientation, subject, style, text_effect, collection, title, subtitle, quality, creativity):
|
234 |
-
API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
|
235 |
-
headers = {"Authorization": f"Bearer {os.getenv('HUGGINGFACE_TOKEN')}"}
|
236 |
-
|
237 |
-
try:
|
238 |
-
# Dimensions optimisées
|
239 |
-
width, height = (768, 1024) if format_size == "A4" else (1024, 1024)
|
240 |
-
if orientation == "Paysage":
|
241 |
-
width, height = height, width
|
242 |
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
prompt += f", {', '.join(coll['prompts'][:1])}" # Limite à un prompt pour stabilité
|
256 |
-
|
257 |
-
if title:
|
258 |
-
prompt += f", with text '{title}'"
|
259 |
-
|
260 |
-
# Configuration optimisée
|
261 |
-
payload = {
|
262 |
-
"inputs": prompt,
|
263 |
-
"parameters": {
|
264 |
-
"negative_prompt": style_config['negative_prompt'],
|
265 |
-
"num_inference_steps": min(style_config['steps'], 50), # Limite pour stabilité
|
266 |
-
"guidance_scale": min(style_config['guidance'] * (creativity/10), 10.0), # Limite max
|
267 |
-
"width": width,
|
268 |
-
"height": height
|
269 |
-
}
|
270 |
-
}
|
271 |
-
|
272 |
-
print(f"Prompt: {prompt}") # Debug
|
273 |
-
print(f"Paramètres: {payload}") # Debug
|
274 |
-
|
275 |
-
response = requests.post(API_URL, headers=headers, json=payload, timeout=30)
|
276 |
-
|
277 |
-
if response.status_code == 200:
|
278 |
-
image = Image.open(io.BytesIO(response.content))
|
279 |
-
return image, "✨ Création réussie!"
|
280 |
-
else:
|
281 |
-
print(f"Erreur API: {response.text}") # Debug
|
282 |
-
return None, f"⚠️ Erreur {response.status_code}: Essayez de modifier les paramètres"
|
283 |
-
|
284 |
-
except Exception as e:
|
285 |
-
print(f"Exception: {str(e)}") # Debug
|
286 |
-
return None, f"⚠️ Erreur: {str(e)}"
|
287 |
-
|
288 |
-
# Effets de texte
|
289 |
-
TEXT_EFFECTS = {
|
290 |
-
"Standard": {
|
291 |
-
"prompt_suffix": "with clear readable text",
|
292 |
-
"text_params": {"weight": 1.0}
|
293 |
-
},
|
294 |
-
"Néon": {
|
295 |
-
"prompt_suffix": "with glowing neon text effect",
|
296 |
-
"text_params": {"weight": 1.2}
|
297 |
-
},
|
298 |
-
"3D": {
|
299 |
-
"prompt_suffix": "with 3D text effect, depth and shadows",
|
300 |
-
"text_params": {"weight": 1.3}
|
301 |
-
},
|
302 |
-
"Métallique": {
|
303 |
-
"prompt_suffix": "with metallic text effect, reflective surface",
|
304 |
-
"text_params": {"weight": 1.1}
|
305 |
-
},
|
306 |
-
"Graffiti": {
|
307 |
-
"prompt_suffix": "with graffiti style text, urban art typography",
|
308 |
-
"text_params": {"weight": 1.2}
|
309 |
-
}
|
310 |
-
}
|
311 |
-
|
312 |
-
# Collections thématiques
|
313 |
-
THEME_COLLECTIONS = {
|
314 |
-
"Nature": {
|
315 |
-
"prompts": ["natural elements", "organic composition"],
|
316 |
-
"styles": ["flowing lines", "natural textures"],
|
317 |
-
"negative": "artificial, synthetic"
|
318 |
-
},
|
319 |
-
"Urbain": {
|
320 |
-
"prompts": ["urban landscape", "city elements"],
|
321 |
-
"styles": ["street art", "architectural elements"],
|
322 |
-
"negative": "rural, natural"
|
323 |
-
},
|
324 |
-
"Tech": {
|
325 |
-
"prompts": ["technological elements", "digital aesthetic"],
|
326 |
-
"styles": ["circuit patterns", "tech elements"],
|
327 |
-
"negative": "organic, traditional"
|
328 |
-
}
|
329 |
-
}
|
330 |
-
|
331 |
-
# CSS personnalisé
|
332 |
-
CUSTOM_CSS = """
|
333 |
-
.container { max-width: 1200px; margin: auto; }
|
334 |
-
.welcome { text-align: center; margin: 20px 0; padding: 20px; background: #1e293b; border-radius: 10px; }
|
335 |
-
.quality-controls { margin: 10px 0; padding: 10px; background: #2d3748; border-radius: 5px; }
|
336 |
-
.style-group { background: #2d3748; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
337 |
-
.text-effects { background: #374151; padding: 12px; border-radius: 5px; margin: 8px 0; }
|
338 |
-
.preview-panel { position: relative; }
|
339 |
-
.parameters { display: flex; gap: 10px; }
|
340 |
-
"""
|
341 |
|
342 |
-
def
|
343 |
-
"""
|
344 |
-
style_config = ART_STYLES[style]
|
345 |
-
text_config = TEXT_EFFECTS[text_effect]
|
346 |
-
|
347 |
-
base_prompt = f"{style_config['prompt_prefix']}, {subject}"
|
348 |
-
|
349 |
-
if text_effect != "Standard":
|
350 |
-
base_prompt += f", {text_config['prompt_suffix']}"
|
351 |
-
|
352 |
-
if collection and collection in THEME_COLLECTIONS:
|
353 |
-
collection_data = THEME_COLLECTIONS[collection]
|
354 |
-
collection_elements = collection_data["prompts"] + collection_data["styles"]
|
355 |
-
base_prompt += f", {', '.join(collection_elements)}"
|
356 |
-
|
357 |
-
if additional_details:
|
358 |
-
base_prompt += f", {additional_details}"
|
359 |
|
360 |
-
|
361 |
-
|
362 |
-
|
|
|
|
|
|
|
363 |
|
364 |
-
|
365 |
-
|
366 |
-
def generate_image(format_size, orientation, subject, style, text_effect, collection,
|
367 |
-
title, subtitle, quality, creativity, additional_details=""):
|
368 |
-
"""Fonction de génération d'image"""
|
369 |
-
API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
|
370 |
-
headers = {"Authorization": f"Bearer {os.getenv('HUGGINGFACE_TOKEN')}"}
|
371 |
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
if orientation == "Portrait":
|
376 |
-
if format_size in ["A4", "A3"]:
|
377 |
-
base_width = 768
|
378 |
-
base_height = 1024
|
379 |
-
else:
|
380 |
-
if format_size in ["A4", "A3"]:
|
381 |
-
base_width = 1024
|
382 |
-
base_height = 768
|
383 |
-
|
384 |
-
enhanced_prompt, negative_prompt = enhance_prompt(
|
385 |
-
subject, style, text_effect, collection, additional_details
|
386 |
-
)
|
387 |
-
|
388 |
-
if title:
|
389 |
-
enhanced_prompt += f", with text: '{title}'"
|
390 |
-
if subtitle:
|
391 |
-
enhanced_prompt += f", subtitle: '{subtitle}'"
|
392 |
-
|
393 |
-
print(f"Prompt: {enhanced_prompt}") # Debug
|
394 |
-
|
395 |
-
payload = {
|
396 |
-
"inputs": enhanced_prompt,
|
397 |
-
"parameters": {
|
398 |
-
"negative_prompt": negative_prompt,
|
399 |
-
"num_inference_steps": int(ART_STYLES[style]['steps'] * (quality/100)),
|
400 |
-
"guidance_scale": ART_STYLES[style]['guidance'] * (creativity/10),
|
401 |
-
"width": base_width,
|
402 |
-
"height": base_height
|
403 |
-
}
|
404 |
-
}
|
405 |
-
|
406 |
-
response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
|
407 |
-
|
408 |
-
if response.status_code == 200:
|
409 |
-
image = Image.open(io.BytesIO(response.content))
|
410 |
-
return image, f"✨ Création {style} avec effet {text_effect} réussie!"
|
411 |
-
else:
|
412 |
-
print(f"Erreur API: {response.text}")
|
413 |
-
return None, f"⚠️ Erreur {response.status_code}: Ajustez les paramètres"
|
414 |
-
|
415 |
-
except Exception as e:
|
416 |
-
print(f"Exception: {str(e)}")
|
417 |
-
return None, f"⚠️ Erreur: {str(e)}"
|
418 |
-
|
419 |
-
def create_interface():
|
420 |
-
with gr.Blocks(css=CUSTOM_CSS) as app:
|
421 |
gr.HTML("""
|
422 |
<div class="welcome">
|
423 |
-
<h1
|
424 |
-
<p>
|
425 |
</div>
|
426 |
""")
|
427 |
|
|
|
428 |
with gr.Column(elem_classes="container"):
|
429 |
-
# Format
|
430 |
-
with gr.Group(elem_classes="
|
431 |
-
gr.Markdown("### 📏 Format et Orientation")
|
432 |
with gr.Row():
|
433 |
-
|
434 |
choices=["A4", "A3", "A2", "A1", "A0"],
|
435 |
value="A4",
|
436 |
label="Format"
|
@@ -440,100 +125,91 @@ def create_interface():
|
|
440 |
value="Portrait",
|
441 |
label="Orientation"
|
442 |
)
|
443 |
-
|
444 |
-
# Style
|
445 |
-
with gr.Group(elem_classes="
|
446 |
-
gr.Markdown("### 🎨 Style et Création")
|
447 |
-
with gr.Row():
|
448 |
-
with gr.Column(scale=1):
|
449 |
-
style = gr.Dropdown(
|
450 |
-
choices=list(ART_STYLES.keys()),
|
451 |
-
value="Neo Vintage",
|
452 |
-
label="Style artistique"
|
453 |
-
)
|
454 |
-
text_effect = gr.Dropdown(
|
455 |
-
choices=list(TEXT_EFFECTS.keys()),
|
456 |
-
value="Standard",
|
457 |
-
label="Effet de texte"
|
458 |
-
)
|
459 |
-
collection = gr.Dropdown(
|
460 |
-
choices=list(THEME_COLLECTIONS.keys()),
|
461 |
-
label="Collection (optionnel)"
|
462 |
-
)
|
463 |
-
|
464 |
-
with gr.Column(scale=2):
|
465 |
-
subject = gr.Textbox(
|
466 |
-
label="Sujet principal",
|
467 |
-
placeholder="Ex: loup, paysage urbain..."
|
468 |
-
)
|
469 |
-
additional_details = gr.Textbox(
|
470 |
-
lines=2,
|
471 |
-
label="Détails additionnels",
|
472 |
-
placeholder="Ajoutez des détails..."
|
473 |
-
)
|
474 |
-
|
475 |
-
# Texte
|
476 |
-
with gr.Group(elem_classes="text-effects"):
|
477 |
-
gr.Markdown("### ✍️ Texte")
|
478 |
with gr.Row():
|
479 |
-
|
480 |
-
|
481 |
-
|
|
|
482 |
)
|
483 |
-
|
484 |
-
|
485 |
-
|
|
|
486 |
)
|
487 |
-
|
488 |
-
#
|
489 |
-
with gr.Group(elem_classes="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
490 |
with gr.Row():
|
491 |
quality = gr.Slider(
|
492 |
minimum=30,
|
493 |
maximum=50,
|
494 |
value=35,
|
495 |
-
step=5,
|
496 |
label="Qualité"
|
497 |
)
|
498 |
creativity = gr.Slider(
|
499 |
minimum=5,
|
500 |
maximum=15,
|
501 |
value=7.5,
|
502 |
-
step=0.5,
|
503 |
label="Créativité"
|
504 |
)
|
505 |
-
|
|
|
506 |
with gr.Row():
|
507 |
generate_btn = gr.Button("✨ Générer", variant="primary")
|
508 |
clear_btn = gr.Button("🗑️ Effacer", variant="secondary")
|
509 |
-
|
510 |
-
|
|
|
511 |
status = gr.Textbox(label="Statut", interactive=False)
|
512 |
-
|
513 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
514 |
generate_btn.click(
|
515 |
-
|
516 |
inputs=[
|
517 |
-
|
518 |
orientation,
|
519 |
subject,
|
520 |
style,
|
521 |
text_effect,
|
522 |
-
collection,
|
523 |
title,
|
524 |
-
subtitle,
|
525 |
quality,
|
526 |
-
creativity
|
527 |
-
additional_details
|
528 |
],
|
529 |
outputs=[image_output, status]
|
530 |
)
|
531 |
|
532 |
clear_btn.click(
|
533 |
-
lambda: (None, "Image effacée"),
|
534 |
outputs=[image_output, status]
|
535 |
)
|
536 |
-
|
537 |
return app
|
538 |
|
539 |
if __name__ == "__main__":
|
|
|
1 |
import gradio as gr
|
2 |
import os
|
3 |
+
from PIL import Image
|
4 |
import requests
|
5 |
import io
|
6 |
+
import gc
|
7 |
+
from typing import Tuple, Optional, Dict, Any
|
8 |
+
import logging
|
9 |
+
from dotenv import load_dotenv
|
10 |
|
11 |
+
# Configuration du logging
|
12 |
+
logging.basicConfig(level=logging.INFO)
|
13 |
+
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
+
# Chargement des variables d'environnement
|
16 |
+
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
+
class ImageGenerator:
|
19 |
+
"""Gestionnaire de génération d'images optimisé pour les ressources limitées"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
+
def __init__(self):
|
22 |
+
self.API_URL = "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0"
|
23 |
+
self.headers = {"Authorization": f"Bearer {os.getenv('HUGGINGFACE_TOKEN')}"}
|
24 |
+
|
25 |
+
# Configurations optimisées pour les ressources limitées
|
26 |
+
self.MAX_IMAGE_SIZE = 768 # Limite la taille maximale pour la mémoire
|
27 |
+
self.MAX_STEPS = 40 # Limite les steps pour optimiser le temps
|
28 |
+
self.DEFAULT_TIMEOUT = 30
|
29 |
+
|
30 |
+
@staticmethod
|
31 |
+
def get_image_dimensions(format_size: str, orientation: str) -> Tuple[int, int]:
|
32 |
+
"""Calcule les dimensions optimisées selon le format"""
|
33 |
+
base_dimensions = {
|
34 |
+
"A4": (768, 1024),
|
35 |
+
"A3": (768, 1024),
|
36 |
+
"A2": (768, 1024),
|
37 |
+
"A1": (768, 1024),
|
38 |
+
"A0": (768, 1024)
|
39 |
+
}
|
40 |
+
width, height = base_dimensions.get(format_size, (768, 768))
|
41 |
+
return (height, width) if orientation == "Paysage" else (width, height)
|
42 |
+
|
43 |
+
def generate(self, params: Dict[str, Any]) -> Tuple[Optional[Image.Image], str]:
|
44 |
+
"""Génère une image avec gestion optimisée des ressources"""
|
45 |
+
try:
|
46 |
+
# Nettoyage mémoire préventif
|
47 |
+
gc.collect()
|
48 |
|
49 |
+
# Préparation des dimensions
|
50 |
+
width, height = self.get_image_dimensions(params["format_size"], params["orientation"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
+
# Construction du prompt
|
53 |
+
prompt = self._build_prompt(params)
|
54 |
|
55 |
+
# Configuration de la requête
|
56 |
+
payload = {
|
57 |
+
"inputs": prompt,
|
58 |
+
"parameters": {
|
59 |
+
"negative_prompt": self._get_negative_prompt(params["style"]),
|
60 |
+
"num_inference_steps": min(int(35 * (params["quality"]/100)), self.MAX_STEPS),
|
61 |
+
"guidance_scale": min(7.5 * (params["creativity"]/10), 10.0),
|
62 |
+
"width": width,
|
63 |
+
"height": height
|
64 |
+
}
|
65 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
|
67 |
+
logger.info(f"Génération avec prompt: {prompt}")
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
+
# Requête API avec gestion d'erreur
|
70 |
+
response = requests.post(
|
71 |
+
self.API_URL,
|
72 |
+
headers=self.headers,
|
73 |
+
json=payload,
|
74 |
+
timeout=self.DEFAULT_TIMEOUT
|
75 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
+
if response.status_code == 200:
|
78 |
+
image = Image.open(io.BytesIO(response.content))
|
79 |
+
return image, "✨ Création réussie!"
|
80 |
+
else:
|
81 |
+
logger.error(f"Erreur API: {response.status_code} - {response.text}")
|
82 |
+
return None, f"⚠️ Erreur {response.status_code}: Ajustez les paramètres"
|
83 |
+
|
84 |
+
except Exception as e:
|
85 |
+
logger.error(f"Erreur de génération: {str(e)}")
|
86 |
+
return None, f"⚠️ Erreur: {str(e)}"
|
87 |
+
finally:
|
88 |
+
gc.collect() # Nettoyage final
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
|
90 |
+
def create_interface():
|
91 |
+
"""Crée l'interface utilisateur Gradio"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
|
93 |
+
# CSS optimisé
|
94 |
+
css = """
|
95 |
+
.container { max-width: 1200px; margin: auto; }
|
96 |
+
.welcome { text-align: center; margin: 20px 0; padding: 20px; background: #1e293b; border-radius: 10px; }
|
97 |
+
.controls-group { background: #2d3748; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
98 |
+
"""
|
99 |
|
100 |
+
# Initialisation du générateur
|
101 |
+
generator = ImageGenerator()
|
|
|
|
|
|
|
|
|
|
|
102 |
|
103 |
+
# Interface Gradio optimisée
|
104 |
+
with gr.Blocks(css=css) as app:
|
105 |
+
# En-tête
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
gr.HTML("""
|
107 |
<div class="welcome">
|
108 |
+
<h1>🎨 Equity Artisan 3.0</h1>
|
109 |
+
<p>Assistant de création d'affiches professionnelles</p>
|
110 |
</div>
|
111 |
""")
|
112 |
|
113 |
+
# Conteneur principal
|
114 |
with gr.Column(elem_classes="container"):
|
115 |
+
# Groupe Format
|
116 |
+
with gr.Group(elem_classes="controls-group"):
|
|
|
117 |
with gr.Row():
|
118 |
+
format_size = gr.Dropdown(
|
119 |
choices=["A4", "A3", "A2", "A1", "A0"],
|
120 |
value="A4",
|
121 |
label="Format"
|
|
|
125 |
value="Portrait",
|
126 |
label="Orientation"
|
127 |
)
|
128 |
+
|
129 |
+
# Groupe Style
|
130 |
+
with gr.Group(elem_classes="controls-group"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
with gr.Row():
|
132 |
+
style = gr.Dropdown(
|
133 |
+
choices=["Art Moderne", "Neo Vintage", "Pop Art", "Minimaliste"],
|
134 |
+
value="Neo Vintage",
|
135 |
+
label="Style artistique"
|
136 |
)
|
137 |
+
text_effect = gr.Dropdown(
|
138 |
+
choices=["Standard", "Néon", "3D", "Métallique", "Graffiti"],
|
139 |
+
value="Standard",
|
140 |
+
label="Effet de texte"
|
141 |
)
|
142 |
+
|
143 |
+
# Groupe Contenu
|
144 |
+
with gr.Group(elem_classes="controls-group"):
|
145 |
+
subject = gr.Textbox(
|
146 |
+
label="Description",
|
147 |
+
placeholder="Décrivez votre vision..."
|
148 |
+
)
|
149 |
+
title = gr.Textbox(
|
150 |
+
label="Titre",
|
151 |
+
placeholder="Titre de l'affiche..."
|
152 |
+
)
|
153 |
+
|
154 |
+
# Groupe Qualité
|
155 |
+
with gr.Group(elem_classes="controls-group"):
|
156 |
with gr.Row():
|
157 |
quality = gr.Slider(
|
158 |
minimum=30,
|
159 |
maximum=50,
|
160 |
value=35,
|
|
|
161 |
label="Qualité"
|
162 |
)
|
163 |
creativity = gr.Slider(
|
164 |
minimum=5,
|
165 |
maximum=15,
|
166 |
value=7.5,
|
|
|
167 |
label="Créativité"
|
168 |
)
|
169 |
+
|
170 |
+
# Boutons
|
171 |
with gr.Row():
|
172 |
generate_btn = gr.Button("✨ Générer", variant="primary")
|
173 |
clear_btn = gr.Button("🗑️ Effacer", variant="secondary")
|
174 |
+
|
175 |
+
# Sortie
|
176 |
+
image_output = gr.Image(label="Aperçu")
|
177 |
status = gr.Textbox(label="Statut", interactive=False)
|
178 |
+
|
179 |
+
# Événements
|
180 |
+
def generate(*args):
|
181 |
+
params = {
|
182 |
+
"format_size": args[0],
|
183 |
+
"orientation": args[1],
|
184 |
+
"subject": args[2],
|
185 |
+
"style": args[3],
|
186 |
+
"text_effect": args[4],
|
187 |
+
"title": args[5],
|
188 |
+
"quality": args[6],
|
189 |
+
"creativity": args[7]
|
190 |
+
}
|
191 |
+
return generator.generate(params)
|
192 |
+
|
193 |
generate_btn.click(
|
194 |
+
generate,
|
195 |
inputs=[
|
196 |
+
format_size,
|
197 |
orientation,
|
198 |
subject,
|
199 |
style,
|
200 |
text_effect,
|
|
|
201 |
title,
|
|
|
202 |
quality,
|
203 |
+
creativity
|
|
|
204 |
],
|
205 |
outputs=[image_output, status]
|
206 |
)
|
207 |
|
208 |
clear_btn.click(
|
209 |
+
lambda: (None, "🗑️ Image effacée"),
|
210 |
outputs=[image_output, status]
|
211 |
)
|
212 |
+
|
213 |
return app
|
214 |
|
215 |
if __name__ == "__main__":
|