| |
| """ |
| 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 |
|
|
| |
| |
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| @app.route('/') |
| def index(): |
| return send_from_directory('web', 'auth-login.html') |
|
|
| @app.route('/<path:path>') |
| def static_files(path): |
| return send_from_directory('web', path) |
|
|
|
|
| |
| |
| |
| @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'] |
| } |
| }) |
|
|
|
|
| |
| |
| |
| @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/<int:user_id>', 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/<int:user_id>', 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'}) |
|
|
|
|
| |
| |
| |
| @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/<int:patient_id>', 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 |
| |
| 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/<int:patient_id>', 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/<int:patient_id>', 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}) |
|
|
| |
| @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/<int:patient_id>/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/<int:patient_id>/risk-factors/<int:rf_id>', 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}) |
|
|
|
|
| |
| |
| |
| @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 |
|
|
|
|
| |
| |
| |
| @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/<int:consultation_id>', 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/<int:consultation_id>', 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 |
|
|
|
|
| |
| |
| |
| @app.route('/api/dashboard/stats', methods=['GET']) |
| @login_required |
| def dashboard_stats(): |
| result = db.get_dashboard_stats(session['user_id'], session['role']) |
| |
| 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__ |
| }) |
|
|
|
|
| |
| |
| |
| @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/<int:task_id>/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/<int:task_id>', methods=['DELETE']) |
| @login_required |
| def delete_task(task_id): |
| db.delete_task(task_id, session['user_id']) |
| return jsonify({'success': True}) |
|
|
|
|
|
|
| |
| |
| |
| @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}) |
|
|
|
|
| |
| |
| |
|
|
|
|
| |
| 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) |