iagofp commited on
Commit
c205cfa
·
1 Parent(s): 8bb75f6

Fachada DAO, Preparacion Render

Browse files
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend - Dockerfile
2
+ FROM python:3.11-slim
3
+
4
+ WORKDIR /app
5
+
6
+ # Instalar dependencias del sistema
7
+ RUN apt-get update && apt-get install -y \
8
+ gcc \
9
+ g++ \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copiar requirements
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copiar el código del backend
17
+ COPY backend/ ./backend/
18
+ COPY data/ ./data/
19
+
20
+ # Crear directorio para la base de datos
21
+ RUN mkdir -p /app/backend
22
+
23
+ WORKDIR /app/backend
24
+
25
+ # Exponer puerto
26
+ EXPOSE 5000
27
+
28
+ # Variables de entorno por defecto
29
+ ENV FLASK_APP=aplicacion.py
30
+ ENV PYTHONUNBUFFERED=1
31
+
32
+ # Comando para iniciar la app
33
+ CMD ["python", "main.py"]
backend/aplicacion.py CHANGED
@@ -7,11 +7,7 @@ from flask import Flask, jsonify, request
7
  from flask_cors import CORS
8
 
9
  from config import OMDB_API_KEY
10
- from dao.ciclo_dao import CicloDAO
11
- from dao.emocion_dao import EmocionDAO
12
- from dao.historial_dao import HistorialDAO
13
- from dao.pelicula_dao import PeliculaDAO
14
- from dao.usuario_dao import UsuarioDao
15
  from base_datos import iniciar_historial_usuario
16
  from vo import PeliculaVistaVO
17
  from services.pipeline import AnalysisService
@@ -44,11 +40,7 @@ print(
44
 
45
  _analysis_service = AnalysisService(_modelo, _movies_df, _media_rating_global)
46
 
47
- _usuario_dao = UsuarioDao()
48
- _emocion_dao = EmocionDAO()
49
- _ciclo_dao = CicloDAO()
50
- _historial_dao = HistorialDAO()
51
- _pelicula_dao = PeliculaDAO()
52
 
53
 
54
  # ------------------------------------------------------------------
@@ -66,7 +58,7 @@ def register():
66
  return jsonify({"error": "El usuario debe tener al menos 3 caracteres"}), 400
67
  if len(password) < 6:
68
  return jsonify({"error": "La contraseña debe tener al menos 6 caracteres"}), 400
69
- usuario = _usuario_dao.registrar(username, password)
70
  if not usuario:
71
  return jsonify({"error": "El nombre de usuario ya existe"}), 409
72
  return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token}), 201
@@ -78,7 +70,7 @@ def login():
78
  password = str(payload.get("password", "")).strip()
79
  if not username or not password:
80
  return jsonify({"error": "username y password son obligatorios"}), 400
81
- usuario = _usuario_dao.login(username, password)
82
  if not usuario:
83
  return jsonify({"error": "Credenciales incorrectas"}), 401
84
  return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token})
@@ -87,7 +79,7 @@ def login():
87
  def logout():
88
  payload = request.json or {}
89
  token = str(payload.get("token", "")).strip()
90
- _usuario_dao.cerrar_sesion(token)
91
  return jsonify({"ok": True})
92
 
93
  @app.route("/auth/password", methods=["POST"])
@@ -100,8 +92,8 @@ def change_password():
100
  return jsonify({"error": "token, old_password y new_password son obligatorios"}), 400
101
  if len(new_password) < 6:
102
  return jsonify({"error": "La nueva contraseña debe tener al menos 6 caracteres"}), 400
103
- usuario = _usuario_dao.obtener_por_token(token)
104
- if not usuario or not _usuario_dao.actualizar_contraseña(usuario.id, new_password):
105
  return jsonify({"error": "Contraseña actual incorrecta o sesión inválida"}), 401
106
  return jsonify({"ok": True})
107
 
@@ -111,7 +103,7 @@ def verify_token():
111
  token = str(payload.get("token", "")).strip()
112
  if not token:
113
  return jsonify({"valid": False}), 400
114
- usuario = _usuario_dao.obtener_por_token(token)
115
  if not usuario:
116
  return jsonify({"valid": False}), 401
117
  return jsonify({"valid": True, "user_id": usuario.id, "username": usuario.username})
@@ -122,13 +114,13 @@ def delete_account():
122
  token = str(payload.get("token", "")).strip()
123
  if not token:
124
  return jsonify({"error": "token es obligatorio"}), 400
125
- usuario = _usuario_dao.obtener_por_token(token)
126
  if not usuario:
127
  return jsonify({"error": "Token inválido o cuenta no encontrada"}), 401
128
- _historial_dao.borrar_por_usuario(usuario.id)
129
- _emocion_dao.borrar_por_usuario(usuario.id)
130
- _ciclo_dao.borrar_por_usuario(usuario.id)
131
- _usuario_dao.eliminar(usuario.id)
132
  return jsonify({"ok": True, "deleted_user": usuario.username})
133
 
134
 
@@ -166,16 +158,16 @@ def seguimiento_recomendacion():
166
  if not user_id or not cycle_id or not texto_posterior:
167
  return jsonify({"error": "user_id, ciclo_recomendacion_id y texto_post son obligatorios"}), 400
168
 
169
- ciclo = _ciclo_dao.obtener_por_id(cycle_id, user_id)
170
  if not ciclo:
