PaulinaAa's picture
Update app.py
aaf6a9f verified
raw
history blame
16.9 kB
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) ---")