File size: 29,887 Bytes
9a70318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0048e22
9a70318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
05d9097
9a70318
 
 
 
 
 
0048e22
05d9097
9a70318
 
0048e22
9a70318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7f8818
9a70318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7f8818
9a70318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
#Eine neue Stable Diffusion (SD) Generation kommt mit neuem Tokenizer, mehrsprachiger Unterstützung, längerem Kontext 
#und deutlich besserem Prompt-Verständnis - (Änderung Architektur).
#Eine deutsche Alternative zur Umsetzung von Text-Bild zu Bild ist Flux - mit einer völlig anderen Architektur als SD!
import gradio as gr
from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline
from diffusers import StableDiffusionInpaintPipeline
from controlnet_module import controlnet_processor
import torch
from PIL import Image, ImageDraw
import time
import os
import tempfile
import random



# === OPTIMIERTE EINSTELLUNGEN ===
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if device == "cuda" else torch.float32
IMG_SIZE = 512

print(f"Running on: {device}")

# === GESICHTSMASKEN-FUNKTIONEN ===
def create_face_mask(image, bbox_coords, face_preserve):
    """Erzeugt eine Gesichtsmaske - WEIßE Bereiche werden VERÄNDERT, SCHWARZE BLEIBEN"""
    mask = Image.new("L", image.size, 0)  # Start mit komplett schwarzer Maske (alles geschützt)
    
    if bbox_coords and all(coord is not None for coord in bbox_coords):
        x1, y1, x2, y2 = bbox_coords
        draw = ImageDraw.Draw(mask)
        
        if face_preserve:
            # GESICHTSERHALTUNG: Maske um das Gesicht herum zeichnen
            draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255)  # Alles weiß = verändern
            draw.rectangle([x1, y1, x2, y2], fill=0)  # Gesicht schwarz = geschützt (rechteckig)
            print("Gesicht wird GESCHÜTZT - Umgebung wird verändert (rechteckige Maske)")
        else:
            # NUR GESICHT VERÄNDERN: Nur das Gesicht wird weiß (verändert)
            draw.rectangle([x1, y1, x2, y2], fill=255)  # Gesicht weiß = verändern (rechteckig)
            print("Nur Gesicht wird verändert - Umgebung bleibt erhalten (rechteckige Maske)")
    
    return mask

def auto_detect_face_area(image):
    """Optimierten Vorschlag für Gesichtsbereich ohne externe Bibliotheken"""
    width, height = image.size
    # Größere Bounding Box für bessere Abdeckung (50% statt 40%)
    face_size = min(width, height) * 0.4
    # Verschiebe y1 nach oben, um Stirn und Kinn besser abzudecken
    x1 = (width - face_size) / 2
    y1 = (height - face_size) / 4  # Höher positioniert (25% statt 33%)
    x2 = x1 + face_size
    y2 = y1 + face_size * 1.2  # Leicht länglicher für ovale Gesichter
    # Stelle sicher, dass Koordinaten innerhalb des Bildes liegen
    x1, y1 = max(0, int(x1)), max(0, int(y1))
    x2, y2 = min(width, int(x2)), min(height, int(y2))
    print(f"Geschätzte Gesichtskoordinaten: [{x1}, {y1}, {x2}, {y2}]")
    return [x1, y1, x2, y2]

# === PIPELINES ===
pipe_txt2img = None
pipe_img2img = None

def load_txt2img():
    global pipe_txt2img
    if pipe_txt2img is None:
        print("Loading Text-to-Image model...")
        pipe_txt2img = StableDiffusionPipeline.from_pretrained(
            "runwayml/stable-diffusion-v1-5",
            torch_dtype=torch_dtype,
            use_safetensors=True,
            safety_checker=None,
            requires_safety_checker=False,
            #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!
        ).to(device)
        
        from diffusers import DPMSolverMultistepScheduler
        pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(pipe_txt2img.scheduler.config)
        pipe_txt2img.enable_attention_slicing()
    return pipe_txt2img

