Spaces:
Sleeping
Sleeping
| import torch | |
| from transformers import ViTImageProcessor, ViTForImageClassification | |
| from PIL import Image | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import gradio as gr | |
| import io | |
| import base64 | |
| from torchvision import transforms | |
| import torch.nn.functional as F | |
| # --- MODELOS VERIFICADOS DISPONIBLES EN HUGGING FACE --- | |
| # 1. Google Derm Foundation (VERIFICADO - existe en Hugging Face) | |
| try: | |
| derm_processor = ViTImageProcessor.from_pretrained("google/derm-foundation") | |
| derm_model = ViTForImageClassification.from_pretrained("google/derm-foundation") | |
| derm_model.eval() | |
| DERM_AVAILABLE = True | |
| print("✅ Google Derm Foundation cargado exitosamente") | |
| except Exception as e: | |
| DERM_AVAILABLE = False | |
| print(f"❌ Google Derm Foundation no disponible: {e}") | |
| # 2. Modelo HAM10k especializado (VERIFICADO) | |
| try: | |
| ham_processor = ViTImageProcessor.from_pretrained("bsenst/skin-cancer-HAM10k") | |
| ham_model = ViTForImageClassification.from_pretrained("bsenst/skin-cancer-HAM10k") | |
| ham_model.eval() | |
| HAM_AVAILABLE = True | |
| print("✅ HAM10k especializado cargado exitosamente") | |
| except Exception as e: | |
| HAM_AVAILABLE = False | |
| print(f"❌ HAM10k especializado no disponible: {e}") | |
| # 3. Modelo ISIC 2024 con SMOTE (VERIFICADO) | |
| try: | |
| isic_processor = ViTImageProcessor.from_pretrained("jhoppanne/SkinCancerClassifier_smote-V0") | |
| isic_model = ViTForImageClassification.from_pretrained("jhoppanne/SkinCancerClassifier_smote-V0") | |
| isic_model.eval() | |
| ISIC_AVAILABLE = True | |
| print("✅ ISIC 2024 SMOTE cargado exitosamente") | |
| except Exception as e: | |
| ISIC_AVAILABLE = False | |
| print(f"❌ ISIC 2024 SMOTE no disponible: {e}") | |
| # 4. Modelo genérico de detección (VERIFICADO) | |
| try: | |
| generic_processor = ViTImageProcessor.from_pretrained("syaha/skin_cancer_detection_model") | |
| generic_model = ViTForImageClassification.from_pretrained("syaha/skin_cancer_detection_model") | |
| generic_model.eval() | |
| GENERIC_AVAILABLE = True | |
| print("✅ Modelo genérico cargado exitosamente") | |
| except Exception as e: | |
| GENERIC_AVAILABLE = False | |
| print(f"❌ Modelo genérico no disponible: {e}") | |
| # 5. Modelo de melanoma específico (VERIFICADO) | |
| try: | |
| melanoma_processor = ViTImageProcessor.from_pretrained("milutinNemanjic/Melanoma-detection-model") | |
| melanoma_model = ViTForImageClassification.from_pretrained("milutinNemanjic/Melanoma-detection-model") | |
| melanoma_model.eval() | |
| MELANOMA_AVAILABLE = True | |
| print("✅ Modelo melanoma específico cargado exitosamente") | |
| except Exception as e: | |
| MELANOMA_AVAILABLE = False | |
| print(f"❌ Modelo melanoma específico no disponible: {e}") | |
| # 6. Tu modelo actual como respaldo | |
| try: | |
| backup_processor = ViTImageProcessor.from_pretrained("Anwarkh1/Skin_Cancer-Image_Classification") | |
| backup_model = ViTForImageClassification.from_pretrained("Anwarkh1/Skin_Cancer-Image_Classification") | |
| backup_model.eval() | |
| BACKUP_AVAILABLE = True | |
| print("✅ Modelo de respaldo cargado exitosamente") | |
| except Exception as e: | |
| BACKUP_AVAILABLE = False | |
| print(f"❌ Modelo de respaldo no disponible: {e}") | |
| # Clases HAM10000 estándar | |
| CLASSES = [ | |
| "Queratosis actínica / Bowen", "Carcinoma células basales", | |
| "Lesión queratósica benigna", "Dermatofibroma", | |
| "Melanoma maligno", "Nevus melanocítico", "Lesión vascular" | |
| ] | |
| RISK_LEVELS = { | |
| 0: {'level': 'Alto', 'color': '#ff6b35', 'weight': 0.7}, # akiec | |
| 1: {'level': 'Crítico', 'color': '#cc0000', 'weight': 0.9}, # bcc | |
| 2: {'level': 'Bajo', 'color': '#44ff44', 'weight': 0.1}, # bkl | |
| 3: {'level': 'Bajo', 'color': '#44ff44', 'weight': 0.1}, # df | |
| 4: {'level': 'Crítico', 'color': '#990000', 'weight': 1.0}, # melanoma | |
| 5: {'level': 'Bajo', 'color': '#66ff66', 'weight': 0.1}, # nv | |
| 6: {'level': 'Moderado', 'color': '#ffaa00', 'weight': 0.3} # vasc | |
| } | |
| MALIGNANT_INDICES = [0, 1, 4] # akiec, bcc, melanoma | |
| def safe_predict(image, processor, model, model_name, expected_classes=7): | |
| """Predicción segura que maneja diferentes números de clases""" | |
| try: | |
| inputs = processor(image, return_tensors="pt") | |
| with torch.no_grad(): | |
| outputs = model(**inputs) | |
| logits = outputs.logits | |
| # Manejar diferentes números de clases | |
| if logits.shape[1] != expected_classes: | |
| print(f"⚠️ {model_name}: Esperaba {expected_classes} clases, obtuvo {logits.shape[1]}") | |
| if logits.shape[1] == 2: # Modelo binario (benigno/maligno) | |
| probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0] | |
| # Convertir a formato de 7 clases (simplificado) | |
| expanded_probs = np.zeros(expected_classes) | |
| if probabilities[1] > 0.5: # Maligno | |
| expanded_probs[4] = probabilities[1] * 0.6 # Melanoma | |
| expanded_probs[1] = probabilities[1] * 0.3 # BCC | |
| expanded_probs[0] = probabilities[1] * 0.1 # AKIEC | |
| else: # Benigno | |
| expanded_probs[5] = probabilities[0] * 0.7 # Nevus | |
| expanded_probs[2] = probabilities[0] * 0.2 # BKL | |
| expanded_probs[3] = probabilities[0] * 0.1 # DF | |
| probabilities = expanded_probs | |
| else: | |
| # Para otros números de clases, normalizar o truncar | |
| probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0] | |
| if len(probabilities) > expected_classes: | |
| probabilities = probabilities[:expected_classes] | |
| elif len(probabilities) < expected_classes: | |
| temp = np.zeros(expected_classes) | |
| temp[:len(probabilities)] = probabilities | |
| probabilities = temp | |
| else: | |
| probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0] | |
| predicted_idx = int(np.argmax(probabilities)) | |
| predicted_class = CLASSES[predicted_idx] if predicted_idx < len(CLASSES) else "Desconocido" | |
| confidence = float(probabilities[predicted_idx]) | |
| is_malignant = predicted_idx in MALIGNANT_INDICES | |
| return { | |
| 'model': model_name, | |
| 'class': predicted_class, | |
| 'confidence': confidence, | |
| 'probabilities': probabilities, | |
| 'is_malignant': is_malignant, | |
| 'predicted_idx': predicted_idx, | |
| 'success': True | |
| } | |
| except Exception as e: | |
| print(f"❌ Error en {model_name}: {e}") | |
| return { | |
| 'model': model_name, | |
| 'error': str(e), | |
| 'class': 'Error', | |
| 'confidence': 0.0, | |
| 'is_malignant': False, | |
| 'success': False | |
| } | |
| def ensemble_prediction(predictions): | |
| """Combina múltiples predicciones usando weighted voting inteligente""" | |
| valid_preds = [p for p in predictions if p.get('success', False)] | |
| if not valid_preds: | |
| return None | |
| # Weighted ensemble basado en confianza y disponibilidad del modelo | |
| ensemble_probs = np.zeros(len(CLASSES)) | |
| total_weight = 0 | |
| # Pesos específicos por modelo (basado en calidad esperada) | |
| model_weights = { | |
| "🏥 Google Derm Foundation": 1.0, | |
| "🧠 HAM10k Especializado": 0.9, | |
| "🆕 ISIC 2024 SMOTE": 0.8, | |
| "🔬 Melanoma Específico": 0.7, | |
| "🌐 Genérico": 0.6, | |
| "🔄 Respaldo Original": 0.5 | |
| } | |
| for pred in valid_preds: | |
| model_weight = model_weights.get(pred['model'], 0.5) | |
| confidence_weight = pred['confidence'] | |
| final_weight = model_weight * confidence_weight | |
| ensemble_probs += pred['probabilities'] * final_weight | |
| total_weight += final_weight | |
| if total_weight > 0: | |
| ensemble_probs /= total_weight | |
| ensemble_idx = int(np.argmax(ensemble_probs)) | |
| ensemble_class = CLASSES[ensemble_idx] | |
| ensemble_confidence = float(ensemble_probs[ensemble_idx]) | |
| ensemble_malignant = ensemble_idx in MALIGNANT_INDICES | |
| # Calcular consenso de malignidad | |
| malignant_votes = sum(1 for p in valid_preds if p.get('is_malignant', False)) | |
| malignant_consensus = malignant_votes / len(valid_preds) | |
| return { | |
| 'class': ensemble_class, | |
| 'confidence': ensemble_confidence, | |
| 'probabilities': ensemble_probs, | |
| 'is_malignant': ensemble_malignant, | |
| 'predicted_idx': ensemble_idx, | |
| 'malignant_consensus': malignant_consensus, | |
| 'num_models': len(valid_preds) | |
| } | |
| def calculate_risk_score(ensemble_result): | |
| """Calcula score de riesgo sofisticado""" | |
| if not ensemble_result: | |
| return 0.0 | |
| # Score base del ensemble | |
| base_score = ensemble_result['probabilities'][ensemble_result['predicted_idx']] * \ | |
| RISK_LEVELS[ensemble_result['predicted_idx']]['weight'] | |
| # Ajuste por consenso de malignidad | |
| consensus_boost = ensemble_result['malignant_consensus'] * 0.3 | |
| # Bonus por número de modelos | |
| model_confidence = min(ensemble_result['num_models'] / 5.0, 1.0) * 0.1 | |
| final_score = base_score + consensus_boost + model_confidence | |
| return min(final_score, 1.0) | |
| def analizar_lesion_verificado(img): | |
| """Análisis con modelos verificados existentes""" | |
| predictions = [] | |
| # Probar modelos disponibles en orden de preferencia | |
| models_to_try = [ | |
| (DERM_AVAILABLE, derm_processor, derm_model, "🏥 Google Derm Foundation"), | |
| (HAM_AVAILABLE, ham_processor, ham_model, "🧠 HAM10k Especializado"), | |
| (ISIC_AVAILABLE, isic_processor, isic_model, "🆕 ISIC 2024 SMOTE"), | |
| (MELANOMA_AVAILABLE, melanoma_processor, melanoma_model, "🔬 Melanoma Específico"), | |
| (GENERIC_AVAILABLE, generic_processor, generic_model, "🌐 Genérico"), | |
| (BACKUP_AVAILABLE, backup_processor, backup_model, "🔄 Respaldo Original") | |
| ] | |
| for available, processor, model, name in models_to_try: | |
| if available: | |
| pred = safe_predict(img, processor, model, name) | |
| predictions.append(pred) | |
| if not predictions: | |
| return "❌ No hay modelos disponibles", "" | |
| # Ensemble de predicciones | |
| ensemble_result = ensemble_prediction(predictions) | |
| if not ensemble_result: | |
| return "❌ Error en el análisis ensemble", "" | |
| # Calcular riesgo | |
| risk_score = calculate_risk_score(ensemble_result) | |
| # Generar visualización | |
| colors = [RISK_LEVELS[i]['color'] for i in range(len(CLASSES))] | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7)) | |
| # Gráfico principal del ensemble | |
| bars = ax1.bar(CLASSES, ensemble_result['probabilities'] * 100, color=colors, alpha=0.8) | |
| ax1.set_title("🎯 Predicción Ensemble (Modelos Combinados)", fontsize=16, fontweight='bold', pad=20) | |
| ax1.set_ylabel("Probabilidad (%)", fontsize=12) | |
| ax1.set_xticklabels(CLASSES, rotation=45, ha='right', fontsize=10) | |
| ax1.grid(axis='y', alpha=0.3) | |
| ax1.set_ylim(0, 100) | |
| # Destacar la predicción principal | |
| bars[ensemble_result['predicted_idx']].set_edgecolor('black') | |
| bars[ensemble_result['predicted_idx']].set_linewidth(3) | |
| bars[ensemble_result['predicted_idx']].set_alpha(1.0) | |
| # Gráfico de consenso | |
| consensus_data = ['Benigno', 'Maligno'] | |
| consensus_values = [1 - ensemble_result['malignant_consensus'], ensemble_result['malignant_consensus']] | |
| consensus_colors = ['#27ae60', '#e74c3c'] | |
| bars2 = ax2.bar(consensus_data, consensus_values, color=consensus_colors, alpha=0.8) | |
| ax2.set_title(f"🤝 Consenso Malignidad ({ensemble_result['num_models']} modelos)", | |
| fontsize=16, fontweight='bold', pad=20) | |
| ax2.set_ylabel("Proporción de Modelos", fontsize=12) | |
| ax2.set_ylim(0, 1) | |
| ax2.grid(axis='y', alpha=0.3) | |
| # Añadir valores en las barras | |
| for bar, value in zip(bars2, consensus_values): | |
| height = bar.get_height() | |
| ax2.text(bar.get_x() + bar.get_width()/2., height + 0.02, | |
| f'{value:.1%}', ha='center', va='bottom', fontweight='bold') | |
| plt.tight_layout() | |
| buf = io.BytesIO() | |
| plt.savefig(buf, format="png", dpi=120, bbox_inches='tight') | |
| plt.close(fig) | |
| chart_html = f'<img src="data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}" style="max-width:100%; border-radius:8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>' | |
| # Generar reporte detallado | |
| informe = f""" | |
| <div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 1000px; margin: auto; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); padding: 25px; border-radius: 15px;"> | |
| <h1 style="color: #2c3e50; text-align: center; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);"> | |
| 🏥 Análisis Dermatológico Multi-Modelo IA | |
| </h1> | |
| <div style="background: white; padding: 25px; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> | |
| <h2 style="color: #34495e; margin-top: 0; border-bottom: 3px solid #3498db; padding-bottom: 10px;"> | |
| 📊 Resultados Individuales por Modelo | |
| </h2> | |
| <div style="overflow-x: auto;"> | |
| <table style="width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 15px;"> | |
| <thead> | |
| <tr style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;"> | |
| <th style="padding: 15px; text-align: left; border-radius: 8px 0 0 0;">Modelo</th> | |
| <th style="padding: 15px; text-align: left;">Diagnóstico</th> | |
| <th style="padding: 15px; text-align: left;">Confianza</th> | |
| <th style="padding: 15px; text-align: left;">Estado</th> | |
| <th style="padding: 15px; text-align: left; border-radius: 0 8px 0 0;">Malignidad</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for i, pred in enumerate(predictions): | |
| row_color = "#f8f9fa" if i % 2 == 0 else "#ffffff" | |
| if pred.get('success', False): | |
| status_icon = "✅" | |
| status_color = "#27ae60" | |
| status_text = "Activo" | |
| malignant_color = "#e74c3c" if pred.get('is_malignant', False) else "#27ae60" | |
| malignant_text = "🚨 Maligno" if pred.get('is_malignant', False) else "✅ Benigno" | |
| informe += f""" | |
| <tr style="background: {row_color};"> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1; font-weight: bold;">{pred['model']}</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1;"><strong>{pred['class']}</strong></td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1;">{pred['confidence']:.1%}</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: {status_color};"><strong>{status_icon} {status_text}</strong></td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: {malignant_color};"><strong>{malignant_text}</strong></td> | |
| </tr> | |
| """ | |
| else: | |
| informe += f""" | |
| <tr style="background: {row_color};"> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1; font-weight: bold; color: #7f8c8d;">{pred['model']}</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: #e67e22;">❌ No disponible</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1;">N/A</td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: #e74c3c;"><strong>❌ Error</strong></td> | |
| <td style="padding: 12px; border-bottom: 1px solid #ecf0f1;">N/A</td> | |
| </tr> | |
| """ | |
| # Resultado del ensemble | |
| ensemble_status_color = "#e74c3c" if ensemble_result.get('is_malignant', False) else "#27ae60" | |
| ensemble_status_text = "🚨 MALIGNO" if ensemble_result.get('is_malignant', False) else "✅ BENIGNO" | |
| informe += f""" | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);"> | |
| <h2 style="margin-top: 0; color: white; display: flex; align-items: center;"> | |
| 🎯 Diagnóstico Final (Consenso de {ensemble_result['num_models']} modelos) | |
| </h2> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;"> | |
| <div> | |
| <p style="font-size: 18px; margin: 8px 0;"><strong>Diagnóstico:</strong> {ensemble_result['class']}</p> | |
| <p style="margin: 8px 0;"><strong>Confianza:</strong> {ensemble_result['confidence']:.1%}</p> | |
| <p style="margin: 8px 0; color: {ensemble_status_color}; background: rgba(255,255,255,0.2); padding: 8px; border-radius: 5px;"><strong>Estado: {ensemble_status_text}</strong></p> | |
| </div> | |
| <div> | |
| <p style="margin: 8px 0;"><strong>Consenso Malignidad:</strong> {ensemble_result['malignant_consensus']:.1%}</p> | |
| <p style="margin: 8px 0;"><strong>Score de Riesgo:</strong> {risk_score:.2f}</p> | |
| <p style="margin: 8px 0;"><strong>Modelos Activos:</strong> {ensemble_result['num_models']}/6</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # Recomendación clínica | |
| informe += """ | |
| <div style="background: white; padding: 25px; border-radius: 12px; border-left: 6px solid #3498db; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> | |
| <h2 style="color: #2c3e50; margin-top: 0; display: flex; align-items: center;"> | |
| 🩺 Recomendación Clínica Automatizada | |
| </h2> | |
| """ | |
| if risk_score > 0.7: | |
| informe += ''' | |
| <div style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
| <h3 style="margin: 0; font-size: 18px;">🚨 DERIVACIÓN URGENTE</h3> | |
| <p style="margin: 10px 0 0 0; font-size: 16px;">Contactar con oncología dermatológica en 24-48 horas</p> | |
| </div>''' | |
| elif risk_score > 0.5: | |
| informe += ''' | |
| <div style="background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
| <h3 style="margin: 0; font-size: 18px;">⚠️ EVALUACIÓN PRIORITARIA</h3> | |
| <p style="margin: 10px 0 0 0; font-size: 16px;">Consulta dermatológica en 1-2 semanas</p> | |
| </div>''' | |
| elif risk_score > 0.3: | |
| informe += ''' | |
| <div style="background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
| <h3 style="margin: 0; font-size: 18px;">📋 SEGUIMIENTO PROGRAMADO</h3> | |
| <p style="margin: 10px 0 0 0; font-size: 16px;">Consulta dermatológica en 4-6 semanas</p> | |
| </div>''' | |
| else: | |
| informe += ''' | |
| <div style="background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
| <h3 style="margin: 0; font-size: 18px;">✅ MONITOREO RUTINARIO</h3> | |
| <p style="margin: 10px 0 0 0; font-size: 16px;">Seguimiento en 3-6 meses</p> | |
| </div>''' | |
| informe += f""" | |
| <div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #e67e22;"> | |
| <p style="margin: 0; font-style: italic; color: #7f8c8d; font-size: 13px;"> | |
| ⚠️ <strong>Disclaimer Médico:</strong> Este análisis utiliza {ensemble_result['num_models']} modelos de IA como herramienta de apoyo diagnóstico. | |
| El resultado NO sustituye el criterio médico profesional. Siempre consulte con un dermatólogo certificado | |
| para un diagnóstico definitivo y plan de tratamiento apropiado. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return informe, chart_html | |
| # Interfaz Gradio mejorada | |
| demo = gr.Interface( | |
| fn=analizar_lesion_verificado, | |
| inputs=gr.Image(type="pil", label="📷 Cargar imagen dermatoscópica o foto de lesión cutánea"), | |
| outputs=[ | |
| gr.HTML(label="📋 Informe Diagnóstico Completo"), | |
| gr.HTML(label="📊 Análisis Visual de Resultados") | |
| ], | |
| title="🏥 Sistema Avanzado de Detección de Cáncer de Piel - Multi-Modelo IA", | |
| description=""" | |
| Sistema de análisis dermatológico que utiliza múltiples modelos de IA especializados verificados: | |
| • Google Derm Foundation (modelo más avanzado de Google Health) | |
| • Modelos especializados en HAM10000, ISIC 2024, y detección de melanoma | |
| • Ensemble inteligente con weighted voting y análisis de consenso | |
| """, | |
| theme=gr.themes.Soft(), | |
| allow_flagging="never", | |
| examples=None | |
| ) | |
| if __name__ == "__main__": | |
| print("\n🚀 Iniciando sistema de detección de cáncer de piel...") | |
| print("📋 Modelos verificados y disponibles en Hugging Face:") | |
| print("✅ google/derm-foundation") | |
| print("✅ bsenst/skin-cancer-HAM10k") | |
| print("✅ jhoppanne/SkinCancerClassifier_smote-V0") | |
| print("✅ syaha/skin_cancer_detection_model") | |
| print("✅ milutinNemanjic/Melanoma-detection-model") | |
| print("✅ Anwarkh1/Skin_Cancer-Image_Classification") | |
| print("\n🌐 Lanzando interfaz web...") | |
| demo.launch(share=False) |