171
  return jsonify({"error": "ciclo de recomendacion no encontrado"}), 404
172
 
173
- emocion_pre = _emocion_dao.obtener_por_id(ciclo.emocion_pre_id)
174
 
175
  momento_analisis = datetime.now(timezone.utc).isoformat()
176
  result_post, emocion_posterior, valencia_posterior = analizar_texto(_modelo, texto_posterior)
177
 
178
- emocion_post_obj = _emocion_dao.añadir(
179
  user_id=user_id,
180
  texto=texto_posterior,
181
  emocion=emocion_posterior,
@@ -184,9 +176,9 @@ def seguimiento_recomendacion():
184
  )
185
 
186
  if id_pelicula:
187
- _pelicula_dao.guardar_si_no_existe(id_pelicula, titulo_pelicula, genero=_genero_pelicula(id_pelicula))
188
  if emocion_post_obj:
189
- _ciclo_dao.cerrar_ciclo(
190
  ciclo_id=cycle_id,
191
  user_id=user_id,
192
  pelicula_id=id_pelicula,
@@ -238,12 +230,12 @@ def guardar_visto():
238
  if rating_usuario < 1 or rating_usuario > 5:
239
  return jsonify({"error": "rating_usuario debe estar entre 1 y 5"}), 400
240
 
241
- _pelicula_dao.guardar_si_no_existe(id_pelicula, titulo_pelicula, genero=_genero_pelicula(id_pelicula))
242
 
243
  emocion_id = None
244
  if emocion_str:
245
  valencia = "positiva" if emocion_str in ("alegria", "sorpresa") else "negativa"
246
- emocion_obj = _emocion_dao.añadir(
247
  user_id=user_id,
248
  texto=texto,
249
  emocion=emocion_str,
@@ -252,7 +244,7 @@ def guardar_visto():
252
  )
253
  emocion_id = emocion_obj.id if emocion_obj else None
254
 
255
- entrada = _historial_dao.añadir_pelicula(
256
  user_id=user_id,
257
  pelicula_id=id_pelicula,
258
  emocion_id=emocion_id,
@@ -280,7 +272,7 @@ def obtener_historial():
280
  user_id = str(payload.get("user_id", "") or request.args.get("user_id", "")).strip()
281
  if not user_id:
282
  return jsonify({"error": "user_id es obligatorio"}), 400
283
- deleted = _historial_dao.borrar_por_usuario(user_id)
284
  return jsonify({"ok": True, "user_id": user_id, "deleted": deleted})
285
 
286
  user_id = str(request.args.get("user_id", "")).strip()
@@ -293,7 +285,7 @@ def obtener_historial():
293
  limit = 30
294
  limit = max(1, min(limit, 200))
295
 
296
- vistas = _historial_dao.obtener_vistas_por_usuario(user_id=user_id, limit=limit)
297
  return jsonify({"items": [dataclasses.asdict(vo) for vo in vistas], "count": len(vistas)})
298
 
299
 
@@ -313,13 +305,13 @@ def obtener_transiciones():
313
  limit = 20
314
  limit = max(1, min(limit, 100))
315
 
316
- emociones = _emocion_dao.obtener_por_usuario(user_id, limit=500)
317
- entradas_h = _historial_dao.obtener_por_usuario(user_id, limit=1000)
318
 
319
  peliculas_map: dict[str, str] = {}
320
  for h in entradas_h:
321
  if h.pelicula_id not in peliculas_map:
322
- peli = _pelicula_dao.obtener_por_id(h.pelicula_id)
323
  peliculas_map[h.pelicula_id] = peli.titulo if peli else ""
324
 
325
  emociones_asc = sorted(emociones, key=lambda e: e.analizado_en)
@@ -428,7 +420,7 @@ def onboarding_historial():
428
 
429
  if not user_id or not token:
430
  return jsonify({"error": "user_id y token son obligatorios"}), 400
431
- if not _usuario_dao.obtener_por_token(token):
432
  return jsonify({"error": "Token inválido"}), 401
433
  if not isinstance(peliculas, list):
434
  return jsonify({"error": "peliculas debe ser una lista"}), 400
@@ -451,8 +443,8 @@ def onboarding_historial():
451
  valoracion = v if 1.0 <= v <= 5.0 else None
452
  except (TypeError, ValueError):
453
  pass
454
- _pelicula_dao.guardar_si_no_existe(movie_id, titulo, genero=genero)
455
- entrada = _historial_dao.añadir_pelicula(
456
  user_id=user_id,
457
  pelicula_id=movie_id,
458
  emocion_id=None,
 
7
  from flask_cors import CORS
8
 
9
  from config import OMDB_API_KEY
10
+ from dao.dao_facade import DAOFacade
 
 
 
 
11
  from base_datos import iniciar_historial_usuario
12
  from vo import PeliculaVistaVO
13
  from services.pipeline import AnalysisService
 
40
 
41
  _analysis_service = AnalysisService(_modelo, _movies_df, _media_rating_global)
42
 
43
+ _dao = DAOFacade.obtener_instancia()
 
 
 
 
44
 
45
 
46
  # ------------------------------------------------------------------
 
58
  return jsonify({"error": "El usuario debe tener al menos 3 caracteres"}), 400
59
  if len(password) < 6:
60
  return jsonify({"error": "La contraseña debe tener al menos 6 caracteres"}), 400
61
+ usuario = _dao.usuario.registrar(username, password)
62
  if not usuario:
63
  return jsonify({"error": "El nombre de usuario ya existe"}), 409
64
  return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token}), 201
 
70
  password = str(payload.get("password", "")).strip()
71
  if not username or not password:
72
  return jsonify({"error": "username y password son obligatorios"}), 400
73
+ usuario = _dao.usuario.login(username, password)
74
  if not usuario:
75
  return jsonify({"error": "Credenciales incorrectas"}), 401
76
  return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token})
 
