#!/usr/bin/env python3 """ APLICACIÓN MÉDICA - BACKEND FLASK Retinopatía Diabética - Versión Web para Hugging Face Spaces """ import os import base64 import json import uuid import numpy as np import cv2 import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt from io import BytesIO from datetime import datetime, timedelta from functools import wraps from typing import Optional from PIL import Image from flask import Flask, request, jsonify, session, send_from_directory import tensorflow as tf from database import DatabaseManager # ------------------------------------------------------------------ # # CONFIGURACIÓN # ------------------------------------------------------------------ # app = Flask(__name__, static_folder='web', static_url_path='') app.secret_key = os.environ.get("SECRET_KEY", "medical-app-secret-2024-change-in-prod") app.permanent_session_lifetime = timedelta(hours=8) # Necesario para que las cookies funcionen en HF Spaces (proxy/iframe) app.config.update( SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, ) db = DatabaseManager() model = None CLASS_NAMES = ['Diabetic Retinopathy', 'No Diabetic Retinopathy'] OPTIMAL_THRESHOLD = 0.28 # SciPy opcional try: from scipy import ndimage SCIPY_AVAILABLE = True except ImportError: SCIPY_AVAILABLE = False class _FakeNdimage: @staticmethod def gaussian_filter(img, sigma): k = int(2 * int(3 * sigma) + 1) if k % 2 == 0: k += 1 return cv2.GaussianBlur(img.astype(np.float32), (k, k), sigma) @staticmethod def label(binary): if len(binary.shape) == 3: binary = cv2.cvtColor(binary.astype(np.uint8), cv2.COLOR_BGR2GRAY) binary = (binary * 255).astype(np.uint8) n, labels = cv2.connectedComponents(binary) return labels, n - 1 @staticmethod def center_of_mass(binary): if len(binary.shape) == 3: binary = cv2.cvtColor(binary.astype(np.uint8), cv2.COLOR_BGR2GRAY) binary = (binary * 255).astype(np.uint8) m = cv2.moments(binary) if m['m00'] != 0: return (m['m01'] / m['m00'], m['m10'] / m['m00']) h, w = binary.shape return (h // 2, w // 2) ndimage = _FakeNdimage() # ------------------------------------------------------------------ # # DECORADORES DE AUTENTICACIÓN # ------------------------------------------------------------------ # def login_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get('is_authenticated'): return jsonify({'success': False, 'error': 'No autenticado', 'redirect_to_login': True}), 401 if datetime.fromisoformat(session.get('expires_at', '2000-01-01')) < datetime.now(): session.clear() return jsonify({'success': False, 'error': 'Sesión expirada', 'redirect_to_login': True}), 401 return f(*args, **kwargs) return decorated def admin_required(f): @wraps(f) def decorated(*args, **kwargs): if not session.get('is_authenticated'): return jsonify({'success': False, 'error': 'No autenticado'}), 401 if session.get('role') != 'Admin': return jsonify({'success': False, 'error': 'Acceso denegado: Solo administradores'}), 403 return f(*args, **kwargs) return decorated # ------------------------------------------------------------------ # # MODELO # ------------------------------------------------------------------ # def load_model(): global model app_dir = os.path.dirname(os.path.abspath(__file__)) model_files = [f for f in os.listdir(app_dir) if f.endswith('.h5')] if not model_files: print(f"ERROR: No hay archivos .h5 en {app_dir}") return False model_path = os.path.join(app_dir, model_files[0]) print(f"Intentando cargar modelo: {model_path}") # Intento 1: carga directa del archivo completo try: model = tf.keras.models.load_model(model_path, compile=False) test = np.random.random((1, 224, 224, 3)).astype(np.float32) * 255 model.predict(test, verbose=0) print(f"Modelo cargado con load_model(): {model_path}") return True except Exception as e1: print(f"load_model() falló: {e1}") # Intento 2: reconstruir arquitectura y cargar pesos try: from tensorflow.keras.applications import EfficientNetB0 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout, BatchNormalization from tensorflow.keras.regularizers import l2 from tensorflow.keras.models import Model base = EfficientNetB0(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) base.trainable = False inputs = tf.keras.Input(shape=(224, 224, 3)) x = tf.keras.applications.efficientnet.preprocess_input(inputs) x = base(x, training=False) x = GlobalAveragePooling2D()(x) x = BatchNormalization()(x) x = Dropout(0.6)(x) x = Dense(64, activation='relu', kernel_regularizer=l2(0.01))(x) x = Dropout(0.5)(x) outputs = Dense(1, activation='sigmoid', name='predictions')(x) model = Model(inputs, outputs) model.load_weights(model_path) test = np.random.random((1, 224, 224, 3)).astype(np.float32) * 255 model.predict(test, verbose=0) print(f"Modelo cargado con load_weights(): {model_path}") return True except Exception as e2: print(f"load_weights() falló: {e2}") model = None return False def preprocess_image(image_bytes) -> Optional[np.ndarray]: try: img = Image.open(BytesIO(image_bytes)).convert('RGB') img = img.resize((224, 224), Image.Resampling.LANCZOS) arr = np.array(img, dtype=np.float32) return np.expand_dims(arr, axis=0) except Exception as e: print(f"Error en preprocesamiento: {e}") return None # ------------------------------------------------------------------ # # GRAD-CAM # ------------------------------------------------------------------ # class SimpleGradCAM: def __init__(self, model_, threshold=0.28): self.model = model_ self.threshold = threshold def generate(self, img_tensor): try: with tf.GradientTape() as tape: tape.watch(img_tensor) preds = self.model(img_tensor, training=False) loss = preds[0, 0] if preds.shape[-1] == 1 else preds[0, tf.argmax(preds[0])] grads = tape.gradient(loss, img_tensor) if grads is not None: heatmap = tf.squeeze(tf.reduce_mean(tf.abs(grads), axis=-1)) heatmap = tf.maximum(heatmap, 0) if tf.reduce_max(heatmap) > 0: heatmap = heatmap / tf.reduce_max(heatmap) return heatmap.numpy(), preds[0].numpy() except Exception as e: print(f"GradCAM error: {e}") return self._attention(img_tensor) def _attention(self, img_tensor): preds = self.model(img_tensor, training=False) gray = tf.reduce_mean(img_tensor[0], axis=-1) k = tf.ones((5, 5, 1, 1)) / 25.0 smooth = tf.nn.conv2d(tf.expand_dims(tf.expand_dims(gray, -1), 0), k, [1,1,1,1], 'SAME') edges = tf.abs(tf.expand_dims(gray, 0) - tf.squeeze(smooth)) att = (gray + edges) / 2.0 att = tf.maximum(att, 0) if tf.reduce_max(att) > 0: att = att / tf.reduce_max(att) return att.numpy(), preds[0].numpy() def find_critical_region(heatmap, zoom_factor=2.2, min_size=60): h, w = heatmap.shape max_y, max_x = np.unravel_index(np.argmax(heatmap), heatmap.shape) thresh = max(0.7, np.percentile(heatmap, 95)) smooth = ndimage.gaussian_filter(heatmap, sigma=1.0) mask = smooth > thresh center_y, center_x = max_y, max_x if np.sum(mask) > 0: labeled, n = ndimage.label(mask) if n > 0: lbl = labeled[max_y, max_x] if lbl > 0: cy, cx = ndimage.center_of_mass(labeled == lbl) center_y, center_x = int(cy), int(cx) zh, zw = max(int(h / zoom_factor), min_size), max(int(w / zoom_factor), min_size) y0 = max(0, min(center_y - zh // 2, h - zh)) x0 = max(0, min(center_x - zw // 2, w - zw)) return y0, y0 + zh, x0, x0 + zw, center_y, center_x # ------------------------------------------------------------------ # # RUTAS - SERVIR FRONTEND # ------------------------------------------------------------------ # @app.route('/') def index(): return send_from_directory('web', 'auth-login.html') @app.route('/') def static_files(path): return send_from_directory('web', path) # ------------------------------------------------------------------ # # RUTAS - AUTENTICACIÓN # ------------------------------------------------------------------ # @app.route('/api/login', methods=['POST']) def login(): data = request.json user = db.authenticate_user(data.get('username', ''), data.get('password', '')) if user: session.permanent = True session['user_id'] = user['userID'] session['username'] = user['username'] session['role'] = user['role'] session['is_authenticated'] = True session['expires_at'] = (datetime.now() + timedelta(hours=8)).isoformat() return jsonify({'success': True, 'user': user, 'message': f'Bienvenido, {user["username"]}'}) return jsonify({'success': False, 'message': 'Usuario o contraseña incorrectos'}), 401 @app.route('/api/logout', methods=['POST']) def logout(): session.clear() return jsonify({'success': True}) @app.route('/api/session', methods=['GET']) @login_required def get_session(): return jsonify({ 'success': True, 'user': { 'userID': session['user_id'], 'username': session['username'], 'role': session['role'] } }) # ------------------------------------------------------------------ # # RUTAS - USUARIOS (solo Admin) # ------------------------------------------------------------------ # @app.route('/api/users', methods=['GET']) @login_required @admin_required def get_users(): return jsonify({'success': True, 'users': db.get_all_users()}) @app.route('/api/users', methods=['POST']) @login_required @admin_required def create_user(): data = request.json username = data.get('username', '').strip() password = data.get('password', '') role = data.get('role', 'Doctor') if not username or not password: return jsonify({'success': False, 'message': 'Usuario y contraseña requeridos'}), 400 if len(password) < 6: return jsonify({'success': False, 'message': 'Contraseña mínimo 6 caracteres'}), 400 if role not in ['Doctor', 'Admin']: return jsonify({'success': False, 'message': 'Rol inválido'}), 400 ok = db.create_user(username, password, role) if ok: return jsonify({'success': True, 'message': f'Usuario {username} creado'}) return jsonify({'success': False, 'message': 'El usuario ya existe'}), 409 @app.route('/api/users/', methods=['PUT']) @login_required @admin_required def update_user(user_id): data = request.json if not db.get_user(user_id): return jsonify({'success': False, 'message': 'Usuario no encontrado'}), 404 db.update_user(user_id, username=data.get('username'), role=data.get('role'), password=data.get('password') or None) return jsonify({'success': True, 'message': 'Usuario actualizado'}) @app.route('/api/users/', methods=['DELETE']) @login_required @admin_required def delete_user(user_id): if user_id == session['user_id']: return jsonify({'success': False, 'message': 'No puedes eliminar tu propia cuenta'}), 400 all_users = db.get_all_users() admins = [u for u in all_users if u['role'] == 'Admin'] target = db.get_user(user_id) if target and target['role'] == 'Admin' and len(admins) <= 1: return jsonify({'success': False, 'message': 'No se puede eliminar el último Admin'}), 400 db.delete_user(user_id) return jsonify({'success': True, 'message': 'Usuario eliminado'}) # ------------------------------------------------------------------ # # RUTAS - PACIENTES # ------------------------------------------------------------------ # @app.route('/api/patients', methods=['GET']) @login_required def get_patients(): search = request.args.get('search', '').strip() uid, role = session['user_id'], session['role'] if search: patients = db.search_patients(search, uid, role) else: patients = db.get_patients(uid, role) return jsonify({'success': True, 'patients': patients}) @app.route('/api/patients', methods=['POST']) @login_required def create_patient(): data = request.json name = (data.get('name') or '').strip() if not name: return jsonify({'success': False, 'message': 'Nombre requerido'}), 400 pid = db.create_patient( created_by_user_id=session['user_id'], name=name, birth_date=data.get('birthDate'), gender=data.get('gender'), diabetes_type=data.get('diabetesType') ) if pid: patient = db.get_patient(pid) return jsonify({'success': True, 'patient': patient}) return jsonify({'success': False, 'message': 'Error creando paciente'}), 500 @app.route('/api/patients/', methods=['GET']) @login_required def get_patient(patient_id): patient = db.get_patient(patient_id) if not patient: return jsonify({'success': False, 'message': 'Paciente no encontrado'}), 404 # Doctors can only see their own patients if session['role'] != 'Admin' and patient['createdByUserID'] != session['user_id']: return jsonify({'success': False, 'message': 'Acceso denegado'}), 403 consultations = db.get_patient_consultations(patient_id) risk_factors = db.get_patient_risk_factors(patient_id) return jsonify({'success': True, 'patient': patient, 'consultations': consultations, 'risk_factors': risk_factors}) @app.route('/api/patients/', methods=['PUT']) @login_required def update_patient(patient_id): data = request.json patient = db.get_patient(patient_id) if not patient: return jsonify({'success': False, 'message': 'Paciente no encontrado'}), 404 if session['role'] != 'Admin' and patient['createdByUserID'] != session['user_id']: return jsonify({'success': False, 'message': 'Acceso denegado'}), 403 db.update_patient(patient_id, name=data.get('name'), birthDate=data.get('birthDate'), gender=data.get('gender'), diabetesType=data.get('diabetesType')) return jsonify({'success': True, 'patient': db.get_patient(patient_id)}) @app.route('/api/patients/', methods=['DELETE']) @login_required def delete_patient(patient_id): patient = db.get_patient(patient_id) if not patient: return jsonify({'success': False, 'message': 'Paciente no encontrado'}), 404 if session['role'] != 'Admin' and patient['createdByUserID'] != session['user_id']: return jsonify({'success': False, 'message': 'Acceso denegado'}), 403 db.delete_patient(patient_id) return jsonify({'success': True}) # Factores de riesgo @app.route('/api/risk-factors', methods=['GET']) @login_required def get_risk_factors(): return jsonify({'success': True, 'risk_factors': db.get_all_risk_factors()}) @app.route('/api/patients//risk-factors', methods=['POST']) @login_required def add_risk_factor(patient_id): data = request.json db.add_patient_risk_factor(patient_id, data['riskFactorID']) return jsonify({'success': True}) @app.route('/api/patients//risk-factors/', methods=['DELETE']) @login_required def remove_risk_factor(patient_id, rf_id): db.remove_patient_risk_factor(patient_id, rf_id) return jsonify({'success': True}) # ------------------------------------------------------------------ # # RUTAS - PREDICCIÓN / IA # ------------------------------------------------------------------ # @app.route('/api/predict', methods=['POST']) @login_required def predict(): global model if model is None: return jsonify({'success': False, 'error': 'Modelo no cargado'}), 503 data = request.json image_data = data.get('imageData', '') filename = data.get('filename', 'image.jpg') if 'base64,' in image_data: image_data = image_data.split('base64,')[1] try: image_bytes = base64.b64decode(image_data) processed = preprocess_image(image_bytes) if processed is None: return jsonify({'success': False, 'error': 'Error procesando imagen'}), 400 prediction = model.predict(processed, verbose=0) raw = float(prediction[0][0]) if raw > OPTIMAL_THRESHOLD: predicted_class = 0 confidence = raw * 100 else: predicted_class = 1 confidence = (1 - raw) * 100 result = { 'success': True, 'prediction': { 'class': CLASS_NAMES[predicted_class], 'class_index': predicted_class, 'confidence': round(confidence, 2), 'raw_output': round(raw, 6) }, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'filename': filename } return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/gradcam', methods=['POST']) @login_required def gradcam(): global model if model is None: return jsonify({'success': False, 'error': 'Modelo no cargado'}), 503 data = request.json image_data = data.get('imageData', '') filename = data.get('filename', 'image.jpg') prediction_result = data.get('predictionResult', {}) if prediction_result.get('prediction', {}).get('class_index', 0) != 1: return jsonify({'success': False, 'error': 'Grad-CAM solo para casos positivos de retinopatía'}), 400 if 'base64,' in image_data: image_data = image_data.split('base64,')[1] try: image_bytes = base64.b64decode(image_data) img_pil = Image.open(BytesIO(image_bytes)).convert('RGB') orig_w, orig_h = img_pil.size img_224 = img_pil.resize((224, 224), Image.Resampling.LANCZOS) img_arr = np.array(img_224, dtype=np.float32) orig_arr = np.array(img_pil, dtype=np.uint8) img_tensor = tf.convert_to_tensor(np.expand_dims(img_arr, 0), dtype=tf.float32) gcam = SimpleGradCAM(model, OPTIMAL_THRESHOLD) heatmap, _ = gcam.generate(img_tensor) y0, y1, x0, x1, cy, cx = find_critical_region(heatmap) sx, sy = orig_w / 224.0, orig_h / 224.0 x0h, x1h = int(x0 * sx), int(x1 * sx) y0h, y1h = int(y0 * sy), int(y1 * sy) zoom_region = orig_arr[y0h:y1h, x0h:x1h] plt.figure(figsize=(10, 10)) if zoom_region.size > 0: plt.imshow(zoom_region) zoom_heat = heatmap[y0:y1, x0:x1] max_act = float(np.max(zoom_heat)) avg_act = float(np.mean(zoom_heat)) high_pct = float(np.sum(zoom_heat > 0.6) / zoom_heat.size * 100) plt.title(f'Zona Crítica HD ({x1h-x0h}×{y1h-y0h}px)\n' f'Activación: máx={max_act:.3f}, prom={avg_act:.3f}', fontsize=12, pad=20) else: zoom_region = img_arr[y0:y1, x0:x1].astype(np.uint8) plt.imshow(zoom_region) plt.title('Zona Crítica', fontsize=12) high_pct, max_act, avg_act = 0.0, 0.0, 0.0 plt.axis('off') plt.tight_layout() buf = BytesIO() plt.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='white', edgecolor='none') buf.seek(0) img_b64 = base64.b64encode(buf.getvalue()).decode() plt.close() if high_pct > 20: clinical_info = f"Lesión focal intensa ({high_pct:.1f}% activación alta)" elif high_pct > 10: clinical_info = f"Cambios moderados en región focal ({high_pct:.1f}%)" else: clinical_info = "Cambios sutiles de DR detectados" return jsonify({ 'success': True, 'gradcam_image': f"data:image/png;base64,{img_b64}", 'analysis': { 'max_activation': max_act, 'avg_activation': avg_act, 'high_activation_pct': high_pct, 'clinical_info': clinical_info, 'zoom_region_hd': (x0h, y0h, x1h, y1h) } }) except Exception as e: import traceback; traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 # ------------------------------------------------------------------ # # RUTAS - CONSULTAS # ------------------------------------------------------------------ # @app.route('/api/consultations', methods=['GET']) @login_required def get_consultations(): page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 10)) search = request.args.get('search', '') filter_type = request.args.get('filter', 'all') result = db.get_consultations(session['user_id'], session['role'], page, per_page, search, filter_type) return jsonify(result) @app.route('/api/consultations/', methods=['GET']) @login_required def get_consultation(consultation_id): result = db.get_consultation_by_id(consultation_id, session['user_id'], session['role']) if result is None: return jsonify({'success': False, 'message': 'Consulta no encontrada o acceso denegado'}), 404 return jsonify({'success': True, 'consultation': result}) @app.route('/api/consultations/', methods=['DELETE']) @login_required def delete_consultation(consultation_id): conn = db.get_connection() try: row = conn.execute( "SELECT createdByUserID FROM Consultations WHERE consultationID=?", (consultation_id,) ).fetchone() if not row: return jsonify({'success': False, 'message': 'Consulta no encontrada'}), 404 if session['role'] != 'Admin' and row['createdByUserID'] != session['user_id']: return jsonify({'success': False, 'message': 'Acceso denegado'}), 403 conn.execute("DELETE FROM Consultations WHERE consultationID=?", (consultation_id,)) conn.commit() return jsonify({'success': True}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() @app.route('/api/consultations', methods=['POST']) @login_required def save_consultation(): data = request.json patient_id = data.get('patientId') if not patient_id: return jsonify({'success': False, 'message': 'patientId requerido'}), 400 patient = db.get_patient(patient_id) if not patient: return jsonify({'success': False, 'message': 'Paciente no encontrado'}), 404 if session['role'] != 'Admin' and patient['createdByUserID'] != session['user_id']: return jsonify({'success': False, 'message': 'Acceso denegado'}), 403 right = data.get('rightEye', {}) left = data.get('leftEye', {}) notes = data.get('notes', '') if right.get('hasAnalysis') and left.get('hasAnalysis'): has_dr = right['diagnosis'] or left['diagnosis'] confidence = (right['confidence'] + left['confidence']) / 2 raw_output = (right.get('rawOutput', 0) + left.get('rawOutput', 0)) / 2 detailed_notes = ( f"BILATERAL - OD: {'Positivo' if right['diagnosis'] else 'Negativo'} " f"({right['confidence']:.1f}%) | " f"OI: {'Positivo' if left['diagnosis'] else 'Negativo'} " f"({left['confidence']:.1f}%)\n{notes}" ) elif right.get('hasAnalysis'): has_dr = right['diagnosis'] confidence = right['confidence'] raw_output = right.get('rawOutput', 0) detailed_notes = f"OJO DERECHO: {'Positivo' if has_dr else 'Negativo'} ({confidence:.1f}%)\n{notes}" elif left.get('hasAnalysis'): has_dr = left['diagnosis'] confidence = left['confidence'] raw_output = left.get('rawOutput', 0) detailed_notes = f"OJO IZQUIERDO: {'Positivo' if has_dr else 'Negativo'} ({confidence:.1f}%)\n{notes}" else: return jsonify({'success': False, 'message': 'Sin análisis de imagen'}), 400 cid = db.create_consultation(patient_id, session['user_id'], has_dr, confidence, raw_output, detailed_notes) if cid: return jsonify({'success': True, 'consultationID': cid, 'message': 'Consulta guardada exitosamente'}) return jsonify({'success': False, 'message': 'Error guardando consulta'}), 500 # ------------------------------------------------------------------ # # RUTAS - DASHBOARD # ------------------------------------------------------------------ # @app.route('/api/dashboard/stats', methods=['GET']) @login_required def dashboard_stats(): result = db.get_dashboard_stats(session['user_id'], session['role']) # Add legacy field aliases for frontend compatibility if result.get('success') and result.get('stats'): s = result['stats'] s['total_unique_patients'] = s.get('total_patients', 0) s['patients_with_rd'] = s.get('positive_cases', 0) s['patients_without_rd'] = s.get('negative_cases', 0) s['summary_stats'] = { 'total_consultations': s.get('total_consultations', 0), 'positive_cases': s.get('positive_cases', 0), 'negative_cases': s.get('negative_cases', 0), 'unique_patients': s.get('total_patients', 0), } return jsonify(result) @app.route('/api/model/info', methods=['GET']) @login_required def model_info(): if model is None: return jsonify({'loaded': False, 'error': 'Modelo no cargado'}) return jsonify({ 'loaded': True, 'model_name': 'EfficientNetB0 - Diabetic Retinopathy Classifier', 'input_shape': str(model.input_shape), 'classes': CLASS_NAMES, 'total_params': int(model.count_params()), 'tensorflow_version': tf.__version__ }) # ------------------------------------------------------------------ # # RUTAS - TAREAS (por usuario) # ------------------------------------------------------------------ # @app.route('/api/tasks', methods=['GET']) @login_required def get_tasks(): return jsonify({'success': True, 'tasks': db.get_tasks(session['user_id'])}) @app.route('/api/tasks', methods=['POST']) @login_required def add_task(): text = (request.json.get('text') or '').strip() if not text: return jsonify({'success': False, 'message': 'Texto requerido'}), 400 task = db.add_task(session['user_id'], text) return jsonify({'success': True, 'task': task}) @app.route('/api/tasks//toggle', methods=['POST']) @login_required def toggle_task(task_id): db.toggle_task(task_id, session['user_id']) return jsonify({'success': True}) @app.route('/api/tasks/', methods=['DELETE']) @login_required def delete_task(task_id): db.delete_task(task_id, session['user_id']) return jsonify({'success': True}) # ------------------------------------------------------------------ # # DEBUG — borrar después de confirmar que funciona # ------------------------------------------------------------------ # @app.route('/api/debug', methods=['GET']) def debug(): import sqlite3 try: conn = db.get_connection() users = conn.execute("SELECT userID, username, role FROM Users").fetchall() conn.close() return jsonify({ 'db_path': db.db_path, 'db_exists': os.path.exists(db.db_path), 'users': [dict(u) for u in users], 'model_loaded': model is not None, 'h5_files': [f for f in os.listdir(os.path.dirname(os.path.abspath(__file__))) if f.endswith('.h5')], 'app_dir': os.path.dirname(os.path.abspath(__file__)) }) except Exception as e: return jsonify({'error': str(e), 'db_path': db.db_path}) # ------------------------------------------------------------------ # # ARRANQUE # ------------------------------------------------------------------ # # Cargar modelo al importar el módulo (funciona con gunicorn) print("=== Cargando modelo TensorFlow ===") load_model() print(f"=== Modelo {'CARGADO' if model is not None else 'NO CARGADO'} ===") if __name__ == '__main__': port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port, debug=False)