File size: 16,916 Bytes
7931060
72f2543
 
aaf6a9f
72f2543
 
 
 
aa29c52
e7d12ee
 
7931060
 
 
 
 
 
 
 
 
72f2543
 
 
 
7931060
 
 
 
72f2543
7931060
 
e7d12ee
72f2543
 
 
 
 
 
 
7931060
 
e7d12ee
aaf6a9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72f2543
e7d12ee
 
36a3e6b
7931060
72f2543
 
36a3e6b
72f2543
 
 
36a3e6b
72f2543
36a3e6b
 
 
 
e7d12ee
 
72f2543
36a3e6b
72f2543
 
 
 
e7d12ee
72f2543
e7d12ee
36a3e6b
e7d12ee
72f2543
e7d12ee
72f2543
e7d12ee
36a3e6b
72f2543
e7d12ee
 
72f2543
36a3e6b
e7d12ee
72f2543
 
 
 
 
 
 
 
 
e7d12ee
 
 
72f2543
 
e7d12ee
72f2543
b5409e4
72f2543
b5409e4
e7d12ee
72f2543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e7d12ee
72f2543
 
 
 
 
 
 
 
e7d12ee
72f2543
e7d12ee
72f2543
 
 
 
 
 
 
 
e7d12ee
 
72f2543
e7d12ee
72f2543
 
e7d12ee
72f2543
e7d12ee
72f2543
 
 
e7d12ee
72f2543
 
 
e7d12ee
72f2543
e7d12ee
72f2543
e7d12ee
72f2543
e7d12ee
 
36a3e6b
 
72f2543
e7d12ee
72f2543
 
 
 
 
 
 
 
 
 
 
7931060
e7d12ee
72f2543
 
e7d12ee
 
 
 
72f2543
e7d12ee
 
72f2543
e7d12ee
72f2543
e7d12ee
 
72f2543
 
 
 
7931060
 
 
72f2543
7931060
72f2543
7931060
 
 
72f2543
 
 
 
 
7931060
 
 
72f2543
7931060
72f2543
7931060
72f2543
7931060
 
 
 
72f2543
 
 
 
 
7931060
72f2543
 
7931060
72f2543
7931060
72f2543
7931060
 
 
 
 
 
72f2543
7931060
72f2543
 
 
7931060
 
 
 
 
 
 
72f2543
 
 
 
 
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
import gradio as gr
# requests jest nadal w requirements.txt, ale nie jest już bezpośrednio używany do głównego wywołania API
import requests
from PIL import Image, ImageOps
import io
import os
import traceback
# Poprawny import dla gradio_client
from gradio_client import Client, file
import uuid
import shutil

print("--- Plik app.py - Start ładowania ---")

# --- Konfiguracja ---
# Odczytanie sekretu (API Token) ustawionego w Space
API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN")

# Sprawdzenie, czy token został załadowany
if not API_TOKEN:
    # Nadal ostrzegamy, ale klient może działać anonimowo
    print("!!! OSTRZEŻENIE: Nie znaleziono sekretu HUGGINGFACE_API_TOKEN. Klient Gradio spróbuje połączyć się anonimowo (mogą obowiązywać limity). !!!")
else:
    print("--- Sekret HUGGINGFACE_API_TOKEN załadowany. ---")

# Profesjonalny prompt, który chcemy uzyskać
LINKEDIN_PROMPT = (
    "linkedin professional profile photo, corporate headshot, high quality, realistic photograph, "
    "person wearing a dark business suit or elegant blouse, plain white background, " # Usunięto potencjalnie problematyczne tagi
    "soft studio lighting, sharp focus, looking at camera, slight smile, natural skin texture"
)

# Adres publicznego Space'a z InstantID
# Używamy identyfikatora Space'a dla gradio_client
TARGET_SPACE_ID = "InstantX/InstantID"

# Kontrolny print, aby sprawdzić, czy zmienna jest ładowana na starcie
# Powinien pojawić się w logach kontenera po restarcie Space'a
print(f"--- Konfiguracja załadowana. Cel dla gradio_client: {TARGET_SPACE_ID} ---")