79
  def logout():
80
  payload = request.json or {}
81
  token = str(payload.get("token", "")).strip()
82
+ _dao.usuario.cerrar_sesion(token)
83
  return jsonify({"ok": True})
84
 
85
  @app.route("/auth/password", methods=["POST"])
 
92
  return jsonify({"error": "token, old_password y new_password son obligatorios"}), 400
93
  if len(new_password) < 6:
94
  return jsonify({"error": "La nueva contraseña debe tener al menos 6 caracteres"}), 400
95
+ usuario = _dao.usuario.obtener_por_token(token)
96
+ if not usuario or not _dao.usuario.actualizar_contraseña(usuario.id, new_password):
97
  return jsonify({"error": "Contraseña actual incorrecta o sesión inválida"}), 401
98
  return jsonify({"ok": True})
99
 
 
103
  token = str(payload.get("token", "")).strip()
104
  if not token:
105
  return jsonify({"valid": False}), 400
106
+ usuario = _dao.usuario.obtener_por_token(token)
107
  if not usuario:
108
  return jsonify({"valid": False}), 401
109
  return jsonify({"valid": True, "user_id": usuario.id, "username": usuario.username})
 
114
  token = str(payload.get("token", "")).strip()
115
  if not token:
116
  return jsonify({"error": "token es obligatorio"}), 400
117
+ usuario = _dao.usuario.obtener_por_token(token)
118
  if not usuario:
119
  return jsonify({"error": "Token inválido o cuenta no encontrada"}), 401
120
+ _dao.historial.borrar_por_usuario(usuario.id)
121
+ _dao.emocion.borrar_por_usuario(usuario.id)
122
+ _dao.ciclo.borrar_por_usuario(usuario.id)
123
+ _dao.usuario.eliminar(usuario.id)
124
  return jsonify({"ok": True, "deleted_user": usuario.username})
125
 
126
 
 
158
  if not user_id or not cycle_id or not texto_posterior:
159
  return jsonify({"error": "user_id, ciclo_recomendacion_id y texto_post son obligatorios"}), 400
160
 
161
+ ciclo = _dao.ciclo.obtener_por_id(cycle_id, user_id)
162
  if not ciclo:
163
  return jsonify({"error": "ciclo de recomendacion no encontrado"}), 404
164
 
165
+ emocion_pre = _dao.emocion.obtener_por_id(ciclo.emocion_pre_id)
166
 
167
  momento_analisis = datetime.now(timezone.utc).isoformat()
168
  result_post, emocion_posterior, valencia_posterior = analizar_texto(_modelo, texto_posterior)
169
 
170
+ emocion_post_obj = _dao.emocion.añadir(
171
  user_id=user_id,
172
  texto=texto_posterior,
173
  emocion=emocion_posterior,
 
176
  )
177
 
178
  if id_pelicula:
179
+ _dao.pelicula.guardar_si_no_existe(id_pelicula, titulo_pelicula, genero=_genero_pelicula(id_pelicula))
180
  if emocion_post_obj:
