Astridkraft commited on
Commit
9a70318
·
verified ·
1 Parent(s): 8c7996f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +667 -0
app.py CHANGED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Eine neue Stable Diffusion (SD) Generation kommt mit neuem Tokenizer, mehrsprachiger Unterstützung, längerem Kontext
2
+ #und deutlich besserem Prompt-Verständnis - (Änderung Architektur).
3
+ #Eine deutsche Alternative zur Umsetzung von Text-Bild zu Bild ist Flux - mit einer völlig anderen Architektur als SD!
4
+ import gradio as gr
5
+ from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline
6
+ from diffusers import StableDiffusionInpaintPipeline
7
+ from controlnet_module import controlnet_processor
8
+ import torch
9
+ from PIL import Image, ImageDraw
10
+ import time
11
+ import os
12
+ import tempfile
13
+ import random
14
+
15
+
16
+
17
+ # === OPTIMIERTE EINSTELLUNGEN ===
18
+ device = "cuda" if torch.cuda.is_available() else "cpu"
19
+ torch_dtype = torch.float16 if device == "cuda" else torch.float32
20
+ IMG_SIZE = 512
21
+
22
+ print(f"Running on: {device}")
23
+
24
+ # === GESICHTSMASKEN-FUNKTIONEN ===
25
+ def create_face_mask(image, bbox_coords, face_preserve):
26
+ """Erzeugt eine Gesichtsmaske - WEIßE Bereiche werden VERÄNDERT, SCHWARZE BLEIBEN"""
27
+ mask = Image.new("L", image.size, 0) # Start mit komplett schwarzer Maske (alles geschützt)
28
+
29
+ if bbox_coords and all(coord is not None for coord in bbox_coords):
30
+ x1, y1, x2, y2 = bbox_coords
31
+ draw = ImageDraw.Draw(mask)
32
+
33
+ if face_preserve:
34
+ # GESICHTSERHALTUNG: Maske um das Gesicht herum zeichnen
35
+ draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255) # Alles weiß = verändern
36
+ draw.rectangle([x1, y1, x2, y2], fill=0) # Gesicht schwarz = geschützt (rechteckig)
37
+ print("Gesicht wird GESCHÜTZT - Umgebung wird verändert (rechteckige Maske)")
38
+ else:
39
+ # NUR GESICHT VERÄNDERN: Nur das Gesicht wird weiß (verändert)
40
+ draw.rectangle([x1, y1, x2, y2], fill=255) # Gesicht weiß = verändern (rechteckig)
41
+ print("Nur Gesicht wird verändert - Umgebung bleibt erhalten (rechteckige Maske)")
42
+
43
+ return mask
44
+
45
+ def auto_detect_face_area(image):
46
+ """Optimierten Vorschlag für Gesichtsbereich ohne externe Bibliotheken"""
47
+ width, height = image.size
48
+ # Größere Bounding Box für bessere Abdeckung (50% statt 40%)
49
+ face_size = min(width, height) * 0.4
50
+ # Verschiebe y1 nach oben, um Stirn und Kinn besser abzudecken
51
+ x1 = (width - face_size) / 2
52
+ y1 = (height - face_size) / 4 # Höher positioniert (25% statt 33%)
53
+ x2 = x1 + face_size
54
+ y2 = y1 + face_size * 1.2 # Leicht länglicher für ovale Gesichter
55
+ # Stelle sicher, dass Koordinaten innerhalb des Bildes liegen
56
+ x1, y1 = max(0, int(x1)), max(0, int(y1))
57
+ x2, y2 = min(width, int(x2)), min(height, int(y2))
58
+ print(f"Geschätzte Gesichtskoordinaten: [{x1}, {y1}, {x2}, {y2}]")
59
+ return [x1, y1, x2, y2]
60
+
61
+ # === PIPELINES ===
62
+ pipe_txt2img = None
63
+ pipe_img2img = None
64
+
65
+ def load_txt2img():
66
+ global pipe_txt2img
67
+ if pipe_txt2img is None:
68
+ print("Loading Text-to-Image model...")
69
+ pipe_txt2img = StableDiffusionPipeline.from_pretrained(
70
+ "runwayml/stable-diffusion-v1-5",
71
+ torch_dtype=torch_dtype,
72
+ use_safetensors=True,
73
+ safety_checker=None,
74
+ requires_safety_checker=False,
75
+ #clean_up_tokenization_spaces=False #bei der neuen Version ändert sich die Architektur, Clip wird ersetzt/erweitert/integriert. Tokenizer nicht mehr nur auf englisch, kein 77-Token Limit!
76
+ ).to(device)
77
+
78
+ from diffusers import DPMSolverMultistepScheduler
79
+ pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(pipe_txt2img.scheduler.config)
80
+ pipe_txt2img.enable_attention_slicing()
81
+ return pipe_txt2img
82
+
83
+ def load_img2img():
84
+ global pipe_img2img
85
+ if pipe_img2img is None:
86
+ print("Loading Inpainting model...")
87
+ try:
88
+ pipe_img2img = StableDiffusionInpaintPipeline.from_pretrained(
89
+ "stabilityai/stable-diffusion-2-inpainting", # Neues Modell
90
+ torch_dtype=torch_dtype,
91
+ use_safetensors=True, # Erzwinge .safetensors
92
+ allow_pickle=False, # Verhindere unsichere Serialisierung
93
+ safety_checker=None,
94
+ #clean_up_tokenization_spaces=False #benötigt neue Transformer-Version
95
+ ).to(device)
96
+ except Exception as e:
97
+ print(f"Fehler beim Laden des Modells: {e}")
98
+ raise
99
+
100
+
101
+ from diffusers import DPMSolverMultistepScheduler
102
+ pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config(
103
+ pipe_img2img.scheduler.config,
104
+ algorithm_type="sde-dpmsolver++",
105
+ use_karras_sigmas=True,
106
+ timestep_spacing="trailing"
107
+ )
108
+
109
+ pipe_img2img.enable_attention_slicing()
110
+ pipe_img2img.enable_vae_tiling()
111
+ pipe_img2img.vae_slicing = True
112
+
113
+ return pipe_img2img
114
+
115
+ # === NEUE CALLBACK-FUNKTIONEN FÜR FORTSCHRITT (kompatibel mit neuer API) ===
116
+ class TextToImageProgressCallback:
117
+ def __init__(self, progress, total_steps):
118
+ self.progress = progress
119
+ self.total_steps = total_steps
120
+ self.current_step = 0
121
+
122
+ def __call__(self, pipe, step, timestep, callback_kwargs):
123
+ """Neue Callback-Signatur für diffusers >= 1.0.0"""
124
+ self.current_step = step + 1
125
+ progress_percent = (step / self.total_steps) * 100
126
+ self.progress(progress_percent / 100, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
127
+ return callback_kwargs
128
+
129
+ class ImageToImageProgressCallback:
130
+ def __init__(self, progress, total_steps, strength):
131
+ self.progress = progress
132
+ self.total_steps = total_steps
133
+ self.current_step = 0
134
+ self.strength = strength
135
+ self.actual_total_steps = None
136
+
137
+ def __call__(self, pipe, step, timestep, callback_kwargs):
138
+ """Neue Callback-Signatur für diffusers >= 1.0.0"""
139
+ self.current_step = step + 1
140
+
141
+ # Korrekte Berechnung der tatsächlichen Steps
142
+ if self.actual_total_steps is None:
143
+ # Bei Strength < 1.0 werden weniger Steps verwendet
144
+ if self.strength < 1.0:
145
+ self.actual_total_steps = int(self.total_steps * self.strength)
146
+ else:
147
+ self.actual_total_steps = self.total_steps
148
+
149
+ print(f"🎯 INTERNE STEP-AUSGABE: Strength {self.strength} → {self.actual_total_steps} tatsächliche Denoising-Schritte")
150
+
151
+ progress_percent = (step / self.actual_total_steps) * 100
152
+ self.progress(progress_percent / 100, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
153
+ return callback_kwargs
154
+
155
+ # === FUNKTIONEN ===
156
+ def text_to_image(prompt, steps, guidance_scale, progress=gr.Progress()):
157
+ try:
158
+ if not prompt or not prompt.strip():
159
+ return None
160
+
161
+ print(f"Starting generation for: {prompt}")
162
+ start_time = time.time()
163
+
164
+ # Statusmeldung anzeigen
165
+ progress(0, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
166
+
167
+ pipe = load_txt2img()
168
+
169
+ # ZUFÄLLIGER SEED für Variation
170
+ seed = random.randint(0, 2**32 - 1)
171
+ generator = torch.Generator(device=device).manual_seed(seed)
172
+ print(f"Using seed: {seed}")
173
+
174
+ # NEUE Callback-Implementierung
175
+ callback = TextToImageProgressCallback(progress, steps)
176
+
177
+ image = pipe(
178
+ prompt=prompt,
179
+ height=IMG_SIZE,
180
+ width=IMG_SIZE,
181
+ num_inference_steps=int(steps),
182
+ guidance_scale=guidance_scale,
183
+ generator=generator,
184
+ callback_on_step_end=callback, # NEUE Parameter-Name
185
+ callback_on_step_end_tensor_inputs=[], # Keine zusätzlichen Tensor-Inputs
186
+ ).images[0]
187
+
188
+ end_time = time.time()
189
+ print(f"Bild generiert in {end_time - start_time:.2f} Sekunden")
190
+
191
+ # Robuste Zwischenspeicherung
192
+ return image
193
+
194
+ except Exception as e:
195
+ print(f"Fehler: {e}")
196
+ import traceback
197
+ traceback.print_exc()
198
+ return None
199
+
200
+ def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2, progress=gr.Progress()):
201
+ try:
202
+ if image is None:
203
+ return None
204
+
205
+ print(f"Img2Img Start → Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
206
+ print(f"Prompt: {prompt}")
207
+ print(f"Negativ-Prompt: {neg_prompt}")
208
+ print(f"Gesicht beibehalten: {face_preserve}")
209
+ start_time = time.time()
210
+
211
+ # Statusmeldung anzeigen zur Zeitüberbrückung - Callback wird erst nach ersten Step aufgerufen!
212
+ progress(0, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
213
+
214
+ # --- CONTROLNET VORSCHALTEN ---
215
+ progress(0.05, desc="ControlNet: Pose-Erkennung...")
216
+
217
+ # Parameter-Tuning für ControlNet (wie in deinem Code)
218
+ adj_strength = min(0.85, strength * 1.3)
219
+ #adj_strength = min(0.85, strength)
220
+ actual_steps_from_strength = int(steps * adj_strength)
221
+ controlnet_steps = min(25, actual_steps_from_strength)
222
+
223
+ print(f"🎯 ControlNet Step-Kalkulation: UI={steps}, Adj-Strength={adj_strength:.3f}, Echte Steps={actual_steps_from_strength}, ControlNet-Steps={controlnet_steps}")
224
+
225
+ # CONTROLNET-STRENGTH BERECHNEN: 50% der Inpaint-Strength
226
+ controlnet_strength = adj_strength * 0.5
227
+ print(f"🎯 ControlNet Step-Kalkulation: UI={steps}, Adj-Strength={adj_strength:.3f}, Echte Steps={actual_steps_from_strength}, ControlNet-Steps={controlnet_steps}")
228
+ print(f"🎯 ControlNet Strength: {controlnet_strength:.3f} (50% von Inpaint-Strength {adj_strength:.3f})")
229
+
230
+
231
+ # ControlNet aufrufen
232
+ controlnet_image = controlnet_processor.generate_with_controlnet(
233
+ image=image,
234
+ prompt=prompt,
235
+ negative_prompt=neg_prompt,
236
+ steps=controlnet_steps,
237
+ guidance_scale=guidance_scale,
238
+ controlnet_strength=controlnet_strength
239
+ )
240
+
241
+ progress(0.3, desc="ControlNet fertig, starte Inpaint...")
242
+
243
+ # --- AB HIER DEIN ORIGINAL-CODE MIT CONTROLNET-BILD ---
244
+ pipe = load_img2img()
245
+ # ControlNet-Bild verwenden statt Original!
246
+ img_resized = controlnet_image.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
247
+
248
+ # --- PARAMETER-TUNING ---
249
+ adj_guidance = min(guidance_scale, 12.0)
250
+
251
+ # ZUFÄLLIGER SEED für Variation
252
+ seed = random.randint(0, 2**32 - 1)
253
+ generator = torch.Generator(device=device).manual_seed(seed)
254
+ print(f"Using seed: {seed}")
255
+
256
+ # --- GESICHTSMASKE ---
257
+ mask = None
258
+ bbox_coords = None
259
+
260
+ if bbox_x1 is not None and bbox_y1 is not None and bbox_x2 is not None and bbox_y2 is not None:
261
+ # Skaliere Koordinaten auf die neue Bildgröße
262
+ orig_width, orig_height = image.size
263
+ scale_x = IMG_SIZE / orig_width
264
+ scale_y = IMG_SIZE / orig_height
265
+
266
+ scaled_coords = [
267
+ int(bbox_x1 * scale_x),
268
+ int(bbox_y1 * scale_y),
269
+ int(bbox_x2 * scale_x),
270
+ int(bbox_y2 * scale_y)
271
+ ]
272
+ bbox_coords = scaled_coords
273
+ print(f"Skalierte Koordinaten: {scaled_coords}")
274
+
275
+ # Maskenlogik basierend auf face_preserve
276
+ if bbox_coords:
277
+ mask = create_face_mask(img_resized, bbox_coords, face_preserve)
278
+ if mask:
279
+ #mask.show() # Zeigt die Maske zum Überprüfen
280
+ #print(f"Maske Größe: {mask.size}, Bereich: {bbox_coords}")
281
+ print("Maske erfolgreich erstellt")
282
+ else:
283
+ print("Keine gültigen Koordinaten - keine Maske angewendet")
284
+ mask = None
285
+
286
+ # Detaillierte Debug-Informationen vor dem Pipeline-Aufruf
287
+ print(f"⚙️ PIPELINE-KONFIGURATION:")
288
+ print(f" - Angefordert: {int(steps)} Steps")
289
+ print(f" - Strength: {adj_strength:.3f}")
290
+ print(f" - Scheduler: {pipe.scheduler.__class__.__name__}")
291
+
292
+ print(f"🎯 KORREKTE INTERNE STEP-AUSGABE: {int(steps)} Steps × Strength {adj_strength:.3f} = {actual_steps_from_strength} tatsächliche Schritte")
293
+
294
+ # NEUE Callback-Implementierung
295
+ callback = ImageToImageProgressCallback(progress, int(steps), adj_strength)
296
+
297
+ # --- PIPELINE-AUFRUF MIT NEUER API ---
298
+ result = pipe(
299
+ prompt=prompt,
300
+ negative_prompt=neg_prompt,
301
+ image=img_resized, # Jetzt: ControlNet-Bild!
302
+ mask_image=mask,
303
+ strength=adj_strength,
304
+ num_inference_steps=int(steps),
305
+ guidance_scale=adj_guidance,
306
+ generator=generator,
307
+ callback_on_step_end=callback,
308
+ callback_on_step_end_tensor_inputs=[],
309
+ )
310
+
311
+ # ZUSÄTZLICHE AUSGABE: Tatsächliche Steps
312
+ try:
313
+ scheduler = pipe.scheduler
314
+ print(f"🔧 SCHEDULER-INFO: {scheduler.__class__.__name__}")
315
+ print(f"📊 TATSÄCHLICHE STEP-KONFIGURATION: {int(steps)} Schritte mit Strength {adj_strength:.3f}")
316
+
317
+ if hasattr(scheduler, 'timesteps'):
318
+ actual_steps = len(scheduler.timesteps)
319
+ print(f"🎯 BESTÄTIGTE INTERNE STEP-AUSGABE: Scheduler verwendete {actual_steps} tatsächliche Denoising-Schritte")
320
+
321
+ except Exception as e:
322
+ print(f"⚠️ Konnte Scheduler-Info nicht auslesen: {e}")
323
+
324
+ end_time = time.time()
325
+ print(f"Bild transformiert in {end_time - start_time:.2f} Sekunden")
326
+
327
+ generated_image = result.images[0]
328
+
329
+ return generated_image
330
+
331
+ except Exception as e:
332
+ print(f"Fehler: {e}")
333
+ import traceback
334
+ traceback.print_exc()
335
+ return None
336
+
337
+
338
+ def update_bbox_from_image(image):
339
+ """Aktualisiert die Bounding-Box-Koordinaten wenn ein Bild hochgeladen wird"""
340
+ if image is None:
341
+ return None, None, None, None
342
+
343
+ bbox = auto_detect_face_area(image)
344
+ return bbox[0], bbox[1], bbox[2], bbox[3]
345
+
346
+ def main_ui():
347
+ with gr.Blocks(
348
+ title="AI Image Generator",
349
+ theme=gr.themes.Base(),
350
+ css="""
351
+ .info-box {
352
+ background-color: #f8f4f0;
353
+ padding: 15px;
354
+ border-radius: 8px;
355
+ border-left: 4px solid #8B7355;
356
+ margin: 20px 0;
357
+ }
358
+ .clickable-file {
359
+ color: #1976d2;
360
+ cursor: pointer;
361
+ text-decoration: none;
362
+ font-family: 'Monaco', 'Consolas', monospace;
363
+ background: #e3f2fd;
364
+ padding: 2px 6px;
365
+ border-radius: 4px;
366
+ border: 1px solid #bbdefb;
367
+ }
368
+ .clickable-file:hover {
369
+ background: #bbdefb;
370
+ text-decoration: underline;
371
+ }
372
+ #start-button {
373
+ background-color: #0080FF !important;
374
+ border: none !important;
375
+ margin: 50px auto !important;
376
+ display: block !important;
377
+ font-weight: 600;
378
+ width: 280px;
379
+ }
380
+ #start-button:hover {
381
+ background-color: #D3D3D3 !important;
382
+ }
383
+ .hint-box {
384
+ margin-top: 20px;
385
+ }
386
+ .custom-text {
387
+ font-size: 25px !important;
388
+ }
389
+ .image-upload .svelte-1p4f8co {
390
+ display: block !important;
391
+ }
392
+ """
393
+ ) as demo:
394
+
395
+ # --- Info-Bereich (Startseite) ---
396
+ gr.Markdown(
397
+ """
398
+ # Demo-Projekt: Stable Diffusion Text-to-Image / Image-to-Image
399
+ <br>
400
+
401
+ <div class="info-box">
402
+ Dieses Projekt ist ein kleines <strong>Demo</strong> um meine Fähigkeiten als <strong>AI-Engineer</strong>
403
+ in technischer Kompetenz und selbstständiger Projektstrukturierung zu zeigen.<br>
404
+ Der Fokus liegt auf <strong>Struktur, Konzept und technischer Umsetzung</strong>
405
+ im Bereich Text-to-Image / Image-to-Image mit dem Diffusionsmodell "Stable Diffusion" <br>
406
+ <strong>nicht</strong> auf einer vollständigen Produktionsversion.
407
+ </div>
408
+ <br>
409
+
410
+ <div class="info-box">
411
+ Zudem führt der Link
412
+ <a class="clickable-file" href="https://huggingface.co/spaces/Astridkraft/Dokumentation" target="_blank">Roadmap</a>
413
+ zu einer <strong>durchdachten, skalierbaren, professionellen Code-Architektur</strong> für Text-to-Image- und Image-to-Image-Entwicklung <br>
414
+ die die <strong>gesamte Komplexität einer professionellen Umsetzung</strong> verdeutlicht.<br><br>
415
+ Die damit gezeigten Fähigkeiten meinerseits sind <strong>sicherlich auf andere Projekte übertragbar</strong>.
416
+ </div>
417
+ <br><br>
418
+
419
+ <div class="info-box">
420
+ <strong>Hinweis:</strong><br>
421
+ Die Anwendung läuft derzeit auf <strong>CPU</strong> ist jedoch <strong>vollständig GPU-fähig </strong>.
422
+ Deshalb muss bei jeder Generierung eine <strong>längere Wartezeit</strong> eingeplant werden.<br>
423
+ Bei <strong>Verbindungsabbrüchen</strong> -insbesondere bei CPU-Nutzung- wird die aktuelle Generierung zunächst serverseitig vollständig abgeschlossen, <br>
424
+ bevor neue Anfragen bearbeitet werden. Das generierte Bild wird in diesem Fall <strong>nicht angezeigt</strong>.
425
+ Die Meldung <strong>Connection re-established</strong> signalisiert, dass <br>
426
+ die Verbindung wiederhergestellt wurde, die laufende Verarbeitung jedoch priorisiert wird. In der Konsequenz werden neue <br>
427
+ Generierungsanfragen in eine Warteschlange gestellt und erst nach vollständigem Abschluss der aktuellen serverseitigen Berechnung verarbeitet.
428
+ </div>
429
+ <br>
430
+
431
+ <div class="info-box">
432
+ <strong>Technischer Hintergrund:</strong> <br>
433
+ Es handelt sich um einen bekannten <strong>Gradio-Bug</strong> - das Framework bietet keine Möglichkeit, generierte Bilder zwischenzuspeichern <br>
434
+ um sie bei UI-Neuladung aus dem Zwischenspeicher zurückzugeben. Dies macht sich <strong>besonders bei CPU-Nutzung bemerkbar</strong>, <br>
435
+ da die Transformationszeiten hier deutlich länger sind und es dadurch vermehrt zu Timeouts und daraus resultierenden UI-Abbrüchen kommen kann.<br>
436
+ Aus diesem Grund kann das Ergebnis nicht an die neu geladene Benutzeroberfläche übermittelt werden obwohl die Bildgenerierung serverseitig <br>
437
+ vollständig abgeschlossen wird.
438
+ </div>
439
+ <br>
440
+
441
+ <div class="info-box">
442
+ <strong>Anwendungsbereich:</strong><br>
443
+ Die <strong>Bild-zu-Bild-Funktion</strong> ermöglicht eine gezielte Bearbeitung eines <strong>beliebigen Objektes oder Bereiches</strong> in einem Bild.<br>
444
+ Sie unterstützt zwei Modi:<br>
445
+ &nbsp;&nbsp;• <strong>Beibehaltung eines ausgewählten Bildbereiches innerhalb eines Rechtecks</strong> (z. B. Gesicht, Objekt, Tier, Gegenstand) bei Veränderung des Rests,<br>
446
+ &nbsp;&nbsp;• oder <strong>Veränderung des rechteckigen Bildbereiches</strong> bei Erhaltung der Umgebung.<br>
447
+ <br>
448
+ <strong>Wichtig:</strong> Das Objekt das beibehalten oder verändert werden soll - je nach gewähltem Modus - muss <strong>im Prompt</strong> klar beschrieben werden.<br>
449
+ Die Verwendung eines <strong>Negativ-Promptes</strong> ist sinnvoll, um unerwünschte Veränderungen zu vermeiden.
450
+ </div>
451
+ <br>
452
+
453
+ <div class="info-box">
454
+ <strong>Empfehlung:</strong><br>
455
+ Für eine präzise Abstimmung der zentralen Parameter – Prompt, Negativ-Prompt, Ver��nderungsstärke (Strength), Inferenz-Schritte (Steps) <br>
456
+ und Prompt-Stärke (Guidance) – liefern leistungsfähige Sprachmodelle wie GPT, Grok oder DeepSeek hochqualitative, kontextbezogene Vorschläge.<br>
457
+ Prompt und Negativ-Prompt sollten auf <strong>Englisch</strong> eingegeben werden, da "Stable Diffusion" mit Bild-Text-Paaren auf Englisch trainiert <br>
458
+ wurde und CLIP einen Tokenizer für ein englisches Vokabular nutzt. Der CLIP-Tokenizer hat außerdem ein <strong>Limit von 77 Token</strong>, wodurch längere <br>
459
+ Prompteingaben automatisch abgeschnitten werden. Deutsche Wörter werden zwar übersetzt, führen aber zu Verzerrungen.
460
+ </div>
461
+ """
462
+ )
463
+
464
+ # --- Button zentriert im unteren Drittel, Taupe-Farbe ---
465
+ with gr.Row():
466
+ with gr.Column(scale=1): # Linker Leerraum
467
+ pass
468
+ with gr.Column(scale=1, min_width=300): # Mittig, feste Mindestbreite
469
+ start_btn = gr.Button(
470
+ "Weiter zur Anwendung",
471
+ variant="primary",
472
+ size="lg",
473
+ elem_id="start-button"
474
+ )
475
+ with gr.Column(scale=1): # Rechter Leerraum
476
+ pass
477
+
478
+ # --- Hauptanwendungsbereich (zunächst versteckt) ---
479
+ with gr.Column(visible=False) as content_area:
480
+ with gr.Tab("Text zu Bild"):
481
+ gr.Markdown("**Beschreibe dein gewünschtes Bild (maximal 77 Token):**")
482
+
483
+ with gr.Row():
484
+ txt_input = gr.Textbox(
485
+ placeholder="z.B. ultra realistic mountain landscape at sunrise, soft mist over the valley, detailed foliage, crisp textures, depth of field, sunlight rays through clouds, shot on medium format camera, 8k, HDR, hyper-detailed, natural lighting, masterpiece, Eingabe unten:(Schritt Inferenz:35, Prompt-Stärke:9)",
486
+ lines=2,
487
+ label="Prompt (Englisch)",
488
+ info="Beschreibe detailliert, was du sehen möchtest. Verwende Kommas zur Trennung."
489
+ )
490
+
491
+ with gr.Row():
492
+ with gr.Column():
493
+ txt_steps = gr.Slider(
494
+ minimum=10, maximum=100, value=35, step=1,
495
+ label="Inferenz-Schritte",
496
+ info="Mehr Schritte = bessere Qualität, aber langsamer (20-50 empfohlen)"
497
+ )
498
+ with gr.Column():
499
+ txt_guidance = gr.Slider(
500
+ minimum=1.0, maximum=20.0, value=7.5, step=0.5,
501
+ label="Prompt-Stärke",
502
+ info="Wie stark der Prompt befolgt wird (7-12 für gute Balance)"
503
+ )
504
+
505
+ generate_btn = gr.Button("Bild generieren", variant="primary")
506
+ txt_output = gr.Image(
507
+ label="Generiertes Bild",
508
+ show_download_button=True,
509
+ type="pil"
510
+ )
511
+
512
+ generate_btn.click(
513
+ fn=text_to_image,
514
+ inputs=[txt_input, txt_steps, txt_guidance],
515
+ outputs=txt_output,
516
+ concurrency_limit=1
517
+ )
518
+
519
+ with gr.Tab("Bild zu Bild"):
520
+ gr.Markdown("**Lade ein Bild hoch und beschreibe die gewünschte Veränderung:**")
521
+
522
+ with gr.Row():
523
+ img_input = gr.Image(
524
+ type="pil",
525
+ label="Eingabebild",
526
+ height=300,
527
+ sources=["upload"] # Nur Upload-Button anzeigen
528
+ )
529
+
530
+ with gr.Row():
531
+ with gr.Column():
532
+ img_prompt = gr.Textbox(
533
+ placeholder="change background to beach with palm trees, keep person unchanged, sunny day",
534
+ lines=2,
535
+ label="Transformations-Prompt (Englisch - maximal 77 Token)",
536
+ info="Was soll verändert werden? Sei spezifisch."
537
+ )
538
+ with gr.Column():
539
+ img_neg_prompt = gr.Textbox(
540
+ placeholder="blurry, deformed, ugly, bad anatomy, extra limbs, poorly drawn hands",
541
+ lines=2,
542
+ label="Negativ-Prompt (Englisch - maximal 77 Token)",
543
+ info="Was soll vermieden werden? Unerwünschte Elemente auflisten."
544
+ )
545
+
546
+ with gr.Row():
547
+ with gr.Column():
548
+ strength_slider = gr.Slider(
549
+ minimum=0.1, maximum=0.9, value=0.4, step=0.05,
550
+ label="Veränderungs-Stärke",
551
+ info="0.1-0.3: Leichte Anpassungen, 0.4-0.6: Mittlere Veränderungen, 0.7-0.9: Starke Umgestaltung"
552
+ )
553
+ with gr.Column():
554
+ img_steps = gr.Slider(
555
+ minimum=10, maximum=100, value=35, step=1,
556
+ label="Inferenz-Schritte",
557
+ info="Anzahl der Verarbeitungsschritte (25-45 für gute Ergebnisse)"
558
+ )
559
+ with gr.Column():
560
+ img_guidance = gr.Slider(
561
+ minimum=1.0, maximum=20.0, value=7.5, step=0.5,
562
+ label="Prompt-Stärke",
563
+ info="Einfluss des Prompts auf das Ergebnis (6-10 für natürliche Ergebnisse)"
564
+ )
565
+
566
+ # GESICHTSOPTIONEN
567
+ with gr.Row():
568
+ face_preserve = gr.Checkbox(
569
+ label="Gesicht, Tier, Gegenstand beibehalten",
570
+ value=True,
571
+ info="Aktiviert: Bildelement bleibt erhalten, Hintergrund wird verändert | Deaktiviert: Nur Bildelement wird verändert"
572
+ )
573
+
574
+ with gr.Row():
575
+ gr.Markdown("**Gesichtsbereich anpassen**")
576
+
577
+ with gr.Row():
578
+ bbox_x1 = gr.Number(
579
+ label="Links (x1)",
580
+ value=100,
581
+ precision=0,
582
+ info="Linke Kante des Gesichtsbereichs"
583
+ )
584
+ bbox_y1 = gr.Number(
585
+ label="Oben (y1)",
586
+ value=100,
587
+ precision=0,
588
+ info="Obere Kante des Gesichtsbereichs"
589
+ )
590
+ bbox_x2 = gr.Number(
591
+ label="Rechts (x2)",
592
+ value=300,
593
+ precision=0,
594
+ info="Rechte Kante des Gesichtsbereichs"
595
+ )
596
+ bbox_y2 = gr.Number(
597
+ label="Unten (y2)",
598
+ value=300,
599
+ precision=0,
600
+ info="Untere Kante des Gesichtsbereichs"
601
+ )
602
+
603
+ with gr.Row():
604
+ gr.Markdown(
605
+ "**Achtung:**\n"
606
+ "• **Automatische Gesichtserkennung** setzt Koordinaten beim Upload\n"
607
+ "• **Koordinaten nur bei erkennbaren Verzerrungen anpassen** (Bereiche leicht verschieben)"
608
+ )
609
+
610
+
611
+ transform_btn = gr.Button("Bild transformieren", variant="primary")
612
+
613
+ with gr.Row():
614
+ img_output = gr.Image(
615
+ label="Transformiertes Bild",
616
+ show_download_button=True,
617
+ type="pil"
618
+ )
619
+
620
+ # Event-Handler für Bild-Upload
621
+ img_input.change(
622
+ fn=update_bbox_from_image,
623
+ inputs=[img_input],
624
+ outputs=[bbox_x1, bbox_y1, bbox_x2, bbox_y2]
625
+ )
626
+
627
+ transform_btn.click(
628
+ fn=img_to_image,
629
+ inputs=[
630
+ img_input, img_prompt, img_neg_prompt,
631
+ strength_slider, img_steps, img_guidance,
632
+ face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2
633
+ ],
634
+ outputs=img_output,
635
+ concurrency_limit=1
636
+ )
637
+
638
+ # Sammle alle Info-Komponenten, die versteckt werden sollen
639
+ info_components = []
640
+ for child in demo.children:
641
+ if child != content_area:
642
+ info_components.append(child)
643
+
644
+ # Event-Handler für Start-Button
645
+ start_btn.click(
646
+ fn=lambda: gr.update(visible=True),
647
+ inputs=None,
648
+ outputs=content_area
649
+ ).then(
650
+ fn=lambda: [gr.update(visible=False) for _ in info_components],
651
+ inputs=None,
652
+ outputs=info_components
653
+ )
654
+
655
+ return demo
656
+
657
+
658
+ if __name__ == "__main__":
659
+ demo = main_ui()
660
+ demo.queue()
661
+ demo.launch(
662
+ server_name="0.0.0.0",
663
+ server_port=7860,
664
+ max_file_size="10MB",
665
+ show_error=True,
666
+ share=False
667
+ )