# --- Logika aplikacji ---
def generate_photo(input_selfie, current_prompt):

    print(f"Otrzymano obrazek typu: {type(input_selfie)}, Oryginalny rozmiar: {input_selfie.size}")
    print(f"Otrzymano prompt (początek): {current_prompt[:100]}...")

    # --- Dodany Krok: Przeskalowanie obrazka wejściowego ---
    try:
        max_size = (1024, 1024) # Maksymalny rozmiar (szerokość, wysokość)
        # Używamy thumbnail, aby zachować proporcje i zmniejszyć tylko jeśli jest większy
        input_selfie_resized = input_selfie.copy() # Pracuj na kopii
        input_selfie_resized.thumbnail(max_size, Image.Resampling.LANCZOS)
        print(f"Obrazek przeskalowany do (maksymalnie) {max_size}. Nowy rozmiar: {input_selfie_resized.size}")
    except Exception as e_resize:
         print(f"BŁĄD podczas skalowania obrazka: {e_resize}")
         traceback.print_exc()
         raise gr.Error(f"Nie można przeskalować obrazka wejściowego: {e_resize}")
    # ---------------------------------------------------------

    # 2. Przygotowanie pliku tymczasowego dla obrazka wejściowego
    # (Używamy teraz przeskalowanego obrazka: input_selfie_resized)
    temp_dir = f"temp_input_{uuid.uuid4()}"
    input_image_path = None
    try:
        os.makedirs(temp_dir, exist_ok=True)
        input_image_path = os.path.join(temp_dir, "input_selfie.jpg")

        # Upewnijmy się, że obraz jest w RGB przed zapisem jako JPEG
        rgb_image = input_selfie_resized # Używamy przeskalowanego!
        if rgb_image.mode == 'RGBA':
            print("Konwertuję przeskalowany obraz RGBA na RGB z białym tłem...")
            # Stwórz nowy obraz RGB wypełniony białym kolorem
            final_image = Image.new("RGB", rgb_image.size, (255, 255, 255))
            # Wklej oryginalny obraz używając jego kanału alfa jako maski
            final_image.paste(rgb_image, mask=rgb_image.split()[3])
        else:
             final_image = rgb_image # Już jest w odpowiednim trybie

        # Zapisz przeskalowany i skonwertowany obraz jako JPEG
        final_image.save(input_image_path, format="JPEG")
        print(f"Przeskalowany obrazek wejściowy zapisany tymczasowo w: {input_image_path}")

    except Exception as e:
        # ... (reszta obsługi błędu zapisu bez zmian) ...

    # 3. Wywołanie zdalnego API Gradio za pomocą gradio_client
    # ... (reszta kodu bez zmian, aż do obsługi błędu AppError) ...

    except Exception as e:
        print(f"BŁĄD podczas komunikacji z klientem Gradio lub przetwarzania wyniku: {e}")
        traceback.print_exc()
        # Dodajmy podpowiedź o możliwej przyczynie błędu AppError
        error_message = f"Problem podczas generowania przez zdalny serwis InstantID: {e}"
        if isinstance(e, AppError): # Sprawdzamy czy to ten konkretny błąd
             error_message = f"Zdalny serwis ({TARGET_SPACE_ID}) zgłosił wewnętrzny błąd. Najczęstszą przyczyną są problemy z obrazkiem wejściowym (np. brak wykrytej twarzy, nietypowy format) lub przeciążenie serwisu. Spróbuj z innym zdjęciem lub ponownie później."
        elif "Could not fetch config" in str(e):
             # ... (reszta obsługi błędów bez zmian)

        raise gr.Error(error_message) # Pokaż (potencjalnie bardziej pomocny) błąd
    
    # Ta funkcja jest teraz wywoływana przez Gradio po kliknięciu przycisku
    print("\n--- Funkcja generate_photo (gradio_client) została wywołana ---")

    # 1. Walidacja danych wejściowych
    if input_selfie is None:
        print("BŁĄD: Nie wgrano zdjęcia wejściowego.")
        raise gr.Error("Proszę najpierw wgrać swoje selfie!") # Pokaż błąd w interfejsie
    if not current_prompt:
        print("BŁĄD: Prompt jest pusty.")
        raise gr.Error("Prompt (opis zdjęcia) nie może być pusty!")
    # Informacja o tokenie (nie jest już krytyczny dla działania, ale może wpływać na limity)
    if not API_TOKEN:
         print("INFO: Brak API Tokena. Połączenie z publicznym Space jako anonimowy użytkownik.")

    print(f"Otrzymano obrazek typu: {type(input_selfie)}, Rozmiar: {input_selfie.size}")
    print(f"Otrzymano prompt (początek): {current_prompt[:100]}...")

    # 2. Przygotowanie pliku tymczasowego dla obrazka wejściowego
    temp_dir = f"temp_input_{uuid.uuid4()}" # Unikalny folder tymczasowy
    input_image_path = None # Inicjalizacja na wypadek błędu przed przypisaniem
    try:
        os.makedirs(temp_dir, exist_ok=True)
        # Pełna ścieżka do pliku tymczasowego
        input_image_path = os.path.join(temp_dir, "input_selfie.jpg")

        # Upewnijmy się, że obraz jest w RGB przed zapisem jako JPEG
        # To ważne, bo format JPEG nie obsługuje przezroczystości (kanału alfa)
        rgb_image = input_selfie
        if input_selfie.mode == 'RGBA':
            print("Konwertuję obraz RGBA na RGB z białym tłem przed zapisem...")
            # Stwórz nowy obraz RGB wypełniony białym kolorem
            rgb_image = Image.new("RGB", input_selfie.size, (255, 255, 255))
            # Wklej oryginalny obraz używając jego kanału alfa jako maski
            rgb_image.paste(input_selfie, mask=input_selfie.split()[3])

        # Zapisz przetworzony obraz jako JPEG
        rgb_image.save(input_image_path, format="JPEG")
        print(f"Obrazek wejściowy zapisany tymczasowo w: {input_image_path}")

    except Exception as e:
        print(f"BŁĄD podczas zapisywania obrazu tymczasowego: {e}")
        traceback.print_exc() # Pokaż pełny błąd w logach
        # Spróbuj posprzątać folder tymczasowy nawet jeśli zapis się nie udał
        if temp_dir and os.path.exists(temp_dir):
             try:
                 shutil.rmtree(temp_dir)
                 print(f"Folder tymczasowy {temp_dir} usunięty po błędzie zapisu.")
             except Exception as e_clean:
                 print(f"OSTRZEŻENIE: Nie udało się usunąć folderu tymczasowego {temp_dir} po błędzie zapisu: {e_clean}")
        # Pokaż błąd użytkownikowi
        raise gr.Error(f"Problem z przygotowaniem obrazu do wysłania: {e}")

    # 3. Wywołanie zdalnego API Gradio za pomocą gradio_client
    output_image = None # Zmienna na wynikowy obrazek
    client = None # Zmienna na obiekt klienta
    try:
        # Używamy TARGET_SPACE_ID zdefiniowanej globalnie
        print(f"Łączenie z docelowym Space Gradio: {TARGET_SPACE_ID}")
        # Inicjalizujemy klienta, przekazując ID Space'a i token (jeśli jest)
        client = Client(TARGET_SPACE_ID, hf_token=API_TOKEN)

        print("Połączono. Próbuję wywołać funkcję na zdalnym Space...")

        # --- Konfiguracja parametrów dla InstantX/InstantID ---
        # Te wartości mogą wymagać dostosowania w zależności od dokładnej
        # konfiguracji zdalnego Space'a i pożądanych efektów.
        negative_prompt = "ugly, deformed, noisy, blurry, low contrast, text, signature, watermark, duplicate, multiple people, cartoon, drawing, illustration, sketch"
        # Popularne style w demo InstantID: Realistic, (No style), Comic book, Disney, Pixar
        style_name = "Realistic"
        # Skale ControlNet i IP-Adapter (siła tożsamości) - typowe wartości to 0.6-1.0
        cn_scale = 0.8
        ip_scale = 0.8
        # Nazwa endpointu API w zdalnym Space'u (często '/predict' lub specyficzna jak '/generate_image')
        # Dla InstantX/InstantID wydaje się, że to '/generate_image'
        api_endpoint_name = "/generate_image"
        # -------------------------------------------------------

        print(f"Wywołuję endpoint '{api_endpoint_name}' z parametrami:")
        print(f"  Input image path: {input_image_path}")
        # Nie drukujemy całego promptu, bo może być długi
        print(f"  Prompt (start): {current_prompt[:60]}...")
        print(f"  Negative Prompt (start): {negative_prompt[:60]}...")
        print(f"  Style: {style_name}")
        print(f"  ControlNet Scale: {cn_scale}")
        print(f"  IP-Adapter Scale: {ip_scale}")

        # Wywołanie funkcji 'predict' na zdalnym kliencie
        result = client.predict(
                        file(input_image_path),  # Obraz twarzy (jako obiekt pliku Gradio)
                        None,                    # Obraz pozy (opcjonalny, dajemy None)
                        current_prompt,          # Prompt tekstowy
                        negative_prompt,         # Negatywny prompt
                        style_name,              # Nazwa stylu
                        cn_scale,                # Skala ControlNet
                        ip_scale,                # Skala IP-Adapter (siła tożsamości)
                        api_name=api_endpoint_name # Nazwa endpointu API
        )

        # Przetwarzanie wyniku zwróconego przez klienta
        print(f"Otrzymano wynik od klienta: {type(result)}")
        # Wydrukuj fragment wyniku, aby zobaczyć jego strukturę
        print(f"Wynik (fragment): {str(result)[:500]}")

        # Sprawdzamy, czy wynik jest listą ścieżek do plików (typowe dla Gradio)
        if isinstance(result, list) and len(result) > 0 and isinstance(result[0], str) and os.path.exists(result[0]):
            output_file_path = result[0] # Bierzemy pierwszą ścieżkę z listy
            print(f"Przetwarzam pierwszy obrazek wynikowy ze ścieżki: {output_file_path}")
            # Wczytaj obrazek wynikowy z tej ścieżki za pomocą Pillow
            output_image = Image.open(output_file_path)
            print(f"Obrazek wynikowy załadowany pomyślnie. Rozmiar: {output_image.size}")
        # Sprawdzamy, czy wynik jest pojedynczą ścieżką do pliku
        elif isinstance(result, str) and os.path.exists(result):
             output_file_path = result
             print(f"Przetwarzam obrazek wynikowy ze ścieżki: {output_file_path}")
             output_image = Image.open(output_file_path)
             print(f"Obrazek wynikowy załadowany pomyślnie. Rozmiar: {output_image.size}")
        else:
            # Jeśli wynik nie jest ani listą ścieżek, ani pojedynczą ścieżką
            print(f"BŁĄD: Otrzymano nieoczekiwany format wyniku od gradio_client: {type(result)}")
            raise gr.Error(f"Nie udało się przetworzyć wyniku ze zdalnego API. Otrzymano: {str(result)[:200]}")

    except Exception as e:
        # Obsługa wszelkich błędów podczas komunikacji z klientem lub przetwarzania wyniku
        print(f"BŁĄD podczas komunikacji z klientem Gradio lub przetwarzania wyniku: {e}")
        traceback.print_exc() # Pokaż pełny błąd w logach
        # Spróbuj dać użytkownikowi bardziej pomocny komunikat
        error_message = f"Problem podczas generowania przez zdalny serwis InstantID: {e}"
        if "Could not fetch config" in str(e):
             error_message = f"Nie można połączyć się z konfiguracją zdalnego serwisu ({TARGET_SPACE_ID}). Może być chwilowo niedostępny, przeciążony lub wymagać logowania. Spróbuj ponownie później."
        elif "timed out" in str(e).lower():
             error_message = "Przekroczono limit czasu oczekiwania na odpowiedź ze zdalnego serwisu. Może być przeciążony. Spróbuj ponownie."
        elif "queue full" in str(e).lower():
             error_message = "Kolejka w zdalnym serwisie jest pełna. Spróbuj ponownie za chwilę."
        # Pokaż błąd w interfejsie Gradio
        raise gr.Error(error_message)

    finally:
        # 4. Sprzątanie - ZAWSZE próbuj usunąć folder tymczasowy, nawet jeśli był błąd
        if temp_dir and os.path.exists(temp_dir):
            try:
                shutil.rmtree(temp_dir)
                print(f"Folder tymczasowy {temp_dir} usunięty.")
            except Exception as e_clean:
                # Tylko ostrzeżenie, jeśli sprzątanie się nie uda
                print(f"OSTRZEŻENIE: Nie udało się usunąć folderu tymczasowego {temp_dir}: {e_clean}")

    # 5. Zwróć wynik (załadowany obrazek PIL), jeśli się udało
    if output_image:
        print("Zwracam wygenerowany obrazek do interfejsu Gradio.")
        return output_image
    else:
        # Ten kod nie powinien zostać osiągnięty, jeśli błędy są poprawnie obsługiwane
        print("BŁĄD KRYTYCZNY: Brak obrazka wynikowego po zakończeniu funkcji, a nie zgłoszono błędu.")
        raise gr.Error("Nie udało się uzyskać obrazka wynikowego z nieznanego powodu.")


