RetinAI / app.py
Akira-Kurusu's picture
Update app.py
1f23442 verified
Raw
History Blame
29.4 kB
#!/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('/<path:path>')
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/<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'})
# ------------------------------------------------------------------ #
# 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/<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
# 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/<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})
# 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/<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})
# ------------------------------------------------------------------ #
# 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/<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
# ------------------------------------------------------------------ #
# 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/<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})
# ------------------------------------------------------------------ #
# 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)