Spaces:
Running
Running
| import gradio as gr | |
| from transformers import pipeline | |
| from PIL import Image | |
| import logging | |
| from datetime import datetime | |
| from functools import lru_cache | |
| import hashlib | |
| from typing import Tuple, Optional, Dict, Any | |
| from collections import defaultdict, deque | |
| from threading import Lock | |
| import traceback | |
| # ==================== KONFIGURACJA ==================== | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(levelname)s - %(message)s', | |
| handlers=[ | |
| logging.FileHandler('skin_analysis.log'), | |
| logging.StreamHandler() | |
| ] | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Limity bezpieczeństwa | |
| MAX_IMAGE_DIMENSION = 4096 | |
| ALLOWED_FORMATS = {'PNG', 'JPEG', 'JPG', 'WEBP'} | |
| RATE_LIMIT_WINDOW = 60 # sekund | |
| MAX_REQUESTS_PER_WINDOW = 10 | |
| Image.MAX_IMAGE_PIXELS = 89478485 # ~178 megapikseli | |
| # ==================== RATE LIMITER ==================== | |
| class RateLimiter: | |
| """Thread-safe rate limiter z automatycznym czyszczeniem""" | |
| def __init__(self, max_requests: int = MAX_REQUESTS_PER_WINDOW, | |
| window: int = RATE_LIMIT_WINDOW): | |
| self.max_requests = max_requests | |
| self.window = window | |
| self.requests = defaultdict(deque) | |
| self.lock = Lock() | |
| self.last_cleanup = datetime.now().timestamp() | |
| def _hash_identifier(self, identifier: str) -> str: | |
| """Hashuje identyfikator dla prywatności""" | |
| return hashlib.sha256(identifier.encode()).hexdigest()[:12] | |
| def _cleanup_old_entries(self, current_time: float): | |
| """Okresowe czyszczenie starych wpisów (co 5 minut)""" | |
| if current_time - self.last_cleanup > 300: # 5 minut | |
| expired_keys = [ | |
| key for key, timestamps in self.requests.items() | |
| if not timestamps or current_time - timestamps[-1] > self.window * 2 | |
| ] | |
| for key in expired_keys: | |
| del self.requests[key] | |
| self.last_cleanup = current_time | |
| if expired_keys: | |
| logger.info(f"Cleaned up {len(expired_keys)} expired rate limit entries") | |
| def is_allowed(self, identifier: str) -> bool: | |
| """Sprawdza czy żądanie jest dozwolone""" | |
| with self.lock: | |
| now = datetime.now().timestamp() | |
| hashed_id = self._hash_identifier(identifier) | |
| # Usuń stare żądania dla tego użytkownika | |
| while (self.requests[hashed_id] and | |
| now - self.requests[hashed_id][0] > self.window): | |
| self.requests[hashed_id].popleft() | |
| # Sprawdź limit | |
| if len(self.requests[hashed_id]) >= self.max_requests: | |
| logger.warning(f"Rate limit exceeded for user {hashed_id}") | |
| return False | |
| # Dodaj nowe żądanie | |
| self.requests[hashed_id].append(now) | |
| # Okresowe czyszczenie | |
| self._cleanup_old_entries(now) | |
| return True | |
| # Globalna instancja rate limitera | |
| rate_limiter = RateLimiter() | |
| # ==================== MODEL ==================== | |
| def load_model(): | |
| """Załaduj model z cache'owaniem""" | |
| try: | |
| logger.info("Loading skin classification model...") | |
| model = pipeline( | |
| "image-classification", | |
| model="Anwarkh1/Skin_Cancer-Image_Classification" | |
| ) | |
| logger.info("Model loaded successfully") | |
| return model | |
| except Exception as e: | |
| logger.error(f"Failed to load model: {str(e)}") | |
| raise | |
| # Inicjalizacja modelu | |
| try: | |
| skin_model = load_model() | |
| except Exception as e: | |
| logger.critical("Application startup failed - model loading error") | |
| skin_model = None | |
| # ==================== DEFINICJE ZMIAN SKÓRNYCH ==================== | |
| SKIN_CONDITIONS: Dict[str, Dict[str, str]] = { | |
| "Actinic keratoses": { | |
| "name": "Rogowacenie słoneczne", | |
| "risk": "medium", | |
| "description": "Rogowacenie słoneczne to stan przednowotworowy, zmiana rozwijająca się pod wpływem przewlekłej ekspozycji na promieniowanie ultrafioletowe.", | |
| "recommendation": "Wymaga kontroli dermatologicznej – ryzyko progresji pojedynczego ogniska do raka kolczystokomórkowego (SCC) jest niskie, ale występowanie wielu zmian zwiększa całkowite ryzyko." | |
| }, | |
| "Basal cell carcinoma": { | |
| "name": "Rak podstawnokomórkowy", | |
| "risk": "high", | |
| "description": "Najczęstszy nowotwór złośliwy u osób rasy kaukaskiej. Charakteryzuje się naciekaniem i niszczeniem sąsiadujących tkanek, co może prowadzić do poważnych defektów estetycznych.", | |
| "recommendation": "PILNIE skonsultuj się z dermatologiem." | |
| }, | |
| "Dermatofibroma": { | |
| "name": "Włókniak twardy", | |
| "risk": "low", | |
| "description": "Łagodna zmiana nowotworowa, zbudowana z tkanki włóknistej.", | |
| "recommendation": "Zwykle nie wymaga leczenia, zalecana konsultacja dermatologiczna w celu potwierdzenia rozpoznania." | |
| }, | |
| "Melanoma": { | |
| "name": "Czerniak", | |
| "risk": "high", | |
| "description": "Złośliwy nowotwór skóry wywodzący się z melanocytów. Jeden z najbardziej agresywnych nowotworów skóry, o wysokim potencjale przerzutowym.", | |
| "recommendation": "NATYCHMIAST skonsultuj się z dermatologiem! Obserwuj zmiany według reguły ABCDE." | |
| }, | |
| "Nevus": { | |
| "name": "Znamię", | |
| "risk": "low", | |
| "description": "Łagodna zmiana barwnikowa skóry, zbudowana z komórek barwnikowych (melanocytów).", | |
| "recommendation": "Obserwuj zmiany według reguły ABCDE, skonsultuj się z dermatologiem w przypadku jakichkolwiek zmian wyglądu." | |
| }, | |
| "Pigmented benign keratosis": { | |
| "name": "Rogowacenie łojotokowe (barwnikowe)", | |
| "risk": "low", | |
| "description": "Zmiana łagodna, niezłośliwa, nie ulega transformacji nowotworowej. Wariant rogowacenia łojotokowego z nasilonym przebarwieniem.", | |
| "recommendation": "Może klinicznie przypominać czerniaka, zaleca się konsultację dermatologiczną." | |
| }, | |
| "Seborrheic keratosis": { | |
| "name": "Rogowacenie łojotokowe", | |
| "risk": "low", | |
| "description": "Zmiana łagodna, niezłośliwa, nie ulega transformacji nowotworowej.", | |
| "recommendation": "Może klinicznie przypominać czerniaka, zaleca się konsultację dermatologiczną." | |
| }, | |
| "Squamous cell carcinoma": { | |
| "name": "Rak kolczystokomórkowy", | |
| "risk": "high", | |
| "description": "Drugi najczęstszy typ raka skóry, charakteryzuje się powolnym wzrostem i zdolnością do tworzenia przerzutów.", | |
| "recommendation": "PILNIE skonsultuj się z dermatologiem." | |
| } | |
| } | |
| # ==================== WALIDACJA OBRAZÓW ==================== | |
| def validate_image(image: Image.Image) -> Tuple[bool, str]: | |
| """Kompleksowa walidacja obrazu wejściowego""" | |
| try: | |
| # Sprawdź czy obraz jest prawidłowym obiektem PIL | |
| if not isinstance(image, Image.Image): | |
| return False, "❌ Nieprawidłowy format danych obrazu" | |
| # Sprawdź format | |
| if image.format and image.format.upper() not in ALLOWED_FORMATS: | |
| return False, f"❌ Nieobsługiwany format. Dozwolone: {', '.join(ALLOWED_FORMATS)}" | |
| # Sprawdź wymiary | |
| width, height = image.size | |
| if width < 10 or height < 10: | |
| return False, "❌ Obraz zbyt mały (minimum 10x10 pikseli)" | |
| if width > MAX_IMAGE_DIMENSION or height > MAX_IMAGE_DIMENSION: | |
| return False, f"❌ Obraz zbyt duży. Maksymalna rozdzielczość: {MAX_IMAGE_DIMENSION}x{MAX_IMAGE_DIMENSION}px" | |
| # Sprawdź całkowitą liczbę pikseli | |
| if width * height > Image.MAX_IMAGE_PIXELS: | |
| return False, "❌ Obraz przekracza bezpieczny limit pikseli" | |
| # Sprawdź tryb koloru | |
| if image.mode not in ['RGB', 'RGBA', 'L', 'P']: | |
| return False, "❌ Nieprawidłowy tryb koloru obrazu" | |
| # Weryfikuj integralność obrazu (używamy kopii) | |
| try: | |
| img_copy = image.copy() | |
| img_copy.verify() | |
| except Exception as e: | |
| return False, f"❌ Uszkodzony obraz: {str(e)}" | |
| return True, "OK" | |
| except Exception as e: | |
| logger.error(f"Image validation error: {str(e)}") | |
| return False, f"❌ Błąd podczas walidacji: {str(e)}" | |
| def sanitize_image(image: Image.Image) -> Image.Image: | |
| """Sanityzacja i optymalizacja obrazu""" | |
| try: | |
| # Konwertuj do RGB | |
| if image.mode != 'RGB': | |
| logger.info(f"Converting image from {image.mode} to RGB") | |
| image = image.convert('RGB') | |
| # Zmniejsz jeśli zbyt duży | |
| max_size = (MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION) | |
| if image.size[0] > max_size[0] or image.size[1] > max_size[1]: | |
| original_size = image.size | |
| image.thumbnail(max_size, Image.Resampling.LANCZOS) | |
| logger.info(f"Image resized from {original_size} to {image.size}") | |
| return image | |
| except Exception as e: | |
| logger.error(f"Image sanitization error: {str(e)}") | |
| raise | |
| # ==================== FORMATOWANIE WYNIKÓW ==================== | |
| def get_risk_indicator(risk_level: str) -> str: | |
| """Zwraca kolorowy wskaźnik ryzyka""" | |
| risk_map = { | |
| "high": "🔴 WYSOKIE RYZYKO", | |
| "medium": "🟡 ŚREDNIE RYZYKO", | |
| "low": "🟢 NISKIE RYZYKO" | |
| } | |
| return risk_map.get(risk_level, "⚪ NIEZNANE RYZYKO") | |
| def format_analysis_result(result: list) -> Tuple[str, str]: | |
| """Formatuje wynik analizy do wyświetlenia""" | |
| if not result or len(result) == 0: | |
| return "❌ Brak wyników analizy", "" | |
| output = "# 🔬 WYNIK ANALIZY ZMIANY SKÓRNEJ\n\n" | |
| # Główny wynik | |
| top_result = result[0] | |
| top_condition = SKIN_CONDITIONS.get(top_result['label'], { | |
| 'name': top_result['label'], | |
| 'risk': 'medium', | |
| 'description': 'Nieznana zmiana skórna', | |
| 'recommendation': 'Skonsultuj się z dermatologiem' | |
| }) | |
| risk_indicator = get_risk_indicator(top_condition['risk']) | |
| output += f"## 🎯 NAJBARDZIEJ PRAWDOPODOBNE:\n" | |
| output += f"**{top_condition['name']}** ({top_result['label']})\n\n" | |
| output += f"**Prawdopodobieństwo:** {top_result['score']:.1%}\n\n" | |
| output += f"**Poziom ryzyka:** {risk_indicator}\n\n" | |
| output += f"### 📋 Opis:\n" | |
| output += f"{top_condition['description']}\n\n" | |
| output += f"### 💡 Zalecenie:\n" | |
| output += f"{top_condition['recommendation']}\n\n" | |
| # Wszystkie wyniki | |
| output += "## 📊 SZCZEGÓŁOWE PRAWDOPODOBIEŃSTWA:\n\n" | |
| for i, r in enumerate(result, 1): | |
| condition = SKIN_CONDITIONS.get(r['label'], {'name': r['label'], 'risk': 'medium'}) | |
| risk_emoji = "🔴" if condition['risk'] == 'high' else "🟡" if condition['risk'] == 'medium' else "🟢" | |
| output += f"{i}. {risk_emoji} **{condition['name']}** – {r['score']:.2%}\n" | |
| # Ostrzeżenia bezpieczeństwa | |
| safety_warning = """ | |
| --- | |
| ## ⚠️ WAŻNE OSTRZEŻENIA BEZPIECZEŃSTWA | |
| ### 🩺 Ta aplikacja NIE ZASTĘPUJE konsultacji lekarskiej! | |
| - Wyniki mają charakter orientacyjny i mogą zawierać błędy | |
| - Dokładność modelu nie została klinicznie zwalidowana | |
| - Ostateczną diagnozę może postawić tylko lekarz dermatolog | |
| - W przypadku jakichkolwiek wątpliwości - skonsultuj się z dermatologiem | |
| ### 🔍 Zasada ABCDE dla znamion | |
| - **Asymetry** Asymetria - połówki znamienia różnią się | |
| - **Borders** Brzegi nieregularne - postrzępione, rozmazane | |
| - **Color** Kolor niejednolity - różne odcienie w jednym znamieniu | |
| - **Diamater** Średnica > 6mm - większa niż gumka na ołówku | |
| - **Evolution** Ewolucja - zmiany wielkości, kształtu lub koloru | |
| ### 📞 Kiedy natychmiast do lekarza | |
| - Zmiany rosnące, krwawiące, swędzące lub bolesne | |
| - Nowe znamiona po 30. roku życia | |
| - Zmiany wyglądu istniejących znamion | |
| - Owrzodzenia, które nie goją się przez 4 tygodnie | |
| """ | |
| return output, safety_warning | |
| # ==================== GŁÓWNA FUNKCJA ANALIZY ==================== | |
| def analyze_skin(image: Optional[Image.Image], request: gr.Request) -> Tuple[str, Optional[Image.Image], str]: | |
| """Główna funkcja analizy zmiany skórnej""" | |
| # Sprawdź czy model jest załadowany | |
| if skin_model is None: | |
| logger.error("Model not available") | |
| return "❌ Model niedostępny. Skontaktuj się z administratorem.", None, "" | |
| # Sprawdź czy obraz został wgrany | |
| if image is None: | |
| return "❌ Proszę wgrać zdjęcie", None, "" | |
| # Rate limiting | |
| client_id = request.client.host if request and hasattr(request, 'client') else "unknown" | |
| if not rate_limiter.is_allowed(client_id): | |
| return "⏳ Zbyt wiele żądań. Proszę poczekać chwilę (max 10 analiz na minutę).", None, "" | |
| try: | |
| # Walidacja obrazu | |
| is_valid, validation_msg = validate_image(image) | |
| if not is_valid: | |
| logger.warning(f"Invalid image from user: {validation_msg}") | |
| return validation_msg, None, "" | |
| # Sanityzacja obrazu | |
| image = sanitize_image(image) | |
| # Logowanie analizy | |
| image_hash = hashlib.sha256(image.tobytes()).hexdigest()[:12] | |
| logger.info(f"Analyzing image {image_hash} from user {rate_limiter._hash_identifier(client_id)}") | |
| # Analiza zmiany skórnej | |
| try: | |
| result = skin_model(image) | |
| except Exception as model_error: | |
| logger.error(f"Model error for image {image_hash}: {str(model_error)}") | |
| return f"❌ Błąd modelu: {str(model_error)}", None, "" | |
| # Walidacja wyniku | |
| if not isinstance(result, list) or len(result) == 0: | |
| logger.error(f"Invalid model output for image {image_hash}: {type(result)}") | |
| return "❌ Model zwrócił nieprawidłowe dane. Spróbuj ponownie.", None, "" | |
| # Formatuj wynik | |
| output, safety_warning = format_analysis_result(result) | |
| logger.info(f"Analysis completed for image {image_hash}: {result[0]['label']} ({result[0]['score']:.2%})") | |
| return output, image, safety_warning | |
| except Exception as e: | |
| logger.error(f"Analysis error: {str(e)}", exc_info=True) | |
| error_trace = traceback.format_exc() | |
| return f"❌ Wystąpił błąd podczas analizy:\n\n```\n{error_trace}\n```", None, "" | |
| # ==================== FUNKCJE UI ==================== | |
| def clear_all(): | |
| """Funkcja czyszcząca wszystkie elementy interfejsu""" | |
| return ( | |
| None, # image_input | |
| "Wgraj zdjęcie i kliknij 'Analizuj' aby rozpocząć", # result_output | |
| None, # uploaded_image | |
| "", # safety_output | |
| gr.update(visible=True), # instructions | |
| gr.update(visible=False), # clear_btn | |
| gr.update(visible=True) # analyze_btn | |
| ) | |
| def show_uploaded_image(img): | |
| """Pokazuje przesłany obraz po analizie""" | |
| if img is not None: | |
| return gr.update(visible=True, value=img) | |
| return gr.update(visible=False) | |
| # ==================== INTERFEJS GRADIO ==================== | |
| with gr.Blocks( | |
| theme=gr.themes.Soft(), | |
| title="🔬 Klasyfikator Zmian Skórnych", | |
| css=""" | |
| .gradio-container {max-width: 1200px; margin: auto;} | |
| #clear_btn { | |
| background: linear-gradient(to bottom right, #dc2626, #b91c1c) !important; | |
| border: none !important; | |
| color: white !important; | |
| } | |
| #clear_btn:hover { | |
| background: linear-gradient(to bottom right, #b91c1c, #991b1b) !important; | |
| transform: scale(1.02); | |
| box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4) !important; | |
| } | |
| /* === Sekcja Informacyjna (Niebieska) === */ | |
| .info-section { | |
| background: #f0f9ff !important; | |
| border-radius: 12px !important; | |
| padding: 2rem !important; | |
| border-left: 4px solid #3b82f6 !important; | |
| box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1) !important; | |
| margin: 1rem 0 !important; | |
| } | |
| /* Uniwersalne nadpisanie dla Safari i innych przeglądarek */ | |
| .info-section *, | |
| .info-section > div, | |
| .info-section .markdown, | |
| .info-section div[data-testid="markdown-body"] { | |
| background-color: transparent !important; | |
| } | |
| .info-section p, | |
| .info-section h2, | |
| .info-section h3, | |
| .info-section ul, | |
| .info-section li, | |
| .info-section strong { | |
| color: #1e40af !important; | |
| } | |
| /* === Sekcja Prywatności (Zielona) === */ | |
| .privacy-section { | |
| background: #f0fdf4 !important; | |
| border-radius: 12px !important; | |
| padding: 2rem !important; | |
| border-left: 4px solid #10b981 !important; | |
| box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1) !important; | |
| margin: 1rem 0 !important; | |
| } | |
| /* Uniwersalne nadpisanie dla Safari i innych przeglądarek */ | |
| .privacy-section *, | |
| .privacy-section > div, | |
| .privacy-section .markdown, | |
| .privacy-section div[data-testid="markdown-body"] { | |
| background-color: transparent !important; | |
| } | |
| .privacy-section p, | |
| .privacy-section h2, | |
| .privacy-section h3, | |
| .privacy-section ul, | |
| .privacy-section li, | |
| .privacy-section strong { | |
| color: #065f46 !important; | |
| } | |
| /* === Sekcja Prawna (Czerwona) === */ | |
| .legal-section { | |
| background: #fef2f2 !important; | |
| border-radius: 12px !important; | |
| padding: 2rem !important; | |
| border-left: 4px solid #dc2626 !important; | |
| box-shadow: 0 2px 8px rgba(220, 38, 38, 0.2) !important; | |
| margin: 1rem 0 !important; | |
| } | |
| /* Uniwersalne nadpisanie dla Safari i innych przeglądarek */ | |
| .legal-section *, | |
| .legal-section > div, | |
| .legal-section .markdown, | |
| .legal-section div[data-testid="markdown-body"] { | |
| background-color: transparent !important; | |
| } | |
| .legal-section p, | |
| .legal-section h2, | |
| .legal-section h3, | |
| .legal-section ul, | |
| .legal-section li, | |
| .legal-section strong { | |
| color: #991b1b !important; | |
| } | |
| """ | |
| ) as demo: | |
| gr.Markdown(""" | |
| # 🔬 Klasyfikator Zmian Skórnych - Detekcja Raka Skóry | |
| **⚠️ WYŁĄCZNIE DO CELÓW EDUKACYJNYCH - NIE STANOWI PORADY MEDYCZNEJ** | |
| Zaawansowana analiza AI wykrywająca 8 typów zmian skórnych: | |
| - Czerniak (Melanoma) 🔴 | |
| - Rak podstawnokomórkowy 🔴 | |
| - Rak kolczystokomórkowy 🔴 | |
| - Rogowacenie słoneczne 🟡 | |
| - Znamiona i inne łagodne zmiany 🟢 | |
| **Formaty:** JPG, PNG, WEBP | **Rate limit:** 10 analiz/minutę | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| image_input = gr.Image( | |
| type="pil", | |
| label="📸 Wgraj zdjęcie zmiany skórnej", | |
| height=400, | |
| sources=["upload"], | |
| show_download_button=False, | |
| show_share_button=False | |
| ) | |
| with gr.Row(): | |
| analyze_btn = gr.Button( | |
| "🔍 ANALIZUJ ZMIANĘ SKÓRNĄ", | |
| variant="primary", | |
| size="lg", | |
| scale=2 | |
| ) | |
| clear_btn = gr.Button( | |
| "🗑️ WYCZYŚĆ", | |
| size="lg", | |
| scale=1, | |
| visible=False, | |
| elem_id="clear_btn" | |
| ) | |
| with gr.Column(): | |
| result_output = gr.Markdown( | |
| label="🔎 Wynik analizy", | |
| value="Wgraj zdjęcie i kliknij 'Analizuj' aby rozpocząć" | |
| ) | |
| with gr.Row(): | |
| uploaded_image = gr.Image( | |
| label="📷 Przesłane zdjęcie", | |
| height=300, | |
| interactive=False, | |
| visible=False, | |
| show_download_button=False, | |
| show_share_button=False | |
| ) | |
| safety_output = gr.Markdown(label="⚠️ Ostrzeżenia bezpieczeństwa") | |
| instructions = gr.Markdown(""" | |
| ## 📋 Instrukcje użycia: | |
| 1. **Wgraj zdjęcie** - najlepiej wysokiej jakości, dobrze oświetlone | |
| 2. **Kliknij 'Analizuj'** - poczekaj na wynik AI (może zająć kilka sekund) | |
| 3. **Sprawdź wynik** - poziom ryzyka i prawdopodobieństwa | |
| 4. **Przeczytaj zalecenia** - czy potrzebna konsultacja lekarska | |
| """, visible=True) | |
| # Event handlers | |
| analyze_output = analyze_btn.click( | |
| fn=analyze_skin, | |
| inputs=[image_input], | |
| outputs=[result_output, uploaded_image, safety_output] | |
| ) | |
| analyze_output.then( | |
| fn=lambda: ( | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=False) | |
| ), | |
| outputs=[instructions, clear_btn, analyze_btn] | |
| ) | |
| clear_btn.click( | |
| fn=clear_all, | |
| outputs=[image_input, result_output, uploaded_image, safety_output, | |
| instructions, clear_btn, analyze_btn] | |
| ) | |
| with gr.Group(elem_classes="info-section"): | |
| gr.Markdown(""" | |
| ## 🎯 Aplikacja analizuje 8 typów zmian: | |
| 🔴 **Wysokie ryzyko:** Czerniak, Rak podstawnokomórkowy, Rak kolczystokomórkowy | |
| 🟡 **Średnie ryzyko:** Rogowacenie słoneczne | |
| 🟢 **Niskie ryzyko:** Znamiona, Włókniak twardy, Rogowacenie łojotokowe | |
| """) | |
| with gr.Group(elem_classes="privacy-section"): | |
| gr.Markdown(""" | |
| ## 🔒 Prywatność i bezpieczeństwo: | |
| ✅ Zdjęcia przetwarzane lokalnie (nie są przechowywane) | |
| ✅ Brak zbierania danych osobowych | |
| ✅ Anonimowe analizy z hashowanymi identyfikatorami (RODO) | |
| ✅ Rate limiting: 10 analiz/minutę | |
| ✅ Automatyczne czyszczenie pamięci cache | |
| """) | |
| with gr.Group(elem_classes="legal-section"): | |
| gr.Markdown(""" | |
| ## ⚖️ Zastrzeżenia prawne: | |
| Ta aplikacja jest narzędziem edukacyjnym i nie powinna być używana do samodzielnej diagnozy. | |
| Model AI może popełniać błędy. W przypadku jakichkolwiek obaw dotyczących zdrowia, | |
| skonsultuj się z wykwalifikowanym lekarzem dermatologiem. | |
| **Wersja:** 3.1 | |
| **Ostatnia aktualizacja:** Październik 2025 | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| show_error=True, | |
| quiet=False | |
| ) |