# --- Budowa Interfejsu Gradio ---
print("--- Definiowanie interfejsu Gradio ---")
# Tworzymy główny blok aplikacji Gradio
with gr.Blocks(css="footer {display: none !important}") as demo: # Ukrywamy domyślny footer Gradio
    # Tytuł i opis aplikacji wyświetlany użytkownikowi
    gr.Markdown(
        """
        # Generator Profesjonalnych Zdjęć Profilowych
        Wgraj swoje selfie, a my (korzystając z modelu InstantID) postaramy się stworzyć profesjonalne zdjęcie w stylu LinkedIn!
        **Wskazówki:**
        *   Użyj wyraźnego zdjęcia twarzy, patrzącej w miarę prosto, dobrze oświetlonej.
        *   Unikaj zdjęć grupowych, bardzo małych lub z mocno zasłoniętą twarzą.
        *   Generowanie może potrwać od 30 sekund do kilku minut, bądź cierpliwy!
        """
    )

    # Układ interfejsu w dwóch kolumnach
    with gr.Row():
        # Kolumna lewa (wejście)
        with gr.Column(scale=1):
            # Komponent do wgrywania obrazka
            input_image = gr.Image(
                label="1. Wgraj swoje selfie (JPG/PNG)",
                type="pil" # Chcemy obiekt PIL w funkcji Pythona
            )
            # Pole tekstowe do wpisania promptu (opisu zdjęcia)
            prompt_input = gr.Textbox(
                label="2. Opis pożądanego zdjęcia (prompt)",
                value=LINKEDIN_PROMPT, # Ustawiamy domyślny prompt
                lines=4 # Określamy wysokość pola
            )
            # Przycisk uruchamiający generowanie
            generate_button = gr.Button("✨ Generuj Zdjęcie Biznesowe ✨", variant="primary") # Wyróżniony przycisk

        # Kolumna prawa (wyjście)
        with gr.Column(scale=1):
            # Komponent do wyświetlania wyniku (obrazka)
            output_image = gr.Image(
                label="Oto Twoje wygenerowane zdjęcie:",
                type="pil" # Oczekujemy obiektu PIL jako wynik
            )

    # --- Podłączenie akcji do przycisku ---
    # Definiujemy, co ma się stać po kliknięciu przycisku 'generate_button'
    generate_button.click(
        fn=generate_photo,                   # Wywołaj funkcję 'generate_photo'
        inputs=[input_image, prompt_input],  # Przekaż zawartość 'input_image' i 'prompt_input' jako argumenty
        outputs=[output_image]               # Wynik funkcji umieść w komponencie 'output_image'
    )

print("--- Interfejs Gradio zdefiniowany ---")

# --- Uruchomienie aplikacji ---
if __name__ == "__main__":
    print("--- Uruchamianie demo.launch() ---")
    # Uruchamiamy aplikację Gradio
    # share=False: nie generuj publicznego linku (zalecane dla bezpieczeństwa)
    # debug=False: wyłącz tryb debugowania Gradio (zalecane dla produkcji)
    demo.launch(share=False, debug=False)
    print("--- Aplikacja Gradio zakończyła działanie (jeśli nie jest w trybie ciągłym serwera) ---")