def load_img2img():
    global pipe_img2img
    if pipe_img2img is None:
        print("Loading Inpainting model...")
        try:
            pipe_img2img = StableDiffusionInpaintPipeline.from_pretrained(
                "stabilityai/stable-diffusion-2-inpainting",  # Neues Modell
                torch_dtype=torch_dtype,
                use_safetensors=True,  # Erzwinge .safetensors
                allow_pickle=False,    # Verhindere unsichere Serialisierung
                safety_checker=None,
                #clean_up_tokenization_spaces=False #benötigt neue Transformer-Version
            ).to(device)
        except Exception as e:
            print(f"Fehler beim Laden des Modells: {e}")
            raise

        
        from diffusers import DPMSolverMultistepScheduler
        pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config(
            pipe_img2img.scheduler.config,
            algorithm_type="sde-dpmsolver++",
            use_karras_sigmas=True,
            timestep_spacing="trailing"
        )
        
        pipe_img2img.enable_attention_slicing()
        pipe_img2img.enable_vae_tiling()
        pipe_img2img.vae_slicing = True

    return pipe_img2img

# === NEUE CALLBACK-FUNKTIONEN FÜR FORTSCHRITT (kompatibel mit neuer API) ===
class TextToImageProgressCallback:
    def __init__(self, progress, total_steps):
        self.progress = progress
        self.total_steps = total_steps
        self.current_step = 0
    
    def __call__(self, pipe, step, timestep, callback_kwargs):
        """Neue Callback-Signatur für diffusers >= 1.0.0"""
        self.current_step = step + 1
        progress_percent = (step / self.total_steps) * 100
        self.progress(progress_percent / 100, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
        return callback_kwargs

class ImageToImageProgressCallback:
    def __init__(self, progress, total_steps, strength):
        self.progress = progress
        self.total_steps = total_steps
        self.current_step = 0
        self.strength = strength
        self.actual_total_steps = None

    def __call__(self, pipe, step, timestep, callback_kwargs):
        """Neue Callback-Signatur für diffusers >= 1.0.0"""
        self.current_step = step + 1
    
        # Korrekte Berechnung der tatsächlichen Steps
        if self.actual_total_steps is None:
            # Bei Strength < 1.0 werden weniger Steps verwendet
            if self.strength < 1.0:
                self.actual_total_steps = int(self.total_steps * self.strength)
            else:
                self.actual_total_steps = self.total_steps
            
            print(f"🎯 INTERNE STEP-AUSGABE: Strength {self.strength}{self.actual_total_steps} tatsächliche Denoising-Schritte")
        
        progress_percent = (step / self.actual_total_steps) * 100
        self.progress(progress_percent / 100, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
        return callback_kwargs

# === FUNKTIONEN ===
def text_to_image(prompt, steps, guidance_scale, progress=gr.Progress()):
    try:
        if not prompt or not prompt.strip():
            return None
            
        print(f"Starting generation for: {prompt}")
        start_time = time.time()
        
        # Statusmeldung anzeigen
        progress(0, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")
        
        pipe = load_txt2img()
        
        # ZUFÄLLIGER SEED für Variation
        seed = random.randint(0, 2**32 - 1)
        generator = torch.Generator(device=device).manual_seed(seed)
        print(f"Using seed: {seed}")
        
        # NEUE Callback-Implementierung
        callback = TextToImageProgressCallback(progress, steps)
        
        image = pipe(
            prompt=prompt,
            height=IMG_SIZE,
            width=IMG_SIZE,
            num_inference_steps=int(steps),
            guidance_scale=guidance_scale,
            generator=generator,
            callback_on_step_end=callback,  # NEUE Parameter-Name
            callback_on_step_end_tensor_inputs=[],  # Keine zusätzlichen Tensor-Inputs
        ).images[0]
        
        end_time = time.time()
        print(f"Bild generiert in {end_time - start_time:.2f} Sekunden")
        
        # Robuste Zwischenspeicherung
        return image
        
    except Exception as e:
        print(f"Fehler: {e}")
        import traceback
        traceback.print_exc()
        return None

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()):
    try:
        if image is None:
            return None

        print(f"Img2Img Start → Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
        print(f"Prompt: {prompt}")
        print(f"Negativ-Prompt: {neg_prompt}")
        print(f"Gesicht beibehalten: {face_preserve}")
        start_time = time.time()

        # Statusmeldung anzeigen zur Zeitüberbrückung - Callback wird erst nach ersten Step aufgerufen!
        progress(0, desc="Generierung läuft - CPU benötigt bis zu 20 Minuten!")

        # --- Progress-Balken zur Überbrückung für 0te Step von CONTROLNET---
        progress(0.05, desc="ControlNet: Pose-Erkennung...")
        
        # Parameter-Tuning für ControlNet (wie in deinem Code)
        adj_strength = min(0.85, strength * 1.3)
        #adj_strength = min(0.85, strength)
        actual_steps_from_strength = int(steps * adj_strength)
        controlnet_steps = min(25, actual_steps_from_strength)
        
        print(f"🎯 ControlNet Step-Kalkulation: UI={steps}, Adj-Strength={adj_strength:.3f}, Echte Steps={actual_steps_from_strength}, ControlNet-Steps={controlnet_steps}")

        # CONTROLNET-STRENGTH BERECHNEN: 50% der Inpaint-Strength
        controlnet_strength = adj_strength * 0.5
        print(f"🎯 ControlNet Step-Kalkulation: UI={steps}, Adj-Strength={adj_strength:.3f}, Echte Steps={actual_steps_from_strength}, ControlNet-Steps={controlnet_steps}")
        print(f"🎯 ControlNet Strength: {controlnet_strength:.3f} (50% von Inpaint-Strength {adj_strength:.3f})")

        
        # ControlNet Modul aufrufen mit Übergabe der Parameter
        controlnet_image = controlnet_processor.generate_with_controlnet(
            image=image,
            prompt=prompt,
            negative_prompt=neg_prompt,
            steps=controlnet_steps,
            guidance_scale=guidance_scale,
            controlnet_strength=controlnet_strength,
            progress=progress
        )
        
        #Progress-Balken zur Überbrückung für 0ten Step von Inpaint
        progress(0.3, desc="ControlNet fertig, starte Inpaint...")

        # --- AB HIER DEIN ORIGINAL-CODE MIT CONTROLNET-BILD ---
        pipe = load_img2img()
        # ControlNet-Bild verwenden statt Original!
        img_resized = controlnet_image.convert("RGB").resize((IMG_SIZE, IMG_SIZE))

        # --- PARAMETER-TUNING ---
        adj_guidance = min(guidance_scale, 12.0)
    
        # ZUFÄLLIGER SEED für Variation
        seed = random.randint(0, 2**32 - 1)
        generator = torch.Generator(device=device).manual_seed(seed)
        print(f"Using seed: {seed}")

        # --- GESICHTSMASKE ---
        mask = None
        bbox_coords = None
        
        if bbox_x1 is not None and bbox_y1 is not None and bbox_x2 is not None and bbox_y2 is not None:
            # Skaliere Koordinaten auf die neue Bildgröße
            orig_width, orig_height = image.size
            scale_x = IMG_SIZE / orig_width
            scale_y = IMG_SIZE / orig_height
            
            scaled_coords = [
                int(bbox_x1 * scale_x),
                int(bbox_y1 * scale_y),
                int(bbox_x2 * scale_x),
                int(bbox_y2 * scale_y)
            ]
            bbox_coords = scaled_coords
            print(f"Skalierte Koordinaten: {scaled_coords}")

        # Maskenlogik basierend auf face_preserve
        if bbox_coords:
            mask = create_face_mask(img_resized, bbox_coords, face_preserve)
            if mask:
                #mask.show()  # Zeigt die Maske zum Überprüfen
                #print(f"Maske Größe: {mask.size}, Bereich: {bbox_coords}")
                print("Maske erfolgreich erstellt")
        else:
            print("Keine gültigen Koordinaten - keine Maske angewendet")
            mask = None

        # Detaillierte Debug-Informationen vor dem Pipeline-Aufruf
        print(f"⚙️  PIPELINE-KONFIGURATION:")
        print(f"   - Angefordert: {int(steps)} Steps")
        print(f"   - Strength: {adj_strength:.3f}")
        print(f"   - Scheduler: {pipe.scheduler.__class__.__name__}")

        print(f"🎯 KORREKTE INTERNE STEP-AUSGABE: {int(steps)} Steps × Strength {adj_strength:.3f} = {actual_steps_from_strength} tatsächliche Schritte")

        # NEUE Callback-Implementierung
        callback = ImageToImageProgressCallback(progress, int(steps), adj_strength)

        # --- PIPELINE-AUFRUF MIT NEUER API ---
        result = pipe(
            prompt=prompt,
            negative_prompt=neg_prompt,
            image=img_resized,  # Jetzt: ControlNet-Bild!
            mask_image=mask,
            strength=adj_strength,
            num_inference_steps=int(steps),
            guidance_scale=adj_guidance,
            generator=generator,
            callback_on_step_end=callback,
            callback_on_step_end_tensor_inputs=[],
        )

        # ZUSÄTZLICHE AUSGABE: Tatsächliche Steps
        try:
            scheduler = pipe.scheduler
            print(f"🔧 SCHEDULER-INFO: {scheduler.__class__.__name__}")
            print(f"📊 TATSÄCHLICHE STEP-KONFIGURATION: {int(steps)} Schritte mit Strength {adj_strength:.3f}")
    
            if hasattr(scheduler, 'timesteps'):
                actual_steps = len(scheduler.timesteps)
                print(f"🎯 BESTÄTIGTE INTERNE STEP-AUSGABE: Scheduler verwendete {actual_steps} tatsächliche Denoising-Schritte")
        
        except Exception as e:
            print(f"⚠️  Konnte Scheduler-Info nicht auslesen: {e}")

        end_time = time.time()
        print(f"Bild transformiert in {end_time - start_time:.2f} Sekunden")
        
        generated_image = result.images[0]

        return generated_image
        
    except Exception as e:
        print(f"Fehler: {e}")
        import traceback
        traceback.print_exc()
        return None


def update_bbox_from_image(image):
    """Aktualisiert die Bounding-Box-Koordinaten wenn ein Bild hochgeladen wird"""
    if image is None:
        return None, None, None, None
    
    bbox = auto_detect_face_area(image)
    return bbox[0], bbox[1], bbox[2], bbox[3]

def main_ui():
    with gr.Blocks(
        title="AI Image Generator", 
        theme=gr.themes.Base(),
        css="""
        .info-box {
            background-color: #f8f4f0; 
            padding: 15px; 
            border-radius: 8px; 
            border-left: 4px solid #8B7355;
            margin: 20px 0;
        }
        .clickable-file {
           color: #1976d2;
           cursor: pointer;
           text-decoration: none;
           font-family: 'Monaco', 'Consolas', monospace;
           background: #e3f2fd;
           padding: 2px 6px;
           border-radius: 4px;
           border: 1px solid #bbdefb;
         }
        .clickable-file:hover {
           background: #bbdefb;
           text-decoration: underline;
         }
        #start-button {
            background-color: #0080FF !important;
            border: none !important;
            margin: 50px auto !important;
            display: block !important;
            font-weight: 600;
            width: 280px;
        }
        #start-button:hover {
            background-color: #D3D3D3 !important;
        }
        .hint-box {
            margin-top: 20px;
        }
        .custom-text {
            font-size: 25px !important;
        }
        .image-upload .svelte-1p4f8co {
            display: block !important;
        }
        """
    ) as demo:

        # --- Info-Bereich (Startseite) ---
        gr.Markdown(
            """
            # Demo-Projekt: Stable Diffusion Text-to-Image / Image-to-Image
            <br>
            
            <div class="info-box">
            Dieses Projekt ist ein kleines <strong>Demo</strong> um meine Fähigkeiten als <strong>AI-Engineer</strong>
            in technischer Kompetenz und selbstständiger Projektstrukturierung zu zeigen.<br>
            Der Fokus liegt auf <strong>Struktur, Konzept und technischer Umsetzung</strong>
            im Bereich Text-to-Image / Image-to-Image mit dem Diffusionsmodell "Stable Diffusion" <br>
            <strong>nicht</strong> auf einer vollständigen Produktionsversion.
            </div>
            <br>
           
            <div class="info-box">
            Zudem führt der Link
            <a class="clickable-file" href="https://huggingface.co/spaces/Astridkraft/Dokumentation" target="_blank">Roadmap</a>
            zu einer <strong>durchdachten, skalierbaren, professionellen Code-Architektur</strong> für Text-to-Image- und Image-to-Image-Entwicklung <br>
            die die <strong>gesamte Komplexität einer professionellen Umsetzung</strong> verdeutlicht.<br><br>     
            Die damit gezeigten Fähigkeiten meinerseits sind <strong>sicherlich auf andere Projekte übertragbar</strong>.
            </div>         
            <br><br>
            
            <div class="info-box">
            <strong>Hinweis:</strong><br>
            Die Anwendung läuft derzeit auf <strong>CPU</strong> ist jedoch <strong>vollständig GPU-fähig </strong>.
            Deshalb muss bei jeder Generierung eine <strong>längere Wartezeit</strong> eingeplant werden.<br>
            Bei <strong>Verbindungsabbrüchen</strong> -insbesondere bei CPU-Nutzung- wird die aktuelle Generierung zunächst serverseitig vollständig abgeschlossen, <br>
            bevor neue Anfragen bearbeitet werden. Das generierte Bild wird in diesem Fall <strong>nicht angezeigt</strong>. 
            Die Meldung <strong>Connection re-established</strong> signalisiert, dass <br>
            die Verbindung wiederhergestellt wurde, die laufende Verarbeitung jedoch priorisiert wird. In der Konsequenz werden neue <br>
            Generierungsanfragen in eine Warteschlange gestellt und erst nach vollständigem Abschluss der aktuellen serverseitigen Berechnung verarbeitet.
            </div>
             <br>
             
            <div class="info-box">
            <strong>Technischer Hintergrund:</strong> <br>
            Es handelt sich um einen bekannten <strong>Gradio-Bug</strong> - das Framework bietet keine Möglichkeit, generierte Bilder zwischenzuspeichern <br>
            um sie bei UI-Neuladung aus dem Zwischenspeicher zurückzugeben. Dies macht sich <strong>besonders bei CPU-Nutzung bemerkbar</strong>, <br>
            da die Transformationszeiten hier deutlich länger sind und es dadurch vermehrt zu Timeouts und daraus resultierenden UI-Abbrüchen kommen kann.<br>
            Aus diesem Grund kann das Ergebnis nicht an die neu geladene Benutzeroberfläche übermittelt werden obwohl die Bildgenerierung serverseitig <br>
            vollständig abgeschlossen wird.
            </div>
            <br>

            <div class="info-box">
            <strong>Anwendungsbereich:</strong><br>
            Die <strong>Bild-zu-Bild-Funktion</strong> ermöglicht eine gezielte Bearbeitung eines <strong>beliebigen Objektes oder Bereiches</strong> in einem Bild.<br>
            Sie unterstützt zwei Modi:<br>
            &nbsp;&nbsp;• <strong>Beibehaltung eines ausgewählten Bildbereiches innerhalb eines Rechtecks</strong> (z. B. Gesicht, Objekt, Tier, Gegenstand) bei Veränderung des Rests,<br>
            &nbsp;&nbsp;• oder <strong>Veränderung des rechteckigen Bildbereiches</strong> bei Erhaltung der Umgebung.<br>
            <br>
            <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>
            Die Verwendung eines <strong>Negativ-Promptes</strong> ist sinnvoll, um unerwünschte Veränderungen zu vermeiden.
            </div>
            <br>
            
            <div class="info-box">
            <strong>Empfehlung:</strong><br>
            Für eine präzise Abstimmung der zentralen Parameter – Prompt, Negativ-Prompt, Veränderungsstärke (Strength), Inferenz-Schritte (Steps) <br>
            und Prompt-Stärke (Guidance) – liefern leistungsfähige Sprachmodelle wie GPT, Grok oder DeepSeek hochqualitative, kontextbezogene Vorschläge.<br>
            Prompt und Negativ-Prompt sollten auf <strong>Englisch</strong> eingegeben werden, da "Stable Diffusion" mit Bild-Text-Paaren auf Englisch trainiert <br>
            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>
            Prompteingaben automatisch abgeschnitten werden. Deutsche Wörter werden zwar übersetzt, führen aber zu Verzerrungen.
            </div>
            """
        )

        # --- Button zentriert im unteren Drittel, Taupe-Farbe ---
        with gr.Row():
            with gr.Column(scale=1):  # Linker Leerraum
                pass
            with gr.Column(scale=1, min_width=300):  # Mittig, feste Mindestbreite
                start_btn = gr.Button(
                    "Weiter zur Anwendung",
                    variant="primary",
                    size="lg",
                    elem_id="start-button"
                )
            with gr.Column(scale=1):  # Rechter Leerraum
                pass

        # --- Hauptanwendungsbereich (zunächst versteckt) ---
        with gr.Column(visible=False) as content_area:
            with gr.Tab("Text zu Bild"):
                gr.Markdown("**Beschreibe dein gewünschtes Bild (maximal 77 Token):**")
                
                with gr.Row():
                    txt_input = gr.Textbox(
                        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)",
                        lines=2,
                        label="Prompt (Englisch)",
                        info="Beschreibe detailliert, was du sehen möchtest. Verwende Kommas zur Trennung."
                    )
                
                with gr.Row():
                    with gr.Column():
                        txt_steps = gr.Slider(
                            minimum=10, maximum=100, value=35, step=1,
                            label="Inferenz-Schritte",
                            info="Mehr Schritte = bessere Qualität, aber langsamer (20-50 empfohlen)"
                        )
                    with gr.Column():
                        txt_guidance = gr.Slider(
                            minimum=1.0, maximum=20.0, value=7.5, step=0.5,
                            label="Prompt-Stärke",
                            info="Wie stark der Prompt befolgt wird (7-12 für gute Balance)"
                        )
                
                generate_btn = gr.Button("Bild generieren", variant="primary")
                txt_output = gr.Image(
                    label="Generiertes Bild", 
                    show_download_button=True,
                    type="pil"
                )
                
                generate_btn.click(
                    fn=text_to_image,
                    inputs=[txt_input, txt_steps, txt_guidance],
                    outputs=txt_output,
                    concurrency_limit=1
                )

            with gr.Tab("Bild zu Bild"):
                gr.Markdown("**Lade ein Bild hoch und beschreibe die gewünschte Veränderung:**")
                
                with gr.Row():
                    img_input = gr.Image(
                        type="pil", 
                        label="Eingabebild",
                        height=300,
                        sources=["upload"]  # Nur Upload-Button anzeigen
                    )
                
                with gr.Row():
                    with gr.Column():
                        img_prompt = gr.Textbox(
                            placeholder="change background to beach with palm trees, keep person unchanged, sunny day",
                            lines=2,
                            label="Transformations-Prompt (Englisch - maximal 77 Token)",
                            info="Was soll verändert werden? Sei spezifisch."
                        )
                    with gr.Column():
                        img_neg_prompt = gr.Textbox(
                            placeholder="blurry, deformed, ugly, bad anatomy, extra limbs, poorly drawn hands",
                            lines=2,
                            label="Negativ-Prompt (Englisch - maximal 77 Token)",
                            info="Was soll vermieden werden? Unerwünschte Elemente auflisten."
                        )
                
                with gr.Row():
                    with gr.Column():
                        strength_slider = gr.Slider(
                            minimum=0.1, maximum=0.9, value=0.4, step=0.05,
                            label="Veränderungs-Stärke",
                            info="0.1-0.3: Leichte Anpassungen, 0.4-0.6: Mittlere Veränderungen, 0.7-0.9: Starke Umgestaltung"
                        )
                    with gr.Column():
                        img_steps = gr.Slider(
                            minimum=10, maximum=100, value=35, step=1,
                            label="Inferenz-Schritte",
                            info="Anzahl der Verarbeitungsschritte (25-45 für gute Ergebnisse)"
                        )
                    with gr.Column():
                        img_guidance = gr.Slider(
                            minimum=1.0, maximum=20.0, value=7.5, step=0.5,
                            label="Prompt-Stärke",
                            info="Einfluss des Prompts auf das Ergebnis (6-10 für natürliche Ergebnisse)"
                        )
                
                # GESICHTSOPTIONEN
                with gr.Row():
                    face_preserve = gr.Checkbox(
                        label="Gesicht, Tier, Gegenstand beibehalten", 
                        value=True,
                        info="Aktiviert: Bildelement bleibt erhalten, Hintergrund wird verändert | Deaktiviert: Nur Bildelement wird verändert"
                    )
                
                with gr.Row():
                    gr.Markdown("**Bildelementbereich anpassen**")
                
                with gr.Row():
                    bbox_x1 = gr.Number(
                        label="Links (x1)", 
                        value=100, 
                        precision=0,
                        info="Linke Kante des Gesichtsbereichs"
                    )
                    bbox_y1 = gr.Number(
                        label="Oben (y1)", 
                        value=100, 
                        precision=0,
                        info="Obere Kante des Gesichtsbereichs"
                    )
                    bbox_x2 = gr.Number(
                        label="Rechts (x2)", 
                        value=300, 
                        precision=0,
                        info="Rechte Kante des Gesichtsbereichs"
                    )
                    bbox_y2 = gr.Number(
                        label="Unten (y2)", 
                        value=300, 
                        precision=0,
                        info="Untere Kante des Gesichtsbereichs"
                    )

                with gr.Row():
                   gr.Markdown(
                     "**Achtung:**\n"
                     "• **Automatische Bildelementerkennung** setzt Koordinaten beim Upload\n"  
                     "• **Koordinaten nur bei erkennbaren Verzerrungen anpassen** (Bereiche leicht verschieben)"
                    )
                
                          
                transform_btn = gr.Button("Bild transformieren", variant="primary")
                
                with gr.Row():
                    img_output = gr.Image(
                        label="Transformiertes Bild",
                        show_download_button=True,
                        type="pil"
                    )
                
                # Event-Handler für Bild-Upload
                img_input.change(
                    fn=update_bbox_from_image,
                    inputs=[img_input],
                    outputs=[bbox_x1, bbox_y1, bbox_x2, bbox_y2]
                )
                
                transform_btn.click(
                    fn=img_to_image,
                    inputs=[
                        img_input, img_prompt, img_neg_prompt, 
                        strength_slider, img_steps, img_guidance, 
                        face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2
                    ],
                    outputs=img_output,
                    concurrency_limit=1
                )

        # Sammle alle Info-Komponenten, die versteckt werden sollen
        info_components = []
        for child in demo.children:
            if child != content_area:
                info_components.append(child)

        # Event-Handler für Start-Button
        start_btn.click(
            fn=lambda: gr.update(visible=True),
            inputs=None,
            outputs=content_area
        ).then(
            fn=lambda: [gr.update(visible=False) for _ in info_components],
            inputs=None,
            outputs=info_components
        )

    return demo


if __name__ == "__main__":
    demo = main_ui()
    demo.queue()
    demo.launch(
        server_name="0.0.0.0", 
        server_port=7860,
        max_file_size="10MB",
        show_error=True,
        share=False
    )