181
+ _dao.ciclo.cerrar_ciclo(
182
  ciclo_id=cycle_id,
183
  user_id=user_id,
184
  pelicula_id=id_pelicula,
 
230
  if rating_usuario < 1 or rating_usuario > 5:
231
  return jsonify({"error": "rating_usuario debe estar entre 1 y 5"}), 400
232
 
233
+ _dao.pelicula.guardar_si_no_existe(id_pelicula, titulo_pelicula, genero=_genero_pelicula(id_pelicula))
234
 
235
  emocion_id = None
236
  if emocion_str:
237
  valencia = "positiva" if emocion_str in ("alegria", "sorpresa") else "negativa"
238
+ emocion_obj = _dao.emocion.añadir(
239
  user_id=user_id,
240
  texto=texto,
241
  emocion=emocion_str,
 
244
  )
245
  emocion_id = emocion_obj.id if emocion_obj else None
246
 
247
+ entrada = _dao.historial.añadir_pelicula(
248
  user_id=user_id,
249
  pelicula_id=id_pelicula,
250
  emocion_id=emocion_id,
 
272
  user_id = str(payload.get("user_id", "") or request.args.get("user_id", "")).strip()
273
  if not user_id:
274
  return jsonify({"error": "user_id es obligatorio"}), 400
275
+ deleted = _dao.historial.borrar_por_usuario(user_id)
276
  return jsonify({"ok": True, "user_id": user_id, "deleted": deleted})
277
 
278
  user_id = str(request.args.get("user_id", "")).strip()
 
285
  limit = 30
286
  limit = max(1, min(limit, 200))
287
 
288
+ vistas = _dao.historial.obtener_vistas_por_usuario(user_id=user_id, limit=limit)
289
  return jsonify({"items": [dataclasses.asdict(vo) for vo in vistas], "count": len(vistas)})
290
 
291
 
 
305
  limit = 20
306
  limit = max(1, min(limit, 100))
307
 
308
+ emociones = _dao.emocion.obtener_por_usuario(user_id, limit=500)
309
+ entradas_h = _dao.historial.obtener_por_usuario(user_id, limit=1000)
310
 
311
  peliculas_map: dict[str, str] = {}
312
  for h in entradas_h:
313
  if h.pelicula_id not in peliculas_map:
314
+ peli = _dao.pelicula.obtener_por_id(h.pelicula_id)
315
  peliculas_map[h.pelicula_id] = peli.titulo if peli else ""
316
 
317
  emociones_asc = sorted(emociones, key=lambda e: e.analizado_en)
 
420
 
421
  if not user_id or not token:
422
  return jsonify({"error": "user_id y token son obligatorios"}), 400
423
+ if not _dao.usuario.obtener_por_token(token):
424
  return jsonify({"error": "Token inválido"}), 401
425
  if not isinstance(peliculas, list):
426
  return jsonify({"error": "peliculas debe ser una lista"}), 400
 
443
  valoracion = v if 1.0 <= v <= 5.0 else None
444
  except (TypeError, ValueError):
445
  pass
446
+ _dao.pelicula.guardar_si_no_existe(movie_id, titulo, genero=genero)
447
+ entrada = _dao.historial.añadir_pelicula(
448
  user_id=user_id,
449
  pelicula_id=movie_id,
450
  emocion_id=None,
backend/dao/fachada_dao.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from usuario_dao import UsuarioDao
2
+ from pelicula_dao import PeliculaDAO
3
+ from emocion_dao import EmocionDAO
4
+ from ciclo_dao import CicloDAO
5
+ from historial_dao import HistorialDAO
6
+
7
+
8
+ class DAOFacade:
9
+ """Fachada centralizada para acceso a todos los DAO."""
10
+
11
+ _instance = None
12
+
13
+ def __new__(cls):
14
+ if cls._instance is None:
15
+ cls._instance = super().__new__(cls)
16
+ cls._instance._initialized = False
17
+ return cls._instance
18
+
19
+ def __init__(self):
20
+ if self._initialized:
21
+ return
22
+ self.usuario = UsuarioDao()
23
+ self.pelicula = PeliculaDAO()
24
+ self.emocion = EmocionDAO()
25
+ self.ciclo = CicloDAO()
26
+ self.historial = HistorialDAO()
27
+ self._initialized = True
28
+
29
+ @staticmethod
30
+ def obtener_instancia():
31
+ """Obtiene la instancia única de la fachada."""
32
+ return DAOFacade()
backend/main.py CHANGED
@@ -1,9 +1,13 @@
1
  """
2
  Este archivo es el punto de entrada del servidor.
3
- Importa la app Flask singleton y la ejecuta en el puerto 5000.
4
  """
5
 
 
6
  from aplicacion import app
7
 
8
  if __name__ == "__main__":
9
- app.run(port=5000)
 
 
 
 
1
  """
2
  Este archivo es el punto de entrada del servidor.
3
+ Importa la app Flask singleton y la ejecuta en el puerto indicado (por defecto 5000).
4
  """
5
 
6
+ import os
7
  from aplicacion import app
8
 
9
  if __name__ == "__main__":
10
+ port = int(os.getenv("PORT", 5000))
11
+ host = os.getenv("HOST", "0.0.0.0")
12
+ debug = os.getenv("FLASK_ENV", "production") == "development"
13
+ app.run(host=host, port=port, debug=debug)
backend/services/pipeline.py CHANGED
@@ -6,10 +6,7 @@ Desacopla la logica de negocio de las rutas Flask.
6
  from datetime import datetime, timezone
7
 
8
  from config import POSITIVE_EMOTIONS
9
- from dao.ciclo_dao import CicloDAO
10
- from dao.emocion_dao import EmocionDAO
11
- from dao.historial_dao import HistorialDAO
12
- from dao.pelicula_dao import PeliculaDAO
13
  from modelos_servicios import ContextoEmocional, ResultadoAnalisis
14
  from vo import EmocionVO
15
  from services.chatbot import generar_texto_chatbot
@@ -27,23 +24,20 @@ class AnalysisService:
27
  self._modelo = modelo
28
  self._movies_df = movies_df
29
  self._media_global_ratings = media_global_ratings
30
- self._emocion_dao = EmocionDAO()
31
- self._ciclo_dao = CicloDAO()
32
- self._historial_dao = HistorialDAO()
33
- self._pelicula_dao = PeliculaDAO()
34
 
35
  def analizar(self, texto: str, user_id: str, estrategia: str) -> ResultadoAnalisis:
36
  momento_analisis = datetime.now(timezone.utc).isoformat()
37
 
38
  # Emoción previa antes de registrar la actual
39
- emocion_previa_obj = self._emocion_dao.obtener_ultima(user_id)
40
  emocion_previa_vo = EmocionVO.desde(emocion_previa_obj) if emocion_previa_obj else None
41
 
42
  resultado, emocion_dominante, valencia_dominante = analizar_texto(self._modelo, texto)
43
  arousal_actual = calculo_arousal(resultado)
44
  valencia_continua = calcular_valencia_continua(resultado)
45
 
46
- historial_eventos = self._emocion_dao.obtener_por_usuario(user_id=user_id, limit=200)
47
  historico_arousal = [
48
  estimar_arousal_emocion_es(e.emocion)
49
  for e in historial_eventos
@@ -70,7 +64,7 @@ class AnalysisService:
70
  modo_recomendacion = "diferente" if emocion_dominante in POSITIVE_EMOTIONS else "similar"
71
 
72
  # Registrar emoción actual
73
- emocion_actual = self._emocion_dao.añadir(
74
  user_id=user_id,
75
  texto=texto,
76
  emocion=emocion_dominante,
@@ -81,7 +75,7 @@ class AnalysisService:
81
  # Crear ciclo vinculado a la emoción recién registrada
82
  cycle_id = None
83
  if emocion_actual:
84
- ciclo = self._ciclo_dao.crear(
85
  user_id=user_id,
86
  emocion_pre_id=emocion_actual.id,
87
  estrategia=modo_recomendacion,
@@ -92,13 +86,13 @@ class AnalysisService:
92
  # Película vista entre la emoción previa y la actual
93
  pelicula_transicion = None
94
  if emocion_previa_vo:
95
- hp = self._historial_dao.obtener_entre_fechas(
96
  user_id=user_id,
97
  inicio=emocion_previa_vo.analizado_en,
98
  fin=momento_analisis,
99
  )
100
  if hp:
101
- peli = self._pelicula_dao.obtener_por_id(hp[0].pelicula_id)
102
  if peli:
103
  pelicula_transicion = {
104
  "movie_id": peli.id,
 
6
  from datetime import datetime, timezone
7
 
8
  from config import POSITIVE_EMOTIONS
9
+ from dao.dao_facade import DAOFacade
 
 
 
10
  from modelos_servicios import ContextoEmocional, ResultadoAnalisis
11
  from vo import EmocionVO
12
  from services.chatbot import generar_texto_chatbot
 
24
  self._modelo = modelo
25
  self._movies_df = movies_df
26
  self._media_global_ratings = media_global_ratings
27
+ self._dao = DAOFacade.obtener_instancia()
 
 
 
28
 
29
  def analizar(self, texto: str, user_id: str, estrategia: str) -> ResultadoAnalisis:
30
  momento_analisis = datetime.now(timezone.utc).isoformat()
31
 
32
  # Emoción previa antes de registrar la actual
33
+ emocion_previa_obj = self._dao.emocion.obtener_ultima(user_id)
34
  emocion_previa_vo = EmocionVO.desde(emocion_previa_obj) if emocion_previa_obj else None
35
 
36
  resultado, emocion_dominante, valencia_dominante = analizar_texto(self._modelo, texto)
37
  arousal_actual = calculo_arousal(resultado)
38
  valencia_continua = calcular_valencia_continua(resultado)
39
 
40
+ historial_eventos = self._dao.emocion.obtener_por_usuario(user_id=user_id, limit=200)
41
  historico_arousal = [
42
  estimar_arousal_emocion_es(e.emocion)
43
  for e in historial_eventos
 
64
  modo_recomendacion = "diferente" if emocion_dominante in POSITIVE_EMOTIONS else "similar"
65
 
66
  # Registrar emoción actual
67
+ emocion_actual = self._dao.emocion.añadir(
68
  user_id=user_id,
69
  texto=texto,
70
  emocion=emocion_dominante,
 
75
  # Crear ciclo vinculado a la emoción recién registrada
76
  cycle_id = None
77
  if emocion_actual:
78
+ ciclo = self._dao.ciclo.crear(
79
  user_id=user_id,
80
  emocion_pre_id=emocion_actual.id,
81
  estrategia=modo_recomendacion,
 
86
  # Película vista entre la emoción previa y la actual
87
  pelicula_transicion = None
88
  if emocion_previa_vo:
89
+ hp = self._dao.historial.obtener_entre_fechas(
90
  user_id=user_id,
91
  inicio=emocion_previa_vo.analizado_en,
92
  fin=momento_analisis,
93
  )
94
  if hp:
95
+ peli = self._dao.pelicula.obtener_por_id(hp[0].pelicula_id)
96
  if peli:
97
  pelicula_transicion = {
98
  "movie_id": peli.id,
backend/services/recomendacion.py CHANGED
@@ -4,16 +4,16 @@ Combina calidad global (suavizado bayesiano), similitud de generos y preferencia
4
  Adapta la estrategia segun el estado emocional usando el patron Strategy con EstrategiaFactory.
5
  """
6
 
7
- from dao.historial_dao import HistorialDAO
8
  from modelos_servicios import ContextoEmocional
9
  from services.estrategias_recomendacion import EstrategiaFactory
10
  from services.calculos import construir_perfil_usuario, recomendar_calidad_aleatoria
11
 
12
- _historial_dao = HistorialDAO()
13
 
14
 
15
  def obtener_historial_usuario(user_id: str, limit: int = 200) -> list[dict]:
16
- entradas = _historial_dao.obtener_por_usuario(user_id, limit=limit)
17
  return [{"movie_id": h.pelicula_id, "user_rating": h.valoracion} for h in entradas]
18
 
19
  def recomendar_peliculas(
 
4
  Adapta la estrategia segun el estado emocional usando el patron Strategy con EstrategiaFactory.
5
  """
6
 
7
+ from dao.dao_facade import DAOFacade
8
  from modelos_servicios import ContextoEmocional
9
  from services.estrategias_recomendacion import EstrategiaFactory
10
  from services.calculos import construir_perfil_usuario, recomendar_calidad_aleatoria
11
 
12
+ _dao = DAOFacade.obtener_instancia()
13
 
14
 
15
  def obtener_historial_usuario(user_id: str, limit: int = 200) -> list[dict]:
16
+ entradas = _dao.historial.obtener_por_usuario(user_id, limit=limit)
17
  return [{"movie_id": h.pelicula_id, "user_rating": h.valoracion} for h in entradas]
18
 
19
  def recomendar_peliculas(
chatbot/.env.production ADDED
@@ -0,0 +1 @@
 
 
1
+ VITE_API_BASE_URL=https://tu-api-render-aqui.onrender.com
chatbot/src/components/AppHero.vue CHANGED
@@ -90,6 +90,7 @@
90
  import { computed, ref } from "vue";
91
  import { useRouter } from "vue-router";
92
  import { useTheme } from "vuetify";
 
93
 
94
  defineProps({
95
  username: { type: String, default: "" },
@@ -111,7 +112,7 @@ function toggleTheme() {
111
  async function logout() {
112
  const token = localStorage.getItem("vs_token") || "";
113
  try {
114
- await fetch("http://localhost:5000/auth/logout", {
115
  method: "POST",
116
  headers: { "Content-Type": "application/json" },
117
  body: JSON.stringify({ token }),
@@ -125,7 +126,7 @@ async function confirmDelete() {
125
  deleteLoading.value = true;
126
  const token = localStorage.getItem("vs_token") || "";
127
  try {
128
- await fetch("http://localhost:5000/auth/account", {
129
  method: "DELETE",
130
  headers: { "Content-Type": "application/json" },
131
  body: JSON.stringify({ token }),
 
90
  import { computed, ref } from "vue";
91
  import { useRouter } from "vue-router";
92
  import { useTheme } from "vuetify";
93
+ import API_BASE_URL from "../config.js";
94
 
95
  defineProps({
96
  username: { type: String, default: "" },
 
112
  async function logout() {
113
  const token = localStorage.getItem("vs_token") || "";
114
  try {
115
+ await fetch(`${API_BASE_URL}/auth/logout`, {
116
  method: "POST",
117
  headers: { "Content-Type": "application/json" },
118
  body: JSON.stringify({ token }),
 
126
  deleteLoading.value = true;
127
  const token = localStorage.getItem("vs_token") || "";
128
  try {
129
+ await fetch(`${API_BASE_URL}/auth/account`, {
130
  method: "DELETE",
131
  headers: { "Content-Type": "application/json" },
132
  body: JSON.stringify({ token }),
chatbot/src/components/MessageFeed.vue CHANGED
@@ -215,6 +215,7 @@ import { computed, nextTick, reactive, ref, watch } from "vue";
215
  import { useTheme } from "vuetify";
216
  import { dominantEmotionInfo, emotionInfo } from "../constants/emotions";
217
  import MessageComposer from "./MessageComposer.vue";
 
218
 
219
  const props = defineProps({
220
  messages: { type: Array, default: () => [] },
@@ -236,7 +237,7 @@ async function loadPoster(imdbId) {
236
  if (!imdbId || key in posters) return;
237
  posters[key] = null;
238
  try {
239
- const res = await fetch(`http://localhost:5000/poster/${key}`);
240
  const data = await res.json();
241
  posters[key] = data.poster_url || null;
242
  } catch { /* keep null */ }
 
215
  import { useTheme } from "vuetify";
216
  import { dominantEmotionInfo, emotionInfo } from "../constants/emotions";
217
  import MessageComposer from "./MessageComposer.vue";
218
+ import API_BASE_URL from "../config.js";
219
 
220
  const props = defineProps({
221
  messages: { type: Array, default: () => [] },
 
237
  if (!imdbId || key in posters) return;
238
  posters[key] = null;
239
  try {
240
+ const res = await fetch(`${API_BASE_URL}/poster/${key}`);
241
  const data = await res.json();
242
  posters[key] = data.poster_url || null;
243
  } catch { /* keep null */ }
chatbot/src/config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ // API Base URL configuration
2
+ // Uses environment variable VITE_API_BASE_URL if available
3
+ // Falls back to localhost:5000 for local development
4
+
5
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:5000";
6
+
7
+ export default API_BASE_URL;
chatbot/src/views/AuthView.vue CHANGED
@@ -231,6 +231,7 @@
231
  <script setup>
232
  import { ref, onMounted } from "vue";
233
  import { useRouter } from "vue-router";
 
234
 
235
  const router = useRouter();
236
  const activeTab = ref("login");
@@ -268,7 +269,7 @@ async function submitLogin() {
268
  }
269
  loginLoading.value = true;
270
  try {
271
- const res = await fetch("http://localhost:5000/auth/login", {
272
  method: "POST",
273
  headers: { "Content-Type": "application/json" },
274
  body: JSON.stringify({ username: loginForm.value.username, password: loginForm.value.password }),
@@ -302,7 +303,7 @@ async function submitRegister() {
302
  }
303
  registerLoading.value = true;
304
  try {
305
- const res = await fetch("http://localhost:5000/auth/register", {
306
  method: "POST",
307
  headers: { "Content-Type": "application/json" },
308
  body: JSON.stringify({ username: registerForm.value.username, password: registerForm.value.password }),
@@ -326,7 +327,7 @@ async function submitRegister() {
326
 
327
  async function cargarPopulares() {
328
  try {
329
- const res = await fetch("http://localhost:5000/peliculas/populares?limit=20");
330
  if (res.ok) {
331
  const data = await res.json();
332
  popularMovies.value = data.items || [];
@@ -345,7 +346,7 @@ function onSearchInput() {
345
  searchTimer = setTimeout(async () => {
346
  try {
347
  const q = encodeURIComponent(searchQuery.value.trim());
348
- const res = await fetch(`http://localhost:5000/peliculas/buscar?q=${q}&limit=15`);
349
  if (res.ok) {
350
  const data = await res.json();
351
  searchResults.value = data.items || [];
@@ -389,7 +390,7 @@ async function finalizarOnboarding() {
389
  }
390
  onboardingLoading.value = true;
391
  try {
392
- await fetch("http://localhost:5000/onboarding/historial", {
393
  method: "POST",
394
  headers: { "Content-Type": "application/json" },
395
  body: JSON.stringify({
 
231
  <script setup>
232
  import { ref, onMounted } from "vue";
233
  import { useRouter } from "vue-router";
234
+ import API_BASE_URL from "../config.js";
235
 
236
  const router = useRouter();
237
  const activeTab = ref("login");
 
269
  }
270
  loginLoading.value = true;
271
  try {
272
+ const res = await fetch(`${API_BASE_URL}/auth/login`, {
273
  method: "POST",
274
  headers: { "Content-Type": "application/json" },
275
  body: JSON.stringify({ username: loginForm.value.username, password: loginForm.value.password }),
 
303
  }
304
  registerLoading.value = true;
305
  try {
306
+ const res = await fetch(`${API_BASE_URL}/auth/register`, {
307
  method: "POST",
308
  headers: { "Content-Type": "application/json" },
309
  body: JSON.stringify({ username: registerForm.value.username, password: registerForm.value.password }),
 
327
 
328
  async function cargarPopulares() {
329
  try {
330
+ const res = await fetch(`${API_BASE_URL}/peliculas/populares?limit=20`);
331
  if (res.ok) {
332
  const data = await res.json();
333
  popularMovies.value = data.items || [];
 
346
  searchTimer = setTimeout(async () => {
347
  try {
348
  const q = encodeURIComponent(searchQuery.value.trim());
349
+ const res = await fetch(`${API_BASE_URL}/peliculas/buscar?q=${q}&limit=15`);
350
  if (res.ok) {
351
  const data = await res.json();
352
  searchResults.value = data.items || [];
 
390
  }
391
  onboardingLoading.value = true;
392
  try {
393
+ await fetch(`${API_BASE_URL}/onboarding/historial`, {
394
  method: "POST",
395
  headers: { "Content-Type": "application/json" },
396
  body: JSON.stringify({
chatbot/src/views/ChatView.vue CHANGED
@@ -220,6 +220,7 @@ import { computed, onMounted, ref, watch } from "vue";
220
  import AppHero from "../components/AppHero.vue";
221
  import MessageFeed from "../components/MessageFeed.vue";
222
  import { emotionInfoBySpanish } from "../constants/emotions";
 
223
 
224
  const messages = ref([]);
225
  const input = ref("");
@@ -255,7 +256,7 @@ onMounted(async () => {
255
 
256
  if (storedToken) {
257
  try {
258
- const res = await fetch("http://localhost:5000/auth/verify", {
259
  method: "POST",
260
  headers: { "Content-Type": "application/json" },
261
  body: JSON.stringify({ token: storedToken }),
@@ -281,7 +282,7 @@ watch(estrategia, v => localStorage.setItem("vs_estrategia", v));
281
  watch(userId, async id => {
282
  if (!id) return;
283
  try {
284
- const res = await fetch(`http://localhost:5000/historial?user_id=${encodeURIComponent(id)}&limit=20`);
285
  const data = await res.json();
286
  history.value = data.items || [];
287
  } catch { history.value = []; }
@@ -294,7 +295,7 @@ async function analyze() {
294
  input.value = "";
295
  loading.value = true;
296
  try {
297
- const res = await fetch("http://localhost:5000/analizar", {
298
  method: "POST",
299
  headers: { "Content-Type": "application/json" },
300
  body: JSON.stringify({ texto: text, user_id: userId.value, estrategia: estrategia.value }),
@@ -342,7 +343,7 @@ async function submitRating() {
342
  ratingDialog.value.open = false;
343
  pendingViewOp.value = null;
344
  try {
345
- const res = await fetch("http://localhost:5000/historial/visto", {
346
  method: "POST",
347
  headers: { "Content-Type": "application/json" },
348
  body: JSON.stringify({
@@ -375,7 +376,7 @@ async function submitPost() {
375
  postDialog.value.open = false;
376
  if (!text.trim() || !cycleId) return;
377
  try {
378
- const res = await fetch("http://localhost:5000/recomendacion/seguimiento", {
379
  method: "POST",
380
  headers: { "Content-Type": "application/json" },
381
  body: JSON.stringify({
@@ -396,7 +397,7 @@ async function submitPost() {
396
  async function confirmClearHistory() {
397
  clearHistoryLoading.value = true;
398
  try {
399
- const res = await fetch("http://localhost:5000/historial", {
400
  method: "DELETE",
401
  headers: { "Content-Type": "application/json" },
402
  body: JSON.stringify({ user_id: userId.value }),
 
220
  import AppHero from "../components/AppHero.vue";
221
  import MessageFeed from "../components/MessageFeed.vue";
222
  import { emotionInfoBySpanish } from "../constants/emotions";
223
+ import API_BASE_URL from "../config.js";
224
 
225
  const messages = ref([]);
226
  const input = ref("");
 
256
 
257
  if (storedToken) {
258
  try {
259
+ const res = await fetch(`${API_BASE_URL}/auth/verify`, {
260
  method: "POST",
261
  headers: { "Content-Type": "application/json" },
262
  body: JSON.stringify({ token: storedToken }),
 
282
  watch(userId, async id => {
283
  if (!id) return;
284
  try {
285
+ const res = await fetch(`${API_BASE_URL}/historial?user_id=${encodeURIComponent(id)}&limit=20`);
286
  const data = await res.json();
287
  history.value = data.items || [];
288
  } catch { history.value = []; }
 
295
  input.value = "";
296
  loading.value = true;
297
  try {
298
+ const res = await fetch(`${API_BASE_URL}/analizar`, {
299
  method: "POST",
300
  headers: { "Content-Type": "application/json" },
301
  body: JSON.stringify({ texto: text, user_id: userId.value, estrategia: estrategia.value }),
 
343
  ratingDialog.value.open = false;
344
  pendingViewOp.value = null;
345
  try {
346
+ const res = await fetch(`${API_BASE_URL}/historial/visto`, {
347
  method: "POST",
348
  headers: { "Content-Type": "application/json" },
349
  body: JSON.stringify({
 
376
  postDialog.value.open = false;
377
  if (!text.trim() || !cycleId) return;
378
  try {
379
+ const res = await fetch(`${API_BASE_URL}/recomendacion/seguimiento`, {
380
  method: "POST",
381
  headers: { "Content-Type": "application/json" },
382
  body: JSON.stringify({
 
397
  async function confirmClearHistory() {
398
  clearHistoryLoading.value = true;
399
  try {
400
+ const res = await fetch(`${API_BASE_URL}/historial`, {
401
  method: "DELETE",
402
  headers: { "Content-Type": "application/json" },
403
  body: JSON.stringify({ user_id: userId.value }),
chatbot/src/views/PreferencesView.vue CHANGED
@@ -102,6 +102,7 @@
102
  import { computed, ref } from "vue";
103
  import { useRouter } from "vue-router";
104
  import { useTheme } from "vuetify";
 
105
 
106
  const router = useRouter();
107
  const theme = useTheme();
@@ -134,7 +135,7 @@ async function changePassword() {
134
  }
135
  pwdLoading.value = true;
136
  try {
137
- const res = await fetch("http://localhost:5000/auth/password", {
138
  method: "POST",
139
  headers: { "Content-Type": "application/json" },
140
  body: JSON.stringify({
 
102
  import { computed, ref } from "vue";
103
  import { useRouter } from "vue-router";
104
  import { useTheme } from "vuetify";
105
+ import API_BASE_URL from "../config.js";
106
 
107
  const router = useRouter();
108
  const theme = useTheme();
 
135
  }
136
  pwdLoading.value = true;
137
  try {
138
+ const res = await fetch(`${API_BASE_URL}/auth/password`, {
139
  method: "POST",
140
  headers: { "Content-Type": "application/json" },
141
  body: JSON.stringify({
chatbot/vite.config.js CHANGED
@@ -9,4 +9,12 @@ export default defineConfig({
9
  }),
10
  vuetify({ autoImport: true }),
11
  ],
 
 
 
 
 
 
 
 
12
  });
 
9
  }),
10
  vuetify({ autoImport: true }),
11
  ],
12
+ server: {
13
+ port: 5173,
14
+ host: '0.0.0.0',
15
+ },
16
+ preview: {
17
+ port: 4173,
18
+ host: '0.0.0.0',
19
+ },
20
  });
docs/singleton_bd.md CHANGED
@@ -7,6 +7,4 @@ classDiagram
7
  +instancia() ConexionBD
8
  +obtener_conexion() Connection
9
  }
10
-
11
- note for ConexionBD "Singleton clásico que garantiza una única instancia de conexión a la base de datos SQLite"
12
  ```
 
7
  +instancia() ConexionBD
8
  +obtener_conexion() Connection
9
  }
 
 
10
  ```
render.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: valor-sentimental-backend
4
+ runtime: python
5
+ plan: standard
6
+ buildCommand: pip install -r requirements.txt
7
+ startCommand: cd backend && python main.py
8
+ envVars:
9
+ - key: PORT
10
+ value: 5000
11
+ - key: FLASK_ENV
12
+ value: production
13
+ - key: HF_TOKEN
14
+ scope: secret
15
+ - key: OMDB_API_KEY
16
+ scope: secret
17
+
18
+ - type: web
19
+ name: valor-sentimental-frontend
20
+ runtime: node
21
+ plan: standard
22
+ buildCommand: cd chatbot && npm install && npm run build
23
+ startCommand: cd chatbot && npm run preview
24
+ envVars:
25
+ - key: VITE_API_BASE_URL
26
+ value: https://valor-sentimental-backend.onrender.com